chore: update submodule refs, clean up plans/evidence, update .gitignore
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

This commit is contained in:
datadunia
2026-06-07 23:53:15 +07:00
parent 281ac48d28
commit cbacfea7f2
43 changed files with 139 additions and 2 deletions
@@ -0,0 +1,487 @@
# 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
- [x] Device with active WG peer shows `is_active=true` in API
- [x] Device without WG handshake shows `is_active=false`
- [x] `ping 10.172.21.1` from WG peer → RTO (dropped)
- [x] TCP 8080 from WG peer → works (API access)
- [x] UDP 51820 from WG peer → works (WG tunnel)
- [x] Non-WG traffic → unaffected (Docker, SSH, etc.)
- [x] 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
- [x] 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**:
- [x] `go build -tags dev ./...` passes
- [x] `GetPeerHandshakes()` returns non-empty map when peers are connected
- [x] 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`
- [x] 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**:
- [x] `go build -tags dev ./...` passes
- [x] Device with active WG peer → `is_active=true` in DB after SyncToDB cycle
- [x] Device without WG handshake → `is_active=false`
- [x] 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`
- [x] 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**:
- [x] `go build -tags dev ./...` passes
- [x] `nft list chain ip nexusguard input` shows all 5 rules after server start
- [x] 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`
- [x] 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**:
- [x] `go build -tags dev ./...` passes locally
- [x] `npm run build` passes locally
- [x] Production server running with new code
- [x] `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
- [x] 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**:
- [x] All 7 acceptance tests pass
- [x] 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.
- [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. Check evidence files exist. Compare deliverables against plan.
Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT`
- [x] F2. **Code Quality Review** — `unspecified-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`
- [x] F3. **Real Manual QA** — `unspecified-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`
- [x] F4. **Scope Fidelity Check** — `deep`
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
```bash
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
- [x] All "Must Have" present
- [x] All "Must NOT Have" absent
- [x] Device with active WG peer shows Online
- [x] Firewall INPUT chain drops non-allowed WG traffic
- [x] Rules persist across restart