# Fix InterfaceAddress Override Bug (wg/up + Edit Form) ## TL;DR > **Quick Summary**: Two bugs prevent custom WireGuard server InterfaceAddress from sticking: (1) `wg/up` endpoint always recalculates from IPPoolCIDR instead of using stored DB value; (2) Server edit form always pre-fills `ipInput` as network+1 instead of showing stored InterfaceAddress. > > **Deliverables**: > - `apps/server-core/api/wg.go` — use `wgServer.InterfaceAddress` from DB first, fallback to calc > - `apps/dashboard-ui/src/views/Servers.vue` — use `srv.InterfaceAddress` for edit form pre-fill > > **Estimated Effort**: Small (2 files, ~10 lines changed) > **Parallel Execution**: YES — 2 parallel tasks > **Critical Path**: Task 1 → (build verification) --- ## Context ### Original Request User reported: Interface Address (CIDR) for nodes cannot be changed from `.1` to `.4`. WireGuard itself has no such limitation — this is a NexusGuard bug. ### Root Cause **Bug 1 — `api/wg.go:60-71`**: The `Up()` handler always recalculates `interfaceAddr` from `IPPoolCIDR` using `ip[3]++` (network+1). The stored `wgServer.InterfaceAddress` from the database is completely ignored — it's never read. **Bug 2 — `Servers.vue:315-323`**: The `openEdit()` function always reconstructs `ipInput` as network+1 from `IPPoolCIDR`. Even though `srv.InterfaceAddress` is read at line 333, it's immediately overwritten by `parseIpInput()` at line 364 which resets it. The stored value is ignored. ### Data Flow (the bug path) ``` User sets InterfaceAddress = 10.172.21.4/24 → DB stores .4 ✅ │ ┌──────────────────────────────┤ │ │ ▼ ▼ openEdit() (Servers.vue) wg Up() (wg.go) │ │ ipParts[3]++ = .1 ip[3]++ = .1 (ignores srv.InterfaceAddress) (ignores wgServer.InterfaceAddress) │ │ ▼ ▼ Shows .1 ❌ Tunnel uses .1 ❌ ``` ### Metis Analysis - Backend create/update handlers (`api/servers.go`) correctly store `InterfaceAddress` to DB — no changes needed there - `parseIpInput()` in `Servers.vue` correctly computes `interfaceAddress` from `ipInput` — bug is what feeds it, not how it works - IPAM (`internal/ipam/manager.go`) correctly queries `interface_address` from DB for peer allocation — no changes needed - `calcInterfaceAddress()` in `servers.go` is used by fallback/create paths — no changes needed --- ## Work Objectives ### Core Objective Fix custom InterfaceAddress to persist through wg/up and display correctly in the edit form. ### Concrete Deliverables - `apps/server-core/api/wg.go:60-71` — use stored `wgServer.InterfaceAddress` first, fallback to calc from pool if empty - `apps/dashboard-ui/src/views/Servers.vue:315-323` — use `srv.InterfaceAddress` for `ipInput` pre-fill, fallback to network+1 if empty ### Definition of Done - [x] Setting InterfaceAddress to custom value via API → wg/up uses that value (not network+1) - [x] Setting InterfaceAddress to custom value → edit form shows that value (not network+1) - [x] Empty InterfaceAddress + IPPoolCIDR → fallback to network+1 still works - [x] Malformed InterfaceAddress in DB → wg/up falls back to calc (doesn't crash) - [x] `npm run build` passes ### Must Have - Custom InterfaceAddress survives wg/up call - Edit form displays stored InterfaceAddress ### Must NOT Have (Guardrails) - Do NOT touch `parseIpInput()` in Servers.vue (lines 360-425) — it works correctly - Do NOT touch create/update handlers in `api/servers.go` — they correctly store InterfaceAddress - Do NOT touch IPAM (`internal/ipam/manager.go`) — it correctly excludes InterfaceAddress from peer allocation - Do NOT touch `calcInterfaceAddress` in `api/servers.go` - Do NOT touch wg_test.go — existing tests cover only the fallback path - Do NOT refactor the unified `ipInput` → `ipPoolCidr` + `interfaceAddress` form pattern --- ## Verification Strategy > **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. ### Test Decision - **Infrastructure exists**: No test suite for this specific path - **Automated tests**: None for custom InterfaceAddress path - **Primary verification**: `npm run build` + `go build ./...` + grep assertions ### QA Policy Every task MUST include agent-executed QA scenarios. - **Backend**: Build check + grep verification - **Frontend**: Build check - **Evidence**: `.sisyphus/evidence/task-{N}-{scenario-slug}.txt` --- ## Execution Strategy ### Parallel Execution Waves ``` Wave 1 (Start immediately — parallel): ├── Task 1: Fix api/wg.go — use stored InterfaceAddress [quick] ├── Task 2: Fix Servers.vue — use stored InterfaceAddress for pre-fill [quick] Wave FINAL: ├── Task F1: Plan compliance audit (oracle) ├── Task F2: Code quality + build check (unspecified-high) ├── Task F3: Real manual QA (unspecified-high) ├── Task F4: Scope fidelity check (deep) ``` ### Agent Dispatch Summary - **Wave 1**: 2 parallel tasks - **FINAL**: 4 parallel reviews --- ## TODOs - [x] 1. Fix `api/wg.go` — use stored InterfaceAddress for wg/up **What to do**: - In `apps/server-core/api/wg.go` lines 60-71: - Change `interfaceAddr := ""` to `interfaceAddr := wgServer.InterfaceAddress` - Change the `if` condition from `if wgServer.IPPoolCIDR != ""` to `if interfaceAddr == "" && wgServer.IPPoolCIDR != ""` - This way: stored value wins; if empty, fall back to pool calculation **Current code block (lines 60-71)**: ```go interfaceAddr := "" if wgServer.IPPoolCIDR != "" { if ip, ipnet, err := net.ParseCIDR(wgServer.IPPoolCIDR); err == nil { ip4 := ip.To4() if ip4 != nil { ip4[3]++ if ones, _ := ipnet.Mask.Size(); ones > 0 { interfaceAddr = fmt.Sprintf("%s/%d", ip4.String(), ones) } } } } ``` **Changed to**: ```go interfaceAddr := wgServer.InterfaceAddress if interfaceAddr == "" && wgServer.IPPoolCIDR != "" { if ip, ipnet, err := net.ParseCIDR(wgServer.IPPoolCIDR); err == nil { ip4 := ip.To4() if ip4 != nil { ip4[3]++ if ones, _ := ipnet.Mask.Size(); ones > 0 { interfaceAddr = fmt.Sprintf("%s/%d", ip4.String(), ones) } } } } ``` - Verify with `go build ./...` **Must NOT do**: - Do NOT touch `calcInterfaceAddress` in `api/servers.go` - Do NOT touch create/update handlers in `api/servers.go` - Do NOT touch IPAM or wgmanager **Recommended Agent Profile**: - **Category**: `quick` - Reason: Single file, 6-line change, minimal logic - **Skills**: none needed **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Task 2) - **Blocks**: F1-F4 - **Blocked By**: None **References**: - `apps/server-core/api/wg.go:55-78` — context: wgServer variable is already loaded from DB, so `wgServer.InterfaceAddress` is available - `apps/server-core/api/wg.go:60-71` — the exact lines to change **WHY**: - The stored `InterfaceAddress` is the user's explicit choice. The pool calculation was always meant to be a fallback for empty values. - No CIDR validation needed — if stored value is malformed, `ip addr add` will fail which is acceptable (the DB should have valid data) **Acceptance Criteria**: **QA Scenarios (MANDATORY):** ``` Scenario: Verify code uses stored InterfaceAddress Tool: Bash (grep) Preconditions: Codebase clean Steps: 1. `Select-String -Path "apps/server-core/api/wg.go" -Pattern 'interfaceAddr := wgServer.InterfaceAddress'` Expected Result: Match found — the new code is in place Evidence: .sisyphus/evidence/task-1-code-check.txt Scenario: Verify fallback still exists Tool: Bash (grep) Steps: 1. `Select-String -Path "apps/server-core/api/wg.go" -Pattern 'interfaceAddr == "" &&'` Expected Result: Match found — fallback to pool calc still works Evidence: .sisyphus/evidence/task-1-fallback.txt Scenario: Build passes Tool: Bash Steps: 1. `cd apps/server-core && go build ./...` Expected Result: Exit 0 Evidence: .sisyphus/evidence/task-1-build.txt ``` **Evidence to Capture:** - [ ] `.sisyphus/evidence/task-1-code-check.txt` - [ ] `.sisyphus/evidence/task-1-fallback.txt` - [ ] `.sisyphus/evidence/task-1-build.txt` **Commit**: YES (with Task 2) - Message: `fix(api): respect stored InterfaceAddress in wg/up instead of always recalculating` - Files: `apps/server-core/api/wg.go` - [x] 2. Fix `Servers.vue` — use stored InterfaceAddress for edit form pre-fill **What to do**: - In `apps/dashboard-ui/src/views/Servers.vue` lines 315-323, change `openEdit()` to use `srv.InterfaceAddress` first, fallback to pool network+1 **Current code block (lines 315-323)**: ```ts let ipInput = '' if (srv.IPPoolCIDR) { const parts = srv.IPPoolCIDR.split('/') const poolPrefix = parts[1] || '' const poolIp = parts[0] const ipParts = poolIp.split('.').map(Number) ipParts[3]++ ipInput = poolPrefix ? `${ipParts.join(".")}/${poolPrefix}` : '' } ``` **Changed to**: ```ts let ipInput = '' if (srv.InterfaceAddress) { ipInput = srv.InterfaceAddress } else if (srv.IPPoolCIDR) { const parts = srv.IPPoolCIDR.split('/') const poolPrefix = parts[1] || '' const poolIp = parts[0] const ipParts = poolIp.split('.').map(Number) ipParts[3]++ ipInput = poolPrefix ? `${ipParts.join(".")}/${poolPrefix}` : '' } ``` - **Critical note**: Line 333 (`interfaceAddress: srv.InterfaceAddress || ''`) will be overwritten by `parseIpInput` at line 364 (which resets it to `''`) and then line 420 (which sets it from `ipInput`). This is correct behavior — the stored value feeds `ipInput`, `parseIpInput` derives everything from `ipInput`. - Verify with `npm run build` **Must NOT do**: - Do NOT touch `parseIpInput()` (lines 360-425) — works correctly - Do NOT change line 333 (`interfaceAddress: srv.InterfaceAddress || ''`) — it's overwritten by parseIpInput, harmless - Do NOT change create flow — only edit flow **Recommended Agent Profile**: - **Category**: `quick` - Reason: Single file, 5-line change, straightforward - **Skills**: none needed **Parallelization**: - **Can Run In Parallel**: YES - **Parallel Group**: Wave 1 (with Task 1) - **Blocks**: F1-F4 - **Blocked By**: None **References**: - `apps/dashboard-ui/src/views/Servers.vue:315-323` — exact lines to change - `apps/dashboard-ui/src/views/Servers.vue:360-425` — `parseIpInput` function (read-only reference) **WHY**: - `parseIpInput` derives `ipPoolCidr` and `interfaceAddress` from `ipInput`. Pre-filling `ipInput` with the stored value makes it flow correctly through the existing logic. **Acceptance Criteria**: **QA Scenarios (MANDATORY):** ``` Scenario: Verify code uses stored InterfaceAddress Tool: Bash (grep) Steps: 1. `Select-String -Path "apps/dashboard-ui/src/views/Servers.vue" -Pattern "srv.InterfaceAddress"` Expected Result: Match at line ~315 (before the else if for IPPoolCIDR) Evidence: .sisyphus/evidence/task-2-code-check.txt Scenario: Verify fallback still exists Tool: Bash (grep) Steps: 1. `Select-String -Path "apps/dashboard-ui/src/views/Servers.vue" -Pattern "ipParts\[3\]\+\+" -SimpleMatch` Expected Result: Match found — fallback to network+1 still works Evidence: .sisyphus/evidence/task-2-fallback.txt Scenario: Build passes Tool: Bash Steps: 1. `cd apps/dashboard-ui && npm run build` Expected Result: Exit 0 Evidence: .sisyphus/evidence/task-2-build.txt ``` **Evidence to Capture:** - [ ] `.sisyphus/evidence/task-2-code-check.txt` - [ ] `.sisyphus/evidence/task-2-fallback.txt` - [ ] `.sisyphus/evidence/task-2-build.txt` **Commit**: YES (with Task 1) - Message: `fix(ui): use stored InterfaceAddress in edit form instead of always reconstructing` - Files: `apps/dashboard-ui/src/views/Servers.vue` --- ## Final Verification Wave - [x] F1. **Plan Compliance Audit** — `oracle` Read the plan end-to-end. For each Must Have: verify implementation exists. For each Must NOT Have: search codebase for forbidden patterns. Output: `VERDICT: APPROVE/REJECT` - [x] F2. **Code Quality Review** — `unspecified-high` Run `npm run build` and `go build ./...`. Check for AI slop. Output: `Build [PASS/FAIL] | VERDICT` - [x] F3. **Real Manual QA** — `unspecified-high` Verify both fix scenarios. No integration testing — these are compile-time/logic fixes. Output: `Scenarios [N/N pass] | VERDICT` - [x] F4. **Scope Fidelity Check** — `deep` For each task: read "What to do", read actual diff. No scope creep. Output: `Tasks [N/N compliant] | VERDICT` --- ## Commit Strategy - **1+2**: `fix: respect stored InterfaceAddress in wg/up and edit form` - `apps/server-core/api/wg.go` - `apps/dashboard-ui/src/views/Servers.vue` --- ## Success Criteria ### Verification Commands ```bash cd apps/server-core && go build ./... # Backend builds cd apps/dashboard-ui && npm run build # Frontend builds ``` ### Final Checklist - [x] All "Must Have" present - [x] All "Must NOT Have" absent - [x] All builds pass