I wanted my whole home on a VPN — and within a week I wanted most of it off again. The TV stopped authenticating. The printer went unreachable. One service that checks client IPs decided my exit node was a datacenter and locked the account pending review. The classic fix — per-device VPN clients — works until you own fifteen devices that don't have one.
The answer I settled on: run the VPN on the router, but route by policy, not by default. Devices (or destinations) you choose go through the tunnel; everything else keeps the normal ISP path. This is the setup I run on an OpenWrt-based home router, and the lessons apply to any policy-routing-capable gateway.
The shape of the problem
A default-route VPN changes the egress for the entire LAN. That breaks:
- devices that refuse to work from unfamiliar geolocations or datacenter IPs;
- local-only services that assume the ISP network path;
- anything your ISP filters differently when traffic arrives from its own CGNAT range versus a foreign exit.
What we want instead: two routing paths and a rule that picks between them per packet, based on the source device (or the destination).
Architecture in one paragraph
Bring the VPN tunnel up as its own interface on the router. Create a second routing table whose default route points into the tunnel. Then mark the packets you want tunneled (by source address, MAC-derived address, or destination) and add an ip rule that sends marked packets to the tunnel table. Unmarked packets follow the normal table. DNS needs the same split treatment or you'll leak.
Step 1 — The tunnel as its own interface
Whether it's WireGuard or a userspace client exposing a TUN device, the requirement is the same: the tunnel must be a first-class interface with its own default route in a separate table (say, table 100). Never make it the global default.
Step 2 — Mark the traffic you want tunneled
Firewall marks are the cleanest selector. Example: tunnel two specific devices by their LAN addresses:
# mark packets from the devices we choose
iptables -t mangle -A PREROUTING -s 192.168.1.30 -j MARK --set-mark 0x1
iptables -t mangle -A PREROUTING -s 192.168.1.31 -j MARK --set-mark 0x1
Prefer marking in PREROUTING by source so the mark is attached before the routing decision. If you need per-destination policy instead (e.g., "tunnel only traffic to these networks"), mark on destination — the mechanics are identical.
Step 3 — Policy route the marked packets
ip rule add fwmark 0x1 table 100
That single line is the entire trick: marked packets consult table 100 (tunnel default route); everything else stays on the ISP path. Add a matching rule for connection tracking so reply packets stay consistent, and you're routing.
Step 4 — Split DNS or you leak
Routing is only half of it. If every DNS query goes to the ISP resolver from the router's ISP address, your "private" devices are still identifiable, and split behavior becomes guesswork. Two sane options:
- send tunneled devices' DNS to a resolver over the tunnel (DoT/DoH to a provider you trust), and keep the rest on the ISP resolver;
- or run a local recursive resolver and steer it per client.
Either way, make the leak explicit and tested: resolve from a tunneled device and check the resolver's observed source.
Step 5 — Fail closed (or knowingly fail open)
Decide what happens when the tunnel drops:
- Fail closed for the tunneled set: drop marked traffic when the tunnel is down. Safer for privacy-sensitive devices.
- Fail open with a loud log for everything else, so the household doesn't lose the internet because a VPN provider had a bad minute.
Implement the decision once, deliberately. The worst failure mode is the silent one where a "tunneled" device quietly egresses via the ISP and nobody notices.
Gotchas I hit in the field
- Hardware NAT bypasses your rules. Many home routers offload forwarding to a hardware engine that never sees your marks. If your policy routing "doesn't work", check whether HW NAT/fast path is enabled — disabling it costs some throughput and buys correctness.
- MTU. Tunnel overhead plus a 1500 LAN MTU produces silent breakage on some sites. Clamp MSS on the tunnel path and test with large responses, not just pings.
- CGNAT at the ISP. If your WAN address is in a shared range, inbound is dead anyway — which simplifies your threat model (nothing is exposed) but also means you can't easily verify from outside; test from the inside.
- Firmware phones home. Consumer firmware with TR-069, auto-update, and cloud management will route around your intentions. Lock those down before you trust the box with policy decisions.
- Test the boring path too. The most common regression is not VPN leakage — it's the non-tunneled half breaking because a rule was too broad. Verify that a device not in your marked set still resolves and browses normally.
Verification checklist
- Tunneled device's public IP matches the VPN exit.
- Untunneled device's public IP matches the ISP.
- DNS for each group resolves from the expected source.
- Kill the tunnel: tunneled devices either drop (fail closed) or visibly fail over — never silently leak.
- Reboot the router: everything comes back in the right state without you touching it.
The payoff
You end up with a house where the default answer is "normal internet", and the VPN is a deliberate lane you assign to a device in one line of firewall config. Adding or removing a device from the tunnel is a one-liner, not a Saturday project.
If you've fought a router VPN setup before, I'm curious which part bit you — hardware NAT and DNS leaks are my top candidates.
Top comments (0)