Implement VPN over native AP

This commit is contained in:
Mygod
2018-01-05 00:25:56 +08:00
parent b86a9590fb
commit dc2db049c7
9 changed files with 271 additions and 135 deletions

View File

@@ -2,7 +2,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="be.mygod.vpnhotspot">
<uses-feature android:name="android.hardware.wifi.direct" android:required="true"/>
<uses-feature android:name="android.hardware.wifi"/>
<uses-feature android:name="android.hardware.wifi.direct" android:required="false"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_STATE"/>
<uses-permission android:name="android.permission.INTERNET"/>

View File

@@ -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 {
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))
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<WifiP2pInfo>(WifiP2pManager.EXTRA_WIFI_P2P_INFO)
val net = intent.getParcelableExtra<NetworkInfo>(WifiP2pManager.EXTRA_NETWORK_INFO)
val group = intent.getParcelableExtra<WifiP2pGroup>(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,33 +144,21 @@ 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) {
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()
@@ -152,13 +169,33 @@ class HotspotService : Service(), WifiP2pManager.ChannelListener {
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()
"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)
val builder = NotificationCompat.Builder(this, 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))
.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))
.build())
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)
}

View File

@@ -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<String, String>
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()

View File

@@ -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
}
}

View File

@@ -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<String>()
private val stopScript = LinkedList<String>()
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)
}

View File

@@ -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)
}

View File

@@ -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<String>): 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<String>): 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())

View File

@@ -5,4 +5,5 @@
<item quantity="one">1 connected device</item>
<item quantity="other">%d connected devices</item>
</plurals>
<string name="exception_interface_not_found">Fatal: Downstream interface not found</string>
</resources>

View File

@@ -8,6 +8,11 @@
android:title="Upstream interface"
android:summary="%s"
android:defaultValue="tun0"/>
<be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreference
android:key="service.wifi"
android:title="Wi-Fi interface"
android:summary="%s"
android:defaultValue="wlan0"/>
<AutoSummaryEditTextPreference
android:key="service.dns"
android:title="Downstream DNS server:port"