Zygote
Connectivity, location & sensorsintermediate

The networking stack

Android does not just have a TCP stack. It has a policy engine in front of one: per-app routing, a per-UID firewall, a DNS resolver that can be encrypted, and a rule that plain HTTP is refused by default.

13 minAPI 1–37 · 17interactivequizverified 2026-10-27

A phone is a device that is constantly switching networks: Wi-Fi to cellular in a lift, cellular to Wi-Fi at home, a VPN that appears and disappears. Android's answer is not a smarter TCP stack. It is a policy layer wrapped around an ordinary Linux stack, which decides which network an app's socket uses, whether the traffic is allowed, and how names are resolved — all without the app asking.

TL;DR#

  • The app talks to ConnectivityManager; the decision is made by ConnectivityService in system_server and applied by netd, a native daemon.
  • Sockets carry a UID owner, which is how the kernel can route, firewall and account traffic per app rather than per device.
  • A VPN is just another network to the routing layer, which is how per-app VPN and "block traffic outside the tunnel" are both expressible as routing rules.
  • Android 9 (API 28) made two things default: cleartext HTTP is blocked unless declared, and Private DNS (DNS over TLS) can be set per network or device-wide.
  • Since Android 17 (API 37) reaching devices on the local network requires ACCESS_LOCAL_NETWORK, a runtime permission — the LAN is no longer implicitly trusted.

Mental model#

Think of a large office building with a single mailroom.

Every department writes letters, but nobody addresses an envelope to a post office. They drop the letter in the mailroom slot. The mailroom (netd) knows which department sent it, which courier contract is active today (Wi-Fi or cellular), whether that department is allowed to send non-urgent post right now (Data Saver), and whether the recipient's address should be looked up in the public phone book or a private one (Private DNS).

The departments never see the courier. They only see "delivered" or "refused".

Where the analogy breaks#

A real mailroom is a single choke point, but a phone's mailroom is not on the data path. Once a socket is connected, packets go from the app's process through the kernel to the network interface without passing through a userspace daemon. netd sets up rules; the kernel enforces them. That is the key architectural fact: policy is installed once and enforced in the kernel, so it costs almost nothing per packet.

The big picture#

Loading diagram…
Text description of this diagram

A sequence diagram of one HTTPS request. The app hands a request to its HTTP library, which calls socket() and connect() in libc. DNS resolution is handled by netd rather than a library in the app. netd records the socket's UID owner in the kernel so routing and accounting can be per app. The kernel routes the packet to the selected interface, which sends encrypted bytes to the remote server; the response travels back the same way. A second variant shows the same request under Data Saver on a metered network: netd's per-UID eBPF program returns a deny verdict on a background connect, so the app sees a socket error instead of traffic.

The two variants in that diagram are the same code path. That is deliberate: an app does not opt into policy, and it cannot opt out. The only difference is what netd decides at the moment of connect().

Deep dive#

The layers, and who owns each#

There are four things to keep separate, because they change on different schedules.

PieceWhere it livesWhat it decides
ConnectivityManagerframework, app processthe API you call; delivers callbacks
ConnectivityServicesystem_serverwhich network is default, per-app; metering; VPN
netdnative daemonrouting rules, DNS, firewall, eBPF programs
kernelLinuxactual packet forwarding and socket lookup

The first two are Java; the third is C++ speaking a Binder interface; the fourth is the kernel. ConnectivityService does not move packets. It computes a policy decision and asks netd to install it. netd then configures routes in the kernel's routing tables and attaches eBPF programs to the socket and packet paths.1

That separation is why a connectivity bug can look like a DNS bug, a routing bug or a firewall bug depending on which layer you inspect, and why dumpsys connectivity, dumpsys netd and ip rule give three different views of the same problem.

Per-UID routing: the idea that makes everything else possible#

Linux routes by destination address. Android needs to route by who is asking. The mechanism is a combination of three kernel features:

  1. Every socket is tagged with the owning UID (SO_PEERCRED-style attribution recorded when netd sees the connect()).
  2. Routing rules (ip rule) match on the socket's UID or on a firewall mark.
  3. A per-network routing table is selected for that UID.

This is how an app bound to a VPN can send its traffic through the tunnel while another app on the same device goes direct — on the same Wi-Fi. Without per-UID routing, VPN-per-app would be impossible.2

The firewall is eBPF, and it is per app#

Android uses eBPF programs for three jobs on the network path: per-UID packet accounting, socket filtering (the firewall), and, on newer kernels, traffic tagging for other subsystems. The programs are loaded by netd and attached at defined hook points; the policy itself lives in maps that netd updates.3

Two consequences follow, and both surprise people:

  • A firewall rule does not need a userspace process to run. Turning off an app's background data changes a map value. The next packet is dropped in the kernel.
  • Accounting is free. Because the packet is already being inspected, counting bytes per UID costs a map increment. That is what makes the per-app data usage screen possible without a proxy.

Metered, unmetered, and Data Saver#

Each network has properties: metered or not, roaming or not, and a NetworkCapabilities set. Apps can observe these through NetworkCallback. But observation is not enforcement. Enforcement is three separate policies:

  • Data Saver is a user-facing switch. When it is on, apps in the background are generally refused network access unless the user has allowlisted them.
  • Background restrictions can also come from the app standby bucket or from the user explicitly restricting an app.
  • Metered-ness does not by itself block anything. It informs the app, and it makes the system more aggressive about deferring background work.

The distinction matters when debugging: "my request fails only on cellular" is usually not a metered-network block, it is either a carrier policy, a captive portal, or Data Saver plus a background execution state. dumpsys connectivity shows which.

