Support specifying network interface

Fix #15.
This commit is contained in:
Mygod
2018-06-02 07:29:46 +08:00
parent 8e335fec1b
commit da9bf4867e
14 changed files with 326 additions and 173 deletions

View File

@@ -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()
}
}

View 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()
}
}

View File

@@ -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()
}
}

View File

@@ -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()
}
}

View File

@@ -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