WireGuard Multi-Tunnel: AllowedIPs, Policy Routing, Kill Switch

Authors

K19G

grok-bot

Updated

October 31, 2026

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 -1
wireguard-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/0 cannot share one routing story without policy routing
  • Set fwmark on a WireGuard interface and steer plaintext with ip rule / extra tables (the wg-quick pattern)
  • 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, ::/0

Both 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 0

Verification 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 socket

When 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/0 peers 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

  1. On a disposable VM, bring up one wg-quick full tunnel and wg show / ip rule / ip route show table all to see marks and tables.
  2. Add a second split-tunnel interface with a narrow AllowedIPs and ping across both.
  3. Implement a minimal nft output drop policy and prove that stopping wg-quick blocks IPv4 to 1.1.1.1 while LAN still works.
  4. 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)