From 7b8080577c282a0eef57fac5f0d8807119dba5ec Mon Sep 17 00:00:00 2001 From: datadunia Date: Thu, 28 May 2026 06:43:43 +0700 Subject: [PATCH] fix: update submodule refs for T3 + T4 T3: add internal_ip update to device endpoint (IP change) T4: allow empty port range for all-port firewall rules --- .sisyphus/plans/post-deploy-fixes.md | 387 +++++++++++++++++++++++++++ apps/dashboard-ui | 2 +- apps/server-core | 2 +- 3 files changed, 389 insertions(+), 2 deletions(-) create mode 100644 .sisyphus/plans/post-deploy-fixes.md diff --git a/.sisyphus/plans/post-deploy-fixes.md b/.sisyphus/plans/post-deploy-fixes.md new file mode 100644 index 0000000..7baaba1 --- /dev/null +++ b/.sisyphus/plans/post-deploy-fixes.md @@ -0,0 +1,387 @@ +# 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 +└── T4: Fix firewall port range to allow empty (models + API + frontend + nftables) + +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 + +- [ ] F1. **Build Verification** — semua build pass +- [ ] F2. **DNS clear test** — curl PUT dengan dns:"" → ter-clear +- [ ] F3. **IP change test** — 3 skenario (berhasil, duplikat, invalid) +- [ ] F4. **Config endpoint test** — endpoint baru return config sesuai spec + +--- + +## 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 +- [ ] DNS override bisa dikosongkan +- [ ] Debug procedure documented +- [ ] IP bisa diganti via API + UI +- [ ] Config delivery endpoint sesuai kebutuhan user +- [ ] Semua build pass diff --git a/apps/dashboard-ui b/apps/dashboard-ui index 8649e6c..c003a09 160000 --- a/apps/dashboard-ui +++ b/apps/dashboard-ui @@ -1 +1 @@ -Subproject commit 8649e6c3fbc8e74ce4ded07844a24c01a9a2dcac +Subproject commit c003a09b1b7966bba7ebf4d54186b899e0d03867 diff --git a/apps/server-core b/apps/server-core index acac7a1..ffb00e2 160000 --- a/apps/server-core +++ b/apps/server-core @@ -1 +1 @@ -Subproject commit acac7a176d59ca1d93be38d82ab72f82be6eccd6 +Subproject commit ffb00e2de6374505178b95b4eb52d20c9ce4970f