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

38 KiB
Raw Blame History

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

  • go build ./... compiles without errors
  • go test ./... -tags dev — ALL tests pass (crypto, IPAM, models, API handlers, heartbeat)
  • go run -tags dev . starts server on :8080 without panic
  • POST /api/v1/auth/register returns JWT token
  • POST /api/v1/auth/login returns JWT token with valid credentials
  • POST /api/v1/devices creates device + returns registration token
  • POST /api/v1/provisioning with valid token + HWID returns encrypted config
  • POST /api/v1/provisioning with used token returns 409
  • Redis heartbeat: ping device → shows online; TTL expires → shows offline
  • nftables: SSH to Linux server, verify nft list table ip nexusguard shows DROP policy + user sets
  • Gate: git tag phase-1-server-core pushed to remote
  • 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:

  • git tag -l phase-1-server-core exists in apps/server-core
  • Server API is running (locally or remote) for provisioning integration test
  • 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):

  • go build -o sys-bridge . compiles without errors
  • go test ./... — ALL tests pass (HWID, UAPI conversion, provisioning client, heartbeat)
  • Agent runs on Linux VM: ./sys-bridge starts without crash
  • GET /sys/class/dmi/id/product_uuid → HWID is deterministic SHA256 hash
  • Agent provisions against Phase 1 server: token + HWID → tunnel starts
  • wg show (on agent) shows handshake with server
  • Agent heartbeat appears in Redis: GET device:{id}:ping exists with TTL
  • No files created in /etc/wireguard/ after agent runs
  • Agent reconnects after server restart (auto-heal)
  • Gate: git tag phase-2-device-agent pushed to remote
  • 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:
    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:

  • git tag -l phase-1-server-core exists in apps/server-core
  • git tag -l phase-2-device-agent exists in apps/device-agent
  • Server binary compiles (go build -o bin/server-core . in server-core)
  • Agent binary compiles (go build -o sys-bridge . in device-agent)
  • User has confirmed Phase 2 is done

Exit Criteria (all must pass before Phase 4):

  • docker-compose up starts PostgreSQL 16 + Redis 7 + Server-Core
  • Server-Core inside container connects to PG + Redis (health checks pass)
  • docker-compose down cleans up without errors
  • Bash installer script prints usage when run with --help
  • Bash installer on Ubuntu VM: detects OS, installs deps, downloads binary, creates systemd service
  • Systemd service: systemctl start sys-bridge → agent runs
  • Gitea Actions pipeline for server-core: push → test → build → docker image
  • Gitea Actions pipeline for device-agent: push → test → cross-build → release
  • Gitea Actions pipeline for dashboard-ui: push → test → build → (manual deploy)
  • .env.example files exist in all 3 repos with all variables documented
  • Gate: git tag phase-3-devops pushed to main repo (nexus-guard-suite)
  • 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:
    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:
    SERVER_URL=https://nxg.example.com
    REG_TOKEN=<from-dashboard>
    BINARY_NAME=sys-bridge
    LOG_LEVEL=info
    
  • dashboard-ui .env.example:
    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.tslogin(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.