Files
Nexus-Guard-Suite/.omo/plans/archived/firewall-device-status-fix.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

19 KiB

Plan: Firewall INPUT Fix + Device Status via WireGuard Handshake

TL;DR

Fix two critical production issues: (1) firewall INPUT chain doesn't isolate WireGuard peers, (2) device status shows offline because it depends on Redis heartbeats instead of WireGuard handshake data.

Deliverables:

  • WireGuard per-peer handshake status (replace Redis heartbeat dependency)
  • Firewall INPUT chain rules in code (persist across restarts)
  • ICMP + established/related accept rules for WG traffic

Estimated Effort: Medium Parallel Execution: YES - 2 waves Critical Path: T1 (wgctrl) → T2 (SyncToDB) → T3 (INPUT rules) → F1-F4


Context

Original Request

  1. Device gogo2 connected via WireGuard (peer shows handshake in wg show) but dashboard shows offline (IsActive=false)
  2. Firewall INPUT chain rules don't work — ping from WG peer still works even with drop rules
  3. User wants a proper plan, not ad-hoc fixes

Interview Summary

Key Discussions:

  • Device was created manually via API, not via device-agent → no heartbeats sent
  • Heartbeat system: device-agent → POST /api/v1/heartbeat → Redis TTL 90s → SyncToDB sets is_active
  • Without device-agent, no heartbeats → is_active stays false
  • Solution: use WireGuard handshake time from wgctrl instead of Redis heartbeats
  • Firewall: table ip nexusguard INPUT chain has rules but ping still works
  • Root cause: rules not in code (manually added, lost on restart), ICMP not explicitly allowed, possible table inet vs table ip priority conflict

Research Findings:

  • wgmanager_linux.go:50 iterates dev.Peers and reads peer.LastHandshakeTime — but only stores MAX across all peers, losing per-peer granularity
  • WgStatus struct has no per-peer handshake map
  • SyncToDB() in heartbeat/redis.go checks Redis key existence → sets is_active
  • Dashboard reads device.IsActive from DB — no frontend changes needed if SyncToDB is fixed
  • InitNetwork() creates INPUT chain with zero rules — all rules were manual
  • AddInputRule() only adds udp dport rules — no ICMP, no established/related

Metis Review

Identified Gaps (addressed):

  • INPUT chain rules must be added to InitNetwork() code for persistence
  • ICMP accept rule missing — only TCP/UDP explicitly allowed
  • Per-peer handshake data needed (current GetStatus() loses per-peer granularity)
  • Remote node devices won't have local WG data — scoped to local-only
  • Docker's inet filter chains must not be modified

Work Objectives

Core Objective

Device status reflects actual WireGuard connectivity (not Redis heartbeats), and firewall INPUT chain properly isolates WG peers.

Concrete Deliverables

  • wgmanager interface: new GetPeerHandshakes() method
  • heartbeat/redis.go: SyncToDB() uses WG handshake data
  • nftables_linux.go: INPUT chain rules in InitNetwork()
  • Production deployment with verification

Definition of Done

  • Device with active WG peer shows is_active=true in API
  • Device without WG handshake shows is_active=false
  • ping 10.172.21.1 from WG peer → RTO (dropped)
  • TCP 8080 from WG peer → works (API access)
  • UDP 51820 from WG peer → works (WG tunnel)
  • Non-WG traffic → unaffected (Docker, SSH, etc.)
  • Rules persist across server restart

Must Have

  • GetPeerHandshakes() map[string]time.Time in wgmanager
  • SyncToDB() matches device public keys to WG peer public keys
  • INPUT chain rules in InitNetwork() code (not manual)
  • ICMP accept rule for WG traffic
  • established/related accept rule for WG traffic

Must NOT Have (Guardrails)

  • NEVER nft flush table — project anti-pattern
  • NEVER change policy accept on any chain
  • NEVER modify Docker's inet filter chains
  • NEVER add IPv6 rules
  • NEVER remove heartbeat API endpoint (keep as fallback)
  • NEVER add new API endpoints — modify existing only
  • NEVER touch shared/crypto/encryptor.go
  • NEVER add per-device firewall INPUT rules (INPUT is server-level, not peer isolation)

