Root is not what lets an Android VPN app carry traffic, block leaks or survive a reboot. People summarize all of that as "the app has VPN permission", which hides three separate mechanisms: a user consent grant, a packet interface the system establishes, and a platform blocking policy that is not the app's to define. Reviewing an Android VPN design means asking which of the three a claim actually rests on, because each has a different owner and a different failure mode.

Consent arrives through a system activity

VpnService.prepare() does not grant anything. It returns an intent to the system consent activity when consent is missing, and the app launches that dialog so the user can approve the service. Starting the app's own process, adding a permission to the manifest, or showing a custom "enable VPN" screen does not create the grant.

The grant is also scoped. It belongs to a package in a user profile, so the personal copy and the work copy are separate decisions, and a device owner can prepare a managed profile differently from a personal one. If the dialog never appears and prepare() returns null, consent already exists for that package in that profile; it is not evidence that the app is trustworthy, only that the grant is in place.

The interface is a request the system fulfils

After consent, the app describes what it wants with VpnService.Builder: addresses, routes, DNS servers, MTU and, where supported, the applications to include or exclude. establish() asks the system to create the tunnel interface and route the matching traffic into it. The system owns the routing decision; the app receives the file descriptor and must then do something useful with the packets.

What you observeWho actually owns it
VPN consent dialogThe system, triggered by prepare()
Routes, DNS and app listsThe app requests them; the system applies them
One active VPN per profileThe platform; a new service replaces the old one
Always-on and block-without-VPNDevice policy enforced by the platform

That table is the architecture in one place. The app participates in packet handling; it does not own the interface, the routing table, or the number of services that may run.

The tunnel socket has to escape its own tunnel

The connection that carries the encrypted traffic is itself a socket. If it is routed back into the interface the app just created, the tunnel depends on the tunnel and stalls. VpnService.protect() binds that socket to the underlying network so it bypasses the capture, which is why a correct client protects its outer socket before the first handshake.

adb shell dumpsys connectivity | grep -i -A5 vpn
adb shell settings get global always_on_vpn_app
adb shell settings get global always_on_vpn_lockdown

These commands show the installed VPN network, the always-on package and whether lockdown is set. They are read-only observations: if the values do not match what the design claims, the claim is about the app's UI rather than the system's state.

Blocking may belong to the platform

Android documents a policy that blocks connections which do not use the VPN. That enforcement is a platform contract, not an app retrying connections in a loop. The switch sits in the device's VPN settings as Block connections without VPN, and on a managed device a device owner can turn it on for the whole profile. The distinction matters when you evaluate a marketing claim: a client can display a lock icon and reconnect aggressively while direct traffic remains possible, or covered applications can stay offline because the system is enforcing lockdown.

A lock icon is drawn by the app, so it proves only that the app believes it is connected. Whether another app loses its network access is decided by the platform, so the useful check is to watch that app fail while the tunnel is up.

An app-store "always on" label does not replace that contract either. Always-on and lockdown are device settings, often applied by a device owner, and their behaviour for excluded apps is defined by the platform. Read the setting, not the badge.

Four questions for a design review

First, who consented: which package, which profile, and was it the user or a managed-device policy? Second, what did the app request: which routes, which families, which included or excluded apps? Third, what happens when the tunnel is unavailable: does the system block, or does traffic fall back to the underlying network? Fourth, what does the app do with the packets it receives?

The fourth question is the one the permission model cannot answer. Consent and a TUN interface prove that the app can receive traffic, not that it encrypts it, authenticates a peer or recovers cleanly. Those are implementation properties, and they are where an honest review spends its time.

An unnecessary root request is not made legitimate by calling the result a kill switch, and a permission dialog is not a security audit. Keep the three grants separate and each claim becomes checkable.

ဆက်စပ်ဆောင်းပါးများ