Self-hosted travel router
A Pi 4 you carry. Connect it to whatever WiFi the hotel hands you; expose your own AP to your devices; route everything through a pool of three VPN exits in different jurisdictions. When the hotel WiFi drops, the kill-switch engages — devices see “no internet” instead of fumbling onto the airport open network across the road.
An operator travels for work. They want every device they carry — laptop, phone, e-reader — to:
- Connect to a single trusted AP they bring with them, regardless of which hotel / café WiFi they’re on.
- Route all outbound traffic through a pool of three VPN tunnels.
- Never leak through the upstream WiFi, even briefly, when tunnels cycle.
- Work in countries with active DPI — pluggable transports as a fallback.
Hardware & install
Section titled “Hardware & install”- Raspberry Pi 4 (4 GB) with the official PoE+ HAT or a USB battery
- Single internal WiFi (
wlan0) — used as the AP (downstream). - Single USB WiFi dongle (
wlan1) — used as the client (upstream to hotel WiFi). - Optional: an LTE USB modem (
wwan0) as a fallback upstream.
Install profile: rpi. Network mode: portable.
sudo ./scripts/install-gateway.sh --profile rpiThen in the bootstrap config (/var/lib/gateway/config.yaml):
network: mode: portable wan_iface: wlan1 # upstream — USB dongle lan_iface: wlan0 # AP — internal radioHow “portable” mode differs
Section titled “How “portable” mode differs”Portable mode tells gatewayd:
- No DHCP relay on WAN — the upstream gives the gateway an address; downstream the gateway runs its own DHCP for the AP.
- No SLAAC / RA acceptance on WAN — don’t trust the upstream’s IPv6 advertisement (and probably block IPv6 outbound anyway).
- AP-first DNS — the AP’s DHCP hands out the gateway as the DNS server; the upstream DNS is never consulted directly by clients.
Connecting to a new hotel WiFi
Section titled “Connecting to a new hotel WiFi”The Interfaces page in the UI shows nearby SSIDs on wlan1 — select one, paste the password, and the daemon brings up the link. All client policies bound to wlan0 continue to apply, so changing the upstream is transparent to clients.
Behind the scenes the daemon writes a wpa_supplicant config for wlan1 and brings the interface up; you can do the same manually if you prefer (wpa_passphrase → wpa_supplicant -i wlan1 -c …), then leave it to the reconciler to keep the rest of the dataplane consistent.
Tunnels
Section titled “Tunnels”Three WireGuard tunnels with different exit countries:
wg-eu-01(Frankfurt)wg-us-01(NYC)wg-asia-01(Singapore)
Plus one bridge bridge-eu for countries with active DPI.
Privacy class
Section titled “Privacy class”Single class applied to every connected device. POST to https://gateway.lan:8080/api/v1/privacy-classes:
{ "name": "travel", "force_dns": true, "ipv6_policy": "block", "quic_policy": "block", "block_webrtc": true, "allow_direct_fallback": false}ipv6_policy: block so the hotel’s IPv6 isn’t trusted. quic_policy: block neutralises captive-portal hijacks that ride QUIC. allow_direct_fallback: false is what guarantees the kill-switch — clients never fall back to hotel WiFi when tunnels are down.
Plus, system-wide under Settings → Privacy, turn on block_doh and switch DNS mode to paranoid (which is what drops TCP/853 DoT) so even apps that hard-code their own encrypted-DNS endpoints can’t bypass the resolver.
Policy
Section titled “Policy”One policy on the AP interface catches every client. No per-device config needed when travelling. Multi-vpn-hop + splitting: true produces the 3-way round-robin pool, with the fail-closed kill-switch on the policy itself:
curl -X POST https://gateway.lan:8080/api/v1/routing/policies \ -H "Authorization: Bearer $API_KEY" -d '{ "name": "travel", "priority": 100, "enabled": true, "source_type": "interface", "source_value": "wlan0", "hops": [ {"type":"vpn","target":"wg-eu-01"}, {"type":"vpn","target":"wg-us-01"}, {"type":"vpn","target":"wg-asia-01"} ], "splitting": true, "fail_policy": "fail-closed" }'
curl -X POST https://gateway.lan:8080/api/v1/routing/policies/apply \ -H "Authorization: Bearer $API_KEY"Privacy level 4 (jitter + padding + cover traffic + rotation) lives on the policy’s route profile — set via PUT /api/v1/profiles/{id}. See Traffic splitting.
Captive portal handling
Section titled “Captive portal handling”Most hotel networks require accepting a captive portal page before granting internet. Run gwctl captive to detect the redirect:
gwctl captive# Detected: Yes# Probe URL: http://detectportal.example/# Status Code: 302# Redirect To: https://hotel-portal.example/loginOnce detected, the operator typically:
- Adds a short-lived exception routing the captive-detection target (and the portal hostname)
directwith a 10-minute TTL. - Opens a browser, accepts the portal terms.
- Waits for the exception to auto-expire — the kill-switch and full policy resume.
There is no global “captive mode” toggle; the daemon prefers explicit, time-bounded exceptions so you never forget to re-enable fail-closed.
Bluetooth tethering as WAN failover
Section titled “Bluetooth tethering as WAN failover”For trips where the hotel WiFi is unreliable but a phone tether always works: pair the operator’s phone as a Bluetooth PAN peer once, and let the daemon failover to it automatically when the primary uplink goes down.
# one-time pairing on the boxgwctl bluetooth enablegwctl bluetooth scangwctl bluetooth pair <phone-MAC>Then configure the failover policy:
curl -X PUT https://gateway.lan:8080/api/v1/bluetooth/failover \ -H "Authorization: Bearer $API_KEY" \ -d '{ "enabled": true, "phone_mac": "AA:BB:CC:DD:EE:FF", "delay_seconds": 30, "auto_connect": true }'How it behaves:
- The daemon polls the primary WAN every 10 s. After
delay_seconds / 10consecutive failures (minimum 3), it connects to the phone over Bluetooth PAN and brings upbnep0as a WAN uplink. bnep0is treated as untrusted WAN, exactly like the hotel WiFi was. Traffic still has to go through your VPN pool — the kill-switch will never letbnep0itself act as a tunnel.- When the primary returns and stays up for one poll cycle, the daemon disconnects PAN automatically so the phone stops carrying traffic.
This is for emergencies — Bluetooth PAN sits at 1-2 Mbit/s in practice. Web works, video doesn’t.
Or: serve other devices over Bluetooth (NAP host mode)
Section titled “Or: serve other devices over Bluetooth (NAP host mode)”The inverse of failover. When WiFi is jammed or you’re in a no-WiFi-radio zone (some museums, certain offices), the gateway can act as a Bluetooth Network Access Point — other devices pair with the Pi as a PANU client and get internet through it:
curl -X POST https://gateway.lan:8080/api/v1/bluetooth/nap/start \ -H "Authorization: Bearer $API_KEY" \ -d '{"subnet":"10.44.0.1/24"}'This creates a bt-pan0 bridge interface, runs DHCP (range .10-.50, 12 h leases) on it, and registers org.bluez.NetworkServer1 for nap. The Pi advertises itself for 120 seconds (discoverable timeout — keeps the attack window narrow), then accepts paired clients silently.
NAP and failover are mutually exclusive on the radio. Run failover when the Pi needs an uplink; run NAP when other devices need one. Switch with POST /api/v1/bluetooth/nap/stop before re-arming failover.
What happens when
Section titled “What happens when”| Scenario | Result |
|---|---|
| Connected to hotel WiFi, all tunnels up | All client traffic exits via a 3-way split. Hotel WiFi sees only WireGuard handshakes. |
| Hotel WiFi drops | Tunnels go down. Kill-switch engages. Clients see “no internet” — no fallback to the open WiFi at the airport across the road. |
| Hotel WiFi drops, BT failover armed | After ~30 s of WAN-down, the daemon auto-connects PAN to the paired phone; tunnels re-handshake over bnep0; clients stay online at 1-2 Mbit/s. Switch back is automatic. |
| Hotel uses DPI to block WireGuard | Bring up bridge-eu and add a policy with dest_domain: * over the bridge for affected destinations. |
| Battery / no AC | The PoE HAT runs the Pi off a battery bank for hours. The AP keeps serving. |
Why this beats “just use a VPN client on the laptop”
Section titled “Why this beats “just use a VPN client on the laptop””- A laptop VPN client only protects the laptop. Your phone, your e-reader, and any guest device sharing your AP all leak.
- A laptop VPN client doesn’t fail-closed cleanly on every OS. The Gateway always does.
- A laptop VPN client doesn’t give you split tunnels across regions for jurisdictional diversity.
- A laptop VPN client doesn’t run a bridge if WireGuard is blocked.
You move one device — the Pi — and every device behind it gets the same posture.
Next steps
Section titled “Next steps”Before you travel:
- Turn on the dead-man’s switch with
hours: 24. If the Pi is left behind in a hotel room and you don’t return, sensitive tables auto-wipe. - Enable LUKS storage for forensic resistance on a powered-off device. Accept the SD-wear cost — it’s a travel box.
- Pair your phone for Bluetooth WAN failover so an unreliable hotel uplink doesn’t leave you offline.
- Test the leak checker at every new network —
gwctl leak-checkconfirms posture in 5 seconds.
Related
Section titled “Related”- Privacy & kill-switch — what keeps the hotel WiFi from ever seeing a plaintext packet.
- Bridge proxies — the DPI-resistant fallback.
- Troubleshooting — captive-portal edge cases, mDNS, common hotel-WiFi quirks.