Skip to content

Commit 283e1eb

Browse files
authored
android: fix network callback race (#493)
ConnectivityManager doesn't make guarantees about the order of network updates. Only use network updates for currently active network. Also, use registerDefaultNetworkCallback so that we are only listening for default networks. Updates tailscale/tailscale#13173 Signed-off-by: kari-ts <[email protected]>
1 parent 9f87446 commit 283e1eb

File tree

1 file changed

+69
-28
lines changed

1 file changed

+69
-28
lines changed

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

Lines changed: 69 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2,48 +2,73 @@
22
// SPDX-License-Identifier: BSD-3-Clause
33
package com.tailscale.ipn
44

5-
import android.content.Context
65
import android.net.ConnectivityManager
76
import android.net.LinkProperties
87
import android.net.Network
98
import android.net.NetworkCapabilities
10-
import android.net.NetworkRequest
119
import android.util.Log
1210
import libtailscale.Libtailscale
1311
import java.net.InetAddress
14-
import java.net.NetworkInterface
1512

1613
object NetworkChangeCallback {
1714

18-
// requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is
19-
// possible that this might return an unusuable network, eg a captive portal.
15+
private const val TAG = "NetworkChangeCallback"
16+
17+
// Cache LinkProperties and NetworkCapabilities since synchronous ConnectivityManager calls are
18+
// prone to races.
19+
// Since there is no guarantee for which update might come first, maybe update DNS configs on
20+
// both.
21+
val networkCapabilitiesCache = mutableMapOf<Network, NetworkCapabilities>()
22+
val linkPropertiesCache = mutableMapOf<Network, LinkProperties>()
23+
24+
// requestDefaultNetworkCallback receives notifications about the default network. Listen for
25+
// changes to the capabilities, which are guaranteed to come after a network becomes available per
26+
// https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network),
27+
// in order to filter on non-VPN networks.
2028
fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) {
21-
val networkConnectivityRequest =
22-
NetworkRequest.Builder()
23-
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
24-
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
25-
.build()
26-
27-
connectivityManager.registerNetworkCallback(
28-
networkConnectivityRequest,
29+
30+
connectivityManager.registerDefaultNetworkCallback(
2931
object : ConnectivityManager.NetworkCallback() {
30-
override fun onAvailable(network: Network) {
31-
super.onAvailable(network)
32-
33-
val sb = StringBuilder()
34-
val linkProperties: LinkProperties? = connectivityManager.getLinkProperties(network)
35-
val dnsList: MutableList<InetAddress> = linkProperties?.dnsServers ?: mutableListOf()
36-
for (ip in dnsList) {
37-
sb.append(ip.hostAddress).append(" ")
38-
}
39-
val searchDomains: String? = linkProperties?.domains
40-
if (searchDomains != null) {
41-
sb.append("\n")
42-
sb.append(searchDomains)
32+
override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
33+
super.onCapabilitiesChanged(network, capabilities)
34+
networkCapabilitiesCache[network] = capabilities
35+
val linkProperties = linkPropertiesCache[network]
36+
if (linkProperties != null &&
37+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
38+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
39+
maybeUpdateDNSConfig(linkProperties, dns)
40+
} else {
41+
if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) ||
42+
!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
43+
Log.d(
44+
TAG,
45+
"Capabilities changed for network $network.toString(), but not updating DNS config because either this is a VPN network or non-internet network")
46+
} else {
47+
Log.d(
48+
TAG,
49+
"Capabilities changed for network $network.toString(), but not updating DNS config, because the LinkProperties hasn't been gotten yet")
50+
}
4351
}
52+
}
4453

45-
if (dns.updateDNSFromNetwork(sb.toString())) {
46-
Libtailscale.onDNSConfigChanged(linkProperties?.interfaceName)
54+
override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
55+
super.onLinkPropertiesChanged(network, linkProperties)
56+
linkPropertiesCache[network] = linkProperties
57+
val capabilities = networkCapabilitiesCache[network]
58+
if (capabilities != null &&
59+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
60+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
61+
maybeUpdateDNSConfig(linkProperties, dns)
62+
} else {
63+
if (capabilities == null) {
64+
Log.d(
65+
TAG,
66+
"Capabilities changed for network $network.toString(), but not updating DNS config because capabilities haven't been gotten for this network yet")
67+
} else {
68+
Log.d(
69+
TAG,
70+
"Capabilities changed for network $network.toString(), but not updating DNS config, because this is a VPN network or non-Internet network")
71+
}
4772
}
4873
}
4974

@@ -55,4 +80,20 @@ object NetworkChangeCallback {
5580
}
5681
})
5782
}
83+
84+
fun maybeUpdateDNSConfig(linkProperties: LinkProperties, dns: DnsConfig) {
85+
val sb = StringBuilder()
86+
val dnsList: MutableList<InetAddress> = linkProperties.dnsServers ?: mutableListOf()
87+
for (ip in dnsList) {
88+
sb.append(ip.hostAddress).append(" ")
89+
}
90+
val searchDomains: String? = linkProperties.domains
91+
if (searchDomains != null) {
92+
sb.append("\n")
93+
sb.append(searchDomains)
94+
}
95+
if (dns.updateDNSFromNetwork(sb.toString())) {
96+
Libtailscale.onDNSConfigChanged(linkProperties.interfaceName)
97+
}
98+
}
5899
}

0 commit comments

Comments
 (0)