diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt index 8eb2aea8..13253b09 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/App.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/App.kt @@ -80,8 +80,6 @@ class App : Application() { .build() } - val masquerade get() = pref.getBoolean("service.masquerade", true) - val onPreCleanRoutings = Event0() val onRoutingsCleaned = Event0() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt index f89b32c5..2e1b8892 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/LocalOnlyInterfaceManager.kt @@ -23,10 +23,10 @@ class LocalOnlyInterfaceManager(val downstream: String) { routing = try { Routing(downstream, owner).apply { try { - ipForward() // local only interfaces need to enable ip_forward + ipForward() // local only interfaces need to enable ip_forward forward() - if (app.masquerade) masquerade() - commit() + masquerade(Routing.masquerade) + commit(true) } catch (e: Exception) { revert() throw e diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt index 62d2df97..880d4675 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsPreferenceFragment.kt @@ -30,6 +30,7 @@ import java.net.SocketException class SettingsPreferenceFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { preferenceManager.preferenceDataStore = SharedPreferenceDataStore(app.pref) + Routing.masquerade = Routing.masquerade // flush default value addPreferencesFromResource(R.xml.pref_settings) val boot = findPreference("service.repeater.startOnBoot") as SwitchPreference if (RepeaterService.supported) { diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt index 07a79517..b3095571 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/TetheringService.kt @@ -69,10 +69,8 @@ class TetheringService : IpNeighbourMonitoringService() { try { routings[downstream] = Routing(downstream).apply { try { - // system tethering already has working forwarding rules - // so it doesn't make sense to add additional forwarding rules forward() - if (app.masquerade) masquerade() + masquerade(Routing.masquerade) if (disableIpv6) disableIpv6() commit() } catch (e: Exception) { 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 2e3968f2..535029c8 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/Routing.kt @@ -26,6 +26,8 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : * Since Android 5.0, RULE_PRIORITY_TETHERING = 18000. * This also works for Wi-Fi direct where there's no rule at 18000. * + * We override system tethering rules by adding our own rules at higher priority. + * * Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65 */ private const val RULE_PRIORITY_UPSTREAM = 17800 @@ -39,6 +41,15 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : */ val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w" const val KEY_DNS = "service.dns" + const val KEY_MASQUERADE = "service.masqueradeMode" + + var masquerade: MasqueradeMode + get() { + app.pref.getString(KEY_MASQUERADE, null)?.let { return MasqueradeMode.valueOf(it) } + return if (app.pref.getBoolean("service.masquerade", true)) // legacy settings + MasqueradeMode.Simple else MasqueradeMode.None + } + set(value) = app.pref.edit().putString(KEY_MASQUERADE, value.name).apply() fun clean() { TrafficRecorder.clean() @@ -64,6 +75,15 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : iptables("$IPTABLES -t $table -A $content", "$IPTABLES -t $table -D $content") private fun RootSession.Transaction.iptablesInsert(content: String, table: String = "filter") = iptables("$IPTABLES -t $table -I $content", "$IPTABLES -t $table -D $content") + + private fun RootSession.Transaction.ndc(name: String, command: String, revert: String) { + val result = execQuiet(command, revert) + RootSession.checkOutput(command, result, result.out.joinToString("\n") != "200 0 $name operation succeeded") + } + } + + enum class MasqueradeMode { + None, Simple, Netd } class InterfaceNotFoundException : SocketException() { @@ -75,7 +95,7 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}" private val transaction = RootSession.beginTransaction() - var hasMasquerade = false + var masqueradeMode = MasqueradeMode.None private val upstreams = HashSet() private open inner class Upstream(val priority: Int) : UpstreamMonitor.Callback { @@ -89,10 +109,22 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : // 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") + when (masqueradeMode) { + // note: specifying -i wouldn't work for POSTROUTING + MasqueradeMode.Simple -> { + iptablesAdd(if (upstream == null) "vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE" else + "vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat") + } + MasqueradeMode.Netd -> { + check(upstream != null) + /** + * 0 means that there are no interface addresses coming after, which is unused anyway. + * + * https://android.googlesource.com/platform/frameworks/base/+/android-5.0.0_r1/services/core/java/com/android/server/NetworkManagementService.java#1251 + * https://android.googlesource.com/platform/system/netd/+/android-5.0.0_r1/server/CommandListener.cpp#638 + */ + ndc("Nat", "ndc nat enable $downstream $upstream 0", "ndc nat disable $downstream $upstream 0") + } } } } @@ -196,10 +228,8 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : */ fun ipForward() { if (Build.VERSION.SDK_INT >= 23) { - val command = "ndc ipfwd enable vpnhotspot_$downstream" - val result = transaction.execQuiet(command, "ndc ipfwd disable vpnhotspot_$downstream") - RootSession.checkOutput(command, result, result.out.joinToString("\n") != - "200 0 ipfwd operation succeeded") + transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream", + "ndc ipfwd disable vpnhotspot_$downstream") } else transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward") } @@ -218,11 +248,13 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : // the real forwarding filters will be added in Subrouting when clients are connected } - fun masquerade() { - transaction.execQuiet("$IPTABLES -t nat -N vpnhotspot_masquerade") - transaction.iptablesInsert("POSTROUTING -j vpnhotspot_masquerade", "nat") - hasMasquerade = true - // further rules are added when upstreams are found + fun masquerade(mode: MasqueradeMode) { + masqueradeMode = mode + if (mode == MasqueradeMode.Simple) { + transaction.execQuiet("$IPTABLES -t nat -N vpnhotspot_masquerade") + transaction.iptablesInsert("POSTROUTING -j vpnhotspot_masquerade", "nat") + // further rules are added when upstreams are found + } } private inner class DnsRoute(val dns: String) { @@ -255,10 +287,12 @@ class Routing(val downstream: String, ownerAddress: InterfaceAddress? = null) : UpstreamMonitor.unregisterCallback(upstream) } - fun commit() { + fun commit(localOnly: Boolean = false) { transaction.commit() Timber.i("Started routing for $downstream") - FallbackUpstreamMonitor.registerCallback(fallbackUpstream) + if (localOnly || masqueradeMode != MasqueradeMode.Netd) { + FallbackUpstreamMonitor.registerCallback(fallbackUpstream) + } UpstreamMonitor.registerCallback(upstream) IpNeighbourMonitor.registerCallback(this) } diff --git a/mobile/src/main/res/values-zh-rCN/strings.xml b/mobile/src/main/res/values-zh-rCN/strings.xml index 62cc276f..cd32c5e0 100644 --- a/mobile/src/main/res/values-zh-rCN/strings.xml +++ b/mobile/src/main/res/values-zh-rCN/strings.xml @@ -80,8 +80,10 @@ 上游 下游 - IP 掩蔽 - 建议使用广告拦截器与 socksfier 等虚拟 VPN 应用时禁用此选项。 + IP 掩蔽模式 + + 简易 + Android Netd 服务 Wi\u2011Fi 运行频段 (不稳定) "自动 (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz)" 禁用 IPv6 共享 diff --git a/mobile/src/main/res/values/arrays.xml b/mobile/src/main/res/values/arrays.xml index b532e81c..de210ed6 100644 --- a/mobile/src/main/res/values/arrays.xml +++ b/mobile/src/main/res/values/arrays.xml @@ -1,5 +1,16 @@ + + @string/settings_service_masquerade_none + @string/settings_service_masquerade_simple + @string/settings_service_masquerade_netd + + + None + Simple + Netd + + @string/settings_service_wifi_lock_none @string/settings_service_wifi_lock_full @@ -10,6 +21,7 @@ Full HighPerf + @string/settings_service_ip_monitor_monitor @string/settings_service_ip_monitor_monitor_root diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 513e0564..9e80294e 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -86,9 +86,10 @@ Upstream Downstream - IP Masquerade - Recommended to disable this option for dummy VPNs like - ad-blockers and socksifiers. + IP Masquerade Mode + None + Simple + Android Netd Service Operating Wi\u2011Fi channel (unstable) Auto (1\u201114 = 2.4GHz, 15\u2011165 = 5GHz) Disable IPv6 tethering diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml index 1a44c4c1..4a5694b5 100644 --- a/mobile/src/main/res/xml/pref_settings.xml +++ b/mobile/src/main/res/xml/pref_settings.xml @@ -17,12 +17,13 @@ app:icon="@drawable/ic_action_settings_input_component" app:title="@string/settings_upstream_fallback" app:summary="@string/settings_upstream_fallback_auto"/> - + app:entries="@array/settings_service_masquerade" + app:entryValues="@array/settings_service_masquerade_values" + app:useSimpleSummaryProvider="true"/> @@ -52,8 +53,8 @@ app:entries="@array/settings_service_wifi_lock" app:entryValues="@array/settings_service_wifi_lock_values" app:defaultValue="Full" - app:summary="%s" - app:title="@string/settings_service_wifi_lock"/> + app:title="@string/settings_service_wifi_lock" + app:useSimpleSummaryProvider="true"/> + app:title="@string/settings_service_ip_monitor" + app:useSimpleSummaryProvider="true"/>