From dc2db049c760f2d2c86a59a60ea139367ae5c212 Mon Sep 17 00:00:00 2001 From: Mygod Date: Fri, 5 Jan 2018 00:25:56 +0800 Subject: [PATCH] Implement VPN over native AP --- mobile/src/main/AndroidManifest.xml | 3 +- .../be/mygod/vpnhotspot/HotspotService.kt | 235 ++++++++++-------- .../java/be/mygod/vpnhotspot/MainActivity.kt | 22 +- .../main/java/be/mygod/vpnhotspot/NetUtils.kt | 35 +++ .../main/java/be/mygod/vpnhotspot/Routing.kt | 77 ++++++ .../be/mygod/vpnhotspot/SettingsFragment.kt | 15 +- .../main/java/be/mygod/vpnhotspot/Utils.kt | 13 +- mobile/src/main/res/values/strings.xml | 1 + mobile/src/main/res/xml/pref_settings.xml | 5 + 9 files changed, 271 insertions(+), 135 deletions(-) create mode 100644 mobile/src/main/java/be/mygod/vpnhotspot/Routing.kt diff --git a/mobile/src/main/AndroidManifest.xml b/mobile/src/main/AndroidManifest.xml index 7863af97..4a3d7529 100644 --- a/mobile/src/main/AndroidManifest.xml +++ b/mobile/src/main/AndroidManifest.xml @@ -2,7 +2,8 @@ - + + diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/HotspotService.kt b/mobile/src/main/java/be/mygod/vpnhotspot/HotspotService.kt index 9097768a..2886264e 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/HotspotService.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/HotspotService.kt @@ -5,6 +5,8 @@ import android.app.Service import android.content.Context import android.content.Intent import android.net.NetworkInfo +import android.net.wifi.WifiConfiguration +import android.net.wifi.WifiManager import android.net.wifi.p2p.WifiP2pGroup import android.net.wifi.p2p.WifiP2pInfo import android.net.wifi.p2p.WifiP2pManager @@ -16,7 +18,6 @@ import android.support.v4.content.LocalBroadcastManager import android.util.Log import android.widget.Toast import be.mygod.vpnhotspot.App.Companion.app -import java.net.NetworkInterface import java.util.regex.Pattern class HotspotService : Service(), WifiP2pManager.ChannelListener { @@ -24,7 +25,15 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener { const val CHANNEL = "hotspot" const val STATUS_CHANGED = "be.mygod.vpnhotspot.HotspotService.STATUS_CHANGED" const val KEY_UPSTREAM = "service.upstream" + const val KEY_WIFI = "service.wifi" private const val TAG = "HotspotService" + // constants from WifiManager + private const val WIFI_AP_STATE_CHANGED_ACTION = "android.net.wifi.WIFI_AP_STATE_CHANGED" + private const val WIFI_AP_STATE_ENABLED = 13 + + private val upstream get() = app.pref.getString(KEY_UPSTREAM, "tun0") + private val wifi get() = app.pref.getString(KEY_WIFI, "wlan0") + private val dns get() = app.pref.getString("service.dns", "8.8.8.8:53") /** * Matches the output of dumpsys wifip2p. This part is available since Android 4.2. @@ -37,10 +46,17 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener { * https://android.googlesource.com/platform/frameworks/base.git/+/220871a/core/java/android/net/NetworkInfo.java#415 */ private val patternNetworkInfo = "^mNetworkInfo .* (isA|a)vailable: (true|false)".toPattern(Pattern.MULTILINE) + + private val isWifiApEnabledMethod = WifiManager::class.java.getDeclaredMethod("isWifiApEnabled") + val WifiManager.isWifiApEnabled get() = isWifiApEnabledMethod.invoke(this) as Boolean + + init { + isWifiApEnabledMethod.isAccessible = true + } } enum class Status { - IDLE, STARTING, ACTIVE + IDLE, STARTING, ACTIVE_P2P, ACTIVE_AP } inner class HotspotBinder : Binder() { @@ -48,25 +64,33 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener { var data: MainActivity.Data? = null fun shutdown() { - p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { - override fun onSuccess() = clean() - override fun onFailure(reason: Int) { - if (reason == WifiP2pManager.BUSY) clean() else { // assuming it's already gone - Toast.makeText(this@HotspotService, "Failed to remove P2P group (${formatReason(reason)})", - Toast.LENGTH_SHORT).show() - LocalBroadcastManager.getInstance(this@HotspotService).sendBroadcast(Intent(STATUS_CHANGED)) + when (status) { + Status.ACTIVE_P2P -> p2pManager.removeGroup(channel, object : WifiP2pManager.ActionListener { + override fun onSuccess() = clean() + override fun onFailure(reason: Int) { + if (reason == WifiP2pManager.BUSY) clean() else { // assuming it's already gone + Toast.makeText(this@HotspotService, "Failed to remove P2P group (${formatReason(reason)})", + Toast.LENGTH_SHORT).show() + LocalBroadcastManager.getInstance(this@HotspotService) + .sendBroadcast(Intent(STATUS_CHANGED)) + } } - } - }) + }) + else -> clean() + } } } - private lateinit var p2pManager: WifiP2pManager - private lateinit var channel: WifiP2pManager.Channel + private val wifiManager by lazy { getSystemService(Context.WIFI_SERVICE) as WifiManager } + private val p2pManager by lazy { getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager } + private var _channel: WifiP2pManager.Channel? = null + private val channel: WifiP2pManager.Channel get() { + if (_channel == null) onChannelDisconnected() + return _channel!! + } lateinit var group: WifiP2pGroup private set - var hostAddress: String? = null - private set + private var apConfiguration: WifiConfiguration? = null private val binder = HotspotBinder() private var receiverRegistered = false private val receiver = broadcastReceiver { _, intent -> @@ -78,25 +102,30 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener { val info = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO) val net = intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO) val group = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP) - if (downstream == null) onGroupCreated(info, group) + if (routing == null) onGroupCreated(info, group) this.group = group binder.data?.onGroupChanged() showNotification(group) Log.d(TAG, "${intent.action}: $info, $net, $group") } + WIFI_AP_STATE_CHANGED_ACTION -> + if (intent.getIntExtra(WifiManager.EXTRA_WIFI_STATE, 0) != WIFI_AP_STATE_ENABLED) clean() } } - var downstream: String? = null + val ssid get() = when (status) { + HotspotService.Status.ACTIVE_P2P -> group.networkName + HotspotService.Status.ACTIVE_AP -> apConfiguration?.SSID ?: "Unknown" + else -> null + } + val password get() = when (status) { + HotspotService.Status.ACTIVE_P2P -> group.passphrase + HotspotService.Status.ACTIVE_AP -> apConfiguration?.preSharedKey + else -> null + } + + var routing: Routing? = null private set - private val upstream get() = app.pref.getString(KEY_UPSTREAM, "tun0") - /** - * subnetPrefixLength has been the same forever but this option is here anyways. Source: - * https://android.googlesource.com/platform/frameworks/base/+/android-4.0.1_r1/wifi/java/android/net/wifi/p2p/WifiP2pService.java#1028 - * https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/a8d5e40/service/java/com/android/server/wifi/p2p/WifiP2pServiceImpl.java#2547 - */ - private var subnetPrefixLength: Short = 24 - private val dns get() = app.pref.getString("service.dns", "8.8.8.8:53") var status = Status.IDLE private set(value) { @@ -115,50 +144,58 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener { override fun onBind(intent: Intent) = binder - override fun onCreate() { - super.onCreate() - p2pManager = getSystemService(Context.WIFI_P2P_SERVICE) as WifiP2pManager - onChannelDisconnected() - } - override fun onChannelDisconnected() { - channel = p2pManager.initialize(this, Looper.getMainLooper(), this) + _channel = p2pManager.initialize(this, Looper.getMainLooper(), this) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (status != Status.IDLE) return START_NOT_STICKY status = Status.STARTING val matcher = patternNetworkInfo.matcher(loggerSu("dumpsys ${Context.WIFI_P2P_SERVICE}")) - if (!matcher.find()) { - startFailure("Root unavailable") - return START_NOT_STICKY - } - if (matcher.group(2) != "true") { - startFailure("Wi-Fi direct unavailable") - return START_NOT_STICKY - } - if (!receiverRegistered) { - registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION, - WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) - 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@HotspotService, - "Failed to remove old P2P group (${formatReason(reason)})", Toast.LENGTH_SHORT) - .show() + when { + !matcher.find() -> startFailure("Root unavailable") + matcher.group(2) == "true" -> { + unregisterReceiver() + registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION, + WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) + 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@HotspotService, + "Failed to remove old P2P group (${formatReason(reason)})", + Toast.LENGTH_SHORT).show() + } + }) } - }) - } + } + }) } - }) + wifiManager.isWifiApEnabled -> { + unregisterReceiver() + registerReceiver(receiver, intentFilter(WIFI_AP_STATE_CHANGED_ACTION)) + receiverRegistered = true + val routing = try { + Routing(upstream, wifi) + } catch (_: Routing.InterfaceNotFoundException) { + startFailure(getString(R.string.exception_interface_not_found)) + return START_NOT_STICKY + }.apRule().forward().dnsRedirect(dns) + if (routing.start()) { + this.routing = routing + apConfiguration = NetUtils.loadApConfiguration() + status = Status.ACTIVE_AP + showNotification() + } else startFailure("Something went wrong, please check logcat.") + } + else -> startFailure("Wi-Fi direct unavailable and hotspot disabled, please enable either") + } return START_NOT_STICKY } @@ -173,72 +210,50 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener { }) private fun doStart(group: WifiP2pGroup) { this.group = group - status = Status.ACTIVE + status = Status.ACTIVE_P2P showNotification(group) } private fun showNotification(group: WifiP2pGroup? = null) { - val deviceCount = group?.clientList?.size ?: 0 - startForeground(1, - NotificationCompat.Builder(this@HotspotService, CHANNEL) - .setWhen(0) - .setColor(ContextCompat.getColor(this@HotspotService, R.color.colorPrimary)) - .setContentTitle(group?.networkName) - .setContentText(group?.passphrase) - .setSubText(resources.getQuantityString(R.plurals.notification_connected_devices, - deviceCount, deviceCount)) - .setSmallIcon(R.drawable.ic_device_wifi_tethering) - .setContentIntent(PendingIntent.getActivity(this, 0, - Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) - .build()) + val builder = NotificationCompat.Builder(this, CHANNEL) + .setWhen(0) + .setColor(ContextCompat.getColor(this, R.color.colorPrimary)) + .setContentTitle(group?.networkName ?: ssid ?: "Connecting...") + .setSmallIcon(R.drawable.ic_device_wifi_tethering) + .setContentIntent(PendingIntent.getActivity(this, 0, + Intent(this, MainActivity::class.java), PendingIntent.FLAG_UPDATE_CURRENT)) + if (group != null) builder.setContentText(resources.getQuantityString(R.plurals.notification_connected_devices, + group.clientList.size, group.clientList.size)) + startForeground(1, builder.build()) } private fun onGroupCreated(info: WifiP2pInfo, group: WifiP2pGroup) { val owner = info.groupOwnerAddress - val hostAddress = owner?.hostAddress val downstream = group.`interface` - if (!info.groupFormed || !info.isGroupOwner || downstream == null || hostAddress == null) return - this.downstream = downstream - this.hostAddress = hostAddress - var subnetPrefixLength = NetworkInterface.getByName(downstream)?.interfaceAddresses - ?.singleOrNull { it.address == owner }?.networkPrefixLength - if (subnetPrefixLength == null) { - Log.w(TAG, "Unable to find prefix length of interface $downstream, 24 is assumed") - subnetPrefixLength = 24 - } - this.subnetPrefixLength = subnetPrefixLength - if (noisySu("echo 1 >/proc/sys/net/ipv4/ip_forward", - "ip route add default dev $upstream scope link table 62", - "ip route add $hostAddress/$subnetPrefixLength dev $downstream scope link table 62", - "ip route add broadcast 255.255.255.255 dev $downstream scope link table 62", - "ip rule add iif $downstream lookup 62", - "iptables -N vpnhotspot_fwd", - "iptables -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT", - "iptables -A vpnhotspot_fwd -i $downstream -o $upstream -j ACCEPT", - "iptables -I FORWARD -j vpnhotspot_fwd", - "iptables -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", - "iptables -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns")) { + if (!info.groupFormed || !info.isGroupOwner || downstream == null || owner == null) return + receiverRegistered = true + val routing = try { + Routing(upstream, downstream, owner) + } catch (_: Routing.InterfaceNotFoundException) { + startFailure(getString(R.string.exception_interface_not_found)) + return + }.p2pRule().forward().dnsRedirect(dns) + if (routing.start()) { + this.routing = routing doStart(group) } else startFailure("Something went wrong, please check logcat.") } - private fun clean() { + private fun unregisterReceiver() { if (receiverRegistered) { unregisterReceiver(receiver) receiverRegistered = false } - if (downstream != null) - if (noisySu("iptables -t nat -D PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", - "iptables -t nat -D PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", - "iptables -D FORWARD -j vpnhotspot_fwd", - "iptables -F vpnhotspot_fwd", - "iptables -X vpnhotspot_fwd", - "ip rule del iif $downstream lookup 62", - "ip route del broadcast 255.255.255.255 dev $downstream scope link table 62", - "ip route del $hostAddress/$subnetPrefixLength dev $downstream scope link table 62", - "ip route del default dev $upstream scope link table 62")) { - hostAddress = null - downstream = null - } else Toast.makeText(this, "Something went wrong, please check logcat.", Toast.LENGTH_SHORT).show() + } + private fun clean() { + unregisterReceiver() + if (routing?.stop() == false) + Toast.makeText(this, "Something went wrong, please check logcat.", Toast.LENGTH_SHORT).show() + routing = null status = Status.IDLE stopForeground(true) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt index 6891eef8..d36a0a07 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/MainActivity.kt @@ -24,14 +24,13 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC inner class Data : BaseObservable() { val switchEnabled: Boolean @Bindable get() = when (binder?.service?.status) { - HotspotService.Status.IDLE -> true - HotspotService.Status.ACTIVE -> true + HotspotService.Status.IDLE, HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP -> true else -> false } var serviceStarted: Boolean @Bindable get() = when (binder?.service?.status) { - HotspotService.Status.STARTING -> true - HotspotService.Status.ACTIVE -> true + HotspotService.Status.STARTING, HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP -> + true else -> false } set(value) { @@ -40,13 +39,12 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC HotspotService.Status.IDLE -> if (value) ContextCompat.startForegroundService(this@MainActivity, Intent(this@MainActivity, HotspotService::class.java)) - HotspotService.Status.ACTIVE -> if (!value) binder.shutdown() + HotspotService.Status.ACTIVE_P2P, HotspotService.Status.ACTIVE_AP -> if (!value) binder.shutdown() } } - val running get() = binder?.service?.status == HotspotService.Status.ACTIVE - val ssid: String @Bindable get() = if (running) binder!!.service.group.networkName else "" - val password: String @Bindable get() = if (running) binder!!.service.group.passphrase else "" + val ssid @Bindable get() = binder?.service?.ssid ?: "Service inactive" + val password @Bindable get() = binder?.service?.password ?: "" fun onStatusChanged() { notifyPropertyChanged(BR.switchEnabled) @@ -69,11 +67,11 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC private lateinit var arpCache: Map fun fetchClients() { - if (data.running) { - val binder = binder!! + val binder = binder + if (binder?.service?.status == HotspotService.Status.ACTIVE_P2P) { owner = binder.service.group.owner clients = binder.service.group.clientList - arpCache = NetUtils.arp(binder.service.downstream) + arpCache = NetUtils.arp(binder.service.routing?.downstream) } else owner = null notifyDataSetChanged() // recreate everything binding.swipeRefresher.isRefreshing = false @@ -89,7 +87,7 @@ class MainActivity : AppCompatActivity(), ServiceConnection, Toolbar.OnMenuItemC } holder.binding.device = device holder.binding.ipAddress = when (position) { - 0 -> binder?.service?.hostAddress + 0 -> binder?.service?.routing?.hostAddress else -> arpCache[device?.deviceAddress] } holder.binding.executePendingBindings() diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/NetUtils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/NetUtils.kt index ee4f0080..5f28b2e5 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/NetUtils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/NetUtils.kt @@ -1,8 +1,13 @@ package be.mygod.vpnhotspot +import android.net.wifi.WifiConfiguration +import android.util.Log +import java.io.DataInputStream import java.io.File +import java.io.IOException object NetUtils { + private const val TAG = "NetUtils" private val spaces = " +".toPattern() private val mac = "^([0-9a-f]{2}:){5}[0-9a-f]{2}$".toPattern() @@ -14,4 +19,34 @@ object NetUtils { mac.matcher(it[3]).matches() } .associateBy({ it[3] }, { it[0] }) } + + /** + * Load AP configuration from persistent storage. + * + * Based on: https://android.googlesource.com/platform/frameworks/opt/net/wifi/+/0cafbe0/service/java/com/android/server/wifi/WifiApConfigStore.java#138 + */ + fun loadApConfiguration(): WifiConfiguration? = try { + loggerSuStream("cat /data/misc/wifi/softap.conf").buffered().use { + val data = DataInputStream(it) + val version = data.readInt() + when (version) { + 1, 2 -> { + val config = WifiConfiguration() + config.SSID = data.readUTF() + if (version >= 2) data.readLong() // apBand and apChannel + val authType = data.readInt() + config.allowedKeyManagement.set(authType) + if (authType != WifiConfiguration.KeyMgmt.NONE) config.preSharedKey = data.readUTF() + config + } + else -> { + Log.e(TAG, "Bad version on hotspot configuration file $version") + null + } + } + } + } catch (e: IOException) { + Log.e(TAG, "Error reading hotspot configuration $e") + null + } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/Routing.kt b/mobile/src/main/java/be/mygod/vpnhotspot/Routing.kt new file mode 100644 index 00000000..7fbc10cc --- /dev/null +++ b/mobile/src/main/java/be/mygod/vpnhotspot/Routing.kt @@ -0,0 +1,77 @@ +package be.mygod.vpnhotspot + +import java.io.IOException +import java.net.Inet4Address +import java.net.InetAddress +import java.net.NetworkInterface +import java.util.* + +class Routing(private val upstream: String, val downstream: String, ownerAddress: InetAddress? = null) { + companion object { + fun clean() = noisySu( + "iptables -t nat -F PREROUTING", + "while iptables -D FORWARD -j vpnhotspot_fwd; do done", + "iptables -F vpnhotspot_fwd", + "iptables -X vpnhotspot_fwd", + "while ip rule del lookup 62; do done", + "ip route flush table 62", + "while ip rule del priority 17999; do done") + } + + class InterfaceNotFoundException : IOException() + + val hostAddress: String + private val subnetPrefixLength: Short + private val startScript = LinkedList() + private val stopScript = LinkedList() + init { + val address = NetworkInterface.getByName(downstream)?.interfaceAddresses + ?.singleOrNull { if (ownerAddress == null) it.address is Inet4Address else it.address == ownerAddress } + ?: throw InterfaceNotFoundException() + hostAddress = address.address.hostAddress + subnetPrefixLength = address.networkPrefixLength + } + + fun p2pRule(): Routing { + startScript.add("echo 1 >/proc/sys/net/ipv4/ip_forward") // Wi-Fi direct doesn't enable ip_forward + startScript.add("ip route add default dev $upstream scope link table 62") + startScript.add("ip route add $hostAddress/$subnetPrefixLength dev $downstream scope link table 62") + startScript.add("ip route add broadcast 255.255.255.255 dev $downstream scope link table 62") + startScript.add("ip rule add iif $downstream lookup 62") + stopScript.addFirst("ip route del default dev $upstream scope link table 62") + stopScript.addFirst("ip route del $hostAddress/$subnetPrefixLength dev $downstream scope link table 62") + stopScript.addFirst("ip route del broadcast 255.255.255.255 dev $downstream scope link table 62") + stopScript.addFirst("ip rule del iif $downstream lookup 62") + return this + } + + /* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000. + * https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#65 */ + fun apRule(): Routing { + startScript.add("ip rule add from all iif $downstream lookup $upstream priority 17999") + stopScript.addFirst("ip rule del from all iif $downstream lookup $upstream priority 17999") + return this + } + + fun forward(): Routing { + startScript.add("iptables -N vpnhotspot_fwd") + 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") + startScript.add("iptables -I FORWARD -j vpnhotspot_fwd") + stopScript.addFirst("iptables -X vpnhotspot_fwd") + stopScript.addFirst("iptables -F vpnhotspot_fwd") + stopScript.addFirst("iptables -D FORWARD -j vpnhotspot_fwd") + return this + } + + fun dnsRedirect(dns: String): Routing { + startScript.add("iptables -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") + startScript.add("iptables -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") + stopScript.addFirst("iptables -t nat -D PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") + stopScript.addFirst("iptables -t nat -D PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") + return this + } + + fun start() = noisySu(startScript) + fun stop() = noisySu(stopScript) +} diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt index 4f358eeb..d32b5899 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/SettingsFragment.kt @@ -27,12 +27,7 @@ class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection { addPreferencesFromResource(R.xml.pref_settings) service = findPreference("service") findPreference("service.clean").setOnPreferenceClickListener { - noisySu("iptables -t nat -F PREROUTING", - "while iptables -D FORWARD -j vpnhotspot_fwd; do done", - "iptables -F vpnhotspot_fwd", - "iptables -X vpnhotspot_fwd", - "while ip rule del lookup 62; do done", - "ip route flush table 62") + Routing.clean() true } findPreference("misc.logcat").setOnPreferenceClickListener { @@ -59,7 +54,13 @@ class SettingsFragment : PreferenceFragmentCompatDividers(), ServiceConnection { Bundle().put(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS, NetworkInterface.getNetworkInterfaces().asSequence() .filter { it.isUp && !it.isLoopback && it.interfaceAddresses.isNotEmpty() } - .map { it.name }.toList().toTypedArray())) + .map { it.name }.sorted().toList().toTypedArray())) + HotspotService.KEY_WIFI -> displayPreferenceDialog( + AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat(), HotspotService.KEY_WIFI, + Bundle().put(AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat.KEY_SUGGESTIONS, + NetworkInterface.getNetworkInterfaces().asSequence() + .filter { !it.isLoopback } // wlan0 is down in airplane mode + .map { it.name }.sorted().toList().toTypedArray())) else -> super.onDisplayPreferenceDialog(preference) } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/Utils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/Utils.kt index 24496147..8b2220b6 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/Utils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/Utils.kt @@ -6,6 +6,7 @@ import android.content.Intent import android.content.IntentFilter import android.os.Bundle import android.util.Log +import java.io.InputStream fun broadcastReceiver(receiver: (Context, Intent) -> Unit) = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) = receiver(context, intent) @@ -24,21 +25,23 @@ fun Bundle.put(key: String, map: Array): Bundle { const val NOISYSU_TAG = "NoisySU" const val NOISYSU_SUFFIX = "SUCCESS\n" -fun loggerSu(vararg commands: String): String { - val process = ProcessBuilder("su", "-c", commands.joinToString("\n")) +fun loggerSuStream(command: String): InputStream { + val process = ProcessBuilder("su", "-c", command) .redirectErrorStream(true) .start() process.waitFor() val err = process.errorStream.bufferedReader().use { it.readText() } if (!err.isBlank()) Log.e(NOISYSU_TAG, err) - return process.inputStream.bufferedReader().use { it.readText() } + return process.inputStream } -fun noisySu(vararg commands: String): Boolean { +fun loggerSu(command: String): String = loggerSuStream(command).bufferedReader().use { it.readText() } +fun noisySu(commands: Iterable): Boolean { var out = loggerSu("""function noisy() { "$@" || echo "$@" exited with $?; } ${commands.joinToString("\n") { if (it.startsWith("while ")) it else "noisy $it" }} echo $NOISYSU_SUFFIX""") - val result = out != NOISYSU_SUFFIX + val result = out == NOISYSU_SUFFIX out = out.removeSuffix(NOISYSU_SUFFIX) if (!out.isBlank()) Log.i(NOISYSU_TAG, out) return result } +fun noisySu(vararg commands: String) = noisySu(commands.asIterable()) diff --git a/mobile/src/main/res/values/strings.xml b/mobile/src/main/res/values/strings.xml index 6a5b8c98..7d69e61b 100644 --- a/mobile/src/main/res/values/strings.xml +++ b/mobile/src/main/res/values/strings.xml @@ -5,4 +5,5 @@ 1 connected device %d connected devices + Fatal: Downstream interface not found diff --git a/mobile/src/main/res/xml/pref_settings.xml b/mobile/src/main/res/xml/pref_settings.xml index ffdbc615..52f19be8 100644 --- a/mobile/src/main/res/xml/pref_settings.xml +++ b/mobile/src/main/res/xml/pref_settings.xml @@ -8,6 +8,11 @@ android:title="Upstream interface" android:summary="%s" android:defaultValue="tun0"/> +