@@ -0,0 +1,70 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
|
||||
class InterfaceMonitor(val iface: String) : UpstreamMonitor() {
|
||||
companion object {
|
||||
/**
|
||||
* Based on: https://android.googlesource.com/platform/external/iproute2/+/70556c1/ip/ipaddress.c#1053
|
||||
*/
|
||||
private val parser = ("^(Deleted )?-?\\d+: ([^:@]+)").toRegex()
|
||||
}
|
||||
|
||||
private inner class IpLinkMonitor : IpMonitor() {
|
||||
override val monitoredObject: String get() = "link"
|
||||
|
||||
override fun processLine(line: String) {
|
||||
val match = parser.find(line) ?: return
|
||||
if (match.groupValues[2] != iface) return
|
||||
setPresent(match.groupValues[1].isEmpty())
|
||||
}
|
||||
|
||||
override fun processLines(lines: Sequence<String>) =
|
||||
setPresent(lines.any { parser.find(it)?.groupValues?.get(2) == iface })
|
||||
}
|
||||
|
||||
private fun setPresent(present: Boolean) = if (initializing) {
|
||||
initializedPresent = present
|
||||
currentIface = if (present) iface else null
|
||||
} else synchronized(this) {
|
||||
val old = currentIface != null
|
||||
if (present == old) return
|
||||
currentIface = if (present) iface else null
|
||||
if (present) {
|
||||
val dns = dns
|
||||
callbacks.forEach { it.onAvailable(iface, dns) }
|
||||
} else callbacks.forEach { it.onLost() }
|
||||
}
|
||||
|
||||
private var monitor: IpLinkMonitor? = null
|
||||
private var initializing = false
|
||||
private var initializedPresent: Boolean? = null
|
||||
override var currentIface: String? = null
|
||||
private set
|
||||
private val dns get() = app.connectivity.allNetworks
|
||||
.map { app.connectivity.getLinkProperties(it) }
|
||||
.singleOrNull { it.interfaceName == iface }
|
||||
?.dnsServers ?: emptyList()
|
||||
|
||||
override fun registerCallbackLocked(callback: Callback): Boolean {
|
||||
var monitor = monitor
|
||||
val present = if (monitor == null) {
|
||||
initializing = true
|
||||
initializedPresent = null
|
||||
monitor = IpLinkMonitor()
|
||||
this.monitor = monitor
|
||||
monitor.run()
|
||||
initializing = false
|
||||
initializedPresent!!
|
||||
} else currentIface != null
|
||||
if (present) callback.onAvailable(iface, dns)
|
||||
return !present
|
||||
}
|
||||
|
||||
override fun destroyLocked() {
|
||||
val monitor = monitor ?: return
|
||||
this.monitor = null
|
||||
currentIface = null
|
||||
monitor.destroy()
|
||||
}
|
||||
}
|
||||
67
mobile/src/main/java/be/mygod/vpnhotspot/net/IpMonitor.kt
Normal file
67
mobile/src/main/java/be/mygod/vpnhotspot/net/IpMonitor.kt
Normal file
@@ -0,0 +1,67 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.util.thread
|
||||
import java.io.InterruptedIOException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
abstract class IpMonitor : Runnable {
|
||||
protected abstract val monitoredObject: String
|
||||
protected abstract fun processLine(line: String)
|
||||
protected abstract fun processLines(lines: Sequence<String>)
|
||||
|
||||
private var monitor: Process? = null
|
||||
private var pool: ScheduledExecutorService? = null
|
||||
|
||||
init {
|
||||
thread("${javaClass.simpleName}-input") {
|
||||
// monitor may get rejected by SELinux
|
||||
val monitor = ProcessBuilder("sh", "-c",
|
||||
"ip monitor $monitoredObject || su -c 'ip monitor $monitoredObject'")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
this.monitor = monitor
|
||||
thread("${javaClass.simpleName}-error") {
|
||||
try {
|
||||
monitor.errorStream.bufferedReader().forEachLine { Log.e(javaClass.simpleName, it) }
|
||||
} catch (_: InterruptedIOException) { }
|
||||
}
|
||||
try {
|
||||
monitor.inputStream.bufferedReader().forEachLine(this::processLine)
|
||||
monitor.waitFor()
|
||||
if (monitor.exitValue() == 0) return@thread
|
||||
Log.w(javaClass.simpleName, "Failed to set up monitor, switching to polling")
|
||||
val pool = Executors.newScheduledThreadPool(1)
|
||||
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
|
||||
this.pool = pool
|
||||
} catch (_: InterruptedIOException) { }
|
||||
}
|
||||
}
|
||||
|
||||
fun flush() = thread("${javaClass.simpleName}-flush") { run() }
|
||||
|
||||
override fun run() {
|
||||
val process = ProcessBuilder("ip", monitoredObject)
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.waitFor()
|
||||
thread("${javaClass.simpleName}-flush-error") {
|
||||
val err = process.errorStream.bufferedReader().readText()
|
||||
if (err.isNotBlank()) {
|
||||
Log.e(javaClass.simpleName, err)
|
||||
app.toast(R.string.noisy_su_failure)
|
||||
}
|
||||
}
|
||||
process.inputStream.bufferedReader().useLines(this::processLines)
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
val monitor = monitor
|
||||
if (monitor != null) thread("${javaClass.simpleName}-killer") { monitor.destroy() }
|
||||
pool?.shutdown()
|
||||
}
|
||||
}
|
||||
@@ -1,19 +1,10 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.util.debugLog
|
||||
import be.mygod.vpnhotspot.util.thread
|
||||
import java.io.InterruptedIOException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class IpNeighbourMonitor private constructor() : Runnable {
|
||||
class IpNeighbourMonitor private constructor() : IpMonitor() {
|
||||
companion object {
|
||||
private const val TAG = "IpNeighbourMonitor"
|
||||
private val callbacks = HashSet<Callback>()
|
||||
var instance: IpNeighbourMonitor? = null
|
||||
|
||||
@@ -39,75 +30,37 @@ class IpNeighbourMonitor private constructor() : Runnable {
|
||||
fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>)
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
private var updatePosted = false
|
||||
val neighbours = HashMap<String, IpNeighbour>()
|
||||
private var monitor: Process? = null
|
||||
private var pool: ScheduledExecutorService? = null
|
||||
|
||||
init {
|
||||
thread("$TAG-input") {
|
||||
// monitor may get rejected by SELinux
|
||||
val monitor = ProcessBuilder("sh", "-c", "ip monitor neigh || su -c 'ip monitor neigh'")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
this.monitor = monitor
|
||||
thread("$TAG-error") {
|
||||
try {
|
||||
monitor.errorStream.bufferedReader().forEachLine { Log.e(TAG, it) }
|
||||
} catch (_: InterruptedIOException) { }
|
||||
}
|
||||
try {
|
||||
monitor.inputStream.bufferedReader().forEachLine {
|
||||
synchronized(neighbours) {
|
||||
val neighbour = IpNeighbour.parse(it) ?: return@forEachLine
|
||||
debugLog(TAG, it)
|
||||
val changed = if (neighbour.state == IpNeighbour.State.DELETING)
|
||||
neighbours.remove(neighbour.ip) != null
|
||||
else neighbours.put(neighbour.ip, neighbour) != neighbour
|
||||
if (changed) postUpdateLocked()
|
||||
}
|
||||
}
|
||||
monitor.waitFor()
|
||||
if (monitor.exitValue() == 0) return@thread
|
||||
Log.w(TAG, "Failed to set up monitor, switching to polling")
|
||||
val pool = Executors.newScheduledThreadPool(1)
|
||||
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
|
||||
this.pool = pool
|
||||
} catch (_: InterruptedIOException) { }
|
||||
override val monitoredObject: String get() = "neigh"
|
||||
|
||||
override fun processLine(line: String) {
|
||||
synchronized(neighbours) {
|
||||
val neighbour = IpNeighbour.parse(line) ?: return
|
||||
debugLog(javaClass.simpleName, line)
|
||||
val changed = if (neighbour.state == IpNeighbour.State.DELETING)
|
||||
neighbours.remove(neighbour.ip) != null
|
||||
else neighbours.put(neighbour.ip, neighbour) != neighbour
|
||||
if (changed) postUpdateLocked()
|
||||
}
|
||||
}
|
||||
|
||||
fun flush() = thread("$TAG-flush") { run() }
|
||||
|
||||
override fun run() {
|
||||
val process = ProcessBuilder("ip", "neigh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.waitFor()
|
||||
thread("$TAG-flush-error") {
|
||||
val err = process.errorStream.bufferedReader().readText()
|
||||
if (err.isNotBlank()) {
|
||||
Log.e(TAG, err)
|
||||
app.toast(R.string.noisy_su_failure)
|
||||
}
|
||||
}
|
||||
process.inputStream.bufferedReader().useLines {
|
||||
synchronized(neighbours) {
|
||||
neighbours.clear()
|
||||
neighbours.putAll(it
|
||||
.map(IpNeighbour.Companion::parse)
|
||||
.filterNotNull()
|
||||
.filter { it.state != IpNeighbour.State.DELETING } // skip entries without lladdr
|
||||
.associateBy { it.ip })
|
||||
postUpdateLocked()
|
||||
}
|
||||
override fun processLines(lines: Sequence<String>) {
|
||||
synchronized(neighbours) {
|
||||
neighbours.clear()
|
||||
neighbours.putAll(lines
|
||||
.map(IpNeighbour.Companion::parse)
|
||||
.filterNotNull()
|
||||
.filter { it.state != IpNeighbour.State.DELETING } // skip entries without lladdr
|
||||
.associateBy { it.ip })
|
||||
postUpdateLocked()
|
||||
}
|
||||
}
|
||||
|
||||
private fun postUpdateLocked() {
|
||||
if (updatePosted || instance != this) return
|
||||
handler.post {
|
||||
app.handler.post {
|
||||
val neighbours = synchronized(neighbours) {
|
||||
updatePosted = false
|
||||
neighbours.values.toList()
|
||||
@@ -116,10 +69,4 @@ class IpNeighbourMonitor private constructor() : Runnable {
|
||||
}
|
||||
updatePosted = true
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
val monitor = monitor
|
||||
if (monitor != null) thread("$TAG-killer") { monitor.destroy() }
|
||||
pool?.shutdown()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import java.net.InetAddress
|
||||
|
||||
abstract class UpstreamMonitor {
|
||||
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
private const val KEY = "service.upstream"
|
||||
|
||||
init {
|
||||
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
private fun generateMonitor(): UpstreamMonitor {
|
||||
val upstream = app.pref.getString(KEY, null)
|
||||
return if (upstream.isNullOrEmpty()) VpnMonitor else InterfaceMonitor(upstream)
|
||||
}
|
||||
private var monitor = generateMonitor()
|
||||
|
||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) = synchronized(this) {
|
||||
monitor.registerCallback(callback, failfast)
|
||||
}
|
||||
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
if (key == KEY) synchronized(this) {
|
||||
val old = monitor
|
||||
val (active, callbacks) = synchronized(old) {
|
||||
val active = old.currentIface != null
|
||||
val callbacks = old.callbacks.toList()
|
||||
old.callbacks.clear()
|
||||
old.destroyLocked()
|
||||
Pair(active, callbacks)
|
||||
}
|
||||
val new = generateMonitor()
|
||||
monitor = new
|
||||
callbacks.forEach { new.registerCallback(it) { if (active) it.onLost() } }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
/**
|
||||
* Called if some interface is available. This might be called on different ifname without having called onLost.
|
||||
*/
|
||||
fun onAvailable(ifname: String, dns: List<InetAddress>)
|
||||
/**
|
||||
* Called if no interface is available.
|
||||
*/
|
||||
fun onLost()
|
||||
}
|
||||
|
||||
protected val callbacks = HashSet<Callback>()
|
||||
abstract val currentIface: String?
|
||||
protected abstract fun registerCallbackLocked(callback: Callback): Boolean
|
||||
protected abstract fun destroyLocked()
|
||||
|
||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
|
||||
if (synchronized(this) {
|
||||
if (!callbacks.add(callback)) return
|
||||
registerCallbackLocked(callback)
|
||||
}) failfast?.invoke()
|
||||
}
|
||||
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
||||
if (callbacks.remove(callback) && callbacks.isEmpty()) destroyLocked()
|
||||
}
|
||||
}
|
||||
@@ -6,21 +6,14 @@ import android.net.NetworkCapabilities
|
||||
import android.net.NetworkRequest
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.debugLog
|
||||
import java.net.InetAddress
|
||||
|
||||
object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
interface Callback {
|
||||
fun onAvailable(ifname: String, dns: List<InetAddress>)
|
||||
fun onLost(ifname: String)
|
||||
}
|
||||
|
||||
object VpnMonitor : UpstreamMonitor() {
|
||||
private const val TAG = "VpnMonitor"
|
||||
|
||||
private val request = NetworkRequest.Builder()
|
||||
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
.build()
|
||||
private val callbacks = HashSet<Callback>()
|
||||
private var registered = false
|
||||
|
||||
/**
|
||||
@@ -28,65 +21,64 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
||||
*/
|
||||
private val available = HashMap<Network, String>()
|
||||
private var currentNetwork: Network? = null
|
||||
override fun onAvailable(network: Network) {
|
||||
val properties = app.connectivity.getLinkProperties(network)
|
||||
val ifname = properties?.interfaceName ?: return
|
||||
synchronized(this) {
|
||||
if (available.put(network, ifname) != null) return
|
||||
debugLog(TAG, "onAvailable: $ifname, ${properties.dnsServers.joinToString()}")
|
||||
val old = currentNetwork
|
||||
if (old != null) {
|
||||
val name = available[old]!!
|
||||
debugLog(TAG, "Assuming old VPN interface $name is dying")
|
||||
callbacks.forEach { it.onLost(name) }
|
||||
override val currentIface: String? get() {
|
||||
val currentNetwork = currentNetwork
|
||||
return if (currentNetwork == null) null else available[currentNetwork]
|
||||
}
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
val properties = app.connectivity.getLinkProperties(network)
|
||||
val ifname = properties?.interfaceName ?: return
|
||||
synchronized(this@VpnMonitor) {
|
||||
if (available.put(network, ifname) != null) return
|
||||
debugLog(TAG, "onAvailable: $ifname, ${properties.dnsServers.joinToString()}")
|
||||
val old = currentNetwork
|
||||
if (old != null) debugLog(TAG, "Assuming old VPN interface ${available[old]} is dying")
|
||||
currentNetwork = network
|
||||
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
||||
}
|
||||
currentNetwork = network
|
||||
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) = synchronized(this@VpnMonitor) {
|
||||
val ifname = available.remove(network) ?: return
|
||||
debugLog(TAG, "onLost: $ifname")
|
||||
if (currentNetwork != network) return
|
||||
while (available.isNotEmpty()) {
|
||||
val next = available.entries.first()
|
||||
currentNetwork = next.key
|
||||
val properties = app.connectivity.getLinkProperties(next.key)
|
||||
if (properties != null) {
|
||||
debugLog(TAG, "Switching to ${next.value} as VPN interface")
|
||||
callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) }
|
||||
return
|
||||
}
|
||||
available.remove(next.key)
|
||||
}
|
||||
callbacks.forEach { it.onLost() }
|
||||
currentNetwork = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) = synchronized(this) {
|
||||
val ifname = available.remove(network) ?: return
|
||||
debugLog(TAG, "onLost: $ifname")
|
||||
if (currentNetwork != network) return
|
||||
callbacks.forEach { it.onLost(ifname) }
|
||||
while (available.isNotEmpty()) {
|
||||
val next = available.entries.first()
|
||||
currentNetwork = next.key
|
||||
val properties = app.connectivity.getLinkProperties(next.key)
|
||||
if (properties != null) {
|
||||
debugLog(TAG, "Switching to ${next.value} as VPN interface")
|
||||
callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) }
|
||||
return
|
||||
}
|
||||
available.remove(next.key)
|
||||
override fun registerCallbackLocked(callback: Callback) = if (registered) {
|
||||
val currentNetwork = currentNetwork
|
||||
if (currentNetwork == null) true else {
|
||||
callback.onAvailable(available[currentNetwork]!!,
|
||||
app.connectivity.getLinkProperties(currentNetwork)?.dnsServers ?: emptyList())
|
||||
false
|
||||
}
|
||||
} else {
|
||||
app.connectivity.registerNetworkCallback(request, networkCallback)
|
||||
registered = true
|
||||
app.connectivity.allNetworks.all {
|
||||
val cap = app.connectivity.getNetworkCapabilities(it)
|
||||
cap == null || !cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
}
|
||||
currentNetwork = null
|
||||
}
|
||||
|
||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
|
||||
if (synchronized(this) {
|
||||
if (!callbacks.add(callback)) return
|
||||
if (!registered) {
|
||||
app.connectivity.registerNetworkCallback(request, this)
|
||||
registered = true
|
||||
app.connectivity.allNetworks.all {
|
||||
val cap = app.connectivity.getNetworkCapabilities(it)
|
||||
cap == null || !cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||
}
|
||||
} else if (available.isEmpty()) true else {
|
||||
available.forEach {
|
||||
callback.onAvailable(it.value,
|
||||
app.connectivity.getLinkProperties(it.key)?.dnsServers ?: emptyList())
|
||||
}
|
||||
false
|
||||
}
|
||||
}) failfast?.invoke()
|
||||
}
|
||||
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
||||
if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return
|
||||
app.connectivity.unregisterNetworkCallback(this)
|
||||
override fun destroyLocked() {
|
||||
if (!registered) return
|
||||
app.connectivity.unregisterNetworkCallback(networkCallback)
|
||||
registered = false
|
||||
available.clear()
|
||||
currentNetwork = null
|
||||
|
||||
Reference in New Issue
Block a user