RC RANDOM CHAOS

Keepalive 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.

· 4 min read
Keepalive packets bypass Android lockdown

An OpenWrt router sitting between a Pixel 8 Pro and the internet logged a one-byte UDP packet to port 4500 every ten seconds. Each one left from the phone’s real Wi-Fi address, 192.168.1.182, bound for a destination the sending application had picked. Always-on VPN and “Block connections without VPN” were both enabled for that application the entire time. The capture was taken on the router, past Android’s VPN policy boundary, so the packets had already escaped lockdown by the time they were recorded.

The phone is doing what the app asked, through a documented public API. Android lets an ordinary application keep an IPsec NAT-T mapping alive on UDP/4500 without the app waking up for each packet. The app opens an IpSecManager.UdpEncapsulationSocket, calls ConnectivityManager.createSocketKeepalive(...), and starts it with a ten-second interval, the public minimum. The framework builds a finished keepalive packet and hands it to the Wi-Fi keepalive offload path in the HAL or chipset firmware, which emits it on a timer.

That offload path is where confinement fails. VPN lockdown filters each app’s socket writes through per-UID fwmark/netd routing and firewall rules. Once a keepalive is armed, the firmware sends packets with no socket write from the app, so none of those controls ever see the traffic. The emitter sits below the layer where lockdown operates.

The request should be refused for a UID under lockdown, and it is not. Admission runs through startNattKeepaliveWithFd(...), which takes a duplicated file descriptor and an IpSec resource ID and proves nothing about either. The validation routine, isNattKeepaliveSocketValid(fd, resourceId), accepts any non-null fd and does not meaningfully consult the resource ID; values like 0, -2 and Integer.MAX_VALUE pass. Nothing in the path checks whether the calling UID is covered by a non-bypassable VPN before offload starts.

This is a trust model that collapsed over time. The fd-based method began as a privileged raw-fd API. When Android added public IPsec and IKE support, public UdpEncapsulationSocket keepalives were routed through the same method and the unconditional permission check was removed to let ordinary apps use it. A later patch restored safety by validating caller UID ownership, pinning the encapsulation socket for the keepalive’s lifetime and releasing it on stop. It was reverted over service-dependency and deadlock concerns and replaced with per-UID and per-network quotas. Quotas cap resource exhaustion; they do not authenticate the fd, hold an IpSec lease, or enforce VPN policy.

What the attacker gets is narrow and useful. The payload is the platform’s fixed NAT-T format, so there is no arbitrary data exfiltration. The attacker chooses the destination and reads the source. A server at that destination learns the device’s real address as seen from the physical Wi-Fi network, that the device is online, and the packet cadence. Depending on the network, the source IP can reveal ISP, organisation or travel state, and it correlates a device that is supposed to be behind a VPN with a specific non-VPN access point. For a user who enabled lockdown precisely to keep that identity off the network, the guarantee is gone.

The bar to pull this off is low. The validated path needs no root, no ADB, no hidden-API access, no JNI, no raw Binder, no dangerous runtime permission and not the privileged PACKET_KEEPALIVE_OFFLOAD permission. The proof-of-concept apps declared only INTERNET and ACCESS_NETWORK_STATE.

Once armed on the Pixel, the keepalive stayed up through backgrounding, screen lock, Doze, battery saver, the restricted standby bucket, the Binder freezer, and a GrapheneOS relock to before-first-unlock without a reboot. It stopped only on force-stop, uninstall, network loss and reboot; a Wi-Fi drop ended it with a network-loss error, and re-arming restored it when Wi-Fi returned. On a Samsung SM-F966B the active keepalive slot held a continuous router-directed lease for 24 hours and 32 minutes.

The researchers confirmed the behaviour on three OEMs running Android 16: the Pixel 8 Pro on Broadcom Wi-Fi, which produced the only controlled packet capture, and a Samsung SM-F966B and Nothing A059 on Qualcomm, which contributed active-slot confirmation but no external capture. The vulnerable admission code is shared Android 12+ framework behaviour. A firmware census across seven WLAN families, covering an estimated 91.24% of Android-derived shipments since late 2021, found keepalive or offload surfaces in all of them. On that basis the authors describe device-class exposure across most Android 12+ devices, with about 9% of the long-tail SKU space unresolved.

Hardly any legitimate app uses the machinery being abused. A static scan of 4,679 distinct open-source origins from F-Droid and IzzyOnDroid found zero uses of Android’s framework IPsec, IKE or NAT-T APIs; the 73 VpnService apps in the corpus all used the standard TUN path. That makes NAT-T keepalive admission a reasonable thing to lock down hard, though the sample excludes proprietary enterprise, OEM and carrier VPN clients, so the zero is not universal.

The proposed fix lands at admission: authenticate that the fd and resource ID belong to a live caller-owned IpSec resource, and enforce the caller UID’s effective VPN-lockdown policy before offload begins. Until a platform patch ships, there is nothing an app or a user can do on the device to stop the emission, because it happens below the app socket layer. If you rely on lockdown to keep a device’s real address off a network, the only place to catch this is the network itself: watch for UDP/4500 leaving an interface that is supposed to carry VPN traffic only.


#ad Contains an affiliate link.

Share

Keep Reading

Latest on the Wire

Full wire →

New signal daily · RSS

Stay in the loop

New writing delivered when it's ready. No schedule, no spam.