# PROJECT KNOWLEDGE BASE

**Generated:** 2026-05-22
**Commit:** `92051d5`
**Branch:** `main`

## OVERVIEW
NexusGuard SD-WAN Suite — Enterprise Zero-Trust SD-WAN with WireGuard tunneling, centralized IPAM, and real-time nftables network isolation. Monorepo with 3 git submodules: Go backend (Gin), Vue 3 dashboard, Go device agent.

## STRUCTURE
```
./
├── apps/
│   ├── server-core/    # Go/Gin API backend (submodule)
│   ├── dashboard-ui/   # Vue 3 + Vite frontend (submodule)
│   └── device-agent/   # Go stealth daemon (submodule)
├── docker-compose.yml  # Production orchestration
├── docker-compose.dev.yml  # Dev (air hot-reload)
├── Makefile            # up/down/dev/migrate/reset-db
├── setup.sh            # First-run: generate .env + random keys
├── update.sh           # Docker update: pull/build/migrate
├── nexusguard-install.sh   # Native install (systemd + nginx)
├── nexusguard-uninstall.sh # Native uninstall
├── .env.example        # DB/JWT/SALT/VITE config template
├── .gitmodules         # 3 submodules → git.datadunia.com
└── .opencode/          # IDE agent config (tooling, not project code)
```

**CRITICAL**: `apps/*` are **git submodules** — clone with `--recurse-submodules`.

## WHERE TO LOOK
| Task | Location | Notes |
|------|----------|-------|
| API handlers | `apps/server-core/api/` | 17 files: auth, devices, peers, rules, share, provisioning, servers, wg |
| Backend core | `apps/server-core/internal/` | auth, config, firewall, heartbeat, ipam, models, wgmanager |
| Dev migration | `apps/server-core/main_dev.go` | GORM AutoMigrate (build tag `dev`) |
| Firewall rules | `apps/server-core/internal/firewall/` | nftables Linux rules |
| Dashboard views | `apps/dashboard-ui/src/views/` | Vue SFC pages |
| Dashboard API client | `apps/dashboard-ui/src/api/` | Axios API modules |
| Dashboard stores | `apps/dashboard-ui/src/stores/` | Pinia state stores |
| Agent client | `apps/device-agent/internal/client/` | Provisioning + heartbeat |
| Agent tunnel | `apps/device-agent/internal/tunnel/` | Memory-injected WireGuard |
| Shared crypto | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (duplicated identical) |
| CI workflows | `apps/*/.gitea/workflows/build.yml` | Gitea Actions per submodule |
| Build config | `apps/dashboard-ui/vite.config.ts` | Vite 8 + Vue + TailwindCSS v4 |
| Source of truth | `apps/server-core/docs/` | API_SPEC, KEY_ROTATION, PEER_DISCOVERY |
| Plan guardrails | `.sisyphus/plans/` | Anti-patterns, "Must NOT do" rules |

## CODE MAP
| Symbol | Type | Location | Role |
|--------|------|----------|------|
| `main()` (server-core) | func | `apps/server-core/main.go` | Entry: CLI flags + Gin init |
| `main()` (device-agent) | func | `apps/device-agent/main.go` | Entry: agent daemon lifecycle |
| `config.Load()` | func | `apps/server-core/internal/config/` | Env-based config loader |
| `config.LoadConfFile()` | func | `apps/server-core/internal/config/config_loader.go` | Config file parser (.env / nexusguard.conf) |
| `auth.Init()` | func | `apps/server-core/internal/auth/` | JWT sign/verify init |
| `firewall.InitNetwork()` | func | `apps/server-core/internal/firewall/` | nftables table/set creation |
| `ipam.AllocateIP()` | func | `apps/server-core/internal/ipam/` | IP pool allocation from CIDR |
| `wgmanager.SetConfig()` | func | `apps/server-core/internal/wgmanager/` | WireGuard config push |
| `models.AutoMigrate()` | func | `apps/server-core/internal/models/` | GORM schema migration |
| `encrypt()` / `decrypt()` | func | `apps/*/shared/crypto/encryptor.go` | AES-256-GCM (identical) |

## CONVENTIONS
- **Go**: Standard layout (`main.go` in root, `internal/`, `api/`)
- **Vue 3**: Composition API + `<script setup lang="ts">` throughout
- **State management**: Pinia stores in `src/stores/`
- **API client**: Axios-based services in `src/api/` + base `src/services/`
- **Styling**: TailwindCSS v4 (no PostCSS — `@tailwindcss/vite` plugin)
- **DB**: GORM ORM, PostgreSQL, AutoMigrate in dev / `-migrate-prod` in prod
- **Naming**: `UPPER_SNAKE_CASE` env vars, `camelCase` Go vars, `PascalCase` exported Go
- **Auth**: JWT, admin-only enforced by middleware pattern
- **Build tags**: `//go:build dev` for AutoMigrate
- **Capabilities**: Server containers need `NET_ADMIN` + `NET_RAW`
- **Ports**: API 8080, Dashboard 80 (Nginx), Postgres 5432, Redis 6379
- **Config**: Docker uses `.env`, native uses `/etc/nexusguard/nexusguard.conf`

