14 KiB
Fix InterfaceAddress Override Bug (wg/up + Edit Form)
TL;DR
Quick Summary: Two bugs prevent custom WireGuard server InterfaceAddress from sticking: (1)
wg/upendpoint always recalculates from IPPoolCIDR instead of using stored DB value; (2) Server edit form always pre-fillsipInputas network+1 instead of showing stored InterfaceAddress.Deliverables:
apps/server-core/api/wg.go— usewgServer.InterfaceAddressfrom DB first, fallback to calcapps/dashboard-ui/src/views/Servers.vue— usesrv.InterfaceAddressfor edit form pre-fillEstimated 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 storeInterfaceAddressto DB — no changes needed there parseIpInput()inServers.vuecorrectly computesinterfaceAddressfromipInput— bug is what feeds it, not how it works- IPAM (
internal/ipam/manager.go) correctly queriesinterface_addressfrom DB for peer allocation — no changes needed calcInterfaceAddress()inservers.gois 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 storedwgServer.InterfaceAddressfirst, fallback to calc from pool if emptyapps/dashboard-ui/src/views/Servers.vue:315-323— usesrv.InterfaceAddressforipInputpre-fill, fallback to network+1 if empty
Definition of Done
- Setting InterfaceAddress to custom value via API → wg/up uses that value (not network+1)
- Setting InterfaceAddress to custom value → edit form shows that value (not network+1)
- Empty InterfaceAddress + IPPoolCIDR → fallback to network+1 still works
- Malformed InterfaceAddress in DB → wg/up falls back to calc (doesn't crash)
npm run buildpasses
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
calcInterfaceAddressinapi/servers.go - Do NOT touch wg_test.go — existing tests cover only the fallback path
- Do NOT refactor the unified
ipInput→ipPoolCidr+interfaceAddressform 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
-
1. Fix
api/wg.go— use stored InterfaceAddress for wg/upWhat to do:
- In
apps/server-core/api/wg.golines 60-71:- Change
interfaceAddr := ""tointerfaceAddr := wgServer.InterfaceAddress - Change the
ifcondition fromif wgServer.IPPoolCIDR != ""toif interfaceAddr == "" && wgServer.IPPoolCIDR != "" - This way: stored value wins; if empty, fall back to pool calculation
- Change
Current code block (lines 60-71):
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:
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
calcInterfaceAddressinapi/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, sowgServer.InterfaceAddressis availableapps/server-core/api/wg.go:60-71— the exact lines to change
WHY:
- The stored
InterfaceAddressis 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 addwill 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.txtEvidence 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
- In
-
2. Fix
Servers.vue— use stored InterfaceAddress for edit form pre-fillWhat to do:
- In
apps/dashboard-ui/src/views/Servers.vuelines 315-323, changeopenEdit()to usesrv.InterfaceAddressfirst, fallback to pool network+1
Current code block (lines 315-323):
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:
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 byparseIpInputat line 364 (which resets it to'') and then line 420 (which sets it fromipInput). This is correct behavior — the stored value feedsipInput,parseIpInputderives everything fromipInput. - 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 changeapps/dashboard-ui/src/views/Servers.vue:360-425—parseIpInputfunction (read-only reference)
WHY:
parseIpInputderivesipPoolCidrandinterfaceAddressfromipInput. Pre-fillingipInputwith 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.txtEvidence 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
- In
Final Verification Wave
-
F1. Plan Compliance Audit —
oracleRead 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 -
F2. Code Quality Review —
unspecified-highRunnpm run buildandgo build ./.... Check for AI slop. Output:Build [PASS/FAIL] | VERDICT -
F3. Real Manual QA —
unspecified-highVerify both fix scenarios. No integration testing — these are compile-time/logic fixes. Output:Scenarios [N/N pass] | VERDICT -
F4. Scope Fidelity Check —
deepFor 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 formapps/server-core/api/wg.goapps/dashboard-ui/src/views/Servers.vue
Success Criteria
Verification Commands
cd apps/server-core && go build ./... # Backend builds
cd apps/dashboard-ui && npm run build # Frontend builds
Final Checklist
- All "Must Have" present
- All "Must NOT Have" absent
- All builds pass