App-based and destination-based split tunneling are not two settings for the same job. One asks which process is sending a packet; the other asks where that packet is going. A rule built for the first question quietly fails when the traffic comes from a helper process, and a rule built for the second fails when a hostname resolves to an address you never listed. Choose the object that matches the requirement, then prove it with two test packets: one that must use the tunnel and one that must not.

The two rules match at different layers

An app rule is enforced above the network layer, against the identity the operating system assigns to the sender. On Android that is the UID behind a package, and the VPN service decides per UID whether traffic enters the tunnel. Nothing about the destination is consulted, so the selected app can reach anything and no other app can reach the internal network.

A destination rule is enforced on the packet itself. The tunnel interface owns a set of prefixes, the routing table sends matching traffic into it, and the sending process is irrelevant. That is why a destination rule covers a service, a printer or a batch job nobody remembered to list, and why it cannot be expressed as "this one program" on a platform whose client never looks at the process.

App rules fit mobile clients and managed agents

Android exposes the choice directly: VpnService.Builder.addAllowedApplication() and addDisallowedApplication() take package names and enforce the decision per UID, as described in Android's VPN developer guide. iOS has an equivalent through per-app VPN rules keyed on bundle identifiers, usually delivered by an MDM profile rather than typed by hand.

Desktop clients are the weak spot. Most Windows and macOS VPN clients route by destination and offer no per-process selector at all, because the tunnel driver sees packets, not processes. If your plan depends on tunneling one application on a laptop, check the client's actual capabilities before you design around it; you may be choosing between a destination list and a second machine.

A corollary people miss: you cannot imitate an app rule with routes on those platforms. Adding a host route for the app's server tunnels everyone's traffic to that server, including the browser and the updater.

Destination rules fit subnet lists and site reachability

WireGuard makes the model explicit. AllowedIPs is cryptokey routing: an inner packet whose destination falls inside a peer's list is encrypted to that peer, and anything else leaves unencrypted through the normal path. There is no process to name and no ambiguity about what is included.

OpenVPN reaches the same place through the pushed route table. route-nopull stops the server from dictating routes, after which a client-side route 10.20.0.0 255.255.0.0 vpn_gateway covers exactly the office prefix. IPv6 is a separate statement in every one of these clients, and forgetting it is the most common way to leak around a correct-looking IPv4 split.

A hostname is not a route. If the profile offers a domain field, treat it as a name the client resolves once, not as a permanent address range; content-delivery networks and load balancers will hand out different addresses tomorrow.

Where both models break: helpers, shared services, resolvers

Helper processes break app rules in a way that looks like a server fault. A browser's updater, a crash reporter, a sync service running as SYSTEM and a WebView embedded in a different package are all separate identities, so tunneling "the browser" can leave the actual request outside the tunnel. The reverse is just as confusing: an excluded app that calls a shared library inside a tunneled process still has its traffic tunneled.

Destination rules break on shared services and on names. File shares, printers, internal DNS and the licensing server all live at addresses, so a one-app tunnel gives the browser access while the mapped drive stays dead. That coverage difference is not a bug in either model, but it is the reason a one-app tunnel so often produces "the VPN is up and the share is gone".

Resolvers cut across both. If the client sets internal DNS servers but the operating system uses DNS over HTTPS to a public resolver, the answer comes from outside the tunnel and the destination rule may never match. Check which resolver actually answered before you conclude the rule is wrong, and confirm on the client side whether it installed the tunnel resolver at all. A split that leaves DNS outside the tunnel looks perfect in the routing table and still fails every internal name.

Runbook: choose, apply, verify

Start from the requirement, not from the menu. If the goal is "only this app may use the tunnel", you need app rules and a platform that supports them. If the goal is "these networks are reachable through the tunnel", you need prefixes. If the answer is "both", implement them as two separate controls and write down which one owns which failure.

Apply the narrowest form you can express, then verify by asking the routing table rather than the client's status icon:

ip route get 10.20.30.40
ip -6 route get 2001:db8:10::40
wg show wg0 allowed-ips
Get-NetRoute -DestinationPrefix 10.20.0.0/16
Find-NetRoute -RemoteIPAddress 10.20.30.40

The first two lines name the interface a real destination would use; wg show prints the configured prefix list; on Windows, Find-NetRoute answers the same question the stack will answer when the packet is sent. If the interface is the tunnel, the rule matched.

Then test a second packet that should be excluded, from a different application, to the same destination. Record the client version, the rule type, the prefix list or package list, and the two observed results in the ticket. That pair of results is what tells the next engineer whether the policy is misconfigured or the traffic never belonged to it.

Winndannde jowitiiɗe heen