Excluding a package is a routing instruction, not a block, so the pages that load after the VPN connects are expected rather than evidence of a failure. The exclusion removes the app from the tunnel instead of blocking it, and the ordinary network carries its traffic. Two separate policies meet here: one chooses processes, the other chooses destinations. Reading them in that order explains why a connected icon and a working app are not a contradiction.

Why an exclusion keeps the network open

VpnService.Builder accepts either an allow list or a disallow list, never both. Packages in an allow list are the only ones routed through the tunnel. A disallow list inverts that: every package except the listed ones uses the tunnel, and the listed ones keep the physical path. An unlisted package under an allow list is also outside the tunnel, so exclusion is the normal state for most apps in that mode.

That makes a local disallow entry a routing decision, not a firewall rule. If the requirement is to stop an app from reaching a host, the tunnel has to cover it and the server or a local policy has to refuse the traffic. Removing an app from the tunnel only changes which interface carries its packets.

The Play Store case is the usual deliberate example. Excluding the store package lets downloads use the underlying connection when the VPN exit interferes with transfers, and it shows that exclusion means bypass rather than block.

One profile, one list

The builder throws if both calls are used, so a profile in front of you is either allow-list or disallow-list. That single fact determines how to read every later observation. Check the code or the managed configuration before the routing table, because the list decides what the rest of the output means. A hybrid profile is not a state you can be looking at.

The distinction also predicts what a newly installed app will do. Under an allow list it stays outside until someone adds it, while under a disallow list it joins the tunnel by default.

Managed configurations expose the same choice under different key names, and some consoles describe it as per-app VPN mode rather than as a list. The behaviour still reduces to those two calls, so trace the setting back to the package list instead of trusting the label. If the label and the list disagree, the list wins.

Follow the UID rule to the tunnel table

Android gives every app its own Linux user ID, and the VPN service installs policy rules that match UID ranges. Matching traffic is sent to a routing table whose default route points at the tun device. The table number is allocated per VPN instance, so it changes after a reconnect or after a second VPN profile is created.

The rule output is the map. Each line names a lookup table, and that table holds the default route deciding whether matching packets enter the tunnel.

adb shell ip rule show
adb shell ip route show table all
adb shell cmd package list packages -U | grep <package>

Run the third command first to get the UID, then find the rule whose range contains it. That rule points at a table number, and ip route show table <n> with that number shows whether a default route through the tun device exists. If the table has no default, the app is selected but has no path, which is a different failure.

These commands need a debuggable build or a device where adb shell can read network state. On a production handset the package manager still answers, but the routing inspection may be refused, so capture what you can and treat the rest as unverified.

A route claims a prefix, not an app

Builder.addRoute claims destination prefixes for the tunnel. A profile that adds only 10.0.0.0/8 sends that prefix through the tunnel and leaves every other destination on the physical path. That is ordinary split tunnelling, and it is independent of which apps the rules selected.

Server-pushed configuration mirrors the same model. Individual route pushes add prefixes, while a redirect-gateway directive replaces the default route and captures everything for the apps the tunnel covers. Removing that directive can shrink a working tunnel to one subnet without any change on the device.

A pushed route with an unusual gateway or metric qualifier may also fail to install. Prefer the plain prefix form and let the server handle forwarding and NAT.

Read the intersection before you change a setting

The tunnel carries the intersection of the covered processes and the claimed destinations. A covered app asking for an address outside the published prefixes leaves by the physical network. A disallowed package stays outside even when it asks for an address inside those prefixes. Neither case is a bug; both are the two policies doing their jobs.

A per-app VPN can also leave an associated app with no connectivity. When on-demand is off and the tunnel is down, the app remains bound to the per-app VPN and has no fallback, which looks like a broken app rather than a policy state. Enabling on-demand, or removing the association through management, changes that.

LayerWhat it selectsWhere to lookTypical mismatch
Package listProcessesBuilder calls or managed configurationApp excluded but expected to be blocked
UID policy ruleWhich table a UID usesip rule showTunnel icon on, app still on Wi-Fi
Routing tableDestinations for matched trafficip route show table <n>Covered app reaches only one subnet
Server pushPrefixes offered to the clientServer route configurationTunnel shrinks after a server edit

Diagnose one axis at a time. Confirm allow or disallow first, read the installed UID rules and tables second, and test a second app against the same destination last. The second app separates process selection from destination selection in a single request.

Write the result down as app, destination and table number. That tuple is what the next change has to preserve.

ບົດຄວາມທີ່ກ່ຽວຂ້ອງ