diff --git a/docs/wireguard-hub.md b/docs/wireguard-hub.md index 781200e..eb3358f 100644 --- a/docs/wireguard-hub.md +++ b/docs/wireguard-hub.md @@ -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 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 Mocked tests cannot prove a tunnel. Against the running stack: