docs(wireguard): document the VPN console and the invariants behind it
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>feat/portal-design
parent
d1302e17bc
commit
cad245c5df
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue