Allow clients on different tethered interfaces to communicate

Previously, the routing rules were too strict. We should probably also deprecate TrafficRecord.upstream sometime.
This commit is contained in:
Mygod
2019-01-01 12:18:17 +08:00
parent 8e2b27ff8e
commit 68fface4b9
6 changed files with 87 additions and 138 deletions

View File

@@ -4,9 +4,14 @@ import android.os.Build
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.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
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.net.monitor.UpstreamMonitor
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.RootSession
import be.mygod.vpnhotspot.util.computeIfAbsentCompat
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.lang.RuntimeException
@@ -18,7 +23,7 @@ import java.util.concurrent.atomic.AtomicLong
* *
* Once revert is called, this object no longer serves any purpose. * Once revert is called, this object no longer serves any purpose.
*/ */
class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) { class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : IpNeighbourMonitor.Callback {
companion object { companion object {
/** /**
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000. * Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
@@ -55,9 +60,9 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
} }
} }
fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") = private fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
exec("$IPTABLES -t $table -A $content", "$IPTABLES -t $table -D $content", true) exec("$IPTABLES -t $table -A $content", "$IPTABLES -t $table -D $content", true)
fun RootSession.Transaction.iptablesInsert(content: String, table: String = "filter") = private fun RootSession.Transaction.iptablesInsert(content: String, table: String = "filter") =
exec("$IPTABLES -t $table -I $content", "$IPTABLES -t $table -D $content", true) exec("$IPTABLES -t $table -I $content", "$IPTABLES -t $table -D $content", true)
} }
@@ -74,6 +79,24 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
private val upstreams = HashSet<String>() private val upstreams = HashSet<String>()
private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback { private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback {
/**
* The only case when upstream is null is on API 23- and we are using system default rules.
*/
inner class Subrouting(priority: Int, val upstream: String? = null) {
val transaction = RootSession.beginTransaction().safeguard {
if (upstream != null) {
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 (hasMasquerade) {
iptablesAdd(if (upstream == null) "vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE" else
"vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
}
}
}
var subrouting: Subrouting? = null var subrouting: Subrouting? = null
var dns: List<InetAddress> = emptyList() var dns: List<InetAddress> = emptyList()
@@ -83,7 +106,7 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
subrouting != null -> check(subrouting.upstream == ifname) subrouting != null -> check(subrouting.upstream == ifname)
!upstreams.add(ifname) -> return !upstreams.add(ifname) -> return
else -> this.subrouting = try { else -> this.subrouting = try {
Subrouting(this@Routing, priority, ifname) Subrouting(priority, ifname)
} catch (e: Exception) { } catch (e: Exception) {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
Timber.w(e) Timber.w(e)
@@ -98,9 +121,7 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
val subrouting = subrouting ?: return val subrouting = subrouting ?: return
// we could be removing fallback subrouting which no collision could ever happen, check before removing // we could be removing fallback subrouting which no collision could ever happen, check before removing
if (subrouting.upstream != null) check(upstreams.remove(subrouting.upstream)) if (subrouting.upstream != null) check(upstreams.remove(subrouting.upstream))
subrouting.close() subrouting.transaction.revert()
TrafficRecorder.update() // record stats before removing rules to prevent stats losing
subrouting.revert()
this.subrouting = null this.subrouting = null
dns = emptyList() dns = emptyList()
updateDnsRoute() updateDnsRoute()
@@ -110,7 +131,7 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
override fun onFallback() = synchronized(this@Routing) { override fun onFallback() = synchronized(this@Routing) {
check(subrouting == null) check(subrouting == null)
subrouting = try { subrouting = try {
Subrouting(this@Routing, priority) Subrouting(priority)
} catch (e: Exception) { } catch (e: Exception) {
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
Timber.w(e) Timber.w(e)
@@ -121,6 +142,47 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
} }
private val upstream = Upstream(RULE_PRIORITY_UPSTREAM) private val upstream = Upstream(RULE_PRIORITY_UPSTREAM)
private inner class Client(private val ip: Inet4Address, mac: String) : AutoCloseable {
private val transaction = RootSession.beginTransaction().safeguard {
val address = ip.hostAddress
iptablesInsert("vpnhotspot_fwd -i $downstream -s $address -j ACCEPT")
iptablesInsert("vpnhotspot_fwd -o $downstream -d $address -m state --state ESTABLISHED,RELATED -j ACCEPT")
}
init {
try {
TrafficRecorder.register(ip, downstream, mac)
} catch (e: Exception) {
close()
throw e
}
}
override fun close() {
TrafficRecorder.unregister(ip, downstream)
transaction.revert()
}
}
private val clients = HashMap<InetAddress, Client>()
override fun onIpNeighbourAvailable(neighbours: List<IpNeighbour>) = synchronized(this) {
val toRemove = HashSet(clients.keys)
for (neighbour in neighbours) {
if (neighbour.dev != downstream || neighbour.ip !is Inet4Address ||
AppDatabase.instance.clientRecordDao.lookup(neighbour.lladdr.macToLong()).blocked) continue
toRemove.remove(neighbour.ip)
try {
clients.computeIfAbsentCompat(neighbour.ip) { Client(neighbour.ip, neighbour.lladdr) }
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
}
}
if (toRemove.isNotEmpty()) {
TrafficRecorder.update() // record stats before removing rules to prevent stats losing
for (address in toRemove) clients.remove(address)!!.close()
}
}
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",
@@ -181,22 +243,23 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) {
} }
fun stop() { fun stop() {
IpNeighbourMonitor.unregisterCallback(this)
FallbackUpstreamMonitor.unregisterCallback(fallbackUpstream) FallbackUpstreamMonitor.unregisterCallback(fallbackUpstream)
fallbackUpstream.subrouting?.close()
UpstreamMonitor.unregisterCallback(upstream) UpstreamMonitor.unregisterCallback(upstream)
upstream.subrouting?.close()
} }
fun commit() { fun commit() {
transaction.commit() transaction.commit()
FallbackUpstreamMonitor.registerCallback(fallbackUpstream) FallbackUpstreamMonitor.registerCallback(fallbackUpstream)
UpstreamMonitor.registerCallback(upstream) UpstreamMonitor.registerCallback(upstream)
IpNeighbourMonitor.registerCallback(this)
} }
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
fallbackUpstream.subrouting?.revert() clients.forEach { (_, subroute) -> subroute.close() }
upstream.subrouting?.revert() fallbackUpstream.subrouting?.transaction?.revert()
upstream.subrouting?.transaction?.revert()
transaction.revert() transaction.revert()
} }
} }

View File

@@ -1,110 +0,0 @@
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 {
try {
TrafficRecorder.register(ip, upstream, parent.downstream, mac)
} catch (e: Exception) {
close()
throw e
}
}
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 {
Timber.d("Subrouting initialized from %s to %s", parent.downstream, upstream)
try {
IpNeighbourMonitor.registerCallback(this)
} catch (e: Exception) {
close()
revert()
throw e
}
}
/**
* Unregister client listener. This should be always called even after clean.
*/
override fun close() {
IpNeighbourMonitor.unregisterCallback(this)
Timber.d("Subrouting closed from %s to %s", parent.downstream, upstream)
}
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).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

@@ -22,27 +22,25 @@ object TrafficRecorder {
private var scheduled = false private var scheduled = false
private var lastUpdate = 0L private var lastUpdate = 0L
private val records = HashMap<Triple<InetAddress, String?, String>, TrafficRecord>() private val records = HashMap<Pair<InetAddress, 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, downstream: String, mac: String) {
val record = TrafficRecord( val record = TrafficRecord(
mac = mac.macToLong(), mac = mac.macToLong(),
ip = ip, ip = ip,
upstream = upstream,
downstream = downstream) downstream = downstream)
AppDatabase.instance.trafficRecordDao.insert(record) AppDatabase.instance.trafficRecordDao.insert(record)
synchronized(this) { synchronized(this) {
DebugHelper.log(TAG, "Registering ($ip, $upstream, $downstream)") DebugHelper.log(TAG, "Registering $ip%$downstream")
check(records.put(Triple(ip, upstream, downstream), record) == null) check(records.put(Pair(ip, downstream), record) == null)
scheduleUpdateLocked() scheduleUpdateLocked()
} }
} }
fun unregister(ip: InetAddress, upstream: String?, downstream: String) = synchronized(this) { fun unregister(ip: InetAddress, downstream: String) = synchronized(this) {
update() // flush stats before removing update() // flush stats before removing
DebugHelper.log(TAG, "Unregistering ($ip, $upstream, $downstream)") DebugHelper.log(TAG, "Unregistering $ip%$downstream")
if (records.remove(Triple(ip, upstream, downstream)) == null) Timber.w( if (records.remove(Pair(ip, downstream)) == null) Timber.w("Failed to find traffic record for $ip%$downstream.")
"Failed to find traffic record for ($ip, $downstream, $upstream).")
} }
private fun unscheduleUpdateLocked() { private fun unscheduleUpdateLocked() {
@@ -78,15 +76,12 @@ object TrafficRecorder {
check(isReceive != isSend) // this check might fail when the user performed an upgrade from 1.x check(isReceive != isSend) // this check might fail when the user performed an upgrade from 1.x
val ip = parseNumericAddress(columns[if (isReceive) 8 else 7]) val ip = parseNumericAddress(columns[if (isReceive) 8 else 7])
val downstream = columns[if (isReceive) 6 else 5] val downstream = columns[if (isReceive) 6 else 5]
var upstream: String? = columns[if (isReceive) 5 else 6] val key = Pair(ip, downstream)
if (upstream == "*") upstream = null
val key = Triple(ip, upstream, downstream)
val oldRecord = records[key] ?: continue@loop // assuming they're legacy old rules val oldRecord = records[key] ?: continue@loop // assuming they're legacy old rules
val record = if (oldRecord.id == null) oldRecord else TrafficRecord( val record = if (oldRecord.id == null) oldRecord else TrafficRecord(
timestamp = timestamp, timestamp = timestamp,
mac = oldRecord.mac, mac = oldRecord.mac,
ip = ip, ip = ip,
upstream = upstream,
downstream = downstream, downstream = downstream,
sentPackets = -1, sentPackets = -1,
sentBytes = -1, sentBytes = -1,

View File

@@ -27,6 +27,7 @@ data class TrafficRecord(
* For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case. * For now only stats for IPv4 will be recorded. But I'm going to put the more general class here just in case.
*/ */
val ip: InetAddress, val ip: InetAddress,
@Deprecated("This field is no longer used.")
val upstream: String? = null, val upstream: String? = null,
val downstream: String, val downstream: String,
var sentPackets: Long = 0, var sentPackets: Long = 0,

View File

@@ -66,7 +66,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>

View File

@@ -70,8 +70,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">Rerouted 1 time since %2$s</item> <item quantity="one">Connected 1 time since %2$s</item>
<item quantity="other">Rerouted %1$s times since %2$s</item> <item quantity="other">Connected %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>