From 5a17b0e86667d8912125093584bc1b8d8c55b11c Mon Sep 17 00:00:00 2001 From: Local Dev Date: Sun, 27 Sep 2026 19:06:58 +0200 Subject: [PATCH] docs(vpn): runbook for the key-issuer, and the gap it leaves open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records what the gateway routes do, the four things that still have to happen on the servers before the dropdown can light up (UUID pool per inbound, the pool file, X-Forwarded-For on the vhost, deploy-from-repo), and — deliberately at the same prominence — that expiry is bookkeeping rather than enforcement until xray's handler API is wired per box. Also flags the per-client byte cap as an unverified cheaper mitigation, marked as needing a test rather than written up as though it works. --- bundled-addons/vpn/DESIGN.md | 85 ++++++++++++++++++++++++++++++++++++ 1 file changed, 85 insertions(+) diff --git a/bundled-addons/vpn/DESIGN.md b/bundled-addons/vpn/DESIGN.md index 8766e49f..af33905e 100644 --- a/bundled-addons/vpn/DESIGN.md +++ b/bundled-addons/vpn/DESIGN.md @@ -68,6 +68,91 @@ Working credentials for the three live exits are in the repo-root `.keys/vpn-servers.json` (gitignored via `.gitignore:70 /.keys/`) for operator testing via the panel's Custom box. They stay there. +### Built: the gateway side + +Landed in `Argus/src/gateway/public-gateway.mjs` as two routes. Smoke +test: `node scratchpad/vpn-session-smoke.mjs` (17 checks, stages the +flat VPS layout so it runs the real file). + +**`GET /api/vpn/servers`** — exit list, Reality material only, no UUID. +Cached 5 min. This is what the addon's `SERVER_LIST_URL` already points +at, so once the pool file exists on the VPS the dropdown lights up with +no addon change. + +**`POST /api/vpn/session`** `{ serverId, tier?, address?, ts?, sig? }` +→ `{ vless, uuid, serverId, tier, expiresAt, reused }`. Leases one +pre-provisioned UUID for `BNS_VPN_TTL_MS` (default 24 h). Re-asking +inside the TTL returns the same lease. + +#### What still has to happen on the servers + +1. **Provision a UUID pool per inbound.** Each exit's xray config needs + N clients instead of one. On each box, for N=50: + ```bash + for i in $(seq 50); do /usr/local/bin/xray uuid; done + ``` + then put them in that inbound's `settings.clients` array (each with + `"flow": "xtls-rprx-vision"`) and `systemctl restart xray`. + +2. **Write `/opt/bns-gateway/vpn-pool.json`** on the gateway box. Shape: + ```json + { "servers": [ + { "id": "sm-1", "label": "Silent Mode · 1", + "host": "…", "port": 8443, "sni": "www.cloudflare.com", + "fp": "chrome", "pbk": "", "sid": "", + "flow": "xtls-rprx-vision", + "tiers": ["free","pro","max"], + "pool": ["", "…"] } ] } + ``` + `chmod 600`, owned by the gateway user. Never committed, never + uploaded, never served. `tiers` is what gates free/pro/max, so + moving an exit between tiers is an edit here, not a code change. + +3. **Add `X-Forwarded-For` to the vhost.** In + `VPS/SilentMode/silentmode-st.angie.conf`, the `/api/` location + forwards only `Host`. Without the client address the gateway cannot + tell callers apart and `/api/vpn/session` answers 503 by design: + ``` + location /api/ { + proxy_pass http://127.0.0.1:8700; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $remote_addr; # <- add + } + ``` + +4. **Deploy the gateway from the repo** — per + [gateway-deploy-from-repo](../../../../Users/valer/.claude/projects/D--Dev-SilentMode/memory/gateway-deploy-from-repo.md), + never edit `/opt/bns-gateway` in place: + ```bash + ssh silentmode 'cp /opt/bns-gateway/public-gateway.mjs /opt/bns-gateway/public-gateway.mjs.bak.$(date +%s)' + scp Argus/src/gateway/public-gateway.mjs silentmode:/opt/bns-gateway/ + ssh silentmode 'systemctl restart bns-gateway' + curl https://navigate.st/api/tlds | head # must still say TLD_BEACON + curl -s https://navigate.st/api/vpn/servers | head + ``` + +#### The gap this does not close + +`expiresAt` is bookkeeping. xray keeps honouring a leased UUID until the +inbound config is rotated, so a TTL returns the slot to the pool but +does not disconnect anyone. Two consequences worth holding onto: + +- A user who keeps their UUID after expiry keeps working. Capacity, not + access, is what the TTL protects. +- Two people can end up on the same UUID over time (issued, expired, + re-issued), which weakens attribution. + +Closing it needs xray's handler API reachable per box — enable the `api` +inbound in each xray config, run a small localhost agent that does +`xray api adduser` / `rmuser`, and have the gateway call those agents +over an authenticated channel. At that point `pool` disappears from the +pool file and only the Reality material stays. + +A cheaper partial mitigation worth checking first: xray supports a +per-client byte `total` with the stats/policy services enabled. If that +works on a VLESS+Reality inbound it bounds abuse per UUID without any +new service. **Unverified — test before relying on it.** + ### `POST /api/vpn/new-client` Request body: