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 <kari@tailscale.com>
pull/495/head
kari-ts 3 months ago committed by GitHub
parent 9f87446ab6
commit 283e1ebcd8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -2,48 +2,73 @@
// SPDX-License-Identifier: BSD-3-Clause // SPDX-License-Identifier: BSD-3-Clause
package com.tailscale.ipn package com.tailscale.ipn
import android.content.Context
import android.net.ConnectivityManager import android.net.ConnectivityManager
import android.net.LinkProperties import android.net.LinkProperties
import android.net.Network import android.net.Network
import android.net.NetworkCapabilities import android.net.NetworkCapabilities
import android.net.NetworkRequest
import android.util.Log import android.util.Log
import libtailscale.Libtailscale import libtailscale.Libtailscale
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface
object NetworkChangeCallback { object NetworkChangeCallback {
// requestNetwork attempts to find the best network that matches the passed NetworkRequest. It is private const val TAG = "NetworkChangeCallback"
// possible that this might return an unusuable network, eg a captive portal.
// Cache LinkProperties and NetworkCapabilities since synchronous ConnectivityManager calls are
// prone to races.
// Since there is no guarantee for which update might come first, maybe update DNS configs on
// both.
val networkCapabilitiesCache = mutableMapOf<Network, NetworkCapabilities>()
val linkPropertiesCache = mutableMapOf<Network, LinkProperties>()
// requestDefaultNetworkCallback receives notifications about the default network. Listen for
// changes to the capabilities, which are guaranteed to come after a network becomes available per
// https://developer.android.com/reference/android/net/ConnectivityManager.NetworkCallback#onAvailable(android.net.Network),
// in order to filter on non-VPN networks.
fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) { fun monitorDnsChanges(connectivityManager: ConnectivityManager, dns: DnsConfig) {
val networkConnectivityRequest =
NetworkRequest.Builder() connectivityManager.registerDefaultNetworkCallback(
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
.build()
connectivityManager.registerNetworkCallback(
networkConnectivityRequest,
object : ConnectivityManager.NetworkCallback() { object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onCapabilitiesChanged(network: Network, capabilities: NetworkCapabilities) {
super.onAvailable(network) super.onCapabilitiesChanged(network, capabilities)
networkCapabilitiesCache[network] = capabilities
val sb = StringBuilder() val linkProperties = linkPropertiesCache[network]
val linkProperties: LinkProperties? = connectivityManager.getLinkProperties(network) if (linkProperties != null &&
val dnsList: MutableList<InetAddress> = linkProperties?.dnsServers ?: mutableListOf() capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
for (ip in dnsList) { capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
sb.append(ip.hostAddress).append(" ") maybeUpdateDNSConfig(linkProperties, dns)
} } else {
val searchDomains: String? = linkProperties?.domains if (!capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) ||
if (searchDomains != null) { !capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
sb.append("\n") Log.d(
sb.append(searchDomains) TAG,
"Capabilities changed for network $network.toString(), but not updating DNS config because either this is a VPN network or non-internet network")
} else {
Log.d(
TAG,
"Capabilities changed for network $network.toString(), but not updating DNS config, because the LinkProperties hasn't been gotten yet")
}
} }
}
if (dns.updateDNSFromNetwork(sb.toString())) { override fun onLinkPropertiesChanged(network: Network, linkProperties: LinkProperties) {
Libtailscale.onDNSConfigChanged(linkProperties?.interfaceName) super.onLinkPropertiesChanged(network, linkProperties)
linkPropertiesCache[network] = linkProperties
val capabilities = networkCapabilitiesCache[network]
if (capabilities != null &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) &&
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) {
maybeUpdateDNSConfig(linkProperties, dns)
} else {
if (capabilities == null) {
Log.d(
TAG,
"Capabilities changed for network $network.toString(), but not updating DNS config because capabilities haven't been gotten for this network yet")
} else {
Log.d(
TAG,
"Capabilities changed for network $network.toString(), but not updating DNS config, because this is a VPN network or non-Internet network")
}
} }
} }
@ -55,4 +80,20 @@ object NetworkChangeCallback {
} }
}) })
} }
fun maybeUpdateDNSConfig(linkProperties: LinkProperties, dns: DnsConfig) {
val sb = StringBuilder()
val dnsList: MutableList<InetAddress> = linkProperties.dnsServers ?: mutableListOf()
for (ip in dnsList) {
sb.append(ip.hostAddress).append(" ")
}
val searchDomains: String? = linkProperties.domains
if (searchDomains != null) {
sb.append("\n")
sb.append(searchDomains)
}
if (dns.updateDNSFromNetwork(sb.toString())) {
Libtailscale.onDNSConfigChanged(linkProperties.interfaceName)
}
}
} }

Loading…
Cancel
Save