Support non-strict mode for repeater

This commit makes possible:

* Starting repeater without VPN;
* Repeater will work better with stub VPN apps;
* LAN addresses will be reachable.
This commit is contained in:
Mygod
2018-02-17 18:56:20 -08:00
parent 73b2b513f1
commit 2d30b5154b
9 changed files with 109 additions and 96 deletions

View File

@@ -138,11 +138,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
App.ACTION_CLEAN_ROUTINGS -> {
val routing = routing
routing!!.started = false
if (status == Status.ACTIVE) {
val upstream = upstream
if (upstream != null && !initRouting(upstream, routing.downstream, routing.hostAddress))
Toast.makeText(this@RepeaterService, R.string.noisy_su_failure, Toast.LENGTH_SHORT).show()
}
if (status == Status.ACTIVE) resetup(routing, upstream)
}
}
}
@@ -186,7 +182,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (status != Status.IDLE) return START_NOT_STICKY
status = Status.STARTING
VpnMonitor.registerCallback(this) { startFailure(getString(R.string.repeater_vpn_unavailable)) }
VpnMonitor.registerCallback(this) { setup() }
return START_NOT_STICKY
}
private fun startFailure(msg: CharSequence?, group: WifiP2pGroup? = null) {
@@ -198,45 +194,53 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
/**
* startService 2nd stop, also called when VPN re-established
*/
override fun onAvailable(ifname: String) = when (status) {
Status.STARTING -> {
val matcher = patternNetworkInfo.matcher(loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
when {
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
matcher.group(2) == "true" -> {
unregisterReceiver()
upstream = ifname
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
LocalBroadcastManager.getInstance(this)
.registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
receiverRegistered = true
p2pManager.requestGroupInfo(channel, {
when {
it == null -> doStart()
it.isGroupOwner -> doStart(it)
else -> {
Log.i(TAG, "Removing old group ($it)")
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() = doStart()
override fun onFailure(reason: Int) {
Toast.makeText(this@RepeaterService,
formatReason(R.string.repeater_remove_old_group_failure, reason),
Toast.LENGTH_SHORT).show()
}
})
}
private fun setup(ifname: String? = null) {
val matcher = patternNetworkInfo.matcher(loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "")
when {
!matcher.find() -> startFailure(getString(R.string.root_unavailable))
matcher.group(2) == "true" -> {
unregisterReceiver()
upstream = ifname
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
LocalBroadcastManager.getInstance(this)
.registerReceiver(receiver, intentFilter(App.ACTION_CLEAN_ROUTINGS))
receiverRegistered = true
p2pManager.requestGroupInfo(channel, {
when {
it == null -> doStart()
it.isGroupOwner -> doStart(it)
else -> {
Log.i(TAG, "Removing old group ($it)")
p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener {
override fun onSuccess() = doStart()
override fun onFailure(reason: Int) {
Toast.makeText(this@RepeaterService,
formatReason(R.string.repeater_remove_old_group_failure, reason),
Toast.LENGTH_SHORT).show()
}
})
}
})
}
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
}
})
}
else -> startFailure(getString(R.string.repeater_p2p_unavailable))
}
}
private fun resetup(routing: Routing, ifname: String? = null) =
initRouting(ifname, routing.downstream, routing.hostAddress)
override fun onAvailable(ifname: String) = when (status) {
Status.STARTING -> setup(ifname)
Status.ACTIVE -> {
val routing = routing
check(!routing!!.started)
if (!initRouting(ifname, routing.downstream, routing.hostAddress))
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show() else { }
val routing = routing!!
if (routing.started) {
routing.stop()
check(routing.upstream == null)
}
resetup(routing, ifname)
while (false) { }
}
else -> throw IllegalStateException("RepeaterService is in unexpected state when receiving onAvailable")
}
@@ -244,6 +248,7 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
if (routing?.stop() == false)
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
upstream = null
if (status == Status.ACTIVE) resetup(routing!!)
}
private fun doStart() = p2pManager.createGroup(channel, object : WifiP2pManager.ActionListener {
@@ -275,22 +280,25 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, VpnMonitor.Ca
val downstream = group.`interface` ?: return
receiverRegistered = true
try {
if (initRouting(upstream ?: throw Routing.InterfaceNotFoundException(), downstream, owner))
doStart(group) else startFailure(getText(R.string.noisy_su_failure), group)
if (initRouting(upstream, downstream, owner)) doStart(group)
} catch (e: Routing.InterfaceNotFoundException) {
startFailure(e.message, group)
return
}
}
private fun initRouting(upstream: String, downstream: String, owner: InetAddress): Boolean {
private fun initRouting(upstream: String?, downstream: String, owner: InetAddress): Boolean {
val routing = Routing(upstream, downstream, owner)
.ipForward() // Wi-Fi direct doesn't enable ip_forward
.rule().forward().dnsRedirect(app.dns)
return if (routing.start()) {
this.routing = routing
true
} else {
this.routing = routing
val strict = app.pref.getBoolean("service.repeater.strict", false)
if (strict && upstream == null) return true // in this case, nothing to be done
return if (routing
.ipForward() // Wi-Fi direct doesn't enable ip_forward
.rule()
.forward(strict)
.dnsRedirect(app.dns)
.start()) true else {
routing.stop()
Toast.makeText(this, getText(R.string.noisy_su_failure), Toast.LENGTH_SHORT).show()
false
}
}

View File

@@ -11,10 +11,9 @@ import android.service.quicksettings.TileService
import android.support.annotation.RequiresApi
import android.support.v4.content.ContextCompat
import android.support.v4.content.LocalBroadcastManager
import be.mygod.vpnhotspot.net.VpnMonitor
@RequiresApi(24)
class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callback {
class RepeaterTileService : TileService(), ServiceConnection {
private val statusListener = broadcastReceiver { _, _ -> updateTile() }
private val tileOff by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_off) }
private val tileOn by lazy { Icon.createWithResource(application, R.drawable.ic_quick_settings_tile_on) }
@@ -44,7 +43,6 @@ class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callbac
override fun onServiceConnected(name: ComponentName?, service: IBinder) {
binder = service as RepeaterService.RepeaterBinder
updateTile()
VpnMonitor.registerCallback(this)
LocalBroadcastManager.getInstance(this).registerReceiver(statusListener,
intentFilter(RepeaterService.ACTION_STATUS_CHANGED))
}
@@ -52,11 +50,10 @@ class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callbac
override fun onServiceDisconnected(name: ComponentName?) {
binder = null
LocalBroadcastManager.getInstance(this).unregisterReceiver(statusListener)
VpnMonitor.unregisterCallback(this)
}
private fun updateTile() {
when (if (VpnMonitor.available.isEmpty()) null else binder?.service?.status) {
when (binder?.service?.status) {
RepeaterService.Status.IDLE -> {
qsTile.state = Tile.STATE_INACTIVE
qsTile.icon = tileOff
@@ -80,6 +77,4 @@ class RepeaterTileService : TileService(), ServiceConnection, VpnMonitor.Callbac
}
qsTile.updateTile()
}
override fun onAvailable(ifname: String) = updateTile()
override fun onLost(ifname: String) = updateTile()
}

View File

@@ -47,7 +47,9 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
if (upstream != null) {
var failed = false
for ((downstream, value) in routings) if (value == null) {
val routing = Routing(upstream, downstream).rule().forward().dnsRedirect(app.dns)
// system tethering already has working forwarding rules
// so it doesn't make sense to add additional forwarding rules
val routing = Routing(upstream, downstream).rule().forward(true).dnsRedirect(app.dns)
if (routing.start()) routings[downstream] = routing else {
failed = true
routing.stop()

View File

@@ -10,7 +10,7 @@ import java.net.InetAddress
import java.net.NetworkInterface
import java.util.*
class Routing(private val upstream: String, val downstream: String, ownerAddress: InetAddress? = null) {
class Routing(val upstream: String?, val downstream: String, ownerAddress: InetAddress? = null) {
companion object {
/**
* -w <seconds> is not supported on 7.1-.
@@ -23,6 +23,7 @@ class Routing(private val upstream: String, val downstream: String, ownerAddress
fun clean() = noisySu(
"$IPTABLES -t nat -F PREROUTING",
"quiet while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done",
"quiet while $IPTABLES -t nat -D POSTROUTING -j MASQUERADE; do done",
"$IPTABLES -F vpnhotspot_fwd",
"$IPTABLES -X vpnhotspot_fwd",
"quiet while ip rule del priority 17900; do done")
@@ -50,19 +51,33 @@ class Routing(private val upstream: String, val downstream: String, ownerAddress
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65
*/
fun rule(): Routing {
startScript.add("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
stopScript.addFirst("ip rule del from all iif $downstream priority 17900")
if (upstream != null) {
startScript.add("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
stopScript.addFirst("ip rule del from all iif $downstream priority 17900")
}
return this
}
fun forward(): Routing {
fun forward(strict: Boolean = true): Routing {
startScript.add("quiet $IPTABLES -N vpnhotspot_fwd 2>/dev/null")
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
if (strict) {
check(upstream != null)
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
} else {
// for not strict mode, allow downstream packets to be redirected to anywhere
// also enable unconditional NAT masquerade
startScript.add("$IPTABLES -A vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -j ACCEPT")
startScript.add("$IPTABLES -t nat -A POSTROUTING -j MASQUERADE")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -j ACCEPT")
stopScript.addFirst("$IPTABLES -t nat -D POSTROUTING -j MASQUERADE")
}
startScript.add("$IPTABLES -I FORWARD -j vpnhotspot_fwd")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT")
stopScript.addFirst("$IPTABLES -D FORWARD -j vpnhotspot_fwd")
return this
}

View File

@@ -45,28 +45,20 @@ object VpnMonitor : ConnectivityManager.NetworkCallback() {
fun registerCallback(callback: Callback, failfast: (() -> Unit)? = null) {
if (synchronized(this) {
if (!callbacks.add(callback)) return
if (registered) {
if (failfast != null && available.isEmpty()) {
callbacks.remove(callback)
true
} else {
available.forEach { callback.onAvailable(it.value) }
false
}
} else if (failfast != null && manager.allNetworks.all {
val cap = manager.getNetworkCapabilities(it)
!cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
}) {
callbacks.remove(callback)
true
} else {
manager.registerNetworkCallback(request, this)
registered = true
false
}
}) failfast!!()
if (!callbacks.add(callback)) return
if (!registered) {
manager.registerNetworkCallback(request, this)
registered = true
manager.allNetworks.all {
val cap = manager.getNetworkCapabilities(it)
!cap.hasTransport(NetworkCapabilities.TRANSPORT_VPN) ||
cap.hasCapability(NetworkCapabilities.NET_CAPABILITY_NOT_VPN)
}
} else if (available.isEmpty()) true else {
available.forEach { callback.onAvailable(it.value) }
false
}
}) failfast?.invoke()
}
fun unregisterCallback(callback: Callback) = synchronized(this) {
if (!callbacks.remove(callback) || callbacks.isNotEmpty() || !registered) return