Verification Strategy

ZERO HUMAN INTERVENTION — ALL verification is agent-executed.

Test Decision

  • Infrastructure exists: YES (Go build, nft CLI, curl)
  • Automated tests: Tests-after (verify existing tests still pass)
  • Framework: go build -tags dev ./..., nft list chain, curl API

QA Policy

Every task includes agent-executed QA scenarios. Evidence saved to .sisyphus/evidence/task-{N}-{scenario-slug}.{ext}.

  • Backend: Use Bash (curl) — API requests, assert status + response fields
  • Firewall: Use Bash (ssh + nft) — rule verification, counter checks
  • WireGuard: Use Bash (ssh + wg) — handshake verification

Execution Strategy

Parallel Execution Waves

Wave 1 (Start Immediately - backend core):
├── Task 1: wgmanager GetPeerHandshakes() [quick]
├── Task 2: SyncToDB uses WG handshake [quick]
└── Task 3: InitNetwork INPUT chain rules [quick]

Wave 2 (After Wave 1 - deployment + verification):
├── Task 4: Build + deploy to production [quick]
└── Task 5: Verify all acceptance criteria [quick]

Wave FINAL (After ALL tasks — 4 parallel reviews, then user okay):
├── Task F1: Plan compliance audit (oracle)
├── Task F2: Code quality review (unspecified-high)
├── Task F3: Real manual QA (unspecified-high)
└── Task F4: Scope fidelity check (deep)
-> Present results -> Get explicit user okay

Dependency Matrix

Task Depends On Blocks
T1 None T2
T2 T1 T4
T3 None T4
T4 T2, T3 T5, F1-F4
T5 T4 F1-F4

Agent Dispatch Summary

  • Wave 1: 3 tasks — T1 quick, T2 quick, T3 quick
  • Wave 2: 2 tasks — T4 quick, T5 quick
  • FINAL: 4 tasks — F1 oracle, F2 unspecified-high, F3 unspecified-high, F4 deep

