chore: update submodules + docker-compose fixes + new plan docs
NexusGuard CI / server-core-test (push) Failing after 37s
NexusGuard CI / device-agent-test (push) Failing after 32s
NexusGuard CI / dashboard-ui-build (push) Failing after 35s

This commit is contained in:
datadunia
2026-05-28 14:43:50 +07:00
parent 8c6206d2e5
commit f7a5431db8
6 changed files with 46 additions and 552 deletions
+13 -386
View File
@@ -1,388 +1,15 @@
# Post-Deploy Fixes + Config Delivery Plan
# Post-Deploy Fixes
## 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)
> - [x] Debug procedure documented for peer detection failures (informational)
> - [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**:
- [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
Fix DNS clear, peer debug, IP change, port range issues after initial deployment.
## Status
- [x] T1: DNS clear fix (DeviceDetail.vue) — DONE (commit `c1150b3`)
- [x] T2: Debug peer discovery guide — DONE (plan)
- [x] T3: IP change validation (servers.go) — DONE (commit `5e60741`)
- [x] T4: Port range nullable (models.go + rules.go + FirewallEditor.vue) — DONE (commit `ffb00e2` + `c003a09`)
## Final Verification
- [x] `go build -tags dev ./...` — PASS
- [x] `npm run build` — PASS
- [x] Deployed to production