Android leaks keepalive packets past its VPN kill switch
A normal Android app can push NAT-T keepalive packets to the physical network on UDP/4500 while Always-on VPN lockdown is enabled, leaking the real IP.
On a Pixel 8 Pro running Android 16, with Always-on VPN on and “Block connections without VPN” enabled, an ordinary app made a one-byte UDP packet appear on the physical Wi-Fi router every ten seconds, sent to an attacker-chosen address on port 4500. The kill switch was on. The packets went straight to the access point, outside the tunnel.
The capture came off a separate OpenWrt router, which is the right place to look: anything recorded there has already crossed Android’s VPN policy boundary.
2026-05-28 18:36:31.709917 phy1-ap0 P IP 192.168.1.182.38904 > 1.2.3.4.4500: UDP, length 1
2026-05-28 18:36:41.710042 phy1-ap0 P IP 192.168.1.182.38904 > 1.2.3.4.4500: UDP, length 1
How a normal app does it
The app uses Android’s public NAT-T socket-keepalive API, the one meant for keeping an IPsec NAT mapping alive on UDP/4500:
IpSecManager.UdpEncapsulationSocket socket = ipSecManager.openUdpEncapsulationSocket();
SocketKeepalive keepalive = connectivityManager.createSocketKeepalive(
network, socket, sourceAddress, destinationAddress, executor, callback);
keepalive.start(10);
No root, no ADB, no hidden API, no raw Binder, no JNI, and none of the privileged PACKET_KEEPALIVE_OFFLOAD permission. The proof-of-concept declared INTERNET and ACCESS_NETWORK_STATE, the two capabilities almost every app already holds.
It slips past lockdown because a keepalive is not an ordinary socket write. The app hands a duplicated file descriptor and an IpSec resource ID through IConnectivityManager.startNattKeepaliveWithFd(...) to ConnectivityService, KeepaliveTracker, a NetworkAgent, and finally the Wi-Fi HAL and chipset firmware, which emits the frames itself without waking the app. Android’s VPN lockdown enforces at the per-UID socket, fwmark/netd routing, and firewall layer. The offloaded keepalive is emitted below all of that.
The missing check
The fd-based admission method used to be privileged, built for a raw-fd NAT-T keepalive path. During API review the public UdpEncapsulationSocket path was routed through the same method and the unconditional permission check was removed so that public IPsec/IKE apps could use it. The compensating control was supposed to be resource ownership: a public caller would have to prove it owned a live IpSec encapsulation socket. That validation was added, then reverted over service-dependency and deadlock concerns, and replaced with per-UID and per-network quotas.
Quotas cap how many keepalives a caller can hold. They do not authenticate the fd/resource pair and they do not consult the caller’s VPN policy. So two checks are absent at admission. The server never proves the duplicated fd corresponds to a live, caller-owned IpSec resource (the reviewed isNattKeepaliveSocketValid accepts any non-null fd, and bogus resource IDs like 0, -2, and Integer.MAX_VALUE sail through). And it never checks whether the caller UID’s effective VPN-lockdown policy forbids emitting on the physical underlay before it starts hardware offload.
What leaks
The attacker picks the destination, within the usual API and routing constraints, and the payload stays the platform’s fixed one-byte NAT-T format. There is no arbitrary data channel here. What the chosen endpoint learns is the device’s real source IP as seen from the physical network, the fact that the device is online, and the timing and cadence of the keepalive. That source IP can give away ISP, organization, or travel state, and it correlates a device that is supposed to be behind a VPN with the non-VPN network it is actually sitting on. Those are exactly the disclosures that “Block connections without VPN” exists to prevent.
Once armed, the leak is durable. On the Pixel it kept running through backgrounding, screen lock, Doze, battery saver, the restricted standby bucket, the Binder freezer, and a GrapheneOS relock back to before-first-unlock without a reboot. It stopped only on force-stop, uninstall, network loss, or reboot; losing Wi-Fi ended it with a network-loss error, and re-arming brought it back when Wi-Fi returned. On the Samsung, a single active keepalive slot stayed leased continuously for 24 hours and 32 minutes. One slot is all an app needs: the Pixel exposed exactly one unprivileged slot to the app UID, and further attempts failed with ERROR_INSUFFICIENT_RESOURCES (-32).
How wide
Three OEMs were confirmed, all on Android 16. The Pixel 8 Pro (Broadcom Wi-Fi) is the only controlled external packet capture. A Samsung SM-F966B (Qualcomm) independently reached the same public physical-gateway path and held the active slot for the day-plus lease. A Nothing A059 (Qualcomm) confirmed admission and the active callback on a third OEM, with no packet capture or duration collected.
The admission path is shared Android 12+ framework code, so the exposure window starts with Android 12. A firmware census across seven WLAN stack families (Qualcomm QCA and WCNSS, Broadcom/Cypress bcmdhd, MediaTek Connac, Unisoc SPRDWL, Samsung Exynos, and HiSilicon Hi11xx) found keepalive or offloaded-packet surfaces in all of them, and those families account for an estimated 91.24% of Android-derived shipments from late 2021 on. The researchers leave the remaining 8.76% unresolved. This is a device-class problem affecting most Android 12+ phones.
Worth knowing on the other side: a static scan of 4,679 distinct F-Droid and IzzyOnDroid git origins found no use of Android’s framework IPsec, IKE, or NAT-T APIs at all, and a manual pass found 73 ordinary VpnService apps, none of them touching this path. That sample excludes proprietary enterprise, OEM, carrier, and sideloaded software, so it is not proof of universal absence, but it suggests the public API carrying this bug has essentially no legitimate open-source consumers. The proposed repair fits that shape: authenticate the fd and resource ownership, and enforce the caller’s effective VPN-lockdown policy before handing anything to the offload path.
#ad Contains an affiliate link.
Keep Reading
androidKeepalive packets bypass Android lockdown
A normal Android app can drive NAT-T keepalive packets onto UDP/4500 past Always-on VPN lockdown, leaking the device's real IP.
androidOne WAP Push SMS crashes Android below the October 2026 patch
A malformed WAP Push SMS crashes com.android.phone on unpatched Android, dropping calls and looping reboots. How it works, recovery, and the October 2026 fix.
androidGoogle wants your ID to ship Android apps
Google's 2027 rule forces every Android developer to register identity and signing-key evidence before their apps can install, even outside the Play Store.
Latest on the Wire
Full wire →- Bending Spoons Destroys Harvest With 10x Price Hike and LLM SupportHacker News
- CISA Flags SonicWall MFA Bypass Amid Exploitation ReportsThe Hacker News
- Claude Opus Ports 3D Models from Three.js to Vanilla WebGLSimon Willison
- Collapsing AI PR descriptions with HTML details tagsSimon Willison
New signal daily · RSS
Stay in the loop
New writing delivered when it's ready. No schedule, no spam.