Migrate from NoisySu to RootSession

Fix #24. Note that just like before, IpMonitor doesn't use RootSession.
This commit is contained in:
Mygod
2018-09-06 15:36:34 +08:00
parent aa624708bb
commit 823ae9633b
12 changed files with 273 additions and 205 deletions

View File

@@ -109,7 +109,7 @@ Undocumented system binaries are all bundled and executable:
* `echo`; * `echo`;
* `ip` (`link monitor neigh rule` with proper output format); * `ip` (`link monitor neigh rule` with proper output format);
* `iptables` (with correct version corresponding to API level); * `iptables` (with correct version corresponding to API level);
* `su` (needs to support `-c` argument). * `su`.
If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox. If some of these are unavailable, you can alternatively install a recent version (v1.28.1 or higher) of Busybox.

View File

@@ -49,6 +49,7 @@ dependencies {
implementation 'com.android.billingclient:billing:1.1' implementation 'com.android.billingclient:billing:1.1'
implementation 'com.crashlytics.sdk.android:crashlytics:2.9.5' implementation 'com.crashlytics.sdk.android:crashlytics:2.9.5'
implementation 'com.github.luongvo:BadgeView:1.1.5' implementation 'com.github.luongvo:BadgeView:1.1.5'
implementation 'com.github.topjohnwu:libsu:2.0.1'
implementation "com.google.android.material:material:$androidxVersion" implementation "com.google.android.material:material:$androidxVersion"
implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.19.1' implementation 'com.linkedin.dexmaker:dexmaker-mockito:2.19.1'
implementation 'com.takisoft.preferencex:preferencex:1.0.0-alpha2' implementation 'com.takisoft.preferencex:preferencex:1.0.0-alpha2'

View File

@@ -7,7 +7,6 @@ import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import java.net.InetAddress import java.net.InetAddress
import java.net.InterfaceAddress import java.net.InterfaceAddress
import java.net.SocketException
class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callback { class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callback {
private var routing: Routing? = null private var routing: Routing? = null
@@ -21,13 +20,13 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
override fun onAvailable(ifname: String, dns: List<InetAddress>) { override fun onAvailable(ifname: String, dns: List<InetAddress>) {
val routing = routing val routing = routing
initRouting(ifname, if (routing == null) null else { initRouting(ifname, if (routing == null) null else {
routing.stop() routing.revert()
routing.hostAddress routing.hostAddress
}, dns) }, dns)
} }
override fun onLost() { override fun onLost() {
val routing = routing ?: return val routing = routing ?: return
routing.stop() routing.revert()
initRouting(null, routing.hostAddress, emptyList()) initRouting(null, routing.hostAddress, emptyList())
} }
@@ -39,9 +38,10 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
private fun initRouting(upstream: String? = null, owner: InterfaceAddress? = null, private fun initRouting(upstream: String? = null, owner: InterfaceAddress? = null,
dns: List<InetAddress> = this.dns) { dns: List<InetAddress> = this.dns) {
try {
this.dns = dns this.dns = dns
try {
this.routing = Routing(upstream, downstream, owner).apply { this.routing = Routing(upstream, downstream, owner).apply {
try {
val strict = app.strict val strict = app.strict
if (strict && upstream == null) return@apply // in this case, nothing to be done if (strict && upstream == null) return@apply // in this case, nothing to be done
if (app.dhcpWorkaround) dhcpWorkaround() if (app.dhcpWorkaround) dhcpWorkaround()
@@ -50,9 +50,14 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
forward(strict) forward(strict)
if (app.masquerade) masquerade(strict) if (app.masquerade) masquerade(strict)
dnsRedirect(dns) dnsRedirect(dns)
if (!start()) SmartSnackbar.make(R.string.noisy_su_failure).show() } catch (e: Exception) {
revert()
throw e
} finally {
commit()
} }
} catch (e: SocketException) { }
} catch (e: Exception) {
SmartSnackbar.make(e.localizedMessage).show() SmartSnackbar.make(e.localizedMessage).show()
Crashlytics.logException(e) Crashlytics.logException(e)
routing = null routing = null
@@ -62,6 +67,6 @@ class LocalOnlyInterfaceManager(val downstream: String) : UpstreamMonitor.Callba
fun stop() { fun stop() {
UpstreamMonitor.unregisterCallback(this) UpstreamMonitor.unregisterCallback(this)
app.cleanRoutings -= this app.cleanRoutings -= this
routing?.stop() routing?.revert()
} }
} }

View File

@@ -176,10 +176,17 @@ class RepeaterService : Service(), WifiP2pManager.ChannelListener, SharedPrefere
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (status != Status.IDLE) return START_NOT_STICKY if (status != Status.IDLE) return START_NOT_STICKY
status = Status.STARTING status = Status.STARTING
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher( val out = try {
loggerSu("exec dumpsys ${Context.WIFI_P2P_SERVICE}") ?: "") RootSession.use { it.execOut("dumpsys ${Context.WIFI_P2P_SERVICE}") }
} catch (e: RuntimeException) {
e.printStackTrace()
Crashlytics.logException(e)
startFailure(e.localizedMessage)
return START_NOT_STICKY
}
val matcher = WifiP2pManagerHelper.patternNetworkInfo.matcher(out)
when { when {
!matcher.find() -> startFailure(getString(R.string.root_unavailable)) !matcher.find() -> startFailure(out)
matcher.group(2) == "true" -> { matcher.group(2) == "true" -> {
unregisterReceiver() unregisterReceiver()
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION, registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,

View File

@@ -14,7 +14,7 @@ import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.UpstreamMonitor import be.mygod.vpnhotspot.net.UpstreamMonitor
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragmentCompat
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
import be.mygod.vpnhotspot.util.loggerSuStream import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import com.takisoft.preferencex.PreferenceFragmentCompat import com.takisoft.preferencex.PreferenceFragmentCompat
@@ -35,8 +35,15 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
} }
boot.isChecked = BootReceiver.enabled boot.isChecked = BootReceiver.enabled
findPreference("service.clean").setOnPreferenceClickListener { findPreference("service.clean").setOnPreferenceClickListener {
if (Routing.clean() == null) SmartSnackbar.make(R.string.root_unavailable).show() val cleaned = try {
else app.cleanRoutings() Routing.clean()
true
} catch (e: RuntimeException) {
e.printStackTrace()
SmartSnackbar.make(e.localizedMessage).show()
false
}
if (cleaned) app.cleanRoutings()
true true
} }
findPreference("misc.logcat").setOnPreferenceClickListener { findPreference("misc.logcat").setOnPreferenceClickListener {
@@ -82,8 +89,9 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
|logcat -d |logcat -d
""".trimMargin()) """.trimMargin())
try { try {
loggerSuStream(commands.toString())?.use { it.copyTo(out) } out.write(RootSession.use { it.execQuiet(commands.toString(), true).out }
} catch (e: IOException) { .joinToString("\n").toByteArray())
} catch (e: Exception) {
e.printStackTrace(writer) e.printStackTrace(writer)
Crashlytics.logException(e) Crashlytics.logException(e)
writer.flush() writer.flush()

View File

@@ -12,7 +12,6 @@ import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import java.net.InetAddress import java.net.InetAddress
import java.net.SocketException
class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback { class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callback {
companion object { companion object {
@@ -34,7 +33,7 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
private val receiver = broadcastReceiver { _, intent -> private val receiver = broadcastReceiver { _, intent ->
synchronized(routings) { synchronized(routings) {
for (iface in routings.keys - TetheringManager.getTetheredIfaces(intent.extras!!)) for (iface in routings.keys - TetheringManager.getTetheredIfaces(intent.extras!!))
routings.remove(iface)?.stop() routings.remove(iface)?.revert()
updateRoutingsLocked() updateRoutingsLocked()
} }
} }
@@ -61,13 +60,13 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
val upstream = upstream val upstream = upstream
val disableIpv6 = app.pref.getBoolean("service.disableIpv6", false) val disableIpv6 = app.pref.getBoolean("service.disableIpv6", false)
if (upstream != null || app.strict || disableIpv6) { if (upstream != null || app.strict || disableIpv6) {
var failed = false
val iterator = routings.iterator() val iterator = routings.iterator()
while (iterator.hasNext()) { while (iterator.hasNext()) {
val (downstream, value) = iterator.next() val (downstream, value) = iterator.next()
if (value != null) if (value.upstream == upstream) continue else value.stop() if (value != null) if (value.upstream == upstream) continue else value.revert()
try { try {
routings[downstream] = Routing(upstream, downstream).apply { routings[downstream] = Routing(upstream, downstream).apply {
try {
if (app.dhcpWorkaround) dhcpWorkaround() if (app.dhcpWorkaround) dhcpWorkaround()
// system tethering already has working forwarding rules // system tethering already has working forwarding rules
// so it doesn't make sense to add additional forwarding rules // so it doesn't make sense to add additional forwarding rules
@@ -78,16 +77,20 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
if (app.masquerade) masquerade() if (app.masquerade) masquerade()
if (upstream != null) dnsRedirect(dns) if (upstream != null) dnsRedirect(dns)
if (disableIpv6) disableIpv6() if (disableIpv6) disableIpv6()
if (!start()) failed = true } catch (e: Exception) {
revert()
throw e
} finally {
commit()
} }
} catch (e: SocketException) { }
} catch (e: Exception) {
e.printStackTrace() e.printStackTrace()
Crashlytics.logException(e) Crashlytics.logException(e)
SmartSnackbar.make(e.localizedMessage).show()
iterator.remove() iterator.remove()
failed = true
} }
} }
if (failed) SmartSnackbar.make(R.string.noisy_su_failure).show()
} }
updateNotification() updateNotification()
} }
@@ -101,7 +104,7 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE) val iface = intent.getStringExtra(EXTRA_ADD_INTERFACE)
synchronized(routings) { synchronized(routings) {
if (iface != null) routings[iface] = null if (iface != null) routings[iface] = null
routings.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.stop() routings.remove(intent.getStringExtra(EXTRA_REMOVE_INTERFACE))?.revert()
updateRoutingsLocked() updateRoutingsLocked()
} }
} else if (routings.isEmpty()) stopSelf(startId) } else if (routings.isEmpty()) stopSelf(startId)
@@ -120,7 +123,7 @@ class TetheringService : IpNeighbourMonitoringService(), UpstreamMonitor.Callbac
this.dns = emptyList() this.dns = emptyList()
synchronized(routings) { synchronized(routings) {
for ((iface, routing) in routings) { for ((iface, routing) in routings) {
routing?.stop() routing?.revert()
routings[iface] = null routings[iface] = null
} }
} }

View File

@@ -21,17 +21,10 @@ abstract class IpMonitor : Runnable {
private var monitor: Process? = null private var monitor: Process? = null
private var pool: ScheduledExecutorService? = null private var pool: ScheduledExecutorService? = null
init { private fun handleProcess(process: Process): Int {
thread("${javaClass.simpleName}-input") { val err = thread("${javaClass.simpleName}-error") {
// monitor may get rejected by SELinux
val monitor = ProcessBuilder("sh", "-c",
"ip monitor $monitoredObject || exec su -c 'exec ip monitor $monitoredObject'")
.redirectErrorStream(true)
.start()
this.monitor = monitor
thread("${javaClass.simpleName}-error") {
try { try {
monitor.errorStream.bufferedReader().forEachLine { process.errorStream.bufferedReader().forEachLine {
Crashlytics.log(Log.ERROR, javaClass.simpleName, it) Crashlytics.log(Log.ERROR, javaClass.simpleName, it)
} }
} catch (_: InterruptedIOException) { } catch (e: IOException) { } catch (_: InterruptedIOException) { } catch (e: IOException) {
@@ -40,18 +33,27 @@ abstract class IpMonitor : Runnable {
} }
} }
try { try {
monitor.inputStream.bufferedReader().forEachLine(this::processLine) process.inputStream.bufferedReader().forEachLine(this::processLine)
monitor.waitFor() } catch (_: InterruptedIOException) { } catch (e: IOException) {
if (monitor.exitValue() == 0) return@thread e.printStackTrace()
Crashlytics.logException(e)
}
err.join()
process.waitFor()
return process.exitValue()
}
init {
thread("${javaClass.simpleName}-input") {
// monitor may get rejected by SELinux
if (handleProcess(ProcessBuilder("ip", "monitor", monitoredObject).start()) == 0) return@thread
if (handleProcess(ProcessBuilder("su", "-c", "exec ip monitor $monitoredObject").start()) == 0)
return@thread
Crashlytics.log(Log.WARN, javaClass.simpleName, "Failed to set up monitor, switching to polling") Crashlytics.log(Log.WARN, javaClass.simpleName, "Failed to set up monitor, switching to polling")
Crashlytics.logException(MonitorFailure()) Crashlytics.logException(MonitorFailure())
val pool = Executors.newScheduledThreadPool(1) val pool = Executors.newScheduledThreadPool(1)
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS) pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
this.pool = pool this.pool = pool
} catch (_: InterruptedIOException) { } catch (e: IOException) {
e.printStackTrace()
Crashlytics.logException(e)
}
} }
} }

View File

@@ -3,11 +3,15 @@ package be.mygod.vpnhotspot.net
import android.os.Build import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.util.debugLog import be.mygod.vpnhotspot.util.debugLog
import be.mygod.vpnhotspot.util.noisySu
import java.net.* import java.net.*
import java.util.*
/**
* A transaction wrapper that helps set up routing environment.
*
* Once revert is called, this object no longer serves any purpose.
*/
class Routing(val upstream: String?, private val downstream: String, ownerAddress: InterfaceAddress? = null) { class Routing(val upstream: String?, private val downstream: String, ownerAddress: InterfaceAddress? = null) {
companion object { companion object {
/** /**
@@ -18,17 +22,22 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
*/ */
private val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w" private val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
fun clean() = noisySu( fun clean() = RootSession.use {
"$IPTABLES -t nat -F PREROUTING", it.submit("$IPTABLES -t nat -F PREROUTING")
"quiet while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done", it.submit("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
"$IPTABLES -F vpnhotspot_fwd", it.submit("$IPTABLES -F vpnhotspot_fwd")
"$IPTABLES -X vpnhotspot_fwd", it.submit("$IPTABLES -X vpnhotspot_fwd")
"quiet while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done", it.submit("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
"$IPTABLES -t nat -F vpnhotspot_masquerade", it.submit("$IPTABLES -t nat -F vpnhotspot_masquerade")
"$IPTABLES -t nat -X vpnhotspot_masquerade", it.submit("$IPTABLES -t nat -X vpnhotspot_masquerade")
"quiet while ip rule del priority 17900; do done", it.submit("while ip rule del priority 17900; do done")
"quiet while ip rule del iif lo uidrange 0-0 lookup local_network priority 11000; do done", it.submit("while ip rule del iif lo uidrange 0-0 lookup local_network priority 11000; do done")
report = false) }
fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
exec("$IPTABLES -t $table -A $content", "$IPTABLES -t $table -D $content")
fun RootSession.Transaction.iptablesInsert(content: String, table: String = "filter") =
exec("$IPTABLES -t $table -I $content", "$IPTABLES -t $table -D $content")
} }
class InterfaceNotFoundException : SocketException() { class InterfaceNotFoundException : SocketException() {
@@ -37,18 +46,13 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.interfaceAddresses?.asSequence() val hostAddress = ownerAddress ?: NetworkInterface.getByName(downstream)?.interfaceAddresses?.asSequence()
?.singleOrNull { it.address is Inet4Address } ?: throw InterfaceNotFoundException() ?.singleOrNull { it.address is Inet4Address } ?: throw InterfaceNotFoundException()
private val startScript = LinkedList<String>() private val transaction = RootSession.beginTransaction()
private val stopScript = LinkedList<String>()
var started = false var started = false
fun ipForward() { fun ipForward() = transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
startScript.add("echo 1 >/proc/sys/net/ipv4/ip_forward")
}
fun disableIpv6() { fun disableIpv6() = transaction.exec("echo 1 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6",
startScript.add("echo 1 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6") "echo 0 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
stopScript.add("echo 0 >/proc/sys/net/ipv6/conf/$downstream/disable_ipv6")
}
/** /**
* Since Android 5.0, RULE_PRIORITY_TETHERING = 18000. * Since Android 5.0, RULE_PRIORITY_TETHERING = 18000.
@@ -58,63 +62,50 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
*/ */
fun rule() { fun rule() {
if (upstream != null) { if (upstream != null) {
startScript.add("ip rule add from all iif $downstream lookup $upstream priority 17900") transaction.exec("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 // 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") "ip rule del from all iif $downstream priority 17900")
} }
} }
fun forward(strict: Boolean = true) { fun forward(strict: Boolean = true) {
startScript.add("quiet $IPTABLES -N vpnhotspot_fwd 2>/dev/null") transaction.execQuiet("$IPTABLES -N vpnhotspot_fwd")
transaction.iptablesInsert("FORWARD -j vpnhotspot_fwd")
if (strict) { if (strict) {
if (upstream != null) { if (upstream != null) {
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $upstream -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT") transaction.iptablesAdd("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") transaction.iptablesAdd("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 nothing needs to be done } // else nothing needs to be done
} else { } else {
// for not strict mode, allow downstream packets to be redirected to anywhere // for not strict mode, allow downstream packets to be redirected to anywhere
// because we don't wanna keep track of default network changes // because we don't wanna keep track of default network changes
startScript.add("$IPTABLES -A vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT") transaction.iptablesAdd("vpnhotspot_fwd -o $downstream -m state --state ESTABLISHED,RELATED -j ACCEPT")
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -j ACCEPT") transaction.iptablesAdd("vpnhotspot_fwd -i $downstream -j ACCEPT")
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")
} }
startScript.add("$IPTABLES -I FORWARD -j vpnhotspot_fwd")
stopScript.addFirst("$IPTABLES -D FORWARD -j vpnhotspot_fwd")
} }
fun overrideSystemRules() { fun overrideSystemRules() = transaction.iptablesAdd("vpnhotspot_fwd -i $downstream -j DROP")
startScript.add("$IPTABLES -A vpnhotspot_fwd -i $downstream -j DROP")
stopScript.addFirst("$IPTABLES -D vpnhotspot_fwd -i $downstream -j DROP")
}
fun masquerade(strict: Boolean = true) { fun masquerade(strict: Boolean = true) {
val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}" val hostSubnet = "${hostAddress.address.hostAddress}/${hostAddress.networkPrefixLength}"
startScript.add("quiet $IPTABLES -t nat -N vpnhotspot_masquerade 2>/dev/null") transaction.execQuiet("$IPTABLES -t nat -N vpnhotspot_masquerade")
transaction.iptablesInsert("POSTROUTING -j vpnhotspot_masquerade", "nat")
// note: specifying -i wouldn't work for POSTROUTING // note: specifying -i wouldn't work for POSTROUTING
if (strict) { if (strict) {
if (upstream != null) { if (upstream != null) {
startScript.add("$IPTABLES -t nat -A vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE") transaction.iptablesAdd("vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE", "nat")
stopScript.addFirst("$IPTABLES -t nat -D vpnhotspot_masquerade -s $hostSubnet -o $upstream -j MASQUERADE")
} // else nothing needs to be done } // else nothing needs to be done
} else { } else {
startScript.add("$IPTABLES -t nat -A vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE") transaction.iptablesAdd("vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE", "nat")
stopScript.addFirst("$IPTABLES -t nat -D vpnhotspot_masquerade -s $hostSubnet -j MASQUERADE")
} }
startScript.add("$IPTABLES -t nat -I POSTROUTING -j vpnhotspot_masquerade")
stopScript.addFirst("$IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade")
} }
fun dnsRedirect(dnses: List<InetAddress>) { fun dnsRedirect(dnses: List<InetAddress>) {
val hostAddress = hostAddress.address.hostAddress val hostAddress = hostAddress.address.hostAddress
val dns = dnses.firstOrNull { it is Inet4Address }?.hostAddress ?: app.pref.getString("service.dns", "8.8.8.8") val dns = dnses.firstOrNull { it is Inet4Address }?.hostAddress ?: app.pref.getString("service.dns", "8.8.8.8")
debugLog("Routing", "Using $dns from ($dnses)") debugLog("Routing", "Using $dns from ($dnses)")
startScript.add("$IPTABLES -t nat -A PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") transaction.iptablesAdd("PREROUTING -i $downstream -p tcp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
startScript.add("$IPTABLES -t nat -A PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns") transaction.iptablesAdd("PREROUTING -i $downstream -p udp -d $hostAddress --dport 53 -j DNAT --to-destination $dns", "nat")
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")
} }
/** /**
@@ -125,20 +116,9 @@ class Routing(val upstream: String?, private val downstream: String, ownerAddres
* *
* Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#57 * Source: https://android.googlesource.com/platform/system/netd/+/b9baf26/server/RouteController.cpp#57
*/ */
fun dhcpWorkaround() { fun dhcpWorkaround() = transaction.exec("ip rule add iif lo uidrange 0-0 lookup local_network priority 11000",
startScript.add("ip rule add iif lo uidrange 0-0 lookup local_network priority 11000") "ip rule del iif lo uidrange 0-0 lookup local_network priority 11000")
stopScript.addFirst("ip rule del iif lo uidrange 0-0 lookup local_network priority 11000")
}
fun start(): Boolean { fun commit() = transaction.commit()
if (started) return true fun revert() = transaction.revert()
started = true
if (noisySu(startScript) != true) stop()
return started
}
fun stop(): Boolean {
if (!started) return true
started = false
return noisySu(stopScript, false) == true
}
} }

View File

@@ -6,8 +6,7 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.util.Log import android.util.Log
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.loggerSu import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.util.noisySu
import com.crashlytics.android.Crashlytics import com.crashlytics.android.Crashlytics
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@@ -29,6 +28,7 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
* PSK parser can be found here: https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488 * PSK parser can be found here: https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
*/ */
private val pskParser = "^[\\r\\t ]*psk=(ext:|\"(.*)\"|\"(.*)|[0-9a-fA-F]{64}\$)".toRegex(RegexOption.MULTILINE) private val pskParser = "^[\\r\\t ]*psk=(ext:|\"(.*)\"|\"(.*)|[0-9a-fA-F]{64}\$)".toRegex(RegexOption.MULTILINE)
private val whitespaceMatcher = "\\s+".toRegex()
private val confPath = if (Build.VERSION.SDK_INT >= 28) private val confPath = if (Build.VERSION.SDK_INT >= 28)
"/data/vendor/wifi/wpa/p2p_supplicant.conf" else "/data/misc/wifi/p2p_supplicant.conf" "/data/vendor/wifi/wpa/p2p_supplicant.conf" else "/data/misc/wifi/p2p_supplicant.conf"
@@ -40,12 +40,12 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
} }
override fun describeContents() = 0 override fun describeContents() = 0
private val contentDelegate = lazy { initContent ?: loggerSu("exec cat $confPath") } private val contentDelegate = lazy { initContent ?: RootSession.use { it.execOut("cat $confPath") } }
private val content by contentDelegate private val content by contentDelegate
fun readPsk(handler: ((RuntimeException) -> Unit)? = null): String? { fun readPsk(handler: ((RuntimeException) -> Unit)? = null): String? {
return try { return try {
val match = pskParser.findAll(content ?: return null).single() val match = pskParser.findAll(content).single()
if (match.groups[2] == null && match.groups[3] == null) "" else { if (match.groups[2] == null && match.groups[3] == null) "" else {
// only one will match and hold non-empty value // only one will match and hold non-empty value
val result = match.groupValues[2] + match.groupValues[3] val result = match.groupValues[2] + match.groupValues[3]
@@ -64,8 +64,7 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
} }
} }
fun update(config: WifiConfiguration): Boolean? { fun update(config: WifiConfiguration) {
val content = content ?: return null
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.cacheDir) val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.cacheDir)
try { try {
var ssidFound = 0 var ssidFound = 0
@@ -86,13 +85,18 @@ class P2pSupplicantConfiguration(private val initContent: String? = null) : Parc
} }
if (ssidFound != 1 || pskFound != 1) { if (ssidFound != 1 || pskFound != 1) {
Crashlytics.log(Log.WARN, TAG, "Invalid conf ($ssidFound, $pskFound): $content") Crashlytics.log(Log.WARN, TAG, "Invalid conf ($ssidFound, $pskFound): $content")
Crashlytics.logException(InvalidConfigurationError()) if (ssidFound == 0 || pskFound == 0) throw InvalidConfigurationError()
else Crashlytics.logException(InvalidConfigurationError())
} }
if (ssidFound == 0 || pskFound == 0) return false
// pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md // pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
return noisySu("cat ${tempFile.absolutePath} > $confPath", RootSession.use {
if (Build.VERSION.SDK_INT >= 23) "pkill wpa_supplicant" it.exec("cat ${tempFile.absolutePath} > $confPath")
else "set `ps | grep wpa_supplicant`; kill \$2") if (Build.VERSION.SDK_INT >= 23) it.exec("pkill wpa_supplicant") else {
val result = it.execOut("ps | grep wpa_supplicant").split(whitespaceMatcher)
check(result.size >= 2) { "wpa_supplicant not found, please toggle Airplane mode manually" }
it.exec("kill ${result[1]}")
}
}
} finally { } finally {
if (!tempFile.delete()) tempFile.deleteOnExit() if (!tempFile.delete()) tempFile.deleteOnExit()
} }

View File

@@ -15,6 +15,7 @@ import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.manage.TetheringFragment import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.crashlytics.android.Crashlytics
import com.google.android.material.textfield.TextInputLayout import com.google.android.material.textfield.TextInputLayout
import java.nio.charset.Charset import java.nio.charset.Charset
@@ -85,13 +86,14 @@ class WifiP2pDialogFragment : DialogFragment(), TextWatcher, DialogInterface.OnC
override fun onClick(dialog: DialogInterface?, which: Int) { override fun onClick(dialog: DialogInterface?, which: Int) {
when (which) { when (which) {
DialogInterface.BUTTON_POSITIVE -> when (configurer.update(config!!)) { DialogInterface.BUTTON_POSITIVE -> try {
true -> { configurer.update(config!!)
app.handler.postDelayed((targetFragment as TetheringFragment).adapter.repeaterManager app.handler.postDelayed((targetFragment as TetheringFragment).adapter.repeaterManager
.binder!!::requestGroupUpdate, 1000) .binder!!::requestGroupUpdate, 1000)
} } catch (e: RuntimeException) {
false -> SmartSnackbar.make(R.string.noisy_su_failure).show() e.printStackTrace()
null -> SmartSnackbar.make(R.string.root_unavailable).show() Crashlytics.logException(e)
SmartSnackbar.make(e.localizedMessage).show()
} }
DialogInterface.BUTTON_NEUTRAL -> DialogInterface.BUTTON_NEUTRAL ->
(targetFragment as TetheringFragment).adapter.repeaterManager.binder!!.resetCredentials() (targetFragment as TetheringFragment).adapter.repeaterManager.binder!!.resetCredentials()

View File

@@ -1,51 +0,0 @@
package be.mygod.vpnhotspot.util
import android.util.Log
import be.mygod.vpnhotspot.App.Companion.app
import com.crashlytics.android.Crashlytics
import java.io.IOException
import java.io.InputStream
private const val NOISYSU_TAG = "NoisySU"
private const val NOISYSU_SUFFIX = "SUCCESS\n"
private class SuFailure(msg: String?) : RuntimeException(msg)
fun loggerSuStream(command: String): InputStream? {
val process = try {
ProcessBuilder("su", "-c", command)
.directory(app.deviceStorage.cacheDir)
.redirectErrorStream(true)
.start()
} catch (e: IOException) {
e.printStackTrace()
Crashlytics.logException(e)
return null
}
return process.inputStream
}
fun loggerSu(command: String): String? {
val stream = loggerSuStream(command) ?: return null
return try {
stream.bufferedReader().readText()
} catch (e: IOException) {
e.printStackTrace()
Crashlytics.logException(e)
null
}
}
fun noisySu(commands: Iterable<String>, report: Boolean = true): Boolean? {
var out = loggerSu("""function noisy() { "$@" || echo "$@" exited with $?; }
${commands.joinToString("\n") { if (it.startsWith("quiet ")) it.substring(6) else "noisy $it" }}
echo $NOISYSU_SUFFIX""")
val result = if (out == null) null else out == NOISYSU_SUFFIX
out = out?.removeSuffix(NOISYSU_SUFFIX)
if (!out.isNullOrBlank()) {
Crashlytics.log(Log.INFO, NOISYSU_TAG, out)
if (report) Crashlytics.logException(SuFailure(out))
}
return result
}
fun noisySu(vararg commands: String, report: Boolean = true) = noisySu(commands.asIterable(), report)

View File

@@ -0,0 +1,107 @@
package be.mygod.vpnhotspot.util
import android.util.Log
import com.crashlytics.android.Crashlytics
import com.topjohnwu.superuser.Shell
import java.util.*
import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock
class RootSession {
companion object {
const val TAG = "RootSession"
const val INIT_CHECKPOINT = "$TAG initialized successfully"
private val monitor = ReentrantLock()
private var instance: RootSession? = null
private fun ensureInstance(): RootSession {
var instance = instance
if (instance == null) instance = RootSession().also { RootSession.instance = it }
return instance
}
fun <T> use(operation: (RootSession) -> T) = monitor.withLock { operation(ensureInstance()) }
fun beginTransaction(): Transaction {
monitor.lock()
return try {
ensureInstance()
} catch (e: RuntimeException) {
monitor.unlock()
throw e
}.Transaction()
}
}
class UnexpectedOutputException(msg: String) : RuntimeException(msg)
private fun checkOutput(command: String, result: Shell.Result, out: Boolean = result.out.isNotEmpty(),
err: Boolean = stderr.isNotEmpty()) {
if (result.isSuccess && !out && !err) return
val msg = StringBuilder("$command exited with ${result.code}")
if (out) result.out.forEach { msg.append("\n$it") }
// TODO bug: https://github.com/topjohnwu/libsu/pull/23
if (err) stderr.forEach { msg.append("\nE $it") }
throw UnexpectedOutputException(msg.toString())
}
private val shell = Shell.newInstance("su")
private val stdout = ArrayList<String>()
private val stderr = ArrayList<String>()
init {
// check basic shell functionality very basically
val result = execQuiet("echo $INIT_CHECKPOINT")
checkOutput("echo", result, result.out.joinToString("\n").trim() != INIT_CHECKPOINT)
}
/**
* Don't care about the results, but still sync.
*/
fun submit(command: String) {
val result = execQuiet(command)
if (result.code != 0) Crashlytics.log(Log.VERBOSE, TAG, "$command exited with ${result.code}")
var msg = stderr.joinToString("\n").trim()
if (msg.isNotEmpty()) Crashlytics.log(Log.VERBOSE, TAG, msg)
msg = result.out.joinToString("\n").trim()
if (msg.isNotEmpty()) Crashlytics.log(Log.VERBOSE, TAG, msg)
}
fun execQuiet(command: String, redirect: Boolean = false): Shell.Result {
stdout.clear()
return shell.newJob().add(command).to(stdout, if (redirect) stdout else {
stderr.clear()
stderr
}).exec()
}
fun exec(command: String) = checkOutput(command, execQuiet(command))
fun execOut(command: String): String {
val result = execQuiet(command)
checkOutput(command, result, false)
return result.out.joinToString("\n")
}
/**
* This transaction is different from what you may have in mind since you can revert it after committing it.
*/
inner class Transaction {
private val revertCommands = LinkedList<String>()
fun exec(command: String, revert: String? = null) {
if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails
this@RootSession.exec(command)
}
fun execQuiet(command: String) = this@RootSession.execQuiet(command)
fun commit() = monitor.unlock()
fun revert() {
if (revertCommands.isEmpty()) return
val shell = if (monitor.isHeldByCurrentThread) this@RootSession else {
monitor.lock()
ensureInstance()
}
revertCommands.forEach { shell.submit(it) }
revertCommands.clear()
monitor.unlock() // commit
}
}
}