docs(wireguard): document the VPN console and the invariants behind it

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
feat/portal-design
nexxo 2026-07-25 22:02:46 +02:00
parent d1302e17bc
commit cad245c5df
1 changed files with 44 additions and 0 deletions

View File

@ -56,6 +56,50 @@ and skipping every `hosts.wg_ip` already taken. `addPeer()`/`removePeer()` run
`wg set` and then `wg-quick save wg0` — without the save, peers are lost on the `wg set` and then `wg-quick save wg0` — without the save, peers are lost on the
next restart. next restart.
## The console (/admin/vpn)
Create, block and remove accesses, and see who is actually connected. Requires
the `vpn.manage` capability (Owner/Admin) — it also hides the nav entry, because
a VPN access reaches the management network.
The web container has no wg0, so the page never talks to WireGuard. `SyncVpnPeers`
runs on the provisioning queue (every minute from the scheduler, plus a throttled
nudge from the page's five-second poll) and copies handshakes, traffic counters
and source endpoints into `vpn_peers`. The page reads the database.
Two fields carry the state, and keeping them apart is what makes the page honest:
- `enabled` — the operator's intent
- `present` — what the hub actually reports
They disagree only while a change is in flight, which the UI shows as
"wird angewendet". A sync never writes `enabled`.
Peers the host pipeline registers are adopted into the same list and named after
their host, so "who is on the VPN" has no blind spots.
Things that are deliberate, and worth knowing before changing them:
- **Deleting is a soft delete.** The row survives as a tombstone until the hub
confirms the peer is gone. A hard delete would let the next sync see the
still-present peer and adopt it back as a live access — silently restoring
what was just revoked. The tombstone also keeps the key and tunnel address
reserved meanwhile.
- **Every hub mutation takes `Cache::lock('wireguard:hub')`** — ApplyVpnPeer,
RemoveWireguardPeer and ConfigureWireguard. Without it a sync could read the
interface around a mutation and then write what it saw.
- **Address allocation takes `Cache::lock('wireguard:allocate')`.** Addresses
come from one subnet but live in two tables (`hosts.wg_ip`,
`vpn_peers.allowed_ip`), so no unique index can catch the other side's insert.
- **ApplyVpnPeer reads the current row**, not the state captured at dispatch: a
retried job must not undo a newer decision. A missing row can only mean the
access is gone, so a stale payload may remove but never enable.
- **A host's key cannot be re-used** for an operator access: `wg set` would
rewrite that host's allowed-ip and cut its management tunnel.
- Generated keypairs come from libsodium in PHP, not `wg genkey` — this
container has no interface, and it keeps generation testable. The private key
is shown once and never stored.
## Verifying it actually works ## Verifying it actually works
Mocked tests cannot prove a tunnel. Against the running stack: Mocked tests cannot prove a tunnel. Against the running stack: