@@ -37,8 +37,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
||||
val iface = ifaces.singleOrNull()
|
||||
binder.iface = iface
|
||||
if (iface == null) {
|
||||
routingManager?.stop()
|
||||
routingManager = null
|
||||
unregisterReceiver()
|
||||
ServiceNotification.stopForeground(this)
|
||||
stopSelf()
|
||||
@@ -103,6 +101,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
||||
}
|
||||
|
||||
private fun unregisterReceiver() {
|
||||
routingManager?.stop()
|
||||
routingManager = null
|
||||
if (receiverRegistered) {
|
||||
unregisterReceiver(receiver)
|
||||
IpNeighbourMonitor.unregisterCallback(this)
|
||||
|
||||
@@ -3,28 +3,28 @@ package be.mygod.vpnhotspot
|
||||
import android.widget.Toast
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.net.VpnMonitor
|
||||
import be.mygod.vpnhotspot.net.UpstreamMonitor
|
||||
import java.net.InetAddress
|
||||
import java.net.SocketException
|
||||
|
||||
class LocalOnlyInterfaceManager(val downstream: String, private val owner: InetAddress? = null) : VpnMonitor.Callback {
|
||||
class LocalOnlyInterfaceManager(val downstream: String, private val owner: InetAddress? = null) :
|
||||
UpstreamMonitor.Callback {
|
||||
private var routing: Routing? = null
|
||||
private var dns = emptyList<InetAddress>()
|
||||
|
||||
init {
|
||||
app.cleanRoutings[this] = this::clean
|
||||
VpnMonitor.registerCallback(this) { initRouting() }
|
||||
UpstreamMonitor.registerCallback(this) { initRouting() }
|
||||
}
|
||||
|
||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||
val routing = routing
|
||||
initRouting(ifname, if (routing == null) owner else {
|
||||
routing.stop()
|
||||
check(routing.upstream == null)
|
||||
routing.hostAddress
|
||||
}, dns)
|
||||
}
|
||||
override fun onLost(ifname: String) {
|
||||
override fun onLost() {
|
||||
val routing = routing ?: return
|
||||
if (!routing.stop()) app.toast(R.string.noisy_su_failure)
|
||||
initRouting(null, routing.hostAddress, emptyList())
|
||||
@@ -54,7 +54,7 @@ class LocalOnlyInterfaceManager(val downstream: String, private val owner: InetA
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
VpnMonitor.unregisterCallback(this)
|
||||
UpstreamMonitor.unregisterCallback(this)
|
||||
app.cleanRoutings -= this
|
||||
if (routing?.stop() == false) app.toast(R.string.noisy_su_failure)
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
p2pManager.requestGroupInfo(channel, {
|
||||
when {
|
||||
it == null -> doStart()
|
||||
it.isGroupOwner -> doStart(it)
|
||||
it.isGroupOwner -> if (routingManager == null) doStart(it)
|
||||
else -> {
|
||||
Log.i(TAG, "Removing old group ($it)")
|
||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||
@@ -220,6 +220,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
||||
*/
|
||||
private fun doStart(group: WifiP2pGroup, ownerAddress: InetAddress? = null) {
|
||||
this.group = group
|
||||
check(routingManager == null)
|
||||
routingManager = LocalOnlyInterfaceManager(group.`interface`!!, ownerAddress)
|
||||
status = Status.ACTIVE
|
||||
showNotification(group)
|
||||
|
||||
@@ -8,12 +8,12 @@ import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.Routing
|
||||
import be.mygod.vpnhotspot.net.TetheringManager
|
||||
import be.mygod.vpnhotspot.net.VpnMonitor
|
||||
import be.mygod.vpnhotspot.net.UpstreamMonitor
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import java.net.InetAddress
|
||||
import java.net.SocketException
|
||||
|
||||
class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||
class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback {
|
||||
companion object {
|
||||
const val EXTRA_ADD_INTERFACE = "interface.add"
|
||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||
@@ -45,18 +45,20 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||
val upstream = upstream
|
||||
if (upstream != null) {
|
||||
var failed = false
|
||||
for ((downstream, value) in routings) if (value == null) try {
|
||||
// system tethering already has working forwarding rules
|
||||
// so it doesn't make sense to add additional forwarding rules
|
||||
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
||||
if (app.pref.getBoolean("service.disableIpv6", false)) routing.disableIpv6()
|
||||
routings[downstream] = routing
|
||||
if (!routing.start()) failed = true
|
||||
} catch (e: SocketException) {
|
||||
e.printStackTrace()
|
||||
routings.remove(downstream)
|
||||
failed = true
|
||||
}
|
||||
for ((downstream, value) in routings) if (value == null || value.upstream != upstream)
|
||||
try {
|
||||
if (value?.stop() == false) failed = true
|
||||
// system tethering already has working forwarding rules
|
||||
// so it doesn't make sense to add additional forwarding rules
|
||||
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
||||
if (app.pref.getBoolean("service.disableIpv6", false)) routing.disableIpv6()
|
||||
routings[downstream] = routing
|
||||
if (!routing.start()) failed = true
|
||||
} catch (e: SocketException) {
|
||||
e.printStackTrace()
|
||||
routings.remove(downstream)
|
||||
failed = true
|
||||
}
|
||||
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||
} else if (!receiverRegistered) {
|
||||
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||
@@ -67,7 +69,7 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||
}
|
||||
}
|
||||
IpNeighbourMonitor.registerCallback(this)
|
||||
VpnMonitor.registerCallback(this)
|
||||
UpstreamMonitor.registerCallback(this)
|
||||
receiverRegistered = true
|
||||
}
|
||||
updateNotification()
|
||||
@@ -94,14 +96,13 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||
}
|
||||
|
||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||
check(upstream == null || upstream == ifname)
|
||||
if (upstream == ifname) return
|
||||
upstream = ifname
|
||||
this.dns = dns
|
||||
synchronized(routings) { updateRoutingsLocked() }
|
||||
}
|
||||
|
||||
override fun onLost(ifname: String) {
|
||||
check(upstream == null || upstream == ifname)
|
||||
override fun onLost() {
|
||||
upstream = null
|
||||
this.dns = emptyList()
|
||||
var failed = false
|
||||
@@ -124,7 +125,7 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
||||
unregisterReceiver(receiver)
|
||||
app.cleanRoutings -= this
|
||||
IpNeighbourMonitor.unregisterCallback(this)
|
||||
VpnMonitor.unregisterCallback(this)
|
||||
UpstreamMonitor.unregisterCallback(this)
|
||||
upstream = null
|
||||
receiverRegistered = false
|
||||
}
|
||||
|
||||
@@ -59,10 +59,8 @@ class ClientsFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
val clients = clients
|
||||
if (clients != null) {
|
||||
clients.clientsChanged -= this
|
||||
this.clients = null
|
||||
}
|
||||
val clients = clients ?: return
|
||||
clients.clientsChanged -= this
|
||||
this.clients = null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,9 +115,8 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
val context = requireContext()
|
||||
tetheringBinder?.fragment = null
|
||||
(tetheringBinder ?: return).fragment = null
|
||||
tetheringBinder = null
|
||||
context.unregisterReceiver(receiver)
|
||||
requireContext().unregisterReceiver(receiver)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -63,6 +63,8 @@
|
||||
<string name="settings_service_disable_ipv6">禁用 IPv6 共享</string>
|
||||
<string name="settings_service_disable_ipv6_summary">防止 IPv6 VPN 泄漏。</string>
|
||||
<string name="settings_service_dns">备用 DNS 服务器[:端口]</string>
|
||||
<string name="settings_service_upstream">上游网络接口</string>
|
||||
<string name="settings_service_upstream_auto">自动检测系统 VPN</string>
|
||||
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
||||
<string name="settings_misc">杂项</string>
|
||||
<string name="settings_misc_logcat">导出调试信息</string>
|
||||
|
||||
@@ -67,6 +67,8 @@
|
||||
<string name="settings_service_disable_ipv6">Disable IPv6 tethering</string>
|
||||
<string name="settings_service_disable_ipv6_summary">Enabling this option will prevent VPN leaks via IPv6.</string>
|
||||
<string name="settings_service_dns">Fallback DNS server[:port]</string>
|
||||
<string name="settings_service_upstream">Upstream network interface</string>
|
||||
<string name="settings_service_upstream_auto">Auto detect system VPN</string>
|
||||
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
||||
<string name="settings_misc">Misc</string>
|
||||
<string name="settings_misc_logcat">Export debug information</string>
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<PreferenceCategory
|
||||
android:title="@string/settings_service">
|
||||
<Preference
|
||||
android:key="service.clean"
|
||||
android:title="@string/settings_service_clean"/>
|
||||
<SwitchPreference
|
||||
android:key="service.repeater.strict"
|
||||
android:title="@string/settings_service_repeater_strict"
|
||||
@@ -15,9 +18,12 @@
|
||||
android:title="@string/settings_service_dns"
|
||||
android:singleLine="true"
|
||||
android:defaultValue="8.8.8.8"/>
|
||||
<Preference
|
||||
android:key="service.clean"
|
||||
android:title="@string/settings_service_clean"/>
|
||||
<AutoSummaryEditTextPreference
|
||||
android:key="service.upstream"
|
||||
android:title="@string/settings_service_upstream"
|
||||
android:summary="@string/settings_service_upstream_auto"
|
||||
android:hint="@string/settings_service_upstream_auto"
|
||||
android:singleLine="true"/>
|
||||
</PreferenceCategory>
|
||||
<PreferenceCategory
|
||||
android:title="@string/settings_misc">
|
||||
|
||||
Reference in New Issue
Block a user