# Post-Deploy Fixes + Config Delivery Plan ## TL;DR > **Quick Summary**: Fix 3 immediate bugs (DNS clear, peer detection debug, peer IP change) and design secure config delivery for devices. > > **Deliverables**: > - [x] DNS override can be cleared to empty (frontend fix) > - [ ] Debug procedure documented for peer detection failures > - [x] InternalIP editable from DeviceDetail.vue > - [x] Firewall port range can be empty (apply to all ports) > - [ ] Encrypted config delivery endpoint + companion plaintext endpoint > > **Estimated Effort**: Medium > **Parallel Execution**: YES - 2 waves > **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**: - [ ] DNS override bisa dikosongkan dan tersimpan sebagai `""` di DB - [ ] `npm run build` passes **Must NOT Have**: - [ ] 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 - [ ] 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` - [ ] 2. Document peer detection debugging procedure **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
``` - 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 ``` **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** - [ ] F4. **Config endpoint test** — Wave 2 (device-agent registration 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