Skip to content

Commit 0e60ef4

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 e118bbd commit 0e60ef4

4 files changed

Lines changed: 101 additions & 23 deletions

File tree

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

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,11 +445,34 @@ class App : UninitializedApp(), libtailscale.AppContext, ViewModelStoreOwner {
445445
return getKeyStore().public(id)
446446
}
447447

448-
@Throws(NoSuchKeyException::class)
449-
override fun hardwareAttestationKeyLoad(id: String) {
450-
return getKeyStore().load(id)
448+
@Throws(NoSuchKeyException::class)
449+
override fun hardwareAttestationKeyLoad(id: String) {
450+
return getKeyStore().load(id)
451+
}
452+
453+
override fun bindSocketToNetwork(fd: Int): Boolean {
454+
val net = NetworkChangeCallback.cachedDefaultNetwork ?: run {
455+
TSLog.d(TAG, "bindSocketToActiveNetwork: no cached default network; noop")
456+
return false
457+
}
458+
459+
val iface = NetworkChangeCallback.cachedDefaultInterfaceName
460+
?: NetworkChangeCallback.cachedDefaultNetworkInfo?.linkProps?.interfaceName
461+
462+
TSLog.d(TAG, "bindSocketToActiveNetwork: binding fd=$fd to net=$net iface=$iface")
463+
464+
return try {
465+
android.os.ParcelFileDescriptor.fromFd(fd).use { pfd ->
466+
net.bindSocket(pfd.fileDescriptor)
467+
}
468+
true
469+
} catch (e: Exception) {
470+
TSLog.w(TAG, "bindSocketToActiveNetwork: bind failed fd=$fd net=$net iface=$iface: $e")
471+
false
472+
}
451473
}
452474
}
475+
453476
/**
454477
* UninitializedApp contains all of the methods of App that can be used without having to initialize
455478
* 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: 66 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,30 @@ object NetworkChangeCallback {
1717

1818
private const val TAG = "NetworkChangeCallback"
1919

20-
private data class NetworkInfo(var caps: NetworkCapabilities, var linkProps: LinkProperties)
20+
private data class NetworkInfo(
21+
var caps: NetworkCapabilities,
22+
var linkProps: LinkProperties
23+
)
2124

2225
private val lock = ReentrantLock()
2326

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

2645
// monitorDnsChanges sets up a network callback to monitor changes to the
2746
// system's network state and update the DNS configuration when interfaces
@@ -45,34 +64,45 @@ object NetworkChangeCallback {
4564
connectivityManager.registerNetworkCallback(
4665
networkConnectivityRequest,
4766
object : ConnectivityManager.NetworkCallback() {
67+
4868
override fun onAvailable(network: Network) {
4969
super.onAvailable(network)
5070

51-
TSLog.d(TAG, "onAvailable: network ${network}")
71+
TSLog.d(TAG, "onAvailable: network $network")
72+
5273
lock.withLock {
5374
activeNetworks[network] = NetworkInfo(NetworkCapabilities(), LinkProperties())
75+
recomputeDefaultNetworkLocked("onAvailable")
5476
}
5577
}
5678

5779
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
5880
super.onCapabilitiesChanged(network, capabilities)
59-
lock.withLock { activeNetworks[network]?.caps = capabilities }
81+
82+
lock.withLock {
83+
activeNetworks[network]?.caps = capabilities
84+
recomputeDefaultNetworkLocked("onCapabilitiesChanged")
85+
}
6086
}
6187

6288
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
6389
super.onLinkPropertiesChanged(network, linkProperties)
90+
6491
lock.withLock {
6592
activeNetworks[network]?.linkProps = linkProperties
93+
recomputeDefaultNetworkLocked("onLinkPropertiesChanged")
6694
maybeUpdateDNSConfig("onLinkPropertiesChanged", dns)
6795
}
6896
}
6997

7098
override fun onLost(network: Network) {
7199
super.onLost(network)
72100

73-
TSLog.d(TAG, "onLost: network ${network}")
101+
TSLog.d(TAG, "onLost: network $network")
102+
74103
lock.withLock {
75104
activeNetworks.remove(network)
105+
recomputeDefaultNetworkLocked("onLost")
76106
maybeUpdateDNSConfig("onLost", dns)
77107
}
78108
}
@@ -101,14 +131,14 @@ object NetworkChangeCallback {
101131
activeNetworks.filter { (_, info) ->
102132
info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
103133
info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
104-
info.linkProps.dnsServers.isNotEmpty() == true
134+
info.linkProps.dnsServers.isNotEmpty()
105135
}
106136

107137
// If we have one; just return it; otherwise, prefer networks that are also
108138
// not metered (i.e. cell modems).
109-
val nonMeteredNetwork = pickNonMetered(networks)
110-
if (nonMeteredNetwork != null) {
111-
return nonMeteredNetwork
139+
val nonMetered = pickNonMetered(networks)
140+
if (nonMetered != null) {
141+
return nonMetered
112142
}
113143

114144
// Okay, less good; just return the first network that has the INTERNET and
@@ -121,47 +151,63 @@ object NetworkChangeCallback {
121151
info.caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)) {
122152
Log.w(
123153
TAG,
124-
"no networks available that also have DNS servers set; falling back to first network ${network}")
154+
"no networks with DNS; falling back to first network $network")
125155
return network
126156
}
127157
}
128158

129159
// Otherwise, return nothing; we don't want to return a VPN network since
130160
// it could result in a routing loop, and a non-INTERNET network isn't
131161
// helpful.
132-
Log.w(TAG, "no networks available to pick a default network")
162+
Log.w(TAG, "no networks available to pick default network")
133163
return null
134164
}
135165

136-
// maybeUpdateDNSConfig will maybe update our DNS configuration based on the
137-
// current set of active Networks.
166+
// Update cached default network + log interface name.
167+
private fun recomputeDefaultNetworkLocked(why: String) {
168+
val newNetwork = pickDefaultNetwork()
169+
cachedDefaultNetwork = newNetwork
170+
171+
val info = if (newNetwork != null) activeNetworks[newNetwork] else null
172+
cachedDefaultNetworkInfo = info
173+
cachedDefaultInterfaceName = info?.linkProps?.interfaceName
174+
175+
TSLog.d(
176+
TAG,
177+
"$why: cachedDefaultNetwork=$newNetwork iface=${cachedDefaultInterfaceName ?: "none"}")
178+
}
179+
180+
/**
181+
* Update DNS config when underlying network changes.
182+
*/
138183
private fun maybeUpdateDNSConfig(why: String, dns: DnsConfig) {
139-
val defaultNetwork = pickDefaultNetwork()
184+
val defaultNetwork = cachedDefaultNetwork
140185
if (defaultNetwork == null) {
141-
TSLog.d(TAG, "${why}: no default network available; not updating DNS config")
186+
TSLog.d(TAG, "$why: no default network available; not updating DNS")
142187
return
143188
}
144-
val info = activeNetworks[defaultNetwork]
189+
190+
val info = cachedDefaultNetworkInfo
145191
if (info == null) {
146-
Log.w(
147-
TAG,
148-
"${why}: [unexpected] no info available for default network; not updating DNS config")
192+
Log.w(TAG, "$why: no info for default network; not updating DNS")
149193
return
150194
}
151195

152196
val sb = StringBuilder()
153197
for (ip in info.linkProps.dnsServers) {
154198
sb.append(ip.hostAddress).append(" ")
155199
}
200+
156201
val searchDomains: String? = info.linkProps.domains
157202
if (searchDomains != null) {
158203
sb.append("\n")
159204
sb.append(searchDomains)
160205
}
206+
161207
if (dns.updateDNSFromNetwork(sb.toString())) {
162208
TSLog.d(
163209
TAG,
164-
"${why}: updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName})")
210+
"$why: updated DNS config for iface=${info.linkProps.interfaceName}")
165211
Libtailscale.onDNSConfigChanged(info.linkProps.interfaceName)
166212
}
167213
}

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)