@@ -37,8 +37,6 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
val iface = ifaces.singleOrNull()
|
val iface = ifaces.singleOrNull()
|
||||||
binder.iface = iface
|
binder.iface = iface
|
||||||
if (iface == null) {
|
if (iface == null) {
|
||||||
routingManager?.stop()
|
|
||||||
routingManager = null
|
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
ServiceNotification.stopForeground(this)
|
ServiceNotification.stopForeground(this)
|
||||||
stopSelf()
|
stopSelf()
|
||||||
@@ -103,6 +101,8 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun unregisterReceiver() {
|
private fun unregisterReceiver() {
|
||||||
|
routingManager?.stop()
|
||||||
|
routingManager = null
|
||||||
if (receiverRegistered) {
|
if (receiverRegistered) {
|
||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
IpNeighbourMonitor.unregisterCallback(this)
|
IpNeighbourMonitor.unregisterCallback(this)
|
||||||
|
|||||||
@@ -3,28 +3,28 @@ package be.mygod.vpnhotspot
|
|||||||
import android.widget.Toast
|
import android.widget.Toast
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
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.InetAddress
|
||||||
import java.net.SocketException
|
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 routing: Routing? = null
|
||||||
private var dns = emptyList<InetAddress>()
|
private var dns = emptyList<InetAddress>()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
app.cleanRoutings[this] = this::clean
|
app.cleanRoutings[this] = this::clean
|
||||||
VpnMonitor.registerCallback(this) { initRouting() }
|
UpstreamMonitor.registerCallback(this) { initRouting() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
val routing = routing
|
val routing = routing
|
||||||
initRouting(ifname, if (routing == null) owner else {
|
initRouting(ifname, if (routing == null) owner else {
|
||||||
routing.stop()
|
routing.stop()
|
||||||
check(routing.upstream == null)
|
|
||||||
routing.hostAddress
|
routing.hostAddress
|
||||||
}, dns)
|
}, dns)
|
||||||
}
|
}
|
||||||
override fun onLost(ifname: String) {
|
override fun onLost() {
|
||||||
val routing = routing ?: return
|
val routing = routing ?: return
|
||||||
if (!routing.stop()) app.toast(R.string.noisy_su_failure)
|
if (!routing.stop()) app.toast(R.string.noisy_su_failure)
|
||||||
initRouting(null, routing.hostAddress, emptyList())
|
initRouting(null, routing.hostAddress, emptyList())
|
||||||
@@ -54,7 +54,7 @@ class LocalOnlyInterfaceManager(val downstream: String, private val owner: InetA
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
VpnMonitor.unregisterCallback(this)
|
UpstreamMonitor.unregisterCallback(this)
|
||||||
app.cleanRoutings -= this
|
app.cleanRoutings -= this
|
||||||
if (routing?.stop() == false) app.toast(R.string.noisy_su_failure)
|
if (routing?.stop() == false) app.toast(R.string.noisy_su_failure)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
p2pManager.requestGroupInfo(channel, {
|
p2pManager.requestGroupInfo(channel, {
|
||||||
when {
|
when {
|
||||||
it == null -> doStart()
|
it == null -> doStart()
|
||||||
it.isGroupOwner -> doStart(it)
|
it.isGroupOwner -> if (routingManager == null) doStart(it)
|
||||||
else -> {
|
else -> {
|
||||||
Log.i(TAG, "Removing old group ($it)")
|
Log.i(TAG, "Removing old group ($it)")
|
||||||
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
|
||||||
@@ -220,6 +220,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
*/
|
*/
|
||||||
private fun doStart(group: WifiP2pGroup, ownerAddress: InetAddress? = null) {
|
private fun doStart(group: WifiP2pGroup, ownerAddress: InetAddress? = null) {
|
||||||
this.group = group
|
this.group = group
|
||||||
|
check(routingManager == null)
|
||||||
routingManager = LocalOnlyInterfaceManager(group.`interface`!!, ownerAddress)
|
routingManager = LocalOnlyInterfaceManager(group.`interface`!!, ownerAddress)
|
||||||
status = Status.ACTIVE
|
status = Status.ACTIVE
|
||||||
showNotification(group)
|
showNotification(group)
|
||||||
|
|||||||
@@ -8,12 +8,12 @@ import be.mygod.vpnhotspot.manage.TetheringFragment
|
|||||||
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.Routing
|
import be.mygod.vpnhotspot.net.Routing
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
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 be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.net.SocketException
|
import java.net.SocketException
|
||||||
|
|
||||||
class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback {
|
||||||
companion object {
|
companion object {
|
||||||
const val EXTRA_ADD_INTERFACE = "interface.add"
|
const val EXTRA_ADD_INTERFACE = "interface.add"
|
||||||
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
const val EXTRA_REMOVE_INTERFACE = "interface.remove"
|
||||||
@@ -45,18 +45,20 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
|||||||
val upstream = upstream
|
val upstream = upstream
|
||||||
if (upstream != null) {
|
if (upstream != null) {
|
||||||
var failed = false
|
var failed = false
|
||||||
for ((downstream, value) in routings) if (value == null) try {
|
for ((downstream, value) in routings) if (value == null || value.upstream != upstream)
|
||||||
// system tethering already has working forwarding rules
|
try {
|
||||||
// so it doesn't make sense to add additional forwarding rules
|
if (value?.stop() == false) failed = true
|
||||||
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
// system tethering already has working forwarding rules
|
||||||
if (app.pref.getBoolean("service.disableIpv6", false)) routing.disableIpv6()
|
// so it doesn't make sense to add additional forwarding rules
|
||||||
routings[downstream] = routing
|
val routing = Routing(upstream, downstream).rule().forward().masquerade().dnsRedirect(dns)
|
||||||
if (!routing.start()) failed = true
|
if (app.pref.getBoolean("service.disableIpv6", false)) routing.disableIpv6()
|
||||||
} catch (e: SocketException) {
|
routings[downstream] = routing
|
||||||
e.printStackTrace()
|
if (!routing.start()) failed = true
|
||||||
routings.remove(downstream)
|
} catch (e: SocketException) {
|
||||||
failed = true
|
e.printStackTrace()
|
||||||
}
|
routings.remove(downstream)
|
||||||
|
failed = true
|
||||||
|
}
|
||||||
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
if (failed) Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
|
||||||
} else if (!receiverRegistered) {
|
} else if (!receiverRegistered) {
|
||||||
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
@@ -67,7 +69,7 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
IpNeighbourMonitor.registerCallback(this)
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
VpnMonitor.registerCallback(this)
|
UpstreamMonitor.registerCallback(this)
|
||||||
receiverRegistered = true
|
receiverRegistered = true
|
||||||
}
|
}
|
||||||
updateNotification()
|
updateNotification()
|
||||||
@@ -94,14 +96,13 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
check(upstream == null || upstream == ifname)
|
if (upstream == ifname) return
|
||||||
upstream = ifname
|
upstream = ifname
|
||||||
this.dns = dns
|
this.dns = dns
|
||||||
synchronized(routings) { updateRoutingsLocked() }
|
synchronized(routings) { updateRoutingsLocked() }
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onLost(ifname: String) {
|
override fun onLost() {
|
||||||
check(upstream == null || upstream == ifname)
|
|
||||||
upstream = null
|
upstream = null
|
||||||
this.dns = emptyList()
|
this.dns = emptyList()
|
||||||
var failed = false
|
var failed = false
|
||||||
@@ -124,7 +125,7 @@ class TetheringService : IpNeighbourMonitoringService(), VpnMonitor.Callback {
|
|||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
app.cleanRoutings -= this
|
app.cleanRoutings -= this
|
||||||
IpNeighbourMonitor.unregisterCallback(this)
|
IpNeighbourMonitor.unregisterCallback(this)
|
||||||
VpnMonitor.unregisterCallback(this)
|
UpstreamMonitor.unregisterCallback(this)
|
||||||
upstream = null
|
upstream = null
|
||||||
receiverRegistered = false
|
receiverRegistered = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,10 +59,8 @@ class ClientsFragment : Fragment(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
val clients = clients
|
val clients = clients ?: return
|
||||||
if (clients != null) {
|
clients.clientsChanged -= this
|
||||||
clients.clientsChanged -= this
|
this.clients = null
|
||||||
this.clients = null
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,9 +115,8 @@ class TetheringFragment : Fragment(), ServiceConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
val context = requireContext()
|
(tetheringBinder ?: return).fragment = null
|
||||||
tetheringBinder?.fragment = null
|
|
||||||
tetheringBinder = 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
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
import android.os.Handler
|
|
||||||
import android.util.Log
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
|
||||||
import be.mygod.vpnhotspot.util.debugLog
|
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 {
|
companion object {
|
||||||
private const val TAG = "IpNeighbourMonitor"
|
|
||||||
private val callbacks = HashSet<Callback>()
|
private val callbacks = HashSet<Callback>()
|
||||||
var instance: IpNeighbourMonitor? = null
|
var instance: IpNeighbourMonitor? = null
|
||||||
|
|
||||||
@@ -39,75 +30,37 @@ class IpNeighbourMonitor private constructor() : Runnable {
|
|||||||
fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>)
|
fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val handler = Handler()
|
|
||||||
private var updatePosted = false
|
private var updatePosted = false
|
||||||
val neighbours = HashMap<String, IpNeighbour>()
|
val neighbours = HashMap<String, IpNeighbour>()
|
||||||
private var monitor: Process? = null
|
|
||||||
private var pool: ScheduledExecutorService? = null
|
|
||||||
|
|
||||||
init {
|
override val monitoredObject: String get() = "neigh"
|
||||||
thread("$TAG-input") {
|
|
||||||
// monitor may get rejected by SELinux
|
override fun processLine(line: String) {
|
||||||
val monitor = ProcessBuilder("sh", "-c", "ip monitor neigh || su -c 'ip monitor neigh'")
|
synchronized(neighbours) {
|
||||||
.redirectErrorStream(true)
|
val neighbour = IpNeighbour.parse(line) ?: return
|
||||||
.start()
|
debugLog(javaClass.simpleName, line)
|
||||||
this.monitor = monitor
|
val changed = if (neighbour.state == IpNeighbour.State.DELETING)
|
||||||
thread("$TAG-error") {
|
neighbours.remove(neighbour.ip) != null
|
||||||
try {
|
else neighbours.put(neighbour.ip, neighbour) != neighbour
|
||||||
monitor.errorStream.bufferedReader().forEachLine { Log.e(TAG, it) }
|
if (changed) postUpdateLocked()
|
||||||
} 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) { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun flush() = thread("$TAG-flush") { run() }
|
override fun processLines(lines: Sequence<String>) {
|
||||||
|
synchronized(neighbours) {
|
||||||
override fun run() {
|
neighbours.clear()
|
||||||
val process = ProcessBuilder("ip", "neigh")
|
neighbours.putAll(lines
|
||||||
.redirectErrorStream(true)
|
.map(IpNeighbour.Companion::parse)
|
||||||
.start()
|
.filterNotNull()
|
||||||
process.waitFor()
|
.filter { it.state != IpNeighbour.State.DELETING } // skip entries without lladdr
|
||||||
thread("$TAG-flush-error") {
|
.associateBy { it.ip })
|
||||||
val err = process.errorStream.bufferedReader().readText()
|
postUpdateLocked()
|
||||||
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()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun postUpdateLocked() {
|
private fun postUpdateLocked() {
|
||||||
if (updatePosted || instance != this) return
|
if (updatePosted || instance != this) return
|
||||||
handler.post {
|
app.handler.post {
|
||||||
val neighbours = synchronized(neighbours) {
|
val neighbours = synchronized(neighbours) {
|
||||||
updatePosted = false
|
updatePosted = false
|
||||||
neighbours.values.toList()
|
neighbours.values.toList()
|
||||||
@@ -116,10 +69,4 @@ class IpNeighbourMonitor private constructor() : Runnable {
|
|||||||
}
|
}
|
||||||
updatePosted = true
|
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 android.net.NetworkRequest
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.util.debugLog
|
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 const val TAG = "VpnMonitor"
|
||||||
|
|
||||||
private val request = NetworkRequest.Builder()
|
private val request = NetworkRequest.Builder()
|
||||||
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
.addTransportType(NetworkCapabilities.TRANSPORT_VPN)
|
||||||
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
.removeCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
.build()
|
.build()
|
||||||
private val callbacks = HashSet<Callback>()
|
|
||||||
private var registered = false
|
private var registered = false
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,65 +21,64 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
|
|||||||
*/
|
*/
|
||||||
private val available = HashMap<Network, String>()
|
private val available = HashMap<Network, String>()
|
||||||
private var currentNetwork: Network? = null
|
private var currentNetwork: Network? = null
|
||||||
override fun onAvailable(network: Network) {
|
override val currentIface: String? get() {
|
||||||
val properties = app.connectivity.getLinkProperties(network)
|
val currentNetwork = currentNetwork
|
||||||
val ifname = properties?.interfaceName ?: return
|
return if (currentNetwork == null) null else available[currentNetwork]
|
||||||
synchronized(this) {
|
}
|
||||||
if (available.put(network, ifname) != null) return
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
debugLog(TAG, "onAvailable: $ifname, ${properties.dnsServers.joinToString()}")
|
override fun onAvailable(network: Network) {
|
||||||
val old = currentNetwork
|
val properties = app.connectivity.getLinkProperties(network)
|
||||||
if (old != null) {
|
val ifname = properties?.interfaceName ?: return
|
||||||
val name = available[old]!!
|
synchronized(this@VpnMonitor) {
|
||||||
debugLog(TAG, "Assuming old VPN interface $name is dying")
|
if (available.put(network, ifname) != null) return
|
||||||
callbacks.forEach { it.onLost(name) }
|
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) {
|
override fun registerCallbackLocked(callback: Callback) = if (registered) {
|
||||||
val ifname = available.remove(network) ?: return
|
val currentNetwork = currentNetwork
|
||||||
debugLog(TAG, "onLost: $ifname")
|
if (currentNetwork == null) true else {
|
||||||
if (currentNetwork != network) return
|
callback.onAvailable(available[currentNetwork]!!,
|
||||||
callbacks.forEach { it.onLost(ifname) }
|
app.connectivity.getLinkProperties(currentNetwork)?.dnsServers ?: emptyList())
|
||||||
while (available.isNotEmpty()) {
|
false
|
||||||
val next = available.entries.first()
|
}
|
||||||
currentNetwork = next.key
|
} else {
|
||||||
val properties = app.connectivity.getLinkProperties(next.key)
|
app.connectivity.registerNetworkCallback(request, networkCallback)
|
||||||
if (properties != null) {
|
registered = true
|
||||||
debugLog(TAG, "Switching to ${next.value} as VPN interface")
|
app.connectivity.allNetworks.all {
|
||||||
callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) }
|
val cap = app.connectivity.getNetworkCapabilities(it)
|
||||||
return
|
cap == null || !cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
|
||||||
}
|
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
|
||||||
available.remove(next.key)
|
|
||||||
}
|
}
|
||||||
currentNetwork = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
|
override fun destroyLocked() {
|
||||||
if (synchronized(this) {
|
if (!registered) return
|
||||||
if (!callbacks.add(callback)) return
|
app.connectivity.unregisterNetworkCallback(networkCallback)
|
||||||
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)
|
|
||||||
registered = false
|
registered = false
|
||||||
available.clear()
|
available.clear()
|
||||||
currentNetwork = null
|
currentNetwork = null
|
||||||
|
|||||||
@@ -63,6 +63,8 @@
|
|||||||
<string name="settings_service_disable_ipv6">禁用 IPv6 共享</string>
|
<string name="settings_service_disable_ipv6">禁用 IPv6 共享</string>
|
||||||
<string name="settings_service_disable_ipv6_summary">防止 IPv6 VPN 泄漏。</string>
|
<string name="settings_service_disable_ipv6_summary">防止 IPv6 VPN 泄漏。</string>
|
||||||
<string name="settings_service_dns">备用 DNS 服务器[:端口]</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_service_clean">清理/重新应用路由规则</string>
|
||||||
<string name="settings_misc">杂项</string>
|
<string name="settings_misc">杂项</string>
|
||||||
<string name="settings_misc_logcat">导出调试信息</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">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_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_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_service_clean">Clean/reapply routing rules</string>
|
||||||
<string name="settings_misc">Misc</string>
|
<string name="settings_misc">Misc</string>
|
||||||
<string name="settings_misc_logcat">Export debug information</string>
|
<string name="settings_misc_logcat">Export debug information</string>
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/settings_service">
|
android:title="@string/settings_service">
|
||||||
|
<Preference
|
||||||
|
android:key="service.clean"
|
||||||
|
android:title="@string/settings_service_clean"/>
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="service.repeater.strict"
|
android:key="service.repeater.strict"
|
||||||
android:title="@string/settings_service_repeater_strict"
|
android:title="@string/settings_service_repeater_strict"
|
||||||
@@ -15,9 +18,12 @@
|
|||||||
android:title="@string/settings_service_dns"
|
android:title="@string/settings_service_dns"
|
||||||
android:singleLine="true"
|
android:singleLine="true"
|
||||||
android:defaultValue="8.8.8.8"/>
|
android:defaultValue="8.8.8.8"/>
|
||||||
<Preference
|
<AutoSummaryEditTextPreference
|
||||||
android:key="service.clean"
|
android:key="service.upstream"
|
||||||
android:title="@string/settings_service_clean"/>
|
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>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/settings_misc">
|
android:title="@string/settings_misc">
|
||||||
|
|||||||
Reference in New Issue
Block a user