The mistaken assumption is that a VpnService app flips a system switch and from then on the phone is protected. What actually happens is narrower: the app asks for consent, builds an interface, reads and writes packets through a file descriptor, and gives the interface back when it stops. Only one such tunnel can be alive at a time, and the system can take it away without asking. Most of the confusing behaviour people blame on the protocol comes from that ownership model.

Treat every lifecycle transition as part of the design rather than as an edge case.

Myth: the App Turns the Phone VPN On

The flow starts with VpnService.prepare(), which returns an Intent the user must accept, and continues with establish(), which can still fail afterwards for reasons the app does not control. Establishing a second tunnel deactivates the first interface and replaces the system notification. Nowhere in that sequence does the app own a device-wide toggle; it owns the descriptor it was granted and the packets that land on it. Even the persistent notification belongs to the system rather than to the app's design.

Because the interface is granted rather than seized, another client can take the slot at any time. The app learns about that through a callback, not through a return value.

Force-stopping the app is visible in the notification shade for the same reason: the system is reporting what it granted, not what the app chose to display.

Myth: Consent Is Given Once

When the system hands the tunnel to another client, it sends a revoke transaction on the binder that the base class returns from onBind(). An app that replaces that binder with its own must forward the revoke transaction itself, or VpnService.onRevoke() never fires and the service keeps writing into a descriptor that no longer exists. Approval can also be withdrawn by the user, and the system can stop the service during a restart or an always-on reconnect.

Handling it is concrete work rather than a log line: stop reading the descriptor, close it, clear the connected flag and tell the user which state they are in. Then decide whether the app should re-establish, prompt again or wait, because on an always-on configuration the system may bring the service back on its own.

An app that treats revocation as an exception rather than a state will report connected while every packet it writes is dropped, which is the worst possible failure mode for a privacy tool.

Myth: the Interface Filters Traffic for You

What arrives is raw IP. Capturing or filtering means parsing IP, TCP, UDP and IPv6 headers, tracking flows and writing replies back to the descriptor, which makes a capture app a small network stack rather than a system facility. protect() exists so the app's own transport socket bypasses its tunnel instead of capturing itself, and it returns a boolean that deserves a check on every socket created after the tunnel is up.

The builder's allowed and disallowed application lists decide whose traffic enters the interface. They do not grant access to payloads beyond the IP packets the interface carries, and no API exposes another client's tunnel or lists the system's VPN profiles. Network capabilities can say that a network is VPN transport, but not whose tunnel it is.

That is why status questions about other apps get contradictory answers: the information simply is not exposed to your process.

What the Lifecycle Actually Requires

Track the states you control: prepared, established, revoked, stopped. Reconnection should be teardown followed by establish, with a single owner for the descriptor and the transport socket, because two components racing to establish will fight over the one available slot. Background starts are restricted on modern Android, so a boot broadcast is not a guarantee; use a foreground service with its notification where the API level allows.

Log the transition rather than only the final state, and keep the state machine small enough to test every path. A state you cannot reach in a test is a state you will meet in the field without a log.

Test the Lifecycle on a Device

Emulators can fake connectivity but not the consent and revocation path convincingly, so run these on a real device with a second VPN client installed to steal the slot.

adb shell dumpsys connectivity | grep -i -A3 vpn
adb shell dumpsys activity services | grep -i -A2 vpnservice
adb logcat -s VpnService:V AndroidRuntime:E
adb shell am force-stop com.example.vpnapp

Start the tunnel, confirm the notification and the interface, then connect the other client and watch whether the revoke callback runs and the state clears. Force-stop the app and check whether anything still reports a live descriptor.

Repeat the sequence after an always-on restart, because that path differs from a user-initiated connect. What you are looking for is not a green icon but evidence that the app stopped using a descriptor it no longer owned.

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