VPN tunnels
A tunnel in The Gateway is any layer-3 path the daemon can route through. Today that means:
- WireGuard — preferred. In-kernel, fast, easy to script.
- OpenVPN — supported for providers that don’t offer WireGuard.
- SOCKS5 — for proxy endpoints (commercial proxies, custom relays, bridge nodes).
Every tunnel is an independent object with its own lifecycle, its own fwmark range, and its own routing table.
What you get per tunnel
Section titled “What you get per tunnel”- A managed interface (
wgNfor WireGuard,tunNfor OpenVPN). - A dedicated
ip rule+ routing table — never collides withmain. - An isolated network namespace (
netns) per tunnel via avethpair — prevents cross-tunnel route bleed and contains any per-tunnel daemon (redsocks, openvpn client) inside its namespace. - A latency probe that fires every few seconds and feeds the splitting engine’s “fastest tunnel” selector.
- A handshake watchdog that surfaces stale tunnels as alerts and (optionally) auto-reconnects.
- A per-tunnel kill-switch hook: traffic for policies bound to this tunnel drops if the tunnel goes down.
- Per-tunnel byte counters (RX / TX) for capacity planning.
Configuration
Section titled “Configuration”A WireGuard tunnel, as the daemon stores it:
{ "id": "wg-eu-01", "name": "EU exit", "type": "wireguard", "interface": "wg0", "listen_port": 51820, "private_key": "REDACTED_BASE64=", "address": "10.10.0.2/24", "peers": [{ "public_key": "REDACTED_BASE64=", "endpoint": "vpn.example.invalid:51820", "allowed_ips": ["0.0.0.0/0", "::/0"], "persistent_keepalive": 25 }]}The POST /api/v1/tunnels endpoint only accepts type: wireguard or type: socks5 (per internal/daemon/tunnel_handlers.go:151-156). OpenVPN uses a separate import path — POST /api/v1/tunnels/upload-ovpn with the raw .ovpn blob as the request body. The daemon parses it, runs the client in its own netns, and the tunnel then appears in the regular /tunnels list.
Fail-closed behaviour is not a tunnel attribute — it’s set on the policy or route profile that uses this tunnel. See Routing policies.
AmneziaWG obfuscation (DPI resistance)
Section titled “AmneziaWG obfuscation (DPI resistance)”For deployments in networks that fingerprint the WireGuard handshake, the daemon supports AmneziaWG — a wire-protocol-compatible WireGuard variant with operator-defined junk-packet padding and header transforms. Each install picks its own parameters; the resulting traffic doesn’t match the canonical WireGuard fingerprint that DPI gear keys on.
Add the obfuscation fields to a WireGuard tunnel create body:
{ "name": "wg-amnezia-eu", "type": "wireguard", "interface": "wg0", "address": "10.10.0.2/24", "private_key": "REDACTED=", "peers": [{ "public_key": "REDACTED=", "endpoint": "vpn.example.invalid:51820", "allowed_ips": ["0.0.0.0/0","::/0"], "persistent_keepalive": 25 }],
"amnezia_jc": 4, "amnezia_jmin": 50, "amnezia_jmax": 1000, "amnezia_s1": 100, "amnezia_s2": 200, "amnezia_h1": 1, "amnezia_h2": 2, "amnezia_h3": 3, "amnezia_h4": 4}When any of amnezia_jc / amnezia_s1 / amnezia_s2 is non-zero the daemon switches that interface from wireguard-go to amneziawg-go. The peer needs matching parameters — Amnezia configs are not interoperable with vanilla WireGuard peers.
Health probes
Section titled “Health probes”Two independent health signals are tracked per tunnel:
- Handshake age — for WireGuard, the kernel exposes the last-handshake timestamp. A handshake older than the keepalive interval + grace is flagged.
- Latency — an ICMP / UDP probe to a configured target inside the tunnel (default: the tunnel’s gateway IP). The result feeds the
splittingengine’s endpoint selector and is displayed per-tunnel in the UI.
When a handshake goes stale, the daemon will:
- fire an alert (rule
tunnel-flap, categorytunnel) - cycle the interface (
wg-quick down && up) - re-engage the kill-switch for any policy bound to the tunnel if reconnect fails
API surface
Section titled “API surface”| Method | Path | Purpose |
|---|---|---|
GET |
/api/v1/tunnels |
List with status, RTT, RX/TX |
POST |
/api/v1/tunnels |
Create a tunnel |
GET |
/api/v1/tunnels/{id} |
Single tunnel detail |
PUT |
/api/v1/tunnels/{id} |
Update |
POST |
/api/v1/tunnels/{id}/up |
Bring up (alias: /connect) |
POST |
/api/v1/tunnels/{id}/down |
Bring down (alias: /disconnect) |
GET |
/api/v1/tunnels/{id}/config |
Render the underlying WG / OVPN config |
DELETE |
/api/v1/tunnels/{id} |
Delete (refuses if in active policy) |
Next steps
Section titled “Next steps”A tunnel on its own routes nothing. The natural next moves:
- Bind it to traffic via a routing policy — without a policy your new tunnel sits idle.
- Add a second tunnel of a different provider, then enable traffic splitting to spread connections across both. Jurisdictional diversity, with one config.
- Set
fail_policy: fail-closedon the policy that uses it so a stale handshake doesn’t leak to your default route. See Privacy & kill-switch. - Watch the
tunnel-flapandtunnel-downalerts — a flaky provider hides easily behind RX/TX counters that look fine in aggregate.
Related
Section titled “Related”- Routing policies — bind a tunnel to traffic.
- Traffic splitting — distribute connections across a pool of tunnels.
- Privacy & kill-switch — what happens when a tunnel goes down.
- Recipes → Tunnels & bridges — add, cycle and register snippets.