@@ -6,94 +6,162 @@ import android.net.ConnectivityManager
6
6
import android.net.LinkProperties
7
7
import android.net.Network
8
8
import android.net.NetworkCapabilities
9
+ import android.net.NetworkRequest
9
10
import android.util.Log
10
11
import libtailscale.Libtailscale
11
12
import java.net.InetAddress
13
+ import java.util.concurrent.locks.ReentrantLock
14
+ import kotlin.concurrent.withLock
12
15
13
16
object NetworkChangeCallback {
14
17
15
18
private const val TAG = " NetworkChangeCallback"
16
19
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 .
20
+ private data class NetworkInfo (
21
+ var caps : NetworkCapabilities ,
22
+ var linkProps : LinkProperties
23
+ )
24
+
25
+ private val lock = ReentrantLock ()
26
+ private val activeNetworks = mutableMapOf< Network , NetworkInfo >() // keyed by Network
27
+
28
+ // monitorDnsChanges sets up a network callback to monitor changes to the
29
+ // system's network state and update the DNS configuration when interfaces
30
+ // become available or properties of those interfaces change .
28
31
fun monitorDnsChanges (connectivityManager : ConnectivityManager , dns : DnsConfig ) {
32
+ val networkConnectivityRequest =
33
+ NetworkRequest .Builder ()
34
+ .addCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET )
35
+ .addCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN )
36
+ .build()
29
37
30
- connectivityManager.registerDefaultNetworkCallback(
38
+ // Use registerNetworkCallback to listen for updates from all networks, and
39
+ // then update DNS configs for the best network.
40
+ //
41
+ // Note that we can't use registerDefaultNetworkCallback because the
42
+ // default network used by Tailscale will always show up with capability
43
+ // NOT_VPN=false, and we must filter out NOT_VPN networks to avoid routing
44
+ // loops.
45
+ connectivityManager.registerNetworkCallback(
46
+ networkConnectivityRequest,
31
47
object : ConnectivityManager .NetworkCallback () {
48
+ override fun onAvailable (network : Network ) {
49
+ super .onAvailable(network)
50
+
51
+ Log .d(TAG , " onAvailable: network ${network} " )
52
+ lock.withLock {
53
+ activeNetworks[network] = NetworkInfo (NetworkCapabilities (), LinkProperties ())
54
+ maybeUpdateDNSConfigLocked(" onAvailable" , dns)
55
+ }
56
+ }
57
+
32
58
override fun onCapabilitiesChanged (network : Network , capabilities : NetworkCapabilities ) {
33
59
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
- }
60
+ lock.withLock {
61
+ activeNetworks[network]?.caps = capabilities
62
+ maybeUpdateDNSConfigLocked(" onCapabilitiesChanged" , dns)
51
63
}
52
64
}
53
65
54
66
override fun onLinkPropertiesChanged (network : Network , linkProperties : LinkProperties ) {
55
67
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
- }
68
+ lock.withLock {
69
+ activeNetworks[network]?.linkProps = linkProperties
70
+ maybeUpdateDNSConfigLocked(" onLinkPropertiesChanged" , dns)
72
71
}
73
72
}
74
73
75
74
override fun onLost (network : Network ) {
76
75
super .onLost(network)
77
- if (dns.updateDNSFromNetwork(" " )) {
78
- Libtailscale .onDNSConfigChanged(" " )
76
+
77
+ Log .d(TAG , " onLost: network ${network} " )
78
+ lock.withLock {
79
+ activeNetworks.remove(network)
80
+ maybeUpdateDNSConfigLocked(" onLost" , dns)
79
81
}
80
82
}
81
83
})
82
84
}
83
85
84
- fun maybeUpdateDNSConfig (linkProperties : LinkProperties , dns : DnsConfig ) {
86
+ // pickNonMetered returns the first non-metered network in the list of
87
+ // networks, or the first network if none are non-metered.
88
+ private fun pickNonMetered (networks : Map <Network , NetworkInfo >): Network ? {
89
+ for ((network, info) in networks) {
90
+ if (info.caps.hasCapability(NetworkCapabilities .NET_CAPABILITY_NOT_METERED )) {
91
+ return network
92
+ }
93
+ }
94
+ return networks.keys.firstOrNull()
95
+ }
96
+
97
+ // pickDefaultNetwork returns a non-VPN network to use as the 'default'
98
+ // network; one that is used as a gateway to the internet and from which we
99
+ // obtain our DNS servers.
100
+ private fun pickDefaultNetwork (): Network ? {
101
+ // Filter the list of all networks to those that have the INTERNET
102
+ // capability, are not VPNs, and have a non-zero number of DNS servers
103
+ // available.
104
+ val networks = activeNetworks.filter { (_, info) ->
105
+ info.caps.hasCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET ) &&
106
+ info.caps.hasCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN ) &&
107
+ info.linkProps.dnsServers.isNotEmpty() == true
108
+ }
109
+
110
+ // If we have one; just return it; otherwise, prefer networks that are also
111
+ // not metered (i.e. cell modems).
112
+ val nonMeteredNetwork = pickNonMetered(networks)
113
+ if (nonMeteredNetwork != null ) {
114
+ return nonMeteredNetwork
115
+ }
116
+
117
+ // Okay, less good; just return the first network that has the INTERNET and
118
+ // NOT_VPN capabilities; even though this interface doesn't have any DNS
119
+ // servers set, we'll use our DNS fallback servers to make queries. It's
120
+ // strictly better to return an interface + use the DNS fallback servers
121
+ // than to return nothing and not be able to route traffic.
122
+ for ((network, info) in activeNetworks) {
123
+ if (info.caps.hasCapability(NetworkCapabilities .NET_CAPABILITY_INTERNET ) &&
124
+ info.caps.hasCapability(NetworkCapabilities .NET_CAPABILITY_NOT_VPN )) {
125
+ Log .w(TAG , " no networks available that also have DNS servers set; falling back to first network ${network} " )
126
+ return network
127
+ }
128
+ }
129
+
130
+ // Otherwise, return nothing; we don't want to return a VPN network since
131
+ // it could result in a routing loop, and a non-INTERNET network isn't
132
+ // helpful.
133
+ Log .w(TAG , " no networks available to pick a default network" )
134
+ return null
135
+ }
136
+
137
+ // maybeUpdateDNSConfig will maybe update our DNS configuration based on the
138
+ // current set of active Networks.
139
+ //
140
+ // 'lock' must be held.
141
+ private fun maybeUpdateDNSConfigLocked (why : String , dns : DnsConfig ) {
142
+ val defaultNetwork = pickDefaultNetwork()
143
+ if (defaultNetwork == null ) {
144
+ Log .d(TAG , " ${why} : no default network available; not updating DNS config" )
145
+ return
146
+ }
147
+ val info = activeNetworks[defaultNetwork]
148
+ if (info == null ) {
149
+ Log .w(TAG , " ${why} : [unexpected] no info available for default network; not updating DNS config" )
150
+ return
151
+ }
152
+
85
153
val sb = StringBuilder ()
86
- val dnsList: MutableList <InetAddress > = linkProperties.dnsServers ? : mutableListOf ()
87
- for (ip in dnsList) {
154
+ for (ip in info.linkProps.dnsServers) {
88
155
sb.append(ip.hostAddress).append(" " )
89
156
}
90
- val searchDomains: String? = linkProperties .domains
157
+ val searchDomains: String? = info.linkProps .domains
91
158
if (searchDomains != null ) {
92
159
sb.append(" \n " )
93
160
sb.append(searchDomains)
94
161
}
95
162
if (dns.updateDNSFromNetwork(sb.toString())) {
96
- Libtailscale .onDNSConfigChanged(linkProperties.interfaceName)
163
+ Log .d(TAG , " ${why} : updated DNS config for network ${defaultNetwork} (${info.linkProps.interfaceName} )" )
164
+ Libtailscale .onDNSConfigChanged(info.linkProps.interfaceName)
97
165
}
98
166
}
99
167
}
0 commit comments