@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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()
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user