Android: TCP connections to tailnet peers are closed about 2 s after the phone joins Wi-Fi when mobile data stays connected in the background
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Newbie friendliness
- 35/100
- Issue type
- Bug
- Clarity
- Mostly clear
- Activity status
- Active
- Tech stack
- android, go
- Domain
- mobile, networking
Research direction
Start with the Android VPN setup and network-change handling; the report points to VpnService.Builder.allowBypass() and Tailscale Android issue #790, but does not name repository files or tests. First determine whether Tailscale can change the socket network behavior without allowing apps to bypass the VPN, or whether this needs an Android report or documentation. Done means selecting and implementing a supported resolution for the described TCP disconnects.
Written by the indexing model from the issue text.
Description
What is the issue?
On Android, a TCP connection that another app opens to a tailnet peer while the phone is on mobile data is closed about two seconds after the phone joins Wi-Fi. The app gets ECONNABORTED, and ssh prints client_loop: send disconnect: Broken pipe. The tunnel stays up and keeps its Android network (netId) across the switch, and a new connection works at once. A connection that opened on Wi-Fi survives a move to mobile data and back.
It happens when mobile data stays connected in the background after Wi-Fi takes over, which is what Developer options > Mobile data always active does. I expected a connection that the tunnel carries to survive a switch of the network under the tunnel.
Cause, as far as I could trace it
- At
connect(), netd tags the socket with the netId of the default physical network unless the VPN for that UID is bypassable.NetworkController::getNetworkForConnectLockedreturns the netId of the VPN onlyif (virtualNetwork && !virtualNetwork->isSecure()). The Tailscale app does not callVpnService.Builder.allowBypass(), so a socket to a tailnet peer has the netId of Wi-Fi or mobile data in its fwmark, although the routing rule of the VPN carries it. In Termux,getsockopt(SO_MARK)on a socket whose local address was the phone's Tailscale address returned0x160: netId 352, which was mobile data. The VPN was netId 337. - When the previous default network finishes lingering and stays connected, ConnectivityService removes
FOREGROUNDfrom it and changes its permission toNETWORK(getNetworkPermissionandupdateNetworkPermissions). Android then destroys every live TCP socket that has that netId in its fwmark and whose owner lacks the permission. In netd that isPhysicalNetwork::setPermission, which callsdestroySocketsLackingPermission. On Android 17 the log shows the same step fromInetDiagMessage.
So Android destroys the socket because of a network that the socket does not use.
logcat on Android 17 (times in UTC), followed by what the two apps reported:
03:02:36.318 ConnectivityService: Sending CONNECTED broadcast for type 1 [360 WIFI] isDefaultNetwork=true
03:02:36.667 Vpn : Established by com.tailscale.ipn on tun1
03:02:38.267 ConnectivityService: handleLingerComplete for [352 CELLULAR]
03:02:38.267 ConnectivityService: Update capabilities for net 352 : -FOREGROUND
03:02:38.268 InetDiagMessage: Destroyed 2 sockets, proto=IPPROTO_TCP, family=AF_INET, states=14
03:02:38.269 InetDiagMessage: Destroyed live tcp sockets for lacking permission for netId:352 and permission:1 in 2ms
03:02:38.273 ssh: client_loop: send disconnect: Broken pipe
03:02:38.282 node: read ECONNABORTED
In eight switches from mobile data to Wi-Fi, each connection that had opened on mobile data (ssh and Node) ended 1.8 to 2.0 s after Wi-Fi became the default network.
To confirm the cause, I tagged a socket with the network of the VPN before connect(). I called setNetworkForSocket in libnetd_client.so, which is the call behind android_setsocknetwork and Network.bindSocket. The fwmark of that socket has netId 337 and the explicit-selection bit. In six of those switches such a connection ran next to an untagged one, and it stayed open each time.
With Mobile data always active off, Android disconnects mobile data when Wi-Fi takes over, and the connection survived in the one switch that I measured. The cost is in the other direction: mobile data must connect after Wi-Fi drops, and a connection paused for 5.6 s in the one switch that I measured. With the setting on, that pause varied from under 2 s to 7.8 s. #22337 describes the cause.
A second, smaller case
A socket with no netId in its fwmark is destroyed each time the app establishes the tunnel again. Programs that make the connect system call themselves have such sockets, for example Go programs in Termux. From the same switch, with a Go program that held a connection to a tailnet peer:
03:02:36.639 gojni : LinkChange: major, rebinding: ...
03:02:36.640 gojni : updateTUN: changed
03:02:36.667 Vpn : Established by com.tailscale.ipn on tun1
03:02:36.701 InetDiagMessage: Destroyed 1 sockets, proto=IPPROTO_TCP, family=AF_INET, states=14
03:02:36.702 InetDiagMessage: Destroyed live tcp sockets for local address /100.x.y.z, netIdRanges[[0, 99]], uidRanges:null in 1ms
03:02:36.707 go: splice: software caused connection abort
Sockets that have a netId survive this step, which I take to be the effect of tailscale/tailscale-android#790.
What I am asking
I do not know whether Tailscale can fix the first case alone, because Android destroys the socket. These are the options that I can see:
- Report it to Android. The destroy step selects sockets by the netId in the fwmark and does not consider that a VPN that is not bypassable carries them. You may have a better channel for that than a user report.
- Offer a setting that calls
allowBypass(). According to the comment abovegetNetworkForConnectLocked, netd then tags each socket with the netId of the VPN. The cost is that apps can bypass the tunnel. - Describe the behavior in the Android documentation, with Mobile data always active off as the workaround.
Steps to reproduce
- Connect Tailscale on an Android phone and turn on Developer options > Mobile data always active.
- Turn Wi-Fi off. In Termux, run
ssh PEER 'while sleep 1; do date; done'for a tailnet peer. - Turn Wi-Fi on. About two seconds after Wi-Fi becomes the default network, ssh ends with
client_loop: send disconnect: Broken pipe. adb logcat | grep -E 'handleLingerComplete|InetDiagMessage'shows the lines above.
Are there any recent changes that introduced the issue?
It started when I turned on Mobile data always active.
OS
Android
OS version
Android 17 on a Samsung Galaxy Z Fold 8 (SM-F971U1)
Tailscale version
1.104.5
Other software
Termux with OpenSSH 10.6p2 and Node 24.22.2. Always-on VPN is on for Tailscale, and Block connections without VPN is off.
Bug report
No response
- Dominant language
- Go
- Stars
- 38.8k
- Forks
- 3.4k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 194
Getting set up
- Ships a Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from tailscale/tailscale
-
FR: update golang.org/x/sys to v0.50.0Possibly taken @wrenix claimed this today. Openfr needs-triage
Difficulty 2/5 Under an hour Newbie friendliness 62/100
Maintainers usually reply within 1 day
-
fr
Difficulty 2/5 1-3 hours Newbie friendliness 76/100
Maintainers usually reply within 1 day
-
bug kubernetes
Difficulty 2/5 1-3 hours Newbie friendliness 67/100
Maintainers usually reply within 1 day
-
Document using Docker Compose secrets support for TS_AUTHKEYPossibly taken @Marouane-elfadli claimed this 6 days ago. OpenDocumentation
Difficulty 1/5 1-3 hours Newbie friendliness 84/100
tailscale/tailscale#22214 · 1 reaction ·
Maintainers usually reply within 1 day
-
fr OS-linux
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
Maintainers usually reply within 1 day
All issues in tailscale/tailscale
Similar issues
-
area: interop area: sip-core v3
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
fonoster/routr#705 · 1 comment ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
-
kind/bug require/reproduce require/severity
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 75/100
oxc-project/tsgolint#1362 ·
Maintainers usually reply within 1 day
-
enhancement
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
hashicorp/terraform-provider-google#30703 ·
Maintainers usually reply within 1 day