TODOs

  • 1. wgmanager: add GetPeerHandshakes() method

    What to do:

    • Add GetPeerHandshakes() map[string]time.Time to WgManager interface in internal/wgmanager/manager.go
    • Implement in internal/wgmanager/wgmanager_linux.go: iterate dev.Peers, return map of peer.PublicKey.String() → peer.LastHandshakeTime
    • Implement stub in internal/wgmanager/wgmanager_stub.go (returns empty map)
    • Add test in internal/wgmanager/ if test file exists

    Must NOT do:

    • Don't modify existing GetStatus() method
    • Don't change WgStatus struct

    Recommended Agent Profile:

    • Category: quick
    • Skills: []

    Parallelization:

    • Can Run In Parallel: YES (with T3)
    • Parallel Group: Wave 1 (with T3)
    • Blocks: T2
    • Blocked By: None

    References:

    • apps/server-core/internal/wgmanager/manager.go:27-39 — WgManager interface + UpConfig struct
    • apps/server-core/internal/wgmanager/wgmanager_linux.go:27-58 — GetStatus() implementation, iterates dev.Peers at line 50
    • apps/server-core/internal/wgmanager/wgmanager_stub.go — Stub implementation

    Acceptance Criteria:

    • go build -tags dev ./... passes
    • GetPeerHandshakes() returns non-empty map when peers are connected
    • Method is on both interface and implementations

    QA Scenarios:

    Scenario: GetPeerHandshakes returns peer handshake data
      Tool: Bash (go test or manual verification)
      Preconditions: Server running with connected WG peer
      Steps:
        1. SSH to server
        2. Run: docker exec nexus-guard-suite-server-core-1 ./server-core -tags dev -test.run TestGetPeerHandshakes 2>&1 || echo "verify via API"
        3. Check wg show wg0 latest-handshakes for reference
      Expected Result: Map contains public key → timestamp for connected peer
      Evidence: .sisyphus/evidence/task1-peer-handshakes.json
    

    Commit: YES

    • Message: feat(wgmanager): add GetPeerHandshakes() for per-peer handshake data
    • Files: internal/wgmanager/manager.go, internal/wgmanager/wgmanager_linux.go, internal/wgmanager/wgmanager_stub.go
  • 2. heartbeat: SyncToDB uses WireGuard handshake instead of Redis

    What to do:

    • Modify SyncToDB() in internal/heartbeat/redis.go to:
      1. Query wgMgr.GetPeerHandshakes() (inject wgMgr into Manager or pass as parameter)
      2. For each device, match device.PublicKey against WG peer public keys
      3. If match found AND handshake within 120s → set is_active=true, last_handshake=handshake_time
      4. If no match OR handshake > 120s → set is_active=false
    • Keep Redis heartbeat recording active (don't break existing device-agents)
    • Update NewHeartbeatManager to accept wgmanager.WgManager parameter
    • Update main.go to pass wgMgr to heartbeat manager

    Must NOT do:

    • Don't remove Redis heartbeat recording (keep as fallback)
    • Don't change the heartbeat API endpoint
    • Don't change RecordPing() method

    Recommended Agent Profile:

    • Category: quick
    • Skills: []

    Parallelization:

    • Can Run In Parallel: NO
    • Parallel Group: Wave 1 (after T1)
    • Blocks: T4
    • Blocked By: T1

    References:

    • apps/server-core/internal/heartbeat/redis.go:57-84 — Current SyncToDB implementation
    • apps/server-core/internal/heartbeat/redis.go:14-17 — Manager struct (needs wgMgr field)
    • apps/server-core/main.go:216-219 — HeartbeatManager creation (needs wgMgr param)
    • apps/server-core/api/devices.go:35-60 — Device List handler (reads is_active from DB)
    • apps/dashboard-ui/src/views/Devices.vue — Dashboard reads IsActive from API response

    Acceptance Criteria:

    • go build -tags dev ./... passes
    • Device with active WG peer → is_active=true in DB after SyncToDB cycle
    • Device without WG handshake → is_active=false
    • Redis heartbeat recording still works (existing device-agents unaffected)

    QA Scenarios:

    Scenario: Device with active WG peer shows online
      Tool: Bash (curl + ssh)
      Preconditions: Server running, device gogo2 connected via WG
      Steps:
        1. SSH to server, get token: TOKEN=$(curl -s http://localhost:8080/api/v1/auth/login -X POST -H "Content-Type: application/json" -d '{"username":"admin","password":"admin123"}' | python3 -c "import sys,json; print(json.load(sys.stdin)['token'])")
        2. Wait 35s for SyncToDB cycle
        3. curl -s http://localhost:8080/api/v1/devices -H "Authorization: Bearer $TOKEN" | python3 -c "import sys,json; devices=json.load(sys.stdin); [print(d['Name'], d.get('is_active')) for d in devices]"
      Expected Result: gogo2 shows True (is_active=true)
      Evidence: .sisyphus/evidence/task2-device-online.json
    
    Scenario: WG peer handshake data present
      Tool: Bash (ssh)
      Steps:
        1. SSH to server
        2. wg show wg0 latest-handshakes
        3. Compare timestamp with device LastHandshake in DB
      Expected Result: Timestamps match (within 30s)
      Evidence: .sisyphus/evidence/task2-handshake-match.json
    

    Commit: YES (groups with T1)

    • Message: feat(heartbeat): use WireGuard handshake for device status instead of Redis
    • Files: internal/heartbeat/redis.go, main.go
  • 3. nftables: add INPUT chain rules to InitNetwork()

    What to do:

    • In InitNetwork() (after forward chain rules), add INPUT chain rules via nft CLI:
      nft add rule ip nexusguard input ct state established,related accept comment "estab"
      nft add rule ip nexusguard input icmp type echo-request accept comment "icmp_ping"
      nft add rule ip nexusguard input ip saddr 10.172.21.0/24 tcp dport 8080 accept comment "wg_api"
      nft add rule ip nexusguard input ip saddr 10.172.21.0/24 udp dport 51820 accept comment "wg_tunnel"
      nft add rule ip nexusguard input ip saddr 10.172.21.0/24 drop comment "wg_isolation"
      
    • Each rule must be idempotent (check comment before adding)
    • Use detectWGSubnet() (already exists) for WG subnet detection
    • Remove old manual rules on production server during deployment

    Must NOT do:

    • Don't change policy accept on INPUT chain
    • Don't add rules to Docker's inet filter chains
    • Don't add IPv6 rules

    Recommended Agent Profile:

    • Category: quick
    • Skills: []

    Parallelization:

    • Can Run In Parallel: YES (with T1)
    • Parallel Group: Wave 1 (with T1)
    • Blocks: T4
    • Blocked By: None

    References:

    • apps/server-core/internal/firewall/nftables_linux.go:25-43 — Current InitNetwork (INPUT chain created but empty)
    • apps/server-core/internal/firewall/nftables_linux.go:46-62 — detectWGSubnet() function
    • apps/server-core/internal/firewall/nftables_linux.go:156-174 — AddInputRule/RemoveInputRule (existing pattern)

    Acceptance Criteria:

    • go build -tags dev ./... passes
    • nft list chain ip nexusguard input shows all 5 rules after server start
    • Rules are idempotent (restart doesn't duplicate rules)

    QA Scenarios:

    Scenario: INPUT chain rules exist after restart
      Tool: Bash (ssh + nft)
      Preconditions: Server rebuilt with new code
      Steps:
        1. SSH to server
        2. docker compose down && docker compose up -d --build
        3. sleep 5
        4. nft list chain ip nexusguard input
      Expected Result: 5 rules visible (estab, icmp, wg_api, wg_tunnel, wg_isolation)
      Evidence: .sisyphus/evidence/task3-input-rules.txt
    
    Scenario: Ping from WG peer is dropped
      Tool: Bash (ssh)
      Steps:
        1. SSH to server
        2. nft list chain ip nexusguard input | grep wg_isolation
        3. Count packets: nft -a list chain ip nexusguard input | grep wg_isolation
      Expected Result: Drop rule exists with counter > 0
      Evidence: .sisyphus/evidence/task3-ping-dropped.txt
    
    Scenario: Docker networking unaffected
      Tool: Bash (ssh + curl)
      Steps:
        1. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:80
        2. docker ps
      Expected Result: HTTP 200, containers running
      Evidence: .sisyphus/evidence/task3-docker-ok.txt
    

    Commit: YES

    • Message: fix(nftables): add INPUT chain rules for WG peer isolation in InitNetwork
    • Files: internal/firewall/nftables_linux.go
  • 4. Build + deploy to production

    What to do:

    • Build server-core: cd apps/server-core && go build -tags dev ./...
    • Build dashboard-ui: cd apps/dashboard-ui && npm run build
    • Commit all changes
    • Push to remote
    • SSH to production: git pull --recurse-submodules && docker compose down && docker compose up -d --build
    • Remove old manual nftables rules on production

    Must NOT do:

    • Don't skip build verification
    • Don't push broken code

    Recommended Agent Profile:

    • Category: quick
    • Skills: []

    Parallelization:

    • Can Run In Parallel: NO
    • Parallel Group: Wave 2
    • Blocks: T5, F1-F4
    • Blocked By: T2, T3

    References:

    • connect_remote.txt — Production server SSH details
    • docker-compose.yml — Build configuration

    Acceptance Criteria:

    • go build -tags dev ./... passes locally
    • npm run build passes locally
    • Production server running with new code
    • curl http://172.20.8.191:80 returns 200

    QA Scenarios:

    Scenario: Production deployment successful
      Tool: Bash (ssh + curl)
      Steps:
        1. SSH to server
        2. docker logs nexus-guard-suite-server-core-1 2>&1 | tail -5
        3. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:80
        4. curl -s -o /dev/null -w '%{http_code}' http://172.20.8.191:8080/api/v1/health
      Expected Result: Server started, HTTP 200 for both endpoints
      Evidence: .sisyphus/evidence/task4-deployment.txt
    

    Commit: YES

    • Message: feat: firewall INPUT fix + WG handshake device status
    • Files: all changed files
  • 5. Verify all acceptance criteria on production

    What to do:

    • Run all acceptance tests from Metis review
    • Verify device status (gogo2 shows online)
    • Verify firewall (ping dropped, API works, Docker unaffected)
    • Verify rules persist (restart server, check rules)
    • Save all evidence

    Must NOT do:

    • Don't skip any acceptance test
    • Don't assume — verify each criterion

    Recommended Agent Profile:

    • Category: quick
    • Skills: []

    Parallelization:

    • Can Run In Parallel: NO
    • Parallel Group: Wave 2 (after T4)
    • Blocks: F1-F4
    • Blocked By: T4

    References:

    • connect_remote.txt — Production server SSH details
    • Plan "Definition of Done" section

    Acceptance Criteria:

    • All 7 acceptance tests pass
    • Evidence files saved

    QA Scenarios:

    Scenario: Full acceptance test suite
      Tool: Bash (ssh + curl + nft + wg)
      Steps:
        1. Test 1: Device gogo2 is_active=true via API
        2. Test 2: ping 10.172.21.1 from WG peer → RTO
        3. Test 3: curl http://10.172.21.1:8080/api/v1/health → 200
        4. Test 4: curl http://172.20.8.191:80 → 200 (Docker)
        5. Test 5: nft list chain ip nexusguard input → 5 rules
        6. Test 6: Restart server, verify rules persist
        7. Test 7: Dashboard shows device Online
      Expected Result: All 7 tests pass
      Evidence: .sisyphus/evidence/task5-full-qa.txt
    

    Commit: NO


Final Verification Wave

4 review agents run in PARALLEL. ALL must APPROVE. Present consolidated results to user and get explicit "okay" before completing.

  • 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. Check evidence files exist. Compare deliverables against plan. Output: Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT

  • F2. Code Quality Reviewunspecified-high Run go build -tags dev ./... + go vet ./.... Review all changed files for: empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: excessive comments, over-abstraction. Output: Build [PASS/FAIL] | Vet [PASS/FAIL] | Files [N clean/N issues] | VERDICT

  • F3. Real Manual QAunspecified-high Start from clean state. Execute EVERY QA scenario from EVERY task. Test cross-task integration. Test edge cases. Save to .sisyphus/evidence/final-qa/. Output: Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT

  • F4. Scope Fidelity Checkdeep For each task: read "What to do", read actual diff. Verify 1:1. Check "Must NOT do" compliance. Flag unaccounted changes. Output: Tasks [N/N compliant] | Unaccounted [CLEAN/N files] | VERDICT


Commit Strategy

  • Commit #1: Backend — wgmanager + heartbeat SyncToDB
  • Commit #2: Backend — nftables INPUT chain rules
  • Commit #3: Root — submodule refs

Success Criteria

Verification Commands

go build -tags dev ./...  # Expected: no errors
go vet ./...              # Expected: no warnings
wg show wg0 latest-handshakes  # Expected: Unix timestamps
curl -s http://localhost:8080/api/v1/devices | jq '.[].is_active'  # Expected: true for connected devices
nft list chain ip nexusguard input  # Expected: rules with icmp, established, drop

Final Checklist

  • All "Must Have" present
  • All "Must NOT Have" absent
  • Device with active WG peer shows Online
  • Firewall INPUT chain drops non-allowed WG traffic
  • Rules persist across restart