chore: update submodules + docker-compose fixes + new plan docs
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
# Node Edit Fixes
|
||||||
|
|
||||||
|
## TL;DR
|
||||||
|
Fix `|| undefined` bug in Servers.vue (preup/postdown/peer defaults can't be cleared) + add device rename field.
|
||||||
|
|
||||||
|
## Status
|
||||||
|
- [x] DeviceDetail.vue rename field — DONE (commit `4abd9f4`)
|
||||||
|
- [x] Servers.vue clear field fix — DONE (commit `715ac07` → main `57082cc`)
|
||||||
|
- [x] Push + rebuild production — DONE (all 3 submodules + root pushed)
|
||||||
|
|
||||||
|
## Final Verification
|
||||||
|
- [x] `npm run build` passes
|
||||||
|
- [x] Device rename field visible in DeviceDetail.vue
|
||||||
|
- [x] `git push` all submodules + root
|
||||||
|
- [x] Deployed to production
|
||||||
@@ -1,388 +1,15 @@
|
|||||||
# Post-Deploy Fixes + Config Delivery Plan
|
# Post-Deploy Fixes
|
||||||
|
|
||||||
## TL;DR
|
## TL;DR
|
||||||
|
Fix DNS clear, peer debug, IP change, port range issues after initial deployment.
|
||||||
> **Quick Summary**: Fix 3 immediate bugs (DNS clear, peer detection debug, peer IP change) and design secure config delivery for devices.
|
|
||||||
>
|
## Status
|
||||||
> **Deliverables**:
|
- [x] T1: DNS clear fix (DeviceDetail.vue) — DONE (commit `c1150b3`)
|
||||||
> - [x] DNS override can be cleared to empty (frontend fix)
|
- [x] T2: Debug peer discovery guide — DONE (plan)
|
||||||
> - [x] Debug procedure documented for peer detection failures (informational)
|
- [x] T3: IP change validation (servers.go) — DONE (commit `5e60741`)
|
||||||
> - [x] InternalIP editable from DeviceDetail.vue
|
- [x] T4: Port range nullable (models.go + rules.go + FirewallEditor.vue) — DONE (commit `ffb00e2` + `c003a09`)
|
||||||
> - [x] Firewall port range can be empty (apply to all ports)
|
|
||||||
> - [ ] Encrypted config delivery endpoint + companion plaintext endpoint
|
## Final Verification
|
||||||
>
|
- [x] `go build -tags dev ./...` — PASS
|
||||||
> **Estimated Effort**: Medium
|
- [x] `npm run build` — PASS
|
||||||
> **Parallel Execution**: YES - 2 waves
|
- [x] Deployed to production
|
||||||
> **Critical Path**: T1 → [T2] → T3
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
### Original Issues
|
|
||||||
|
|
||||||
1. **DNS tidak bisa dikosongkan**: Ketika user kosongkan field DNS Override di DeviceDetail.vue lalu save, `advForm.value.dns` jadi `""`, tapi `"" || undefined` di JavaScript menghasilkan `undefined`. Akibatnya field `dns` tidak terkirim ke API, nilai DB tidak berubah.
|
|
||||||
|
|
||||||
2. **Peer tidak mendeteksi koneksi**: Device official WG client tetap Offline meski terkoneksi. Perlu debugging multi-layer: (1) cek collector jalan, (2) cek kernel handshake, (3) cek frontend isDeviceOnline().
|
|
||||||
|
|
||||||
3. **Tidak bisa ganti peer IP**: `UpdateDeviceRequest` tidak memiliki field `InternalIP`. IP hanya dialokasikan sekali oleh IPAM saat provisioning.
|
|
||||||
|
|
||||||
4. **Config delivery**: Ingin endpoint aman untuk device ambil config, tapi juga bisa debug plaintext untuk WireGuard client manual.
|
|
||||||
|
|
||||||
### Current State
|
|
||||||
|
|
||||||
- **DNS backend**: `devices.go:236-238` — sudah handle `req.DNS != nil`, termasuk empty string ✅
|
|
||||||
- **DNS frontend**: `DeviceDetail.vue:300` — `dns: advForm.value.dns || undefined` ❌ bug
|
|
||||||
- **Peer IP**: `UpdateDeviceRequest` di `devices.go:190` — TIDAK ada field `InternalIP`
|
|
||||||
- **Config endpoints**:
|
|
||||||
- `GET /devices/:id/config` → plaintext (admin only)
|
|
||||||
- `POST /provision` → `encrypted_config` AES-256-GCM (token-based)
|
|
||||||
- `GET /devices/:id/config/qr` → QR image (admin only)
|
|
||||||
- **Tidak ada endpoint publik untuk device mengambil config sendiri**
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Work Objectives
|
|
||||||
|
|
||||||
### Objective A: Fix Bugs (Issue 1)
|
|
||||||
|
|
||||||
**Must Have**:
|
|
||||||
- [x] DNS override bisa dikosongkan dan tersimpan sebagai `""` di DB
|
|
||||||
- [x] `npm run build` passes
|
|
||||||
|
|
||||||
**Must NOT Have**:
|
|
||||||
- [x] Tidak mengubah struktur API atau backend
|
|
||||||
|
|
||||||
### Objective B: Debug Guide (Issue 2)
|
|
||||||
|
|
||||||
**Must Have**:
|
|
||||||
- [ ] Step-by-step debugging procedure untuk peer detection
|
|
||||||
- [ ] Commands untuk tiap layer (kernel, collector, DB, frontend)
|
|
||||||
|
|
||||||
### Objective C: Peer IP Change (Issue 3)
|
|
||||||
|
|
||||||
**Must Have**:
|
|
||||||
- [ ] Admin bisa mengubah `internal_ip` device via API
|
|
||||||
- [ ] API memvalidasi format IP sebelum update
|
|
||||||
- [ ] API mengecek duplikasi IP di IPAM sebelum update
|
|
||||||
- [ ] Firewall rule re-sync jika IP berubah
|
|
||||||
- [ ] Frontend input field untuk InternalIP di DeviceDetail.vue
|
|
||||||
|
|
||||||
**Must NOT Have**:
|
|
||||||
- [ ] Jangan override alokasi IPAM untuk device yang belum provisioning
|
|
||||||
- [ ] Jangan izinkan non-admin mengganti IP
|
|
||||||
|
|
||||||
### Objective D: Config Delivery (Issue 4)
|
|
||||||
|
|
||||||
**Questions to resolve** (akan ditanyakan ke user):
|
|
||||||
- Apakah device perlu endpoint autentikasi sendiri (token/API key) untuk ambil config?
|
|
||||||
- Atau cukup admin ambilkan seperti sekarang?
|
|
||||||
- Encrypted payload (AES-GCM) atau cukup HTTPS saja?
|
|
||||||
- Perlu QR code juga atau config text saja?
|
|
||||||
|
|
||||||
**Must Have** (asumsi awal):
|
|
||||||
- [ ] Endpoint untuk device mengambil config sendiri (setelah provisioning)
|
|
||||||
- [ ] Opsi plaintext untuk debugging
|
|
||||||
- [ ] Opsi encrypted payload untuk production
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Verification Strategy
|
|
||||||
|
|
||||||
> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed.
|
|
||||||
|
|
||||||
- **Frontend**: `npm run build` + Playwright (cek DNS save + IP input)
|
|
||||||
- **Backend**: `go build -tags dev ./...` + curl API test
|
|
||||||
- **Config**: curl endpoint + validasi response format
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Execution Strategy
|
|
||||||
|
|
||||||
```
|
|
||||||
Wave 1 (Parallel — all remaining work):
|
|
||||||
├── T1: Fix DNS clear bug (frontend) [DONE]
|
|
||||||
├── T2: Write debugging docs [INFORMATIONAL]
|
|
||||||
├── T3: Add InternalIP change to backend + frontend [DONE]
|
|
||||||
├── T4: Fix firewall port range to allow empty (models + API + frontend + nftables) [DONE]
|
|
||||||
└── F1-F3: Build verification + code review
|
|
||||||
|
|
||||||
Wave 2 (Future — separate plan):
|
|
||||||
├── Device-Agent Registration + Approval flow
|
|
||||||
└── Encrypted config delivery endpoint
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## TODOs
|
|
||||||
|
|
||||||
- [x] 1. Fix DNS clear bug
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
- File: `apps/dashboard-ui/src/views/DeviceDetail.vue` baris 300
|
|
||||||
- Ubah `dns: advForm.value.dns || undefined` menjadi `dns: advForm.value.dns === '' ? '' : advForm.value.dns || undefined`
|
|
||||||
- Run `npm run build` untuk verifikasi
|
|
||||||
|
|
||||||
**Must NOT do**:
|
|
||||||
- Jangan ubah backend — sudah support empty string
|
|
||||||
|
|
||||||
**Parallelization**: Wave 1, parallel with T2, T3
|
|
||||||
|
|
||||||
**Acceptance Criteria**:
|
|
||||||
- [ ] `npm run build` passes
|
|
||||||
- [ ] Set DNS → kosongkan → save → reload → DNS tetap kosong
|
|
||||||
|
|
||||||
**QA Scenarios**:
|
|
||||||
```
|
|
||||||
Scenario: Clear DNS override saves correctly
|
|
||||||
Tool: Bash (curl)
|
|
||||||
Preconditions: Device with existing DNS
|
|
||||||
Steps:
|
|
||||||
1. curl -X PUT /api/v1/devices/$ID -H "Content-Type: application/json" -d '{"dns": ""}' -H "Authorization: Bearer $TOKEN"
|
|
||||||
2. curl -X GET /api/v1/devices/$ID -H "Authorization: Bearer $TOKEN"
|
|
||||||
Expected Result: Response JSON includes "dns": "" or null
|
|
||||||
Evidence: .sisyphus/evidence/postdeploy-t1-dns-clear.txt
|
|
||||||
|
|
||||||
Scenario: Build passes
|
|
||||||
Tool: Bash
|
|
||||||
Steps: cd apps/dashboard-ui && npm run build
|
|
||||||
Expected Result: vue-tsc + vite build succeeds
|
|
||||||
Evidence: .sisyphus/evidence/postdeploy-t1-build.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Commit**: YES — `fix(ui): allow clearing DNS override to empty`
|
|
||||||
|
|
||||||
- [x] 2. Document peer detection debugging procedure (informational)
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
Create debugging doc covering 5 layers:
|
|
||||||
1. Kernel: `sudo wg show` — peer handshake dari kernel
|
|
||||||
2. Collector: `docker logs` grep handshake — apakah collector jalan
|
|
||||||
3. Node type: query DB `wg_servers.name` — local node atau remote
|
|
||||||
4. DB data: `psql` cek `is_active`, `last_handshake`, `client_info`
|
|
||||||
5. Status API: `curl /devices/:id/status` — realtime status dari API
|
|
||||||
|
|
||||||
**Parallelization**: Wave 1, parallel
|
|
||||||
|
|
||||||
**Acceptance Criteria**:
|
|
||||||
- [ ] Setiap langkah punya command exact yang bisa di-copy-paste
|
|
||||||
- [ ] Setiap langkah punya expected output
|
|
||||||
|
|
||||||
**Commit**: NO (informational)
|
|
||||||
|
|
||||||
- [x] 3. Add InternalIP to UpdateDeviceRequest + handler + UI
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
|
|
||||||
**Backend** — `apps/server-core/api/devices.go`:
|
|
||||||
- Tambah `InternalIP *string json:"internal_ip"` ke `UpdateDeviceRequest` struct
|
|
||||||
- Di Update handler, setelah blok DNS (line ~238), tambah:
|
|
||||||
```go
|
|
||||||
if req.InternalIP != nil {
|
|
||||||
ip := net.ParseIP(*req.InternalIP)
|
|
||||||
if ip == nil {
|
|
||||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid internal_ip format"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// Check duplicate IP across all devices + servers
|
|
||||||
var count int64
|
|
||||||
h.db.Model(&models.Device{}).Where("internal_ip = ? AND id != ?", *req.InternalIP, id).Count(&count)
|
|
||||||
if count > 0 {
|
|
||||||
c.JSON(http.StatusConflict, gin.H{"error": "internal_ip already in use"})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
updates["internal_ip"] = *req.InternalIP
|
|
||||||
// Re-sync firewall if endpoint_allowed_ips also changed or existed
|
|
||||||
if req.EndpointAllowedIPs != nil && *req.EndpointAllowedIPs != "" {
|
|
||||||
destCIDR := *req.EndpointAllowedIPs
|
|
||||||
// Remove old rule, add new rule
|
|
||||||
h.fw.AddForwardRule(device.Name, ip, destCIDR)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**Frontend** — `apps/dashboard-ui/src/views/DeviceDetail.vue`:
|
|
||||||
- Di advanced settings section, tambah input field:
|
|
||||||
```html
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs text-gray-500 mb-1">Internal IP (WireGuard)</label>
|
|
||||||
<input v-model="advForm.internalIP" placeholder="10.172.21.x" class="w-full bg-black/50 border border-white/10 rounded p-2 text-white text-sm focus:border-cyan-500 focus:outline-none" />
|
|
||||||
</div>
|
|
||||||
```
|
|
||||||
- Di `advForm` ref, tambah `internalIP: ''`
|
|
||||||
- Di `load()`, tambah `advForm.value.internalIP = device.value.InternalIP || ''`
|
|
||||||
- Di `saveAdvanced()`, tambah `internal_ip: advForm.value.internalIP || undefined`
|
|
||||||
|
|
||||||
**Must NOT do**:
|
|
||||||
- Jangan izinkan non-admin mengganti IP
|
|
||||||
- Jangan override IP device yang belum provisioning (`!device.InternalIP == nil`)
|
|
||||||
- Jangan lupa re-sync firewall rule
|
|
||||||
|
|
||||||
**Parallelization**: Wave 1
|
|
||||||
|
|
||||||
**Acceptance Criteria**:
|
|
||||||
- [ ] `go build -tags dev ./...` passes
|
|
||||||
- [ ] `npm run build` passes
|
|
||||||
- [ ] curl PUT dengan internal_ip → 200, IP berubah
|
|
||||||
- [ ] curl PUT dengan IP duplikat → 409 Conflict
|
|
||||||
- [ ] curl PUT dengan IP invalid → 400 Bad Request
|
|
||||||
- [ ] IP baru muncul di Devices table + DeviceDetail
|
|
||||||
|
|
||||||
**QA Scenarios**:
|
|
||||||
```
|
|
||||||
Scenario: Change IP successfully
|
|
||||||
Tool: Bash (curl)
|
|
||||||
Preconditions: Existing device with internal_ip "10.172.21.5"
|
|
||||||
Steps:
|
|
||||||
1. curl -X PUT /devices/$ID -d '{"internal_ip":"10.172.21.100"}' -H "Authorization: Bearer $ADMIN_TOKEN"
|
|
||||||
2. curl -X GET /devices/$ID -H "Authorization: Bearer $ADMIN_TOKEN"
|
|
||||||
Expected Result: internal_ip = "10.172.21.100"
|
|
||||||
Evidence: .sisyphus/evidence/postdeploy-t3-ip-change.txt
|
|
||||||
|
|
||||||
Scenario: Duplicate IP rejected
|
|
||||||
Tool: Bash (curl)
|
|
||||||
Preconditions: Another device already has 10.172.21.5
|
|
||||||
Steps:
|
|
||||||
1. curl -X PUT /devices/$ID -d '{"internal_ip":"10.172.21.5"}' -H "Authorization: Bearer $ADMIN_TOKEN"
|
|
||||||
Expected Result: HTTP 409, error "internal_ip already in use"
|
|
||||||
Evidence: .sisyphus/evidence/postdeploy-t3-ip-duplicate.txt
|
|
||||||
|
|
||||||
Scenario: Invalid IP rejected
|
|
||||||
Tool: Bash (curl)
|
|
||||||
Steps:
|
|
||||||
1. curl -X PUT /devices/$ID -d '{"internal_ip":"not-an-ip"}' -H "Authorization: Bearer $ADMIN_TOKEN"
|
|
||||||
Expected Result: HTTP 400, error "invalid internal_ip format"
|
|
||||||
Evidence: .sisyphus/evidence/postdeploy-t3-ip-invalid.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
**Commit**: YES
|
|
||||||
- Message: `feat(api): add internal_ip update to device endpoint`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### (Tambahan) T4: Fix port range firewall agar bisa kosong
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
- **models.go** (`apps/server-core/internal/models/models.go:100`):
|
|
||||||
Ubah `DestPortRange string \`gorm:"not null;size:32"\`` menjadi:
|
|
||||||
```go
|
|
||||||
DestPortRange string `gorm:"size:32;default:''"`
|
|
||||||
```
|
|
||||||
- **rules.go** (`apps/server-core/api/rules.go:44`):
|
|
||||||
Ubah `DestPortRange string \`json:"dest_port_range" binding:"required"\`` menjadi:
|
|
||||||
```go
|
|
||||||
DestPortRange string `json:"dest_port_range"`
|
|
||||||
```
|
|
||||||
- **rules.go** (handler `Create` di `rules.go`):
|
|
||||||
Tambah default empty string jika kosong:
|
|
||||||
```go
|
|
||||||
if req.DestPortRange == "" {
|
|
||||||
req.DestPortRange = "" // "all ports"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **rules.go** (nftables sync `rules.go:126-134`):
|
|
||||||
Jika `DestPortRange` kosong, jangan generate dport match di nftables:
|
|
||||||
```go
|
|
||||||
if rule.DestPortRange != "" {
|
|
||||||
// parse port range dan apply
|
|
||||||
} else {
|
|
||||||
// rule tanpa port filter — berlaku untuk semua port
|
|
||||||
}
|
|
||||||
```
|
|
||||||
- **FirewallEditor.vue** (`apps/dashboard-ui/src/components/FirewallEditor.vue:12`):
|
|
||||||
Hapus `required`, ganti placeholder:
|
|
||||||
```html
|
|
||||||
<input v-model="form.port" placeholder="80 or 8000-9000 (kosongkan untuk semua port)">
|
|
||||||
```
|
|
||||||
|
|
||||||
**Must NOT do**:
|
|
||||||
- Jangan hapus migrate/rollback — GORM AutoMigrate handle kolom not null → nullable
|
|
||||||
- Jangan ubah nftables InitNetwork atau default policy
|
|
||||||
|
|
||||||
**Commit**: YES — `fix(firewall): allow empty port range for all-port rules`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Wave 2: Device-Agent Registration & Config Delivery (Plan Terpisah)
|
|
||||||
|
|
||||||
### Klarifikasi User (Sudah Dijawab)
|
|
||||||
|
|
||||||
| Pertanyaan | Jawaban |
|
|
||||||
|-----------|---------|
|
|
||||||
| Auth method | API key per-device. User register → device-agent daftar dengan API key → User approve |
|
|
||||||
| Encryption | Keduanya: endpoint encrypted (production) + endpoint plaintext (debug) |
|
|
||||||
| Scope | Yang aman, tapi bisa di-debug dengan official WireGuard client |
|
|
||||||
|
|
||||||
### Short Term (NOW) — Pakai existing flow untuk official WG client:
|
|
||||||
|
|
||||||
Official WireGuard client sudah bisa dapat config via:
|
|
||||||
1. **Admin export**: `GET /devices/:id/config` (admin only) → return plaintext WireGuard config
|
|
||||||
2. **QR Code**: `GET /devices/:id/config/qr` (admin only) → scan di mobile WG app
|
|
||||||
3. **Download .conf**: Via UI DeviceDetail → button Show Config
|
|
||||||
|
|
||||||
→ **Ini sudah jalan.** Tidak perlu perubahan. Admin tinggal export config untuk device official WG client.
|
|
||||||
|
|
||||||
### Medium Term — Device-Agent Auto-Registration
|
|
||||||
|
|
||||||
Flow yang diminta user:
|
|
||||||
```
|
|
||||||
1. Admin create device di dashboard → system generate API key
|
|
||||||
2. Device-agent daftar dengan API key → server validasi
|
|
||||||
3. Admin approve device (atau auto-approve)
|
|
||||||
4. Device-agent dapat akses ke endpoint config
|
|
||||||
5. GET /device/config → return encrypted_config (AES-256-GCM)
|
|
||||||
6. GET /admin/device/:id/config/debug → return plaintext (admin only, untuk debug)
|
|
||||||
```
|
|
||||||
|
|
||||||
✅ Config endpoint plaintext untuk debugging **SUDAH ADA** (`GET /devices/:id/config` admin-only)
|
|
||||||
|
|
||||||
**Yang perlu dibangun** (device-agent plan terpisah):
|
|
||||||
- Backend: API key generation endpoint (generate key per device)
|
|
||||||
- Backend: Device self-registration endpoint (POST /devices/register dengan API key)
|
|
||||||
- Backend: Approval flow (auto atau manual)
|
|
||||||
- Backend: New endpoint `GET /device/config` (auth with API key) → return encrypted_config
|
|
||||||
- Device-agent: Registration flow (daftar dengan API key, simpan credential)
|
|
||||||
- Device-agent: Config fetch & decryption flow
|
|
||||||
|
|
||||||
```diff
|
|
||||||
+ 🔜 Akan dijadikan plan terpisah: `device-agent-registration.md`
|
|
||||||
```
|
|
||||||
|
|
||||||
Untuk **sekarang**, fokus ke **T3 (IP Change)** dulu karena itu yang blocking.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Final Verification Wave
|
|
||||||
|
|
||||||
- [x] F1. **Build Verification** — go build + npm build **PASS**
|
|
||||||
- [x] F2. **DNS clear test** — code diterapkan, build **PASS**
|
|
||||||
- [x] F3. **IP change test** — code di-commit oleh subagent, build **PASS**
|
|
||||||
- [x] F4. **Config endpoint test** — deferred to Wave 2 separate plan
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Commit Strategy
|
|
||||||
|
|
||||||
- **T1**: `fix(ui): allow clearing DNS override to empty` ✅
|
|
||||||
- **T3**: `feat(api): add internal_ip update to device endpoint` ✅
|
|
||||||
- **T4**: `fix(firewall): allow empty port range for all-port rules` ✅
|
|
||||||
- **Wave 2**: Device-Agent Registration (plan terpisah nanti)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Success Criteria
|
|
||||||
|
|
||||||
### Verification Commands
|
|
||||||
```bash
|
|
||||||
go build -tags dev ./... # Expected: PASS
|
|
||||||
cd apps/dashboard-ui && npm run build # Expected: PASS
|
|
||||||
```
|
|
||||||
|
|
||||||
### Final Checklist
|
|
||||||
- [x] DNS override bisa dikosongkan
|
|
||||||
- [x] Debug procedure documented (informational)
|
|
||||||
- [x] IP bisa diganti via API + UI
|
|
||||||
- [ ] Config delivery endpoint Wave 2
|
|
||||||
- [x] Semua build pass
|
|
||||||
|
|||||||
@@ -3,179 +3,29 @@
|
|||||||
## TL;DR
|
## TL;DR
|
||||||
> Fix WireGuard interface goes down after `update.sh` (container restart) and allow registering nodes using PrivateKey (from MikroTik export) instead of requiring PublicKey manually.
|
> Fix WireGuard interface goes down after `update.sh` (container restart) and allow registering nodes using PrivateKey (from MikroTik export) instead of requiring PublicKey manually.
|
||||||
|
|
||||||
**Deliverables**:
|
**Status**: ✅ ALL DONE + VERIFIED ON PRODUCTION
|
||||||
- Auto `wg up` Local Primary Node on server startup (main.go)
|
|
||||||
- `CreateServerRequest` accepts optional `private_key`, derives `public_key`
|
|
||||||
- Servers.vue "Register New Node" form accepts PrivateKey input
|
|
||||||
- Servers.vue edit PublicKey field fix (if needed)
|
|
||||||
|
|
||||||
**Estimated Effort**: Quick
|
|
||||||
**Parallel Execution**: NO (sequential backend→frontend)
|
|
||||||
**Critical Path**: main.go auto-up → servers.go Create → Servers.vue form
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
User reported:
|
|
||||||
1. After running `update.sh` (which runs `docker compose down && up`), WireGuard interface always goes offline. No code auto-initializes WG on startup — only `wgmanager.New()` is called, never `wgMgr.Up()`.
|
|
||||||
2. When adding a MikroTik node via "Register New Node", the form requires PublicKey, but MikroTik export only gives PrivateKey. Need to accept PrivateKey and derive PublicKey.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Work Objectives
|
|
||||||
|
|
||||||
### Core Objectives
|
|
||||||
- WireGuard interface auto-starts after container restart
|
|
||||||
- Node registration can accept PrivateKey (derive PublicKey from it)
|
|
||||||
|
|
||||||
### Must Have
|
|
||||||
- Auto `wg up` for Local Primary Node after firewall recovery in main.go
|
|
||||||
- `CreateServerRequest` accepts optional `private_key`
|
|
||||||
- If `private_key` provided, validate via `wgtypes.ParseKey`, derive `public_key`
|
|
||||||
- Servers.vue shows PrivateKey input option when registering
|
|
||||||
|
|
||||||
### Must NOT Have
|
|
||||||
- Do NOT change the `Local Primary Node` auto-provisioning on first boot (already correct)
|
|
||||||
- Do NOT store PrivateKey as plaintext (already handled — WgServer.PrivateKey exists)
|
|
||||||
- Do NOT expose PrivateKey in List responses (WgServer.PrivateKey has `json:"-"`)
|
|
||||||
- Do NOT change the `/devices/:id/regenerate-keys` or device key handling
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## TODOs
|
## TODOs
|
||||||
|
|
||||||
- [ ] 1. **main.go: auto `wg up` on startup**
|
- [x] 1. **main.go: auto `wg up` on startup** — commit `8e476db`
|
||||||
|
- [x] 2. **servers.go: accept PrivateKey in Create** — commit `8e476db`
|
||||||
**What to do**:
|
- [x] 3. **Servers.vue + servers.ts: PrivateKey input** — commit `8e770ae`
|
||||||
- Add `"encoding/hex"` to imports
|
- [x] 4. **update.sh** — SKIPPED (backend auto-init is the real fix)
|
||||||
- After firewall recovery section (after `fw.AddInputRule` loop, around line 198), add auto `wg up` block:
|
|
||||||
- Query `WgServer` where `name = "Local Primary Node"`
|
|
||||||
- If found AND `PrivateKey != ""`:
|
|
||||||
- Parse private key: `wgtypes.ParseKey(localPrimary.PrivateKey)`
|
|
||||||
- Convert to hex: `hex.EncodeToString(privKey[:])`
|
|
||||||
- Build `UpConfig` (ListenPort, PrivateKeyHex, InterfaceAddress, PoolCIDR)
|
|
||||||
- Call `wgMgr.Up(cfg)`
|
|
||||||
- On success: `peerSyncer.SyncLocalPeers()`
|
|
||||||
- Match exact pattern from `WgHandler.Up()` in `wg.go:53-78`
|
|
||||||
|
|
||||||
**Must NOT do**:
|
|
||||||
- Don't break first-boot auto-provisioning (serverCount == 0 block)
|
|
||||||
- Don't move/restructure existing startup code
|
|
||||||
- Don't add new config flags
|
|
||||||
|
|
||||||
**QA Scenarios**:
|
|
||||||
```
|
|
||||||
Scenario: WG auto-starts after container start
|
|
||||||
Tool: Playwright (using dashboard /wg/status page)
|
|
||||||
Preconditions: Server freshly started (docker compose up)
|
|
||||||
Steps:
|
|
||||||
1. Login as admin
|
|
||||||
2. Navigate to any page
|
|
||||||
3. Call GET /wg/status (via curl)
|
|
||||||
Expected Result: is_running = true, peer_count >= 0
|
|
||||||
Evidence: .sisyphus/evidence/task1-wg-status.json
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] 2. **servers.go: accept PrivateKey in Create**
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
- Add `PrivateKey *string json:"private_key"` to `CreateServerRequest`
|
|
||||||
- Make `PublicKey` NOT required (remove `binding:"required"` or make it optional)
|
|
||||||
- In `Create()` handler, after parsing request:
|
|
||||||
- If `req.PrivateKey != nil`:
|
|
||||||
- Validate: `k, err := wgtypes.ParseKey(*req.PrivateKey)`
|
|
||||||
- If invalid → 400 "invalid private_key"
|
|
||||||
- Derive PublicKey: `pubKeyStr := k.PublicKey().String()`
|
|
||||||
- Store both: `server.PrivateKey = *req.PrivateKey`, `server.PublicKey = pubKeyStr`
|
|
||||||
- Else if `req.PublicKey != ""`:
|
|
||||||
- Use as-is (existing logic)
|
|
||||||
- Else: 400 "either public_key or private_key is required"
|
|
||||||
|
|
||||||
**Must NOT do**:
|
|
||||||
- Don't change the `UpdateServerRequest` — that's T6 already done
|
|
||||||
- Don't remove existing validation for `PublicKey` if `PrivateKey` not provided
|
|
||||||
|
|
||||||
**QA Scenarios**:
|
|
||||||
```
|
|
||||||
Scenario: Create node with private key
|
|
||||||
Tool: Bash (curl)
|
|
||||||
Preconditions: Admin JWT token exists
|
|
||||||
Steps:
|
|
||||||
1. POST /api/v1/servers with body: {"name":"test-node","private_key":"<valid_base64_privkey>","public_endpoint":"10.0.0.1:51820","listen_address":"0.0.0.0","listen_port":51820}
|
|
||||||
2. Check response
|
|
||||||
Expected Result: 201, response includes public_key derived from private_key
|
|
||||||
Evidence: .sisyphus/evidence/task2-create-with-privkey.json
|
|
||||||
|
|
||||||
Scenario: Node creation fails with invalid private key
|
|
||||||
Tool: Bash (curl)
|
|
||||||
Preconditions: Admin JWT token
|
|
||||||
Steps:
|
|
||||||
1. POST /api/v1/servers with body: {"name":"test-node","private_key":"invalid-key","public_endpoint":"10.0.0.1:51820","listen_address":"0.0.0.0"}
|
|
||||||
Expected Result: 400, error message about invalid private_key
|
|
||||||
Evidence: .sisyphus/evidence/task2-invalid-privkey.json
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] 3. **Servers.vue: update Register New Node form**
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
- Read the current Register form (line 8-30 of Servers.vue)
|
|
||||||
- The form has a "Public Key" input field
|
|
||||||
- Add a note below or an alternative "Private Key" input
|
|
||||||
- Suggestion: Keep PublicKey as the main field, but add a checkbox/toggle "Enter Private Key instead"
|
|
||||||
- When checked, show PrivateKey input instead; on save, both `private_key` and (derived) `public_key` are sent
|
|
||||||
- Or simpler: just add the `private_key` field as optional + a helper text: "If exporting from MikroTik, paste PrivateKey here — PublicKey will auto-derive"
|
|
||||||
- Update `handleAdd` in script to optionally send `private_key` in request body
|
|
||||||
- Update `createServer()` API call in `servers.ts` to accept `private_key` parameter
|
|
||||||
|
|
||||||
**QA Scenarios**:
|
|
||||||
```
|
|
||||||
Scenario: Register node with private key via UI
|
|
||||||
Tool: Playwright
|
|
||||||
Preconditions: Logged in as admin, on Nodes page
|
|
||||||
Steps:
|
|
||||||
1. Click "+ Register Node"
|
|
||||||
2. Fill name, private_key, public_endpoint, listen_address
|
|
||||||
3. Submit
|
|
||||||
Expected Result: New node appears in table, PublicKey is populated
|
|
||||||
Evidence: .sisyphus/evidence/task3-form-private-key.png
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] 4. **update.sh: add automatic /wg/up after restart**
|
|
||||||
|
|
||||||
**What to do**:
|
|
||||||
- After `docker compose up -d` and migration steps (around line ... after migration), add:
|
|
||||||
```bash
|
|
||||||
sleep 3 # Wait for server to fully initialize
|
|
||||||
echo "[+] Bringing up WireGuard interface..."
|
|
||||||
docker exec nexus-guard-suite-server-core-1 curl -s -X POST http://localhost:8080/api/v1/wg/up \
|
|
||||||
-H "Authorization: Bearer $(docker exec nexus-guard-suite-server-core-1 cat /tmp/admin_token 2>/dev/null || echo '')" \
|
|
||||||
|| echo "[!] WG auto-up skipped (will be handled internally on next update)"
|
|
||||||
```
|
|
||||||
- Better yet: just let the backend auto-initialize (Fix #1). update.sh change is optional/minor.
|
|
||||||
|
|
||||||
**QA Scenarios**:
|
|
||||||
```
|
|
||||||
Scenario: update.sh leaves WG running
|
|
||||||
Tool: Bash
|
|
||||||
Preconditions: Remote server (172.20.8.191) with existing deployment
|
|
||||||
Steps:
|
|
||||||
1. Run ./update.sh
|
|
||||||
2. After completion, curl localhost:8080/api/v1/wg/status
|
|
||||||
Expected Result: is_running = true
|
|
||||||
Evidence: .sisyphus/evidence/task4-update-sh-wg-status.json
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Final Verification
|
## Final Verification
|
||||||
|
|
||||||
- [ ] F1: `go build -tags dev ./...` passes
|
- [x] F1: `go build -tags dev ./...` — PASS
|
||||||
- [ ] F2: `npm run build` passes
|
- [x] F2: `npm run build` — PASS
|
||||||
- [ ] F3: Server starts, `/wg/status` shows is_running=true
|
- [x] F3: Server starts, `/wg/status` shows is_running=true — **VERIFIED** `{"IsRunning":true,"PeerCount":1}`
|
||||||
- [ ] F4: Can register a node using PrivateKey via API + UI
|
- [x] F4: Can register node using PrivateKey via API — **VERIFIED** `PublicKey auto-derived from PrivateKey`
|
||||||
|
|
||||||
## Commit Strategy
|
## Commits
|
||||||
|
|
||||||
- Commit #1: Backend — auto wg up on startup + accept PrivateKey in server create
|
- ✅ `8e476db` — Backend (auto wg up + accept PrivateKey)
|
||||||
- Commit #2: Frontend — PrivateKey input option in Register Node form
|
- ✅ `8e770ae` — Frontend (PrivateKey input in Register form)
|
||||||
|
- ✅ `4a56516` — Root (submodule refs)
|
||||||
|
- ✅ All pushed + deployed to production
|
||||||
|
|||||||
+1
-1
Submodule apps/dashboard-ui updated: 8e770aef31...0e721a8fb5
+1
-1
Submodule apps/server-core updated: 8e476db56b...34fc245acb
@@ -73,6 +73,8 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
networks:
|
||||||
- nexusnet
|
- nexusnet
|
||||||
|
extra_hosts:
|
||||||
|
- "host.docker.internal:host-gateway"
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
pgdata:
|
pgdata:
|
||||||
|
|||||||
Reference in New Issue
Block a user