librootkotlinx

Fixes #14, #27, #114, #117.
This commit is contained in:
Mygod
2020-06-21 05:33:39 +08:00
parent 7b1f610f9a
commit ad218d7ec6
51 changed files with 1781 additions and 574 deletions

View File

@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.net
import android.content.SharedPreferences
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.root.RoutingCommands
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.GlobalScope
@@ -32,11 +33,11 @@ object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener {
RootSession.use {
try {
it.exec("ip rule $action iif lo uidrange 0-0 lookup local_network priority 11000")
} catch (e: RootSession.UnexpectedOutputException) {
if (e.result.out.isEmpty() && (e.result.code == 2 || e.result.code == 254) && if (enabled) {
e.result.err.joinToString("\n") == "RTNETLINK answers: File exists"
} catch (e: RoutingCommands.UnexpectedOutputException) {
if (e.result.out.isEmpty() && (e.result.exit == 2 || e.result.exit == 254) && if (enabled) {
e.result.err == "RTNETLINK answers: File exists"
} else {
e.result.err.joinToString("\n") == "RTNETLINK answers: No such file or directory"
e.result.err == "RTNETLINK answers: No such file or directory"
}) return@use
Timber.w(IOException("Failed to tweak dhcp workaround rule", e))
SmartSnackbar.make(e).show()

View File

@@ -3,7 +3,10 @@ package be.mygod.vpnhotspot.net
import android.os.Build
import android.system.ErrnoException
import android.system.OsConstants
import be.mygod.vpnhotspot.root.ReadArp
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.parseNumericAddress
import kotlinx.coroutines.runBlocking
import timber.log.Timber
import java.io.File
import java.io.FileNotFoundException
@@ -35,6 +38,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
fun parse(line: String): List<IpNeighbour> {
if (line.isBlank()) return emptyList()
return try {
val match = parser.matchEntire(line)!!
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
@@ -87,17 +91,24 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
private const val ARP_CACHE_EXPIRE = 1L * 1000 * 1000 * 1000
private var arpCache = emptyList<List<String>>()
private var arpCacheTime = -ARP_CACHE_EXPIRE
private fun Sequence<String>.makeArp() = this
.map { it.split(spaces) }
.drop(1)
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
.toList()
private fun arp(): List<List<String>> {
if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try {
arpCache = File("/proc/net/arp").bufferedReader().readLines()
.asSequence()
.map { it.split(spaces) }
.drop(1)
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
.toList()
arpCache = File("/proc/net/arp").bufferedReader().lineSequence().makeArp()
} catch (e: IOException) {
if (e !is FileNotFoundException || Build.VERSION.SDK_INT < 29 ||
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e)
if (e is FileNotFoundException && Build.VERSION.SDK_INT >= 29 &&
(e.cause as? ErrnoException)?.errno == OsConstants.EACCES) try {
arpCache = runBlocking {
RootManager.use { it.execute(ReadArp()) }
}.value.lineSequence().makeArp()
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
Timber.w(eRoot)
} else Timber.w(e)
}
return arpCache
}

View File

@@ -11,9 +11,12 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.RoutingCommands
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber
import java.io.BufferedWriter
import java.io.IOException
import java.net.Inet4Address
import java.net.InetAddress
@@ -41,42 +44,38 @@ class Routing(private val caller: Any, private val downstream: String,
private const val RULE_PRIORITY_UPSTREAM_FALLBACK = 17900
private const val RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM = 17980
/**
* -w <seconds> is not supported on 7.1-.
* Fortunately there also isn't a time limit for starting a foreground service back in 7.1-.
*
* Source: https://android.googlesource.com/platform/external/iptables/+/android-5.0.0_r1/iptables/iptables.c#1574
*/
val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
val IP6TABLES = if (Build.VERSION.SDK_INT >= 26) "ip6tables -w 1" else "ip6tables -w"
const val IPTABLES ="iptables -w"
const val IP6TABLES = "ip6tables -w"
fun clean() {
fun appendCleanCommands(commands: BufferedWriter) {
commands.appendln("$IPTABLES -t nat -F PREROUTING")
commands.appendln("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
commands.appendln("$IPTABLES -F vpnhotspot_fwd")
commands.appendln("$IPTABLES -X vpnhotspot_fwd")
commands.appendln("$IPTABLES -F vpnhotspot_acl")
commands.appendln("$IPTABLES -X vpnhotspot_acl")
commands.appendln("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
commands.appendln("$IPTABLES -t nat -F vpnhotspot_masquerade")
commands.appendln("$IPTABLES -t nat -X vpnhotspot_masquerade")
commands.appendln("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
commands.appendln("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
commands.appendln("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
commands.appendln("$IP6TABLES -F vpnhotspot_filter")
commands.appendln("$IP6TABLES -X vpnhotspot_filter")
commands.appendln("while ip rule del priority $RULE_PRIORITY_DNS; do done")
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; do done")
}
suspend fun clean() {
TrafficRecorder.clean()
RootSession.use {
it.execQuiet("$IPTABLES -t nat -F PREROUTING")
it.execQuiet("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
it.execQuiet("$IPTABLES -F vpnhotspot_fwd")
it.execQuiet("$IPTABLES -X vpnhotspot_fwd")
it.execQuiet("$IPTABLES -F vpnhotspot_acl")
it.execQuiet("$IPTABLES -X vpnhotspot_acl")
it.execQuiet("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
it.execQuiet("$IPTABLES -t nat -F vpnhotspot_masquerade")
it.execQuiet("$IPTABLES -t nat -X vpnhotspot_masquerade")
it.execQuiet("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
it.execQuiet("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
it.execQuiet("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
it.execQuiet("$IP6TABLES -F vpnhotspot_filter")
it.execQuiet("$IP6TABLES -X vpnhotspot_filter")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_DNS; do done")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; do done")
}
RootManager.use { it.execute(RoutingCommands.Clean()) }
}
private fun RootSession.Transaction.iptables(command: String, revert: String) {
val result = execQuiet(command, revert)
val message = RootSession.checkOutput(command, result, err = false)
val message = result.message(listOf(command), err = false)
if (result.err.isNotEmpty()) Timber.i(message) // busy wait message
}
private fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
@@ -88,9 +87,9 @@ class Routing(private val caller: Any, private val downstream: String,
private fun RootSession.Transaction.ndc(name: String, command: String, revert: String? = null) {
val result = execQuiet(command, revert)
val log = RootSession.checkOutput(command, result,
result.out.lastOrNull() != "200 0 $name operation succeeded")
if (result.out.size > 1) Timber.i(log)
val suffix = "200 0 $name operation succeeded\n"
result.check(listOf(command), !result.out.endsWith(suffix))
if (result.out.length > suffix.length) Timber.i(result.message(listOf(command), true))
}
}
@@ -270,7 +269,7 @@ class Routing(private val caller: Any, private val downstream: String,
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
"ndc ipfwd disable vpnhotspot_$downstream")
return
} catch (e: RootSession.UnexpectedOutputException) {
} catch (e: RoutingCommands.UnexpectedOutputException) {
Timber.w(IOException("ndc ipfwd enable failure", e))
}
transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")

View File

@@ -3,7 +3,9 @@ package be.mygod.vpnhotspot.net
import android.provider.Settings
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.SettingsGlobalPut
import be.mygod.vpnhotspot.util.Services
/**
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
@@ -16,11 +18,18 @@ import be.mygod.vpnhotspot.util.RootSession
@RequiresApi(27)
object TetherOffloadManager {
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
var enabled: Boolean
get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
set(value) {
RootSession.use {
it.exec("settings put global $TETHER_OFFLOAD_DISABLED ${if (value) 0 else 1}")
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
suspend fun setEnabled(value: Boolean) {
val int = if (value) 0 else 1
try {
check(Settings.Global.putInt(Services.context.contentResolver, TETHER_OFFLOAD_DISABLED, int))
} catch (e: SecurityException) {
try {
RootManager.use { it.execute(SettingsGlobalPut(TETHER_OFFLOAD_DISABLED, int.toString())) }
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
throw eRoot
}
}
}
}

View File

@@ -15,11 +15,19 @@ import androidx.annotation.RequiresApi
import androidx.collection.SparseArrayCompat
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.StartTethering
import be.mygod.vpnhotspot.root.StopTethering
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
import com.android.dx.stock.ProxyBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
import java.io.File
import java.lang.ref.WeakReference
import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
@@ -49,7 +57,10 @@ object TetheringManager {
*/
fun onTetheringFailed(error: Int? = null) { }
fun onException() { }
/**
* ADDED: Called when a local Exception occurred.
*/
fun onException(e: Exception) { }
}
/**
@@ -130,7 +141,6 @@ object TetheringManager {
/**
* Ncm local tethering type.
*
* Requires NETWORK_SETTINGS permission, which is sadly not obtainable.
* @see [startTethering]
*/
@RequiresApi(30)
@@ -149,7 +159,7 @@ object TetheringManager {
@get:RequiresApi(30)
private val instance by lazy @TargetApi(30) {
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
val service = app.getSystemService(TETHERING_SERVICE)
val service = Services.context.getSystemService(TETHERING_SERVICE)
service
}
@get:RequiresApi(30)
@@ -211,12 +221,64 @@ object TetheringManager {
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
@Deprecated("Legacy API")
@RequiresApi(24)
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
val reference = WeakReference(callback)
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(cacheDir)
handler { proxy, method, args ->
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
@Suppress("NAME_SHADOWING") val callback = reference.get()
when (method.name) {
"onTetheringStarted" -> callback?.onTetheringStarted()
"onTetheringFailed" -> callback?.onTetheringFailed()
else -> ProxyBuilder.callSuper(proxy, method, args)
}
}
}.build()
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
}
@RequiresApi(30)
fun startTethering(type: Int, exemptFromEntitlementCheck: Boolean, showProvisioningUi: Boolean, executor: Executor,
proxy: Any) {
startTethering(instance, newTetheringRequestBuilder.newInstance(type).let { builder ->
// setting exemption requires TETHER_PRIVILEGED permission
if (exemptFromEntitlementCheck) setExemptFromEntitlementCheck(builder, true)
setShouldShowEntitlementUi(builder, showProvisioningUi)
build(builder)
}, executor, proxy)
}
@RequiresApi(30)
fun proxy(callback: StartTetheringCallback): Any {
val reference = WeakReference(callback)
return Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING") val callback = reference.get()
return when (val name = method.name) {
"onTetheringStarted" -> {
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringStarted()
}
"onTetheringFailed" -> {
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringFailed(args?.get(0) as Int)
}
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
}
}
})
}
/**
* Runs tether provisioning for the given type if needed and then starts tethering if
* the check succeeds. If no carrier provisioning is required for tethering, tethering is
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
* schedules tether provisioning re-checks if appropriate.
*
* CHANGED BEHAVIOR: This method will not throw Exceptions, instead, callback.onException will be called.
*
* @param type The type of tethering to start. Must be one of
* {@link ConnectivityManager.TETHERING_WIFI},
* {@link ConnectivityManager.TETHERING_USB}, or
@@ -234,48 +296,57 @@ object TetheringManager {
*/
@RequiresApi(24)
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null) {
val reference = WeakReference(callback)
if (Build.VERSION.SDK_INT >= 30) {
val request = newTetheringRequestBuilder.newInstance(type).let { builder ->
// setting exemption requires TETHER_PRIVILEGED permission
if (app.checkSelfPermission("android.permission.TETHER_PRIVILEGED") ==
PackageManager.PERMISSION_GRANTED) setExemptFromEntitlementCheck(builder, true)
setShouldShowEntitlementUi(builder, showProvisioningUi)
build(builder)
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
if (Build.VERSION.SDK_INT >= 30) try {
val proxy = proxy(callback)
val executor = handler.makeExecutor()
try {
startTethering(type, true, showProvisioningUi, executor, proxy)
} catch (e1: InvocationTargetException) {
if (e1.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
val result = try {
RootManager.use { it.execute(StartTethering(type, showProvisioningUi)) }
} catch (e2: Exception) {
e2.addSuppressed(e1)
try {
// last resort: start tethering without trying to bypass entitlement check
startTethering(type, false, showProvisioningUi, executor, proxy)
Timber.w(e2)
} catch (e3: Exception) {
e3.addSuppressed(e2)
Timber.w(e3)
callback.onException(e3)
}
return@launch
}
if (result == null) callback.onTetheringStarted()
else callback.onTetheringFailed(result.value)
} else callback.onException(e1)
}
val proxy = Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
@Suppress("NAME_SHADOWING") val callback = reference.get()
return when (val name = method.name) {
"onTetheringStarted" -> {
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringStarted()
}
"onTetheringFailed" -> {
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
callback?.onTetheringFailed(args?.getOrNull(0) as? Int?)
}
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
}
} catch (e: Exception) {
callback.onException(e)
} else @Suppress("DEPRECATION") try {
startTetheringLegacy(type, showProvisioningUi, callback, handler, cacheDir)
} catch (e: InvocationTargetException) {
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
val result = try {
val rootCache = File(cacheDir, "root")
rootCache.mkdirs()
check(rootCache.exists()) { "Creating root cache dir failed" }
RootManager.use {
it.execute(be.mygod.vpnhotspot.root.StartTetheringLegacy(
rootCache, type, showProvisioningUi))
}.value
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
Timber.w(eRoot)
callback.onException(eRoot)
return@launch
}
})
startTethering(instance, request, handler.makeExecutor(), proxy)
} else {
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
dexCache(app.deviceStorage.cacheDir)
handler { proxy, method, args ->
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
@Suppress("NAME_SHADOWING") val callback = reference.get()
when (method.name) {
"onTetheringStarted" -> callback?.onTetheringStarted()
"onTetheringFailed" -> callback?.onTetheringFailed()
else -> ProxyBuilder.callSuper(proxy, method, args)
}
}
}.build()
startTetheringLegacy(app.connectivity, type, showProvisioningUi, proxy, handler)
if (result) callback.onTetheringStarted() else callback.onTetheringFailed()
} else callback.onException(e)
} catch (e: Exception) {
callback.onException(e)
}
}
@@ -290,7 +361,21 @@ object TetheringManager {
*/
@RequiresApi(24)
fun stopTethering(type: Int) {
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type) else stopTetheringLegacy(app.connectivity, type)
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
else stopTetheringLegacy(Services.connectivity, type)
}
@RequiresApi(24)
fun stopTethering(type: Int, callback: (Exception) -> Unit) = try {
stopTethering(type)
} catch (e: InvocationTargetException) {
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
try {
RootManager.use { it.execute(StopTethering(type)) }
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
callback(eRoot)
}
} else callback(e)
}
/**
@@ -517,7 +602,7 @@ object TetheringManager {
* @return error The error code of the last error tethering or untethering the named
* interface
*/
fun getLastTetherError(iface: String): Int = getLastTetherError(app.connectivity, iface) as Int
fun getLastTetherError(iface: String): Int = getLastTetherError(Services.connectivity, iface) as Int
// tether errors defined in ConnectivityManager up to Android 10
private val tetherErrors29 = arrayOf("TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE",

View File

@@ -1,9 +1,12 @@
package be.mygod.vpnhotspot.net.monitor
import android.annotation.TargetApi
import android.net.*
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -23,7 +26,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
.build()
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val properties = app.connectivity.getLinkProperties(network)
val properties = Services.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return
var switching = false
synchronized(this@DefaultNetworkMonitor) {
@@ -83,9 +86,9 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
}
} else {
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
app.connectivity.registerDefaultNetworkCallback(networkCallback)
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
} else try {
app.connectivity.requestNetwork(networkRequest, networkCallback)
Services.connectivity.requestNetwork(networkRequest, networkCallback)
} catch (e: SecurityException) {
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
if (Build.VERSION.SDK_INT != 23) throw e
@@ -98,7 +101,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
override fun destroyLocked() {
if (!registered) return
app.connectivity.unregisterNetworkCallback(networkCallback)
Services.connectivity.unregisterNetworkCallback(networkCallback)
registered = false
currentNetwork = null
currentLinkProperties = null

View File

@@ -1,7 +1,7 @@
package be.mygod.vpnhotspot.net.monitor
import android.net.LinkProperties
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
@@ -27,8 +27,8 @@ class InterfaceMonitor(val iface: String) : UpstreamMonitor() {
private var registered = false
override var currentIface: String? = null
private set
override val currentLinkProperties get() = app.connectivity.allNetworks
.map { app.connectivity.getLinkProperties(it) }
override val currentLinkProperties get() = Services.connectivity.allNetworks
.map { Services.connectivity.getLinkProperties(it) }
.singleOrNull { it?.interfaceName == iface }
override fun registerCallbackLocked(callback: Callback) {

View File

@@ -17,7 +17,7 @@ class IpLinkMonitor private constructor() : IpMonitor() {
monitor = IpLinkMonitor()
instance = monitor
}
monitor.flush()
monitor.flushAsync()
}
fun unregisterCallback(owner: Any) = synchronized(this) {
if (callbacks.remove(owner) == null || callbacks.isNotEmpty()) return@synchronized

View File

@@ -4,20 +4,25 @@ import android.os.Build
import android.system.ErrnoException
import android.system.OsConstants
import androidx.core.content.edit
import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.root.ProcessData
import be.mygod.vpnhotspot.root.ProcessListener
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.root.RoutingCommands
import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.consumeEach
import timber.log.Timber
import java.io.IOException
import java.io.InterruptedIOException
import java.util.concurrent.Executors
import java.util.concurrent.ScheduledExecutorService
import java.util.concurrent.TimeUnit
import kotlin.concurrent.thread
import kotlin.coroutines.EmptyCoroutineContext
abstract class IpMonitor : Runnable {
abstract class IpMonitor {
companion object {
const val KEY = "service.ipMonitor"
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
@@ -51,7 +56,7 @@ abstract class IpMonitor : Runnable {
@Volatile
private var destroyed = false
private var monitor: Process? = null
private var pool: ScheduledExecutorService? = null
private val worker = Job()
private fun handleProcess(builder: ProcessBuilder) {
val process = try {
@@ -79,8 +84,18 @@ abstract class IpMonitor : Runnable {
if ((e.cause as? ErrnoException)?.errno != OsConstants.EBADF) Timber.w(e)
}
err.join()
process.waitFor()
Timber.d("Monitor process exited with ${process.exitValue()}")
Timber.d("Monitor process exited with ${process.waitFor()}")
}
private suspend fun handleChannel(channel: ReceiveChannel<ProcessData>) {
channel.consumeEach {
when (it) {
is ProcessData.StdoutLine -> if (errorMatcher.containsMatchIn(it.line)) {
Timber.w(it.line)
} else processLine(it.line)
is ProcessData.StderrLine -> Timber.e(it.line)
is ProcessData.Exit -> Timber.d("Root monitor process exited with ${it.code}")
}
}
}
init {
@@ -92,17 +107,64 @@ abstract class IpMonitor : Runnable {
handleProcess(ProcessBuilder("ip", "monitor", monitoredObject))
if (destroyed) return@thread
}
handleProcess(ProcessBuilder("su", "-c", "exec ip monitor $monitoredObject"))
try {
runBlocking(EmptyCoroutineContext + worker) {
RootManager.use { server ->
// while we only need to use this server once, we need to also keep the server alive
handleChannel(server.create(ProcessListener(errorMatcher, "ip", "monitor", monitoredObject),
this))
}
}
} catch (e: CancellationException) {
} catch (e: Exception) {
Timber.w(e)
}
if (destroyed) return@thread
app.logEvent("ip_monitor_failure")
}
val pool = Executors.newScheduledThreadPool(1)
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
this.pool = pool
GlobalScope.launch(Dispatchers.IO + worker) {
var server: RootServer? = null
try {
while (isActive) {
delay(1000)
server = work(server)
}
} finally {
if (server != null) RootManager.release(server)
}
}
}
}
fun flush() = thread(name = "${javaClass.simpleName}-flush") { run() }
/**
* Possibly blocking. Should run in IO dispatcher or use [flushAsync].
*/
suspend fun flush() = work(null)?.let { RootManager.release(it) }
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
private suspend fun work(server: RootServer?): RootServer? {
if (currentMode != Mode.PollRoot) try {
poll()
return server
} catch (e: IOException) {
app.logEvent("ip_poll_failure")
Timber.d(e)
}
var newServer = server
try {
val command = listOf("ip", monitoredObject)
val result = (server ?: RootManager.acquire().also { newServer = it })
.execute(RoutingCommands.Process(command))
result.check(command, false)
val lines = result.out.lines()
if (lines.any { errorMatcher.containsMatchIn(it) }) throw IOException(result.out)
processLines(lines.asSequence())
} catch (e: RuntimeException) {
app.logEvent("ip_su_poll_failure") { param("cause", e.message.toString()) }
Timber.d(e)
}
return newServer
}
private fun poll() {
val process = ProcessBuilder("ip", monitoredObject)
@@ -125,32 +187,9 @@ abstract class IpMonitor : Runnable {
}
}
override fun run() {
if (currentMode != Mode.PollRoot) try {
return poll()
} catch (e: IOException) {
app.logEvent("ip_poll_failure")
Timber.d(e)
}
try {
val command = "ip $monitoredObject"
RootSession.use { shell ->
val result = shell.execQuiet(command)
RootSession.checkOutput(command, result, false)
if (result.out.any { errorMatcher.containsMatchIn(it) }) {
throw IOException(result.out.joinToString("\n"))
}
processLines(result.out.asSequence())
}
} catch (e: RuntimeException) {
app.logEvent("ip_su_poll_failure") { param("cause", e.message.toString()) }
Timber.d(e)
}
}
fun destroy() {
destroyed = true
monitor?.destroy()
pool?.shutdown()
worker.cancel()
}
}

View File

@@ -20,7 +20,7 @@ class IpNeighbourMonitor private constructor() : IpMonitor() {
if (monitor == null) {
monitor = IpNeighbourMonitor()
instance = monitor
monitor.flush()
monitor.flushAsync()
null
} else monitor.neighbours.values
}?.let { callback.onIpNeighbourAvailable(it) }

View File

@@ -42,6 +42,7 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout:
var enabled
get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
set(value) {
// TODO: WRITE_SECURE_SETTINGS permission
check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0))
}
@Deprecated("Use SoftApConfigurationCompat instead")

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.net.monitor
import android.util.LongSparseArray
import androidx.collection.LongSparseArray
import androidx.collection.set
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.room.AppDatabase
@@ -63,10 +64,11 @@ object TrafficRecorder {
loop@ for (line in RootSession.use {
val command = "$IPTABLES -nvx -L vpnhotspot_acl"
val result = it.execQuiet(command)
val message = RootSession.checkOutput(command, result, false, false)
val message = result.message(listOf(command))
if (result.err.isNotEmpty()) Timber.i(message)
result.out.drop(2)
result.out.lineSequence().drop(2)
}) {
if (line.isBlank()) continue
val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
try {
check(columns.size >= 9)
@@ -104,7 +106,7 @@ object TrafficRecorder {
}
if (oldRecord.id != null) {
check(records.put(key, record) == oldRecord)
oldRecords.put(oldRecord.id!!, oldRecord)
oldRecords[oldRecord.id!!] = oldRecord
}
}
else -> check(false)

View File

@@ -1,7 +1,10 @@
package be.mygod.vpnhotspot.net.monitor
import android.net.*
import be.mygod.vpnhotspot.App.Companion.app
import android.net.ConnectivityManager
import android.net.LinkProperties
import android.net.Network
import android.net.NetworkCapabilities
import be.mygod.vpnhotspot.util.Services
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber
@@ -21,7 +24,7 @@ object VpnMonitor : UpstreamMonitor() {
}
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
val properties = app.connectivity.getLinkProperties(network)
val properties = Services.connectivity.getLinkProperties(network)
val ifname = properties?.interfaceName ?: return
var switching = false
synchronized(this@VpnMonitor) {
@@ -88,14 +91,14 @@ object VpnMonitor : UpstreamMonitor() {
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties)
}
} else {
app.connectivity.registerNetworkCallback(request, networkCallback)
Services.connectivity.registerNetworkCallback(request, networkCallback)
registered = true
}
}
override fun destroyLocked() {
if (!registered) return
app.connectivity.unregisterNetworkCallback(networkCallback)
Services.connectivity.unregisterNetworkCallback(networkCallback)
registered = false
available.clear()
currentNetwork = null

View File

@@ -1,24 +1,20 @@
package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.p2p.WifiP2pGroup
import android.os.Build
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.util.RootSession
import be.mygod.vpnhotspot.root.RepeaterCommands
import be.mygod.vpnhotspot.root.RootManager
import com.google.firebase.crashlytics.FirebaseCrashlytics
import java.io.File
/**
* This parser is based on:
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
*/
class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerAddress: String? = null) {
class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
companion object {
private const val TAG = "P2pSupplicantConfiguration"
private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf"
private const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf"
private const val PERSISTENT_MAC = "p2p_device_persistent_mac_addr="
private val networkParser =
"^(bssid=(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})|psk=(ext:|\"(.*)\"|[0-9a-fA-F]{64}\$)?)".toRegex()
@@ -36,12 +32,11 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
override fun toString() = joinToString("\n")
}
private class Parser(val lines: List<String>) {
private val iterator = lines.iterator()
private class Parser(val lines: Iterator<String>) {
lateinit var line: String
lateinit var trimmed: String
fun next() = if (iterator.hasNext()) {
line = iterator.next().apply { trimmed = trimStart('\r', '\t', ' ') }
fun next() = if (lines.hasNext()) {
line = lines.next().apply { trimmed = trimStart('\r', '\t', ' ') }
true
} else false
}
@@ -49,14 +44,12 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
private data class Content(val lines: ArrayList<Any>, var target: NetworkBlock, var persistentMacLine: Int?,
var legacy: Boolean)
private val content = RootSession.use {
private lateinit var content: Content
suspend fun init(ownerAddress: String? = null) {
val result = ArrayList<Any>()
var target: NetworkBlock? = null
var persistentMacLine: Int? = null
val command = "cat $CONF_PATH_TREBLE || cat $CONF_PATH_LEGACY"
val shell = it.execQuiet(command)
RootSession.checkOutput(command, shell, false, false)
val parser = Parser(shell.out)
val (config, legacy) = RootManager.use { it.execute(RepeaterCommands.ReadP2pConfig()) }
try {
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
.distinct()
@@ -68,6 +61,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
false
}
}
val parser = Parser(config.lineSequence().iterator())
while (parser.next()) {
if (parser.trimmed.startsWith("network={")) {
val block = NetworkBlock()
@@ -129,22 +123,22 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
if (target == null) target = this
})
}
Content(result, target!!.apply {
content = Content(result, target!!.apply {
RepeaterService.lastMac = bssid!!
}, persistentMacLine, shell.err.isNotEmpty())
} catch (e: RuntimeException) {
}, persistentMacLine, legacy)
} catch (e: Exception) {
FirebaseCrashlytics.getInstance().apply {
setCustomKey(TAG, parser.lines.joinToString("\n"))
setCustomKey(TAG, config)
setCustomKey("$TAG.ownerAddress", ownerAddress.toString())
setCustomKey("$TAG.p2pGroup", group.toString())
}
throw e
}
}
val psk = group?.passphrase ?: content.target.psk!!
val bssid = MacAddressCompat.fromString(content.target.bssid!!)
val psk by lazy { group?.passphrase ?: content.target.psk!! }
val bssid by lazy { MacAddressCompat.fromString(content.target.bssid!!) }
fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
val (lines, block, persistentMacLine, legacy) = content
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
@@ -153,25 +147,6 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
block[block.bssidLine!!] = "\tbssid=$bssid"
}
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.deviceStorage.cacheDir)
try {
tempFile.printWriter().use { writer ->
lines.forEach { writer.println(it) }
}
// pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
RootSession.use {
it.exec("cat ${tempFile.absolutePath} > ${if (legacy) CONF_PATH_LEGACY else CONF_PATH_TREBLE}")
if (Build.VERSION.SDK_INT >= 23) it.exec("pkill wpa_supplicant") else {
val result = try {
it.execOut("ps | grep wpa_supplicant").split(whitespaceMatcher).apply { check(size >= 2) }
} catch (e: Exception) {
throw IllegalStateException("wpa_supplicant not found, please toggle Airplane mode manually", e)
}
it.exec("kill ${result[1]}")
}
}
} finally {
if (!tempFile.delete()) tempFile.deleteOnExit()
}
RootManager.use { it.execute(RepeaterCommands.WriteP2pConfig(lines.joinToString("\n"), legacy)) }
}
}

View File

@@ -38,10 +38,10 @@ data class SoftApConfigurationCompat(
/**
* TODO
*/
const val BAND_ANY = -1
const val BAND_2GHZ = 0
const val BAND_5GHZ = 1
const val BAND_6GHZ = 2
const val BAND_ANY = 0
const val BAND_2GHZ = 1
const val BAND_5GHZ = 2
const val BAND_6GHZ = 3
const val CH_INVALID = 0
// TODO: localize?
@@ -144,7 +144,9 @@ data class SoftApConfigurationCompat(
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
}
@get:RequiresApi(30)
private val setChannel by lazy { classBuilder.getDeclaredMethod("setChannel", Int::class.java) }
private val setChannel by lazy {
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
}
@get:RequiresApi(30)
private val setClientControlByUserEnabled by lazy {
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
@@ -156,7 +158,9 @@ data class SoftApConfigurationCompat(
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
}
@get:RequiresApi(30)
private val setPassphrase by lazy { classBuilder.getDeclaredMethod("setPassphrase", String::class.java) }
private val setPassphrase by lazy {
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
}
@get:RequiresApi(30)
private val setShutdownTimeoutMillis by lazy {
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
@@ -186,7 +190,7 @@ data class SoftApConfigurationCompat(
}
},
preSharedKey,
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) else BAND_ANY, // TODO
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) + 1 else BAND_ANY, // TODO
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else CH_INVALID, // TODO
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
0, // TODO: unsupported field should have @RequiresApi?
@@ -275,10 +279,10 @@ data class SoftApConfigurationCompat(
// TODO: can we always call copy constructor?
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
setSsid(builder, ssid)
// TODO: setSecurityType
setPassphrase(builder, passphrase)
setBand(builder, band)
setChannel(builder, channel)
setPassphrase(builder, passphrase, securityType)
// TODO: how to use these?
// setBand(builder, band)
// setChannel(builder, band, channel)
setBssid(builder, bssid?.toPlatform())
setMaxNumberOfClients(builder, maxNumberOfClients)
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)

View File

@@ -5,8 +5,8 @@ import android.net.wifi.SoftApConfiguration
import android.net.wifi.WifiManager
import android.os.Build
import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.util.Services
object WifiApManager {
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
@@ -22,22 +22,18 @@ object WifiApManager {
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
}
var configuration: SoftApConfigurationCompat
get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
(getWifiApConfiguration(app.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat.empty()
} else (getSoftApConfiguration(app.wifi) as SoftApConfiguration).toCompat()
set(value) = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
require(setWifiApConfiguration(app.wifi,
value.toWifiConfiguration()) as Boolean) { "setWifiApConfiguration failed" }
} else require(setSoftApConfiguration(app.wifi, value.toPlatform()) as Boolean) {
"setSoftApConfiguration failed"
}
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
?: SoftApConfigurationCompat.empty()
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat()
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
private val cancelLocalOnlyHotspotRequest by lazy {
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
}
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi)
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
@Suppress("DEPRECATION")
private val setWifiApEnabled by lazy {
@@ -66,13 +62,13 @@ object WifiApManager {
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26, malfunctioning on API 25")
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
app.wifi.isWifiEnabled = false
app.wifi.setWifiApEnabled(wifiConfig, true)
Services.wifi.isWifiEnabled = false
Services.wifi.setWifiApEnabled(wifiConfig, true)
}
@Suppress("DEPRECATION")
@Deprecated("Not usable since API 26")
fun stop() {
app.wifi.setWifiApEnabled(null, false)
app.wifi.isWifiEnabled = true
Services.wifi.setWifiApEnabled(null, false)
Services.wifi.isWifiEnabled = true
}
}

View File

@@ -13,6 +13,7 @@ import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services
/**
* This mechanism is used to maximize profit. Source: https://stackoverflow.com/a/29657230/2245107
@@ -91,7 +92,7 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
override fun onDestroy(owner: LifecycleOwner) = app.pref.unregisterOnSharedPreferenceChangeListener(this)
}
private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
private val wifi = Services.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
@SuppressLint("WakelockTimeout")
private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() }