Android: netstack stops transmitting after tun swap on network change; DNS forwarder (100.100.100.100) and PeerAPI go dead while WireGuard stays up
What is the issue?
On Android, after a Wi-Fi to cellular handoff, the client rebuilds its tun device and from that moment the userspace netstack can receive packets but can no longer send them. Everything served by netstack goes dark:
- The built-in DNS forwarder at 100.100.100.100 / fd7a:115c:a1e0::53 stops replying to any query (UDP or TCP). MagicDNS is disabled on this tailnet; the forwarder is in the path only because one split DNS route exists, which on Android forces quad-100 to be the sole OS nameserver.
- PeerAPI (netstack-served port 1 on Android) stops accepting connections; the client logs
CreateEndpoint error ... operation timed outfor every inbound attempt.
Meanwhile the rest of the client is healthy, so the UI says Connected and the failure looks like "no DNS":
- WireGuard data path works (
tailscale ping --tsmpand--icmpfrom a peer both pong). - Packets traverse the tun to the Android kernel (TCP to a closed kernel port gets an immediate RST).
- ICMP echo to 100.100.100.100 is answered (that is faked in tstun, not netstack).
Because Android sends all DNS from VPN-covered apps only to the VPN's nameservers, every app loses DNS. Each lookup takes ~24 s to fail (netd retries, plus a DNS-over-TLS probe to quad-100:853 that also hangs). Push notifications stop, and only return when Tailscale is turned off.
Toggling the VPN off/on in the app does not recover it: the Go process is not restarted, only the tun is rebuilt. Force-stopping the app (or rebooting) and reconnecting recovers immediately. The same wedge was also observed once in a process that had been alive for 2 days 18 hours, after an earlier Wi-Fi drop/return.
The trigger in the log
At the exact moment of the handoff, while the old tun is being torn down and before the new one is added, netstack tries to write to the tun and gets EIO. Nothing netstack sends after this point ever reaches the OS:
14:33:31.637 NetworkChangeCallback: onLost: network 121
14:33:31.644 NetworkChangeCallback: onLost: cachedDefaultNetwork=118 iface=rmnet16
14:33:31.674 NetworkChangeCallback: onLost: updated DNS config for iface=rmnet16
14:33:31.692 LinkChange: major, rebinding: old: interfaces.State{defaultRoute=rmnet16 ifs={... tun1:[100.119.246.83/32 ...] ...}
14:33:31.693 dns: Set: {DefaultResolvers:[] Routes:{test.:[100.116.83.2]} SearchDomains:[] Hosts:0}
14:33:31.693 dns: Resolvercfg: {Routes:{.:[fc00:a:a::400 172.26.38.2] test.:[100.116.83.2]} Hosts:0 LocalDomains:[]}
14:33:31.694 dns: OScfg: {Nameservers:[100.100.100.100 fd7a:115c:a1e0::53] }
14:33:31.694 updateTUN: changed
14:33:31.707 updateTUN: got new builder
14:33:31.707 updateTUN: set MTU
14:33:31.707 updateTUN: set nameservers
14:33:31.709 Received error: PollNetMap: read tcp 192.168.1.32:57076->192.200.0.114:80: use of closed network connection
14:33:31.713 updateTUN: added 7 routes (exclude-mode), localRoutes=0
14:33:31.713 updateTUN: added 2 local addrs
14:33:31.747 netstack inject inbound: write /dev/tun: input/output error <-- here
14:33:31.751 updateTUN: established VPN
14:33:31.751 updateTUN: detached FD
14:33:31.762 updateTUN: created TUN device
14:33:31.762 updateTUN: added TUN device
14:33:31.762 updateTUN: finished
14:33:31.762 wgengine: set DNS config again after major link change
14:33:31.784 Rebind; defIf="rmnet16", ips=[10.42.9.130/32 ...]
14:33:31.784 magicsock: closing connection to derp-1 (rebind-default-route-change), age 1m55s
...
14:36:07.473 CreateEndpoint error for 100.116.83.2:53384 -> 100.119.246.83:1: operation timed out
14:36:13.127 CreateEndpoint error for 100.116.83.2:53391 -> 100.119.246.83:1: operation timed out
(10 of these over the next two minutes, one per PeerAPI connection attempt from a peer)Note the DNS config itself is updated correctly on the handoff (upstream switched from 192.168.1.1 to the carrier resolvers), so this is not a stale-upstream problem and adding a global nameserver does not help; the query never gets a reply regardless of destination.
Measurements while wedged (from adb shell, uid 2000 is VPN-covered)
| Test | Result |
|---|---|
| A query, UDP, to 100.100.100.100 or fd7a:115c:a1e0::53 | no reply |
| PTR for a tailnet peer to 100.100.100.100 | no reply |
| Name under the split domain to 100.100.100.100 | no reply |
| TCP SYN to 100.100.100.100:53 | no SYN-ACK, no RST |
| Same queries directly to the split resolver 100.116.83.2 over the tunnel | answered |
| Same queries to the carrier/router resolver | answered |
| ICMP echo to 100.100.100.100 | answered in ~1 ms |
| tun packet counters over 5 DNS queries to quad-100 | +5 in, +0 out |
| tun packet counters over 5 pings to quad-100 | +5 in, +5 out |
From a peer: tailscale ping --tsmp / --icmp |
pong |
| From a peer: TCP to phone port 22 | refused immediately (kernel) |
From a peer: curl http://<phone>:1/ (PeerAPI) |
timeout, no recovery over 90 s |
After am force-stop com.tailscale.ipn and reconnecting (still on cellular): quad-100 answers all of the above, PeerAPI returns HTTP 200.
Relationship to #20983
#20983 (forwarded DNS to a tailnet-IP resolver intermittently dropped) looks similar from the user's side but is a different layer, and I think it should stay separate, as a commenter there already suggested for the rebind cases:
- In #20983 the netstack is alive and quad-100 answers quickly from the shell; what fails is the forwarder's upstream UDP socket to a tailnet IP, with
dns udp query: ... context deadline exceededin the log and adns-forward-failinghealth warning. Failures are intermittent, ~5 s stalls. - Here quad-100 never replies to anything, including queries whose upstream is the carrier resolver on the physical network, and a TCP SYN to quad-100:53 gets no SYN-ACK, so the query never reaches the forwarder at all. There are zero
dns udp query/deadline exceededlines and no health warning; the app thinks everything is fine. PeerAPI, which has nothing to do with DNS forwarding, is dead at the same time. The failure is total and permanent until the process is restarted.
The sibling case posted in #20983 on 2026-09-02 (1.102.3, Android 17, Wi-Fi/cellular roaming, "Connected" but nothing resolves, only a toggle recovers) may well be this bug rather than that one; it was not tested for PeerAPI or the tun write error.
Steps to reproduce
- Tailnet DNS settings: MagicDNS disabled, no global nameservers, one split DNS route (
test-> a tailnet node). - Android client with "Use Tailscale DNS" on, connected on Wi-Fi. Confirm DNS works.
- Disable Wi-Fi so the phone moves to cellular (or walk out of range).
- Within seconds, DNS for every VPN-covered app fails; app still shows Connected; peers still reachable by IP.
adb logcat -s gojni:*showsnetstack inject inbound: write /dev/tun: input/output errorat the handoff, thenCreateEndpoint error ... operation timed outfor any inbound PeerAPI connection.- Toggling the VPN in the app does not recover. Force-stopping the app and reconnecting does.
Reproduced deterministically on the first handoff after a fresh process start. Full logcat -s gojni:* App:* across the handoff attached.
Are there any recent changes that introduced the issue?
Unknown. Has been happening on network handoffs for a while; this is the first time it was traced.
OS
Android
OS version
Android 17, build CP2A.260805.005 (security patch 2026-08-05), Pixel 8 (shiba)
Tailscale version
1.102.3-t9329c3677-gaea8f60c0 (versionCode 297862600), Play Store build
Other software
None relevant. Private DNS is in the default "Automatic" mode. A handful of apps are excluded from the VPN via the app's split-tunneling list. No exit node, no subnet routes accepted.
Bug report
Not yet generated. The in-app bug report was not captured while wedged; I will add a BUG- ID on the next occurrence. The relevant client log is attached below.
Fulladb logcat -s gojni:* App:* across the handoff (161 lines, personal addresses redacted)--------- beginning of main
09-08 14:33:31.637 D/NetworkChangeCallback( 3675): onLost: network 121
09-08 14:33:31.639 D/gojni ( 3675): NetworkChangeCallback: onLost: network 121
09-08 14:33:31.644 D/NetworkChangeCallback( 3675): onLost: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:31.644 D/gojni ( 3675): NetworkChangeCallback: onLost: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:31.674 D/NetworkChangeCallback( 3675): onLost: updated DNS config for iface=rmnet16
09-08 14:33:31.675 D/gojni ( 3675): NetworkChangeCallback: onLost: updated DNS config for iface=rmnet16
09-08 14:33:31.682 D/gojni ( 3675): defaultgateway: update from Android, gateway = 0.0.0.0 (was 192.168.1.1)
09-08 14:33:31.692 D/gojni ( 3675): LinkChange: major, rebinding: old: interfaces.State{defaultRoute=rmnet16 ifs={rmnet16:[10.42.9.130/32 <phone-carrier-ipv6>/64 llu6] rmnet17:[<phone-carrier-ipv6-ims>/64 llu6] tun1:[100.119.246.83/32 fd7a:115c:a1e0::ee32:f654/128 llu6] wlan0:[192.168.1.32/24 llu6]} v4=true v6=true} new: interfaces.State{defaultRoute=rmnet16 ifs={rmnet16:[10.42.9.130/32 <phone-carrier-ipv6>/64 llu6] rmnet17:[<phone-carrier-ipv6-ims>/64 llu6] tun1:[100.119.246.83/32 fd7a:115c:a1e0::ee32:f654/128 llu6]} v4=true v6=true} diff: numInterfaces: 6->5; numInterfaceIPs: 6->5; if wlan0: removed; ips wlan0: [192.168.1.32/24 fe80::58ed:46ff:fe37:fbe6/128]->[] rebind-reason=[ips-changed]
09-08 14:33:31.693 D/gojni ( 3675): dns: Set: {DefaultResolvers:[] Routes:{test.:[100.116.83.2]} SearchDomains:[] Hosts:0}
09-08 14:33:31.693 D/gojni ( 3675): dns: Resolvercfg: {Routes:{.:[fc00:a:a::400 172.26.38.2] test.:[100.116.83.2]} Hosts:0 LocalDomains:[]}
09-08 14:33:31.694 D/gojni ( 3675): dns: OScfg: {Nameservers:[100.100.100.100 fd7a:115c:a1e0::53] }
09-08 14:33:31.694 D/gojni ( 3675): updateTUN: changed
09-08 14:33:31.700 D/IPNService( 3675): Application packages were set by user: []
09-08 14:33:31.700 D/gojni ( 3675): IPNService: Application packages were set by user: []
09-08 14:33:31.700 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.700 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.701 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.702 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.703 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.703 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.704 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.705 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.705 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.705 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.705 D/gojni ( 3675): Log [73 80 78 83 101 114 118 105 99 101 58 32 68 105 115 97 108 108 111 119 105 110 103 32 97 112 112 58 32 99 111 109 46 99 111 109 99 97 115 116 46 109 111 100 101 115 116 111 46 118 118 109 46 99 108 105 101 110 116] not sent
09-08 14:33:31.705 D/IPNService( 3675): Disallowing app: <excluded-app>
09-08 14:33:31.705 D/gojni ( 3675): Log [73 80 78 83 101 114 118 105 99 101 58 32 68 105 115 97 108 108 111 119 105 110 103 32 97 112 112 58 32 99 111 109 46 103 111 111 103 108 101 46 97 110 100 114 111 105 100 46 97 112 112 115 46 115 99 111 110 101] not sent
09-08 14:33:31.707 D/gojni ( 3675): updateTUN: got new builder
09-08 14:33:31.707 D/gojni ( 3675): updateTUN: set MTU
09-08 14:33:31.707 D/gojni ( 3675): updateTUN: set nameservers
09-08 14:33:31.708 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.708 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.708 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.708 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.708 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.708 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.709 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.709 D/gojni ( 3675): Received error: PollNetMap: read tcp 192.168.1.32:57076->192.200.0.114:80: use of closed network connection
09-08 14:33:31.709 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.709 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.709 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.709 D/gojni ( 3675): IPNService: Disallowing app: <excluded-app>
09-08 14:33:31.713 D/gojni ( 3675): updateTUN: added 7 routes (exclude-mode), localRoutes=0
09-08 14:33:31.713 D/gojni ( 3675): updateTUN: added 2 local addrs
09-08 14:33:31.747 D/gojni ( 3675): netstack inject inbound: write /dev/tun: input/output error
09-08 14:33:31.751 D/gojni ( 3675): Setting vpn activity status to true
09-08 14:33:31.751 D/gojni ( 3675): updateTUN: established VPN
09-08 14:33:31.751 D/gojni ( 3675): updateTUN: detached FD
09-08 14:33:31.762 D/gojni ( 3675): updateTUN: created TUN device
09-08 14:33:31.762 D/gojni ( 3675): updateTUN: added TUN device
09-08 14:33:31.762 D/gojni ( 3675): updateTUN: finished
09-08 14:33:31.762 D/gojni ( 3675): wgengine: set DNS config again after major link change
09-08 14:33:31.767 D/NetworkChangeCallback( 3675): onCapabilitiesChanged: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:31.768 D/gojni ( 3675): NetworkChangeCallback: onCapabilitiesChanged: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:31.776 D/App ( 3675): bindSocketToActiveNetwork: binding fd=5 to net=118 iface=rmnet16
09-08 14:33:31.777 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=5 to net=118 iface=rmnet16
09-08 14:33:31.779 D/gojni ( 3675): magicsock: [warning] failed to force-set UDP read buffer size to 7340032: operation not permitted; using kernel default values (impacts throughput only)
09-08 14:33:31.779 D/gojni ( 3675): magicsock: [warning] failed to force-set UDP write buffer size to 7340032: operation not permitted; using kernel default values (impacts throughput only)
09-08 14:33:31.780 D/App ( 3675): bindSocketToActiveNetwork: binding fd=126 to net=118 iface=rmnet16
09-08 14:33:31.782 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=126 to net=118 iface=rmnet16
09-08 14:33:31.784 D/gojni ( 3675): magicsock: [warning] failed to force-set UDP read buffer size to 7340032: operation not permitted; using kernel default values (impacts throughput only)
09-08 14:33:31.784 D/gojni ( 3675): magicsock: [warning] failed to force-set UDP write buffer size to 7340032: operation not permitted; using kernel default values (impacts throughput only)
09-08 14:33:31.784 D/gojni ( 3675): Rebind; defIf="rmnet16", ips=[10.42.9.130/32 <phone-carrier-ipv6>/64 fe80::200:ff:fe00:0/128]
09-08 14:33:31.784 D/gojni ( 3675): magicsock: closing connection to derp-1 (rebind-default-route-change), age 1m55s
09-08 14:33:31.784 D/gojni ( 3675): magicsock: 0 active derp conns
09-08 14:33:31.785 D/gojni ( 3675): magicsock: adding connection to derp-1 for home-keep-alive
09-08 14:33:31.785 D/gojni ( 3675): magicsock: 1 active derp conns: derp-1=cr0s,wr0s
09-08 14:33:31.785 D/App ( 3675): bindSocketToActiveNetwork: binding fd=128 to net=118 iface=rmnet16
09-08 14:33:31.788 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=128 to net=118 iface=rmnet16
09-08 14:33:31.789 D/gojni ( 3675): derphttp.Client.Recv: connecting to derp-1 (nyc)
09-08 14:33:31.790 D/App ( 3675): bindSocketToActiveNetwork: binding fd=128 to net=118 iface=rmnet16
09-08 14:33:31.790 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=128 to net=118 iface=rmnet16
09-08 14:33:31.790 D/App ( 3675): bindSocketToActiveNetwork: binding fd=134 to net=118 iface=rmnet16
09-08 14:33:31.791 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=134 to net=118 iface=rmnet16
09-08 14:33:31.791 D/App ( 3675): bindSocketToActiveNetwork: binding fd=135 to net=118 iface=rmnet16
09-08 14:33:31.791 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=135 to net=118 iface=rmnet16
09-08 14:33:31.840 D/App ( 3675): bindSocketToActiveNetwork: binding fd=132 to net=118 iface=rmnet16
09-08 14:33:31.840 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=132 to net=118 iface=rmnet16
09-08 14:33:32.138 D/gojni ( 3675): control: controlhttp: forcing port 443 dial due to recent noise dial
09-08 14:33:32.152 D/App ( 3675): bindSocketToActiveNetwork: binding fd=127 to net=118 iface=rmnet16
09-08 14:33:32.154 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=127 to net=118 iface=rmnet16
09-08 14:33:32.156 D/gojni ( 3675): magicsock: endpoints changed: <home-wan-ip>:54405 (portmap), <carrier-nat-ip>:62576 (stun), [<phone-carrier-ipv6>]:54880 (stun), 10.42.9.130:54405 (local), [<phone-carrier-ipv6>]:54405 (local), [<phone-carrier-ipv6-ims>]:54405 (local)
09-08 14:33:32.158 D/gojni ( 3675): control: NetInfo: NetInfo{varies=false ipv6=true ipv6os=true udp=true icmpv4=false derp=#1 portmap= link="" firewallmode=""}
09-08 14:33:32.159 D/gojni ( 3675): control: NetInfo: NetInfo{varies=false ipv6=true ipv6os=true udp=true icmpv4=false derp=#1 portmap=active- link="" firewallmode=""}
09-08 14:33:32.266 D/gojni ( 3675): magicsock: derp-1 connected; connGen=1
09-08 14:33:32.346 D/App ( 3675): bindSocketToActiveNetwork: binding fd=133 to net=118 iface=rmnet16
09-08 14:33:32.346 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=133 to net=118 iface=rmnet16
09-08 14:33:32.398 D/App ( 3675): bindSocketToActiveNetwork: binding fd=134 to net=118 iface=rmnet16
09-08 14:33:32.399 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=134 to net=118 iface=rmnet16
09-08 14:33:32.637 D/gojni ( 3675): control: netmap: got new dial plan from control
09-08 14:33:37.724 D/NetworkChangeCallback( 3675): onCapabilitiesChanged: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:37.748 D/gojni ( 3675): NetworkChangeCallback: onCapabilitiesChanged: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:43.765 D/NetworkChangeCallback( 3675): onCapabilitiesChanged: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:43.805 D/gojni ( 3675): NetworkChangeCallback: onCapabilitiesChanged: cachedDefaultNetwork=118 iface=rmnet16
09-08 14:33:53.823 D/App ( 3675): bindSocketToActiveNetwork: binding fd=127 to net=118 iface=rmnet16
09-08 14:33:53.825 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=127 to net=118 iface=rmnet16
09-08 14:33:53.898 D/gojni ( 3675): control: NetInfo: NetInfo{varies=false ipv6=true ipv6os=true udp=true icmpv4=false derp=#1 portmap= link="" firewallmode=""}
09-08 14:34:00.351 D/gojni ( 3675): magicsock: new contact: peer=[Jo2Zq] usec=744124888 cached=false via=derp
09-08 14:34:00.432 D/gojni ( 3675): magicsock: disco: node [Jo2Zq] d:847689a371310720 now using <home-wan-ip>:41641 mtu=1360 tx=10f4910204e9
09-08 14:34:00.433 D/gojni ( 3675): magicsock: new contact: peer=[Jo2Zq] usec=744207304 cached=false via=direct
09-08 14:34:14.134 D/App ( 3675): bindSocketToActiveNetwork: binding fd=127 to net=118 iface=rmnet16
09-08 14:34:14.135 D/gojni ( 3675): App: bindSocketToActiveNetwork: binding fd=127 to net=118 iface=rmnet16
09-08 14:34:39.477 D/App ( 3675): bindSocketToActiveNetwork: binding fd=133 to net=118 iface=rmnet16
09-08 14Source: tailscale/tailscale