diff --git a/.sisyphus/plans/docker-swagger-fix.md b/.sisyphus/plans/docker-swagger-fix.md index 3441661..b661321 100644 --- a/.sisyphus/plans/docker-swagger-fix.md +++ b/.sisyphus/plans/docker-swagger-fix.md @@ -37,9 +37,9 @@ Root cause: `apps/server-core/.gitignore` (lines 40-43) ignores `docs/docs.go`, Make Docker build succeed with Swagger docs generated during build process. ### Must Have -- [ ] `apps/server-core/Dockerfile` runs `swag init` before `go build` -- [ ] `swag init` uses pinned CLI version matching `go.mod` (`v1.16.6`) -- [ ] `docker compose build server-core` passes +- [x] `apps/server-core/Dockerfile` runs `swag init` before `go build` ✅ +- [x] `swag init` uses pinned CLI version matching `go.mod` (`v1.16.6`) ✅ +- [ ] `docker compose build server-core` passes (requires Docker host) ### Must NOT Have - Do NOT remove swagger files from `.gitignore` @@ -56,7 +56,7 @@ Single task, no waves needed. ## TODOs -- [ ] 1. Fix Dockerfile — Add `swag init` in builder stage +- [x] 1. Fix Dockerfile — Add `swag init` in builder stage (ALREADY APPLIED — Dockerfile line 7 sudah ada `swag init`) **What to do**: - Edit `apps/server-core/Dockerfile` @@ -121,9 +121,9 @@ Single task, no waves needed. ## Final Verification -- [ ] F1. **Verify Docker Build** — Run `docker build` and confirm exit 0 -- [ ] F2. **Verify Swagger** — Start container, `curl http://localhost:8080/swagger/index.html` returns 200 -- [ ] F3. **Verify Clean Environment** — Simulate fresh clone + build +- [x] F1. **Verify Dockerfile** ✅ — `swag init` present on line 7 with correct version `v1.16.6` +- [ ] F2. **Verify Docker Build** — Requires Docker host (runs `swag init` then `go build`) +- [ ] F3. **Verify Swagger** — Requires running container --- diff --git a/.sisyphus/plans/wg-vpn-fix.md b/.sisyphus/plans/wg-vpn-fix.md new file mode 100644 index 0000000..8d2fdcd --- /dev/null +++ b/.sisyphus/plans/wg-vpn-fix.md @@ -0,0 +1,1357 @@ +# WireGuard VPN Functional Fix — End-to-End + +## TL;DR + +> **Quick Summary**: Fix 4 blocking gaps that prevent WireGuard VPN from functioning end-to-end: server wg0 has no IP, no peers are synced to kernel interface, no NAT/masquerade, and device-agent TUN has no IP. Full fix across 10 files in server-core and device-agent. + +> **Deliverables**: +> - Server wg0 gets `InterfaceAddress` from DB applied (`ip addr add`) +> - `WgManager.SyncPeers()` replaces ALL peers on wg0 atomically +> - nftables masquerade + `ip_forward=1` on `Up()` +> - All device create/delete/suspend/provision actions sync peers +> - Device-agent assigns `InternalIP` to TUN device +> - Unit + integration tests for the new logic + +> **Estimated Effort**: Medium +> **Parallel Execution**: YES — 4 waves +> **Critical Path**: Interface types → Linux impl → Handler wiring → Device-agent + +--- + +## Context + +### Original Request +WireGuard VPN tidak berfungsi end-to-end. Tombol Turn ON/OFF di dashboard tidak terkoneksi dengan Menu Nodes, dan peers tidak bisa saling terkoneksi karena 4 gap kritis. + +### Interview Summary +**Key Discussions**: +- **Scope**: ALL gaps — server IP, peer management, NAT, device-agent IP +- **Test strategy**: Tests-after with unit + integration tests +- **NAT interface**: Auto-detect via `ip route show default`, manual override via `WG_NAT_INTERFACE` env var +- **Peer sync design**: `SyncPeers()` (replace-all), not individual add/remove — atomic safety + +### Research Findings +- `wgmanager_linux.go:Up()` creates wg0 interface but NO IP assignment, NO routes, NO NAT +- `WgManager` interface has only `GetStatus()`, `Up()`, `Down()` — no peer management +- `api/devices.go`, `api/peers.go`, `api/provisioning.go` create devices but NEVER add peers to wg0 kernel +- `device-agent/main.go` receives `InternalIP` from provisioning but only logs it +- `models.WgServer` already has `InterfaceAddress`, `IPPoolCIDR` stored in DB but never applied + +--- + +## Work Objectives + +### Core Objective +WireGuard VPN berfungsi penuh: server wg0 punya IP, peers tersync ke kernel, NAT/forwarding aktif, device-agent punya IP di TUN. + +### Concrete Deliverables +- File: `internal/wgmanager/manager.go` — expanded interface + new types +- File: `internal/wgmanager/wgmanager_linux.go` — IP assignment, SyncPeers, NAT +- File: `internal/wgmanager/wgmanager_stub.go` — match new interface +- Files: `api/wg.go`, `api/devices.go`, `api/peers.go`, `api/provisioning.go` — handler integration +- File: `main.go` — inject wgMgr to new handlers +- Files: `device-agent/internal/tunnel/wireguard.go`, `device-agent/main.go` — agent IP assignment +- New test files: `internal/wgmanager/*_test.go`, `api/devices_test.go` (extend), `api/wg_test.go` + +### Definition of Done +- [x] `make test` passes (all existing + new tests) ✅ +- [ ] `npm run build` passes (dashboard) — requires submodule init +- [x] Server wg0 gets `ip addr add dev wg0` on Up() ✅ +- [x] nftables masquerade rule + `ip_forward=1` on Up() ✅ +- [x] Creating a device → `SyncPeers()` called → peers synced to kernel (tested via mock) ✅ +- [x] Suspending a device → `SyncPeers()` called → peer removed from query (tested via mock) ✅ +- [x] Deleting a device → `SyncPeers()` called → peer removed (tested via mock) ✅ +- [x] Provisioning via agent → sync called after tx.Commit ✅ +- [x] Turn Off (Down) → wg0 removed via `ip link del` ✅ +- [x] Turn On (Up) → all active peers restored via `SyncLocalPeers()` after `Up()` ✅ + +### Must Have +- Server wg0 gets InterfaceAddress assigned from DB +- Peers are synced to kernel wg0 on create/delete/suspend/provision +- NAT masquerade + IP forwarding enabled when wg is up +- Device-agent assigns InternalIP to TUN device +- All existing tests continue to pass + +### Must NOT Have (Guardrails) +- Do NOT modify `shared/crypto/encryptor.go` (duplicated, known debt) +- Do NOT change existing test patterns (sqlite in-memory, mock middleware) +- Do NOT touch heartbeat/redis, firewall nftables init, or IPAM allocation logic +- Do NOT refactor handler auth duplication +- Do NOT execute `nft flush table` anywhere +- Do NOT log encryption keys or provisioning tokens + +--- + +## Verification Strategy + +> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. + +### Test Decision +- **Infrastructure exists**: YES (Go tests, sqlite in-memory) +- **Automated tests**: Tests-after +- **Framework**: Go `testing` package +- **Stub tests**: Unit test `wgmanager_stub.go` for interface contract +- **Linux tests**: Skipped via build tags; verified via QA scenarios on Linux infra +- **Handler tests**: Integration tests with mocked `WgManager` + +### QA Policy +Every task MUST include agent-executed QA scenarios. Evidence saved to `.sisyphus/evidence/task-{N}-{scenario}.{ext}`. + +- **Backend Go**: `go test -tags dev ./... -v -count=1` +- **Linux-only code**: Run `go build -tags linux` to verify compilation +- **nftables/WG**: tmux interactive_bash on Linux container + +--- + +## Execution Strategy + +### Parallel Execution Waves + +``` +Wave 1 (Types + Interface — foundation): +├── Task 1: Update WgManager interface — add PeerConfig, UpConfig, SyncPeers(), new Up() sig +├── Task 2: Update wgmanager_stub.go — match new interface +├── Task 3: Dashboard.vue frontend sync fix (already done) +└── Task 4: Add helpers.go — syncDevicePeers(), defaultRouteInterface() + +Wave 2 (Linux implementation — core engine): +├── Task 5: wgmanager_linux.go — IP address assignment on Up() +├── Task 6: wgmanager_linux.go — SyncPeers() implementation +├── Task 7: wgmanager_linux.go — nftables masquerade + ip_forward on Up() +└── Task 8: wgmanager_linux.go — NAT cleanup on Down() + +Wave 3 (Handler integration — wire everything): +├── Task 9: api/wg.go — pass InterfaceAddress/PoolCIDR, sync peers after Up +├── Task 10: api/devices.go — add WgManager dep, sync peers on create/delete/suspend +├── Task 11: api/peers.go — add WgManager dep, sync peers on create +├── Task 12: api/provisioning.go — add WgManager dep, sync peers on provision +└── Task 13: main.go — wire wgMgr to handlers + +Wave 4 (Device-agent — client-side fix): +├── Task 14: tunnel/wireguard.go — add internalIP param, ip addr add +└── Task 15: main.go — pass cfg.InternalIP to StartStealthTunnel() + +Wave 5 (Tests + QA — verification): +├── Task 16: Unit tests for wgmanager (stub interface contract) +├── Task 17: Integration tests — handler Create/Delete/Suspend peer sync flow +└── Task 18: Manual QA — full end-to-end verification on Linux + +Critical Path: Task 1 → Task 5/6 → Task 9 → Task 10/11/12 → Task 13 → verify +``` + +--- + +## TODOs + +> Implementation + Test = ONE Task. Every task MUST have QA Scenarios. + +- [x] 1. Update `WgManager` Interface — Add Types + Methods + + **What to do**: + - In `internal/wgmanager/manager.go`, add these types: + ```go + type PeerConfig struct { + PublicKey string + PresharedKey string + AllowedIPs string // comma-separated CIDRs + } + + type UpConfig struct { + ListenPort int + PrivateKeyHex string + InterfaceAddress string // e.g., "10.0.0.1/24" + PoolCIDR string // e.g., "10.0.0.0/24" + } + ``` + - Change `Up(listenPort int, privateKeyHex string) error` → `Up(cfg UpConfig) error` + - Add `SyncPeers(peers []PeerConfig) error` to the `WgManager` interface + - Add `sync.Mutex` field comment (will be implemented in linux/stub) + - Preserve all existing method signatures (`GetStatus`, `Down`) + + **Must NOT do**: + - Do NOT change `WgStatus` struct + - Do NOT modify existing `Down()` or `GetStatus()` signatures + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Simple type definitions and interface changes, no complex logic + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 2, 4) + - **Blocks**: Tasks 5, 6, 7, 8, 9 + - **Blocked By**: None (start immediately) + + **References**: + - `internal/wgmanager/manager.go:18-22` — Current interface to extend + - `internal/wgmanager/wgmanager_linux.go:14-20` — Current struct to see deviceName field + - `internal/wgmanager/wgmanager_stub.go:5-9` — Current stub struct + + **Acceptance Criteria**: + - [ ] `go build -tags dev ./internal/wgmanager/...` passes + - [ ] All types (`PeerConfig`, `UpConfig`) exported and usable + - [ ] Interface has `SyncPeers([]PeerConfig) error` method + - [ ] `Up()` signature changed to accept `UpConfig` + + **QA Scenarios**: + ``` + Scenario: Interface compiles with new types + Tool: Bash + Preconditions: Go toolchain installed + Steps: + 1. cd apps/server-core + 2. go vet -tags dev ./internal/wgmanager/... + Expected Result: Exit code 0, no errors + Evidence: .sisyphus/evidence/task-1-interface-compile.txt + ``` + + **Commit**: YES + - Message: `feat(wgmanager): add PeerConfig, UpConfig types and SyncPeers method` + - Files: `apps/server-core/internal/wgmanager/manager.go` + +- [x] 2. Update `wgmanager_stub.go` — Match New Interface + + **What to do**: + - Update `StubWgManager.Up()` signature to accept `UpConfig` + - Add no-op `SyncPeers(peers []PeerConfig) error` returning nil + - Keep all existing no-op behavior + + **Must NOT do**: + - Do NOT add any real logic (stub is for non-Linux builds) + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Trivial stub method updates + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 4) + - **Blocks**: Tasks 5-13 + - **Blocked By**: Task 1 + + **References**: + - `internal/wgmanager/wgmanager_stub.go` — Full file to modify + - Updated `manager.go` from Task 1 — interface contract to match + + **Acceptance Criteria**: + - [ ] `go build -tags dev ./internal/wgmanager/...` passes + - [ ] `go build -tags '!linux' ./internal/wgmanager/...` passes (stub build) + - [ ] `StubWgManager` implements all methods of `WgManager` + + **QA Scenarios**: + ``` + Scenario: Stub compiles on non-Linux + Tool: Bash + Preconditions: Go toolchain installed + Steps: + 1. cd apps/server-core + 2. go build -tags '!linux' ./internal/wgmanager/... + Expected Result: Exit code 0 + Evidence: .sisyphus/evidence/task-2-stub-compile.txt + ``` + + **Commit**: YES (group with task 1) + - Files: `apps/server-core/internal/wgmanager/wgmanager_stub.go` + +- [x] 3. Dashboard.vue — Sync WG Toggle with Nodes Menu Status (ALREADY FIXED) + + **What to do**: Verify the fix is already applied. The fix added `fetchServers()` call after `handleToggleWg()` and in the 10s polling interval. + + **Must NOT do**: Do NOT re-apply or modify + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Verification task only + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 + - **Blocks**: None + - **Blocked By**: None + + **References**: + - `apps/dashboard-ui/src/views/Dashboard.vue:52-95` — Already fixed + + **Acceptance Criteria**: + - [ ] `Dashboard.vue` imports `fetchServers` and calls it after toggle + in interval + + **QA Scenarios**: None (verification only) + + **Commit**: NO (already applied) + +- [x] 4. Add `syncDevicePeers()` Helper and `defaultRouteInterface()` Helper + + **What to do**: + - In `api/helpers.go` (or new file `api/peer_sync.go`), add: + ```go + package api + + type PeerSyncer struct { + db *gorm.DB + mgr wgmanager.WgManager + } + + func NewPeerSyncer(db *gorm.DB, mgr wgmanager.WgManager) *PeerSyncer { + return &PeerSyncer{db: db, mgr: mgr} + } + + // SyncLocalPeers reads all active, non-suspended devices for the + // Local Primary Node and syncs them to the kernel wg0 interface. + // No-op if wg0 is not running. + func (s *PeerSyncer) SyncLocalPeers() { + status, err := s.mgr.GetStatus() + if err != nil || !status.IsRunning { + return // wg0 is not running, no point syncing + } + + var localServer models.WgServer + if err := s.db.Where("name = ?", "Local Primary Node").First(&localServer).Error; err != nil { + return + } + + var devices []models.Device + s.db.Where("wg_server_id = ? AND is_suspended = ? AND internal_ip IS NOT NULL", + localServer.ID, false).Find(&devices) + + var peers []wgmanager.PeerConfig + for _, d := range devices { + allowedIPs := *d.InternalIP + "/32" + if d.AllowInternet { + allowedIPs = "0.0.0.0/0" + } + if d.EndpointAllowedIPs != "" { + allowedIPs = d.EndpointAllowedIPs + } + peers = append(peers, wgmanager.PeerConfig{ + PublicKey: d.PublicKey, + PresharedKey: d.PresharedKey, + AllowedIPs: allowedIPs, + }) + } + + if err := s.mgr.SyncPeers(peers); err != nil { + log.Printf("ERROR: Failed to sync peers: %v", err) + } + } + ``` + - In `internal/wgmanager/manager.go`, add: + ```go + // DefaultRouteInterface returns the interface name of the default route. + // Used for NAT masquerade. Falls back to "eth0" if detection fails. + func DefaultRouteInterface() string { + if env := os.Getenv("WG_NAT_INTERFACE"); env != "" { + return env + } + out, err := exec.Command("sh", "-c", "ip route show default | awk '{print $5}'").Output() + if err != nil || len(out) == 0 { + return "eth0" + } + return strings.TrimSpace(string(out)) + } + ``` + - Import `log`, `os`, `os/exec`, `strings` as needed + + **Must NOT do**: + - Do NOT add any peer sync logic related to external (non-local) servers + - Do NOT modify existing handlers' constructor signatures yet + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Single helpers file with straightforward logic + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 2) + - **Blocks**: Tasks 9, 10, 11, 12, 13 + - **Blocked By**: Task 1 (needs PeerConfig type) + + **References**: + - `api/helpers.go` — Existing helper functions location + - `api/devices.go:86-117` — Device creation pattern to understand AllowedIPs logic + - `api/peers.go:120-126` — AllowedIPs assignment pattern + - `api/servers.go:50-53` — "Local Primary Node" name check pattern + - `internal/wgmanager/manager.go:UpConfig` (from Task 1) — PoolCIDR for masquerade + + **Acceptance Criteria**: + - [ ] `go build -tags dev ./...` passes + - [ ] `PeerSyncer` exported and constructable + - [ ] `DefaultRouteInterface()` returns "eth0" or detected interface + - [ ] `SyncLocalPeers()` is safe to call when wg0 is down (no-op) + + **QA Scenarios**: + ``` + Scenario: DefaultRouteInterface returns non-empty string + Tool: Bash + Preconditions: Linux machine with default route + Steps: + 1. cd apps/server-core + 2. go test -tags dev -run TestDefaultRouteInterface -v ./internal/wgmanager/ + Expected Result: Test passes, interface name returned + Evidence: .sisyphus/evidence/task-4-route-interface.txt + ``` + + **Commit**: YES + - Message: `feat(api): add PeerSyncer helper and DefaultRouteInterface utility` + - Files: `apps/server-core/api/peer_sync.go`, `apps/server-core/internal/wgmanager/manager.go` + +--- + +## TODOs (continued) + +- [x] 5. `wgmanager_linux.go` — IP Address Assignment on `Up()` + + **What to do**: + - Update `func (m *LinuxWgManager) Up(cfg UpConfig) error` to: + 1. Keep existing interface creation + wgctrl config + `ip link set up` (unchanged) + 2. NEW: After interface is up, assign IP address: + ```go + if cfg.InterfaceAddress != "" { + // Remove existing IP if any, then assign + exec.Command("ip", "addr", "flush", "dev", m.deviceName).Run() + if err := exec.Command("ip", "addr", "add", cfg.InterfaceAddress, "dev", m.deviceName).Run(); err != nil { + return fmt.Errorf("failed to assign IP %s to %s: %w", cfg.InterfaceAddress, m.deviceName, err) + } + } + ``` + 3. Add `sync.Mutex` usage: + ```go + type LinuxWgManager struct { + deviceName string + mu sync.Mutex + } + ``` + 4. Lock/unlock in `Up()`, `Down()`, `GetStatus()`, `SyncPeers()` + - Import `sync` package + + **Must NOT do**: + - Do NOT change the interface creation logic (kernel module → wireguard-go fallback) + - Do NOT add NAT or ip_forward here (separate task) + - Do NOT add SyncPeers logic here (separate task) + + **Recommended Agent Profile**: + - **Category**: `deep` + - Reason: Linux kernel interface operations + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: NO (sequential within Wave 2) + - **Parallel Group**: Wave 2 (sequential: 5 → 6 → 7 → 8) + - **Blocks**: Tasks 6, 7, 8, 9 + - **Blocked By**: Task 1 (needs UpConfig type) + + **References**: + - `internal/wgmanager/wgmanager_linux.go:54-99` — Current Up() implementation + - `models.WgServer.InterfaceAddress`, `IPPoolCIDR` — Values passed via UpConfig + + **Acceptance Criteria**: + - [ ] `go build -tags linux ./internal/wgmanager/...` passes + - [ ] Mutex used in all exported methods + - [ ] `ip addr flush` + `ip addr add` called after `ip link set up` + + **QA Scenarios**: + ``` + Scenario: Linux build compiles + Tool: Bash + Steps: go build -tags linux ./internal/wgmanager/... + Expected: exit 0 + Evidence: .sisyphus/evidence/task-5-linux-build.txt + ``` + + **Commit**: YES (group with Tasks 6, 7, 8) + +- [x] 6. `wgmanager_linux.go` — `SyncPeers()` Implementation + + **What to do**: + - Implement `func (m *LinuxWgManager) SyncPeers(peers []PeerConfig) error`: + ```go + func (m *LinuxWgManager) SyncPeers(peers []PeerConfig) error { + m.mu.Lock() + defer m.mu.Unlock() + + client, err := wgctrl.New() + if err != nil { + return fmt.Errorf("failed to open wgctrl: %w", err) + } + defer client.Close() + + var wgPeers []wgtypes.PeerConfig + for _, p := range peers { + pubKey, err := wgtypes.ParseKey(p.PublicKey) + if err != nil { + log.Printf("WARNING: Skipping invalid public key: %v", err) + continue + } + var psk wgtypes.Key + if p.PresharedKey != "" { + if k, err := wgtypes.ParseKey(p.PresharedKey); err == nil { + psk = k + } + } + var allowedIPs []net.IPNet + for _, cidr := range strings.Split(p.AllowedIPs, ",") { + cidr = strings.TrimSpace(cidr) + if cidr == "" { continue } + if _, ipNet, err := net.ParseCIDR(cidr); err == nil { + allowedIPs = append(allowedIPs, *ipNet) + } + } + wgPeers = append(wgPeers, wgtypes.PeerConfig{ + PublicKey: pubKey, + PresharedKey: &psk, + AllowedIPs: allowedIPs, + ReplaceAllowedIPs: true, + }) + } + + cfg := wgtypes.Config{ + ReplacePeers: true, // atomic replacement + Peers: wgPeers, + } + return client.ConfigureDevice(m.deviceName, cfg) + } + ``` + - Add imports: `net`, `strings`, `log` + + **Must NOT do**: + - Do NOT add individual AddPeer/RemovePeer (SyncPeers replaces atomically) + + **Recommended Agent Profile**: + - **Category**: `deep` + - Reason: wgctrl netlink API, key parsing, CIDR handling + - **Skills**: `[]` + + **Parallelization**: NO (Wave 2, after Task 5) + - **Blocked By**: Task 1 (PeerConfig type), Task 5 (mutex in struct) + + **Acceptance Criteria**: + - [ ] `go build -tags linux ./internal/wgmanager/...` passes + - [ ] Empty peers slice removes all peers (clean interface) + - [ ] Comma-separated AllowedIPs parsed correctly + - [ ] Mutex locked during entire operation + + **QA Scenarios**: + ``` + Scenario: SyncPeers compiles + Tool: Bash + Steps: go build -tags linux ./internal/wgmanager/... + Expected: exit 0 + Evidence: .sisyphus/evidence/task-6-syncpeers-compile.txt + ``` + + **Commit**: YES (group with Tasks 5, 7, 8) + +- [x] 7. `wgmanager_linux.go` — nftables Masquerade + IP Forwarding + + **What to do**: + - Extend `Up(cfg UpConfig)` after IP assignment: + ```go + // Enable IP forwarding + exec.Command("sysctl", "-w", "net.ipv4.ip_forward=1").Run() + + // Setup nftables masquerade + if cfg.PoolCIDR != "" { + exec.Command("sh", "-c", + "nft add table ip nat 2>/dev/null; "+ + "nft add chain ip nat postrouting { type nat hook postrouting priority 100 \\; \\} 2>/dev/null").Run() + + natIface := DefaultRouteInterface() + checkCmd := fmt.Sprintf("nft -a list chain ip nat postrouting 2>/dev/null | grep -q 'oif \"%s\".*masquerade'", natIface) + if exec.Command("sh", "-c", checkCmd).Run() != nil { + rule := fmt.Sprintf("nft add rule ip nat postrouting oif %q ip saddr %s masquerade", natIface, cfg.PoolCIDR) + exec.Command("sh", "-c", rule).Run() + } + } + ``` + - NAT setup failures are non-fatal (log warning) + + **Must NOT do**: + - Do NOT `nft flush table` + - Do NOT fail Up() on NAT setup failure + + **Recommended Agent Profile**: + - **Category**: `deep` + - Reason: nftables nat, Linux networking + - **Skills**: `[]` + + **Parallelization**: NO (Wave 2, after Task 6) + - **Blocked By**: Task 4 (DefaultRouteInterface), Task 5 (Up() extension) + + **Acceptance Criteria**: + - [ ] Masquerade rule added for PoolCIDR on default route interface + - [ ] ip_forward=1 enabled + - [ ] Calling Up() twice doesn't duplicate masquerade rules + + **QA Scenarios**: + ``` + Scenario: No duplicate NAT rules + Tool: Bash (Linux) + Preconditions: wg0 up with NAT + Steps: Run Up() twice, count masquerade rules + Expected: Exactly 1 masquerade rule + Evidence: .sisyphus/evidence/task-7-nat-dedup.txt + ``` + + **Commit**: YES (group with Tasks 5, 6, 8) + +- [x] 8. `wgmanager_linux.go` — NAT Cleanup on `Down()` + + **What to do**: + - Extend `Down()` to remove masquerade rule before deleting interface: + ```go + func (m *LinuxWgManager) Down() error { + m.mu.Lock() + defer m.mu.Unlock() + + // Remove masquerade rule (non-fatal) + out, _ := exec.Command("sh", "-c", + "nft -a list chain ip nat postrouting 2>/dev/null | awk '/masquerade/ {print $NF}'").Output() + if handle := strings.TrimSpace(string(out)); handle != "" { + exec.Command("sh", "-c", fmt.Sprintf("nft delete rule ip nat postrouting handle %s", handle)).Run() + } + + // Delete WG interface + return exec.Command("ip", "link", "del", "dev", m.deviceName).Run() + } + ``` + - NAT cleanup failures are non-fatal (logged, not returned) + + **Must NOT do**: + - Do NOT disable ip_forward (harmless, other services may depend on it) + - Do NOT delete entire nat table + + **Recommended Agent Profile**: + - **Category**: `deep` + - Reason: nftables handle-based deletion, error-resilient cleanup + - **Skills**: `[]` + + **Parallelization**: NO (Wave 2, after Task 7) + + **Acceptance Criteria**: + - [ ] Down() removes masquerade rule by handle + - [ ] Down() deletes wg0 interface + - [ ] All cleanup failures logged, not fatal + + **QA Scenarios**: + ``` + Scenario: Down() cleans NAT rule + Tool: Bash (Linux) + Preconditions: wg0 up with NAT + Steps: Down() → check nft list chain ip nat postrouting + Expected: No masquerade rule + Evidence: .sisyphus/evidence/task-8-nat-cleanup.txt + ``` + + **Commit**: YES (group with Tasks 5, 6, 7) + +--- + +## TODOs (continued) + +- [x] 9. `api/wg.go` — Pass InterfaceAddress/PoolCIDR and Sync Peers After Up + + **What to do**: + - Modify `WgHandler` to accept `*PeerSyncer` in constructor or store `*gorm.DB` + `wgmanager.WgManager` directly + - Actually: Modify `WgHandler` to include `syncer *PeerSyncer` field: + ```go + type WgHandler struct { + db *gorm.DB + mgr wgmanager.WgManager + syncer *PeerSyncer + } + + func NewWgHandler(db *gorm.DB, mgr wgmanager.WgManager, syncer *PeerSyncer) *WgHandler { + return &WgHandler{db: db, mgr: mgr, syncer: syncer} + } + ``` + - In `Up()` handler: build `UpConfig` from DB model and pass to `mgr.Up()`: + ```go + func (h *WgHandler) Up(c *gin.Context) { + // ... existing admin check ... + + var wgServer models.WgServer + if err := h.db.First(&wgServer).Error; err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": "No WireGuard node configured"}) + return + } + + // Parse InterfaceAddress from DB (e.g., "10.0.0.1" → "10.0.0.1/24") + interfaceAddr := "" + if wgServer.InterfaceAddress != "" && wgServer.IPPoolCIDR != "" { + _, ipnet, _ := net.ParseCIDR(wgServer.IPPoolCIDR) + if ipnet != nil { + ones, _ := ipnet.Mask.Size() + interfaceAddr = fmt.Sprintf("%s/%d", wgServer.InterfaceAddress, ones) + } + } + + cfg := wgmanager.UpConfig{ + ListenPort: wgServer.ListenPort, + PrivateKeyHex: privHex, + InterfaceAddress: interfaceAddr, + PoolCIDR: wgServer.IPPoolCIDR, + } + + if err := h.mgr.Up(cfg); err != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()}) + return + } + + // Sync all active peers after bringing up + h.syncer.SyncLocalPeers() + + c.JSON(http.StatusOK, gin.H{"status": "up", "public_key": wgServer.PublicKey}) + } + ``` + - In `Down()` handler: after `mgr.Down()`, peers are automatically removed (interface deleted) — no extra sync needed + - Note: The existing privKey parsing logic stays the same + + **Must NOT do**: + - Do NOT change `Status()` or `Down()` handlers except for wgHandler struct + - Do NOT modify `models.WgServer` or `wgmanager.WgManager` interface + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - Reason: Handler logic changes, dependency injection pattern, CIDR parsing + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 3 (sequential with 10, 11, 12, 13) + - **Blocks**: Tasks 10, 11, 12, 13 + - **Blocked By**: Tasks 4 (PeerSyncer), 5/6 (UpConfig, SyncPeers impl) + + **References**: + - `api/wg.go:14-77` — Full file to modify + - `api/wg.go:38-63` — Current Up() handler + - `net.ParseCIDR()`, `net.IPNet.Mask.Size()` — CIDR prefix detection + - `internal/wgmanager/manager.go:UpConfig` — Struct to build + + **Acceptance Criteria**: + - [ ] `go build -tags dev ./...` passes + - [ ] `Up()` handler builds `UpConfig` with InterfaceAddress + PoolCIDR from DB + - [ ] IP address prefix derived from IPPoolCIDR (e.g., /24) + - [ ] `SyncLocalPeers()` called after wg is up + - [ ] InterfaceAddress is empty string when not configured (Backward compatible) + + **QA Scenarios**: + ``` + Scenario: Up handler builds correct UpConfig + Tool: Bash (go test) + Steps: go test -tags dev -run TestWgHandler_Up -v ./api/ + Expected: Config built with correct InterfaceAddress and PoolCIDR + Evidence: .sisyphus/evidence/task-9-upconfig.txt + ``` + + **Commit**: YES (group with Tasks 10, 11, 12, 13) + - Message: `feat(api): integrate wgmanager into handlers — IP, peer sync, NAT` + - Files: All Wave 3 files + +- [x] 10. `api/devices.go` — Add WgManager Dependency, Sync Peers + + **What to do**: + - Add `syncer *PeerSyncer` field to `DevicesHandler`: + ```go + type DevicesHandler struct { + db *gorm.DB + fw firewall.NetManager + ipam *ipam.Manager + syncer *PeerSyncer + } + + func NewDevicesHandler(db *gorm.DB, ipamMgr *ipam.Manager, fw firewall.NetManager, syncer *PeerSyncer) *DevicesHandler { + return &DevicesHandler{db: db, ipam: ipamMgr, fw: fw, syncer: syncer} + } + ``` + - After `Create()` successfully creates a device → call `h.syncer.SyncLocalPeers()` + - Place right after `h.db.Create(&device).Error` check and before `c.JSON(201, ...)` + - After `Delete()` successfully deletes → call `h.syncer.SyncLocalPeers()` + - Place after `h.db.Delete(&device).Error` check + - After `toggleSuspension()` saves state → call `h.syncer.SyncLocalPeers()` + - Place after `h.db.Save(&device).Error` check + - All sync calls are fire-and-forget on best-effort basis (no error return needed) + + **Must NOT do**: + - Do NOT change any existing handler logic (flow, error handling, responses) + - Do NOT add sync for non-local server peers (SyncLocalPeers only handles Local Primary Node) + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - Reason: Dependency injection into existing struct, adding side-effect calls + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: NO + - **Parallel Group**: Wave 3 (with Tasks 9, 11, 12, 13) + - **Blocks**: Task 13 (main.go wiring) + - **Blocked By**: Task 4 (PeerSyncer), Tasks 5-8 (wgmanager impl) + + **References**: + - `api/devices.go:18-24` — Struct definition + constructor + - `api/devices.go:119` — After Create() success (line ~119) + - `api/devices.go:245` — After Delete() success (line ~245) + - `api/devices.go:298` — After Save() in toggleSuspension (line ~298) + + **Acceptance Criteria**: + - [ ] `go build -tags dev ./...` passes + - [ ] `DevicesHandler` has `syncer` field + - [ ] `SyncLocalPeers()` called after Create, Delete, Suspend, Unsuspend + - [ ] All existing tests pass + + **QA Scenarios**: Same build verification as Task 9 group commit + + **Commit**: YES (group with 9, 11, 12, 13) + +- [x] 11. `api/peers.go` — Add WgManager Dependency, Sync Peers + + **What to do**: + - Add `syncer *PeerSyncer` to `PeersHandler`: + ```go + type PeersHandler struct { + db *gorm.DB + ipam *ipam.Manager + fw firewall.NetManager + syncer *PeerSyncer + } + + func NewPeersHandler(db *gorm.DB, ipamMgr *ipam.Manager, fw firewall.NetManager, syncer *PeerSyncer) *PeersHandler { + return &PeersHandler{db: db, ipam: ipamMgr, fw: fw, syncer: syncer} + } + ``` + - After `CreatePeer()` successfully creates device → call `h.syncer.SyncLocalPeers()` + - Place after `h.db.Create(&device).Error` check, before `c.JSON(201, ...)` + + **Must NOT do**: + - Do NOT modify `GetConfig()` or `GetQR()` handlers (no DB state changes) + - Do NOT add sync for non-local servers + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - Reason: Same pattern as Task 10 + - **Skills**: `[]` + + **Parallelization**: Wave 3 (with 9, 10, 12, 13) + - **Blocked By**: Task 4 + + **References**: + - `api/peers.go:18-24` — Struct + constructor + - `api/peers.go:104-107` — After successful device creation + + **Acceptance Criteria**: + - [ ] Build passes + - [ ] `SyncLocalPeers()` called after CreatePeer + + **Commit**: YES (group with 9, 10, 12, 13) + +- [x] 12. `api/provisioning.go` — Add WgManager Dependency, Sync Peers + + **What to do**: + - Add `syncer *PeerSyncer` to `ProvisioningHandler`: + ```go + type ProvisioningHandler struct { + db *gorm.DB + ipam *ipam.Manager + config *config.Config + syncer *PeerSyncer + } + + func NewProvisioningHandler(db *gorm.DB, ipamMgr *ipam.Manager, cfg *config.Config, syncer *PeerSyncer) *ProvisioningHandler { + return &ProvisioningHandler{db: db, ipam: ipamMgr, config: cfg, syncer: syncer} + } + ``` + - After `Provision()` successfully provisions device → call `h.syncer.SyncLocalPeers()` + - Place after `h.db.Commit()` at line ~122-123, before constructing response + + **Must NOT do**: + - Do NOT move sync before commit (only sync after DB is committed) + - Do NOT change the encryption/decryption flow + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - Reason: Same pattern, careful with tx.Commit ordering + - **Skills**: `[]` + + **Parallelization**: Wave 3 (with 9, 10, 11, 13) + - **Blocked By**: Task 4 + + **References**: + - `api/provisioning.go:17-24` — Struct + constructor + - `api/provisioning.go:122` — tx.Commit() location + + **Acceptance Criteria**: + - [ ] Build passes + - [ ] Sync happens AFTER tx.Commit() (not during transaction) + + **Commit**: YES (group with 9, 10, 11, 13) + +- [x] 13. `main.go` — Wire PeerSyncer to All Handlers + + **What to do**: + - Create PeerSyncer instance after wgMgr creation: + ```go + peerSyncer := api.NewPeerSyncer(db, wgMgr) + ``` + - Update all handler constructors to pass peerSyncer: + ```go + wgApiHandler := api.NewWgHandler(db, wgMgr, peerSyncer) + devicesHandler := api.NewDevicesHandler(db, ipamMgr, fw, peerSyncer) + peersHandler := api.NewPeersHandler(db, ipamMgr, fw, peerSyncer) + provHandler := api.NewProvisioningHandler(db, ipamMgr, cfg, peerSyncer) + ``` + - Keep `serversHandler := api.NewServersHandler(db, fw, wgMgr)` unchanged + + **Must NOT do**: + - Do NOT change `NewServersHandler` call (doesn't need peerSyncer) + - Do NOT change `authHandler`, `rulesHandler`, `usersHandler`, `hbHandler`, `shareHandler` + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Simple wiring changes, one-time refactor + - **Skills**: `[]` + + **Parallelization**: NO (Wave 3, after Tasks 9-12) + - **Blocked By**: Tasks 9, 10, 11, 12 (all constructor signatures changed) + + **References**: + - `main.go:151-161` — All handler constructor calls + - `main.go:155-156` — wgMgr and wgApiHandler creation (reference point) + + **Acceptance Criteria**: + - [ ] `go build -tags dev ./...` passes + - [ ] All handler constructors receive peerSyncer + - [ ] No unused variables or imports + + **QA Scenarios**: + ``` + Scenario: Full build passes + Tool: Bash + Steps: + 1. cd apps/server-core + 2. go build -tags dev ./... + Expected: exit 0 + Evidence: .sisyphus/evidence/task-13-build.txt + ``` + + **Commit**: YES (group with 9, 10, 11, 12) + +--- + +## TODOs (continued) + +- [x] 14. `device-agent/internal/tunnel/wireguard.go` — Add InternalIP Assignment + + **What to do**: + - Add `internalIP string` parameter to `StartStealthTunnel()`: + ```go + func (m *TunnelManager) StartStealthTunnel(interfaceName string, uapiConfig string, internalIP string) error { + ``` + - After `dev.Up()` succeeds and before returning, assign the IP to TUN: + ```go + // Assign InternalIP to the TUN device + if internalIP != "" { + // Use /32 prefix for point-to-point WireGuard + ipCIDR := internalIP + if !strings.Contains(internalIP, "/") { + ipCIDR = internalIP + "/32" + } + // ip addr add /32 dev + assignCmd := exec.Command("ip", "addr", "add", ipCIDR, "dev", interfaceName) + if err := assignCmd.Run(); err != nil { + // Non-fatal — tunnel works without IP, but routing won't + log.Printf("WARNING: Failed to assign IP %s to %s: %v", ipCIDR, interfaceName, err) + } + } + ``` + - Add imports: `os/exec`, `strings`, `log` + - Update existing callers of `StartStealthTunnel` — currently only `main.go` + + **Must NOT do**: + - Do NOT make IP assignment failure fatal (wg tunnel can still accept connections) + - Do NOT write IP to `/etc/wireguard/` (stealth invariant) + - Do NOT modify `ConvertToUAPI()` or `StopTunnel()` + - Do NOT modify `shared/crypto/` (known debt, duplicated) + + **Recommended Agent Profile**: + - **Category**: `deep` + - Reason: TUN device IP assignment via iproute2, stealth architecture constraints + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES (with Wave 3 — no file dependencies) + - **Parallel Group**: Wave 4 (with Task 15) + - **Blocks**: Task 15 (main.go needs updated signature) + - **Blocked By**: None + + **References**: + - `device-agent/internal/tunnel/wireguard.go:24-54` — Current StartStealthTunnel + - `device-agent/internal/tunnel/wireguard.go:56-69` — StopTunnel (no changes needed) + - `device-agent/main.go:38-41` — Caller to update + + **Acceptance Criteria**: + - [ ] `go build ./internal/tunnel/...` passes + - [ ] `StartStealthTunnel()` accepts `internalIP string` param + - [ ] `ip addr add` called with `/32` prefix when IP has no CIDR + - [ ] IP assignment failure is non-fatal (logged only) + - [ ] No files written to `/etc/wireguard/` + + **QA Scenarios**: + ``` + Scenario: Device-agent tunnel compiles + Tool: Bash + Steps: + 1. cd apps/device-agent + 2. go build ./... + Expected: exit 0 + Evidence: .sisyphus/evidence/task-14-agent-build.txt + ``` + + **Commit**: YES (group with Task 15) + - Message: `fix(device-agent): assign InternalIP to TUN device after tunnel up` + - Files: `apps/device-agent/internal/tunnel/wireguard.go`, `apps/device-agent/main.go` + +- [x] 15. `device-agent/main.go` — Pass `cfg.InternalIP` to `StartStealthTunnel()` + + **What to do**: + - Update the `onConnected` callback to pass the InternalIP: + ```go + onConnected := func(cfg *client.WireGuardConfig) { + log.Printf("Provisioning successful. Internal IP: %s\n", cfg.InternalIP) + + uapi := tunnel.ConvertToUAPI(cfg.PrivateKey, cfg.ServerPub, cfg.Endpoint, "0.0.0.0/0") + if err := tunMgr.StartStealthTunnel("nexusguard0", uapi, cfg.InternalIP); err != nil { + log.Fatalf("Failed to start tunnel: %v", err) + } + // ... + } + ``` + - Also update any reconnect/re-provision path that calls `StartStealthTunnel` + - Note: The `Reconnect` function calls `onConnected` when re-provisioning succeeds, so it's covered + + **Must NOT do**: + - Do NOT change `client.WireGuardConfig` struct (already has InternalIP) + - Do NOT change `Provision()` or `DecryptConfig()` (already return InternalIP) + - Do NOT log `cfg.InternalIP` twice + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Single parameter pass-through, trivial change + - **Skills**: `[]` + + **Parallelization**: Wave 4 (with Task 14) + - **Blocked By**: Task 14 (StartStealthTunnel signature changed) + + **References**: + - `device-agent/main.go:35-49` — onConnected callback + - `device-agent/internal/client/provisioning.go:28-34` — WireGuardConfig struct + - `device-agent/main.go:54-61` — Provision success → onConnected path + - `device-agent/main.go:58` — Reconnect → onConnected path + + **Acceptance Criteria**: + - [ ] `go build ./...` passes + - [ ] `cfg.InternalIP` passed to `StartStealthTunnel()` + - [ ] All paths (initial provision + reconnect) pass the IP + + **QA Scenarios**: + ``` + Scenario: Full agent build + Tool: Bash + Steps: + 1. cd apps/device-agent + 2. go build ./... + Expected: exit 0 + Evidence: .sisyphus/evidence/task-15-full-agent-build.txt + ``` + + **Commit**: YES (group with Task 14) + +--- + +## TODOs (continued) + +- [x] 16. Unit Tests — wgmanager Interface Contract + DefaultRouteInterface + + **What to do**: + - Create `internal/wgmanager/manager_test.go` (build tag `!linux`): + ```go + //go:build !linux + + package wgmanager + + import ( + "testing" + ) + + func TestStubImplementsInterface(t *testing.T) { + var _ WgManager = (*StubWgManager)(nil) + } + + func TestDefaultRouteInterface(t *testing.T) { + iface := DefaultRouteInterface() + if iface == "" { + t.Error("DefaultRouteInterface returned empty string") + } + } + ``` + - Create `internal/wgmanager/wgmanager_test.go` (build tag `linux`): + ```go + //go:build linux + + package wgmanager + + import ( + "testing" + ) + + func TestLinuxImplementsInterface(t *testing.T) { + var _ WgManager = (*LinuxWgManager)(nil) + } + + func TestUpConfigDefaults(t *testing.T) { + cfg := UpConfig{} + if cfg.ListenPort != 0 { + t.Error("UpConfig should have zero-value defaults") + } + } + + func TestPeerConfigDefaults(t *testing.T) { + p := PeerConfig{} + if p.PublicKey != "" { + t.Error("PeerConfig should have zero-value defaults") + } + } + ``` + - Verify `DefaultRouteInterface()`: + - On Linux: returns the actual default route interface + - On non-Linux: returns "eth0" (fallback) + - With `WG_NAT_INTERFACE` env set: returns that value + + **Must NOT do**: + - Do NOT add tests that require actual WireGuard kernel module (integration tests only) + - Do NOT mock wgctrl (Linux-only, tested in task 17) + + **Recommended Agent Profile**: + - **Category**: `quick` + - Reason: Simple compile-time interface checks + unit tests + - **Skills**: `[]` + + **Parallelization**: Wave 5 (with 17, 18) + - **Blocked By**: Tasks 1-4 (interface + helper types) + + **References**: + - `internal/wgmanager/manager.go` — Interface and types to test + - `internal/wgmanager/wgmanager_stub.go` — Stub implementation + - `internal/wgmanager/wgmanager_linux.go` — Linux implementation + + **Acceptance Criteria**: + - [ ] `go test -tags '!linux' ./internal/wgmanager/...` passes + - [ ] `go test -tags linux ./internal/wgmanager/...` passes (on Linux CI) + - [ ] Interface compliance verified at compile time + + **QA Scenarios**: + ``` + Scenario: All wgmanager unit tests pass + Tool: Bash + Steps: + 1. cd apps/server-core + 2. go test -tags dev -v ./internal/wgmanager/... + Expected: All tests pass + Evidence: .sisyphus/evidence/task-16-wgmanager-tests.txt + ``` + + **Commit**: YES + - Message: `test(wgmanager): add interface contract and DefaultRouteInterface tests` + - Files: `apps/server-core/internal/wgmanager/manager_test.go`, `apps/server-core/internal/wgmanager/wgmanager_test.go` + +- [x] 17. Integration Tests — Handler Peer Sync Flow + + **What to do**: + - Create `api/wg_test.go` or extend `api/devices_test.go`: + ```go + package api + + import ( + "testing" + // ... + ) + + // mockWgManager implements wgmanager.WgManager for testing + type mockWgManager struct { + lastPeers []wgmanager.PeerConfig + isRunning bool + } + + func (m *mockWgManager) GetStatus() (*wgmanager.WgStatus, error) { + return &wgmanager.WgStatus{IsRunning: m.isRunning}, nil + } + func (m *mockWgManager) Up(cfg wgmanager.UpConfig) error { return nil } + func (m *mockWgManager) Down() error { return nil } + func (m *mockWgManager) SyncPeers(peers []wgmanager.PeerConfig) error { + m.lastPeers = peers + return nil + } + ``` + - Test scenarios: + 1. **Create device → SyncPeers called with correct peer**: Create a device, verify `mockWgManager.lastPeers` contains the expected peer + 2. **Delete device → SyncPeers called**: Delete a device, verify peers list updated + 3. **Suspend device → SyncPeers called**: Suspend, verify peer removed from wg0 + 4. **Unsuspend device → SyncPeers called**: Unsuspend, verify peer re-added + 5. **Provision device → SyncPeers called**: Provision, verify peer added + 6. **SyncPeers not called when wg0 is down**: `isRunning: false`, verify SyncPeers not called + - Use existing `setupTestDB()` pattern + - Mock auth middleware (same as `auth_test.go`) + + **Must NOT do**: + - Do NOT test actual WireGuard kernel operations (Linux-only, requires root) + - Do NOT modify existing test patterns + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - Reason: Mock-based integration tests, multiple scenarios + - **Skills**: `[]` + + **Parallelization**: Wave 5 (with 16, 18) + - **Blocked By**: Tasks 9-13 (handler integration complete) + + **References**: + - `api/auth_test.go:18-25` — setupTestDB() pattern to follow + - `api/devices_test.go` — Existing device test patterns (if exists) + - `internal/wgmanager/manager.go` — WgManager interface to mock + + **Acceptance Criteria**: + - [ ] `go test -tags dev -v ./api/... -run TestPeerSync` passes + - [ ] All 6 test scenarios pass + - [ ] Mock WgManager captures peer state correctly + + **QA Scenarios**: + ``` + Scenario: All integration tests pass + Tool: Bash + Steps: + 1. cd apps/server-core + 2. go test -tags dev -v -run TestPeerSync ./api/... + Expected: All tests pass + Evidence: .sisyphus/evidence/task-17-integration-tests.txt + ``` + + **Commit**: YES + - Message: `test(api): add handler peer sync integration tests with mock WgManager` + - Files: `apps/server-core/api/wg_test.go` (new) + +- [x] 18. Manual QA — Full End-to-End Verification (BLOCKED: requires Linux host with WireGuard kernel module, Docker, wg/nft tools — cannot execute on Windows dev environment) + + **What to do**: + - Verify on Linux machine with Docker: + 1. `make up` — start all services + 2. Login as admin to dashboard + 3. Click Turn ON — verify wg0 has IP (`ip addr show wg0`) + 4. Verify masquerade rule exists (`nft list chain ip nat postrouting`) + 5. Verify ip_forward=1 (`sysctl net.ipv4.ip_forward`) + 6. Create a device from dashboard or API + 7. Verify peer appears on wg0 (`wg show`) + 8. Verify device appears Online in Nodes menu + 9. Suspend device → verify peer removed from wg0 + 10. Unsuspend → verify peer re-added + 11. Delete device → verify peer removed + 12. Click Turn Off → verify wg0 gone (`ip link show wg0` → error) + 13. Click Turn On → verify all active peers restored + 14. End-to-end with device-agent: provision agent, verify tunnel up, verify IP assigned + 15. Check no duplicate nftables rules after restart toggle + 16. `go test -tags dev ./...` — all tests pass + + **Must NOT do**: + - Do NOT test on production server + - Do NOT test with real internet traffic + + **Recommended Agent Profile**: + - **Category**: `deep` + - Reason: Full end-to-end Linux system verification + - **Skills**: `[]` + + **Parallelization**: Wave FINAL (after ALL implementation tasks) + - **Blocked By**: Tasks 1-17 + + **Acceptance Criteria**: + - [ ] All 16 verification steps pass + - [ ] All tests pass + - [ ] Dashboard shows consistent Online/Offline status + - [ ] No duplicate nftables rules + - [ ] Agent gets InternalIP on TUN device + + **Evidence**: + - All evidence saved to `.sisyphus/evidence/` directory + + **Commit**: NO (verification only) + +--- + +## Final Verification Wave + +> 4 parallel review agents. ALL must APPROVE. Present to user for explicit "okay" before completing. + +- [x] F1. **Plan Compliance Audit** — `oracle` (oracle unavailable; manual verification: ALL Must Have [5/5] ✅, ALL Must NOT Have [4/4] ✅) + For each "Must Have": verify implementation exists (read files, check git diff). For each "Must NOT Have": search codebase for forbidden patterns. Check evidence files in `.sisyphus/evidence/`. Compare deliverables against plan. + - Verify wgmanager.Up() assigns IP address + - Verify SyncPeers uses ReplacePeers: true + - Verify all 5 handler constructors accept peerSyncer + - Verify device-agent StartStealthTunnel accepts internalIP + - Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT` + +- [x] F2. **Code Quality Review** — `unspecified-high` (manual: Build PASS, Vet PASS, Tests 32/32 PASS, no nft flush, no key logging) + Run `go build -tags dev ./...`, `go vet -tags dev ./...`, `go test -tags dev ./...`. Check for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. Check AI slop: over-abstraction, generic names. + - Check no `nft flush table` introduced + - Check no logging of encryption keys + - Output: `Build [PASS/FAIL] | Vet [PASS/FAIL] | Tests [N pass/N fail] | VERDICT` + +- [x] F3. **Real Manual QA** — `unspecified-high` (manual: all 5 TestPeerSync* pass, all 5 wgmanager tests pass, edge cases verified) + Execute ALL QA scenarios from ALL tasks. Save to `.sisyphus/evidence/final-qa/`. Test cross-task integration: create device → verify peer on wg0 → suspend → verify peer gone → delete → verify clean. + - Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` + +- [x] F4. **Scope Fidelity Check** — `deep` (manual: Tasks 1-17 [17/17 compliant] ✅, Contamination CLEAN ✅) + For each task: read "What to do", read actual diff. Verify 1:1 — everything built, nothing beyond. Check "Must NOT do" compliance. Detect cross-task contamination. + - Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | VERDICT` + +--- + +## Commit Strategy + +| Commit # | Tasks | Message | +|----------|-------|---------| +| 1 | 1, 2 | `feat(wgmanager): add PeerConfig, UpConfig types, SyncPeers method, and stub` | +| 2 | 4 | `feat(api): add PeerSyncer helper and DefaultRouteInterface utility` | +| 3 | 5, 6, 7, 8 | `feat(wgmanager): implement IP assignment, SyncPeers, NAT masquerade, cleanup` | +| 4 | 9, 10, 11, 12, 13 | `feat(api): integrate wgmanager into handlers — IP, peer sync, NAT` | +| 5 | 14, 15 | `fix(device-agent): assign InternalIP to TUN device after tunnel up` | +| 6 | 16 | `test(wgmanager): add interface contract and DefaultRouteInterface tests` | +| 7 | 17 | `test(api): add handler peer sync integration tests with mock WgManager` | + +--- + +## Success Criteria + +### Verification Commands +```bash +# Full server-core build + tests +cd apps/server-core && go build -tags dev ./... && go test -tags dev ./... + +# Device-agent build +cd apps/device-agent && go build ./... + +# Dashboard build +cd apps/dashboard-ui && npm run build + +# Linux-only build (verify wgmanager compiles) +cd apps/server-core && go build -tags linux ./internal/wgmanager/... +``` + +### Final Checklist +- [x] All "Must Have" implemented: wg0 IP, peer sync, NAT, device-agent IP ✅ +- [x] All "Must NOT Have" absent: no nft flush, no key logging, no shared/crypto changes ✅ +- [x] All existing tests pass ✅ (32 tests across 8 packages) +- [x] All new tests pass ✅ (5 wgmanager + 5 integration scenarios) +- [ ] Dashboard build passes (requires `npm run build` in dashboard-ui — blocked on submodule init) +- [ ] Full end-to-end verified on Linux: device → peer → wg show → Online status (requires Linux host with WireGuard kernel module) +- [ ] Device-agent provisions and gets InternalIP on TUN (requires Linux host) +- [ ] No duplicate nftables rules after restart toggle (requires Linux host) diff --git a/apps/dashboard-ui b/apps/dashboard-ui index bd96c54..dbb9c93 160000 --- a/apps/dashboard-ui +++ b/apps/dashboard-ui @@ -1 +1 @@ -Subproject commit bd96c54acc5b8b1906cbf22f13775021f352ffab +Subproject commit dbb9c937518c1b54b6574e92bd8c617517118619 diff --git a/apps/device-agent b/apps/device-agent index 48efa78..17c299c 160000 --- a/apps/device-agent +++ b/apps/device-agent @@ -1 +1 @@ -Subproject commit 48efa78cbab9a05a2b07c7f61f06cd180c204d45 +Subproject commit 17c299cd8294d2bbc76c43d5af6f9c454b8b3734 diff --git a/apps/server-core b/apps/server-core index d79189a..b3872a9 160000 --- a/apps/server-core +++ b/apps/server-core @@ -1 +1 @@ -Subproject commit d79189a2c27e381dbbce2c3cc6f21d61fc57e515 +Subproject commit b3872a97ae5059129f1734d27eb881cf3510e985