#20983·tailscale

android: DNS queries forwarded to a tailnet-IP resolver intermittently dropped in-app; triggers "DNS unavailable" and ~5s stalls

Author: sentrizCreated Aug 25, 2026Updated Sep 17, 2026
LabelsOS-androiddnsbug

What is the issue?

With split DNS pointing at a resolver on a tailnet IP 100.100.6.79, Android intermittently shows the "DNS unavailable" health warning and app DNS lookups stall ~5-20s, while the resolver itself is healthy.

While the warning was showing:

  • From Termux, all resolvers answer fast: quad100, 100.100.6.79, global nameserver. All <100ms
  • curl to a split-domain host stalls 5s on DNS. tcpdump on the resolver's host shows getaddrinfo's first query never arrives. then the 5s retry is answered in 0.2ms.
  • So the netd -> quad100 -> forwarder path fail

logcat during a failure, right after a burst of bindSocketToActiveNetwork rebinds:

gojni: dns udp query: waiting for response or error from [100.100.6.79]: context deadline exceeded

That timeout sets dnsForwarderFailing the warning message.

Fable 5 analysis, unverified

on Android dialer.UseNetstackForIP is never set (only cmd/tailscaled and tsnet set it), so forwarder.dialUDP to a tailnet-IP upstream falls through to stdNetPacketListener - a plain kernel socket with no protect/bind control, which only reaches the resolver by looping back through the app's own VPN TUN. A VPN reconfigure/rebind silently drops the query; direct queries to the same resolver (a normal TUN flow) work throughout

next session

Did some digging on this. I think the root cause is that on Android, dialer.UseNetstackForIP is never set (it's wired up in cmd/tailscaled/tailscaled.go but not in tailscale-android's libtailscale/backend.go). So when the in-app forwarder sends a UDP query to a resolver at a tailnet IP, forwarder.dialUDP (net/dns/resolver/forwarder.go) falls through to stdNetPacketListener - a plain kernel socket whose only route to a tailnet IP is looping back through the app's own VPN TUN. Any time the VPN reconfigures/rebinds (network changes, doze wakeups), those in-flight queries are silently dropped, which matches the intermittent failures and ~5s stalls here.

The netstack UDP path to make this work already exists since #20786 - it looks like the fix is just setting UseNetstackForIP in the Android backend the same way tailscaled does, so forwarded DNS to tailnet IPs goes via netstack instead of the kernel socket.

battery session

A side note on this setup in general: with the resolver on a tailnet IP, every background DNS query from every app rides the tunnel, so the radio and tunnel never sleep. batterystats had the app at 58.9% of mobile-radio-active time (1517 radio wakeups, 1.3M packets, ~35% of total battery blame). After moving DNS off the tailnet IP (public DoH upstream instead, plus the silent-disco nodeAttr), that dropped ~40x to a 7.2% radio-active share and a handful of wakeups. Worth keeping in mind for anyone running this kind of setup even once the forwarder bug is fixed.

possibly related: #19649, #13314

Steps to reproduce

  1. In Admin console, add a split DNS entry pointing a domain at a resolver on a tailnet IP
  2. Use an Android device normally
  3. Intermittently get "DNS unavailable" warning, and system resolver lookups in the split domain stall ~5s. direct dig @<resolver> works fine

Are there any recent changes that introduced the issue?

No response

OS

Android

OS version

Android 17 (Pixel 10a, Graphene OS)

Tailscale version

1.102.2

Other software

no other VPN, Private DNS off

Bug report

BUG-77ea843794da3961009912a4f1dc6c3379a791520f58f853375cad4ef0f1d31f-20260825164135Z-455d1f48004e6f6c