mirror of
https://github.com/wgtunnel/android.git
synced 2026-07-03 14:07:49 +02:00
fix: make network monitor less strict for network capabilities
Network monitor was too strict with capability checks, impacting our DNS resolver which needs to bind to underlying network. Capabilities have been separated out into a separate state property so we always pass the active network to connectivity state for system dns Improve system dns by supporting DnsResolver on modern devices #1270
This commit is contained in:
+48
-37
@@ -162,6 +162,7 @@ class AndroidNetworkMonitor(
|
||||
if (intent.action == Intent.ACTION_AIRPLANE_MODE_CHANGED) {
|
||||
val isOn = intent.getBooleanExtra("state", false)
|
||||
Timber.d("Airplane mode changed: $isOn")
|
||||
if (isOn) activeCellularNetworks.value = emptyMap()
|
||||
airplaneModeState.update { isOn }
|
||||
}
|
||||
}
|
||||
@@ -530,7 +531,7 @@ class AndroidNetworkMonitor(
|
||||
.also { Timber.d("Current SSID via ${method.name}: $it") }
|
||||
}
|
||||
|
||||
private fun hasGoodNetworkCaps(caps: NetworkCapabilities?): Boolean {
|
||||
private fun hasValidatedInternet(caps: NetworkCapabilities?): Boolean {
|
||||
if (caps == null) return false
|
||||
return caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) &&
|
||||
@@ -538,19 +539,6 @@ class AndroidNetworkMonitor(
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED))
|
||||
}
|
||||
|
||||
private fun hasGoodCellularNetwork(): Boolean =
|
||||
activeCellularNetworks.value.values.any { caps ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) && hasGoodNetworkCaps(caps)
|
||||
}
|
||||
|
||||
private fun getGoodCellularNetwork(): Network? =
|
||||
activeCellularNetworks.value.entries
|
||||
.firstOrNull { (_, caps) ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) &&
|
||||
hasGoodNetworkCaps(caps)
|
||||
}
|
||||
?.key
|
||||
|
||||
// default network events don't contain detailed capability information of underlying networks,
|
||||
// so we need to track separately
|
||||
private data class NetworkData(
|
||||
@@ -570,6 +558,25 @@ class AndroidNetworkMonitor(
|
||||
NetworkData(defaultEvent, wifiEvent, cellularEvent, ethernetEvent)
|
||||
}
|
||||
|
||||
// For multi-sim selection, prefers foreground, then validated internet, then not suspended
|
||||
private fun pickBestCellularNetwork(): Network? {
|
||||
if (activeCellularNetworks.value.isEmpty()) return null
|
||||
|
||||
return activeCellularNetworks.value.entries
|
||||
.maxByOrNull { (_, caps) ->
|
||||
var score = 0
|
||||
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_FOREGROUND)) score += 100
|
||||
if (hasValidatedInternet(caps)) score += 50
|
||||
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_SUSPENDED))
|
||||
score += 20
|
||||
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_CONGESTED))
|
||||
score += 10
|
||||
if (caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)) score += 5
|
||||
score
|
||||
}
|
||||
?.key
|
||||
}
|
||||
|
||||
@OptIn(FlowPreview::class)
|
||||
override val connectivityStateFlow: SharedFlow<ConnectivityState> =
|
||||
combine(
|
||||
@@ -605,6 +612,7 @@ class AndroidNetworkMonitor(
|
||||
activeNetwork = ActiveNetwork.Disconnected(),
|
||||
locationPermissionsGranted = permissions.locationPermissionGranted,
|
||||
locationServicesEnabled = permissions.locationServicesEnabled,
|
||||
airplaneModeOn = isAirplaneOn,
|
||||
vpnState = VpnState.Inactive,
|
||||
)
|
||||
}
|
||||
@@ -626,23 +634,15 @@ class AndroidNetworkMonitor(
|
||||
val physicalNetwork: ActiveNetwork =
|
||||
when {
|
||||
networkData.ethernetEvent is TransportEvent.CapabilitiesChanged &&
|
||||
networkData.ethernetEvent.networkCapabilities?.let { caps ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) &&
|
||||
caps.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
} == true -> {
|
||||
networkData.ethernetEvent.networkCapabilities?.hasTransport(
|
||||
NetworkCapabilities.TRANSPORT_ETHERNET
|
||||
) == true -> {
|
||||
ActiveNetwork.Ethernet(networkData.ethernetEvent.network)
|
||||
}
|
||||
|
||||
networkData.wifiNetworkEvent is TransportEvent.CapabilitiesChanged &&
|
||||
networkData.wifiNetworkEvent.networkCapabilities?.let { caps ->
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) &&
|
||||
caps.hasCapability(
|
||||
NetworkCapabilities.NET_CAPABILITY_INTERNET
|
||||
) &&
|
||||
caps.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED)
|
||||
caps.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
|
||||
} == true -> {
|
||||
|
||||
val wifiEvent = networkData.wifiNetworkEvent
|
||||
@@ -689,18 +689,12 @@ class AndroidNetworkMonitor(
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
val defaultIsGoodCellular =
|
||||
!isAirplaneOn &&
|
||||
defaultCaps?.hasTransport(
|
||||
NetworkCapabilities.TRANSPORT_CELLULAR
|
||||
) == true &&
|
||||
hasGoodNetworkCaps(defaultCaps)
|
||||
val cellularNetwork =
|
||||
pickBestCellularNetwork()
|
||||
?: activeCellularNetworks.value.keys.firstOrNull()
|
||||
|
||||
val cellularNet =
|
||||
getGoodCellularNetwork()
|
||||
?: if (defaultIsGoodCellular) defaultNetwork else null
|
||||
if (cellularNet != null) {
|
||||
ActiveNetwork.Cellular(cellularNet)
|
||||
if (cellularNetwork != null) {
|
||||
ActiveNetwork.Cellular(cellularNetwork)
|
||||
} else {
|
||||
ActiveNetwork.Disconnected()
|
||||
}
|
||||
@@ -730,14 +724,31 @@ class AndroidNetworkMonitor(
|
||||
privateDnsHostname = privateDnsSettings.hostname,
|
||||
)
|
||||
|
||||
val physicalCaps: NetworkCapabilities? =
|
||||
when (physicalNetwork) {
|
||||
is ActiveNetwork.Wifi ->
|
||||
(networkData.wifiNetworkEvent as? TransportEvent.CapabilitiesChanged)
|
||||
?.networkCapabilities
|
||||
is ActiveNetwork.Cellular ->
|
||||
activeCellularNetworks.value[physicalNetwork.network]
|
||||
is ActiveNetwork.Ethernet ->
|
||||
(networkData.ethernetEvent as? TransportEvent.CapabilitiesChanged)
|
||||
?.networkCapabilities
|
||||
else -> null
|
||||
}
|
||||
|
||||
val hasValidatedInternet = hasValidatedInternet(physicalCaps)
|
||||
|
||||
ConnectivityState(
|
||||
activeNetwork = physicalNetwork,
|
||||
locationPermissionsGranted = permissions.locationPermissionGranted,
|
||||
locationServicesEnabled = permissions.locationServicesEnabled,
|
||||
vpnState = vpnState,
|
||||
airplaneModeOn = isAirplaneOn,
|
||||
effectiveDnsInfo = effectiveDns,
|
||||
underlyingDnsInfo = underlyingDns,
|
||||
hasIpv6 = hasIpv6Support(underlyingNetwork, physicalNetwork),
|
||||
hasValidatedInternet = hasValidatedInternet,
|
||||
)
|
||||
}
|
||||
.distinctUntilChanged()
|
||||
|
||||
@@ -11,8 +11,11 @@ data class ConnectivityState(
|
||||
val effectiveDnsInfo: DnsInfo = DnsInfo(),
|
||||
val underlyingDnsInfo: DnsInfo = DnsInfo(),
|
||||
val hasIpv6: Boolean = false,
|
||||
val airplaneModeOn: Boolean = false,
|
||||
val hasValidatedInternet: Boolean = false,
|
||||
) {
|
||||
fun hasInternet(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||
|
||||
fun hasActiveNetwork(): Boolean = activeNetwork !is ActiveNetwork.Disconnected
|
||||
|
||||
override fun toString(): String {
|
||||
val networkInfo =
|
||||
|
||||
@@ -440,7 +440,7 @@ class TunnelBackend(
|
||||
|
||||
while (isActive) {
|
||||
val stable = stableNetworkEngine.stableState.value
|
||||
if (stable?.state?.hasInternet() == true) {
|
||||
if (stable?.state?.hasActiveNetwork() == true) {
|
||||
val tunnel = _status.value.activeTunnels[tunnelId] ?: continue
|
||||
tunnel.mode?.let { mode ->
|
||||
reconcilePeers(tunnelId, handle, mode, PeerUpdateReason.DDNS_CHECK)
|
||||
|
||||
@@ -1,29 +1,86 @@
|
||||
package com.zaneschepke.tunnel.backend.dns
|
||||
|
||||
import android.content.Context
|
||||
import android.net.DnsResolver
|
||||
import android.net.Network
|
||||
import android.os.Build
|
||||
import android.os.CancellationSignal
|
||||
import androidx.annotation.RequiresApi
|
||||
import com.zaneschepke.tunnel.model.DnsBootstrapResult
|
||||
import java.net.UnknownHostException
|
||||
import java.net.InetAddress
|
||||
import java.util.concurrent.Executor
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import org.koin.core.component.KoinComponent
|
||||
import org.koin.core.component.inject
|
||||
import timber.log.Timber
|
||||
|
||||
internal class AndroidNetworkResolver(private val network: Network) : PeerResolver {
|
||||
internal class AndroidNetworkResolver(private val network: Network) : PeerResolver, KoinComponent {
|
||||
private val context: Context by inject()
|
||||
|
||||
@Suppress("NewApi")
|
||||
private val dnsResolver: DnsResolver by lazy {
|
||||
if (Build.VERSION.SDK_INT >= 37) {
|
||||
DnsResolver(context, null)
|
||||
} else {
|
||||
@Suppress("DEPRECATION") DnsResolver.getInstance()
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resolve(host: String): DnsBootstrapResult =
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
// use underlying network for resolution
|
||||
val ips = network.getAllByName(host)
|
||||
val ips =
|
||||
withTimeoutOrNull(2_200L.milliseconds) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
resolveAsync(host)
|
||||
} else {
|
||||
network.getAllByName(host).toList()
|
||||
}
|
||||
}
|
||||
?: run {
|
||||
Timber.w("DNS resolution timed out after 2200ms for $host")
|
||||
return@withContext DnsBootstrapResult()
|
||||
}
|
||||
|
||||
Timber.d("Resolution from network bind socket: ${ips.contentToString()}")
|
||||
Timber.d("Resolution from network bind socket: $ips")
|
||||
|
||||
val v4 = ips.filter { it.address.size == 4 }.map { it.hostAddress }
|
||||
val v6 = ips.filter { it.address.size == 16 }.map { it.hostAddress }
|
||||
|
||||
DnsBootstrapResult(v4, v6)
|
||||
} catch (e: UnknownHostException) {
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e, "System DNS failed to resolve host")
|
||||
DnsBootstrapResult()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.Q)
|
||||
private suspend fun resolveAsync(host: String): List<InetAddress> =
|
||||
suspendCancellableCoroutine { continuation ->
|
||||
val signal = CancellationSignal()
|
||||
continuation.invokeOnCancellation { signal.cancel() }
|
||||
|
||||
dnsResolver.query(
|
||||
network,
|
||||
host,
|
||||
DnsResolver.FLAG_EMPTY,
|
||||
Executor { it.run() },
|
||||
signal,
|
||||
object : DnsResolver.Callback<List<InetAddress>> {
|
||||
override fun onAnswer(answer: List<InetAddress>, rcode: Int) {
|
||||
continuation.resume(answer)
|
||||
}
|
||||
|
||||
override fun onError(error: DnsResolver.DnsException) {
|
||||
continuation.resumeWithException(error)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user