OpenLabs

Bind the client, not the route

networkingprivacy 2026-08-23

“I have a VPN” is not a configuration. The interesting question is what your machine does in the two seconds after the tunnel dies — and the default answer, on almost every setup, is: keeps going, over your normal connection, with your real address.

Why default routing fails you

A VPN client typically works by rewriting the routing table so the default route points into the tunnel. That is fine while the tunnel is up. When it drops — process crash, server restart, key rotation, laptop suspend, ISP hiccup — the tunnel route disappears and the previous default route takes over. Traffic keeps flowing. Nothing errors. Your client happily keeps announcing itself to a swarm from your home address, and the first sign of trouble arrives by post.

This is the whole problem. The fix is not a better VPN provider; it is arranging things so that losing the tunnel means losing connectivity, structurally, rather than as a feature that might be enabled.

Three approaches, worst to best

1. Interface binding (in-application)

Most serious torrent clients let you bind to a specific network interface — tun0, wg0 — rather than to whatever the routing table currently prefers. If that interface goes away, the socket has nowhere to live and the client stops rather than falling back.

In qBittorrent: Options → Advanced → Network Interface. Choose the tunnel interface by name, not “Any interface”, and not the IP address — addresses change on reconnect, names usually do not.

This is a real improvement, and it is still application-level trust. It covers this one program. It does not cover the tracker announce that a plugin makes through a different library, a helper process, or anything else on the box. Good; not sufficient.

2. Firewall kill switch

Enforce it in the kernel instead of asking the application nicely. Policy: deny outbound by default, allow the VPN endpoint, allow the tunnel interface, allow LAN, drop everything else.

# conceptual shape (test on console access, not over SSH)
ufw default deny outgoing
ufw allow out on wg0
ufw allow out to 203.0.113.10 port 51820 proto udp   # VPN endpoint
ufw allow out to 192.168.0.0/16                      # LAN

Now, if wg0 disappears, packets have nowhere legal to go and are dropped. This is genuinely solid. Its weakness is that it is a global policy on a machine that probably does other things, and one careless later rule — or one service that needs an exception — punches a hole you will forget about.

3. Network namespace isolation (the good one)

Put the client in a network namespace whose only interface is the tunnel. Not a policy that could be misconfigured — an environment where the physical NIC does not exist. There is no leak path because there is no path.

In Docker this is one line. Run a VPN container, then attach the client to that container's network stack:

services:
  gluetun:
    image: qmcgaw/gluetun
    cap_add: [NET_ADMIN]
    ports:
      - 8080:8080          # the client's web UI is published HERE
    environment:
      VPN_SERVICE_PROVIDER: your-provider
      VPN_TYPE: wireguard
      FIREWALL_OUTBOUND_SUBNETS: 192.168.0.0/16   # so the LAN can reach the UI

  qbittorrent:
    image: your/qbittorrent
    network_mode: "service:gluetun"   # <-- shares gluetun's network namespace
    depends_on: [gluetun]
    # note: no ports: block here. It has no network of its own to publish.

The client now has exactly two interfaces: loopback, and the tunnel. If the tunnel is down, it has no internet. Not “blocked” — absent.

The trap nobody warns you about: restart the VPN container and the client keeps running, still attached to a namespace that no longer exists. It appears healthy and is completely dead — and if your provider gave you a forwarded port, the port it thinks it has is now stale too. Whenever you restart the VPN container, restart everything attached to it. Better, give the client a healthcheck that verifies real connectivity and let an autoheal watcher restart it for you, so a 3 a.m. reconnect fixes itself.

DNS leaks separately, and quietly

Traffic can be perfectly tunnelled while your DNS lookups go straight to your ISP's resolver in cleartext. Nobody sees the transfers; everybody sees exactly what you looked up, which is nearly as informative. Force resolution inside the tunnel — every decent VPN container does this — or point the namespace at a resolver reachable only through it. Then check, because “should be” is not a state.

Prove it. Twice.

Every one of these designs can be silently broken by a later change. Testing takes two minutes.

Is the client's egress actually the tunnel? Run the check from inside the client's network context, not from the host:

docker exec qbittorrent curl -s https://ipinfo.io/ip
# must be the VPN address, never your ISP address

Does it fail closed? This is the test that matters, and the one people skip:

docker stop gluetun
docker exec qbittorrent curl -s --max-time 8 https://ipinfo.io/ip
# correct answer: a timeout / no route. ANY address here means you are leaking.
docker start gluetun && docker restart qbittorrent

Where does the swarm think you are? The definitive check is what a peer sees, not what an API tells you. Use a torrent-tracking IP check — a magnet that reports back the address your client announced. If that matches your VPN address, the whole chain is correct.

DNS: confirm lookups resolve through the tunnel's resolver and not your ISP's.

Re-run these after every provider change, container update, or firewall edit. Put a reminder on it. The failure mode is silent by nature — you will not notice it happening, only afterwards.

Port forwarding, briefly

Without an inbound port you are connectable only to peers who can accept your connection, which means slower transfers and, on trackers that measure it, a worse ratio. Some providers offer forwarded ports; they are usually dynamic and change on reconnect, so the port must be pushed into the client automatically after each reconnect rather than typed in once. If your provider does not offer forwarding, accept the reduced connectivity — do not solve it by forwarding a port on your own router, which defeats the entire arrangement.


Ranked: namespace isolation, then firewall kill switch, then interface binding, then nothing. Whichever you choose, the test above is the part that decides whether you have a configuration or a hope.