Windows per-app VPN trigger vs split tunneling: which isolates
An app-triggered Windows VPN profile only starts a tunnel. A routed split tunnel decides what enters it, and each fails differently for one app.
Two Windows setups look alike in Settings and behave nothing alike. One is a copied VPN profile with an application trigger, so starting one executable brings the tunnel up. The other is a split-tunnel profile whose route list happens to cover what that application talks to. The first answers when the tunnel exists. The second answers which packets enter it, and only the second leaves the rest of the machine on the physical link while the app stays connected.
The trigger is a lifecycle rule, not a routing rule
Add-VpnConnectionTriggerApplication binds an executable path to a profile name. When that process starts, the Remote Access Connection Manager dials the profile, and when the trigger condition ends the connection can drop. Nothing in that exchange inspects sockets. The cmdlet needs an elevated shell, the full path to the binary, and -Force when you replace an existing entry. Confirm the association survived with Get-VpnConnectionTrigger instead of trusting the prompt; Microsoft's Add-VpnConnectionTriggerApplication reference lists the parameters and the examples.
Two limits explain most "the trigger does nothing" reports. Triggers are built around all-user profiles, so the profile scope is the first thing to settle, and the application path has to match the binary that actually runs, not a shortcut or a launcher that spawns a second process. The other limit is by design: on a network Windows classifies as trusted, the trigger stays idle, because the profile is meant to fire away from the office.
Add-VpnConnectionTriggerApplication -Name "APP_VPN" -ApplicationID "C:\Program Files\Vendor\app.exe" -Force
Get-VpnConnectionTrigger -Name "APP_VPN" | Format-List *
Get-VpnConnection -Name "APP_VPN" | Format-List Name,ConnectionStatus,SplitTunneling,AllUserConnection
Split tunneling decides what crosses the tunnel
With split tunneling off, Windows installs a class-based default route with a low metric on the VPN interface. Every socket follows it, including the browser and the update service, so a trigger in that configuration moves the whole machine for as long as the profile is connected. The trigger is still just a lifecycle rule; the blast radius came from the route.
With split tunneling on, the profile carries only the prefixes that the gateway pushes or that you add to the VPN interface. Everything else keeps using the physical default route. That is the arrangement people usually mean by "one app through a separate exit", and it is a routing decision from end to end.
Set-VpnConnection -Name "APP_VPN" -SplitTunneling $true
Get-VpnConnection -Name "APP_VPN" | Select-Object SplitTunneling
Get-NetRoute -InterfaceAlias "APP_VPN" | Sort-Object DestinationPrefix
Where a routed split stops isolating your app
The common failure is a destination that never appears in the tunnel's route list. An app that signs in against one address and then streams from a CDN, or resolves a dozen rotating endpoints, will put some connections on the VPN interface and the rest on Wi-Fi. IPv6 is a separate table, so a profile that pushes only IPv4 prefixes can look correct while the app's IPv6 traffic stays local.
Two smaller effects matter as much. Longest-prefix match wins, so a more specific on-link route, a /32 pushed for a management host, or a second adapter's subnet can beat the tunnel for exactly the address you care about. Resolution happens before routing as well: unless the profile's DNS servers or an NRPT rule cover the name, the app can resolve through the physical adapter and then dial an address that was never inside the tunneled prefixes.
Three shapes, and what each one buys
The table is the part I would tape to the wall, because the decision is about blast radius rather than features. If the app's destinations are stable prefixes you can list, the routed split gives isolation with the smallest footprint. If they are not, a default-route profile behind a VM or a second device keeps the separation honest at the cost of moving more than one process.
| Shape | What starts the tunnel | What enters it | Pick it when |
|---|---|---|---|
| Trigger plus routed split | Process start | Only pushed or added prefixes | The app's endpoints are stable prefixes and the host should stay direct |
| Trigger plus default route | Process start | Everything, machine-wide | You need one exit for all traffic and accept the wider scope |
| Separate VM or device | Guest boot or login | Everything inside that guest | The app's destinations rotate, or the host must keep its own public address |
What none of these provides is socket-level binding on the host. The classic Windows VPN client has no rule that says this process must use that adapter; triggers and route filters live in different layers, and the layer that selects an interface is the route table. If a guide promises per-app enforcement from a copied profile alone, ask which route or which filter is doing the work.
Verify the pair in one pass
Test the lifecycle and the routing separately, in that order. Close the app, confirm the profile reports Disconnected, then start the app and check that the state changed. Only after the tunnel is up should you ask where a specific destination goes, and you should use the address the app actually dials rather than a guess.
$app = Get-Process app | Select-Object -First 1
Get-VpnConnection -Name "APP_VPN" | Select-Object ConnectionStatus
Get-NetTCPConnection -OwningProcess $app.Id -State Established | Select-Object RemoteAddress,RemotePort
Find-NetRoute -RemoteIPAddress 203.0.113.10
Find-NetRoute names the interface and next hop that would carry that destination. A physical alias there means the tunnel is up and irrelevant for that flow: check the pushed prefixes, then the more specific routes, then IPv6. If the state never changes at all, the trigger did not fire, and the executable path, the profile scope and the VPN client operational log are the three places to look.
Undo cleanly so the next test is honest
Remove in the reverse order you built: the application trigger first, then the profile. Deleting the profile while the trigger remains leaves a dangling association that makes the next attempt confusing, and Remove-VpnConnection -Force does not always clear a phonebook entry that was created outside the cmdlets.
Per-user entries live in the user's rasphone.pbk under the Roaming profile; all-user entries live under ProgramData. Check the store that matches the profile scope, reconnect the profile you kept, and confirm its routes and DNS still behave, so a later test measures the change you made rather than leftover state.
- Tunnel starts on process start, not on login.
- Only the intended prefixes appear on the VPN interface.
- The app's real remote address resolves to the VPN alias.
- No dangling trigger is left after the profile is removed.