# Plan: WireGuard Remote Plane Control (WGRplane) Integration ## Metadata | Field | Value | |-------|-------| | **Plan ID** | `wireguard-remote-plane-wgrplane` | | **Date** | 2026-05-03 | | **Planner** | Prometheus | | **Status** | Ready for Execution | --- ## Goal Build WireGuard Remote Plane Control app (**WGRplane**) — identical feature parity with **WGDashboard** (donaldzou/WGDashboard) — with added **policy.json API** integration. Deploy as git submodule at `/app` from `https://git.datadunia.com/hainzero/WGRplane.git`. Create README and push submodule for initialization. --- ## Scope ### IN (Explicitly Included) - Initialize `/app` as submodule from `https://git.datadunia.com/hainzero/WGRplane.git` - Full WGDashboard feature parity: peer CRUD, QR codes, real-time monitoring, scheduling, TOTP auth, multi-server, plugins, i18n, themes - New **`wg-engine-api`** (Go/Golang) with policy.json API endpoints - Policy.json API: `GET /api/policy`, `POST /api/policy`, reload trigger - **`#Access` migration**: API with `#Access` fallback (API tries first, falls back to `#Access` comment parsing) - Custom API authentication: `wg-rplane-datadunia` header - Atomic writes + flock locking (follow existing patterns from `wg-policy-lib.sh`) - Create README.md for submodule with full documentation - Push submodule to remote for initialization - Test strategy: `bats` for shell scripts, Go testing for `wg-engine-api` ### OUT (Explicitly Excluded) - **NO modifications to existing `.sh` scripts** (`wg-sync-policy.sh`, `wg-policy-engine.sh`, etc.) - No CI pipeline (none exists in repo; document manual test commands instead) - No changes to core shell script logic (only new components) --- ## Key Decisions (from Interview) | Decision | Choice | Rationale | |----------|--------|-----------| | **wg-engine-api tech stack** | **Go (Golang)** | Compiled binary, single executable, lightweight, different from WGDashboard's Python | | **Migration strategy** | **API with #Access fallback** | API tries first, falls back to `#Access` comment parsing. Maximum compatibility. | | **Feature scope** | **Full WGDashboard Parity** | All features: peer CRUD, QR codes, scheduling, TOTP, multi-server, plugins, etc. | | **Authentication** | **Custom header: `wg-rplane-datadunia`** | Header-based auth per user spec | | **API storage** | **Separate `api-policy.json`** | API-managed policies stored separately, merged with `#Access` at runtime | | **API port** | **10087** | Avoid conflict with WGDashboard's default 10086 | | **Policy merge logic** | **API overrides #Access** | For same client IP, API policy takes precedence over `#Access` comment | --- ## Architecture & Data Flow (New) ``` wg0.conf (with/without #Access) ↓ wg-engine-api (Go) -- reads API storage (api-policy.json) ↓ ↓ +-- GET /api/policy (merged: API + #Access fallback) +-- POST /api/policy (writes to api-policy.json, triggers sync) ↓ policy.json (merged: API overrides #Access) ↓ wg-policy-engine.sh (unchanged) ↓ iptables / ipset rules ``` ### Key Changes from Original Flow: 1. **New**: `wg-engine-api` (Go) becomes PRIMARY generator of `policy.json` 2. **Merge Logic**: `wg-engine-api` reads both `api-policy.json` (API-managed) and `wg0.conf` (`#Access`), merges them (API overrides) 3. **Fallback**: If no API policy exists for a client, fall back to `#Access` comment 4. **Locking**: `wg-engine-api` uses SAME lock file (`/var/lock/wg-policy.lock`) with `flock` 5. **Atomic Writes**: Follow pattern from `wg-policy-lib.sh` (write to tmp, then `mv`) --- ## Task Sections ### Phase A: Submodule Initialization - [ ] **Task A1**: Initialize WGRplane submodule at `/app` - File: `/app` (new submodule directory) - Command: `git submodule add https://git.datadunia.com/hainzero/WGRplane.git app` - Followed by: `git submodule update --init --recursive` - QA: `git submodule status` shows `app` with commit hash, no errors - QA: `/app` directory exists with WGRplane files (Python/Flask backend, Vue.js frontend) - [ ] **Task A2**: Verify WGRplane structure and dependencies - File: `/app` (submodule contents) - Inspect: `ls /app` — should contain Python backend, Vue.js frontend, requirements.txt - Verify WGDashboard-equivalent structure: `app.py` or similar Flask entry point - QA: WGRplane files present, Python/Flask + Vue.js stack confirmed - QA: `cat /app/requirements.txt` shows Flask, SQLite, other dependencies - [ ] **Task A3**: Create Go module for `wg-engine-api` in `/app` - File: `/app/wg-engine-api/main.go` (new) - File: `/app/wg-engine-api/go.mod` (new) - Command: `cd /app/wg-engine-api && go mod init git.datadunia.com/hainzero/WGRplane/wg-engine-api` - Dependencies: `github.com/gorilla/mux` (router), `github.com/coreos/go-systemd` (optional) - QA: `ls /app/wg-engine-api/` shows `main.go`, `go.mod`, `go.sum` - QA: `cd /app/wg-engine-api && go build` succeeds without errors ### Phase B: WGRplane Base Setup (WGDashboard Parity) - [ ] **Task B1**: Review WGDashboard features for parity checklist - Reference: Librarian findings (bg_f53966bd) — full feature list - Features to implement: peer CRUD, QR codes, real-time monitoring, scheduling, TOTP auth, multi-server, plugins, i18n, themes - File: `/app/README.md` (document feature parity status) - QA: Checklist created with ALL WGDashboard features mapped to WGRplane implementation status - [ ] **Task B2**: Configure WGRplane to use port 10086 (WGDashboard default) - File: `/app/app.py` or `/app/config.json` (WGRplane config) - Set: `app_port = 10086` (consistent with WGDashboard) - Ensure: Does not conflict with `wg-engine-api` on port 10087 - QA: `curl http://localhost:10086` returns WGRplane dashboard page - QA: Port 10086 in use by WGRplane, 10087 available for wg-engine-api - [ ] **Task B3**: Integrate WGRplane with existing WireGuard config path - File: `/app/app.py` (WGRplane backend) - Set WireGuard config path: `/etc/wireguard/wg0.conf` (consistent with existing scripts) - QA: WGRplane can read `/etc/wireguard/wg0.conf` and list peers - QA: WGRplane "Add Peer" creates valid WireGuard config entries - [ ] **Task B4**: Add policy.json API awareness to WGRplane frontend - File: `/app/src/views/` or `/app/src/components/` (Vue.js components) - Add: New UI section for "Policy API" (link to `http://localhost:10087/api/policy`) - Note: WGRplane frontend will proxy or link to Go API (decision: proxy via Flask or direct link) - QA: WGRplane UI shows "Policy API" section with link to `localhost:10087` - QA: Clicking link opens `http://localhost:10087/api/policy` (with auth header) ### Phase C: Go wg-engine-api Development - [ ] **Task C1**: Implement Go API server skeleton with routing - File: `/app/wg-engine-api/main.go` - Framework: `github.com/gorilla/mux` (router) - Port: **10087** (avoid conflict with WGDashboard's 10086) - Endpoints skeleton: `GET /api/policy`, `POST /api/policy`, `POST /api/reload` - Auth middleware: Check `wg-rplane-datadunia` header - QA: `go build` succeeds, binary runs on port 10087 - QA: `curl -H "wg-rplane-datadunia: test" http://localhost:10087/api/policy` returns 200 or 401 (if auth enforced) - [ ] **Task C2**: Implement locking mechanism (flock) in Go - File: `/app/wg-engine-api/main.go` (lock function) - Lock file: `/var/lock/wg-policy.lock` (SAME as existing scripts) - Implementation: Use `syscall.Flock()` or exec `flock` command - Follow pattern from `wg-policy-lib.sh`: `flock -x -w 10` - QA: Simultaneous API calls do not corrupt `policy.json` - QA: Lock acquired within 10 seconds, else return 503 (timeout) - [ ] **Task C3**: Implement atomic write for policy.json in Go - File: `/app/wg-engine-api/main.go` (write function) - Pattern: Write to tmp file → `mv` (atomic, same filesystem) - Reference: `wg-sync-policy.sh` lines 124-126: `mv -f "$tmp_policy" "$POLICY_FILE"` - Tmp path: `/etc/wireguard/policy.json.tmp` - QA: `policy.json` never partially written (crash during write doesn't corrupt) - QA: `jq empty /etc/wireguard/policy.json` validates JSON after write - [ ] **Task C4**: Add CLI flag for sync without HTTP server - File: `/app/wg-engine-api/main.go` (flag parsing) - Flag: `--sync` (perform merge + write to `policy.json`, then exit) - Use case: Called by `wg-policy.service` instead of `wg-sync-policy.sh` - QA: `./wg-engine-api --sync` exits 0, updates `policy.json` - QA: After `--sync`, `wg-policy-ctl policy` shows merged data ### Phase D: Policy API Implementation - [ ] **Task D1**: Implement `GET /api/policy` (merged: API + #Access fallback) - File: `/app/wg-engine-api/main.go` (GET handler) - Step 1: Read API storage (`/etc/wireguard/api-policy.json`) - Step 2: Parse `wg0.conf` for `#Access` comments (fallback, using Go or exec `wg-sync-policy.sh`) - Step 3: Merge (API entries OVERRIDE `#Access` for same IP) - Step 4: Return merged JSON with same structure as `policy.json` - QA: `curl -H "wg-rplane-datadunia: VALID" http://localhost:10087/api/policy` returns merged JSON - QA: Client with API policy + `#Access` → API policy wins in response - QA: Client with ONLY `#Access` → fallback returns `#Access` value - [ ] **Task D2**: Implement `POST /api/policy` (update API-managed policy) - File: `/app/wg-engine-api/main.go` (POST handler) - Input: JSON body `{"ip": "10.0.0.2", "access": ["1.1.1.1/32"], "internet": true}` - Validate: IP and CIDRs using Go validation functions (port from `wg-policy-lib.sh`) - Save to: `/etc/wireguard/api-policy.json` (API-managed storage) - After save: Acquire lock → read/merge → atomic write to `policy.json` → trigger `wg-policy-engine.sh` - QA: POST returns 200, `api-policy.json` updated - QA: `policy.json` updated with merged data (API overrides #Access) - QA: `wg-policy-ctl rules` shows new targets after POST - [ ] **Task D3**: Implement `POST /api/reload` (trigger policy engine) - File: `/app/wg-engine-api/main.go` (reload handler) - Action: Exec `/usr/local/bin/wg-policy-engine.sh` - Optional: Also exec `/usr/local/bin/wg-sync-policy.sh` first (if #Access fallback needed) - QA: `curl -X POST -H "wg-rplane-datadunia: VALID" http://localhost:10087/api/reload` returns 200 - QA: After reload, `wg-policy-ctl status` shows engine applied successfully - [ ] **Task D4**: Implement authentication middleware - File: `/app/wg-engine-api/main.go` (middleware) - Header: `wg-rplane-datadunia` - Validation: Check header exists and matches configured token (from env or config file) - Return: 401 Unauthorized if missing/invalid - QA: `curl http://localhost:10087/api/policy` (no header) → 401 - QA: `curl -H "wg-rplane-datadunia: wrong" http://localhost:10087/api/policy` → 401 - QA: `curl -H "wg-rplane-datadunia: VALID" http://localhost:10087/api/policy` → 200 - [ ] **Task D5**: Create API storage file (`api-policy.json`) with schema - File: `/etc/wireguard/api-policy.json` (new, API-managed) - Schema: Same as `policy.json` but ONLY API-managed entries: ```json { "clients": { "10.0.0.2": { "name": "10.0.0.2", "access": ["1.1.1.1/32"], "internet": true } } } ``` - Initialize: Empty `{"clients": {}}` on first run - QA: `api-policy.json` exists after first API call - QA: JSON structure matches `policy.json` schema ### Phase E: #Access Migration & Merge Logic - [ ] **Task E1**: Implement #Access comment parser in Go (fallback) - File: `/app/wg-engine-api/main.go` (parse function) - Method: Exec `wg-sync-policy.sh` OR parse `wg0.conf` directly in Go - Prefer: Parse `wg0.conf` in Go (avoid exec dependency) - Logic: Read `[Peer]` blocks, extract `#Access` and `#Internet` lines - QA: Go parser extracts same data as `wg-sync-policy.sh` awk script - QA: `curl GET /api/policy` with no API policy returns `#Access` data correctly - [ ] **Task E2**: Implement merge logic (API overrides #Access) - File: `/app/wg-engine-api/main.go` (merge function) - Logic: For each client IP: 1. Start with `#Access` parsed data (fallback) 2. Override with API-managed data (from `api-policy.json`) 3. API takes precedence for same IP - Output: Merged JSON matching `policy.json` structure - QA: Client with API policy `"access": ["1.1.1.1/32"]` + `#Access 2.2.2.2/32` → GET returns `["1.1.1.1/32"]` - QA: Client with ONLY `#Access 2.2.2.2/32` → GET returns `["2.2.2.2/32"]` - [ ] **Task E3**: Handle `internet` flag merge - File: `/app/wg-engine-api/main.go` (merge function extension) - Logic: Same as access merge — API `internet` flag overrides `#Internet` comment - QA: Client with API `internet: true` + no `#Internet` in wg0.conf → GET returns `true` - QA: Client with API `internet: false` + `#Internet true` in wg0.conf → GET returns `false` - [ ] **Task E4**: Update `wg-policy.service` to use Go API sync (optional, recommended) - File: `wg-policy.service` (systemd unit) - Change: `ExecStartPre` from `wg-sync-policy.sh` to `wg-engine-api --sync` - Note: NOT modifying `.sh` scripts (only systemd unit) - QA: `systemctl daemon-reload && systemctl restart wg-policy.service` succeeds - QA: Service uses Go API for sync instead of shell script ### Phase F: Integration & Testing - [ ] **Task F1**: Add `bats` test framework for shell script validation - File: `/tests/` (new directory) or use existing pattern - Test cases: Policy.json validation, JSON structure, lock file behavior - Install: `apt install bats` (add to `install.sh` if needed) - QA: `bats /tests/policy.bats` passes all test cases - QA: Test coverage for `wg-policy-ctl validate` command - [ ] **Task F2**: Add Go tests for `wg-engine-api` - File: `/app/wg-engine-api/main_test.go` (new) - Test cases: Auth middleware, GET/POST handlers, merge logic, lock mechanism - Run: `cd /app/wg-engine-api && go test ./...` - QA: `go test` passes with >80% coverage - QA: Mock `wg0.conf` and `api-policy.json` for isolated tests - [ ] **Task F3**: Integration test: Full flow validation - Test: POST to API → policy.json updated → iptables rules applied - Steps: 1. `curl -X POST ... http://localhost:10087/api/policy` (add client) 2. Verify `policy.json` updated (check with `wg-policy-ctl policy`) 3. Verify iptables rules (check with `wg-policy-ctl rules`) - QA: All 3 steps succeed in sequence - QA: Fallback to `#Access` works when API has no entry for client - [ ] **Task F4**: Manual test documentation in README - File: `/app/README.md` (test section) - Document: How to run bats tests, Go tests, manual QA scenarios - Note: No CI (none exists in repo), document manual commands - QA: README has clear "Testing" section with commands - QA: New developer can follow README to run all tests ### Phase G: Documentation & Push ### Phase G: Documentation & Push - [ ] **Task G1**: Create comprehensive README.md for WGRplane submodule - File: `/app/README.md` (new or update existing) - Sections: Overview, Architecture, API Endpoints, Authentication, Integration with WGDashboard, Testing, Deployment - Document: Go API endpoint (`http://localhost:10087/api/policy`), auth header `wg-rplane-datadunia` - QA: README.md exists with all sections - QA: `cat /app/README.md` shows complete documentation - [ ] **Task G2**: Document integration between WGRplane (Python) and wg-engine-api (Go) - File: `/app/README.md` (integration section) - Explain: WGRplane on port 10086, Go API on port 10087 - Note: Frontend can proxy API requests or link directly - QA: README has "Integration" section with port numbers and proxy examples - QA: Developer understands how Python Flask talks to Go API - [ ] **Task G3**: Push WGRplane submodule to remote - Commands: ```bash cd /app git add . git commit -m "Init WGRplane submodule with Go wg-engine-api" git push origin main # or master, depending on remote default ``` - QA: `git push` succeeds, remote updated - QA: `git submodule status` in parent repo shows app with commit hash - [ ] **Task G4**: Update parent repo to reference pushed submodule - Commands: ```bash cd /path/to/03.wireguard-policy git add .gitmodules app git commit -m "Add WGRplane submodule with policy.json API" git push ``` - QA: Parent repo pushed with submodule reference - QA: Fresh clone of parent repo can `git submodule update --init --recursive` successfully --- ## Final Verification Wave **QA Scenarios (ALL must pass before marking work complete):** 1. **Submodule init**: `git submodule status` shows `app` pointing to `https://git.datadunia.com/hainzero/WGRplane.git` 2. **API auth**: `curl -H "wg-rplane-datadunia: wrong" http://localhost:10087/api/policy` returns **401 Unauthorized** 3. **Policy retrieval**: `curl -H "wg-rplane-datadunia: VALID_TOKEN" http://localhost:10087/api/policy` returns merged JSON (API + #Access fallback) 4. **Policy update**: `curl -X POST -H "Content-Type: application/json" -H "wg-rplane-datadunia: VALID_TOKEN" -d '{"ip": "10.0.0.2", "access": ["1.1.1.1/32"]}' http://localhost:10087/api/policy` returns **200** and updates `policy.json` 5. **#Access fallback**: Client with NO API policy but HAS `#Access` in `wg0.conf` → API returns `#Access` value in GET 6. **iptables application**: After POST, run `wg-policy-ctl rules` → new target visible in `WG_POLICY` chain 7. **Lock conflict prevention**: Simultaneous API call and `wg-sync-policy.sh` do not corrupt `policy.json` 8. **README exists**: `/app/README.md` present with full documentation 9. **Submodule pushed**: `git push` in `/app` succeeds, remote initialized **User Confirmation Required**: Run ALL QA scenarios above and confirm **"okay"** before marking work complete.