Files
Nexus-Guard-Suite/.sisyphus/plans/archived/fix-interface-address-override.md
T
datadunia 075f915a66
NexusGuard CI / server-core-test (push) Failing after 47s
NexusGuard CI / device-agent-test (push) Failing after 35s
NexusGuard CI / dashboard-ui-build (push) Failing after 31s
chore: archive old plans, add new plan docs, update submodules
2026-05-28 02:50:43 +07:00

362 lines
14 KiB
Markdown

# 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