Replace strict mode with fallback upstream interface
Fixes #40. Apparently we can no longer take advantage of default network rules set by Android system since Android 9.0 thanks to this commit: 758627c4d9
This commit is contained in:
@@ -56,7 +56,6 @@ class App : Application() {
|
|||||||
return if (result in 1..165) result else 0
|
return if (result in 1..165) result else 0
|
||||||
}
|
}
|
||||||
val masquerade get() = pref.getBoolean("service.masquerade", true)
|
val masquerade get() = pref.getBoolean("service.masquerade", true)
|
||||||
val strict get() = app.pref.getBoolean("service.repeater.strict", false)
|
|
||||||
val dhcpWorkaround get() = pref.getBoolean("service.dhcpWorkaround", false)
|
val dhcpWorkaround get() = pref.getBoolean("service.dhcpWorkaround", false)
|
||||||
|
|
||||||
val cleanRoutings = Event0()
|
val cleanRoutings = Event0()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.app.Service
|
import android.app.Service
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
|
|
||||||
abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
|
abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService() {
|
|||||||
} else {
|
} else {
|
||||||
val routingManager = routingManager
|
val routingManager = routingManager
|
||||||
if (routingManager == null) {
|
if (routingManager == null) {
|
||||||
this.routingManager = LocalOnlyInterfaceManager(this, iface)
|
this.routingManager = LocalOnlyInterfaceManager(iface)
|
||||||
IpNeighbourMonitor.registerCallback(this)
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
} else check(iface == routingManager.downstream)
|
} else check(iface == routingManager.downstream)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,33 @@
|
|||||||
package be.mygod.vpnhotspot
|
package be.mygod.vpnhotspot
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
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.monitor.UpstreamMonitor
|
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
|
||||||
import java.net.InterfaceAddress
|
import java.net.InterfaceAddress
|
||||||
|
|
||||||
class LocalOnlyInterfaceManager(private val owner: Context, val downstream: String) : UpstreamMonitor.Callback {
|
class LocalOnlyInterfaceManager(val downstream: String) {
|
||||||
private var routing: Routing? = null
|
private var routing: Routing? = null
|
||||||
private var dns = emptyList<InetAddress>()
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
app.cleanRoutings[this] = this::clean
|
app.cleanRoutings[this] = this::clean
|
||||||
UpstreamMonitor.registerCallback(this) { initRouting() }
|
initRouting()
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
|
||||||
val routing = routing
|
|
||||||
initRouting(ifname, if (routing == null) null else {
|
|
||||||
routing.revert()
|
|
||||||
routing.hostAddress
|
|
||||||
}, dns)
|
|
||||||
}
|
|
||||||
override fun onLost() {
|
|
||||||
val routing = routing ?: return
|
|
||||||
routing.revert()
|
|
||||||
initRouting(null, routing.hostAddress, emptyList())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clean() {
|
private fun clean() {
|
||||||
val routing = routing ?: return
|
val routing = routing ?: return
|
||||||
routing.stop()
|
routing.stop()
|
||||||
initRouting(routing.upstream, routing.hostAddress, dns)
|
initRouting(routing.hostAddress)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initRouting(upstream: String? = null, owner: InterfaceAddress? = null,
|
private fun initRouting(owner: InterfaceAddress? = null) {
|
||||||
dns: List<InetAddress> = this.dns) {
|
routing = try {
|
||||||
this.dns = dns
|
Routing(downstream, owner).apply {
|
||||||
try {
|
|
||||||
routing = Routing(this.owner, upstream, downstream, owner, app.strict).apply {
|
|
||||||
try {
|
try {
|
||||||
if (app.dhcpWorkaround) dhcpWorkaround()
|
if (app.dhcpWorkaround) dhcpWorkaround()
|
||||||
ipForward() // local only interfaces need to enable ip_forward
|
ipForward() // local only interfaces need to enable ip_forward
|
||||||
rule()
|
|
||||||
forward()
|
forward()
|
||||||
if (app.masquerade) masquerade()
|
if (app.masquerade) masquerade()
|
||||||
dnsRedirect(dns)
|
|
||||||
commit()
|
commit()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
revert()
|
revert()
|
||||||
@@ -58,12 +37,11 @@ class LocalOnlyInterfaceManager(private val owner: Context, val downstream: Stri
|
|||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
SmartSnackbar.make(e.localizedMessage).show()
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
routing = null
|
null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun stop() {
|
fun stop() {
|
||||||
UpstreamMonitor.unregisterCallback(this)
|
|
||||||
app.cleanRoutings -= this
|
app.cleanRoutings -= this
|
||||||
routing?.revert()
|
routing?.revert()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,7 +232,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
|
|||||||
private fun doStart(group: WifiP2pGroup) {
|
private fun doStart(group: WifiP2pGroup) {
|
||||||
this.group = group
|
this.group = group
|
||||||
check(routingManager == null)
|
check(routingManager == null)
|
||||||
routingManager = LocalOnlyInterfaceManager(this, group.`interface`!!)
|
routingManager = LocalOnlyInterfaceManager(group.`interface`!!)
|
||||||
status = Status.ACTIVE
|
status = Status.ACTIVE
|
||||||
showNotification(group)
|
showNotification(group)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import androidx.preference.Preference
|
|||||||
import androidx.preference.SwitchPreference
|
import androidx.preference.SwitchPreference
|
||||||
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.monitor.FallbackUpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
|
||||||
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
|
||||||
@@ -120,8 +121,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
|
override fun onDisplayPreferenceDialog(preference: Preference) = when (preference.key) {
|
||||||
UpstreamMonitor.KEY -> displayPreferenceDialog(
|
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY -> displayPreferenceDialog(
|
||||||
AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat(), UpstreamMonitor.KEY,
|
AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat(), preference.key,
|
||||||
bundleOf(Pair(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS,
|
bundleOf(Pair(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS,
|
||||||
try {
|
try {
|
||||||
NetworkInterface.getNetworkInterfaces().asSequence()
|
NetworkInterface.getNetworkInterfaces().asSequence()
|
||||||
|
|||||||
@@ -4,16 +4,14 @@ import android.content.Intent
|
|||||||
import android.content.IntentFilter
|
import android.content.IntentFilter
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.manage.TetheringFragment
|
import be.mygod.vpnhotspot.manage.TetheringFragment
|
||||||
import be.mygod.vpnhotspot.net.monitor.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.monitor.UpstreamMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
|
||||||
|
|
||||||
class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback {
|
class TetheringService : IpNeighbourMonitoringService() {
|
||||||
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"
|
||||||
@@ -27,8 +25,6 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
|
|
||||||
private val binder = Binder()
|
private val binder = Binder()
|
||||||
private val routings = HashMap<String, Routing?>()
|
private val routings = HashMap<String, Routing?>()
|
||||||
private var upstream: String? = null
|
|
||||||
private var dns: List<InetAddress> = emptyList()
|
|
||||||
private var receiverRegistered = false
|
private var receiverRegistered = false
|
||||||
private val receiver = broadcastReceiver { _, intent ->
|
private val receiver = broadcastReceiver { _, intent ->
|
||||||
val extras = intent.extras ?: return@broadcastReceiver
|
val extras = intent.extras ?: return@broadcastReceiver
|
||||||
@@ -51,43 +47,36 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
registerReceiver(receiver, IntentFilter(TetheringManager.ACTION_TETHER_STATE_CHANGED))
|
||||||
app.cleanRoutings[this] = {
|
app.cleanRoutings[this] = {
|
||||||
synchronized(routings) {
|
synchronized(routings) {
|
||||||
for (iface in routings.keys) routings[iface] = null
|
for (iface in routings.keys) routings.put(iface, null)?.stop()
|
||||||
updateRoutingsLocked()
|
updateRoutingsLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
IpNeighbourMonitor.registerCallback(this)
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
UpstreamMonitor.registerCallback(this)
|
|
||||||
}
|
}
|
||||||
val upstream = upstream
|
|
||||||
val disableIpv6 = app.pref.getBoolean("service.disableIpv6", false)
|
val disableIpv6 = app.pref.getBoolean("service.disableIpv6", false)
|
||||||
if (upstream != null || app.strict || disableIpv6) {
|
val iterator = routings.iterator()
|
||||||
val iterator = routings.iterator()
|
while (iterator.hasNext()) {
|
||||||
while (iterator.hasNext()) {
|
val (downstream, value) = iterator.next()
|
||||||
val (downstream, value) = iterator.next()
|
if (value != null) continue
|
||||||
if (value != null) if (value.upstream == upstream) continue else value.revert()
|
try {
|
||||||
try {
|
routings[downstream] = Routing(downstream).apply {
|
||||||
routings[downstream] = Routing(this, upstream, downstream).apply {
|
try {
|
||||||
try {
|
if (app.dhcpWorkaround) dhcpWorkaround()
|
||||||
if (app.dhcpWorkaround) dhcpWorkaround()
|
// system tethering already has working forwarding rules
|
||||||
// system tethering already has working forwarding rules
|
// so it doesn't make sense to add additional forwarding rules
|
||||||
// so it doesn't make sense to add additional forwarding rules
|
forward()
|
||||||
rule()
|
if (app.masquerade) masquerade()
|
||||||
// here we always enforce strict mode as fallback is handled by system which we disable
|
if (disableIpv6) disableIpv6()
|
||||||
forward()
|
commit()
|
||||||
if (app.masquerade) masquerade()
|
} catch (e: Exception) {
|
||||||
if (upstream != null) dnsRedirect(dns)
|
revert()
|
||||||
if (disableIpv6) disableIpv6()
|
throw e
|
||||||
commit()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revert()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.w(e)
|
|
||||||
SmartSnackbar.make(e.localizedMessage).show()
|
|
||||||
iterator.remove()
|
|
||||||
}
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
iterator.remove()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (routings.isEmpty()) {
|
if (routings.isEmpty()) {
|
||||||
@@ -113,24 +102,6 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
return START_NOT_STICKY
|
return START_NOT_STICKY
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
|
||||||
if (upstream == ifname) return
|
|
||||||
upstream = ifname
|
|
||||||
this.dns = dns
|
|
||||||
synchronized(routings) { updateRoutingsLocked() }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onLost() {
|
|
||||||
upstream = null
|
|
||||||
this.dns = emptyList()
|
|
||||||
synchronized(routings) {
|
|
||||||
for ((iface, routing) in routings) {
|
|
||||||
routing?.revert()
|
|
||||||
routings[iface] = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
unregisterReceiver()
|
unregisterReceiver()
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
@@ -141,8 +112,6 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
|
|||||||
unregisterReceiver(receiver)
|
unregisterReceiver(receiver)
|
||||||
app.cleanRoutings -= this
|
app.cleanRoutings -= this
|
||||||
IpNeighbourMonitor.unregisterCallback(this)
|
IpNeighbourMonitor.unregisterCallback(this)
|
||||||
UpstreamMonitor.unregisterCallback(this)
|
|
||||||
upstream = null
|
|
||||||
receiverRegistered = false
|
receiverRegistered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import androidx.recyclerview.widget.DiffUtil
|
|||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.net.InetAddressComparator
|
import be.mygod.vpnhotspot.net.InetAddressComparator
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import be.mygod.vpnhotspot.room.AppDatabase
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
import be.mygod.vpnhotspot.room.lookup
|
import be.mygod.vpnhotspot.room.lookup
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import android.content.*
|
|||||||
import android.net.wifi.p2p.WifiP2pDevice
|
import android.net.wifi.p2p.WifiP2pDevice
|
||||||
import android.os.IBinder
|
import android.os.IBinder
|
||||||
import be.mygod.vpnhotspot.RepeaterService
|
import be.mygod.vpnhotspot.RepeaterService
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
import be.mygod.vpnhotspot.net.TetheringManager
|
import be.mygod.vpnhotspot.net.TetheringManager
|
||||||
import be.mygod.vpnhotspot.util.StickyEvent1
|
import be.mygod.vpnhotspot.util.StickyEvent1
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.client
|
package be.mygod.vpnhotspot.client
|
||||||
|
|
||||||
import be.mygod.vpnhotspot.net.monitor.IpNeighbour
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
|
|
||||||
class TetheringClient(private val neighbour: IpNeighbour) : Client() {
|
class TetheringClient(private val neighbour: IpNeighbour) : Client() {
|
||||||
override val iface get() = neighbour.dev
|
override val iface get() = neighbour.dev
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
import be.mygod.vpnhotspot.util.parseNumericAddressNoThrow
|
import be.mygod.vpnhotspot.util.parseNumericAddressNoThrow
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
@@ -1,22 +1,15 @@
|
|||||||
package be.mygod.vpnhotspot.net
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
import android.content.ComponentName
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.Intent
|
|
||||||
import android.content.ServiceConnection
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.IBinder
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.R
|
import be.mygod.vpnhotspot.R
|
||||||
import be.mygod.vpnhotspot.client.Client
|
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.client.ClientMonitorService
|
|
||||||
import be.mygod.vpnhotspot.debugLog
|
|
||||||
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
||||||
|
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.util.computeIfAbsentCompat
|
|
||||||
import be.mygod.vpnhotspot.util.stopAndUnbind
|
|
||||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import java.lang.RuntimeException
|
||||||
import java.net.*
|
import java.net.*
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -24,9 +17,17 @@ import java.net.*
|
|||||||
*
|
*
|
||||||
* Once revert is called, this object no longer serves any purpose.
|
* Once revert is called, this object no longer serves any purpose.
|
||||||
*/
|
*/
|
||||||
class Routing(private val owner: Context, val upstream: String?, private val downstream: String,
|
class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
|
||||||
ownerAddress: InterfaceAddress? = null, private val strict: Boolean = true) : ServiceConnection {
|
|
||||||
companion object {
|
companion object {
|
||||||
|
/**
|
||||||
|
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
|
||||||
|
* This also works for Wi-Fi direct where there's no rule at 18000.
|
||||||
|
*
|
||||||
|
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65
|
||||||
|
*/
|
||||||
|
private const val RULE_PRIORITY_UPSTREAM = 17800
|
||||||
|
private const val RULE_PRIORITY_UPSTREAM_FALLBACK = 17900
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* -w <seconds> is not supported on 7.1-.
|
* -w <seconds> is not supported on 7.1-.
|
||||||
* Fortunately there also isn't a time limit for starting a foreground service back in 7.1-.
|
* Fortunately there also isn't a time limit for starting a foreground service back in 7.1-.
|
||||||
@@ -45,7 +46,8 @@ class Routing(private val owner: Context, val upstream: String?, private val dow
|
|||||||
it.submit("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
it.submit("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
||||||
it.submit("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
it.submit("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
||||||
it.submit("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
it.submit("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
||||||
it.submit("while ip rule del priority 17900; do done")
|
it.submit("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
||||||
|
it.submit("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
||||||
it.submit("while ip rule del iif lo uidrange 0-0 lookup local_network priority 11000; do done")
|
it.submit("while ip rule del iif lo uidrange 0-0 lookup local_network priority 11000; do done")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -62,56 +64,107 @@ class Routing(private val owner: Context, val upstream: String?, private val dow
|
|||||||
|
|
||||||
val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.interfaceAddresses?.asSequence()
|
val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.interfaceAddresses?.asSequence()
|
||||||
?.singleOrNull { it.address is Inet4Address } ?: throw InterfaceNotFoundException()
|
?.singleOrNull { it.address is Inet4Address } ?: throw InterfaceNotFoundException()
|
||||||
|
val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}"
|
||||||
private val transaction = RootSession.beginTransaction()
|
private val transaction = RootSession.beginTransaction()
|
||||||
private val subroutes = HashMap<InetAddress, Subroute>()
|
|
||||||
private var clients: ClientMonitorService.Binder? = null
|
var hasMasquerade = false
|
||||||
|
|
||||||
|
private abstract inner class Upstream : UpstreamMonitor.Callback {
|
||||||
|
var subrouting: Subrouting? = null
|
||||||
|
var dns: List<InetAddress> = emptyList()
|
||||||
|
|
||||||
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
|
this.dns = dns
|
||||||
|
updateDnsRoute()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost() {
|
||||||
|
val subrouting = subrouting ?: return
|
||||||
|
subrouting.close()
|
||||||
|
TrafficRecorder.update() // record stats before removing rules to prevent stats losing
|
||||||
|
subrouting.revert()
|
||||||
|
this.subrouting = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val fallbackUpstream = object : Upstream() {
|
||||||
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
|
val subrouting = subrouting
|
||||||
|
if (subrouting == null) this.subrouting = try {
|
||||||
|
Subrouting(this@Routing, RULE_PRIORITY_UPSTREAM_FALLBACK, ifname)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
Timber.w(e)
|
||||||
|
null
|
||||||
|
} else check(subrouting.upstream == ifname)
|
||||||
|
super.onAvailable(ifname, dns)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFallback() {
|
||||||
|
check(subrouting == null)
|
||||||
|
subrouting = try {
|
||||||
|
Subrouting(this@Routing, RULE_PRIORITY_UPSTREAM_FALLBACK)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
Timber.w(e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
updateDnsRoute()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val upstream = object : Upstream() {
|
||||||
|
override fun onAvailable(ifname: String, dns: List<InetAddress>) {
|
||||||
|
val subrouting = subrouting
|
||||||
|
if (subrouting == null) this.subrouting = try {
|
||||||
|
Subrouting(this@Routing, RULE_PRIORITY_UPSTREAM, ifname)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
Timber.w(e)
|
||||||
|
null
|
||||||
|
} else check(subrouting.upstream == ifname)
|
||||||
|
super.onAvailable(ifname, dns)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun ipForward() = transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
fun ipForward() = transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
||||||
|
|
||||||
fun disableIpv6() = transaction.exec("echo 1 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6",
|
fun disableIpv6() = transaction.exec("echo 1 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6",
|
||||||
"echo 0 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
|
"echo 0 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
|
||||||
|
|
||||||
/**
|
|
||||||
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
|
|
||||||
* This also works for Wi-Fi direct where there's no rule at 18000.
|
|
||||||
*
|
|
||||||
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65
|
|
||||||
*/
|
|
||||||
fun rule() {
|
|
||||||
if (upstream != null) {
|
|
||||||
transaction.exec("ip rule add from all iif $downstream lookup $upstream priority 17900",
|
|
||||||
// by the time stopScript is called, table entry for upstream may already get removed
|
|
||||||
"ip rule del from all iif $downstream priority 17900")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun forward() {
|
fun forward() {
|
||||||
transaction.execQuiet("$IPTABLES -N vpnhotspot_fwd")
|
transaction.execQuiet("$IPTABLES -N vpnhotspot_fwd")
|
||||||
transaction.iptablesInsert("FORWARD -j vpnhotspot_fwd")
|
transaction.iptablesInsert("FORWARD -j vpnhotspot_fwd")
|
||||||
transaction.iptablesAdd("vpnhotspot_fwd -i $downstream ! -o $downstream -j DROP") // ensure blocking works
|
transaction.iptablesAdd("vpnhotspot_fwd -i $downstream ! -o $downstream -j DROP") // ensure blocking works
|
||||||
// the real forwarding filters will be added in Subroute when clients are connected
|
// the real forwarding filters will be added in Subrouting when clients are connected
|
||||||
}
|
}
|
||||||
|
|
||||||
fun masquerade() {
|
fun masquerade() {
|
||||||
val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}"
|
|
||||||
transaction.execQuiet("$IPTABLES -t nat -N vpnhotspot_masquerade")
|
transaction.execQuiet("$IPTABLES -t nat -N vpnhotspot_masquerade")
|
||||||
transaction.iptablesInsert("POSTROUTING -j vpnhotspot_masquerade", "nat")
|
transaction.iptablesInsert("POSTROUTING -j vpnhotspot_masquerade", "nat")
|
||||||
// note: specifying -i wouldn't work for POSTROUTING
|
hasMasquerade = true
|
||||||
if (strict) {
|
// further rules are added when upstreams are found
|
||||||
if (upstream != null) {
|
|
||||||
transaction.iptablesAdd("vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
|
||||||
} // else nothing needs to be done
|
|
||||||
} else {
|
|
||||||
transaction.iptablesAdd("vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE", "nat")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dnsRedirect(dnses: List<InetAddress>) {
|
private inner class DnsRoute(val dns: String) {
|
||||||
val hostAddress = hostAddress.address.hostAddress
|
val transaction = RootSession.beginTransaction().safeguard {
|
||||||
val dns = dnses.firstOrNull { it is Inet4Address }?.hostAddress ?: app.pref.getString("service.dns", "8.8.8.8")
|
val hostAddress = hostAddress.address.hostAddress
|
||||||
debugLog("Routing", "Using $dns from ($dnses)")
|
iptablesAdd("PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
|
||||||
transaction.iptablesAdd("PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
|
iptablesAdd("PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
|
||||||
transaction.iptablesAdd("PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
|
}
|
||||||
|
}
|
||||||
|
private var currentDns: DnsRoute? = null
|
||||||
|
private fun updateDnsRoute() {
|
||||||
|
val dns = (upstream.dns + fallbackUpstream.dns).firstOrNull { it is Inet4Address }?.hostAddress
|
||||||
|
?: app.pref.getString("service.dns", "8.8.8.8")
|
||||||
|
if (dns != currentDns?.dns) {
|
||||||
|
currentDns?.transaction?.revert()
|
||||||
|
currentDns = try {
|
||||||
|
DnsRoute(dns)
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,80 +178,23 @@ class Routing(private val owner: Context, val upstream: String?, private val dow
|
|||||||
fun dhcpWorkaround() = transaction.exec("ip rule add iif lo uidrange 0-0 lookup local_network priority 11000",
|
fun dhcpWorkaround() = transaction.exec("ip rule add iif lo uidrange 0-0 lookup local_network priority 11000",
|
||||||
"ip rule del iif lo uidrange 0-0 lookup local_network priority 11000")
|
"ip rule del iif lo uidrange 0-0 lookup local_network priority 11000")
|
||||||
|
|
||||||
|
fun stop() {
|
||||||
|
FallbackUpstreamMonitor.unregisterCallback(fallbackUpstream)
|
||||||
|
fallbackUpstream.subrouting?.close()
|
||||||
|
UpstreamMonitor.unregisterCallback(upstream)
|
||||||
|
upstream.subrouting?.close()
|
||||||
|
}
|
||||||
|
|
||||||
fun commit() {
|
fun commit() {
|
||||||
transaction.commit()
|
transaction.commit()
|
||||||
owner.bindService(Intent(owner, ClientMonitorService::class.java), this, Context.BIND_AUTO_CREATE)
|
FallbackUpstreamMonitor.registerCallback(fallbackUpstream)
|
||||||
|
UpstreamMonitor.registerCallback(upstream)
|
||||||
}
|
}
|
||||||
fun revert() {
|
fun revert() {
|
||||||
stop()
|
stop()
|
||||||
TrafficRecorder.update() // record stats before exiting to prevent stats losing
|
TrafficRecorder.update() // record stats before exiting to prevent stats losing
|
||||||
synchronized(subroutes) { subroutes.forEach { (_, subroute) -> subroute.close() } }
|
fallbackUpstream.subrouting?.revert()
|
||||||
|
upstream.subrouting?.revert()
|
||||||
transaction.revert()
|
transaction.revert()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Only unregister client listener. This should only be used when a clean has just performed.
|
|
||||||
*/
|
|
||||||
fun stop() = owner.stopAndUnbind(this)
|
|
||||||
|
|
||||||
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
|
||||||
clients = service as ClientMonitorService.Binder
|
|
||||||
service.clientsChanged[this] = {
|
|
||||||
synchronized(subroutes) {
|
|
||||||
val toRemove = HashSet(subroutes.keys)
|
|
||||||
for (client in it) if (!client.record.blocked) updateForClient(client, toRemove)
|
|
||||||
if (toRemove.isNotEmpty()) {
|
|
||||||
TrafficRecorder.update() // record stats before removing rules to prevent stats losing
|
|
||||||
for (address in toRemove) subroutes.remove(address)!!.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
override fun onServiceDisconnected(name: ComponentName?) {
|
|
||||||
val clients = clients ?: return
|
|
||||||
clients.clientsChanged -= this
|
|
||||||
this.clients = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateForClient(client: Client, toRemove: HashSet<InetAddress>? = null) {
|
|
||||||
for ((ip, _) in client.ip) if (ip is Inet4Address) {
|
|
||||||
toRemove?.remove(ip)
|
|
||||||
try {
|
|
||||||
subroutes.computeIfAbsentCompat(ip) { Subroute(ip, client) }
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Timber.w(e)
|
|
||||||
SmartSnackbar.make(e.localizedMessage).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class Subroute(private val ip: Inet4Address, client: Client) : AutoCloseable {
|
|
||||||
private val transaction = RootSession.beginTransaction().apply {
|
|
||||||
try {
|
|
||||||
val address by lazy { ip.hostAddress }
|
|
||||||
if (!strict) {
|
|
||||||
// otw allow downstream packets to be redirected to anywhere
|
|
||||||
// because we don't wanna keep track of default network changes
|
|
||||||
iptablesInsert("vpnhotspot_fwd -i $downstream -s $address -j ACCEPT")
|
|
||||||
iptablesInsert("vpnhotspot_fwd -o $downstream -d $address -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
|
||||||
} else if (upstream != null) {
|
|
||||||
iptablesInsert("vpnhotspot_fwd -i $downstream -s $address -o $upstream -j ACCEPT")
|
|
||||||
iptablesInsert("vpnhotspot_fwd -i $upstream -o $downstream -d $address -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
|
||||||
} // else nothing needs to be done
|
|
||||||
commit()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
revert()
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
TrafficRecorder.register(ip, if (strict) upstream else null, downstream, client.mac)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun close() {
|
|
||||||
TrafficRecorder.unregister(ip, downstream)
|
|
||||||
transaction.revert()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
97
mobile/src/main/java/be/mygod/vpnhotspot/net/Subrouting.kt
Normal file
97
mobile/src/main/java/be/mygod/vpnhotspot/net/Subrouting.kt
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package be.mygod.vpnhotspot.net
|
||||||
|
|
||||||
|
import be.mygod.vpnhotspot.net.Routing.Companion.iptablesAdd
|
||||||
|
import be.mygod.vpnhotspot.net.Routing.Companion.iptablesInsert
|
||||||
|
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||||
|
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
||||||
|
import be.mygod.vpnhotspot.room.AppDatabase
|
||||||
|
import be.mygod.vpnhotspot.room.lookup
|
||||||
|
import be.mygod.vpnhotspot.room.macToLong
|
||||||
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
|
import be.mygod.vpnhotspot.util.computeIfAbsentCompat
|
||||||
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.net.Inet4Address
|
||||||
|
import java.net.InetAddress
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The only case when upstream is null is on API 23- and we are using system default rules.
|
||||||
|
*/
|
||||||
|
class Subrouting(private val parent: Routing, priority: Int, val upstream: String? = null) :
|
||||||
|
IpNeighbourMonitor.Callback, AutoCloseable {
|
||||||
|
private inner class Subroute(private val ip: Inet4Address, mac: String) : AutoCloseable {
|
||||||
|
private val transaction = RootSession.beginTransaction().safeguard {
|
||||||
|
val downstream = parent.downstream
|
||||||
|
val address = ip.hostAddress
|
||||||
|
if (upstream == null) {
|
||||||
|
// otw allow downstream packets to be redirected to anywhere
|
||||||
|
// because we don't wanna keep track of default network changes
|
||||||
|
iptablesInsert("vpnhotspot_fwd -i $downstream -s $address -j ACCEPT")
|
||||||
|
iptablesInsert("vpnhotspot_fwd -o $downstream -d $address -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
|
} else {
|
||||||
|
iptablesInsert("vpnhotspot_fwd -i $downstream -s $address -o $upstream -j ACCEPT")
|
||||||
|
iptablesInsert("vpnhotspot_fwd -i $upstream -o $downstream -d $address -m state --state ESTABLISHED,RELATED -j ACCEPT")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
TrafficRecorder.register(ip, upstream, parent.downstream, mac)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun close() {
|
||||||
|
TrafficRecorder.unregister(ip, upstream, parent.downstream)
|
||||||
|
transaction.revert()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val transaction = RootSession.beginTransaction().safeguard {
|
||||||
|
if (upstream != null) {
|
||||||
|
val downstream = parent.downstream
|
||||||
|
exec("ip rule add from all iif $downstream lookup $upstream priority $priority",
|
||||||
|
// by the time stopScript is called, table entry for upstream may already get removed
|
||||||
|
"ip rule del from all iif $downstream priority $priority")
|
||||||
|
}
|
||||||
|
// note: specifying -i wouldn't work for POSTROUTING
|
||||||
|
if (parent.hasMasquerade) {
|
||||||
|
val hostSubnet = parent.hostSubnet
|
||||||
|
iptablesAdd(if (upstream == null) "vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE" else
|
||||||
|
"vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private val subroutes = HashMap<InetAddress, Subroute>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
IpNeighbourMonitor.registerCallback(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unregister client listener. This should be always called even after clean.
|
||||||
|
*/
|
||||||
|
override fun close() = IpNeighbourMonitor.unregisterCallback(this)
|
||||||
|
|
||||||
|
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) {
|
||||||
|
synchronized(parent) {
|
||||||
|
val toRemove = HashSet(subroutes.keys)
|
||||||
|
for (neighbour in neighbours) {
|
||||||
|
if (neighbour.dev != parent.downstream || neighbour.ip !is Inet4Address ||
|
||||||
|
AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr.macToLong()).blocked) continue
|
||||||
|
toRemove.remove(neighbour.ip)
|
||||||
|
try {
|
||||||
|
subroutes.computeIfAbsentCompat(neighbour.ip) { Subroute(neighbour.ip, neighbour.lladdr) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e.localizedMessage).show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toRemove.isNotEmpty()) {
|
||||||
|
TrafficRecorder.update() // record stats before removing rules to prevent stats losing
|
||||||
|
for (address in toRemove) subroutes.remove(address)!!.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun revert() {
|
||||||
|
subroutes.forEach { (_, subroute) -> subroute.close() }
|
||||||
|
transaction.revert()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
|
import android.annotation.TargetApi
|
||||||
|
import android.net.*
|
||||||
|
import android.os.Build
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
import timber.log.Timber
|
||||||
|
|
||||||
|
object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||||
|
private var registered = false
|
||||||
|
private var currentNetwork: Network? = null
|
||||||
|
override var currentLinkProperties: LinkProperties? = null
|
||||||
|
private set
|
||||||
|
/**
|
||||||
|
* Unfortunately registerDefaultNetworkCallback is going to return VPN interface since Android P DP1:
|
||||||
|
* https://android.googlesource.com/platform/frameworks/base/+/dda156ab0c5d66ad82bdcf76cda07cbc0a9c8a2e
|
||||||
|
*/
|
||||||
|
private val networkRequest = NetworkRequest.Builder()
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||||
|
.addCapability(NetworkCapabilities.NET_CAPABILITY_NOT_RESTRICTED)
|
||||||
|
.build()
|
||||||
|
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||||
|
override fun onAvailable(network: Network) {
|
||||||
|
val properties = app.connectivity.getLinkProperties(network)
|
||||||
|
val ifname = properties?.interfaceName ?: return
|
||||||
|
when (currentNetwork) {
|
||||||
|
null -> currentNetwork = network
|
||||||
|
network -> {
|
||||||
|
val oldProperties = currentLinkProperties!!
|
||||||
|
check(ifname == oldProperties.interfaceName)
|
||||||
|
if (properties.dnsServers == oldProperties.dnsServers) return
|
||||||
|
}
|
||||||
|
else -> check(false)
|
||||||
|
}
|
||||||
|
currentLinkProperties = properties
|
||||||
|
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
|
||||||
|
check(currentNetwork == network)
|
||||||
|
val oldProperties = currentLinkProperties!!
|
||||||
|
currentLinkProperties = properties
|
||||||
|
val ifname = properties.interfaceName!!
|
||||||
|
check(ifname == oldProperties.interfaceName)
|
||||||
|
if (properties.dnsServers != oldProperties.dnsServers)
|
||||||
|
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onLost(network: Network) {
|
||||||
|
check(currentNetwork == network)
|
||||||
|
callbacks.forEach { it.onLost() }
|
||||||
|
currentNetwork = null
|
||||||
|
currentLinkProperties = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerCallbackLocked(callback: Callback) {
|
||||||
|
if (registered) {
|
||||||
|
val currentLinkProperties = currentLinkProperties
|
||||||
|
if (currentLinkProperties != null) {
|
||||||
|
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties.dnsServers)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
||||||
|
app.connectivity.registerDefaultNetworkCallback(networkCallback)
|
||||||
|
} else try {
|
||||||
|
app.connectivity.requestNetwork(networkRequest, networkCallback)
|
||||||
|
} catch (e: SecurityException) {
|
||||||
|
if (Build.VERSION.SDK_INT != 23) throw e
|
||||||
|
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
||||||
|
Timber.w(e)
|
||||||
|
callback.onFallback()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
registered = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun destroyLocked() {
|
||||||
|
if (!registered) return
|
||||||
|
app.connectivity.unregisterNetworkCallback(networkCallback)
|
||||||
|
registered = false
|
||||||
|
currentNetwork = null
|
||||||
|
currentLinkProperties = null
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
|
import android.content.SharedPreferences
|
||||||
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
|
|
||||||
|
abstract class FallbackUpstreamMonitor private constructor() : UpstreamMonitor() {
|
||||||
|
companion object : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
const val KEY = "service.upstream.fallback"
|
||||||
|
|
||||||
|
init {
|
||||||
|
app.pref.registerOnSharedPreferenceChangeListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateMonitor(): UpstreamMonitor {
|
||||||
|
val upstream = app.pref.getString(KEY, null)
|
||||||
|
return if (upstream.isNullOrEmpty()) DefaultNetworkMonitor else InterfaceMonitor(upstream!!)
|
||||||
|
}
|
||||||
|
private var monitor = generateMonitor()
|
||||||
|
|
||||||
|
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
|
||||||
|
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
|
||||||
|
for (callback in callbacks) {
|
||||||
|
if (active) callback.onLost()
|
||||||
|
new.registerCallback(callback)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,42 +23,30 @@ class InterfaceMonitor(val iface: String) : UpstreamMonitor() {
|
|||||||
setPresent(lines.any { parser.find(it)?.groupValues?.get(2) == iface })
|
setPresent(lines.any { parser.find(it)?.groupValues?.get(2) == iface })
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setPresent(present: Boolean) = if (initializing) {
|
private fun setPresent(present: Boolean) = synchronized(this) {
|
||||||
initializedPresent = present
|
|
||||||
currentIface = if (present) iface else null
|
|
||||||
} else synchronized(this) {
|
|
||||||
val old = currentIface != null
|
val old = currentIface != null
|
||||||
if (present == old) return
|
if (present == old) return
|
||||||
currentIface = if (present) iface else null
|
currentIface = if (present) iface else null
|
||||||
if (present) {
|
if (present) {
|
||||||
val dns = dns
|
val dns = currentDns
|
||||||
callbacks.forEach { it.onAvailable(iface, dns) }
|
callbacks.forEach { it.onAvailable(iface, dns) }
|
||||||
} else callbacks.forEach { it.onLost() }
|
} else callbacks.forEach { it.onLost() }
|
||||||
}
|
}
|
||||||
|
|
||||||
private var monitor: IpLinkMonitor? = null
|
private var monitor: IpLinkMonitor? = null
|
||||||
private var initializing = false
|
|
||||||
private var initializedPresent: Boolean? = null
|
|
||||||
override var currentIface: String? = null
|
override var currentIface: String? = null
|
||||||
private set
|
private set
|
||||||
private val dns get() = app.connectivity.allNetworks
|
override val currentLinkProperties get() = app.connectivity.allNetworks
|
||||||
.map { app.connectivity.getLinkProperties(it) }
|
.map { app.connectivity.getLinkProperties(it) }
|
||||||
.singleOrNull { it?.interfaceName == iface }
|
.singleOrNull { it?.interfaceName == iface }
|
||||||
?.dnsServers ?: emptyList()
|
|
||||||
|
|
||||||
override fun registerCallbackLocked(callback: Callback): Boolean {
|
override fun registerCallbackLocked(callback: Callback) {
|
||||||
var monitor = monitor
|
var monitor = monitor
|
||||||
val present = if (monitor == null) {
|
if (monitor == null) {
|
||||||
initializing = true
|
|
||||||
initializedPresent = null
|
|
||||||
monitor = IpLinkMonitor()
|
monitor = IpLinkMonitor()
|
||||||
this.monitor = monitor
|
this.monitor = monitor
|
||||||
monitor.run()
|
monitor.run()
|
||||||
initializing = false
|
} else if (currentIface != null) callback.onAvailable(iface, currentDns)
|
||||||
initializedPresent!!
|
|
||||||
} else currentIface != null
|
|
||||||
if (present) callback.onAvailable(iface, dns)
|
|
||||||
return !present
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun destroyLocked() {
|
override fun destroyLocked() {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.net.monitor
|
|||||||
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.debugLog
|
import be.mygod.vpnhotspot.debugLog
|
||||||
|
import be.mygod.vpnhotspot.net.IpNeighbour
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
class IpNeighbourMonitor private constructor() : IpMonitor() {
|
class IpNeighbourMonitor private constructor() : IpMonitor() {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import be.mygod.vpnhotspot.room.macToLong
|
|||||||
import be.mygod.vpnhotspot.util.Event2
|
import be.mygod.vpnhotspot.util.Event2
|
||||||
import be.mygod.vpnhotspot.util.RootSession
|
import be.mygod.vpnhotspot.util.RootSession
|
||||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||||
|
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
import java.util.concurrent.TimeUnit
|
import java.util.concurrent.TimeUnit
|
||||||
@@ -17,7 +18,8 @@ object TrafficRecorder {
|
|||||||
private const val ANYWHERE = "0.0.0.0/0"
|
private const val ANYWHERE = "0.0.0.0/0"
|
||||||
|
|
||||||
private var scheduled = false
|
private var scheduled = false
|
||||||
private val records = HashMap<Pair<InetAddress, String>, TrafficRecord>()
|
private var lastUpdate = 0L
|
||||||
|
private val records = HashMap<Triple<InetAddress, String?, String>, TrafficRecord>()
|
||||||
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
|
val foregroundListeners = Event2<Collection<TrafficRecord>, LongSparseArray<TrafficRecord>>()
|
||||||
|
|
||||||
fun register(ip: InetAddress, upstream: String?, downstream: String, mac: String) {
|
fun register(ip: InetAddress, upstream: String?, downstream: String, mac: String) {
|
||||||
@@ -28,13 +30,13 @@ object TrafficRecorder {
|
|||||||
downstream = downstream)
|
downstream = downstream)
|
||||||
AppDatabase.instance.trafficRecordDao.insert(record)
|
AppDatabase.instance.trafficRecordDao.insert(record)
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
check(records.put(Pair(ip, downstream), record) == null)
|
check(records.put(Triple(ip, upstream, downstream), record) == null)
|
||||||
scheduleUpdateLocked()
|
scheduleUpdateLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fun unregister(ip: InetAddress, downstream: String) = synchronized(this) {
|
fun unregister(ip: InetAddress, upstream: String?, downstream: String) = synchronized(this) {
|
||||||
update() // flush stats before removing
|
update() // flush stats before removing
|
||||||
check(records.remove(Pair(ip, downstream)) != null)
|
check(records.remove(Triple(ip, upstream, downstream)) != null)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun unscheduleUpdateLocked() {
|
private fun unscheduleUpdateLocked() {
|
||||||
@@ -56,72 +58,85 @@ object TrafficRecorder {
|
|||||||
scheduleUpdateLocked()
|
scheduleUpdateLocked()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun doUpdate(timestamp: Long) {
|
||||||
|
val oldRecords = LongSparseArray<TrafficRecord>()
|
||||||
|
for (line in RootSession.use { it.execOutUnjoined("iptables -nvx -L vpnhotspot_fwd") }.asSequence().drop(2)) {
|
||||||
|
val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
|
||||||
|
try {
|
||||||
|
check(columns.size >= 9)
|
||||||
|
when (columns[2]) {
|
||||||
|
"DROP" -> { }
|
||||||
|
"ACCEPT" -> {
|
||||||
|
val isReceive = columns[7] == ANYWHERE
|
||||||
|
val isSend = columns[8] == ANYWHERE
|
||||||
|
check(isReceive != isSend)
|
||||||
|
val ip = parseNumericAddress(columns[if (isReceive) 8 else 7])
|
||||||
|
val downstream = columns[if (isReceive) 6 else 5]
|
||||||
|
var upstream: String? = columns[if (isReceive) 5 else 6]
|
||||||
|
if (upstream == "*") upstream = null
|
||||||
|
val key = Triple(ip, upstream, downstream)
|
||||||
|
val oldRecord = records[key]!!
|
||||||
|
val record = if (oldRecord.id == null) oldRecord else TrafficRecord(
|
||||||
|
timestamp = timestamp,
|
||||||
|
mac = oldRecord.mac,
|
||||||
|
ip = ip,
|
||||||
|
upstream = upstream,
|
||||||
|
downstream = downstream,
|
||||||
|
sentPackets = -1,
|
||||||
|
sentBytes = -1,
|
||||||
|
receivedPackets = -1,
|
||||||
|
receivedBytes = -1,
|
||||||
|
previousId = oldRecord.id)
|
||||||
|
if (isReceive) {
|
||||||
|
if (record.receivedPackets == -1L && record.receivedBytes == -1L) {
|
||||||
|
record.receivedPackets = columns[0].toLong()
|
||||||
|
record.receivedBytes = columns[1].toLong()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (record.sentPackets == -1L && record.sentBytes == -1L) {
|
||||||
|
record.sentPackets = columns[0].toLong()
|
||||||
|
record.sentBytes = columns[1].toLong()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (oldRecord.id != null) {
|
||||||
|
check(records.put(key, record) == oldRecord)
|
||||||
|
oldRecords.put(oldRecord.id!!, oldRecord)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> check(false)
|
||||||
|
}
|
||||||
|
} catch (e: RuntimeException) {
|
||||||
|
Timber.w(line)
|
||||||
|
Timber.w(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ((_, record) in records) if (record.id == null) {
|
||||||
|
check(record.sentPackets >= 0)
|
||||||
|
check(record.sentBytes >= 0)
|
||||||
|
check(record.receivedPackets >= 0)
|
||||||
|
check(record.receivedBytes >= 0)
|
||||||
|
AppDatabase.instance.trafficRecordDao.insert(record)
|
||||||
|
}
|
||||||
|
foregroundListeners(records.values, oldRecords)
|
||||||
|
}
|
||||||
fun update() {
|
fun update() {
|
||||||
synchronized(this) {
|
synchronized(this) {
|
||||||
|
val wasScheduled = scheduled
|
||||||
scheduled = false
|
scheduled = false
|
||||||
if (records.isEmpty()) return
|
if (records.isEmpty()) return
|
||||||
val timestamp = System.currentTimeMillis()
|
val timestamp = System.currentTimeMillis()
|
||||||
val oldRecords = LongSparseArray<TrafficRecord>()
|
if (timestamp - lastUpdate > 100) {
|
||||||
for (line in RootSession.use { it.execOutUnjoined("iptables -nvx -L vpnhotspot_fwd") }
|
|
||||||
.asSequence().drop(2)) {
|
|
||||||
val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
|
|
||||||
try {
|
try {
|
||||||
check(columns.size >= 9)
|
doUpdate(timestamp)
|
||||||
when (columns[2]) {
|
|
||||||
"DROP" -> { }
|
|
||||||
"ACCEPT" -> {
|
|
||||||
val isReceive = columns[7] == ANYWHERE
|
|
||||||
val isSend = columns[8] == ANYWHERE
|
|
||||||
check(isReceive != isSend)
|
|
||||||
val ip = parseNumericAddress(columns[if (isReceive) 8 else 7])
|
|
||||||
val downstream = columns[if (isReceive) 6 else 5]
|
|
||||||
var upstream: String? = columns[if (isReceive) 5 else 6]
|
|
||||||
if (upstream == "*") upstream = null
|
|
||||||
val key = Pair(ip, downstream)
|
|
||||||
val oldRecord = records[key]!!
|
|
||||||
check(upstream == oldRecord.upstream)
|
|
||||||
val record = if (oldRecord.id == null) oldRecord else TrafficRecord(
|
|
||||||
timestamp = timestamp,
|
|
||||||
mac = oldRecord.mac,
|
|
||||||
ip = ip,
|
|
||||||
upstream = upstream,
|
|
||||||
downstream = downstream,
|
|
||||||
sentPackets = -1,
|
|
||||||
sentBytes = -1,
|
|
||||||
receivedPackets = -1,
|
|
||||||
receivedBytes = -1,
|
|
||||||
previousId = oldRecord.id)
|
|
||||||
if (isReceive) {
|
|
||||||
if (record.receivedPackets == -1L && record.receivedBytes == -1L) {
|
|
||||||
record.receivedPackets = columns[0].toLong()
|
|
||||||
record.receivedBytes = columns[1].toLong()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (record.sentPackets == -1L && record.sentBytes == -1L) {
|
|
||||||
record.sentPackets = columns[0].toLong()
|
|
||||||
record.sentBytes = columns[1].toLong()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (oldRecord.id != null) {
|
|
||||||
check(records.put(key, record) == oldRecord)
|
|
||||||
oldRecords.put(oldRecord.id!!, oldRecord)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else -> check(false)
|
|
||||||
}
|
|
||||||
} catch (e: RuntimeException) {
|
} catch (e: RuntimeException) {
|
||||||
Timber.w(line)
|
|
||||||
Timber.w(e)
|
Timber.w(e)
|
||||||
|
SmartSnackbar.make(e.localizedMessage)
|
||||||
}
|
}
|
||||||
|
lastUpdate = timestamp
|
||||||
|
} else if (wasScheduled) {
|
||||||
|
scheduled = true
|
||||||
|
return
|
||||||
}
|
}
|
||||||
for ((_, record) in records) if (record.id == null) {
|
|
||||||
check(record.sentPackets >= 0)
|
|
||||||
check(record.sentBytes >= 0)
|
|
||||||
check(record.receivedPackets >= 0)
|
|
||||||
check(record.receivedBytes >= 0)
|
|
||||||
AppDatabase.instance.trafficRecordDao.insert(record)
|
|
||||||
}
|
|
||||||
foregroundListeners(records.values, oldRecords)
|
|
||||||
scheduleUpdateLocked()
|
scheduleUpdateLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
|
import android.net.LinkProperties
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import java.net.InetAddress
|
import java.net.InetAddress
|
||||||
|
|
||||||
@@ -18,9 +19,7 @@ abstract class UpstreamMonitor {
|
|||||||
}
|
}
|
||||||
private var monitor = generateMonitor()
|
private var monitor = generateMonitor()
|
||||||
|
|
||||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) = synchronized(this) {
|
fun registerCallback(callback: Callback) = synchronized(this) { monitor.registerCallback(callback) }
|
||||||
monitor.registerCallback(callback, failfast)
|
|
||||||
}
|
|
||||||
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
|
fun unregisterCallback(callback: Callback) = synchronized(this) { monitor.unregisterCallback(callback) }
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
@@ -35,7 +34,10 @@ abstract class UpstreamMonitor {
|
|||||||
}
|
}
|
||||||
val new = generateMonitor()
|
val new = generateMonitor()
|
||||||
monitor = new
|
monitor = new
|
||||||
callbacks.forEach { new.registerCallback(it) { if (active) it.onLost() } }
|
for (callback in callbacks) {
|
||||||
|
if (active) callback.onLost()
|
||||||
|
new.registerCallback(callback)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,24 +45,39 @@ abstract class UpstreamMonitor {
|
|||||||
interface Callback {
|
interface Callback {
|
||||||
/**
|
/**
|
||||||
* Called if some interface is available. This might be called on different ifname without having called onLost.
|
* Called if some interface is available. This might be called on different ifname without having called onLost.
|
||||||
|
* This might also be called on the same ifname but with updated DNS list.
|
||||||
*/
|
*/
|
||||||
fun onAvailable(ifname: String, dns: List<InetAddress>)
|
fun onAvailable(ifname: String, dns: List<InetAddress>)
|
||||||
/**
|
/**
|
||||||
* Called if no interface is available.
|
* Called if no interface is available.
|
||||||
*/
|
*/
|
||||||
fun onLost()
|
fun onLost()
|
||||||
|
/**
|
||||||
|
* Called on API 23- from DefaultNetworkMonitor. This indicates that there isn't a good way of telling the
|
||||||
|
* default network (see DefaultNetworkMonitor) and we are using rules at priority 22000
|
||||||
|
* (RULE_PRIORITY_DEFAULT_NETWORK) as our fallback rules, which would work fine until Android 9.0 broke it in
|
||||||
|
* commit: https://android.googlesource.com/platform/system/netd/+/758627c4d93392190b08e9aaea3bbbfb92a5f364
|
||||||
|
*/
|
||||||
|
fun onFallback() {
|
||||||
|
throw NotImplementedError()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected val callbacks = HashSet<Callback>()
|
val callbacks = HashSet<Callback>()
|
||||||
abstract val currentIface: String?
|
protected abstract val currentLinkProperties: LinkProperties?
|
||||||
protected abstract fun registerCallbackLocked(callback: Callback): Boolean
|
open val currentIface: String? get() = currentLinkProperties?.interfaceName
|
||||||
protected abstract fun destroyLocked()
|
/**
|
||||||
|
* There's no need for overriding currentDns for now.
|
||||||
|
*/
|
||||||
|
val currentDns: List<InetAddress> get() = currentLinkProperties?.dnsServers ?: emptyList()
|
||||||
|
protected abstract fun registerCallbackLocked(callback: Callback)
|
||||||
|
abstract fun destroyLocked()
|
||||||
|
|
||||||
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
|
fun registerCallback(callback: Callback) {
|
||||||
if (synchronized(this) {
|
synchronized(this) {
|
||||||
if (!callbacks.add(callback)) return
|
if (!callbacks.add(callback)) return
|
||||||
registerCallbackLocked(callback)
|
registerCallbackLocked(callback)
|
||||||
}) failfast?.invoke()
|
}
|
||||||
}
|
}
|
||||||
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
fun unregisterCallback(callback: Callback) = synchronized(this) {
|
||||||
if (callbacks.remove(callback) && callbacks.isEmpty()) destroyLocked()
|
if (callbacks.remove(callback) && callbacks.isEmpty()) destroyLocked()
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
package be.mygod.vpnhotspot.net.monitor
|
package be.mygod.vpnhotspot.net.monitor
|
||||||
|
|
||||||
import android.net.ConnectivityManager
|
import android.net.*
|
||||||
import android.net.Network
|
|
||||||
import android.net.NetworkCapabilities
|
|
||||||
import android.net.NetworkRequest
|
|
||||||
import be.mygod.vpnhotspot.App.Companion.app
|
import be.mygod.vpnhotspot.App.Companion.app
|
||||||
import be.mygod.vpnhotspot.debugLog
|
import be.mygod.vpnhotspot.debugLog
|
||||||
|
|
||||||
@@ -16,12 +13,9 @@ object VpnMonitor : UpstreamMonitor() {
|
|||||||
.build()
|
.build()
|
||||||
private var registered = false
|
private var registered = false
|
||||||
|
|
||||||
/**
|
private val available = HashMap<Network, LinkProperties>()
|
||||||
* Obtaining ifname in onLost doesn't work so we need to cache it in onAvailable.
|
|
||||||
*/
|
|
||||||
private val available = HashMap<Network, String>()
|
|
||||||
private var currentNetwork: Network? = null
|
private var currentNetwork: Network? = null
|
||||||
override val currentIface: String? get() {
|
override val currentLinkProperties: LinkProperties? get() {
|
||||||
val currentNetwork = currentNetwork
|
val currentNetwork = currentNetwork
|
||||||
return if (currentNetwork == null) null else available[currentNetwork]
|
return if (currentNetwork == null) null else available[currentNetwork]
|
||||||
}
|
}
|
||||||
@@ -30,49 +24,56 @@ object VpnMonitor : UpstreamMonitor() {
|
|||||||
val properties = app.connectivity.getLinkProperties(network)
|
val properties = app.connectivity.getLinkProperties(network)
|
||||||
val ifname = properties?.interfaceName ?: return
|
val ifname = properties?.interfaceName ?: return
|
||||||
synchronized(this@VpnMonitor) {
|
synchronized(this@VpnMonitor) {
|
||||||
if (available.put(network, ifname) != null) return
|
|
||||||
debugLog(TAG, "onAvailable: $ifname, ${properties.dnsServers.joinToString()}")
|
|
||||||
val old = currentNetwork
|
val old = currentNetwork
|
||||||
if (old != null) debugLog(TAG, "Assuming old VPN interface ${available[old]} is dying")
|
val oldProperties = available.put(network, properties)
|
||||||
currentNetwork = network
|
if (old != network) {
|
||||||
|
if (old != null) {
|
||||||
|
debugLog(TAG, "Assuming old VPN interface ${available[old]} is dying")
|
||||||
|
callbacks.forEach { it.onLost() }
|
||||||
|
}
|
||||||
|
currentNetwork = network
|
||||||
|
} else {
|
||||||
|
check(ifname == oldProperties!!.interfaceName)
|
||||||
|
if (properties.dnsServers == oldProperties.dnsServers) return
|
||||||
|
}
|
||||||
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onLinkPropertiesChanged(network: Network, properties: LinkProperties) {
|
||||||
|
synchronized(this@VpnMonitor) {
|
||||||
|
if (currentNetwork != network) return
|
||||||
|
val oldProperties = available.put(network, properties)!!
|
||||||
|
val ifname = properties.interfaceName!!
|
||||||
|
check(ifname == oldProperties.interfaceName)
|
||||||
|
if (properties.dnsServers != oldProperties.dnsServers)
|
||||||
|
callbacks.forEach { it.onAvailable(ifname, properties.dnsServers) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun onLost(network: Network) = synchronized(this@VpnMonitor) {
|
override fun onLost(network: Network) = synchronized(this@VpnMonitor) {
|
||||||
val ifname = available.remove(network) ?: return
|
if (available.remove(network) == null || currentNetwork != network) return
|
||||||
debugLog(TAG, "onLost: $ifname")
|
if (available.isNotEmpty()) {
|
||||||
if (currentNetwork != network) return
|
|
||||||
while (available.isNotEmpty()) {
|
|
||||||
val next = available.entries.first()
|
val next = available.entries.first()
|
||||||
currentNetwork = next.key
|
currentNetwork = next.key
|
||||||
val properties = app.connectivity.getLinkProperties(next.key)
|
debugLog(TAG, "Switching to ${next.value.interfaceName} as VPN interface")
|
||||||
if (properties != null) {
|
callbacks.forEach { it.onAvailable(next.value.interfaceName!!, next.value.dnsServers) }
|
||||||
debugLog(TAG, "Switching to ${next.value} as VPN interface")
|
} else {
|
||||||
callbacks.forEach { it.onAvailable(next.value, properties.dnsServers) }
|
callbacks.forEach { it.onLost() }
|
||||||
return
|
currentNetwork = null
|
||||||
}
|
|
||||||
available.remove(next.key)
|
|
||||||
}
|
}
|
||||||
callbacks.forEach { it.onLost() }
|
|
||||||
currentNetwork = null
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun registerCallbackLocked(callback: Callback) = if (registered) {
|
override fun registerCallbackLocked(callback: Callback) {
|
||||||
val currentNetwork = currentNetwork
|
if (registered) {
|
||||||
if (currentNetwork == null) true else {
|
val currentLinkProperties = currentLinkProperties
|
||||||
callback.onAvailable(available[currentNetwork]!!,
|
if (currentLinkProperties != null) {
|
||||||
app.connectivity.getLinkProperties(currentNetwork)?.dnsServers ?: emptyList())
|
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties.dnsServers)
|
||||||
false
|
}
|
||||||
}
|
} else {
|
||||||
} else {
|
app.connectivity.registerNetworkCallback(request, networkCallback)
|
||||||
app.connectivity.registerNetworkCallback(request, networkCallback)
|
registered = true
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,14 +123,30 @@ class RootSession : AutoCloseable {
|
|||||||
|
|
||||||
fun revert() {
|
fun revert() {
|
||||||
if (revertCommands.isEmpty()) return
|
if (revertCommands.isEmpty()) return
|
||||||
val shell = if (monitor.isHeldByCurrentThread) this@RootSession else {
|
var locked = monitor.isHeldByCurrentThread
|
||||||
monitor.lock()
|
try {
|
||||||
ensureInstance()
|
val shell = if (locked) this@RootSession else {
|
||||||
|
monitor.lock()
|
||||||
|
locked = true
|
||||||
|
ensureInstance()
|
||||||
|
}
|
||||||
|
shell.haltTimeout()
|
||||||
|
revertCommands.forEach { shell.submit(it) }
|
||||||
|
} catch (e: RuntimeException) { // if revert fails, it should fail silently
|
||||||
|
Timber.d(e)
|
||||||
|
} finally {
|
||||||
|
revertCommands.clear()
|
||||||
|
if (locked) unlock() // commit
|
||||||
}
|
}
|
||||||
shell.haltTimeout()
|
}
|
||||||
revertCommands.forEach { shell.submit(it) }
|
|
||||||
revertCommands.clear()
|
fun safeguard(work: Transaction.() -> Unit) = try {
|
||||||
unlock() // commit
|
work()
|
||||||
|
commit()
|
||||||
|
this
|
||||||
|
} catch (e: Exception) {
|
||||||
|
revert()
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
<vector android:autoMirrored="true" android:height="24dp"
|
|
||||||
android:viewportHeight="24.0" android:viewportWidth="24.0"
|
|
||||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:tint="?attr/colorControlNormal">
|
|
||||||
<path android:fillColor="#FF000000" android:pathData="M23,5.5V20c0,2.2 -1.8,4 -4,4h-7.3c-1.08,0 -2.1,-0.43 -2.85,-1.19L1,14.83s1.26,-1.23 1.3,-1.25c0.22,-0.19 0.49,-0.29 0.79,-0.29 0.22,0 0.42,0.06 0.6,0.16 0.04,0.01 4.31,2.46 4.31,2.46V4c0,-0.83 0.67,-1.5 1.5,-1.5S11,3.17 11,4v7h1V1.5c0,-0.83 0.67,-1.5 1.5,-1.5S15,0.67 15,1.5V11h1V2.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5V11h1V5.5c0,-0.83 0.67,-1.5 1.5,-1.5s1.5,0.67 1.5,1.5z"/>
|
|
||||||
</vector>
|
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24.0"
|
||||||
|
android:viewportHeight="24.0"
|
||||||
|
android:tint="?attr/colorControlNormal">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FF000000"
|
||||||
|
android:pathData="M5,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4L1,6v6h6L7,6L5,6L5,2zM9,16c0,1.3 0.84,2.4 2,2.82L11,23h2v-4.18c1.16,-0.41 2,-1.51 2,-2.82v-2L9,14v2zM1,16c0,1.3 0.84,2.4 2,2.82L3,23h2v-4.18C6.16,18.4 7,17.3 7,16v-2L1,14v2zM21,6L21,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4h-2v6h6L23,6h-2zM13,2c0,-0.55 -0.45,-1 -1,-1s-1,0.45 -1,1v4L9,6v6h6L15,6h-2L13,2zM17,16c0,1.3 0.84,2.4 2,2.82L19,23h2v-4.18c1.16,-0.41 2,-1.51 2,-2.82v-2h-6v2z"/>
|
||||||
|
</vector>
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
<string name="clients_nickname_title">%s 的昵称</string>
|
<string name="clients_nickname_title">%s 的昵称</string>
|
||||||
<string name="clients_stats_title">%s 的流量</string>
|
<string name="clients_stats_title">%s 的流量</string>
|
||||||
<plurals name="clients_stats_message_1">
|
<plurals name="clients_stats_message_1">
|
||||||
<item quantity="other">自 %2$s 以来连了 %1$s 次</item>
|
<item quantity="other">自 %2$s 以来重新路由了 %1$s 次</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="clients_stats_message_2">
|
<plurals name="clients_stats_message_2">
|
||||||
<item quantity="other">上传 %1$s 个包,%2$s</item>
|
<item quantity="other">上传 %1$s 个包,%2$s</item>
|
||||||
@@ -79,14 +79,14 @@
|
|||||||
<string name="settings_service_masquerade_summary">建议使用广告拦截器与 socksfier 等虚拟 VPN 应用时禁用此选项。</string>
|
<string name="settings_service_masquerade_summary">建议使用广告拦截器与 socksfier 等虚拟 VPN 应用时禁用此选项。</string>
|
||||||
<string name="settings_service_repeater_oc">Wi\u2011Fi 运行频段 (不稳定)</string>
|
<string name="settings_service_repeater_oc">Wi\u2011Fi 运行频段 (不稳定)</string>
|
||||||
<string name="settings_service_repeater_oc_summary">"自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)"</string>
|
<string name="settings_service_repeater_oc_summary">"自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)"</string>
|
||||||
<string name="settings_service_repeater_strict">严格模式</string>
|
|
||||||
<string name="settings_service_repeater_strict_summary">只允许通过 VPN 隧道的包通过</string>
|
|
||||||
<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_repeater_start_on_boot">开机自启动中继</string>
|
<string name="settings_service_repeater_start_on_boot">开机自启动中继</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">上游网络接口</string>
|
||||||
<string name="settings_service_upstream_auto">自动检测系统 VPN</string>
|
<string name="settings_service_upstream_auto">自动检测系统 VPN</string>
|
||||||
|
<string name="settings_upstream_fallback">备用上游接口</string>
|
||||||
|
<string name="settings_upstream_fallback_auto">自动检测系统默认网络</string>
|
||||||
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
<string name="settings_service_clean">清理/重新应用路由规则</string>
|
||||||
<string name="settings_service_clean_summary">将修改的设置应用到当前启用的服务上。也可用于修复偶尔会发生的竞态条件。</string>
|
<string name="settings_service_clean_summary">将修改的设置应用到当前启用的服务上。也可用于修复偶尔会发生的竞态条件。</string>
|
||||||
<string name="settings_service_dhcp_workaround">尝试修复 DHCP</string>
|
<string name="settings_service_dhcp_workaround">尝试修复 DHCP</string>
|
||||||
|
|||||||
@@ -68,8 +68,8 @@
|
|||||||
<string name="clients_nickname_title">Nickname for %s</string>
|
<string name="clients_nickname_title">Nickname for %s</string>
|
||||||
<string name="clients_stats_title">Stats for %s</string>
|
<string name="clients_stats_title">Stats for %s</string>
|
||||||
<plurals name="clients_stats_message_1">
|
<plurals name="clients_stats_message_1">
|
||||||
<item quantity="one">Connected 1 time since %2$s</item>
|
<item quantity="one">Rerouted 1 time since %2$s</item>
|
||||||
<item quantity="other">Connected %1$s times since %2$s</item>
|
<item quantity="other">Rerouted %1$s times since %2$s</item>
|
||||||
</plurals>
|
</plurals>
|
||||||
<plurals name="clients_stats_message_2">
|
<plurals name="clients_stats_message_2">
|
||||||
<item quantity="one">Sent 1 packet, %2$s</item>
|
<item quantity="one">Sent 1 packet, %2$s</item>
|
||||||
@@ -87,14 +87,14 @@
|
|||||||
ad-blockers and socksifiers.</string>
|
ad-blockers and socksifiers.</string>
|
||||||
<string name="settings_service_repeater_oc">Operating Wi\u2011Fi channel (unstable)</string>
|
<string name="settings_service_repeater_oc">Operating Wi\u2011Fi channel (unstable)</string>
|
||||||
<string name="settings_service_repeater_oc_summary">Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)</string>
|
<string name="settings_service_repeater_oc_summary">Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)</string>
|
||||||
<string name="settings_service_repeater_strict">Strict mode</string>
|
|
||||||
<string name="settings_service_repeater_strict_summary">Only allow packets that goes through VPN tunnel.</string>
|
|
||||||
<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_repeater_start_on_boot">Start repeater on boot</string>
|
<string name="settings_service_repeater_start_on_boot">Start repeater on boot</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">Upstream network interface</string>
|
||||||
<string name="settings_service_upstream_auto">Auto detect system VPN</string>
|
<string name="settings_service_upstream_auto">Auto detect system VPN</string>
|
||||||
|
<string name="settings_upstream_fallback">Fallback upstream interface</string>
|
||||||
|
<string name="settings_upstream_fallback_auto">Auto detect system default network</string>
|
||||||
<string name="settings_service_dhcp_workaround">Enable DHCP workaround</string>
|
<string name="settings_service_dhcp_workaround">Enable DHCP workaround</string>
|
||||||
<string name="settings_service_dhcp_workaround_summary">Use this if clients cannot obtain IP addresses.</string>
|
<string name="settings_service_dhcp_workaround_summary">Use this if clients cannot obtain IP addresses.</string>
|
||||||
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
<string name="settings_service_clean">Clean/reapply routing rules</string>
|
||||||
|
|||||||
@@ -14,6 +14,13 @@
|
|||||||
android:summary="@string/settings_service_upstream_auto"
|
android:summary="@string/settings_service_upstream_auto"
|
||||||
android:hint="@string/settings_service_upstream_auto"
|
android:hint="@string/settings_service_upstream_auto"
|
||||||
android:singleLine="true"/>
|
android:singleLine="true"/>
|
||||||
|
<be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreference
|
||||||
|
android:key="service.upstream.fallback"
|
||||||
|
android:icon="@drawable/ic_action_settings_input_component"
|
||||||
|
android:title="@string/settings_upstream_fallback"
|
||||||
|
android:summary="@string/settings_upstream_fallback_auto"
|
||||||
|
android:hint="@string/settings_upstream_fallback_auto"
|
||||||
|
android:singleLine="true"/>
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="service.masquerade"
|
android:key="service.masquerade"
|
||||||
android:icon="@drawable/ic_social_people"
|
android:icon="@drawable/ic_social_people"
|
||||||
@@ -23,11 +30,6 @@
|
|||||||
</PreferenceCategory>
|
</PreferenceCategory>
|
||||||
<PreferenceCategory
|
<PreferenceCategory
|
||||||
android:title="@string/settings_downstream">
|
android:title="@string/settings_downstream">
|
||||||
<SwitchPreference
|
|
||||||
android:key="service.repeater.strict"
|
|
||||||
android:icon="@drawable/ic_action_pan_tool"
|
|
||||||
android:title="@string/settings_service_repeater_strict"
|
|
||||||
android:summary="@string/settings_service_repeater_strict_summary"/>
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
android:key="service.disableIpv6"
|
android:key="service.disableIpv6"
|
||||||
android:icon="@drawable/ic_image_looks_6"
|
android:icon="@drawable/ic_image_looks_6"
|
||||||
|
|||||||
Reference in New Issue
Block a user