18 KiB
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
/appas submodule fromhttps://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 #Accessmigration: API with#Accessfallback (API tries first, falls back to#Accesscomment parsing)- Custom API authentication:
wg-rplane-dataduniaheader - 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:
batsfor shell scripts, Go testing forwg-engine-api
OUT (Explicitly Excluded)
- NO modifications to existing
.shscripts (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:
- New:
wg-engine-api(Go) becomes PRIMARY generator ofpolicy.json - Merge Logic:
wg-engine-apireads bothapi-policy.json(API-managed) andwg0.conf(#Access), merges them (API overrides) - Fallback: If no API policy exists for a client, fall back to
#Accesscomment - Locking:
wg-engine-apiuses SAME lock file (/var/lock/wg-policy.lock) withflock - Atomic Writes: Follow pattern from
wg-policy-lib.sh(write to tmp, thenmv)
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 statusshowsappwith commit hash, no errors - QA:
/appdirectory exists with WGRplane files (Python/Flask backend, Vue.js frontend)
- File:
-
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.pyor similar Flask entry point - QA: WGRplane files present, Python/Flask + Vue.js stack confirmed
- QA:
cat /app/requirements.txtshows Flask, SQLite, other dependencies
- File:
-
Task A3: Create Go module for
wg-engine-apiin/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/showsmain.go,go.mod,go.sum - QA:
cd /app/wg-engine-api && go buildsucceeds without errors
- File:
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.pyor/app/config.json(WGRplane config) - Set:
app_port = 10086(consistent with WGDashboard) - Ensure: Does not conflict with
wg-engine-apion port 10087 - QA:
curl http://localhost:10086returns WGRplane dashboard page - QA: Port 10086 in use by WGRplane, 10087 available for wg-engine-api
- File:
-
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.confand list peers - QA: WGRplane "Add Peer" creates valid WireGuard config entries
- File:
-
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)
- File:
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-dataduniaheader - QA:
go buildsucceeds, binary runs on port 10087 - QA:
curl -H "wg-rplane-datadunia: test" http://localhost:10087/api/policyreturns 200 or 401 (if auth enforced)
- File:
-
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 execflockcommand - 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)
- File:
-
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.shlines 124-126:mv -f "$tmp_policy" "$POLICY_FILE" - Tmp path:
/etc/wireguard/policy.json.tmp - QA:
policy.jsonnever partially written (crash during write doesn't corrupt) - QA:
jq empty /etc/wireguard/policy.jsonvalidates JSON after write
- File:
-
Task C4: Add CLI flag for sync without HTTP server
- File:
/app/wg-engine-api/main.go(flag parsing) - Flag:
--sync(perform merge + write topolicy.json, then exit) - Use case: Called by
wg-policy.serviceinstead ofwg-sync-policy.sh - QA:
./wg-engine-api --syncexits 0, updatespolicy.json - QA: After
--sync,wg-policy-ctl policyshows merged data
- File:
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.conffor#Accesscomments (fallback, using Go or execwg-sync-policy.sh) - Step 3: Merge (API entries OVERRIDE
#Accessfor same IP) - Step 4: Return merged JSON with same structure as
policy.json - QA:
curl -H "wg-rplane-datadunia: VALID" http://localhost:10087/api/policyreturns merged JSON - QA: Client with API policy +
#Access→ API policy wins in response - QA: Client with ONLY
#Access→ fallback returns#Accessvalue
- File:
-
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→ triggerwg-policy-engine.sh - QA: POST returns 200,
api-policy.jsonupdated - QA:
policy.jsonupdated with merged data (API overrides #Access) - QA:
wg-policy-ctl rulesshows new targets after POST
- File:
-
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.shfirst (if #Access fallback needed) - QA:
curl -X POST -H "wg-rplane-datadunia: VALID" http://localhost:10087/api/reloadreturns 200 - QA: After reload,
wg-policy-ctl statusshows engine applied successfully
- File:
-
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
- File:
-
Task D5: Create API storage file (
api-policy.json) with schema- File:
/etc/wireguard/api-policy.json(new, API-managed) - Schema: Same as
policy.jsonbut ONLY API-managed entries:{ "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.jsonexists after first API call - QA: JSON structure matches
policy.jsonschema
- File:
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.shOR parsewg0.confdirectly in Go - Prefer: Parse
wg0.confin Go (avoid exec dependency) - Logic: Read
[Peer]blocks, extract#Accessand#Internetlines - QA: Go parser extracts same data as
wg-sync-policy.shawk script - QA:
curl GET /api/policywith no API policy returns#Accessdata correctly
- File:
-
Task E2: Implement merge logic (API overrides #Access)
- File:
/app/wg-engine-api/main.go(merge function) - Logic: For each client IP:
- Start with
#Accessparsed data (fallback) - Override with API-managed data (from
api-policy.json) - API takes precedence for same IP
- Start with
- Output: Merged JSON matching
policy.jsonstructure - 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"]
- File:
-
Task E3: Handle
internetflag merge- File:
/app/wg-engine-api/main.go(merge function extension) - Logic: Same as access merge — API
internetflag overrides#Internetcomment - QA: Client with API
internet: true+ no#Internetin wg0.conf → GET returnstrue - QA: Client with API
internet: false+#Internet truein wg0.conf → GET returnsfalse
- File:
-
Task E4: Update
wg-policy.serviceto use Go API sync (optional, recommended)- File:
wg-policy.service(systemd unit) - Change:
ExecStartPrefromwg-sync-policy.shtowg-engine-api --sync - Note: NOT modifying
.shscripts (only systemd unit) - QA:
systemctl daemon-reload && systemctl restart wg-policy.servicesucceeds - QA: Service uses Go API for sync instead of shell script
- File:
Phase F: Integration & Testing
-
Task F1: Add
batstest 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 toinstall.shif needed) - QA:
bats /tests/policy.batspasses all test cases - QA: Test coverage for
wg-policy-ctl validatecommand
- File:
-
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 testpasses with >80% coverage - QA: Mock
wg0.confandapi-policy.jsonfor isolated tests
- File:
-
Task F3: Integration test: Full flow validation
- Test: POST to API → policy.json updated → iptables rules applied
- Steps:
curl -X POST ... http://localhost:10087/api/policy(add client)- Verify
policy.jsonupdated (check withwg-policy-ctl policy) - Verify iptables rules (check with
wg-policy-ctl rules)
- QA: All 3 steps succeed in sequence
- QA: Fallback to
#Accessworks 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
- File:
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 headerwg-rplane-datadunia - QA: README.md exists with all sections
- QA:
cat /app/README.mdshows complete documentation
- File:
-
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
- File:
-
Task G3: Push WGRplane submodule to remote
- Commands:
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 pushsucceeds, remote updated - QA:
git submodule statusin parent repo shows app with commit hash
- Commands:
-
Task G4: Update parent repo to reference pushed submodule
- Commands:
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 --recursivesuccessfully
- Commands:
Final Verification Wave
QA Scenarios (ALL must pass before marking work complete):
- Submodule init:
git submodule statusshowsapppointing tohttps://git.datadunia.com/hainzero/WGRplane.git - API auth:
curl -H "wg-rplane-datadunia: wrong" http://localhost:10087/api/policyreturns 401 Unauthorized - Policy retrieval:
curl -H "wg-rplane-datadunia: VALID_TOKEN" http://localhost:10087/api/policyreturns merged JSON (API + #Access fallback) - 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/policyreturns 200 and updatespolicy.json - #Access fallback: Client with NO API policy but HAS
#Accessinwg0.conf→ API returns#Accessvalue in GET - iptables application: After POST, run
wg-policy-ctl rules→ new target visible inWG_POLICYchain - Lock conflict prevention: Simultaneous API call and
wg-sync-policy.shdo not corruptpolicy.json - README exists:
/app/README.mdpresent with full documentation - Submodule pushed:
git pushin/appsucceeds, remote initialized
User Confirmation Required: Run ALL QA scenarios above and confirm "okay" before marking work complete.