Tunneling an app does not carry its routing along with whatever that app launches, even though the interface suggests otherwise. A link hands a request to another program, that program opens its own socket, and the platform applies the rules to the new process rather than to the one that displayed the text. That second process is what the tunnel policy actually sees, and it is not the one you configured.

An included app can therefore open a browser that sits outside the tunnel. An excluded browser can hand work back to an app that sits inside it. Both directions of that surprise are ordinary behaviour.

The part of the workflow nobody configured

Per-app policy is written against an executable or package identity, and every connection is evaluated at the moment it is created. The browser, the document viewer, the media player and the system share sheet each carry their own identity, and each gets its own verdict. A deep link carries data, not policy.

That is why the observation feels arbitrary: the same click routes two ways depending on which handler answers it. A tunneled mail client opens a link in a browser that lives outside the tunnel, and the page loads from your home address. A browser outside the tunnel opens a document in a viewer that lives inside it, and the file arrives through the exit. Neither result is a fault in the client.

An already running handler makes it worse. If the browser is already open, the link becomes a message to an existing process, and that process keeps whatever routing it had when it started. Closing and reopening the handler changes the outcome, which is why the problem never reproduces on demand.

PlatformPolicy objectWhat it is evaluated against
AndroidPer-app VPN allowlistPackage name of the process opening the socket
WindowsApplication traffic filtersApplication path in the filter rule
LinuxUID or cgroup routing rulesOwner of the socket, which can be a different program

Which process actually made the connection

Measure the receiving side instead of reasoning about it. Keep the tunnel up, click a harmless link, and look at the sockets the new program holds.

ss -tnp state established '( dport = :443 )'
Get-NetTCPConnection -State Established | Select-Object RemoteAddress,OwningProcess

The first command names the process behind each established socket on Linux, and the second does the same on Windows by resolving the owning process identifier. Compare the process name with the ones in your policy. If the name is the browser and the browser is excluded, the click left the tunnel by design rather than by fault. Matching a policy rule to a live socket is the whole method.

Repeat the check with the handler already running, then with it closed. The two results can differ, and that difference explains most intermittent reports about links escaping a tunnel.

Coverage is a workflow property

If a task must stay inside the tunnel from start to finish, every component it can reach needs to be inside the policy: the launcher, the handler, the viewer, and any broker the platform inserts between them. Broader coverage is often the honest answer, and it is cheaper than auditing every share target.

Excluding a system broker to fix one application is a common mistake. Brokers are shared, so the exclusion follows every other app that uses the same path, usually without anyone noticing until a report arrives weeks later. Read what else depends on the component before you exclude it.

Keep identity separate from routing as well. A link can carry a session token or an account identifier regardless of which exit receives the request. A correctly routed click can still tell the destination exactly who followed it.

Document the workflow, not the first checkbox. When a task spans four programs, the diagram with four programs on it is the one that predicts where the traffic goes, and the single-app setting is only one row of it.

Nuŋlɔɖi siwo ku ɖe eŋu