You switch the VPN to full tunnel, most traffic obeys, and then one address answers directly — a printer, a NAS, a jump host on 10.x. The obvious reading is that the tunnel is leaking, so you lower the metric on the default route and nothing changes. That failed attempt is the clue: a default route is a fallback, not a verdict. The routing lookup takes the longest matching prefix before it ever compares metrics, so a /24 keeps winning against a /0 no matter what number you attach to either one. Once that ordering is clear, the leak is usually a route somebody installed on purpose, or a prefix the client never meant to cover.

What a default route actually promises

0.0.0.0/0 matches every IPv4 destination with zero bits of specificity. It exists so a host can reach anything its table does not describe better. A route to 192.168.1.0/24 carries 24 matching bits, so for 192.168.1.40 it wins before anyone compares metrics, administrative distance, or whatever the client calls its priority field. The same rule holds in the Linux FIB, the Windows routing table, and the BSD stack behind macOS.

On a normal LAN that behavior is a feature. The local subnet is directly attached, the printer keeps working while the tunnel carries your browsing, and nothing is misconfigured. The question worth asking is not why that one address goes direct, but which prefix in which table matches it, and who put it there.

Half-defaults, or why the default entry can look untouched

OpenVPN's redirect-gateway def1 is the cleanest example. Instead of deleting your default and replacing it, it adds 0.0.0.0/1 and 128.0.0.0/1. Together the pair covers all of IPv4, each prefix is longer than /0, and both beat the router's default while leaving it in place. Run a route listing afterwards and the original default is still there, still pointing at your gateway; it is simply never selected for internet destinations.

WireGuard's wg-quick reaches the same outcome through policy routing. With AllowedIPs covering 0.0.0.0/0 it creates its own table, sends marked packets there, and adds a rule that ignores the main table's zero-length prefix. The main table keeps its default entry, and the rules decide that the tunnel table is consulted first for the traffic the interface owns.

Both designs need exactly one route outside the tunnel: a host route to the VPN server's own address through the original gateway. Without it, the client would try to carry the packets that build the tunnel inside the tunnel, and the connection never completes. A non-tunnel route in your table is therefore normal even on a strict full-tunnel profile. The routes worth questioning are the ones covering destinations you actually care about.

Metrics decide ties, prefixes decide winners

Metrics are compared only among candidates that match the destination equally well. On Linux, ip route get 10.20.5.9 prints the winner together with its table, and ip route show table all lists the candidates, including any half-defaults and any table a rule points at. On Windows, Find-NetRoute -RemoteIPAddress 10.20.5.9 returns the selected route and source address, while Get-NetRoute shows the raw table — but the effective cost is route metric plus interface metric, so a slow adapter can lose a tie it appears to win.

Three cases cover most of what you will see:

  • Same destination, /24 against /0: the /24 wins on prefix length, whatever the metrics say.
  • Same destination, two /24s with different metrics: the lower effective metric wins.
  • Same destination, two tables, one selected by a policy rule: the rule picks the table first, and prefix comparison happens inside it.

The third case is where most of these reports end up. A Linux ip rule or a Windows VPN traffic filter can send traffic to a table that never held the tunnel default, and the main table looks perfect the whole time.

Who usually owns the bypass

Start with the profile, not the table. Split-tunnel configurations and per-app VPN settings deliberately leave named prefixes outside the tunnel; a work profile that protects one private subnet and lets general browsing go direct is behaving as designed, and overriding it breaks the policy it was built to enforce. Microsoft's VPN routing guidance draws the same line between force-tunneled traffic and explicit exceptions, which makes it a useful cross-check before you edit anything.

Next, look for leftovers. A route pointing at an interface that no longer exists, or at an address from the previous network, shows up after docking stations, USB adapters, and container bridges come and go. Deleting it once is not the end of it: NetworkManager connection profiles, netplan files, and Windows persistent routes reinstall it on the next connect or boot, so find the file or profile that owns it before editing anything.

Then check IPv6 on its own. ip -6 route and Get-NetRoute -AddressFamily IPv6 hold separate state, separate defaults, and separate client behavior. An IPv4-only audit says nothing about a dual-stack host that quietly prefers a native IPv6 path, and that alone can make a client look split-tunneled.

Confirm with the application's own flow

Record the destination the application actually uses, then ask the system for the path it would take from the source address that application binds. ip route get 10.20.5.9 from 192.168.1.40 answers that on Linux; Find-NetRoute -RemoteIPAddress 10.20.5.9 plus the local address does it on Windows. Compare the result with what the profile claims. A browser's public-IP page tests one destination on one protocol and says nothing about the private address that started the complaint.

Fix the narrowest object that explains the difference. If the exception is approved, write the prefix down and leave it alone; if it is not, remove the route or add an explicit narrower route through the tunnel instead of lowering the default's metric. Lowering a metric cannot outrank a longer prefix, and editing the default while two /1 routes own the internet path changes nothing either.

Then reconnect the tunnel and repeat the same query. Clients reinstall routes on every connect, so a change that only survives until the next handshake has not been tested. If the path still diverges, the remaining suspects are a policy rule, a second table, or a client-side filter — not the default route.

  • Name the destination and the source address the app really uses.
  • Print the winning route with ip route get or Find-NetRoute.
  • Check whether a /1 pair or a policy table owns the internet path.
  • List every prefix longer than /0 that matches the destination.
  • Repeat the IPv6 check separately.
  • Trace the surviving route back to the file, profile, or filter that installs it.
  • Reconnect and re-run the query before calling it fixed.

If all seven agree with the profile, the tunnel is doing exactly what it was configured to do, and the fix belongs in the profile rather than in a metric.

Artitgels cun il medem tema