Split tunneling is a routing policy expressed as a set of rules, and the client is only the thing that installs them. The separation happens in two places: matching rules that pick a path for each packet, and bindings that hold an application to one interface whatever the table says. Which mechanism your client relies on explains most of the surprises people report.

A policy you can state in one line is testable. A policy described as faster browsing is not, and it will not tell you which rule to change when a request goes the wrong way.

The Client Installs Rules, the Kernel Makes the Choice

When the tunnel comes up, the client adds routes and often policy rules to the same table your other interfaces use. Each packet then reaches a lookup that compares its destination against every candidate prefix and keeps the most specific match, while priority settles ties between rules that point at different tables. Nothing in that lookup knows the packet came from a browser tab rather than a backup job. The rule that wins is the one that sets the path, and you can read it before and after the change.

That is why a split tunnel can look arbitrary. Two destinations in one application may take different paths simply because one prefix is narrower than the other.

The same choice exists on every platform, though the names change. Windows pushes the decision into its filter platform, Android exposes a per-app allowlist through its VPN service, and macOS attaches the tunnel to a device that still obeys the routing table. Microsoft's routing notes are worth reading for the Windows case, and a guide written for one platform rarely transfers unchanged to another.

Per-App Binding and Destination Prefixes Are Different Tools

A destination policy asks where the packet is going and consults the route table or a policy rule. A per-app policy asks which program opened the socket, and the platform keeps that program on the tunnel interface even when a more specific route points elsewhere. The second is stronger because it survives table changes, but it covers only processes the platform can attribute. A helper process, an embedded browser, or a service that opens its own connection may not inherit the binding you assumed.

Neither approach survives a name-based policy without help. Addresses for large sites rotate across content networks, so pinning a hostname to a few prefixes produces a rule that quietly stops matching. A policy that must follow names needs a resolver-aware component rather than a static route.

DNS and the Kill Switch Follow Their Own Rules

Name resolution is a separate decision from packet routing, and a split tunnel usually wants two resolvers: the company one for internal names, the local one for everything else. The client may install per-interface DNS settings, hand queries to a stub resolver, or leave the system default untouched. A browser with its own encrypted resolver bypasses all three arrangements. Test a lookup instead of assuming the resolver followed the route.

A kill switch adds a filter on top, and that filter does not always distinguish an intended exclusion from an accidental leak. Some clients block everything outside the tunnel while others permit the exclusions the policy declares, so read which behaviour yours implements before you rely on either.

Check the Split From Both Sides

Test the included path and the excluded path separately, and judge the result by interface name rather than by a public address. These commands assume a Linux host with the tunnel device already up. Replace 203.0.113.10 with a destination your policy routes inside, and 198.51.100.25 with one it sends outside.

ip route get 203.0.113.10
ip route get 198.51.100.25
resolvectl query intranet.example.com
curl -s --interface tun0 https://example.com -o /dev/null -w '%{http_code}\n'

Read the first two answers together, because the interface named in each one shows whether the split put that destination on the tunnel. The resolver query shows which server answered the internal name, and the last command proves the tunnel device can carry a request at all. A failure on the second line while the first succeeds means the policy is doing exactly what it was told.

Repeat the set after a reconnect and after a network change. Lease renewal and interface churn are where a split stops matching the intent it was built for, and a saved output from each run makes the drift obvious.

Winndannde jowitiiɗe heen