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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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