Files
Nexus-Guard-Suite/.sisyphus/plans/nxg-nexusguard-full-build.md
T
2026-05-15 05:34:55 +07:00

684 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NexusGuard SD-WAN — Full System Build Plan
## Overview
Build the complete NexusGuard SD-WAN system across 3 submodules (`server-core`, `dashboard-ui`, `device-agent`). The system provides Zero-Trust network isolation via nftables, stealth WireGuard tunneling via wireguard-go, hardware-bound device identity, and a Vue 3 management dashboard.
## Current State
- **Main repo**: Initialized with 3 git submodules pointing to `git.datadunia.com`
- **server-core**: Empty (README only)
- **dashboard-ui**: Empty (README only)
- **device-agent**: Empty (README only)
- **Environment**: Go 1.25.1, Node 24.7.0, Vite 8.0.13, Git 2.50.1. Docker NOT installed.
- **Remote CI/CD**: Gitea Actions runner already configured
## Architecture Decisions (Binding)
### Stack Per Repo
| Repo | Language | Framework | DB/Cache | Key Deps |
|------|----------|-----------|----------|----------|
| server-core | Go 1.25 | Gin, GORM | PostgreSQL, Redis | google/nftables, crypto/aes, golang-jwt |
| dashboard-ui | TypeScript | Vue 3, Vite, Pinia, Tailwind | — | axios, vue-router |
| device-agent | Go 1.25 | Static binary | None (memory only) | golang.zx2c4.com/wireguard/device, crypto/aes |
### Security Decisions
- **Zero-Trust**: nftables default DROP policy. Per-user sets for isolation.
- **Stealth Agent**: wireguard-go via `device.IpcSet()` — no config files on disk. Ever.
- **Hardware Binding**: SHA256(HWID + Salt) for AES key derivation. HWID = product_uuid | machine-id | cpuinfo.
- **Config Encryption**: AES-256-GCM between Server ↔ Agent. Key never transmitted.
- **Auth**: JWT for Dashboard ↔ Server. X-Token-Auth for Agent ↔ Server (one-time registration token).
### Deployment Decisions
- **Docker Compose**: PostgreSQL + Redis + Server-Core (for dev/CI)
- **Bare Metal**: Systemd service + Go binary (for production)
- **TLS**: Deferred. Document nginx/Caddy reverse proxy config. HTTP-only during development.
- **Android**: Skipped. Web-only dashboard Phase 4.
### Database Migration
- **Dev/Test**: GORM AutoMigrate
- **Production**: `goose` versioned migrations
- **Migration files**: `migrations/` directory in server-core
### Testing Strategy
- **Go unit tests**: All business logic (crypto, IPAM, models, API handlers)
- **nftables**: Manual testing only via SSH to Linux server (no Linux dev env)
- **Vue tests**: Vitest for stores/utils. Manual browser testing for components.
- **E2E**: Post-Phase 4 manual verification
## Scope Guardrails (Must-NOT-Have)
- NO STUN/P2P implementation in Phase 14
- NO key rotation logic in Phase 14
- NO peer discovery mechanism in Phase 14
- NO WebSocket — polling only for "real-time" status
- NO PHP migration scaffolding beyond API_SPEC.md (doc only)
- NO agent disk writes of any kind (not even encrypted cache)
- NO WireGuard kernel module dependency — userspace wireguard-go only
- NO `nft flush table` — element-level operations only
- NO GORM AutoMigrate outside dev/test mode (goose for prod)
---
## Phase Completion Protocol (CRITICAL — READ BEFORE EXECUTING)
### How Phases Work
Each phase is **self-contained and sequential**. Phase N+1 MUST NOT start until Phase N is fully verified and tagged.
### Git Tagging Strategy
Every phase creates a git tag in its respective submodule:
```
phase-1-server-core → apps/server-core
phase-2-device-agent → apps/device-agent
phase-3-devops → nexus-guard-suite (main repo)
phase-4-dashboard-ui → apps/dashboard-ui
```
### Phase Exit Gates (MUST pass before next phase)
| Gate | Check | Who |
|------|-------|-----|
| All tasks committed | `git log --oneline` shows all tasks | Implementer |
| All tests pass | `go test ./...` or `npm test` returns 0 | Implementer |
| Git tag created | `git tag phase-N-name` pushed | Implementer |
| Phase QA verified | Per-phase exit criteria manually checked | User confirms |
| **Gate passed** | ⏸ **STOP** — user approval required to proceed | User signs off |
### Shared Code Rules (NEVER Rebuild)
| Code | Built In | Used By | Rule |
|------|----------|---------|------|
| `shared/crypto/encryptor.go` | Phase 1 Task 1.4 | Phase 2 Task 2.1 | **Copy file from server-core. Do NOT rewrite.** Identical code. |
| `.env.example` patterns | Phase 3 Task 3.5 | All repos | Finalized in Phase 3. Phase 1 creates initial stub only. |
| `Dockerfile` for server-core | Phase 3 Task 3.1 | Phase 3 docker-compose | References binary built in Phase 1. **Do not rebuild Go code.** |
| `API_SPEC.md` | Phase 1 Task 1.11 | Phase 4 (dashboard integration) | Document only. **Do not regenerate.** |
### Dependency Graph (Which Phase Depends On What)
```
Phase 1 (Server Core) — No dependencies. ✓ Foundation.
Phase 2 (Device Agent) — Depends on: Phase 1 (needs server API for provisioning test)
Phase 3 (DevOps) — Depends on: Phase 1 + 2 (needs server binary + agent binary)
Phase 4 (Dashboard UI) — Depends on: Phase 1 (needs stable API surface)
Phase 5 (Advanced Docs) — Depends on: All prior phases (documents existing decisions)
```
### What Happens If a Phase Is Already Complete
- Check `git tag` in the submodule. If the phase tag exists, the phase is done.
- **Do NOT re-run tasks.** Skip to the next phase's entry criteria.
- If code needs fixing, create a NEW task in the current phase. **Never reopen completed phases.**
---
## Phase 1: Server Core (`apps/server-core`)
**Goal**: Build the Go-Gin backend with all core services — database models, nftables manager, IPAM, crypto, API endpoints, and heartbeat tracking.
**Phase Dependency**: None (foundation phase)
**Entry Criteria**: Submodule `apps/server-core` exists with `.git` initialized.
**Exit Criteria** (all must pass before Phase 2):
- [x] `go build ./...` compiles without errors
- [x] `go test ./... -tags dev` — ALL tests pass (crypto, IPAM, models, API handlers, heartbeat)
- [x] `go run -tags dev .` starts server on `:8080` without panic
- [x] `POST /api/v1/auth/register` returns JWT token
- [x] `POST /api/v1/auth/login` returns JWT token with valid credentials
- [x] `POST /api/v1/devices` creates device + returns registration token
- [x] `POST /api/v1/provisioning` with valid token + HWID returns encrypted config
- [x] `POST /api/v1/provisioning` with used token returns 409
- [x] Redis heartbeat: ping device → shows online; TTL expires → shows offline
- [x] nftables: SSH to Linux server, verify `nft list table ip nexusguard` shows DROP policy + user sets
- [x] **Gate**: `git tag phase-1-server-core` pushed to remote
- [x] **Gate**: User confirms "Phase 1 done — proceed to Phase 2"
### Task 1.1: Go Module Init + Project Structure ✅
- **Files**: `go.mod`, `go.sum`, `main.go`, `.env.example`, `internal/config/config.go`
- **Actions**:
- `go mod init github.com/nexusguard/nexus-server-core`
- Create directory structure: `api/`, `internal/models/`, `internal/firewall/`, `internal/ipam/`, `internal/heartbeat/`, `internal/auth/`, `shared/crypto/`, `migrations/`, `docs/`
- Create `main.go` with Gin engine initialization, config loading from env
- Create `internal/config/config.go` with typed config struct (DB, Redis, JWT secret, nftables table name, IPAM pool CIDR)
- Create `.env.example` with all config keys documented
- **QA**: `go build ./...` succeeds. Config loads from env vars with defaults.
- **Test**: `TestConfigLoad` — verify env parsing with mock env
### Task 1.2: GORM Models + AutoMigrate + Goose Migration Setup ✅
- **Files**: `internal/models/models.go`, `internal/models/migrations.go`, `migrations/001_init.sql`
- **Models**:
- `User`: ID (uuid), Username (unique), PasswordHash, Devices (has many)
- `Device`: ID (uuid), UserID (FK), Name, HWID (unique, index), InternalIP (unique), PublicKey, PrivateKey (encrypted at rest), PresharedKey, AllowInternet (default false), LastHandshake, IsActive (default true), FirewallRules (has many)
- `FirewallRule`: ID (uuid), DeviceID (FK), DestIPRange, DestPortRange, Protocol (default tcp), Action (default accept)
- `WgServer`: ID (uuid), Name, PublicKey, Endpoint, ListenPort
- **Actions**:
- Define all GORM models with proper tags, constraints, and relations
- AutoMigrate in `main.go` behind `-tags dev` build flag
- Initialize `goose` with `migrations/001_init.sql` (CREATE TABLE statements matching models)
- Add `goose` as dev tool dependency
- **QA**: `go run -tags dev .` creates all tables. Goose migration applies cleanly.
- **Test**: `TestModelRelations` — create User + Device + Rule, verify FK constraints. `TestAutoMigrate` — verify tables match structs.
### Task 1.3: nftables Manager (Init + Set CRUD + Interval Ranges) ✅ ✅
- **File**: `internal/firewall/nftables.go`, `internal/firewall/nftables_test.go`
- **Key Library**: `github.com/google/nftables`
- **Functions**:
- `NewNetManager() *NetManager` — init nftables connection
- `InitNetwork() error` — create `nexusguard` table (IPv4), `forward` chain with Policy DROP
- `AddUserIsolation(userID string, ipList []net.IP) error` — create Set per user, add elements
- `RemoveUserIsolation(userID string) error` — delete the user's set
- `AddDeviceToSet(userID string, deviceIP net.IP) error` — add single element to existing set
- `RemoveDeviceFromSet(userID string, deviceIP net.IP) error` — remove element from set
- `AddRangeRule(deviceName string, startIP, endIP net.IP, startPort, endPort uint16) error` — interval set with `Interval: true`, `KeyEnd` for IP ranges, `TypeInetService` for port ranges
- `RemoveRangeRule(deviceName string) error`
- **Critical Guardrail**: NEVER call `nft flush table`. Only add/remove individual elements.
- **QA**: Mock nftables.Conn interface. Verify InitNetwork creates table + chain with DROP policy. Verify AddUserIsolation creates set with correct type. Verify AddRangeRule creates interval set with KeyEnd.
- **Test**: `TestInitNetwork` (mock verifies table/chain creation), `TestAddRemoveDevice`, `TestIntervalRange`
- **Note**: Integration testing is MANUAL — run on Linux server via SSH. No automated nftables tests on Windows.
### Task 1.4: AES-256-GCM Crypto Module ✅
- **File**: `shared/crypto/encryptor.go`, `shared/crypto/encryptor_test.go`
- **Functions**:
- `DeriveKey(hwid string, salt []byte) []byte` — SHA256(hwid + salt), returns 32-byte key
- `Encrypt(plaintext []byte, key []byte) ([]byte, error)` — AES-GCM with random nonce, returns nonce\|ciphertext
- `Decrypt(ciphertext []byte, key []byte) ([]byte, error)` — split nonce, GCM Open
- **Security Rules**:
- Nonce must be random (crypto/rand), never zero/sequential
- Decrypt with wrong key must return error (authenticated encryption)
- Plaintext and key must not be logged or printed
- **QA**: Encrypt/Decrypt roundtrip returns original. Wrong key returns error. Different nonces produce different ciphertexts.
- **Test**: `TestEncryptDecryptRoundtrip`, `TestDecryptWrongKey`, `TestKeyDerivation`, `TestNonceUniqueness`
### Task 1.5: IPAM Manager ✅
- **File**: `internal/ipam/manager.go`, `internal/ipam/manager_test.go`
- **Functions**:
- `NewIPAM(poolCIDR string, db *gorm.DB) *Manager` — init with CIDR (default 10.8.0.0/16)
- `AllocateIP() (net.IP, error)` — find next unused /32 from pool, mark as used in DB
- `ReleaseIP(ip net.IP) error` — mark IP as available
- `IsAvailable(ip net.IP) bool` — check if IP is free
- **Edge Cases**:
- Pool exhaustion: return error with pool stats
- Concurrent allocation: DB UNIQUE constraint on Device.InternalIP handles collisions; retry up to 3 times
- Release non-existent IP: no-op, no error
- **QA**: Allocate returns sequential /32. Exhaust pool and verify error returned. Release and re-allocate.
- **Test**: `TestAllocateSequential`, `TestPoolExhaustion`, `TestReleaseAndReallocate`, `TestConcurrentAllocation`
### Task 1.6: JWT Auth Service + API Endpoints ✅
- **Files**: `internal/auth/jwt.go`, `internal/auth/jwt_test.go`, `api/auth.go`
- **Functions**:
- `GenerateToken(userID uuid.UUID, username string) (string, error)` — JWT with 24h expiry
- `ValidateToken(tokenString string) (*Claims, error)` — parse + validate signature + expiry
- `AuthMiddleware() gin.HandlerFunc` — Gin middleware that extracts user from JWT
- **API Endpoints**:
- `POST /api/v1/auth/login` — username + password → JWT token
- `POST /api/v1/auth/register` — create new user (admin only, X-Admin-Key header)
- **Password Storage**: bcrypt hash (cost 12)
- **QA**: Token generation → validation roundtrip. Expired token rejected. Wrong password returns 401. Duplicate registration returns 409.
- **Test**: `TestGenerateAndValidate`, `TestExpiredToken`, `TestAuthMiddleware`, `TestLoginEndpoint`
### Task 1.7: Device Management API ✅
- **File**: `api/devices.go`, `api/devices_test.go`
- **Endpoints** (all require JWT auth):
- `GET /api/v1/devices` — list user's devices (name, IP, status, last handshake)
- `POST /api/v1/devices` — create device (name only), returns device + registration token
- `GET /api/v1/devices/:id` — get device details
- `PUT /api/v1/devices/:id` — update device (name, allow_internet)
- `DELETE /api/v1/devices/:id` — delete device (also removes nftables rules)
- `POST /api/v1/devices/:id/regenerate-token` — invalidate old token, generate new one
- **Token**: Registration token is UUIDv4, stored hashed in DB, single-use (marked used on provisioning)
- **QA**: CRUD operations return correct data. Token regeneration invalidates old token. Delete removes nftables rules.
- **Test**: `TestCreateDevice`, `TestDeleteDeviceRemovesRules`, `TestRegenerateToken`
### Task 1.8: Provisioning API ✅
- **File**: `api/provisioning.go`, `api/provisioning_test.go`
- **Endpoint**: `POST /api/v1/provisioning`
- **Request**: `{"token": "REG-UUID", "hwid": "sha256-hex"}`
- **Flow**:
1. Look up token in DB — 404 if not found, 409 if already used
2. Mark token as used, bind HWID to device
3. Generate WireGuard keys if not exist (server side)
4. Derive AES key: SHA256(HWID + ServerSalt)
5. Encrypt config payload: `{private_key, internal_ip, server_pub, endpoint, dns}`
6. Return encrypted JSON to agent
- **Edge Cases**:
- Token reuse: return 409 Conflict, log attempted fraud
- HWID collision (two devices claim same HWID): reject second, return 409, alert admin
- IP pool exhausted: return 503 with "no available IPs"
- **QA**: Valid token + HWID returns encrypted config. Reused token returns 409. Wrong HWID format returns 400.
- **Test**: `TestProvisioningSuccess`, `TestTokenReuse`, `TestHWIDCollision`
### Task 1.9: Firewall Rules API ✅
- **File**: `api/rules.go`, `api/rules_test.go`
- **Endpoints** (JWT auth + device belongs-to-user check):
- `GET /api/v1/devices/:id/rules` — list rules for device
- `POST /api/v1/devices/:id/rules` — create rule (dest_ip_range, dest_port_range, protocol, action)
- `PUT /api/v1/rules/:ruleId` — update rule
- `DELETE /api/v1/rules/:ruleId` — delete rule (also removes from nftables)
- **Rule Engine**: On create/update/delete, trigger nftables sync:
- If no rules + AllowInternet=false → device isolated (default DROP)
- If AllowInternet=true → accept to 0.0.0.0/0
- If specific rules exist → apply as nftables verdict map
- **QA**: Create rule → appears in GET. Delete rule → removed from DB + nftables. Overlapping ranges handled correctly.
- **Test**: `TestCreateDeleteRule`, `TestAllowInternetToggle`, `TestRuleBelongsToDevice`
### Task 1.10: Redis Heartbeat Worker ✅
- **File**: `internal/heartbeat/redis.go`, `internal/heartbeat/redis_test.go`
- **Functions**:
- `StartHeartbeatCollector(rdb *redis.Client, db *gorm.DB)` — goroutine: every 30s, scan Redis keys `device:{id}:ping`, update `last_handshake` and `is_active` in DB
- `RecordPing(rdb *redis.Client, deviceID uuid.UUID)` — SET `device:{id}:ping` timestamp, EX 90 (TTL)
- `GetOnlineDevices(rdb *redis.Client) ([]Device, error)` — check TTL for all known devices
- **Agent Push**: Device sends periodic ping to `POST /api/v1/heartbeat` (JWT or X-Device-Token auth), which calls `RecordPing`
- **Heartbeat Endpoint**:
- `POST /api/v1/heartbeat` — accept device ID (from auth), call RecordPing
- **Graceful Degradation**: If Redis is down, server logs error and uses DB-only status (stale, but functional)
- **QA**: Ping → device marked online. TTL expires → device marked offline. Redis down → server still responds.
- **Test**: `TestPingAndTTL` (miniredis), `TestRedisDownGraceful`
### Task 1.11: API_SPEC.md Documentation ✅
- **File**: `docs/API_SPEC.md`
- **Content**:
- Full OpenAPI 3.0 specification of all endpoints
- Auth scheme: JWT Bearer (Dashboard), X-Token-Auth header (Agent registration), X-Device-Token (heartbeat)
- Request/response examples for every endpoint
- Error codes catalog (400, 401, 404, 409, 500, 503)
- Deployment notes: TLS via nginx/Caddy reverse proxy (config templates included)
- **QA**: Spec is internally consistent. All endpoints documented with request/response schemas.
---
## Phase 2: Device Agent (`apps/device-agent`)
**Goal**: Build the stealth WireGuard agent — HWID discovery, embedded wireguard-go tunnel via IpcSet, AES-GCM decryption of server config, provisioning client, and auto-reconnect.
**Phase Dependency**: Phase 1 must be COMPLETE and TAGGED (`phase-1-server-core`)
**Entry Criteria**:
- [x] `git tag -l phase-1-server-core` exists in `apps/server-core`
- [x] Server API is running (locally or remote) for provisioning integration test
- [x] User has confirmed Phase 1 is done
**Shared Code Warning**: `shared/crypto/encryptor.go` — COPY from server-core (Phase 1 Task 1.4). Do NOT rewrite. Identical code.
**Exit Criteria** (all must pass before Phase 3):
- [x] `go build -o sys-bridge .` compiles without errors
- [x] `go test ./...` — ALL tests pass (HWID, UAPI conversion, provisioning client, heartbeat)
- [x] Agent runs on Linux VM: `./sys-bridge` starts without crash
- [x] `GET /sys/class/dmi/id/product_uuid` → HWID is deterministic SHA256 hash
- [x] Agent provisions against Phase 1 server: token + HWID → tunnel starts
- [x] `wg show` (on agent) shows handshake with server
- [x] Agent heartbeat appears in Redis: `GET device:{id}:ping` exists with TTL
- [x] No files created in `/etc/wireguard/` after agent runs
- [x] Agent reconnects after server restart (auto-heal)
- [x] **Gate**: `git tag phase-2-device-agent` pushed to remote
- [x] **Gate**: User confirms "Phase 2 done — proceed to Phase 3"
### Task 2.1: Go Module Init + Agent Scaffold ✅
- **Files**: `go.mod`, `main.go`, `.env.example`, `internal/tunnel/wireguard.go`, `internal/identity/hwid.go`, `internal/client/provisioning.go`
- **Actions**:
- `go mod init github.com/nexusguard/nexus-device-agent`
- Create directory structure: `internal/tunnel/`, `internal/identity/`, `internal/client/`, `shared/crypto/`
- Create `main.go` with: config load → HWID discovery → provisioning → tunnel start → heartbeat loop
- Stealth binary name: build with `-o sys-bridge` (configurable)
- Copy `shared/crypto/encryptor.go` from server-core (identical code)
- **QA**: `go build -o sys-bridge .` succeeds. Binary runs without config file.
### Task 2.2: HWID Discovery ✅
- **File**: `internal/identity/hwid.go`, `internal/identity/hwid_test.go`
- **Functions**:
- `GetHWID() (string, error)` — cascading discovery:
1. Read `/sys/class/dmi/id/product_uuid` → if exists, return SHA256(trimmed)
2. Fallback: read `/etc/machine-id` → SHA256
3. Fallback: read `/proc/cpuinfo`, extract "Serial" → SHA256
4. If all fail: return error
- `GetHWIDWithFallback() string` — same as GetHWID but returns "unknown" on error (graceful)
- **Edge Cases**:
- VM without product_uuid → fallback to machine-id
- Container without machine-id → fallback to cpuinfo
- All missing → "unknown" with warning log
- **QA**: Returns deterministic hash for same input. Returns error only when all sources are unavailable.
- **Test**: `TestHWIDFromProductUUID` (mock fs), `TestHWIDFallback`, `TestAllSourcesMissing`
### Task 2.3: Stealth WireGuard Tunnel (IpcSet) ✅
- **File**: `internal/tunnel/wireguard.go`, `internal/tunnel/wireguard_test.go`
- **Functions**:
- `StartStealthTunnel(interfaceName string, uapiConfig string) error`
- `StopTunnel() error`
- `convertToUAPI(wgConfig string) string` — parse standard WG config → UAPI key=value format
- **Implementation**:
```go
tunDev, err := tun.CreateTUN(interfaceName, 1420)
logger := device.NewLogger(device.LogLevelError, "(nxg-wg) ")
dev := device.NewDevice(tunDev, logger)
err = dev.IpcSet(uapiConfig) // INJECT TO MEMORY — NO FILES
dev.Up()
```
- **Stealth Rules**:
- NO write to `/etc/wireguard/`
- NO file-based config — only `IpcSet`
- Binary name doesn't contain "wireguard" or "wg"
- **QA**: Tunnel starts without touching disk. Stop cleans up TUN device. Multiple start/stop cycles work.
- **Test**: `TestUAPIConversion`, `TestStartStopCycle` (mock tun), `TestNoFileWrites`
### Task 2.4: Provisioning Client ✅
- **File**: `internal/client/provisioning.go`, `internal/client/provisioning_test.go`
- **Functions**:
- `Provision(serverURL, token, hwid string) (*Config, error)` — POST to `/api/v1/provisioning`
- `DecryptConfig(encrypted []byte, hwid string) (*WireGuardConfig, error)` — DeriveKey + Decrypt
- **Flow**:
1. Construct POST request with `{"token": token, "hwid": hwid}`
2. Parse response, extract encrypted config
3. Derive AES key from HWID + hardcoded salt (same as server)
4. Decrypt config, parse into `WireGuardConfig` struct
5. Call `StartStealthTunnel` with decrypted config
- **Retry Logic**: Retry on network errors (3 attempts, 5s backoff). No retry on 400/401/409.
- **QA**: Provisioning with valid response starts tunnel. Network failure retries. Invalid token stops.
- **Test**: `TestProvisionSuccess` (httptest server), `TestRetryOnNetworkError`, `TestInvalidToken`
### Task 2.5: Heartbeat + Auto-Reconnect ✅
- **File**: `internal/client/heartbeat.go`, `internal/client/heartbeat_test.go`
- **Functions**:
- `StartHeartbeat(serverURL, deviceID string, interval time.Duration)` — goroutine: every 30s, POST to `/api/v1/heartbeat`
- `MonitorHandshake(wgDev *device.Device, onFailure func())` — check last handshake time, if > 120s, trigger reconnect
- `Reconnect(serverURL, token, hwid string)` — re-provision and restart tunnel
- **Graceful Degradation**:
- If heartbeat fails (server offline): log warning, keep running, retry
- If handshake fails for 120s: attempt re-provisioning
- If re-provisioning fails: exponential backoff (30s, 60s, 120s, 300s max)
- **QA**: Heartbeat fires at correct interval. Handshake timeout triggers reconnection. Exponential backoff caps at 300s.
- **Test**: `TestHeartbeatInterval`, `TestHandshakeTimeout`, `TestReconnectBackoff`
---
## Phase 3: DevOps & Installer
**Goal**: Create the infrastructure — Docker compose for local dev, bash installer for agent deployment, systemd service templates, Gitea CI/CD pipelines, and environment configuration.
**Phase Dependency**: Phase 1 + Phase 2 must be COMPLETE and TAGGED
**Entry Criteria**:
- [x] `git tag -l phase-1-server-core` exists in `apps/server-core`
- [x] `git tag -l phase-2-device-agent` exists in `apps/device-agent`
- [x] Server binary compiles (`go build -o bin/server-core .` in server-core)
- [x] Agent binary compiles (`go build -o sys-bridge .` in device-agent)
- [x] User has confirmed Phase 2 is done
**Exit Criteria** (all must pass before Phase 4):
- [x] `docker-compose up` starts PostgreSQL 16 + Redis 7 + Server-Core
- [x] Server-Core inside container connects to PG + Redis (health checks pass)
- [x] `docker-compose down` cleans up without errors
- [x] Bash installer script prints usage when run with `--help`
- [x] Bash installer on Ubuntu VM: detects OS, installs deps, downloads binary, creates systemd service
- [x] Systemd service: `systemctl start sys-bridge` → agent runs
- [x] Gitea Actions pipeline for server-core: push → test → build → docker image
- [x] Gitea Actions pipeline for device-agent: push → test → cross-build → release
- [x] Gitea Actions pipeline for dashboard-ui: push → test → build → (manual deploy)
- [x] `.env.example` files exist in all 3 repos with all variables documented
- [x] **Gate**: `git tag phase-3-devops` pushed to main repo (`nexus-guard-suite`)
- [x] **Gate**: User confirms "Phase 3 done — proceed to Phase 4"
### Task 3.1: Docker Compose (PostgreSQL + Redis + Server-Core) ✅
- **File**: `docker-compose.yml`, `docker-compose.dev.yml`, `Dockerfile` (in server-core)
- **Services**:
- `postgres`: PostgreSQL 16, volume for data, health check
- `redis`: Redis 7, health check
- `server-core`: Go binary (multi-stage build), depends on postgres+redis, env vars
- **Dockerfile** (server-core): Multi-stage — `golang:1.25-alpine` build stage → `alpine:3.20` runtime
- **docker-compose.dev.yml**: Hot-reload via `air` or `nodemon`, exposed ports for local dev
- **QA**: `docker-compose up` starts all containers. Server connects to postgres+redis. Health checks pass.
### Task 3.2: Bash Installer Script ✅
- **File**: `scripts/install_agent.sh` (in main repo or device-agent)
- **Features**:
- Root check
- OS detection: Debian/Ubuntu/Raspbian → apt, CentOS/RHEL/Fedora → yum/dnf
- Dependency install: nftables, curl, iproute2, wireguard-tools
- Architecture detection: amd64, arm64, armv7l
- Binary download from Gitea releases using X-Token-Auth
- Binary installation to `/usr/local/bin/` with configurable name (default `sys-bridge`)
- Systemd service creation: `/etc/systemd/system/sys-bridge.service`
- Service enable + start
- **Flags**: `--token`, `--server-url`, `--binary-name`, `--help`
- **QA**: Runs on Ubuntu → creates systemd service. Runs on CentOS → uses yum. Missing `--token` prints usage.
### Task 3.3: Systemd Service Template ✅
- **File**: `scripts/sys-bridge.service` (as template), installer generates the actual file
- **Service Config**:
- Type=simple
- ExecStart=/usr/local/bin/sys-bridge
- Restart=always, RestartSec=5
- EnvironmentFile=/etc/sys-bridge.env
- StandardOutput=journal, StandardError=journal
- **Environment File** (`/etc/sys-bridge.env`):
- SERVER_URL, REG_TOKEN, BINARY_NAME, LOG_LEVEL
- **QA**: `systemctl start sys-bridge` starts the agent. `systemctl status sys-bridge` shows running.
### Task 3.4: Gitea Actions CI/CD
- **File**: `.gitea/workflows/build.yml` (in each repo)
- **server-core pipeline**:
- `test`: `go test ./... -tags dev -cover`
- `build`: `go build -o bin/server-core .`
- `docker`: Build and push Docker image to Gitea Container Registry
- **device-agent pipeline**:
- `test`: `go test ./... -cover`
- `cross-build`: Build for linux/amd64, linux/arm64, linux/arm
- `release`: Upload binaries to Gitea Releases (tag-based trigger)
- **dashboard-ui pipeline**:
- `test`: `npm test`
- `build`: `npm run build`
- `deploy`: Copy dist/ to web server (manual approval step)
- **QA**: Push triggers pipeline. Tests pass. Binary releases created.
### Task 3.5: Environment Configuration
- **Files**: `.env.example` in each repo, `scripts/sys-bridge.env.example`
- **server-core .env.example**:
```env
DB_HOST=localhost
DB_PORT=5432
DB_USER=nexusguard
DB_PASSWORD=<generate>
DB_NAME=nexusguard
REDIS_ADDR=localhost:6379
JWT_SECRET=<generate-256bit-hex>
SERVER_SALT=<generate-256bit-hex>
NFTABLES_TABLE=nexusguard
IPAM_POOL=10.8.0.0/16
LOG_LEVEL=info
```
- **device-agent .env.example**:
```env
SERVER_URL=https://nxg.example.com
REG_TOKEN=<from-dashboard>
BINARY_NAME=sys-bridge
LOG_LEVEL=info
```
- **dashboard-ui .env.example**:
```env
VITE_API_BASE_URL=http://localhost:8080/api/v1
```
- **QA**: Docs explain every variable with example values and where to obtain them.
---
## Phase 4: Dashboard UI (`apps/dashboard-ui`)
**Goal**: Build the Vue 3 management dashboard — JWT login, device CRUD, firewall rule editor, real-time status monitoring. Web-only (no Android).
**Phase Dependency**: Phase 1 must be COMPLETE and TAGGED (dashboard consumes Phase 1 API)
**Entry Criteria**:
- [ ] `git tag -l phase-1-server-core` exists in `apps/server-core`
- [ ] Server API is running on `http://localhost:8080/api/v1`
- [ ] At least one user and device exist in database (for testing dashboard features)
- [ ] User has confirmed Phase 3 is done (or Phase 1 if skipping DevOps install)
**Exit Criteria** (all must pass before Phase 5):
- [ ] `npm run dev` starts Vite dev server
- [ ] `npm run build` produces production bundle without errors
- [ ] Login page: valid credentials → redirect to dashboard. Invalid → error message.
- [ ] Device list: shows devices with name, IP, online/offline badge
- [ ] Create device: dialog → POST → registration token displayed with copy button
- [ ] Device detail: edit name, toggle "Allow Internet", regenerate token
- [ ] Delete device: confirmation dialog → device removed from list
- [ ] Firewall rule editor: add rule → appears in list. Delete → removed.
- [ ] Dashboard: summary cards show correct counts. Polling updates status.
- [ ] Error states: loading spinner, error message with retry, empty state
- [ ] Responsive layout: sidebar collapses on mobile, works on 375px viewport
- [ ] API service adds JWT header to all requests automatically
- [ ] 401 response → redirect to /login automatically
- [ ] **Gate**: `git tag phase-4-dashboard-ui` pushed to remote
- [ ] **Gate**: User confirms "Phase 4 done — proceed to Phase 5"
### Task 4.1: Vite + Vue 3 Scaffold + API Layer
- **Files**: scaffold via `npm create vite@latest`, `src/services/api.ts`, `src/stores/auth.ts`
- **Actions**:
- Scaffold with Vue 3 + TypeScript + Vite
- Add dependencies: vue-router, pinia, axios, tailwindcss, @tailwindcss/vite
- Configure Tailwind CSS
- Create `src/services/api.ts` — axios instance with base URL from `import.meta.env.VITE_API_BASE_URL`, interceptors for JWT header + 401 redirect
- Create `src/stores/auth.ts` — Pinia store for JWT token (localStorage persistence), login/logout actions
- Create router with auth guard: redirect to /login if no token
- **QA**: `npm run dev` starts. API service adds JWT header. Auth guard works.
### Task 4.2: Login Page + Auth Flow
- **File**: `src/views/Login.vue`, `src/api/auth.ts`
- **Components**:
- Login form: username + password + submit button
- Error display: invalid credentials, server error, network error
- Loading state during submission
- **API Service**: `src/api/auth.ts` — `login(username, password)`, `register(username, password, adminKey)`
- **Flow**: Submit → POST /auth/login → store token in Pinia + localStorage → redirect to /dashboard
- **QA**: Login with valid credentials → redirect to dashboard. Invalid → error message. Already logged in → redirect to dashboard automatically.
### Task 4.3: Device Management Page
- **File**: `src/views/Devices.vue`, `src/views/DeviceDetail.vue`, `src/api/devices.ts`, `src/stores/devices.ts`
- **Components**:
- Device list table: name, IP, status (online/offline badge), last handshake, actions
- Create device dialog: name input → POST → show registration token (copy button)
- Device detail view: edit name, toggle "Allow Internet", regenerate token
- Delete device: confirmation dialog
- **API Service**: `src/api/devices.ts` — CRUD calls
- **Store**: `src/stores/devices.ts` — Pinia store with device list, selected device, polling interval
- **QA**: Create device → appears in list with token. Delete → removed from list. Status badge reflects online/offline.
### Task 4.4: Firewall Rule Editor
- **File**: `src/views/FirewallRules.vue` (or tab in DeviceDetail), `src/api/rules.ts`
- **Components**:
- Rules list for selected device: table of dest_ip, dest_port, protocol, action, delete button
- Add rule form: dest IP (single/CIDR/range), dest port (single/range), protocol (TCP/UDP/Both), action (Accept/Drop)
- "Allow Internet" toggle switch (separate from specific rules)
- **Validation**:
- IP format validation (single, CIDR, range: 192.168.1.10-192.168.1.20)
- Port validation (single: 80, range: 8000-9000)
- No duplicate rule submission
- **QA**: Add rule → appears in list. Delete rule → removed. Allow Internet toggle → updates device. Invalid IP → validation error.
### Task 4.5: Dashboard + Status Monitoring
- **File**: `src/views/Dashboard.vue`, `src/stores/dashboard.ts`
- **Components**:
- Summary cards: total devices, online count, offline count, active rules count
- Device status grid: cards with name, IP, online/offline indicator, last handshake time
- Auto-refresh: polling every 10s (NOT WebSocket)
- Visual indicators: green dot = online (< 90s since ping), red dot = offline, gray = unknown
- **Store**: `src/stores/dashboard.ts` — polling timer, device status cache
- **QA**: Dashboard shows correct counts. Online/offline indicators update with polling. Summary cards reflect data.
### Task 4.6: Navigation Shell + Responsive Layout
- **File**: `src/App.vue`, `src/components/Sidebar.vue`, `src/components/Navbar.vue`, `src/router/index.ts`
- **Components**:
- Sidebar: logo, nav links (Dashboard, Devices), user info + logout
- Top navbar: breadcrumb, mobile hamburger menu
- Responsive: sidebar collapses on mobile, full sidebar on desktop
- **Routes**:
- `/login` — Login page (public)
- `/dashboard` — Dashboard (protected)
- `/devices` — Device list (protected)
- `/devices/:id` — Device detail + firewall rules (protected)
- **QA**: All routes work. Auth guard redirects to /login. Responsive layout works on mobile viewport.
### Task 4.7: Error Handling + UX Polish
- **Files**: `src/components/ErrorBoundary.vue`, `src/components/LoadingSpinner.vue`, `src/components/EmptyState.vue`
- **Components**:
- Error boundary for API failures: retry button, error message
- Loading spinner for async operations
- Empty state: "No devices yet. Create your first device."
- Toast notification component for success/error feedback (create, delete, update)
- Confirmation dialog for destructive actions (delete device, regenerate token)
- **QA**: API failure shows error with retry. Loading spinner shows during requests. Toast appears after CRUD actions.
---
## Phase 5: Advanced Features (Deferred)
**Goal**: Document the deferred features. No implementation — only architectural notes for future phases.
**Phase Dependency**: All prior phases COMPLETE and TAGGED
**Entry Criteria**:
- [ ] `git tag -l phase-1-server-core` exists
- [ ] `git tag -l phase-2-device-agent` exists
- [ ] `git tag -l phase-3-devops` exists
- [ ] `git tag -l phase-4-dashboard-ui` exists
- [ ] User has confirmed Phase 4 is done
**Exit Criteria**:
- [ ] `docs/TLS_DEPLOYMENT.md` contains nginx + Caddy reverse proxy configs with Let's Encrypt
- [ ] STUN signaling architectural notes documented in `docs/STUN_ARCHITECTURE.md`
- [ ] Key rotation plan documented in `docs/KEY_ROTATION.md`
- [ ] Peer discovery design documented in `docs/PEER_DISCOVERY.md`
- [ ] **Gate**: User confirms "All phases complete. System ready."
### Task 5.1: nginx/Caddy TLS Documentation
- **File**: `docs/TLS_DEPLOYMENT.md` (in server-core)
- **Content**: nginx and Caddy reverse proxy config templates for TLS termination, Let's Encrypt auto-provisioning, HTTP-to-HTTPS redirect
- **Note**: Documentation only. No code changes.
### Task 5.2: STUN Signaling Notes
- **Document**: Architectural notes for UDP hole punching
- **Server**: STUN endpoint on server-core, `/api/v1/stun` — returns public IP:port of the agent
- **Agent**: On start, send STUN request to server. Server records public endpoint. Agent receives peer's public endpoint via polling/push.
- **Implementation**: Deferred to future phase.
### Task 5.3: Key Rotation Notes
- **Document**: 30-day key rotation plan
- **Dual-key buffer**: Server generates new keypair 5 minutes before expiry. Agent fetches new key while old key is active. Graceful handover period.
- **Implementation**: Deferred to future phase.
### Task 5.4: Peer Discovery Notes
- **Document**: `/api/v1/peers` endpoint spec
- **Design**: Server maintains device list with internal IP per user. When new device provisions, server pushes/notifies all peers in same user group.
- **Implementation**: Deferred to future phase.
---
## Roll-up Verification Wave (End-to-End)
**Run this ONLY after all 5 phases are complete and tagged.** This validates the integrated system works end-to-end. Each individual check here should already have passed during per-phase exit gates — this is a final integration smoke test.
### Pre-flight: Phase Tags Check
- [ ] `git tag -l` in `apps/server-core` shows `phase-1-server-core`
- [ ] `git tag -l` in `apps/device-agent` shows `phase-2-device-agent`
- [ ] `git tag -l` in root shows `phase-3-devops`
- [ ] `git tag -l` in `apps/dashboard-ui` shows `phase-4-dashboard-ui`
- [ ] All docs exist from Phase 5
### Integration Smoke Tests
| # | Test | Expected | If Fails |
|---|------|----------|----------|
| 1 | `docker-compose up` → PostgreSQL + Redis + Server-Core start | All 3 containers healthy | Fix Phase 3 |
| 2 | `POST /api/v1/auth/register` (admin) | 201 + JWT token | Fix Phase 1 |
| 3 | `POST /api/v1/auth/login` | 200 + JWT token | Fix Phase 1 |
| 4 | `POST /api/v1/devices` (with JWT) | 201 + device + reg token | Fix Phase 1 |
| 5 | Agent binary runs: `./sys-bridge --server-url http://localhost:8080 --token REG-TOKEN` | Tunnel established, no errors | Fix Phase 2 |
| 6 | `GET /api/v1/devices` → device shows "online" | Status = online | Fix Phase 1/2 |
| 7 | Dashboard login → device list shows device | Device visible in UI | Fix Phase 4 |
| 8 | Dashboard: create firewall rule → SSH verify nftables | Rule appears in `nft list table` | Fix Phase 1/4 |
| 9 | Toggle "Allow Internet" in dashboard → verify nftables | Verdict map changes | Fix Phase 1/4 |
| 10 | Delete device in dashboard → verify nftables cleanup | Set element removed | Fix Phase 1/4 |
| 11 | Kill agent process → server marks offline within 90s | Status = offline | Fix Phase 1/2 |
| 12 | Restart agent → reconnects automatically | Status returns to online | Fix Phase 2 |
| 13 | Bash installer on fresh Ubuntu VM | Installs deps + binary + systemd | Fix Phase 3 |
| 14 | Gitea Actions: push to any repo → pipeline triggers | Green build | Fix Phase 3 |
### Pass / Fail Decision
- **ALL 14 pass**: System is complete and production-ready. 🟢
- **Any fail**: Fix the failing component in its original phase. Do NOT create workarounds in other phases.
**Approval required from user. Run each check, report results.**