cbacfea7f2
NexusGuard CI / server-core-test (push) Failing after 3m6s
NexusGuard CI / server-core-build (push) Has been skipped
NexusGuard CI / device-agent-test (push) Failing after 4s
NexusGuard CI / device-agent-cross-build (amd64, linux) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (amd64, windows) (push) Has been skipped
NexusGuard CI / device-agent-cross-build (arm64, linux) (push) Has been skipped
NexusGuard CI / dashboard-test (push) Failing after 4s
NexusGuard CI / dashboard-dist (push) Has been skipped
362 lines
14 KiB
Markdown
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
|