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:
Mygod
2018-10-03 13:02:28 +08:00
parent 8e3567954e
commit 8e09e8cd8a
26 changed files with 573 additions and 361 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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