## ANTI-PATTERNS (THIS PROJECT)
- **NEVER** `nft flush table` — only atomic add/remove
- **NEVER** log plaintext or encryption keys
- **NEVER** reopen completed phases/commits — fix forward only
- **NEVER** rebuild `shared/crypto/encryptor.go` — copy identical file
- **NEVER** commit build artifacts (binaries, `dist/`)
- **NEVER** force push
- **NEVER** create cross-phase workarounds

## WIREGUARD AllowedIPs — SERVER vs CLIENT (CRITICAL)

WireGuard `AllowedIPs` has **two different meanings** depending on context. Mixing them causes only 1 peer to work.

### Rule
| Context | Where | Value | Purpose |
|---------|-------|-------|---------|
| **Server-side** (kernel `wg set`) | `SyncLocalPeers()` → `peer_sync.go` | `InternalIP/32` per peer | WireGuard routing table — each IP must belong to exactly ONE peer |
| **Client-side** (`.conf` file) | `getDeviceConfig()` → `peers.go` | `EndpointAllowedIPs` from DB (e.g. `/24`, `0.0.0.0/0`) | Tells client which traffic routes through VPN tunnel |
| **Firewall** (nftables) | `AddForwardRule()` → `nftables_linux.go` | `EndpointAllowedIPs` from DB | Controls which IPs peer can reach via forwarding |

### Why
WireGuard uses `AllowedIPs` as an **internal routing table**. When two peers share the same `/24`, WireGuard assigns the AllowedIPs to the **last peer configured only** — the first peer gets `(none)`. This is not a bug; it's how WireGuard routing works.

### Anti-pattern
```go
// WRONG — uses client config for server-side routing
if d.EndpointAllowedIPs != "" {
    allowedIPs = d.EndpointAllowedIPs  // "10.172.21.0/24" ← SAME for both peers!
}
```

### Correct pattern
```go
// CORRECT — server-side always /32 per peer
allowedIPs := *d.InternalIP + "/32"   // "10.172.21.2/32" for gogo2
if d.AllowInternet {
    allowedIPs = "0.0.0.0/0"
}
// Do NOT override with EndpointAllowedIPs here
```

### Database field: `endpoint_allowed_ips`
- Used for **client config** and **firewall rules**
- NOT used for server-side WireGuard kernel config
- Example: `10.172.21.0/24` allows peer to reach full subnet via firewall + routes full subnet through VPN on client

## UNIQUE STYLES
- **Zero-Attack Surface**: `/auth/register` locked; admin via `-create-admin` CLI only
- **Stealth Agent**: No `/etc/wireguard/` — config in memory only
- **Duplicate crypto**: `encryptor.go` copy-pasted in server-core + device-agent (known debt, do not deduplicate)
- **nftables default Accept**: Contradicts Zero-Trust "Default DROP" — intentional gap

## COMMANDS
```bash
# Docker
make up              # Start all services
make down            # Stop all services
make dev             # Start with hot-reload (air)
make logs            # Tail all logs
make migrate         # Run DB migration (requires local Go)
make reset-db        # Nuke PG volume + recreate + migrate

bash update.sh       # Smart update (rebuild only if changes)
bash update.sh --force  # Force rebuild

# Native Install
sudo bash nexusguard-install.sh
sudo bash nexusguard-uninstall.sh
sudo bash nexusguard-uninstall.sh --remove-db

# Development
cd apps/server-core && go run -tags dev .
cd apps/dashboard-ui && npm run dev
cd apps/device-agent && go run .

# Admin
go run -tags dev ./apps/server-core -create-admin -user admin -pass "..."
sudo /usr/local/bin/nexusguard-server -create-admin -user admin -pass "..."
```

## NOTES
- Submodules → private Gitea (`git.datadunia.com`); CI via Gitea Actions per submodule
- Go versions diverge: server-core `1.25.7`, device-agent `1.25.1`
- No root linter configs (`.golangci.yml`, `.eslintrc`, `.editorconfig`)
- Shell scripts use deprecated `docker-compose` v1, Makefile uses `docker compose` v2
- Root has stale artifacts: `connect_remote.txt`, `temp_section*.txt`
- `package.json` name is `"temp-ui"` (stale scaffold remnant)
