From b675bdda0927f2c7665f2d3df5de88f6dd7cc8df Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 11 Sep 2020 15:14:46 -0400 Subject: [PATCH 1/4] Initial support for stacked links --- .../java/be/mygod/vpnhotspot/net/Routing.kt | 81 ++++++++++------- .../net/monitor/DefaultNetworkMonitor.kt | 43 +-------- .../net/monitor/FallbackUpstreamMonitor.kt | 5 +- .../net/monitor/InterfaceMonitor.kt | 91 +++++++++++++------ .../vpnhotspot/net/monitor/IpLinkMonitor.kt | 44 --------- .../vpnhotspot/net/monitor/UpstreamMonitor.kt | 19 +--- .../vpnhotspot/net/monitor/VpnMonitor.kt | 59 +++--------- .../preference/UpstreamsPreference.kt | 39 ++++---- .../java/be/mygod/vpnhotspot/util/Utils.kt | 11 +++ 9 files changed, 165 insertions(+), 227 deletions(-) delete mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpLinkMonitor.kt diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt index ec5362a8..87505c87 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt @@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.net import android.annotation.TargetApi import android.net.LinkProperties +import android.net.RouteInfo import android.os.Build import androidx.annotation.RequiresApi 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.root.RootManager import be.mygod.vpnhotspot.root.RoutingCommands -import be.mygod.vpnhotspot.util.RootSession -import be.mygod.vpnhotspot.util.if_nametoindex -import be.mygod.vpnhotspot.util.parseNumericAddress +import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.widget.SmartSnackbar import kotlinx.coroutines.CancellationException import timber.log.Timber @@ -180,52 +179,65 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh } } - var subrouting: Subrouting? = null - var dns: List = emptyList() + var subrouting = mutableMapOf() + var dns = emptyList>() - override fun onAvailable(ifname: String, properties: LinkProperties) = synchronized(this@Routing) { + override fun onAvailable(properties: LinkProperties?) = synchronized(this@Routing) { if (stopped) return - val subrouting = subrouting - when { - subrouting != null -> check(subrouting.upstream == ifname) { "${subrouting.upstream} != $ifname" } - !upstreams.add(ifname) -> return - else -> this.subrouting = try { - Subrouting(priority, ifname) + val toRemove = subrouting.keys.toMutableSet() + for (link in properties?.allStackedLinks ?: emptySequence()) { + val ifname = link.interfaceName + if (ifname == null || toRemove.remove(ifname) || !upstreams.add(ifname)) continue + try { + subrouting[ifname] = Subrouting(priority, ifname) } catch (e: Exception) { SmartSnackbar.make(e).show() if (e !is CancellationException) Timber.w(e) - null } } - dns = properties.dnsServers - updateDnsRoute() - } - - override fun onLost() = synchronized(this@Routing) { - if (stopped) return - val subrouting = subrouting ?: return - // we could be removing fallback subrouting which no collision could ever happen, check before removing - subrouting.upstream?.let { check(upstreams.remove(it)) } - subrouting.transaction.revert() - this.subrouting = null - dns = emptyList() + for (ifname in toRemove) { + subrouting.remove(ifname)?.transaction?.revert() + check(upstreams.remove(ifname)) + } + val routes = properties?.allRoutes + dns = properties?.dnsServers?.map { dest -> + // based on: + // https://cs.android.com/android/platform/superproject/+/master:frameworks/base/packages/Tethering/src/com/android/networkstack/tethering/TetheringInterfaceUtils.java;l=88;drc=master + // https://cs.android.com/android/platform/superproject/+/master:frameworks/libs/net/common/framework/android/net/util/NetUtils.java;l=44;drc=de5905fe0407a1f5e115423d56c948ee2400683d + val size = dest.address.size + var bestRoute: RouteInfo? = null + 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() } } private val fallbackUpstream = object : Upstream(RULE_PRIORITY_UPSTREAM_FALLBACK) { var fallbackInactive = true + + override fun onAvailable(properties: LinkProperties?) { + check(fallbackInactive) + super.onAvailable(properties) + } + override fun onFallback() = synchronized(this@Routing) { if (stopped) return fallbackInactive = false - check(subrouting == null) - subrouting = try { - Subrouting(priority) + check(subrouting.isEmpty() && upstreams.add("")) + try { + subrouting[""] = Subrouting(priority) } catch (e: Exception) { SmartSnackbar.make(e).show() 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() } } @@ -334,8 +346,9 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh private var currentDns: DnsRoute? = null private fun updateDnsRoute() { val selected = sequenceOf(upstream, fallbackUpstream).flatMap { upstream -> - val ifindex = upstream.subrouting?.ifindex ?: 0 - if (ifindex == 0) emptySequence() else upstream.dns.asSequence().map { ifindex to it } + upstream.dns.asSequence().map { (server, iface) -> + ((if (iface != null) upstream.subrouting[iface]?.ifindex else null) ?: 0) to server + } }.firstOrNull { it.second is Inet4Address } val ifindex = selected?.first ?: 0 var dns = selected?.second?.hostAddress @@ -378,8 +391,8 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh synchronized(this) { clients.values.forEach { it.close() } } currentDns?.transaction?.revert() disableSystem?.revert() - fallbackUpstream.subrouting?.transaction?.revert() - upstream.subrouting?.transaction?.revert() + fallbackUpstream.subrouting.values.forEach { it.transaction.revert() } + upstream.subrouting.values.forEach { it.transaction.revert() } transaction.revert() } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt index 8c28e629..f329461a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/DefaultNetworkMonitor.kt @@ -9,11 +9,9 @@ import android.os.Build import be.mygod.vpnhotspot.util.Services import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch -import timber.log.Timber object DefaultNetworkMonitor : UpstreamMonitor() { private var registered = false - private var currentNetwork: Network? = null override var currentLinkProperties: LinkProperties? = null private set /** @@ -27,62 +25,30 @@ object DefaultNetworkMonitor : UpstreamMonitor() { private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { val properties = Services.connectivity.getLinkProperties(network) - val ifname = properties?.interfaceName ?: return - var switching = false 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 callbacks.toList() - }.forEach { - if (switching) it.onLost() - it.onAvailable(ifname, properties) - } + }.forEach { it.onAvailable(properties) } } override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { - var losing = true - var ifname: String? synchronized(this@DefaultNetworkMonitor) { - if (currentNetwork == null) { - onAvailable(network) - return - } - if (currentNetwork != network) return - val oldProperties = currentLinkProperties!! 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() - }.forEach { - if (losing) { - if (ifname == null) return onLost(network) - it.onLost() - } - ifname?.let { ifname -> it.onAvailable(ifname, properties) } - } + }.forEach { it.onAvailable(properties) } } override fun onLost(network: Network) = synchronized(this@DefaultNetworkMonitor) { - if (currentNetwork != network) return - currentNetwork = null currentLinkProperties = null callbacks.toList() - }.forEach { it.onLost() } + }.forEach { it.onAvailable() } } override fun registerCallbackLocked(callback: Callback) { if (registered) { val currentLinkProperties = currentLinkProperties if (currentLinkProperties != null) GlobalScope.launch { - callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties) + callback.onAvailable(currentLinkProperties) } } else { if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) { @@ -103,7 +69,6 @@ object DefaultNetworkMonitor : UpstreamMonitor() { if (!registered) return Services.connectivity.unregisterNetworkCallback(networkCallback) registered = false - currentNetwork = null currentLinkProperties = null } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/FallbackUpstreamMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/FallbackUpstreamMonitor.kt index e710bd03..94e7219d 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/FallbackUpstreamMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/FallbackUpstreamMonitor.kt @@ -34,10 +34,7 @@ abstract class FallbackUpstreamMonitor private constructor() : UpstreamMonitor() } val new = generateMonitor() monitor = new - for (callback in callbacks) { - callback.onLost() - new.registerCallback(callback) - } + for (callback in callbacks) new.registerCallback(callback) } } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt index 3b2c4cc8..790859d9 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt @@ -1,49 +1,84 @@ package be.mygod.vpnhotspot.net.monitor +import android.net.ConnectivityManager import android.net.LinkProperties +import android.net.Network +import android.net.NetworkCapabilities import be.mygod.vpnhotspot.util.Services import be.mygod.vpnhotspot.util.allInterfaceNames import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch +import timber.log.Timber class InterfaceMonitor(val iface: String) : UpstreamMonitor() { - private fun setPresent(present: Boolean) { - var available: Pair? = null - synchronized(this) { - val old = currentIface != null - if (present == old) return - currentIface = if (present) iface else null - if (present) available = iface to (currentLinkProperties ?: return) - callbacks.toList() - }.forEach { - @Suppress("NAME_SHADOWING") - val available = available - if (available != null) { - val (iface, lp) = available - it.onAvailable(iface, lp) - } else it.onLost() + private val request = networkRequestBuilder().apply { + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) + removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) + removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + }.build() + private var registered = false + + private val available = HashMap() + private var currentNetwork: Network? = null + override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] } + private val networkCallback = object : ConnectivityManager.NetworkCallback() { + override fun onAvailable(network: Network) { + val properties = Services.connectivity.getLinkProperties(network) + if (properties?.allInterfaceNames?.contains(iface) != 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.contains(iface) + 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) { - if (!registered) { - IpLinkMonitor.registerCallback(this, iface, this::setPresent) + if (registered) { + val currentLinkProperties = currentLinkProperties + if (currentLinkProperties != null) GlobalScope.launch { + callback.onAvailable(currentLinkProperties) + } + } else { + Services.connectivity.registerNetworkCallback(request, networkCallback) registered = true - } else if (currentIface != null) GlobalScope.launch { - callback.onAvailable(iface, currentLinkProperties ?: return@launch) } } override fun destroyLocked() { - IpLinkMonitor.unregisterCallback(this) - currentIface = null + if (!registered) return + Services.connectivity.unregisterNetworkCallback(networkCallback) registered = false + available.clear() + currentNetwork = null } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpLinkMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpLinkMonitor.kt deleted file mode 100644 index e6c8e42a..00000000 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/IpLinkMonitor.kt +++ /dev/null @@ -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 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) { - val present = HashSet() - for (it in lines) present.add((parser.find(it) ?: continue).groupValues[2]) - synchronized(IpLinkMonitor) { for ((iface, callback) in callbacks.values) callback(present.contains(iface)) } - } -} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt index 88ca85b4..c8629496 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/UpstreamMonitor.kt @@ -37,18 +37,15 @@ abstract class UpstreamMonitor { if (key == KEY) GlobalScope.launch { // prevent callback called in main synchronized(this) { val old = monitor - val (active, callbacks) = synchronized(old) { - (old.currentIface != null) to old.callbacks.toList().also { + val callbacks = synchronized(old) { + old.callbacks.toList().also { old.callbacks.clear() old.destroyLocked() } } val new = generateMonitor() monitor = new - for (callback in callbacks) { - if (active) callback.onLost() - new.registerCallback(callback) - } + for (callback in callbacks) new.registerCallback(callback) } } } @@ -56,14 +53,9 @@ abstract class UpstreamMonitor { interface Callback { /** - * Called if some interface is available. This might be called on different ifname without having called onLost. - * This might also be called on the same ifname but with updated link properties. + * Called if some possibly stacked interface is available */ - fun onAvailable(ifname: String, properties: LinkProperties) - /** - * Called if no interface is available. - */ - fun onLost() + fun onAvailable(properties: LinkProperties? = null) /** * 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 @@ -77,7 +69,6 @@ abstract class UpstreamMonitor { val callbacks = mutableSetOf() protected abstract val currentLinkProperties: LinkProperties? - open val currentIface: String? get() = currentLinkProperties?.interfaceName protected abstract fun registerCallbackLocked(callback: Callback) abstract fun destroyLocked() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt index b364b1e6..03ef2575 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/VpnMonitor.kt @@ -16,71 +16,40 @@ object VpnMonitor : UpstreamMonitor() { .build() private var registered = false - private val available = HashMap() + private val available = HashMap() private var currentNetwork: Network? = null - override val currentLinkProperties: LinkProperties? get() { - val currentNetwork = currentNetwork - return if (currentNetwork == null) null else available[currentNetwork] - } + override val currentLinkProperties: LinkProperties? get() = currentNetwork?.let { available[it] } private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { val properties = Services.connectivity.getLinkProperties(network) - val ifname = properties?.interfaceName ?: return - var switching = false synchronized(this@VpnMonitor) { - val oldProperties = available.put(network, properties) - if (currentNetwork != network || ifname != oldProperties?.interfaceName) { - if (currentNetwork != null) switching = true - currentNetwork = network - } + available[network] = properties + currentNetwork = network callbacks.toList() - }.forEach { - if (switching) it.onLost() - it.onAvailable(ifname, properties) - } + }.forEach { it.onAvailable(properties) } } override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { - var losing = true - var ifname: String? synchronized(this@VpnMonitor) { - if (currentNetwork == null) { - onAvailable(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") - } + available[network] = properties + if (currentNetwork == null) currentNetwork = network + else if (currentNetwork != network) return callbacks.toList() - }.forEach { - if (losing) { - if (ifname == null) return onLost(network) - it.onLost() - } - ifname?.let { ifname -> it.onAvailable(ifname, properties) } - } + }.forEach { it.onAvailable(properties) } } override fun onLost(network: Network) { - var newProperties: LinkProperties? = null + var properties: LinkProperties? = null synchronized(this@VpnMonitor) { 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.interfaceName} as VPN interface") - newProperties = next.value + Timber.d("Switching to ${next.value} as VPN interface") + properties = next.value } else currentNetwork = null callbacks.toList() - }.forEach { - it.onLost() - newProperties?.let { prop -> it.onAvailable(prop.interfaceName!!, prop) } - } + }.forEach { it.onAvailable(properties) } } } @@ -88,7 +57,7 @@ object VpnMonitor : UpstreamMonitor() { if (registered) { val currentLinkProperties = currentLinkProperties if (currentLinkProperties != null) GlobalScope.launch { - callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties) + callback.onAvailable(currentLinkProperties) } } else { Services.connectivity.registerNetworkCallback(request, networkCallback) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt index 07cf5f1f..0be0fd1f 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt @@ -14,39 +14,40 @@ import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.util.SpanFormatter -import be.mygod.vpnhotspot.util.allRoutes +import be.mygod.vpnhotspot.util.allStackedLinks import be.mygod.vpnhotspot.util.parseNumericAddress import timber.log.Timber class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), DefaultLifecycleObserver { 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 { - protected var currentInterface: Interface? = null - val charSequence get() = currentInterface?.run { + protected var currentInterfaces = emptyList() + val charSequence get() = currentInterfaces.map { (ifname, internet) -> if (internet) SpannableStringBuilder(ifname).apply { setSpan(StyleSpan(Typeface.BOLD), 0, length, 0) } else ifname - } ?: "∅" + }.joinToString().let { if (it.isEmpty()) "∅" else it } - override fun onAvailable(ifname: String, properties: LinkProperties) { - currentInterface = Interface(ifname, properties.allRoutes.any { - try { - it.matches(internetAddress) - } catch (e: RuntimeException) { - Timber.w(e) - false + + override fun onAvailable(properties: LinkProperties?) { + currentInterfaces = properties?.allStackedLinks?.mapNotNull { prop -> + prop.interfaceName?.let { ifname -> + Interface(ifname, prop.routes.any { + try { + it.matches(internetV4Address) || it.matches(internetV6Address) + } catch (e: RuntimeException) { + Timber.w(e) + false + } + }) } - }) - onUpdate() - } - - override fun onLost() { - currentInterface = null + }?.toList() ?: emptyList() onUpdate() } } @@ -54,7 +55,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co private val primary = Monitor() private val fallback: Monitor = object : Monitor() { override fun onFallback() { - currentInterface = Interface("") + currentInterfaces = listOf(Interface("")) onUpdate() } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt index 6b7c3ddd..09c570c8 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt @@ -117,6 +117,17 @@ val LinkProperties.allInterfaceNames get() = getAllInterfaceNames.invoke(this) a private val getAllRoutes by lazy { LinkProperties::class.java.getDeclaredMethod("getAllRoutes") } @Suppress("UNCHECKED_CAST") val LinkProperties.allRoutes get() = getAllRoutes.invoke(this) as List +private val getStackedLinks by lazy { LinkProperties::class.java.getDeclaredMethod("getStackedLinks") } +@Suppress("UNCHECKED_CAST") +private val LinkProperties.stackedLinks get() = getStackedLinks.invoke(this) as List + +private suspend fun SequenceScope.yieldRec(prop: LinkProperties) { + yield(prop) + for (link in prop.stackedLinks) yieldRec(link) +} +val LinkProperties.allStackedLinks get() = let { + sequence { yieldRec(it) } +} fun Context.launchUrl(url: String) { if (app.hasTouch) try { From b5ae2fb5d274d15de2883021655015b8a1eb7b3d Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 11 Sep 2020 15:18:19 -0400 Subject: [PATCH 2/4] Support regex in iface --- .../be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt index 790859d9..cbf2f4b4 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/monitor/InterfaceMonitor.kt @@ -10,7 +10,8 @@ import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import timber.log.Timber -class InterfaceMonitor(val iface: String) : UpstreamMonitor() { +class InterfaceMonitor(ifaceRegex: String) : UpstreamMonitor() { + private val iface = ifaceRegex.toRegex() private val request = networkRequestBuilder().apply { removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED) removeCapability(NetworkCapabilities.NET_CAPABILITY_TRUSTED) @@ -24,7 +25,7 @@ class InterfaceMonitor(val iface: String) : UpstreamMonitor() { private val networkCallback = object : ConnectivityManager.NetworkCallback() { override fun onAvailable(network: Network) { val properties = Services.connectivity.getLinkProperties(network) - if (properties?.allInterfaceNames?.contains(iface) != true) return + if (properties?.allInterfaceNames?.any(iface::matches) != true) return synchronized(this@InterfaceMonitor) { available[network] = properties currentNetwork = network @@ -33,7 +34,7 @@ class InterfaceMonitor(val iface: String) : UpstreamMonitor() { } override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) { - val matched = properties.allInterfaceNames.contains(iface) + val matched = properties.allInterfaceNames.any(iface::matches) synchronized(this@InterfaceMonitor) { if (!matched) { if (currentNetwork == network) currentNetwork = null From 642f93b9f7fc3a68b65336617781bf12c0212544 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 12 Sep 2020 03:35:03 +0800 Subject: [PATCH 3/4] Avoid using greylist api --- .../java/be/mygod/vpnhotspot/net/Routing.kt | 5 ++-- .../preference/UpstreamsPreference.kt | 29 +++++++++---------- .../java/be/mygod/vpnhotspot/util/Utils.kt | 11 ------- 3 files changed, 16 insertions(+), 29 deletions(-) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt index 87505c87..ddec2f1d 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt @@ -185,9 +185,8 @@ class Routing(private val caller: Any, private val downstream: String) : IpNeigh override fun onAvailable(properties: LinkProperties?) = synchronized(this@Routing) { if (stopped) return val toRemove = subrouting.keys.toMutableSet() - for (link in properties?.allStackedLinks ?: emptySequence()) { - val ifname = link.interfaceName - if (ifname == null || toRemove.remove(ifname) || !upstreams.add(ifname)) continue + for (ifname in properties?.allInterfaceNames ?: emptyList()) { + if (toRemove.remove(ifname) || !upstreams.add(ifname)) continue try { subrouting[ifname] = Subrouting(priority, ifname) } catch (e: Exception) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt index 0be0fd1f..56a0d318 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt @@ -14,7 +14,7 @@ import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.util.SpanFormatter -import be.mygod.vpnhotspot.util.allStackedLinks +import be.mygod.vpnhotspot.util.allRoutes import be.mygod.vpnhotspot.util.parseNumericAddress import timber.log.Timber @@ -25,9 +25,8 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co 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 { - protected var currentInterfaces = emptyList() + protected var currentInterfaces = emptyMap() val charSequence get() = currentInterfaces.map { (ifname, internet) -> if (internet) SpannableStringBuilder(ifname).apply { setSpan(StyleSpan(Typeface.BOLD), 0, length, 0) @@ -36,18 +35,18 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co override fun onAvailable(properties: LinkProperties?) { - currentInterfaces = properties?.allStackedLinks?.mapNotNull { prop -> - prop.interfaceName?.let { ifname -> - Interface(ifname, prop.routes.any { - try { - it.matches(internetV4Address) || it.matches(internetV6Address) - } catch (e: RuntimeException) { - Timber.w(e) - false - } - }) + val result = mutableMapOf() + for (route in properties?.allRoutes ?: emptyList()) { + result.compute(route.`interface` ?: continue) { _, internet -> + internet == true || try { + route.matches(internetV4Address) || route.matches(internetV6Address) + } catch (e: RuntimeException) { + Timber.w(e) + false + } } - }?.toList() ?: emptyList() + } + currentInterfaces = result onUpdate() } } @@ -55,7 +54,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co private val primary = Monitor() private val fallback: Monitor = object : Monitor() { override fun onFallback() { - currentInterfaces = listOf(Interface("")) + currentInterfaces = mapOf("" to true) onUpdate() } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt index 09c570c8..6b7c3ddd 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/util/Utils.kt @@ -117,17 +117,6 @@ val LinkProperties.allInterfaceNames get() = getAllInterfaceNames.invoke(this) a private val getAllRoutes by lazy { LinkProperties::class.java.getDeclaredMethod("getAllRoutes") } @Suppress("UNCHECKED_CAST") val LinkProperties.allRoutes get() = getAllRoutes.invoke(this) as List -private val getStackedLinks by lazy { LinkProperties::class.java.getDeclaredMethod("getStackedLinks") } -@Suppress("UNCHECKED_CAST") -private val LinkProperties.stackedLinks get() = getStackedLinks.invoke(this) as List - -private suspend fun SequenceScope.yieldRec(prop: LinkProperties) { - yield(prop) - for (link in prop.stackedLinks) yieldRec(link) -} -val LinkProperties.allStackedLinks get() = let { - sequence { yieldRec(it) } -} fun Context.launchUrl(url: String) { if (app.hasTouch) try { From 467fa4529aff4dd8fbda3a3931584cd7bb72b4e8 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 12 Sep 2020 03:48:08 +0800 Subject: [PATCH 4/4] Fix joinTo for spannables --- .../java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt index 56a0d318..a5b2056a 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/preference/UpstreamsPreference.kt @@ -31,8 +31,7 @@ class UpstreamsPreference(context: Context, attrs: AttributeSet) : Preference(co if (internet) SpannableStringBuilder(ifname).apply { setSpan(StyleSpan(Typeface.BOLD), 0, length, 0) } else ifname - }.joinToString().let { if (it.isEmpty()) "∅" else it } - + }.joinTo(SpannableStringBuilder()).let { if (it.isEmpty()) "∅" else it } override fun onAvailable(properties: LinkProperties?) { val result = mutableMapOf()