Files
Nexus-Guard-Suite/.omo/plans/archived/fix-interface-address-override.md
datadunia 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
chore: update submodule refs, clean up plans/evidence, update .gitignore
2026-06-07 23:53:15 +07:00

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/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

  • 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 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 ipInputipPoolCidr + 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

  • 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):

    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 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
  • 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):

    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 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-425parseIpInput 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

  • F1. Plan Compliance Auditoracle 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

  • F2. Code Quality Reviewunspecified-high Run npm run build and go build ./.... Check for AI slop. Output: Build [PASS/FAIL] | VERDICT

  • F3. Real Manual QAunspecified-high Verify both fix scenarios. No integration testing — these are compile-time/logic fixes. Output: Scenarios [N/N pass] | VERDICT

  • F4. Scope Fidelity Checkdeep 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

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