When an Android app stops working the moment the tunnel comes up, three different things can be responsible, and they produce similar-looking failures. The app may read the phone's own network state and refuse locally. The server may classify the address you arrive from and reject that. Or the refusal may have nothing to do with the VPN at all. The order below collects evidence before it changes anything, because changing the exit is the most expensive step and the one least likely to help if the check is local.

Step 1: find out when the app refuses

Timing is the first discriminator. Clear the log buffer, reproduce the failure, and look at what the app did before it complained:

adb logcat -c
# reproduce the failure in the app
adb logcat -d | grep -iE 'vpn|network|connect|http' | tail -60

If the app refuses before any outbound request is attempted, you are looking at a local decision. If it connects, sends something and then shows an error, the rejection came from the other side. If the app fails identically with the tunnel off, the VPN was never the variable.

Treat that timing as evidence rather than proof. An app can replay a cached response, and background requests may have succeeded or failed before you looked. Note the timestamps of the last successful request and the error, not just the moment the message appeared.

Step 2: read the phone's own view of the network

Android exposes the VPN state as a network capability, so an app does not need a list of commercial providers to notice that a tunnel is active. It asks the connectivity service what the current network is, and the answer includes a VPN transport bit:

val cm = context.getSystemService(ConnectivityManager::class.java)
val caps = cm.getNetworkCapabilities(cm.activeNetwork)
val onVpn = caps?.hasTransport(NetworkCapabilities.TRANSPORT_VPN) == true

That check is local and cheap, and it explains a whole class of "this app does not work over VPN" reports without any remote classification. What the app can see still depends on its own network context, so do not assume every app inspects every profile on the device.

From a workstation you can see the same state on a test device with adb shell dumpsys connectivity and by looking for the VPN network entry. Compare a run with the tunnel up against a run with it down, and keep both outputs.

Step 3: read what the server actually sees

The other observer is the service. It sees the source address that arrives, and it may act on the address, the hosting network behind it, the region, the carrier, or signals tied to the account and device. None of those is a unique identity, and shared addressing makes collateral blocks normal: one restricted address can take out unrelated users behind the same translation.

Get the address from the same path the app uses, not from a browser on a laptop. On the phone, a plain request to an address-echo service shows what the far side receives, and comparing that with a request over cellular tells you whether the address changed at all.

If the app works over cellular and fails over the tunnel with an identical account, the rejection is at the remote end or in the app's own policy. If it fails on both, the VPN is not the differentiator and you should stop testing endpoints.

Step 4: match the evidence to one of three verdicts

Three combinations cover almost every report. A refusal with no outbound attempt while a tunnel is active points at a local transport check. A request that leaves and comes back with an error, a timeout on a specific host, or a region message points at the remote side. A failure that reproduces on cellular with no tunnel points at the account, the app version or the service's own policy, and no VPN change will move it.

Write the verdict down with the evidence that produced it. The value of this step is that it stops the loop where someone changes exit nodes four times to fix a check that runs on the handset.

It also keeps the claims honest. Self-hosting changes the endpoint and its internet exit; it does not make Android forget that a VPN network is active, and it does not guarantee acceptance by a service that classifies addresses. A local filter can trigger on a VPN network you own just as readily as on a commercial one.

Step 5: take the supported path and verify it

Start with the product's documented policy and its support owner. Send the client version, the exact message and a narrow failure window; do not ship full traffic captures or credentials just to prove an error exists.

If the service permits direct access and device policy allows it, a per-app exclusion is the supported mechanism, and it deliberately removes that app from the tunnel rather than hiding the tunnel. The app's traffic then leaves on its own path, which is a policy decision someone has to own. Under a block-without-VPN configuration an excluded app may simply lose connectivity, so test that combination before promising it works.

After any change, repeat the step-one timing test, confirm the app works, and confirm every other app still uses the tunnel. Then restore the previous configuration and check that the failure returns, because a fix you cannot reproduce is a coincidence you will meet again.

Nsɛm a ɛfa ho