WireGuard Multi-Tunnel: AllowedIPs, Policy Routing, Kill Switch
WireGuard Multi-Tunnel: AllowedIPs, Policy Routing, Kill Switch
The cryptokey-routing chapter showed one tunnel and one peer. Real laptops and edge routers often run several WireGuard interfaces at once: a full-tunnel VPN to work, a split-tunnel to a lab, maybe a site-to-site mesh peer. The hard part is not key exchange. It is who owns 0.0.0.0/0, how policy routing and fwmarks keep ciphertext off the wrong table, and how a kill switch (which is not a WireGuard feature) drops plaintext when the tunnel is down.
This chapter makes those three ideas concrete. Lab notes use Debian 13 ideas and wireguard-tools v1.0.20210914 as found on this host; privileged ip netns drills need root and are shown as scripts you run on a disposable machine. No packages are installed in the scout sandbox.
wg version
ip -V | head -1wireguard-tools v1.0.20210914 - https://git.zx2c4.com/wireguard-tools/
ip utility, iproute2-6.15.0, libbpf 1.5.0
Learning goals
By the end of this chapter you can:
- Explain why two peers both listing
AllowedIPs = 0.0.0.0/0cannot share one routing story without policy routing - Set
fwmarkon a WireGuard interface and steer plaintext withip rule/ extra tables (thewg-quickpattern) - Sketch a kill switch as filter rules that drop non-marked traffic, not as a WireGuard knob
- Know when network namespaces (wireguard.com/netns) beat classic route hacks
Concepts in one page
App traffic (no mark) ──► ip rule: lookup table 2468 ──► default dev wg0
wg0 UDP ciphertext ──► socket marked fwmark 1234 ──► main table / ISP
Second tunnel wg1 with its own mark + table
──► rules choose table by mark, uid, or fwmark from cgroup/VRF
| Piece | Job |
|---|---|
AllowedIPs |
Cryptokey routing: outbound peer selection and inbound source filter |
Kernel route to wg0 |
Only gets the packet onto the interface |
wg set … fwmark |
Marks WireGuard’s own UDP so policy routing can exempt it from the tunnel default |
ip rule + custom table |
Sends unmarked (or otherwise classified) traffic into the tunnel table |
| Kill switch | Firewall (iptables/nftables) drop of traffic that is neither local nor marked for a live tunnel |
| Netns physical/init split | Alternative full-tunnel design from the WireGuard site — ciphertext socket stays in the “physical” namespace |
Worked examples
Case 1: Two tunnels, one default — the collision
# Bad idea on one host without policy routing:
# /etc/wireguard/wg0.conf (work VPN)
[Peer]
AllowedIPs = 0.0.0.0/0, ::/0
# /etc/wireguard/wg1.conf (personal VPN)
[Peer]
AllowedIPs = 0.0.0.0/0, ::/0Both interfaces claim the entire Internet in cryptokey routing and (via wg-quick) typically install default routes. Whichever wg-quick up ran last wins the default route; the other peer’s AllowedIPs still claim destinations they will never see. Symptoms: “VPN is up” in wg show but traffic leaks to the wrong peer or blackholes.
Boring fix: at most one peer on a host with 0.0.0.0/0. Other tunnels get split prefixes (10.0.0.0/8, 192.0.2.0/24, …). If you truly need two full tunnels, separate them with namespaces, VRFs, or per-uid / per-mark policy routing — not two defaults in main.
Case 2: fwmark + policy routing (single full tunnel)
This is the improved rule-based pattern from Routing & Network Namespaces — the same idea wg-quick uses:
#!/usr/bin/env bash
# wg-policy-up.sh — docs-faithful sketch (needs root; not run in scout sandbox)
set -euo pipefail
# Assume wg0 already has keys/peers; we only add routing policy.
wg set wg0 fwmark 1234
ip route add default dev wg0 table 2468
ip rule add not fwmark 1234 table 2468
ip rule add table main suppress_prefixlength 0| Step | Meaning |
|---|---|
fwmark 1234 on wg0 |
Ciphertext UDP from WireGuard carries mark 1234 |
default dev wg0 table 2468 |
Tunnel table sends everything into wg0 |
not fwmark 1234 table 2468 |
Plaintext (unmarked) uses the tunnel table |
suppress_prefixlength 0 |
Still allow non-default routes from main (LAN) for unmarked packets |
Without the mark exemption, tunnel ciphertext would be routed back into wg0 (routing loop). With it, only application traffic takes the tunnel default.
# Tear-down sketch
ip rule del not fwmark 1234 table 2468 2>/dev/null || true
ip rule del table main suppress_prefixlength 0 2>/dev/null || true
ip route flush table 2468 2>/dev/null || true
wg set wg0 fwmark 0Verification on this scout host: commands were not executed with root. On a lab VM, wg show wg0 should list fwmark: 0x4d2, and ip rule should show the two policy lines.
Case 3: Multi-tunnel with distinct marks
wg0 (work) fwmark 0x10 table 100 rule: from all fwmark 0x10 lookup 100
wg1 (lab) fwmark 0x20 table 200 rule: from all fwmark 0x20 lookup 200
Mark traffic deliberately:
- systemd: IPAccounting / socket marking via nft / cgroup classifiers
- "work browser" runs under a unit that sets mark 0x10
- default unmarked traffic: either ISP (no kill switch) or drop (kill switch)
# Sketch — two tunnel tables (root; not run here)
wg set wg0 fwmark 16
wg set wg1 fwmark 32
ip route add default dev wg0 table 100
ip route add default dev wg1 table 200
ip rule add fwmark 16 table 100
ip rule add fwmark 32 table 200
# Exempt each interface's own ciphertext from its tunnel table using
# "not fwmark <that-iface-mark>" rules, same idea as Case 2.AllowedIPs on each peer should match the prefixes you actually send into that interface. A work VPN peer might get 0.0.0.0/0 inside table 100 only; a lab peer gets 10.50.0.0/16 only.
Case 4: Kill switch = firewall, not WireGuard
WireGuard has no KillSwitch=true. Operators implement “if the tunnel is down, do not leak” with packet filters:
# nftables sketch — drop forwarded/output traffic not marked for a tunnel
# (illustrative; tighten to your iface list and local subnets before use)
nft add table inet killswitch
nft add chain inet killswitch output '{ type filter hook output priority 0; policy drop; }'
nft add rule inet killswitch output meta mark 16 accept # work tunnel mark
nft add rule inet killswitch output meta mark 32 accept # lab tunnel mark
nft add rule inet killswitch output oifname "lo" accept
nft add rule inet killswitch output ip daddr 192.168.0.0/16 accept # example LAN
# cipher path: accept UDP to known endpoints OR accept mark that wg sets on socketWhen wg0 goes down, marks never get applied by your classifier → output drops → no ISP leak. Mis-write the LAN exceptions and you lock yourself out of SSH; test on console.
Case 5: Namespace alternative for “all traffic through one tunnel”
From the official netns guide: move eth0 into a physical namespace, birth wg0 there, move wg0 to init, default-route init through wg0. Ciphertext UDP stays in physical; ordinary processes never see eth0. That design avoids half of the main-table fights and is often cleaner than multiple full tunnels in one namespace. Multiple simultaneous full tunnels still want marks, VRFs, or separate netns per tunnel.
The trap
- Two
AllowedIPs = 0.0.0.0/0peers without separate policy domains. - Forgetting fwmark exemption → mysterious loss of handshake (ciphertext sucked into the tunnel route).
- Calling
wg-quick“the kill switch” —SaveConfig/Table=help routing; leak prevention is firewall policy. - Skipping endpoint / LAN exceptions and bricking remote admin access.
- Treating X marketing (“multi-tunnel + kill switch”) as WireGuard core features — they are operator composition on top of cryptokey routing.
The boring rule
One default owner per routing domain; mark ciphertext; steer plaintext with ip rule; implement kill switches in nftables/iptables; prefer netns when the whole host should only speak through one tunnel.
Try this
- On a disposable VM, bring up one
wg-quickfull tunnel andwg show/ip rule/ip route show table allto see marks and tables. - Add a second split-tunnel interface with a narrow
AllowedIPsand ping across both. - Implement a minimal nft output drop policy and prove that stopping
wg-quickblocks IPv4 to1.1.1.1while LAN still works. - Read wireguard.com/netns end-to-end and sketch when you would choose netns over fwmark.
Sources
- WireGuard · Routing & Network Namespaces
wg(8),wg-quick(8),ip-rule(8),ip-route(8)- Prior chapter in this book: cryptokey routing and PSK (
02b-wireguard-cryptokey-routing-and-psk.qmd) - Inspiration: CumulusVPN on X (AllowedIPs fights, policy routing, fwmarks, kill switch as firewall)