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: