docs(vpn): runbook for the key-issuer, and the gap it leaves open

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.
This commit is contained in:
Local Dev 2026-09-27 19:06:58 +02:00
parent 83b65498a2
commit 5a17b0e866

View file

@ -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": "<reality public key>", "sid": "<short id>",
"flow": "xtls-rprx-vision",
"tiers": ["free","pro","max"],
"pool": ["<uuid>", "…"] } ] }
```
`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: