Merge pull request #187 from Mygod/stacked-links

Support stacked links
This commit is contained in:
Mygod
2020-09-11 15:53:44 -04:00
committed by GitHub
8 changed files with 153 additions and 228 deletions

View File

@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.net
import android.annotation.TargetApi import android.annotation.TargetApi
import android.net.LinkProperties import android.net.LinkProperties
import android.net.RouteInfo
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
@@ -13,9 +14,7 @@ import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.root.RootManager import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.RoutingCommands import be.mygod.vpnhotspot.root.RoutingCommands
import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.util.if_nametoindex
import be.mygod.vpnhotspot.util.parseNumericAddress
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import timber.log.Timber import timber.log.Timber
@@ -180,52 +179,64 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
} }
} }
var subrouting: Subrouting? = null var subrouting = mutableMapOf<String, Subrouting>()
var dns: List<InetAddress> = emptyList() var dns = emptyList<Pair<InetAddress, String?>>()
override fun onAvailable(ifname: String, properties: LinkProperties) = synchronized(this@Routing) { override fun onAvailable(properties: LinkProperties?) = synchronized(this@Routing) {
if (stopped) return if (stopped) return
val subrouting = subrouting val toRemove = subrouting.keys.toMutableSet()
when { for (ifname in properties?.allInterfaceNames ?: emptyList()) {
subrouting != null -> check(subrouting.upstream == ifname) { "${subrouting.upstream} != $ifname" } if (toRemove.remove(ifname) || !upstreams.add(ifname)) continue
!upstreams.add(ifname) -> return try {
else -> this.subrouting = try { subrouting[ifname] = Subrouting(priority, ifname)
Subrouting(priority, ifname)
} catch (e: Exception) { } catch (e: Exception) {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
if (e !is CancellationException) Timber.w(e) if (e !is CancellationException) Timber.w(e)
null
} }
} }
dns = properties.dnsServers for (ifname in toRemove) {
updateDnsRoute() subrouting.remove(ifname)?.transaction?.revert()
} check(upstreams.remove(ifname))
}
override fun onLost() = synchronized(this@Routing) { val routes = properties?.allRoutes
if (stopped) return dns = properties?.dnsServers?.map { dest ->
val subrouting = subrouting ?: return // based on:
// we could be removing fallback subrouting which no collision could ever happen, check before removing // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java;l=88;drc=master
subrouting.upstream?.let { check(upstreams.remove(it)) } // https://cs.android.com/android/platform/superproject/+/master:frameworks/libs/net/common/framework/android/net/util/NetUtils.java;l=44;drc=de5905fe0407a1f5e115423d56c948ee2400683d
subrouting.transaction.revert() val size = dest.address.size
this.subrouting = null var bestRoute: RouteInfo? = null
dns = emptyList() for (route in routes!!) {
if (route.destination.rawAddress.size == size &&
(bestRoute == null ||
bestRoute.destination.prefixLength < route.destination.prefixLength) &&
route.matches(dest)) {
bestRoute = route
}
}
dest to bestRoute?.`interface`
} ?: emptyList()
updateDnsRoute() updateDnsRoute()
} }
} }
private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) { private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) {
var fallbackInactive = true var fallbackInactive = true
override fun onAvailable(properties: LinkProperties?) {
check(fallbackInactive)
super.onAvailable(properties)
}
override fun onFallback() = synchronized(this@Routing) { override fun onFallback() = synchronized(this@Routing) {
if (stopped) return if (stopped) return
fallbackInactive = false fallbackInactive = false
check(subrouting == null) check(subrouting.isEmpty() && upstreams.add(""))
subrouting = try { try {
Subrouting(priority) subrouting[""] = Subrouting(priority)
} catch (e: Exception) { } catch (e: Exception) {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
if (e !is CancellationException) Timber.w(e) if (e !is CancellationException) Timber.w(e)
null
} }
dns = listOf(parseNumericAddress("8.8.8.8")) dns = listOf(parseNumericAddress("8.8.8.8") to null)
updateDnsRoute() updateDnsRoute()
} }
} }
@@ -334,8 +345,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
private var currentDns: DnsRoute? = null private var currentDns: DnsRoute? = null
private fun updateDnsRoute() { private fun updateDnsRoute() {
val selected = sequenceOf(upstream, fallbackUpstream).flatMap { upstream -> val selected = sequenceOf(upstream, fallbackUpstream).flatMap { upstream ->
val ifindex = upstream.subrouting?.ifindex ?: 0 upstream.dns.asSequence().map { (server, iface) ->
if (ifindex == 0) emptySequence() else upstream.dns.asSequence().map { ifindex to it } ((if (iface != null) upstream.subrouting[iface]?.ifindex else null) ?: 0) to server
}
}.firstOrNull { it.second is Inet4Address } }.firstOrNull { it.second is Inet4Address }
val ifindex = selected?.first ?: 0 val ifindex = selected?.first ?: 0
var dns = selected?.second?.hostAddress var dns = selected?.second?.hostAddress
@@ -378,8 +390,8 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh
synchronized(this) { clients.values.forEach { it.close() } } synchronized(this) { clients.values.forEach { it.close() } }
currentDns?.transaction?.revert() currentDns?.transaction?.revert()
disableSystem?.revert() disableSystem?.revert()
fallbackUpstream.subrouting?.transaction?.revert() fallbackUpstream.subrouting.values.forEach { it.transaction.revert() }
upstream.subrouting?.transaction?.revert() upstream.subrouting.values.forEach { it.transaction.revert() }
transaction.revert() transaction.revert()
} }
} }

View File

@@ -9,11 +9,9 @@ import android.os.Build
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
object DefaultNetworkMonitor : UpstreamMonitor() { object DefaultNetworkMonitor : UpstreamMonitor() {
private var registered = false private var registered = false
private var currentNetwork: Network? = null
override var currentLinkProperties: LinkProperties? = null override var currentLinkProperties: LinkProperties? = null
private set private set
/** /**
@@ -27,62 +25,30 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
private val networkCallback = object : ConnectivityManager.NetworkCallback() { private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
val properties = Services.connectivity.getLinkProperties(network) val properties = Services.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return
var switching = false
synchronized(this@DefaultNetworkMonitor) { synchronized(this@DefaultNetworkMonitor) {
val oldProperties = currentLinkProperties
if (currentNetwork != network || ifname != oldProperties?.interfaceName) {
switching = true // we are using the other default network now
currentNetwork = network
}
currentLinkProperties = properties currentLinkProperties = properties
callbacks.toList() callbacks.toList()
}.forEach { }.forEach { it.onAvailable(properties) }
if (switching) it.onLost()
it.onAvailable(ifname, properties)
}
} }
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
var losing = true
var ifname: String?
synchronized(this@DefaultNetworkMonitor) { synchronized(this@DefaultNetworkMonitor) {
if (currentNetwork == null) {
onAvailable(network)
return
}
if (currentNetwork != network) return
val oldProperties = currentLinkProperties!!
currentLinkProperties = properties currentLinkProperties = properties
ifname = properties.interfaceName
when (ifname) {
null -> Timber.w("interfaceName became null: $oldProperties -> $properties")
oldProperties.interfaceName -> losing = false
else -> Timber.w(RuntimeException("interfaceName changed: $oldProperties -> $properties"))
}
callbacks.toList() callbacks.toList()
}.forEach { }.forEach { it.onAvailable(properties) }
if (losing) {
if (ifname == null) return onLost(network)
it.onLost()
}
ifname?.let { ifname -> it.onAvailable(ifname, properties) }
}
} }
override fun onLost(network: Network) = synchronized(this@DefaultNetworkMonitor) { override fun onLost(network: Network) = synchronized(this@DefaultNetworkMonitor) {
if (currentNetwork != network) return
currentNetwork = null
currentLinkProperties = null currentLinkProperties = null
callbacks.toList() callbacks.toList()
}.forEach { it.onLost() } }.forEach { it.onAvailable() }
} }
override fun registerCallbackLocked(callback: Callback) { override fun registerCallbackLocked(callback: Callback) {
if (registered) { if (registered) {
val currentLinkProperties = currentLinkProperties val currentLinkProperties = currentLinkProperties
if (currentLinkProperties != null) GlobalScope.launch { if (currentLinkProperties != null) GlobalScope.launch {
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) { if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
@@ -103,7 +69,6 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
if (!registered) return if (!registered) return
Services.connectivity.unregisterNetworkCallback(networkCallback) Services.connectivity.unregisterNetworkCallback(networkCallback)
registered = false registered = false
currentNetwork = null
currentLinkProperties = null currentLinkProperties = null
} }
} }

View File

@@ -34,10 +34,7 @@ abstract class FallbackUpstreamMonitor private constructor() : UpstreamMonitor()
} }
val new = generateMonitor() val new = generateMonitor()
monitor = new monitor = new
for (callback in callbacks) { for (callback in callbacks) new.registerCallback(callback)
callback.onLost()
new.registerCallback(callback)
}
} }
} }
} }

View File

@@ -1,49 +1,85 @@
package be.mygod.vpnhotspot.net.monitor package be.mygod.vpnhotspot.net.monitor
import android.net.ConnectivityManager
import android.net.LinkProperties import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.allInterfaceNames import be.mygod.vpnhotspot.util.allInterfaceNames
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import timber.log.Timber
class InterfaceMonitor(val iface: String) : UpstreamMonitor() { class InterfaceMonitor(ifaceRegex: String) : UpstreamMonitor() {
private fun setPresent(present: Boolean) { private val iface = ifaceRegex.toRegex()
var available: Pair<String, LinkProperties>? = null private val request = networkRequestBuilder().apply {
synchronized(this) { removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
val old = currentIface != null removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED)
if (present == old) return removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
currentIface = if (present) iface else null }.build()
if (present) available = iface to (currentLinkProperties ?: return) private var registered = false
callbacks.toList()
}.forEach { private val available = HashMap<Network, LinkProperties?>()
@Suppress("NAME_SHADOWING") private var currentNetwork: Network? = null
val available = available override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] }
if (available != null) { private val networkCallback = object : ConnectivityManager.NetworkCallback() {
val (iface, lp) = available override fun onAvailable(network: Network) {
it.onAvailable(iface, lp) val properties = Services.connectivity.getLinkProperties(network)
} else it.onLost() if (properties?.allInterfaceNames?.any(iface::matches) != true) return
synchronized(this@InterfaceMonitor) {
available[network] = properties
currentNetwork = network
callbacks.toList()
}.forEach { it.onAvailable(properties) }
}
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
val matched = properties.allInterfaceNames.any(iface::matches)
synchronized(this@InterfaceMonitor) {
if (!matched) {
if (currentNetwork == network) currentNetwork = null
available.remove(network)
return
}
available[network] = properties
if (currentNetwork == null) currentNetwork = network
else if (currentNetwork != network) return
callbacks.toList()
}.forEach { it.onAvailable(properties) }
}
override fun onLost(network: Network) {
var properties: LinkProperties? = null
synchronized(this@InterfaceMonitor) {
if (available.remove(network) == null || currentNetwork != network) return
if (available.isNotEmpty()) {
val next = available.entries.first()
currentNetwork = next.key
Timber.d("Switching to ${next.value} for $iface")
properties = next.value
} else currentNetwork = null
callbacks.toList()
}.forEach { it.onAvailable(properties) }
} }
} }
private var registered = false
override var currentIface: String? = null
private set
override val currentLinkProperties get() = Services.connectivity.allNetworks
.map { Services.connectivity.getLinkProperties(it) }
.singleOrNull { it?.allInterfaceNames?.contains(iface) == true }
override fun registerCallbackLocked(callback: Callback) { override fun registerCallbackLocked(callback: Callback) {
if (!registered) { if (registered) {
IpLinkMonitor.registerCallback(this, iface, this::setPresent) val currentLinkProperties = currentLinkProperties
if (currentLinkProperties != null) GlobalScope.launch {
callback.onAvailable(currentLinkProperties)
}
} else {
Services.connectivity.registerNetworkCallback(request, networkCallback)
registered = true registered = true
} else if (currentIface != null) GlobalScope.launch {
callback.onAvailable(iface, currentLinkProperties ?: return@launch)
} }
} }
override fun destroyLocked() { override fun destroyLocked() {
IpLinkMonitor.unregisterCallback(this) if (!registered) return
currentIface = null Services.connectivity.unregisterNetworkCallback(networkCallback)
registered = false registered = false
available.clear()
currentNetwork = null
} }
} }

View File

@@ -1,44 +0,0 @@
package be.mygod.vpnhotspot.net.monitor
class IpLinkMonitor private constructor() : IpMonitor() {
companion object {
/**
* Based on: https://android.googlesource.com/platform/external/iproute2/+/70556c1/ip/ipaddress.c#1053
*/
private val parser = "^(Deleted )?-?\\d+: ([^:@]+)".toRegex()
private val callbacks = HashMap<Any, Pair<String, (Boolean) -> Unit>>()
private var instance: IpLinkMonitor? = null
fun registerCallback(owner: Any, iface: String, callback: (Boolean) -> Unit) = synchronized(this) {
check(callbacks.put(owner, Pair(iface, callback)) == null)
var monitor = instance
if (monitor == null) {
monitor = IpLinkMonitor()
instance = monitor
} else monitor.flushAsync()
}
fun unregisterCallback(owner: Any) = synchronized(this) {
if (callbacks.remove(owner) == null || callbacks.isNotEmpty()) return@synchronized
instance?.destroy()
instance = null
}
}
override val monitoredObject: String get() = "link"
override fun processLine(line: String) {
val match = parser.find(line) ?: return
val iface = match.groupValues[2]
val present = match.groupValues[1].isEmpty()
synchronized(IpLinkMonitor) {
for ((target, callback) in callbacks.values) if (target == iface) callback(present)
}
}
override fun processLines(lines: Sequence<String>) {
val present = HashSet<String>()
for (it in lines) present.add((parser.find(it) ?: continue).groupValues[2])
synchronized(IpLinkMonitor) { for ((iface, callback) in callbacks.values) callback(present.contains(iface)) }
}
}

View File

@@ -37,18 +37,15 @@ abstract class UpstreamMonitor {
if (key == KEY) GlobalScope.launch { // prevent callback called in main if (key == KEY) GlobalScope.launch { // prevent callback called in main
synchronized(this) { synchronized(this) {
val old = monitor val old = monitor
val (active, callbacks) = synchronized(old) { val callbacks = synchronized(old) {
(old.currentIface != null) to old.callbacks.toList().also { old.callbacks.toList().also {
old.callbacks.clear() old.callbacks.clear()
old.destroyLocked() old.destroyLocked()
} }
} }
val new = generateMonitor() val new = generateMonitor()
monitor = new monitor = new
for (callback in callbacks) { for (callback in callbacks) new.registerCallback(callback)
if (active) callback.onLost()
new.registerCallback(callback)
}
} }
} }
} }
@@ -56,14 +53,9 @@ abstract class UpstreamMonitor {
interface Callback { interface Callback {
/** /**
* Called if some interface is available. This might be called on different ifname without having called onLost. * Called if some possibly stacked interface is available
* This might also be called on the same ifname but with updated link properties.
*/ */
fun onAvailable(ifname: String, properties: LinkProperties) fun onAvailable(properties: LinkProperties? = null)
/**
* Called if no interface is available.
*/
fun onLost()
/** /**
* Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the * Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the
* default network (see DefaultNetworkMonitor) and we are using rules at priority 22000 * default network (see DefaultNetworkMonitor) and we are using rules at priority 22000
@@ -77,7 +69,6 @@ abstract class UpstreamMonitor {
val callbacks = mutableSetOf<Callback>() val callbacks = mutableSetOf<Callback>()
protected abstract val currentLinkProperties: LinkProperties? protected abstract val currentLinkProperties: LinkProperties?
open val currentIface: String? get() = currentLinkProperties?.interfaceName
protected abstract fun registerCallbackLocked(callback: Callback) protected abstract fun registerCallbackLocked(callback: Callback)
abstract fun destroyLocked() abstract fun destroyLocked()

View File

@@ -16,71 +16,40 @@ object VpnMonitor : UpstreamMonitor() {
.build() .build()
private var registered = false private var registered = false
private val available = HashMap<Network, LinkProperties>() private val available = HashMap<Network, LinkProperties?>()
private var currentNetwork: Network? = null private var currentNetwork: Network? = null
override val currentLinkProperties: LinkProperties? get() { override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] }
val currentNetwork = currentNetwork
return if (currentNetwork == null) null else available[currentNetwork]
}
private val networkCallback = object : ConnectivityManager.NetworkCallback() { private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) { override fun onAvailable(network: Network) {
val properties = Services.connectivity.getLinkProperties(network) val properties = Services.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return
var switching = false
synchronized(this@VpnMonitor) { synchronized(this@VpnMonitor) {
val oldProperties = available.put(network, properties) available[network] = properties
if (currentNetwork != network || ifname != oldProperties?.interfaceName) { currentNetwork = network
if (currentNetwork != null) switching = true
currentNetwork = network
}
callbacks.toList() callbacks.toList()
}.forEach { }.forEach { it.onAvailable(properties) }
if (switching) it.onLost()
it.onAvailable(ifname, properties)
}
} }
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
var losing = true
var ifname: String?
synchronized(this@VpnMonitor) { synchronized(this@VpnMonitor) {
if (currentNetwork == null) { available[network] = properties
onAvailable(network) if (currentNetwork == null) currentNetwork = network
return else if (currentNetwork != network) return
}
if (currentNetwork != network) return
val oldProperties = available.put(network, properties)!!
ifname = properties.interfaceName
when (ifname) {
null -> Timber.w("interfaceName became null: $oldProperties -> $properties")
oldProperties.interfaceName -> losing = false
else -> Timber.w("interfaceName changed: $oldProperties -> $properties")
}
callbacks.toList() callbacks.toList()
}.forEach { }.forEach { it.onAvailable(properties) }
if (losing) {
if (ifname == null) return onLost(network)
it.onLost()
}
ifname?.let { ifname -> it.onAvailable(ifname, properties) }
}
} }
override fun onLost(network: Network) { override fun onLost(network: Network) {
var newProperties: LinkProperties? = null var properties: LinkProperties? = null
synchronized(this@VpnMonitor) { synchronized(this@VpnMonitor) {
if (available.remove(network) == null || currentNetwork != network) return if (available.remove(network) == null || currentNetwork != network) return
if (available.isNotEmpty()) { if (available.isNotEmpty()) {
val next = available.entries.first() val next = available.entries.first()
currentNetwork = next.key currentNetwork = next.key
Timber.d("Switching to ${next.value.interfaceName} as VPN interface") Timber.d("Switching to ${next.value} as VPN interface")
newProperties = next.value properties = next.value
} else currentNetwork = null } else currentNetwork = null
callbacks.toList() callbacks.toList()
}.forEach { }.forEach { it.onAvailable(properties) }
it.onLost()
newProperties?.let { prop -> it.onAvailable(prop.interfaceName!!, prop) }
}
} }
} }
@@ -88,7 +57,7 @@ object VpnMonitor : UpstreamMonitor() {
if (registered) { if (registered) {
val currentLinkProperties = currentLinkProperties val currentLinkProperties = currentLinkProperties
if (currentLinkProperties != null) GlobalScope.launch { if (currentLinkProperties != null) GlobalScope.launch {
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties) callback.onAvailable(currentLinkProperties)
} }
} else { } else {
Services.connectivity.registerNetworkCallback(request, networkCallback) Services.connectivity.registerNetworkCallback(request, networkCallback)

View File

@@ -21,32 +21,31 @@ import timber.log.Timber
class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs),
DefaultLifecycleObserver { DefaultLifecycleObserver {
companion object { companion object {
private val internetAddress = parseNumericAddress("8.8.8.8") private val internetV4Address = parseNumericAddress("8.8.8.8")
private val internetV6Address = parseNumericAddress("2001:4860:4860::8888")
} }
private data class Interface(val ifname: String, val internet: Boolean = true)
private open inner class Monitor : UpstreamMonitor.Callback { private open inner class Monitor : UpstreamMonitor.Callback {
protected var currentInterface: Interface? = null protected var currentInterfaces = emptyMap<String, Boolean>()
val charSequence get() = currentInterface?.run { val charSequence get() = currentInterfaces.map { (ifname, internet) ->
if (internet) SpannableStringBuilder(ifname).apply { if (internet) SpannableStringBuilder(ifname).apply {
setSpan(StyleSpan(Typeface.BOLD), 0, length, 0) setSpan(StyleSpan(Typeface.BOLD), 0, length, 0)
} else ifname } else ifname
} ?: "" }.joinTo(SpannableStringBuilder()).let { if (it.isEmpty()) "" else it }
override fun onAvailable(ifname: String, properties: LinkProperties) { override fun onAvailable(properties: LinkProperties?) {
currentInterface = Interface(ifname, properties.allRoutes.any { val result = mutableMapOf<String, Boolean>()
try { for (route in properties?.allRoutes ?: emptyList()) {
it.matches(internetAddress) result.compute(route.`interface` ?: continue) { _, internet ->
} catch (e: RuntimeException) { internet == true || try {
Timber.w(e) route.matches(internetV4Address) || route.matches(internetV6Address)
false } catch (e: RuntimeException) {
Timber.w(e)
false
}
} }
}) }
onUpdate() currentInterfaces = result
}
override fun onLost() {
currentInterface = null
onUpdate() onUpdate()
} }
} }
@@ -54,7 +53,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co
private val primary = Monitor() private val primary = Monitor()
private val fallback: Monitor = object : Monitor() { private val fallback: Monitor = object : Monitor() {
override fun onFallback() { override fun onFallback() {
currentInterface = Interface("<default>") currentInterfaces = mapOf("<default>" to true)
onUpdate() onUpdate()
} }
} }