DNS: the quiet part of the stack#

Name resolution does not happen inside your app. getaddrinfo() in Bionic sends the query to the resolver that netd configured for the network the app is bound to. That indirection is what makes these possible:

  • Per-network DNS. A Wi-Fi network can hand out its own servers; switching networks switches resolvers.
  • Private DNS. Since Android 9 (API 28) the user can choose opportunistic, off, or a specific hostname for DNS over TLS. In opportunistic mode the system upgrades to TLS when the network supports it; with a hostname it requires it.4
  • The VPN DNS trap. A badly configured VPN that intercepts DNS but not traffic produces the classic "connected but names do not resolve" failure — the query goes into the tunnel and nothing answers.

Cleartext, network security config, and trust anchors#

The modern rule is easier to state than its history: plain HTTP is refused unless you explicitly allow it. The declaration lives in a network security config XML, referenced from the manifest, which can set:

  • cleartextTrafficPermitted per domain or globally.
  • Which certificate authorities are trusted, and whether user-installed CAs are trusted.
  • Certificate pinning for specific domains.

This is a build-time artefact, not a runtime API. The platform reads it when the app's network stack initialises, and it applies to HttpURLConnection, OkHttp (which reads the platform default) and WebView (with its own nuances).5

The reason to have a config rather than a flag is the same reason Android has resources: different domains need different rules, and a staging domain should not have to trust the same roots as production.

Where Mainline fits#

Connectivity is one of the areas that ships as a Mainline module. The practical meaning is that the connectivity code can be updated through Google Play without a full OS update, on devices that participate. The exact set of components inside the module varies by release and by device.unverified(The composition and release cadence of the Connectivity Mainline module change between releases, and I have not verified the current package list)1

The architectural consequence is more interesting than the list: because the module is versioned separately, the behaviour of connectivity can change without the platform version changing. A bug fix to network selection can arrive on a phone that still reports Android 14.

Relatedly, the TLS implementation itself is shipped as a Mainline module — Conscrypt — so security fixes to the TLS stack do not require an OS update either.6

The local network becomes a permission#

Until recently, an app with INTERNET could scan the LAN freely: find the printer, the smart light, the TV. API 37 changes that. Reaching devices on the local network now requires ACCESS_LOCAL_NETWORK, a dangerous (runtime) permission, and the platform defines a local network as a broadcast-capable interface such as Wi-Fi or Ethernet, explicitly excluding cellular and VPN.7

This is a privacy change, not a capability change. The capability was always there; what changed is that the user now gets a say. It also means library code that auto-discovers devices must degrade gracefully when the permission is denied, rather than treating an empty device list as "no devices exist".

How it evolved#

Permission changes7 releases
APIVersionWhat changed
184.3 Jelly Bean
  • App Ops (hidden) introduced for permission control
236.0 Marshmallow
  • requestPermissions() and the runtime permission flow
289 Pie
  • Foreground service permission
3011
  • One-time permissions
3112
  • Bluetooth scan/connect permissions
3313 Tiramisu
  • POST_NOTIFICATIONS runtime permission
3717 Cinnamon Bun
  • ACCESS_LOCAL_NETWORK runtime permission for local network access

The direction is consistent: each release adds a decision the user or the system can make independently of the app's wishes.

2008–2017Trust the networkApps declared INTERNET and got it. Cleartext was normal, DNS was whatever the network said.API 1
2018 →Network is hostilePrivate DNS, cleartext blocked by default, per-app VPN routing.API 28
2019 →Policy per UIDData Saver, background restrictions and eBPF accounting mature together.API 29
2026 →Local network consentThe LAN stops being implicitly trusted; ACCESS_LOCAL_NETWORK gates it.API 37

Under the hood#

Curiosity corner#

  • Your app cannot tell whether it is on Wi-Fi by asking the socket. It asks ConnectivityManager, which is a separate service with its own view. The socket has no opinion.
  • INTERNET is a normal permission. It is granted at install without a dialog, which is exactly why the interesting controls had to be built somewhere else.
  • A VPN is just a network. To the routing layer, a VPN interface is another interface with another table; that is why per-app VPN and "block traffic outside the tunnel" are both expressible as routing rules rather than special cases.2

Try it#

Inspect the network decision

adb shell dumpsys connectivity | head -60 adb shell dumpsys netd | head -40 adb shell cmd connectivity airplane-mode

Example output
NetworkAgentInfo for wlan0: CONNECTED, default, metered=false
Active default network: 100
UID 10123: allowed=true, metered network, background restricted=false

dumpsys connectivity shows the default network and its capabilities; dumpsys netd shows the rules netd installed. Reading them together is how you tell a policy problem from a radio problem.

Check yourself#

Check yourself

1 / 12

Which component decides which network an app's socket should use?

0 answered · 0 correct

Sources#

Sources

Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.

  1. docsConnectivity overviewsource.android.comaccessed 2026-10-27↩
  2. docsVPN servicesdeveloper.android.comaccessed 2026-10-27↩
  3. docseBPF on Androidsource.android.comaccessed 2026-10-27↩
  4. docsPerubahan perilaku: aplikasi yang menargetkan API level 28+developer.android.comaccessed 2026-10-27↩
  5. docsConnectivity overviewdeveloper.android.comaccessed 2026-10-27↩
  6. docsConscrypt mainline modulesource.android.comaccessed 2026-10-27↩
  7. docsManifest.permission | API referencedeveloper.android.comaccessed 2026-10-27↩