Skip to content

Commit eb2af69

Browse files
committed
android: cache default network and set callback for binding fn
After switching from cellular to wifi without ipv6, ForeachInterface still sees rmnet prefixes, so HaveV6 stays true, and magicsock keeps attempting ipv6 connections that either route through cellular or time out for users on wifi without ipv6 This -Caches cachedDefaultNetwork on nevwork event in NetworkChangeCallbac -Implements BindSocketToNetwork, a callback that binds the socket to the currently selected Network -Sets the callback function whtne the VPN starts and clears it on disconnect Updates tailscale/tailscale#6152 Signed-off-by: kari-ts <kari@tailscale.com>
1 parent 2e37ada commit eb2af69

4 files changed

Lines changed: 90 additions & 18 deletions

File tree

android/src/main/java/com/tailscale/ipn/App.kt

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -449,7 +449,35 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
449449
override fun hardwareAttestationKeyLoad(id: String) {
450450
return getKeyStore().load(id)
451451
}
452+
453+
override fun bindSocketToNetwork(fd: Int): Boolean {
454+
val net =
455+
NetworkChangeCallback.cachedDefaultNetwork
456+
?: run {
457+
TSLog.d(TAG, "bindSocketToActiveNetwork: no cached default network; noop")
458+
return false
459+
}
460+
461+
val iface = NetworkChangeCallback.cachedDefaultInterfaceName
462+
463+
TSLog.d(
464+
TAG,
465+
"bindSocketToActiveNetwork: binding fd=$fd to net=$net iface=$iface",
466+
)
467+
468+
return try {
469+
android.os.ParcelFileDescriptor.fromFd(fd).use { pfd -> net.bindSocket(pfd.fileDescriptor) }
470+
true
471+
} catch (e: Exception) {
472+
TSLog.w(
473+
TAG,
474+
"bindSocketToActiveNetwork: bind failed fd=$fd net=$net iface=$iface: $e",
475+
)
476+
false
477+
}
478+
}
452479
}
480+
453481
/**
454482
* UninitializedApp contains all of the methods of App that can be used without having to initialize
455483
* the Go backend. This is useful when you want to access functions on the App without creating side

android/src/main/java/com/tailscale/ipn/NetworkChangeCallback.kt

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,21 @@ object NetworkChangeCallback {
2121

2222
private val lock = ReentrantLock()
2323

24-
private val activeNetworks = mutableMapOf<Network, NetworkInfo>() // keyed by Network
24+
// All currently active non-VPN networks we know about.
25+
private val activeNetworks = mutableMapOf<Network, NetworkInfo>()
26+
27+
// Cached chosen default network for outbound sockets.
28+
@Volatile
29+
var cachedDefaultNetwork: Network? = null
30+
private set
31+
32+
// Cached info for the chosen default network.
33+
@Volatile private var cachedDefaultNetworkInfo: NetworkInfo? = null
34+
35+
// Convenience: cached interface name for logging.
36+
@Volatile
37+
var cachedDefaultInterfaceName: String? = null
38+
private set
2539

2640
// monitorDnsChanges sets up a network callback to monitor changes to the
2741
// system's network state and update the DNS configuration when interfaces
@@ -45,34 +59,45 @@ object NetworkChangeCallback {
4559
connectivityManager.registerNetworkCallback(
4660
networkConnectivityRequest,
4761
object : ConnectivityManager.NetworkCallback() {
62+
4863
override fun onAvailable(network: Network) {
4964
super.onAvailable(network)
5065

51-
TSLog.d(TAG, "onAvailable: network ${network}")
66+
TSLog.d(TAG, "onAvailable: network $network")
67+
5268
lock.withLock {
5369
activeNetworks[network] = NetworkInfo(NetworkCapabilities(), LinkProperties())
70+
recomputeDefaultNetworkLocked("onAvailable")
5471
}
5572
}
5673

5774
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
5875
super.onCapabilitiesChanged(network, capabilities)
59-
lock.withLock { activeNetworks[network]?.caps = capabilities }
76+
77+
lock.withLock {
78+
activeNetworks[network]?.caps = capabilities
79+
recomputeDefaultNetworkLocked("onCapabilitiesChanged")
80+
}
6081
}
6182

6283
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
6384
super.onLinkPropertiesChanged(network, linkProperties)
85+
6486
lock.withLock {
6587
activeNetworks[network]?.linkProps = linkProperties
88+
recomputeDefaultNetworkLocked("onLinkPropertiesChanged")
6689
maybeUpdateDNSConfig("onLinkPropertiesChanged", dns)
6790
}
6891
}
6992

7093
override fun onLost(network: Network) {
7194
super.onLost(network)
7295

73-
TSLog.d(TAG, "onLost: network ${network}")
96+
TSLog.d(TAG, "onLost: network $network")
97+
7498
lock.withLock {
7599
activeNetworks.remove(network)
100+
recomputeDefaultNetworkLocked("onLost")
76101
maybeUpdateDNSConfig("onLost", dns)
77102
}
78103
}
@@ -101,7 +126,7 @@ object NetworkChangeCallback {
101126
activeNetworks.filter { (_, info) ->
102127
info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
103128
info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
104-
info.linkProps.dnsServers.isNotEmpty() == true
129+
info.linkProps.dnsServers.isNotEmpty()
105130
}
106131

107132
// If we have one; just return it; otherwise, prefer networks that are also
@@ -119,49 +144,59 @@ object NetworkChangeCallback {
119144
for ((network, info) in activeNetworks) {
120145
if (info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
121146
info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
122-
Log.w(
123-
TAG,
124-
"no networks available that also have DNS servers set; falling back to first network ${network}")
147+
Log.w(TAG, "no networks with DNS; falling back to first network $network")
125148
return network
126149
}
127150
}
128151

129152
// Otherwise, return nothing; we don't want to return a VPN network since
130153
// it could result in a routing loop, and a non-INTERNET network isn't
131154
// helpful.
132-
Log.w(TAG, "no networks available to pick a default network")
155+
Log.w(TAG, "no networks available to pick default network")
133156
return null
134157
}
135158

159+
// Update cached default network + log interface name.
160+
private fun recomputeDefaultNetworkLocked(why: String) {
161+
val newNetwork = pickDefaultNetwork()
162+
cachedDefaultNetwork = newNetwork
163+
164+
val info = if (newNetwork != null) activeNetworks[newNetwork] else null
165+
cachedDefaultNetworkInfo = info
166+
cachedDefaultInterfaceName = info?.linkProps?.interfaceName
167+
168+
TSLog.d(
169+
TAG, "$why: cachedDefaultNetwork=$newNetwork iface=${cachedDefaultInterfaceName ?: "none"}")
170+
}
171+
136172
// maybeUpdateDNSConfig will maybe update our DNS configuration based on the
137173
// current set of active Networks.
138174
private fun maybeUpdateDNSConfig(why: String, dns: DnsConfig) {
139-
val defaultNetwork = pickDefaultNetwork()
175+
val defaultNetwork = cachedDefaultNetwork
140176
if (defaultNetwork == null) {
141-
TSLog.d(TAG, "${why}: no default network available; not updating DNS config")
177+
TSLog.d(TAG, "$why: no default network available; not updating DNS")
142178
return
143179
}
144-
val info = activeNetworks[defaultNetwork]
180+
181+
val info = cachedDefaultNetworkInfo
145182
if (info == null) {
146-
Log.w(
147-
TAG,
148-
"${why}: [unexpected] no info available for default network; not updating DNS config")
183+
Log.w(TAG, "$why: no info for default network; not updating DNS")
149184
return
150185
}
151186

152187
val sb = StringBuilder()
153188
for (ip in info.linkProps.dnsServers) {
154189
sb.append(ip.hostAddress).append(" ")
155190
}
191+
156192
val searchDomains: String? = info.linkProps.domains
157193
if (searchDomains != null) {
158194
sb.append("\n")
159195
sb.append(searchDomains)
160196
}
197+
161198
if (dns.updateDNSFromNetwork(sb.toString())) {
162-
TSLog.d(
163-
TAG,
164-
"${why}: updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName})")
199+
TSLog.d(TAG, "$why: updated DNS config for iface=${info.linkProps.interfaceName}")
165200
Libtailscale.onDNSConfigChanged(info.linkProps.interfaceName)
166201
}
167202
}

libtailscale/backend.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,12 @@ func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error {
224224
}
225225
return nil // even on error. see big TODO above.
226226
})
227+
netns.SetAndroidBindToNetworkFunc(func(fd int) error {
228+
if ok := a.appCtx.BindSocketToNetwork(int32(fd)); !ok {
229+
log.Printf("[unexpected] IPNService.bindSocketToNetwork(%d) returned false", fd)
230+
}
231+
return nil
232+
})
227233
log.Printf("onVPNRequested: rebind required")
228234
// TODO(catzkorn): When we start the android application
229235
// we bind sockets before we have access to the VpnService.protect()
@@ -249,6 +255,7 @@ func (a *App) runBackend(ctx context.Context, hardwareAttestation bool) error {
249255
if vpnService.service != nil && vpnService.service.ID() == s.ID() {
250256
b.CloseTUNs()
251257
netns.SetAndroidProtectFunc(nil)
258+
netns.SetAndroidBindToNetworkFunc(nil)
252259
vpnService.service = nil
253260
}
254261
case i := <-onDNSConfigChanged:

libtailscale/interfaces.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ type AppContext interface {
7474
HardwareAttestationKeyPublic(id string) (pub []byte, err error)
7575
HardwareAttestationKeySign(id string, data []byte) (sig []byte, err error)
7676
HardwareAttestationKeyLoad(id string) error
77+
78+
BindSocketToNetwork(fd int32) bool
7779
}
7880

7981
// IPNService corresponds to our IPNService in Java.

0 commit comments

Comments
 (0)