From 2d30b5154b2c8d991876b46970a5b114f897b9a2 Mon Sep 17 00:00:00 2001 From: Mygod Date: Sat, 17 Feb 2018 18:56:20 -0800 Subject: [PATCH] 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. --- README.md | 5 - .../be/mygod/vpnhotspot/RepeaterService.kt | 108 ++++++++++-------- .../mygod/vpnhotspot/RepeaterTileService.kt | 9 +- .../be/mygod/vpnhotspot/TetheringService.kt | 4 +- .../java/be/mygod/vpnhotspot/net/Routing.kt | 33 ++++-- .../be/mygod/vpnhotspot/net/VpnMonitor.kt | 36 +++--- mobile/src/main/res/values-zh-rCN/strings.xml | 3 +- mobile/src/main/res/values/strings.xml | 3 +- mobile/src/main/res/xml/pref_settings.xml | 4 + 9 files changed, 109 insertions(+), 96 deletions(-) diff --git a/README.md b/README.md index d11aad67..44cf169a 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,6 @@ You'll have to use WPS for now to make the repeater switch to 2.4GHz. ### [IPv6 tethering?](https://github.com/Mygod/VPNHotspot/issues/6) -### Start repeater without VPN? - -This app is designed for VPN interface as upstreams to simplify handling of connection change and NAT issues. -You could use stub VPN apps like [Blokada](https://github.com/blokadaorg/blokada) together with this app as a workaround. - ### No root? Without root, you can only: diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt index 65011cbd..af9be687 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterService.kt @@ -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 } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterTileService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterTileService.kt index 3637dbe6..97793527 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterTileService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterTileService.kt @@ -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() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index 4af51381..89bf7d52 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -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() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt index f0fb055c..16615e38 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt @@ -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 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 } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt index 732cdc66..749ce158 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/VpnMonitor.kt @@ -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 diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index 27d34814..5612bf32 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -21,7 +21,6 @@ 重置凭据失败(原因:%s) 未打开 - 无法找到 VPN,请启用任意 VPN Wi-Fi 直连不可用 创建 P2P 群组失败(原因:%s) 关闭已有 P2P 群组失败(原因:%s) @@ -42,6 +41,8 @@ 服务 下游 DNS 服务器[:端口] + 严格模式 (仅用于中继) + 只允许通过 VPN 隧道的包通过 清理/重新应用路由规则 杂项 导出日志 diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index b350a374..2d6af47a 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -23,7 +23,6 @@ Failed to reset credentials (reason: %s) Service inactive - VPN unavailable, please enable any VPN Wi-Fi direct unavailable Failed to create P2P group (reason: %s) Failed to remove P2P group (reason: %s) @@ -44,6 +43,8 @@ Service Downstream DNS server[:port] + Strict mode (repeater only) + Only allow packets that goes through VPN tunnel Clean/reapply routing rules Misc Export logcat diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml index 8ba09a2d..60c641a7 100644 --- a/mobile/src/main/res/xml/pref_settings.xml +++ b/mobile/src/main/res/xml/pref_settings.xml @@ -8,6 +8,10 @@ android:summary="%s" android:singleLine="true" android:defaultValue="8.8.8.8"/> +