You get a shell on the device, read /etc/resolv.conf or dig out an old net.dns1 value, and it gives you one address. The app in front of you resolves through something else entirely. On a current Android release the resolver is per network: the system holds a policy for each Network and applies it to the socket that made the request. The file and the legacy property are leftovers, and neither one describes the path your query actually took.

That gap shows up during split-tunnel and Private DNS rollouts, where two apps on the same phone can legitimately see different answers. This walkthrough starts from the wrong or missing lookup and works backwards to the configuration that owns it.

The resolver belongs to a network, not to the device

Name resolution on Android 9 and later goes through a system resolver that keys off the Network a socket is bound to. Each network carries its own LinkProperties, and those properties hold the DNS server list, the search domains and the Private DNS state for that network alone. Nothing in that model maps onto one device-wide list you can read from a file.

A VPN makes the split obvious. When the tunnel comes up, the VPN app supplies DNS servers that are registered on the VPN's own network, so apps routed through it resolve against those servers while excluded apps keep using the underlying network. One device, two answer paths, no contradiction.

Read the snapshot from inside the affected app

Ask the app itself. A method that has a Context plus the normal android.permission.ACCESS_NETWORK_STATE permission can read the snapshot without touching system properties:

val cm = getSystemService(ConnectivityManager::class.java) ?: return
val network = cm.activeNetwork
val links = network?.let { cm.getLinkProperties(it) }

val dnsServers = links?.dnsServers
val privateDnsActive = links?.isPrivateDnsActive
val privateDnsName = links?.privateDnsServerName

activeNetwork is the default for your process, not the only network on the device, and it can be null when the caller has no usable default. A blocked VPN default is the usual reason. isPrivateDnsActive tells you whether queries on that network go over TLS to a named resolver, which changes what a plain UDP 53 probe will show you. The API surface is documented under LinkProperties, and every value in it is a snapshot of configuration rather than proof that a particular query reached a particular server.

When the default network is not the one doing the work

The snapshot answers only for the default. An app can bind its process or a single socket to another network, and then the lookup follows that choice while your diagnostic still reports the default. A browser shipping its own DNS-over-HTTPS resolver never consults the system list at all, so its answers say nothing about the rest of the device. A work-profile app runs under a separate user with its own policy. Check the binding before you blame the servers.

VPN integration is the other half of this. Receiving a dhcp-option DNS line in an OpenVPN profile and registering that address on the Android VPN network are two separate steps, and a missing route to the server fails just as quietly as a wrong address. From a development machine, adb shell dumpsys connectivity prints the same per-network link properties when the build allows it, which confirms the list without writing any code.

Compare configuration with one lookup you are allowed to make

Pick a name that must resolve inside the private namespace and one public name, then record both results beside the snapshot: network, configured servers, Private DNS state and the exact error text. A direct query to a named server proves that server answers you; it does not prove ordinary application resolution selected it. A cached answer is not a fresh test, so flush or use a name nobody has queried today.

If the resolver only serves clients from a particular access network, what it sees after VPN egress can decide the outcome, and that is a policy question for whoever runs it rather than evidence that carriers block UDP 53. Where Private DNS is active, an unqualified hostname may be refused by design while the same server answers public names normally. Reading the properties tells you which mode is in force, so you stop guessing at packet filters.

Fix the layer that owns the mismatch

Work upward instead of editing global state. If the app is bound to the wrong network, fix the binding. If the profile ships the wrong DNS option, fix the profile. If Private DNS is strict on a network whose resolver cannot serve your private zone, that is a device-management decision rather than a reason to switch the feature off everywhere.

Forcing public resolvers into a DHCP hook or a system property is the repair that keeps causing incidents: private names stop resolving, the change outlives the VPN session, and the next person on the device finds a stale address with no owner. Keep the change inside the VPN, app or management layer that owns the mismatch, then re-read the snapshot to confirm the server list actually changed.

What you want at the end is one app request tied to one network, resolver list and Private DNS state. Once you can show that chain, a familiar address in a file stops being evidence of anything.

Barruulee wal qabatan