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

@@ -31,6 +31,14 @@ I don't know about you but I can't get my stupid Windows 10 to work with
now that they introduced this now that they introduced this
[Mobile hotspot](https://support.microsoft.com/en-us/help/4027762/windows-use-your-pc-as-a-mobile-hotspot). [Mobile hotspot](https://support.microsoft.com/en-us/help/4027762/windows-use-your-pc-as-a-mobile-hotspot).
## Features that requires system app installation
The following features in the app requires it to be installed under `/system/priv-app`.
One way to do this is to use [App systemizer for Magisk](https://github.com/Magisk-Modules-Repo/terminal_systemizer).
* (prior to Android 11) Read/write system Wi-Fi hotspot configuration. ([#117](https://github.com/Mygod/VPNHotspot/issues/117))
* (since Android 11) Use the Bluetooth tethering shortcut switch in app.
## Settings and How to Use Them ## Settings and How to Use Them
Default settings are picked to suit general use cases and maximize compatibility but it might not be optimal for battery Default settings are picked to suit general use cases and maximize compatibility but it might not be optimal for battery
@@ -271,4 +279,4 @@ If some of these are unavailable, you can alternatively install a recent version
Wi-Fi driver `wpa_supplicant`: Wi-Fi driver `wpa_supplicant`:
* P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format; * P2P configuration file is assumed to be saved to [`/data/vendor/wifi/wpa/p2p_supplicant.conf` or `/data/misc/wifi/p2p_supplicant.conf`](https://android.googlesource.com/platform/external/wpa_supplicant_8/+/0b4856b6dc451e290f1f64f6af17e010be78c073/wpa_supplicant/hidl/1.1/supplicant.cpp#26) and have reasonable format;
* Android system is expected to restart `wpa_supplicant` after it crashes. * Android system is expected to restart `wpa_supplicant` after it terminates.

View File

@@ -15,7 +15,7 @@ buildscript {
classpath(kotlin("gradle-plugin", kotlinVersion)) classpath(kotlin("gradle-plugin", kotlinVersion))
classpath("com.android.tools.build:gradle:4.1.0-beta01") classpath("com.android.tools.build:gradle:4.1.0-beta01")
classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0") classpath("com.github.ben-manes:gradle-versions-plugin:0.28.0")
classpath("com.google.firebase:firebase-crashlytics-gradle:2.1.1") classpath("com.google.firebase:firebase-crashlytics-gradle:2.2.0")
classpath("com.google.android.gms:oss-licenses-plugin:0.10.2") classpath("com.google.android.gms:oss-licenses-plugin:0.10.2")
classpath("com.google.gms:google-services:4.3.3") classpath("com.google.gms:google-services:4.3.3")
} }

View File

@@ -84,15 +84,15 @@ dependencies {
implementation("androidx.room:room-ktx:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01") implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0-rc01")
implementation("com.android.billingclient:billing-ktx:3.0.0") implementation("com.android.billingclient:billing-ktx:3.0.0")
implementation("com.github.topjohnwu.libsu:core:2.5.1")
implementation("com.google.android.gms:play-services-oss-licenses:17.0.0") implementation("com.google.android.gms:play-services-oss-licenses:17.0.0")
implementation("com.google.android.material:material:1.2.0-beta01") implementation("com.google.android.material:material:1.2.0-beta01")
implementation("com.google.firebase:firebase-analytics-ktx:17.4.3") implementation("com.google.firebase:firebase-analytics-ktx:17.4.3")
implementation("com.google.firebase:firebase-crashlytics:17.0.1") implementation("com.google.firebase:firebase-crashlytics:17.1.0")
implementation("com.google.zxing:core:3.4.0") implementation("com.google.zxing:core:3.4.0")
implementation("com.jakewharton.timber:timber:4.7.1") implementation("com.jakewharton.timber:timber:4.7.1")
implementation("com.linkedin.dexmaker:dexmaker:2.28.0") implementation("com.linkedin.dexmaker:dexmaker:2.28.0")
implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0") implementation("com.takisoft.preferencex:preferencex-simplemenu:1.1.0")
implementation("eu.chainfire:librootjava:1.3.0")
implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.2") implementation("org.jetbrains.kotlinx:kotlinx-collections-immutable:0.3.2")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7") implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.7")
testImplementation("junit:junit:4.13") testImplementation("junit:junit:4.13")

View File

@@ -36,14 +36,14 @@
tools:ignore="ProtectedPermissions" /> tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.MANAGE_USB" <uses-permission android:name="android.permission.MANAGE_USB"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.NETWORK_SETTINGS"
tools:ignore="ProtectedPermissions" />
<uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG" <uses-permission android:name="android.permission.OVERRIDE_WIFI_CONFIG"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.TETHER_PRIVILEGED" <uses-permission android:name="android.permission.TETHER_PRIVILEGED"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/> <uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS"
tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.WRITE_SETTINGS" <uses-permission android:name="android.permission.WRITE_SETTINGS"
tools:ignore="ProtectedPermissions"/> tools:ignore="ProtectedPermissions"/>
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION"/>

View File

@@ -0,0 +1,427 @@
package be.mygod.librootkotlinx
import android.content.Context
import android.os.Looper
import android.os.Parcelable
import android.os.RemoteException
import android.util.Log
import androidx.collection.LongSparseArray
import androidx.collection.set
import androidx.collection.valueIterator
import eu.chainfire.librootjava.AppProcess
import eu.chainfire.librootjava.RootJava
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.io.*
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.CountDownLatch
import kotlin.system.exitProcess
class RootServer @JvmOverloads constructor(private val warnLogger: (String) -> Unit = { Log.w(TAG, it) }) {
private sealed class Callback {
abstract fun cancel()
abstract fun shouldRemove(result: Byte): Boolean
abstract operator fun invoke(input: DataInputStream, result: Byte)
class Ordinary(private val classLoader: ClassLoader?,
private val callback: CompletableDeferred<Parcelable?>) : Callback() {
override fun cancel() = callback.cancel()
override fun shouldRemove(result: Byte) = true
override fun invoke(input: DataInputStream, result: Byte) {
when (result.toInt()) {
SUCCESS -> callback.complete(input.readParcelable(classLoader))
EX_GENERIC -> callback.completeExceptionally(RemoteException(input.readUTF()))
EX_PARCELABLE -> callback.completeExceptionally(RemoteException().initCause(
input.readParcelable<Parcelable>(classLoader) as Throwable?))
else -> throw IllegalArgumentException("Unexpected result $result")
}
}
}
class Channel(private val classLoader: ClassLoader?,
private val channel: SendChannel<Parcelable?>,
private val server: RootServer,
private val index: Long) : Callback() {
var active = true
val finish: CompletableDeferred<Unit> = CompletableDeferred()
override fun cancel() = finish.cancel()
override fun shouldRemove(result: Byte) = result.toInt() != SUCCESS
override fun invoke(input: DataInputStream, result: Byte) {
when (result.toInt()) {
// the channel we are supporting should never block
SUCCESS -> check(try {
channel.offer(input.readParcelable(classLoader))
} catch (closed: Throwable) {
active = false
GlobalScope.launch(Dispatchers.Unconfined) { sendClosed() }
finish.completeExceptionally(closed)
return
})
EX_GENERIC -> finish.completeExceptionally(RemoteException(input.readUTF()))
EX_PARCELABLE -> finish.completeExceptionally(RemoteException().initCause(
input.readParcelable<Parcelable>(classLoader) as Throwable?))
CHANNEL_CONSUMED -> finish.complete(Unit)
else -> throw IllegalArgumentException("Unexpected result $result")
}
}
suspend fun sendClosed() = server.execute(ChannelClosed(index))
}
}
private lateinit var process: Process
private lateinit var worker: Thread
/**
* Thread safety: needs to be protected by mutex.
*/
private lateinit var output: DataOutputStream
@Volatile
var active = false
private var counter = 0L
private val callbackListenerExit = CompletableDeferred<Unit>()
private val callbackLookup = LongSparseArray<Callback>()
private val mutex = Mutex()
/**
* If we encountered unexpected output from stderr during initialization, its content will be stored here.
*
* It is advised to read this after initializing the instance.
*/
fun readUnexpectedStderr(): String? {
var available = process.errorStream.available()
return if (available <= 0) null else String(ByteArrayOutputStream().apply {
while (available > 0) {
val bytes = ByteArray(available)
val len = process.errorStream.read(bytes)
if (len < 0) throw EOFException() // should not happen
write(bytes, 0, len)
available = process.errorStream.available()
}
}.toByteArray())
}
private fun BufferedReader.lookForToken(token: String) {
while (true) {
val line = readLine() ?: throw EOFException()
if (line.endsWith(token)) {
val extraLength = line.length - token.length
if (extraLength > 0) warnLogger(line.substring(0, extraLength))
break
}
warnLogger(line)
}
}
private fun doInit(context: Context, niceName: String) {
val writer: DataOutputStream
val reader: BufferedReader
try {
process = ProcessBuilder("su").start()
val token1 = UUID.randomUUID().toString()
writer = DataOutputStream(process.outputStream.buffered())
writer.writeBytes("echo $token1\n")
writer.flush()
reader = process.inputStream.bufferedReader()
reader.lookForToken(token1)
} catch (e: Exception) {
throw NoShellException(e)
}
if (DEBUG) Log.d(TAG, "Root shell initialized")
val appProcess = AppProcess.getAppProcess()
val token2 = UUID.randomUUID().toString()
writer.writeBytes(RootJava.getLaunchString(context.packageCodePath + " exec", // hack: plugging in exec
RootServer::class.java.name, appProcess, AppProcess.guessIfAppProcessIs64Bits(appProcess),
arrayOf("$token2\n"), niceName))
writer.flush()
reader.lookForToken(token2) // wait for ready signal
output = writer
require(!active)
active = true
if (DEBUG) Log.d(TAG, "Root server initialized")
}
private fun callbackSpin() {
val input = DataInputStream(process.inputStream.buffered())
while (active) {
val index = try {
input.readLong()
} catch (_: EOFException) {
break
}
val result = input.readByte()
val callback = mutex.synchronized {
callbackLookup[index]!!.also { if (it.shouldRemove(result)) callbackLookup.remove(index) }
}
if (DEBUG) Log.d(TAG, "Received callback #$index: $result")
callback(input, result)
}
}
/**
* Initialize a RootServer synchronously, can throw a lot of exceptions.
*
* @param context Any [Context] from the app.
* @param niceName Name to call the rooted Java process.
*/
suspend fun init(context: Context, niceName: String = "${context.packageName}:root") {
val future = CompletableDeferred<Unit>()
worker = Thread {
try {
doInit(context, niceName)
future.complete(Unit)
} catch (e: Throwable) {
future.completeExceptionally(e)
callbackListenerExit.complete(Unit)
return@Thread
}
try {
callbackSpin()
} catch (e: Throwable) {
callbackListenerExit.completeExceptionally(e)
return@Thread
} finally {
if (DEBUG) Log.d(TAG, "Waiting for exit")
process.waitFor()
runBlocking { closeInternal(true) }
}
check(process.errorStream.available() == 0) // stderr should not be used
callbackListenerExit.complete(Unit)
}
worker.start()
future.await()
}
/**
* Convenience function that initializes and also logs warnings to [Log].
*/
suspend fun initAndroidLog(context: Context, niceName: String = "${context.packageName}:root") = try {
init(context, niceName)
} finally {
readUnexpectedStderr()?.let { Log.e(TAG, it) }
}
/**
* Caller should check for active.
*/
private fun sendLocked(command: Parcelable) {
output.writeParcelable(command)
output.flush()
if (DEBUG) Log.d(TAG, "Sent #$counter: $command")
counter++
}
suspend fun execute(command: RootCommandOneWay) = mutex.withLock { if (active) sendLocked(command) }
@Throws(RemoteException::class)
suspend inline fun <reified T : Parcelable?> execute(command: RootCommand<T>) =
execute(command, T::class.java.classLoader)
@Throws(RemoteException::class)
suspend fun <T : Parcelable?> execute(command: RootCommand<T>, classLoader: ClassLoader?): T {
val future = CompletableDeferred<T>()
mutex.withLock {
if (active) {
@Suppress("UNCHECKED_CAST")
callbackLookup[counter] = Callback.Ordinary(classLoader, future as CompletableDeferred<Parcelable?>)
sendLocked(command)
} else future.cancel()
}
return future.await()
}
@ExperimentalCoroutinesApi
@Throws(RemoteException::class)
inline fun <reified T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope) =
create(command, scope, T::class.java.classLoader)
@ExperimentalCoroutinesApi
@Throws(RemoteException::class)
fun <T : Parcelable?> create(command: RootCommandChannel<T>, scope: CoroutineScope,
classLoader: ClassLoader?) = scope.produce<T>(
capacity = command.capacity.also {
when (it) {
Channel.UNLIMITED, Channel.CONFLATED -> { }
else -> throw IllegalArgumentException("Unsupported channel capacity $it")
}
}) {
@Suppress("UNCHECKED_CAST")
val callback = Callback.Channel(classLoader, this as SendChannel<Parcelable?>, this@RootServer, counter)
mutex.withLock {
if (active) {
callbackLookup[counter] = callback
sendLocked(command)
} else callback.finish.cancel()
}
try {
callback.finish.await()
} finally {
if (callback.active) withContext(NonCancellable) { callback.sendClosed() }
callback.active = false
}
}
private suspend fun closeInternal(fromWorker: Boolean = false) = mutex.withLock {
if (active) {
active = false
if (DEBUG) Log.d(TAG, "Shutting down from client")
sendLocked(Shutdown())
output.close()
process.outputStream.close()
if (DEBUG) Log.d(TAG, "Client closed")
}
if (fromWorker) {
for (callback in callbackLookup.valueIterator()) callback.cancel()
callbackLookup.clear()
}
}
/**
* Shutdown the instance gracefully.
*/
suspend fun close() {
closeInternal()
callbackListenerExit.await()
}
companion object {
/**
* If set to true, debug information will be printed to logcat.
*/
@JvmStatic
var DEBUG = false
private const val TAG = "RootServer"
private const val SUCCESS = 0
private const val EX_GENERIC = 1
private const val EX_PARCELABLE = 2
private const val CHANNEL_CONSUMED = 3
private inline fun <reified T : Parcelable> DataInputStream.readParcelable(
classLoader: ClassLoader? = T::class.java.classLoader
) = ByteArray(readInt()).also { readFully(it) }.toParcelable<T>(classLoader)
private fun DataOutputStream.writeParcelable(data: Parcelable?, parcelableFlags: Int = 0) {
val bytes = data.toByteArray(parcelableFlags)
writeInt(bytes.size)
write(bytes)
}
private inline fun <T> Mutex.synchronized(crossinline block: () -> T): T = runBlocking {
withLock { block() }
}
@JvmStatic
fun main(args: Array<String>) {
Thread.setDefaultUncaughtExceptionHandler { thread, throwable ->
Log.e(TAG, "Uncaught exception from $thread", throwable)
exitProcess(1)
}
rootMain(args)
exitProcess(0) // there might be other non-daemon threads
}
private fun DataOutputStream.pushThrowable(callback: Long, e: Throwable) {
writeLong(callback)
if (e is Parcelable) {
writeByte(EX_PARCELABLE)
writeParcelable(e)
} else {
writeByte(EX_GENERIC)
writeUTF(StringWriter().also {
e.printStackTrace(PrintWriter(it))
}.toString())
}
flush()
}
private fun DataOutputStream.pushResult(callback: Long, result: Parcelable?) {
writeLong(callback)
writeByte(SUCCESS)
writeParcelable(result)
flush()
}
private fun rootMain(args: Array<String>) {
require(args.isNotEmpty())
RootJava.restoreOriginalLdLibraryPath()
val mainInitialized = CountDownLatch(1)
val main = Thread({
@Suppress("DEPRECATION")
Looper.prepareMainLooper()
mainInitialized.countDown()
Looper.loop()
}, "main")
main.start()
val job = Job()
val defaultWorker by lazy {
mainInitialized.await()
CoroutineScope(Dispatchers.Main.immediate + job)
}
val callbackWorker = newSingleThreadContext("callbackWorker")
val channels = LongSparseArray<WeakReference<ReceiveChannel<Parcelable?>>>()
// thread safety: usage of output should be guarded by callbackWorker
val output = DataOutputStream(System.out.buffered().apply {
val writer = writer()
writer.appendln(args[0]) // echo ready signal
writer.flush()
})
// thread safety: usage of input should be in main thread
val input = DataInputStream(System.`in`.buffered())
var counter = 0L
if (DEBUG) Log.d(TAG, "Server entering main loop")
loop@ while (true) {
val command = try {
input.readParcelable<Parcelable>(RootServer::class.java.classLoader)
} catch (e: EOFException) {
break
}
val callback = counter
if (DEBUG) Log.d(TAG, "Received #$callback: $command")
when (command) {
is ChannelClosed -> channels[command.index]?.get()?.cancel()
is RootCommandOneWay -> defaultWorker.launch {
try {
command.execute()
} catch (e: Throwable) {
Log.e(command.javaClass.simpleName, "Unexpected exception in RootCommandOneWay", e)
}
}
is RootCommand<*> -> defaultWorker.launch {
val result = try {
val result = command.execute();
{ output.pushResult(callback, result) }
} catch (e: Throwable) {
{ output.pushThrowable(callback, e) }
}
withContext(callbackWorker) { result() }
}
is RootCommandChannel<*> -> defaultWorker.launch {
val result = try {
command.create(defaultWorker).also {
channels[callback] = WeakReference(it)
}.consumeEach { result ->
withContext(callbackWorker) { output.pushResult(callback, result) }
};
@Suppress("BlockingMethodInNonBlockingContext") {
output.writeByte(CHANNEL_CONSUMED)
output.writeLong(callback)
output.flush()
}
} catch (e: Throwable) {
{ output.pushThrowable(callback, e) }
} finally {
channels.remove(callback)
}
withContext(callbackWorker) { result() }
}
is Shutdown -> break@loop
else -> throw IllegalArgumentException("Unrecognized input: $command")
}
counter++
}
job.cancel()
if (DEBUG) Log.d(TAG, "Clean up initiated before exit. Jobs: ${job.children.joinToString()}")
if (runBlocking { withTimeoutOrNull(5000) { job.join() } } == null) {
Log.w(TAG, "Clean up timeout: ${job.children.joinToString()}")
} else if (DEBUG) Log.d(TAG, "Clean up finished, exiting")
}
}
}

View File

@@ -0,0 +1,106 @@
package be.mygod.librootkotlinx
import kotlinx.coroutines.*
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import java.util.concurrent.TimeUnit
/**
* This object manages creation of [RootServer] and times them out automagically, with default timeout of 5 minutes.
*/
abstract class RootSession {
protected open fun createServer() = RootServer()
protected abstract suspend fun initServer(server: RootServer)
/**
* Timeout to close [RootServer] in milliseconds.
*/
protected open val timeout get() = TimeUnit.MINUTES.toMillis(5)
protected open val timeoutDispatcher get() = Dispatchers.Default
private val mutex = Mutex()
private var server: RootServer? = null
private var timeoutJob: Job? = null
private var usersCount = 0L
private var closePending = false
private suspend fun ensureServerLocked(): RootServer {
server?.let { return it }
check(usersCount == 0L)
val server = createServer()
try {
initServer(server)
this.server = server
return server
} catch (e: Throwable) {
try {
server.close()
} catch (eClose: Throwable) {
throw eClose.apply { addSuppressed(e) }
}
throw e
}
}
private suspend fun closeLocked() {
server?.close()
server = null
}
private fun startTimeoutLocked() {
check(timeoutJob == null)
timeoutJob = GlobalScope.launch(timeoutDispatcher, CoroutineStart.UNDISPATCHED) {
delay(timeout)
mutex.withLock {
check(usersCount == 0L)
closeLocked()
timeoutJob = null
}
}
}
private fun haltTimeoutLocked() {
timeoutJob?.cancel()
timeoutJob = null
}
suspend fun acquire() = withContext(NonCancellable) {
mutex.withLock {
haltTimeoutLocked()
closePending = false
ensureServerLocked().also { ++usersCount }
}
}
suspend fun release(server: RootServer) = withContext(NonCancellable) {
mutex.withLock {
if (this@RootSession.server != server) return@withLock // outdated reference
require(usersCount > 0)
when {
!server.active -> {
usersCount = 0
closeLocked()
closePending = false
return@withLock
}
--usersCount > 0L -> return@withLock
closePending -> {
closeLocked()
closePending = false
}
else -> startTimeoutLocked()
}
}
}
suspend inline fun <T> use(block: (RootServer) -> T): T {
val server = acquire()
try {
return block(server)
} finally {
release(server)
}
}
suspend fun closeExisting() = mutex.withLock {
if (usersCount > 0) closePending = true else {
haltTimeoutLocked()
closeLocked()
}
}
}

View File

@@ -0,0 +1,47 @@
package be.mygod.librootkotlinx
import android.os.Parcelable
import androidx.annotation.MainThread
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.channels.ReceiveChannel
interface RootCommand<Result : Parcelable?> : Parcelable {
/**
* If a throwable was thrown, it will be wrapped in RemoteException only if it implements [Parcelable].
*/
@MainThread
suspend fun execute(): Result
}
typealias RootCommandNoResult = RootCommand<Parcelable?>
/**
* Execute a command and discards its result, even if an exception occurs.
*
* If you want to catch exception, use e.g. [RootCommandNoResult] and return null.
*/
interface RootCommandOneWay : Parcelable {
@MainThread
suspend fun execute()
}
interface RootCommandChannel<T : Parcelable?> : Parcelable {
/**
* The capacity of the channel that is returned by [create] to be used by client.
* Only [Channel.UNLIMITED] and [Channel.CONFLATED] is supported for now to avoid blocking the entire connection.
*/
val capacity: Int get() = Channel.UNLIMITED
@MainThread
fun create(scope: CoroutineScope): ReceiveChannel<T>
}
@Parcelize
internal class ChannelClosed(val index: Long) : RootCommandOneWay {
override suspend fun execute() = throw IllegalStateException("Internal implementation")
}
@Parcelize
internal class Shutdown : Parcelable

View File

@@ -0,0 +1,201 @@
package be.mygod.librootkotlinx
import android.annotation.SuppressLint
import android.os.IBinder
import android.os.Parcel
import android.os.Parcelable
import android.util.*
import kotlinx.android.parcel.Parcelize
class NoShellException(cause: Throwable) : Exception("Root missing", cause)
@Parcelize
data class ParcelableByte(val value: Byte) : Parcelable
@Parcelize
data class ParcelableShort(val value: Short) : Parcelable
@Parcelize
data class ParcelableInt(val value: Int) : Parcelable
@Parcelize
data class ParcelableLong(val value: Long) : Parcelable
@Parcelize
data class ParcelableFloat(val value: Float) : Parcelable
@Parcelize
data class ParcelableDouble(val value: Double) : Parcelable
@Parcelize
data class ParcelableBoolean(val value: Boolean) : Parcelable
@Parcelize
data class ParcelableString(val value: String) : Parcelable
@Parcelize
data class ParcelableByteArray(val value: ByteArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableByteArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableIntArray(val value: IntArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableIntArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableLongArray(val value: LongArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableLongArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableFloatArray(val value: FloatArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableFloatArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableDoubleArray(val value: DoubleArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableDoubleArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableBooleanArray(val value: BooleanArray) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableBooleanArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableStringArray(val value: Array<String>) : Parcelable {
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as ParcelableStringArray
if (!value.contentEquals(other.value)) return false
return true
}
override fun hashCode(): Int {
return value.contentHashCode()
}
}
@Parcelize
data class ParcelableStringList(val value: List<String>) : Parcelable
@Parcelize
data class ParcelableSparseIntArray(val value: SparseIntArray) : Parcelable
@Parcelize
data class ParcelableSparseLongArray(val value: SparseLongArray) : Parcelable
@Parcelize
data class ParcelableSparseBooleanArray(val value: SparseBooleanArray) : Parcelable
@Parcelize
data class ParcelableCharSequence(val value: CharSequence) : Parcelable
@Parcelize
data class ParcelableSize(val value: Size) : Parcelable
@Parcelize
data class ParcelableSizeF(val value: SizeF) : Parcelable
@SuppressLint("Recycle")
inline fun <T> useParcel(block: (Parcel) -> T) = Parcel.obtain().run {
try {
block(this)
} finally {
recycle()
}
}
fun Parcelable?.toByteArray(parcelableFlags: Int = 0) = useParcel { p ->
p.writeParcelable(this, parcelableFlags)
p.marshall()
}
inline fun <reified T : Parcelable> ByteArray.toParcelable(classLoader: ClassLoader? = T::class.java.classLoader) =
useParcel { p ->
p.unmarshall(this, 0, size)
p.setDataPosition(0)
p.readParcelable<T>(classLoader)
}

View File

@@ -2,11 +2,9 @@ package be.mygod.vpnhotspot
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
import android.app.UiModeManager
import android.content.ClipboardManager import android.content.ClipboardManager
import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.Size import androidx.annotation.Size
@@ -18,10 +16,12 @@ import androidx.core.provider.FontRequest
import androidx.emoji.text.EmojiCompat import androidx.emoji.text.EmojiCompat
import androidx.emoji.text.FontRequestEmojiCompatConfig import androidx.emoji.text.FontRequestEmojiCompatConfig
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import be.mygod.librootkotlinx.NoShellException
import be.mygod.vpnhotspot.net.DhcpWorkaround import be.mygod.vpnhotspot.net.DhcpWorkaround
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.DeviceStorageApp import be.mygod.vpnhotspot.util.DeviceStorageApp
import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.util.Services
import com.google.firebase.analytics.ktx.ParametersBuilder import com.google.firebase.analytics.ktx.ParametersBuilder
import com.google.firebase.analytics.ktx.analytics import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.crashlytics.FirebaseCrashlytics import com.google.firebase.crashlytics.FirebaseCrashlytics
@@ -38,6 +38,10 @@ class App : Application() {
lateinit var app: App lateinit var app: App
} }
public override fun attachBaseContext(base: Context?) {
super.attachBaseContext(base)
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
app = this app = this
@@ -47,6 +51,7 @@ class App : Application() {
deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName) deviceStorage.moveSharedPreferencesFrom(this, PreferenceManager(this).sharedPreferencesName)
deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME) deviceStorage.moveDatabaseFrom(this, AppDatabase.DB_NAME)
} else deviceStorage = this } else deviceStorage = this
Services.init(this)
Firebase.initialize(deviceStorage) Firebase.initialize(deviceStorage)
when (val codename = Build.VERSION.CODENAME) { when (val codename = Build.VERSION.CODENAME) {
"REL" -> { } "REL" -> { }
@@ -62,7 +67,9 @@ class App : Application() {
FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message") FirebaseCrashlytics.getInstance().log("${"XXVDIWEF".getOrElse(priority) { 'X' }}/$tag: $message")
} else { } else {
if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message) if (priority >= Log.WARN || priority == Log.DEBUG) Log.println(priority, tag, message)
if (priority >= Log.INFO) FirebaseCrashlytics.getInstance().recordException(t) if (priority >= Log.INFO && t !is NoShellException) {
FirebaseCrashlytics.getInstance().recordException(t)
}
} }
} }
}) })
@@ -89,7 +96,7 @@ class App : Application() {
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
if (level >= TRIM_MEMORY_RUNNING_CRITICAL) GlobalScope.launch { RootSession.trimMemory() } if (level >= TRIM_MEMORY_RUNNING_CRITICAL) GlobalScope.launch { RootManager.closeExisting() }
} }
/** /**
@@ -110,10 +117,7 @@ class App : Application() {
}) })
} }
val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) } val pref by lazy { PreferenceManager.getDefaultSharedPreferences(deviceStorage) }
val connectivity by lazy { getSystemService<ConnectivityManager>()!! }
val clipboard by lazy { getSystemService<ClipboardManager>()!! } val clipboard by lazy { getSystemService<ClipboardManager>()!! }
val uiMode by lazy { getSystemService<UiModeManager>()!! }
val wifi by lazy { getSystemService<WifiManager>()!! }
val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") } val hasTouch by lazy { packageManager.hasSystemFeature("android.hardware.faketouch") }
val customTabsIntent by lazy { val customTabsIntent by lazy {

View File

@@ -7,6 +7,7 @@ import android.content.Intent
import android.content.pm.PackageManager import android.content.pm.PackageManager
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.Services
class BootReceiver : BroadcastReceiver() { class BootReceiver : BroadcastReceiver() {
companion object { companion object {
@@ -27,7 +28,7 @@ class BootReceiver : BroadcastReceiver() {
Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true Intent.ACTION_BOOT_COMPLETED, Intent.ACTION_LOCKED_BOOT_COMPLETED -> started = true
else -> return else -> return
} }
if (RepeaterService.supported) { if (Services.p2p != null) {
ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java)) ContextCompat.startForegroundService(context, Intent(context, RepeaterService::class.java))
} }
} }

View File

@@ -3,6 +3,7 @@ package be.mygod.vpnhotspot
import android.app.Service import android.app.Service
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import java.net.Inet4Address
abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback { abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Callback {
private var neighbours: Collection<IpNeighbour> = emptyList() private var neighbours: Collection<IpNeighbour> = emptyList()
@@ -17,7 +18,7 @@ abstract class IpNeighbourMonitoringService : Service(), IpNeighbourMonitor.Call
protected fun updateNotification() { protected fun updateNotification() {
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) -> val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
neighbours neighbours
.filter { it.state != IpNeighbour.State.FAILED } .filter { it.ip is Inet4Address && it.state != IpNeighbour.State.FAILED }
.distinctBy { it.lladdr } .distinctBy { it.lladdr }
.size .size
} }

View File

@@ -5,7 +5,6 @@ import android.content.IntentFilter
import android.net.wifi.WifiManager import android.net.wifi.WifiManager
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
@@ -13,11 +12,13 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.StickyEvent1 import be.mygod.vpnhotspot.util.StickyEvent1
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
import timber.log.Timber import timber.log.Timber
import java.net.Inet4Address
@RequiresApi(26) @RequiresApi(26)
class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope { class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
@@ -80,7 +81,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
binder.iface = "" binder.iface = ""
updateNotification() // show invisible foreground notification to avoid being killed updateNotification() // show invisible foreground notification to avoid being killed
try { try {
app.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() { Services.wifi.startLocalOnlyHotspot(object : WifiManager.LocalOnlyHotspotCallback() {
override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) { override fun onStarted(reservation: WifiManager.LocalOnlyHotspotReservation?) {
if (reservation == null) onFailed(-2) else { if (reservation == null) onFailed(-2) else {
this@LocalOnlyHotspotService.reservation = reservation this@LocalOnlyHotspotService.reservation = reservation
@@ -128,7 +129,7 @@ class LocalOnlyHotspotService : IpNeighbourMonitoringService(), CoroutineScope {
override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) { override fun onIpNeighbourAvailable(neighbours: Collection<IpNeighbour>) {
super.onIpNeighbourAvailable(neighbours) super.onIpNeighbourAvailable(neighbours)
if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none { if (Build.VERSION.SDK_INT >= 28) timeoutMonitor?.onClientsChanged(neighbours.none {
it.state != IpNeighbour.State.FAILED it.ip is Inet4Address && it.state != IpNeighbour.State.FAILED
}) })
} }

View File

@@ -14,6 +14,7 @@ import be.mygod.vpnhotspot.manage.TetheringFragment
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.util.ServiceForegroundConnector
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import com.google.android.material.bottomnavigation.BottomNavigationView import com.google.android.material.bottomnavigation.BottomNavigationView
import java.net.Inet4Address import java.net.Inet4Address
@@ -28,7 +29,7 @@ class MainActivity : AppCompatActivity(), BottomNavigationView.OnNavigationItemS
binding.navigation.setOnNavigationItemSelectedListener(this) binding.navigation.setOnNavigationItemSelectedListener(this)
if (savedInstanceState == null) displayFragment(TetheringFragment()) if (savedInstanceState == null) displayFragment(TetheringFragment())
val model by viewModels<ClientViewModel>() val model by viewModels<ClientViewModel>()
if (RepeaterService.supported) ServiceForegroundConnector(this, model, RepeaterService::class) if (Services.p2p != null) ServiceForegroundConnector(this, model, RepeaterService::class)
model.clients.observe(this) { clients -> model.clients.observe(this) { clients ->
val count = clients.count { val count = clients.count {
it.ip.any { (ip, state) -> ip is Inet4Address && state != IpNeighbour.State.FAILED } it.ip.any { (ip, state) -> ip is Inet4Address && state != IpNeighbour.State.FAILED }

View File

@@ -14,7 +14,6 @@ import android.provider.Settings
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.edit import androidx.core.content.edit
import androidx.core.content.getSystemService
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.MacAddressCompat import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor import be.mygod.vpnhotspot.net.monitor.TetherTimeoutMonitor
@@ -25,6 +24,8 @@ import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.deletePersistentGroup
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.requestPersistentGroupInfo
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.startWps
import be.mygod.vpnhotspot.root.RepeaterCommands
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.* import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.* import kotlinx.coroutines.*
@@ -51,18 +52,6 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
*/ */
private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot" private const val PLACEHOLDER_NETWORK_NAME = "DIRECT-00-VPNHotspot"
/**
* This is only a "ServiceConnection" to system service and its impact on system is minimal.
*/
private val p2pManager: WifiP2pManager? by lazy {
try {
app.getSystemService<WifiP2pManager>()
} catch (e: RuntimeException) {
Timber.w(e)
null
}
}
val supported get() = p2pManager != null
var persistentSupported = false var persistentSupported = false
@delegate:TargetApi(29) @delegate:TargetApi(29)
@@ -145,7 +134,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
} }
private val p2pManager get() = RepeaterService.p2pManager!! private val p2pManager get() = Services.p2p!!
private var channel: WifiP2pManager.Channel? = null private var channel: WifiP2pManager.Channel? = null
private val binder = Binder() private val binder = Binder()
@RequiresApi(28) @RequiresApi(28)
@@ -207,14 +196,23 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
override fun onBind(intent: Intent) = binder override fun onBind(intent: Intent) = binder
private fun setOperatingChannel(oc: Int = operatingChannel) = try { private fun setOperatingChannel(forceReinit: Boolean = false, oc: Int = operatingChannel) = try {
val channel = channel val channel = channel
if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show() if (channel == null) SmartSnackbar.make(R.string.repeater_failure_disconnected).show()
// we don't care about listening channel // we don't care about listening channel
else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener { else p2pManager.setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
override fun onSuccess() { } override fun onSuccess() { }
override fun onFailure(reason: Int) { override fun onFailure(reason: Int) {
SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show() if (reason == WifiP2pManager.ERROR && Build.VERSION.SDK_INT >= 30) launch(start = CoroutineStart.UNDISPATCHED) {
val rootReason = try {
RootManager.use { it.execute(RepeaterCommands.SetChannel(oc, forceReinit)) }
} catch (e: Exception) {
Timber.w(e)
SmartSnackbar.make(e).show()
null
} ?: return@launch
SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, rootReason.value)).show()
} else SmartSnackbar.make(formatReason(R.string.repeater_set_oc_failure, reason)).show()
} }
}) })
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
@@ -229,7 +227,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
channel = null channel = null
if (status != Status.DESTROYED) try { if (status != Status.DESTROYED) try {
channel = p2pManager.initialize(this, Looper.getMainLooper(), this) channel = p2pManager.initialize(this, Looper.getMainLooper(), this)
if (!safeMode) setOperatingChannel() if (!safeMode) setOperatingChannel(true)
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
Timber.w(e) Timber.w(e)
launch(Dispatchers.Main) { launch(Dispatchers.Main) {
@@ -240,18 +238,19 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
} }
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
if (!safeMode && key == KEY_OPERATING_CHANNEL) setOperatingChannel() if (!safeMode) when (key) {
KEY_OPERATING_CHANNEL -> setOperatingChannel()
KEY_SAFE_MODE -> setOperatingChannel(true)
}
} }
@SuppressLint("NewApi") // networkId is available since Android 4.2 @SuppressLint("NewApi") // networkId is available since Android 4.2
private fun onPersistentGroupsChanged() = launch { private fun onPersistentGroupsChanged() = launch {
val ownerAddress = lastMac?.let(MacAddressCompat.Companion::fromString) ?: withContext(Dispatchers.Default) { val ownerAddress = lastMac?.let(MacAddressCompat.Companion::fromString) ?: try {
try { P2pSupplicantConfiguration().apply { init() }.bssid
P2pSupplicantConfiguration().bssid
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
Timber.d(e) Timber.d(e)
null null
}
} ?: return@launch } ?: return@launch
val channel = channel ?: return@launch val channel = channel ?: return@launch
try { try {
@@ -287,12 +286,8 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
if (status != Status.IDLE) return START_NOT_STICKY if (status != Status.IDLE) return START_NOT_STICKY
val channel = channel ?: return START_NOT_STICKY.also { stopSelf() } val channel = channel ?: return START_NOT_STICKY.also { stopSelf() }
status = Status.STARTING status = Status.STARTING
// bump self to foreground location service to use foreground location permission later // bump self to foreground location service (API 29+) to use location later, also to avoid getting killed
if (Build.VERSION.SDK_INT >= 29 || if (Build.VERSION.SDK_INT >= 26) showNotification()
// or show invisible foreground notification on television to avoid being killed
Build.VERSION.SDK_INT >= 26 && app.uiMode.currentModeType == Configuration.UI_MODE_TYPE_TELEVISION) {
showNotification()
}
launch { launch {
registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION, registerReceiver(receiver, intentFilter(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION,
WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION)) WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION))
@@ -420,7 +415,7 @@ class RepeaterService : Service(), CoroutineScope, WifiP2pManager.ChannelListene
SmartSnackbar.make(msg).apply { SmartSnackbar.make(msg).apply {
if (showWifiEnable) action(R.string.repeater_p2p_unavailable_enable) { if (showWifiEnable) action(R.string.repeater_p2p_unavailable_enable) {
if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") { if (Build.VERSION.SDK_INT < 29) @Suppress("DEPRECATION") {
app.wifi.isWifiEnabled = true Services.wifi.isWifiEnabled = true
} else it.context.startActivity(Intent(Settings.Panel.ACTION_WIFI)) } else it.context.startActivity(Intent(Settings.Panel.ACTION_WIFI))
} }
}.show() }.show()

View File

@@ -7,6 +7,7 @@ import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import java.net.NetworkInterface import java.net.NetworkInterface
@@ -34,7 +35,7 @@ abstract class RoutingManager(private val caller: Any, val downstream: String, p
if (!reinit && active.isEmpty()) return@synchronized if (!reinit && active.isEmpty()) return@synchronized
for (manager in active.values) manager.routing?.stop() for (manager in active.values) manager.routing?.stop()
try { try {
Routing.clean() runBlocking { Routing.clean() }
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
Timber.d(e) Timber.d(e)
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()

View File

@@ -1,6 +1,5 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
@@ -9,7 +8,6 @@ import androidx.preference.Preference
import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceFragmentCompat
import androidx.preference.SwitchPreference import androidx.preference.SwitchPreference
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.net.TetherOffloadManager import be.mygod.vpnhotspot.net.TetherOffloadManager
import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor import be.mygod.vpnhotspot.net.monitor.FallbackUpstreamMonitor
import be.mygod.vpnhotspot.net.monitor.IpMonitor import be.mygod.vpnhotspot.net.monitor.IpMonitor
@@ -18,7 +16,9 @@ import be.mygod.vpnhotspot.net.wifi.WifiDoubleLock
import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment import be.mygod.vpnhotspot.preference.AlwaysAutoCompleteEditTextPreferenceDialogFragment
import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore import be.mygod.vpnhotspot.preference.SharedPreferenceDataStore
import be.mygod.vpnhotspot.preference.SummaryFallbackProvider import be.mygod.vpnhotspot.preference.SummaryFallbackProvider
import be.mygod.vpnhotspot.util.RootSession import be.mygod.vpnhotspot.root.Dump
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.launchUrl import be.mygod.vpnhotspot.util.launchUrl
import be.mygod.vpnhotspot.util.showAllowingStateLoss import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
@@ -27,9 +27,9 @@ import com.google.android.material.snackbar.Snackbar
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileOutputStream
import java.io.IOException import java.io.IOException
import java.io.PrintWriter import java.io.PrintWriter
import kotlin.system.exitProcess import kotlin.system.exitProcess
@@ -48,34 +48,30 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
if (Build.VERSION.SDK_INT >= 27) { if (Build.VERSION.SDK_INT >= 27) {
isChecked = TetherOffloadManager.enabled isChecked = TetherOffloadManager.enabled
setOnPreferenceChangeListener { _, newValue -> setOnPreferenceChangeListener { _, newValue ->
if (TetherOffloadManager.enabled != newValue) { if (TetherOffloadManager.enabled != newValue) GlobalScope.launch(Dispatchers.Main.immediate) {
isEnabled = false isEnabled = false
GlobalScope.launch {
try { try {
TetherOffloadManager.enabled = newValue as Boolean TetherOffloadManager.setEnabled(newValue as Boolean)
} catch (e: Exception) { } catch (e: Exception) {
Timber.d(e) Timber.w(e)
SmartSnackbar.make(e).show() SmartSnackbar.make(e).show()
} }
withContext(Dispatchers.Main) {
isChecked = TetherOffloadManager.enabled isChecked = TetherOffloadManager.enabled
isEnabled = true isEnabled = true
} }
}
}
false false
} }
} else parent!!.removePreference(this) } else parent!!.removePreference(this)
} }
val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!! val boot = findPreference<SwitchPreference>("service.repeater.startOnBoot")!!
if (RepeaterService.supported) { if (Services.p2p != null) {
boot.setOnPreferenceChangeListener { _, value -> boot.setOnPreferenceChangeListener { _, value ->
BootReceiver.enabled = value as Boolean BootReceiver.enabled = value as Boolean
true true
} }
boot.isChecked = BootReceiver.enabled boot.isChecked = BootReceiver.enabled
} else boot.parent!!.removePreference(boot) } else boot.parent!!.removePreference(boot)
if (!RepeaterService.supported || !RepeaterService.safeModeConfigurable) { if (Services.p2p == null || !RepeaterService.safeModeConfigurable) {
val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!! val safeMode = findPreference<Preference>(RepeaterService.KEY_SAFE_MODE)!!
safeMode.parent!!.removePreference(safeMode) safeMode.parent!!.removePreference(safeMode)
} }
@@ -88,7 +84,6 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
setAction(R.string.settings_exit_app) { setAction(R.string.settings_exit_app) {
GlobalScope.launch { GlobalScope.launch {
RoutingManager.clean(false) RoutingManager.clean(false)
RootSession.trimMemory()
exitProcess(0) exitProcess(0)
} }
} }
@@ -109,56 +104,18 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
Runtime.getRuntime().exec(arrayOf("logcat", "-d")).inputStream.use { it.copyTo(out) } Runtime.getRuntime().exec(arrayOf("logcat", "-d")).inputStream.use { it.copyTo(out) }
} catch (e: IOException) { } catch (e: IOException) {
Timber.w(e) Timber.w(e)
e.printStackTrace(writer)
} }
writer.println() writer.println()
val commands = StringBuilder()
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
val iptablesSave = if (Build.VERSION.SDK_INT >= 24) "iptables-save" else
File(app.deviceStorage.cacheDir, "iptables-save").absolutePath.also {
commands.appendln("ln -sf /system/bin/iptables $it")
} }
val ip6tablesSave = if (Build.VERSION.SDK_INT >= 24) "ip6tables-save" else
File(app.deviceStorage.cacheDir, "ip6tables-save").absolutePath.also {
commands.appendln("ln -sf /system/bin/ip6tables $it")
} }
commands.append("""
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|dumpsys ${Context.WIFI_P2P_SERVICE}
|echo
|echo dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|echo
|echo iptables -t filter
|$iptablesSave -t filter
|echo
|echo iptables -t nat
|$iptablesSave -t nat
|echo
|echo ip6tables-save
|$ip6tablesSave
|echo
|echo ip rule
|ip rule
|echo
|echo ip neigh
|ip neigh
|echo
|echo iptables -nvx -L vpnhotspot_fwd
|$IPTABLES -nvx -L vpnhotspot_fwd
|echo
|echo iptables -nvx -L vpnhotspot_acl
|$IPTABLES -nvx -L vpnhotspot_acl
|echo
|echo logcat-su
|logcat -d
""".trimMargin())
try { try {
RootSession.use { it.execQuiet(commands.toString(), true).out.forEach(writer::println) } RootManager.use {
it.execute(Dump(logFile.absolutePath))
}
} catch (e: Exception) { } catch (e: Exception) {
e.printStackTrace(writer) Timber.w(e)
Timber.i(e) PrintWriter(FileOutputStream(logFile, true)).use { e.printStackTrace(it) }
}
}
} }
context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND) context.startActivity(Intent.createChooser(Intent(Intent.ACTION_SEND)
.setType("text/x-log") .setType("text/x-log")
@@ -187,8 +144,8 @@ class SettingsPreferenceFragment : PreferenceFragmentCompat() {
when (preference.key) { when (preference.key) {
UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY -> UpstreamMonitor.KEY, FallbackUpstreamMonitor.KEY ->
AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply { AlwaysAutoCompleteEditTextPreferenceDialogFragment().apply {
setArguments(preference.key, app.connectivity.allNetworks.mapNotNull { setArguments(preference.key, Services.connectivity.allNetworks.mapNotNull {
app.connectivity.getLinkProperties(it)?.interfaceName Services.connectivity.getLinkProperties(it)?.interfaceName
}.toTypedArray()) }.toTypedArray())
setTargetFragment(this@SettingsPreferenceFragment, 0) setTargetFragment(this@SettingsPreferenceFragment, 0)
}.showAllowingStateLoss(parentFragmentManager, preference.key) }.showAllowingStateLoss(parentFragmentManager, preference.key)

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot package be.mygod.vpnhotspot
import android.content.Intent import android.content.Intent
import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing import be.mygod.vpnhotspot.net.Routing
@@ -93,6 +94,8 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
override fun onBind(intent: Intent?) = binder override fun onBind(intent: Intent?) = binder
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
// call this first just in case we are shutting down immediately
if (Build.VERSION.SDK_INT >= 26) updateNotification()
launch { launch {
if (intent != null) { if (intent != null) {
for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) { for (iface in intent.getStringArrayExtra(EXTRA_ADD_INTERFACES) ?: emptyArray()) {
@@ -109,7 +112,6 @@ class TetheringService : IpNeighbourMonitoringService(), TetheringManager.Tether
} else downstream.monitor = true } else downstream.monitor = true
} }
intent.getStringExtra(EXTRA_REMOVE_INTERFACE)?.also { downstreams.remove(it)?.stop() } intent.getStringExtra(EXTRA_REMOVE_INTERFACE)?.also { downstreams.remove(it)?.stop() }
updateNotification() // call this first just in case we are shutting down immediately
onDownstreamsChangedLocked() onDownstreamsChangedLocked()
} else if (downstreams.isEmpty()) stopSelf(startId) } else if (downstreams.isEmpty()) stopSelf(startId)
} }

View File

@@ -7,7 +7,6 @@ import android.os.Parcelable
import android.text.format.DateUtils import android.text.format.DateUtils
import android.text.format.Formatter import android.text.format.Formatter
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.util.LongSparseArray
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View import android.view.View
@@ -15,6 +14,7 @@ import android.view.ViewGroup
import android.widget.EditText import android.widget.EditText
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.PopupMenu
import androidx.collection.LongSparseArray
import androidx.databinding.BaseObservable import androidx.databinding.BaseObservable
import androidx.fragment.app.Fragment import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
@@ -145,7 +145,7 @@ class ClientsFragment : Fragment() {
AppDatabase.instance.clientRecordDao.update(this@apply) AppDatabase.instance.clientRecordDao.update(this@apply)
} }
} }
IpNeighbourMonitor.instance?.flush() IpNeighbourMonitor.instance?.flushAsync()
if (!wasWorking && item.itemId == R.id.block) { if (!wasWorking && item.itemId == R.id.block) {
SmartSnackbar.make(R.string.clients_popup_block_service_inactive).show() SmartSnackbar.make(R.string.clients_popup_block_service_inactive).show()
} }
@@ -223,9 +223,7 @@ class ClientsFragment : Fragment() {
binding.clients.itemAnimator = DefaultItemAnimator() binding.clients.itemAnimator = DefaultItemAnimator()
binding.clients.adapter = adapter binding.clients.adapter = adapter
binding.swipeRefresher.setColorSchemeResources(R.color.colorSecondary) binding.swipeRefresher.setColorSchemeResources(R.color.colorSecondary)
binding.swipeRefresher.setOnRefreshListener { binding.swipeRefresher.setOnRefreshListener { IpNeighbourMonitor.instance?.flushAsync() }
IpNeighbourMonitor.instance?.flush()
}
activityViewModels<ClientViewModel>().value.clients.observe(viewLifecycleOwner) { activityViewModels<ClientViewModel>().value.clients.observe(viewLifecycleOwner) {
adapter.submitList(it.toMutableList()) adapter.submitList(it.toMutableList())
} }

View File

@@ -8,15 +8,11 @@ import android.content.Context
import android.content.Intent import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.os.Build import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.TetheringManager import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
class BluetoothTethering(context: Context, val stateListener: () -> Unit) : class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
@@ -26,9 +22,16 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
* PAN Profile * PAN Profile
*/ */
private const val PAN = 5 private const val PAN = 5
private val isTetheringOn by lazy { private val clazz by lazy { Class.forName("android.bluetooth.BluetoothPan") }
Class.forName("android.bluetooth.BluetoothPan").getDeclaredMethod("isTetheringOn") private val constructor by lazy {
clazz.getDeclaredConstructor(Context::class.java, BluetoothProfile.ServiceListener::class.java)
} }
private val isTetheringOn by lazy { clazz.getDeclaredMethod("isTetheringOn") }
fun pan(context: Context, serviceListener: BluetoothProfile.ServiceListener) =
constructor.newInstance(context, serviceListener) as BluetoothProfile
val BluetoothProfile.isTetheringOn get() = isTetheringOn(this) as Boolean
fun BluetoothProfile.closePan() = BluetoothAdapter.getDefaultAdapter()!!.closeProfileProxy(PAN, this)
private fun registerBluetoothStateListener(receiver: BroadcastReceiver) = private fun registerBluetoothStateListener(receiver: BroadcastReceiver) =
app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)) app.registerReceiver(receiver, IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED))
@@ -41,23 +44,8 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
@TargetApi(24) @TargetApi(24)
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) { when (intent?.getIntExtra(BluetoothAdapter.EXTRA_STATE, BluetoothAdapter.ERROR)) {
BluetoothAdapter.STATE_ON -> try { BluetoothAdapter.STATE_ON -> {
TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, pendingCallback!!) TetheringManager.startTethering(TetheringManager.TETHERING_BLUETOOTH, true, pendingCallback!!)
} catch (e: IOException) {
Timber.w(e)
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
pendingCallback!!.onException()
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
var cause: Throwable? = e
while (cause != null) {
cause = cause.cause
if (cause != null && cause !is InvocationTargetException) {
Toast.makeText(context, cause.readableMessage, Toast.LENGTH_LONG).show()
pendingCallback!!.onException()
break
}
}
} }
BluetoothAdapter.STATE_OFF, BluetoothAdapter.ERROR -> { } BluetoothAdapter.STATE_OFF, BluetoothAdapter.ERROR -> { }
else -> return // ignore transition states else -> return // ignore transition states
@@ -81,18 +69,18 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
} }
} }
private var connected = false
private var pan: BluetoothProfile? = null private var pan: BluetoothProfile? = null
var activeFailureCause: Throwable? = null var activeFailureCause: Throwable? = null
/** /**
* Requires BLUETOOTH_PRIVILEGED on API 30+.
*
* Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java * Based on: https://android.googlesource.com/platform/packages/apps/Settings/+/78d5efd/src/com/android/settings/TetherSettings.java
*/ */
val active: Boolean? get() { val active: Boolean? get() {
activeFailureCause = null
val pan = pan ?: return null val pan = pan ?: return null
if (!connected) return null
activeFailureCause = null
return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try { return BluetoothAdapter.getDefaultAdapter()?.state == BluetoothAdapter.STATE_ON && try {
isTetheringOn(pan) as Boolean pan.isTetheringOn
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
activeFailureCause = e.cause ?: e activeFailureCause = e.cause ?: e
if (e.cause is SecurityException && Build.VERSION.SDK_INT >= 30) Timber.d(e) else Timber.w(e) if (e.cause is SecurityException && Build.VERSION.SDK_INT >= 30) Timber.d(e) else Timber.w(e)
@@ -104,24 +92,23 @@ class BluetoothTethering(context: Context, val stateListener: () -> Unit) :
init { init {
try { try {
BluetoothAdapter.getDefaultAdapter()?.getProfileProxy(context, this, PAN) pan = pan(context, this)
} catch (e: SecurityException) { } catch (e: InvocationTargetException) {
Timber.w(e) Timber.w(e)
SmartSnackbar.make(e).show() activeFailureCause = e
} }
registerBluetoothStateListener(receiver) registerBluetoothStateListener(receiver)
} }
override fun onServiceDisconnected(profile: Int) { override fun onServiceDisconnected(profile: Int) {
pan = null connected = false
} }
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) { override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
pan = proxy connected = true
stateListener() stateListener()
} }
override fun close() { override fun close() {
app.unregisterReceiver(receiver) app.unregisterReceiver(receiver)
BluetoothAdapter.getDefaultAdapter()?.closeProfileProxy(PAN, pan) pan?.closePan()
pan = null
} }
} }

View File

@@ -6,6 +6,7 @@ import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.net.IpNeighbour import be.mygod.vpnhotspot.net.IpNeighbour
import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
import be.mygod.vpnhotspot.util.KillableTileService import be.mygod.vpnhotspot.util.KillableTileService
import java.net.Inet4Address
@RequiresApi(24) @RequiresApi(24)
abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback { abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeighbourMonitor.Callback {
@@ -24,7 +25,7 @@ abstract class IpNeighbourMonitoringTileService : KillableTileService(), IpNeigh
protected fun Tile.subtitleDevices(filter: (String) -> Boolean) { protected fun Tile.subtitleDevices(filter: (String) -> Boolean) {
val size = neighbours val size = neighbours
.filter { it.state != IpNeighbour.State.FAILED && filter(it.dev) } .filter { it.ip is Inet4Address && it.state != IpNeighbour.State.FAILED && filter(it.dev) }
.distinctBy { it.lladdr } .distinctBy { it.lladdr }
.size .size
if (size > 0) subtitle(resources.getQuantityString( if (size > 0) subtitle(resources.getQuantityString(

View File

@@ -210,9 +210,8 @@ class RepeaterManager(private val parent: TetheringFragment) : Manager(), Servic
band = SoftApConfigurationCompat.BAND_ANY band = SoftApConfigurationCompat.BAND_ANY
channel = RepeaterService.operatingChannel channel = RepeaterService.operatingChannel
try { try {
val config = withContext(Dispatchers.Default) { val config = P2pSupplicantConfiguration(group)
P2pSupplicantConfiguration(group, RepeaterService.lastMac) config.init(RepeaterService.lastMac)
}
holder.config = config holder.config = config
passphrase = config.psk passphrase = config.psk
bssid = config.bssid bssid = config.bssid

View File

@@ -12,6 +12,7 @@ import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.RepeaterService import be.mygod.vpnhotspot.RepeaterService
import be.mygod.vpnhotspot.util.KillableTileService import be.mygod.vpnhotspot.util.KillableTileService
import be.mygod.vpnhotspot.util.Services
import be.mygod.vpnhotspot.util.stopAndUnbind import be.mygod.vpnhotspot.util.stopAndUnbind
@RequiresApi(24) @RequiresApi(24)
@@ -22,13 +23,13 @@ class RepeaterTileService : KillableTileService() {
override fun onStartListening() { override fun onStartListening() {
super.onStartListening() super.onStartListening()
if (RepeaterService.supported) { if (Services.p2p != null) {
bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE) bindService(Intent(this, RepeaterService::class.java), this, Context.BIND_AUTO_CREATE)
} else updateTile() } else updateTile()
} }
override fun onStopListening() { override fun onStopListening() {
if (RepeaterService.supported) stopAndUnbind(this) if (Services.p2p != null) stopAndUnbind(this)
super.onStopListening() super.onStopListening()
} }

View File

@@ -20,8 +20,10 @@ import be.mygod.vpnhotspot.net.TetheringManager
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
sealed class TetherManager(protected val parent: TetheringFragment) : Manager(), sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
@@ -50,25 +52,12 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
app.logEvent("manage_write_settings") { param("message", e.toString()) } app.logEvent("manage_write_settings") { param("message", e.toString()) }
} }
val started = manager.isStarted if (manager.isStarted) try {
try { manager.stop()
if (started) manager.stop() else manager.start()
} catch (e: IOException) {
Timber.w(e)
Toast.makeText(mainActivity, e.readableMessage, Toast.LENGTH_LONG).show()
ManageBar.start(itemView.context)
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e) if (e.targetException !is SecurityException) Timber.w(e)
var cause: Throwable? = e manager.onException(e)
while (cause != null) { } else manager.start()
cause = cause.cause
if (cause != null && cause !is InvocationTargetException) {
Toast.makeText(mainActivity, cause.readableMessage, Toast.LENGTH_LONG).show()
ManageBar.start(itemView.context)
break
}
}
}
} }
} }
@@ -96,6 +85,14 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
error?.let { SmartSnackbar.make("$tetherType: ${TetheringManager.tetherErrorMessage(it)}") } error?.let { SmartSnackbar.make("$tetherType: ${TetheringManager.tetherErrorMessage(it)}") }
data.notifyChange() data.notifyChange()
} }
override fun onException(e: Exception) {
if (e !is InvocationTargetException || e.targetException !is SecurityException) Timber.w(e)
GlobalScope.launch(Dispatchers.Main.immediate) {
val context = parent.context ?: app
Toast.makeText(context, e.readableMessage, Toast.LENGTH_LONG).show()
ManageBar.start(context)
}
}
override fun bindTo(viewHolder: RecyclerView.ViewHolder) { override fun bindTo(viewHolder: RecyclerView.ViewHolder) {
(viewHolder as ViewHolder).manager = this (viewHolder as ViewHolder).manager = this
@@ -124,7 +121,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val type get() = VIEW_TYPE_WIFI override val type get() = VIEW_TYPE_WIFI
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
} }
@RequiresApi(24) @RequiresApi(24)
class Usb(parent: TetheringFragment) : TetherManager(parent) { class Usb(parent: TetheringFragment) : TetherManager(parent) {
@@ -133,7 +130,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val type get() = VIEW_TYPE_USB override val type get() = VIEW_TYPE_USB
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
} }
@RequiresApi(24) @RequiresApi(24)
class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver { class Bluetooth(parent: TetheringFragment) : TetherManager(parent), DefaultLifecycleObserver {
@@ -153,8 +150,6 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val type get() = VIEW_TYPE_BLUETOOTH override val type get() = VIEW_TYPE_BLUETOOTH
override val isStarted get() = tethering.active == true override val isStarted get() = tethering.active == true
override fun onException() = ManageBar.start(parent.context ?: app)
private var baseError: CharSequence? = null private var baseError: CharSequence? = null
private fun makeErrorMessage(): CharSequence = listOfNotNull( private fun makeErrorMessage(): CharSequence = listOfNotNull(
if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null, if (tethering.active == null) tethering.activeFailureCause?.readableMessage else null,
@@ -166,7 +161,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override fun start() = BluetoothTethering.start(this) override fun start() = BluetoothTethering.start(this)
override fun stop() { override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH) TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
Thread.sleep(1) // give others a room to breathe Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state onTetheringStarted() // force flush state
} }
@@ -178,7 +173,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val type get() = VIEW_TYPE_ETHERNET override val type get() = VIEW_TYPE_ETHERNET
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
} }
@RequiresApi(30) @RequiresApi(30)
class Ncm(parent: TetheringFragment) : TetherManager(parent) { class Ncm(parent: TetheringFragment) : TetherManager(parent) {
@@ -187,7 +182,7 @@ sealed class TetherManager(protected val parent: TetheringFragment) : Manager(),
override val type get() = VIEW_TYPE_NCM override val type get() = VIEW_TYPE_NCM
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View File

@@ -27,10 +27,9 @@ import be.mygod.vpnhotspot.net.TetheringManager.localOnlyTetheredIfaces
import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces import be.mygod.vpnhotspot.net.TetheringManager.tetheredIfaces
import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment import be.mygod.vpnhotspot.net.wifi.WifiApDialogFragment
import be.mygod.vpnhotspot.net.wifi.WifiApManager import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.ServiceForegroundConnector import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.root.WifiApCommands
import be.mygod.vpnhotspot.util.isNotGone import be.mygod.vpnhotspot.util.*
import be.mygod.vpnhotspot.util.showAllowingStateLoss
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.CompletableDeferred import kotlinx.coroutines.CompletableDeferred
import timber.log.Timber import timber.log.Timber
@@ -89,7 +88,7 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
updateEnabledTypes() updateEnabledTypes()
val list = ArrayList<Manager>() val list = ArrayList<Manager>()
if (RepeaterService.supported) list.add(repeaterManager) if (Services.p2p != null) list.add(repeaterManager)
if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager) if (Build.VERSION.SDK_INT >= 26) list.add(localOnlyHotspotManager)
val monitoredIfaces = binder?.monitoredIfaces ?: emptyList() val monitoredIfaces = binder?.monitoredIfaces ?: emptyList()
updateMonitorList(activeIfaces - monitoredIfaces) updateMonitorList(activeIfaces - monitoredIfaces)
@@ -150,10 +149,12 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
} }
} }
} }
private var apConfigurationRunning = false
override fun onMenuItemClick(item: MenuItem?): Boolean { override fun onMenuItemClick(item: MenuItem?): Boolean {
return when (item?.itemId) { return when (item?.itemId) {
R.id.configuration -> item.subMenu.run { R.id.configuration -> item.subMenu.run {
findItem(R.id.configuration_repeater).isNotGone = RepeaterService.supported findItem(R.id.configuration_repeater).isNotGone = Services.p2p != null
findItem(R.id.configuration_temp_hotspot).isNotGone = findItem(R.id.configuration_temp_hotspot).isNotGone =
adapter.localOnlyHotspotManager.binder?.configuration != null adapter.localOnlyHotspotManager.binder?.configuration != null
true true
@@ -170,16 +171,30 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
}.showAllowingStateLoss(parentFragmentManager) }.showAllowingStateLoss(parentFragmentManager)
true true
} }
R.id.configuration_ap -> try { R.id.configuration_ap -> if (apConfigurationRunning) false else {
WifiApDialogFragment().apply { apConfigurationRunning = true
arg(WifiApDialogFragment.Arg(WifiApManager.configuration)) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
key() try {
}.showAllowingStateLoss(parentFragmentManager) WifiApManager.configuration
true
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e) if (e.targetException !is SecurityException) Timber.w(e)
SmartSnackbar.make(e.targetException).show() try {
false RootManager.use { it.execute(WifiApCommands.GetConfiguration()) }
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
Timber.w(eRoot)
SmartSnackbar.make(eRoot).show()
null
}
}?.let { configuration ->
WifiApDialogFragment().apply {
arg(WifiApDialogFragment.Arg(configuration))
key()
}.showAllowingStateLoss(parentFragmentManager)
}
apConfigurationRunning = false
}
true
} }
else -> false else -> false
} }
@@ -187,13 +202,20 @@ class TetheringFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClick
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret -> AlertDialogFragment.setResultListener<WifiApDialogFragment, WifiApDialogFragment.Arg>(this) { which, ret ->
if (which == DialogInterface.BUTTON_POSITIVE) try { if (which == DialogInterface.BUTTON_POSITIVE) viewLifecycleOwner.lifecycleScope.launchWhenCreated {
WifiApManager.configuration = ret!!.configuration val success = try {
} catch (e: IllegalArgumentException) { WifiApManager.setConfiguration(ret!!.configuration)
Timber.d(e)
SmartSnackbar.make(R.string.configuration_rejected).show()
} catch (e: InvocationTargetException) { } catch (e: InvocationTargetException) {
SmartSnackbar.make(e.targetException).show() try {
RootManager.use { it.execute(WifiApCommands.SetConfiguration(ret!!.configuration)) }
} catch (eRoot: Exception) {
eRoot.addSuppressed(e)
Timber.w(eRoot)
SmartSnackbar.make(eRoot).show()
null
}
}
if (success == false) SmartSnackbar.make(R.string.configuration_rejected).show()
} }
} }
binding = FragmentTetheringBinding.inflate(inflater, container, false) binding = FragmentTetheringBinding.inflate(inflater, container, false)

View File

@@ -11,6 +11,7 @@ import android.service.quicksettings.Tile
import android.widget.Toast import android.widget.Toast
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import be.mygod.vpnhotspot.App
import be.mygod.vpnhotspot.R import be.mygod.vpnhotspot.R
import be.mygod.vpnhotspot.TetheringService import be.mygod.vpnhotspot.TetheringService
import be.mygod.vpnhotspot.net.TetherType import be.mygod.vpnhotspot.net.TetherType
@@ -20,8 +21,10 @@ import be.mygod.vpnhotspot.net.wifi.WifiApManager
import be.mygod.vpnhotspot.util.broadcastReceiver import be.mygod.vpnhotspot.util.broadcastReceiver
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import be.mygod.vpnhotspot.util.stopAndUnbind import be.mygod.vpnhotspot.util.stopAndUnbind
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.IOException
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
@RequiresApi(24) @RequiresApi(24)
@@ -97,30 +100,17 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
} }
} }
protected inline fun safeInvoker(func: () -> Unit) = try {
func()
} catch (e: IOException) {
Timber.w(e)
Toast.makeText(this, e.readableMessage, Toast.LENGTH_LONG).show()
} catch (e: InvocationTargetException) {
if (e.targetException !is SecurityException) Timber.w(e)
var cause: Throwable? = e
while (cause != null) {
cause = cause.cause
if (cause != null && cause !is InvocationTargetException) {
Toast.makeText(this, cause.readableMessage, Toast.LENGTH_LONG).show()
break
}
}
}
override fun onClick() { override fun onClick() {
val interested = interested ?: return val interested = interested ?: return
if (interested.isEmpty()) safeInvoker { start() } else { if (interested.isEmpty()) start() else {
val binder = binder val binder = binder
if (binder == null) tapPending = true else { if (binder == null) tapPending = true else {
val inactive = interested.filterNot(binder::isActive) val inactive = interested.filterNot(binder::isActive)
if (inactive.isEmpty()) safeInvoker { stop() } if (inactive.isEmpty()) try {
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) stop()
} catch (e: Exception) {
onException(e)
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
} }
} }
@@ -132,6 +122,12 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
error?.let { Toast.makeText(this, TetheringManager.tetherErrorMessage(it), Toast.LENGTH_LONG).show() } error?.let { Toast.makeText(this, TetheringManager.tetherErrorMessage(it), Toast.LENGTH_LONG).show() }
updateTile() updateTile()
} }
override fun onException(e: Exception) {
if (e !is InvocationTargetException || e.targetException !is SecurityException) Timber.w(e)
GlobalScope.launch(Dispatchers.Main.immediate) {
Toast.makeText(this@TetheringTileService, e.readableMessage, Toast.LENGTH_LONG).show()
}
}
class Wifi : TetheringTileService() { class Wifi : TetheringTileService() {
override val labelString get() = R.string.tethering_manage_wifi override val labelString get() = R.string.tethering_manage_wifi
@@ -139,14 +135,14 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override val icon get() = R.drawable.ic_device_wifi_tethering override val icon get() = R.drawable.ic_device_wifi_tethering
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_WIFI, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_WIFI, this::onException)
} }
class Usb : TetheringTileService() { class Usb : TetheringTileService() {
override val labelString get() = R.string.tethering_manage_usb override val labelString get() = R.string.tethering_manage_usb
override val tetherType get() = TetherType.USB override val tetherType get() = TetherType.USB
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_USB, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_USB, this::onException)
} }
class Bluetooth : TetheringTileService() { class Bluetooth : TetheringTileService() {
private var tethering: BluetoothTethering? = null private var tethering: BluetoothTethering? = null
@@ -156,7 +152,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override fun start() = BluetoothTethering.start(this) override fun start() = BluetoothTethering.start(this)
override fun stop() { override fun stop() {
TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH) TetheringManager.stopTethering(TetheringManager.TETHERING_BLUETOOTH, this::onException)
Thread.sleep(1) // give others a room to breathe Thread.sleep(1) // give others a room to breathe
onTetheringStarted() // force flush state onTetheringStarted() // force flush state
} }
@@ -202,12 +198,15 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
val binder = binder val binder = binder
if (binder == null) tapPending = true else { if (binder == null) tapPending = true else {
val inactive = (interested ?: return).filterNot(binder::isActive) val inactive = (interested ?: return).filterNot(binder::isActive)
if (inactive.isEmpty()) safeInvoker { stop() } if (inactive.isEmpty()) try {
else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java) stop()
} catch (e: Exception) {
onException(e)
} else ContextCompat.startForegroundService(this, Intent(this, TetheringService::class.java)
.putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray())) .putExtra(TetheringService.EXTRA_ADD_INTERFACES, inactive.toTypedArray()))
} }
} }
false -> safeInvoker { start() } false -> start()
else -> tapPending = true else -> tapPending = true
} }
} }
@@ -218,7 +217,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override val tetherType get() = TetherType.ETHERNET override val tetherType get() = TetherType.ETHERNET
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_ETHERNET, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_ETHERNET, this::onException)
} }
@RequiresApi(30) @RequiresApi(30)
class Ncm : TetheringTileService() { class Ncm : TetheringTileService() {
@@ -226,7 +225,7 @@ sealed class TetheringTileService : IpNeighbourMonitoringTileService(), Tetherin
override val tetherType get() = TetherType.NCM override val tetherType get() = TetherType.NCM
override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this) override fun start() = TetheringManager.startTethering(TetheringManager.TETHERING_NCM, true, this)
override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM) override fun stop() = TetheringManager.stopTethering(TetheringManager.TETHERING_NCM, this::onException)
} }
@Suppress("DEPRECATION") @Suppress("DEPRECATION")

View File

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

View File

@@ -3,7 +3,10 @@ package be.mygod.vpnhotspot.net
import android.os.Build import android.os.Build
import android.system.ErrnoException import android.system.ErrnoException
import android.system.OsConstants import android.system.OsConstants
import be.mygod.vpnhotspot.root.ReadArp
import be.mygod.vpnhotspot.root.RootManager
import be.mygod.vpnhotspot.util.parseNumericAddress import be.mygod.vpnhotspot.util.parseNumericAddress
import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import java.io.File import java.io.File
import java.io.FileNotFoundException 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 private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
fun parse(line: String): List<IpNeighbour> { fun parse(line: String): List<IpNeighbour> {
if (line.isBlank()) return emptyList()
return try { return try {
val match = parser.matchEntire(line)!! val match = parser.matchEntire(line)!!
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty 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 const val ARP_CACHE_EXPIRE = 1L * 1000 * 1000 * 1000
private var arpCache = emptyList<List<String>>() private var arpCache = emptyList<List<String>>()
private var arpCacheTime = -ARP_CACHE_EXPIRE private var arpCacheTime = -ARP_CACHE_EXPIRE
private fun arp(): List<List<String>> { private fun Sequence<String>.makeArp() = this
if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try {
arpCache = File("/proc/net/arp").bufferedReader().readLines()
.asSequence()
.map { it.split(spaces) } .map { it.split(spaces) }
.drop(1) .drop(1)
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() } .filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
.toList() .toList()
private fun arp(): List<List<String>> {
if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try {
arpCache = File("/proc/net/arp").bufferedReader().lineSequence().makeArp()
} catch (e: IOException) { } catch (e: IOException) {
if (e !is FileNotFoundException || Build.VERSION.SDK_INT < 29 || if (e is FileNotFoundException && Build.VERSION.SDK_INT >= 29 &&
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e) (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 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.TrafficRecorder
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
import be.mygod.vpnhotspot.room.AppDatabase 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.util.RootSession
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import timber.log.Timber import timber.log.Timber
import java.io.BufferedWriter
import java.io.IOException import java.io.IOException
import java.net.Inet4Address import java.net.Inet4Address
import java.net.InetAddress 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_FALLBACK = 17900
private const val RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM = 17980 private const val RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM = 17980
/** const val IPTABLES ="iptables -w"
* -w <seconds> is not supported on 7.1-. const val IP6TABLES = "ip6tables -w"
* 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"
fun clean() { fun appendCleanCommands(commands: BufferedWriter) {
TrafficRecorder.clean() commands.appendln("$IPTABLES -t nat -F PREROUTING")
RootSession.use { commands.appendln("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
it.execQuiet("$IPTABLES -t nat -F PREROUTING") commands.appendln("$IPTABLES -F vpnhotspot_fwd")
it.execQuiet("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done") commands.appendln("$IPTABLES -X vpnhotspot_fwd")
it.execQuiet("$IPTABLES -F vpnhotspot_fwd") commands.appendln("$IPTABLES -F vpnhotspot_acl")
it.execQuiet("$IPTABLES -X vpnhotspot_fwd") commands.appendln("$IPTABLES -X vpnhotspot_acl")
it.execQuiet("$IPTABLES -F vpnhotspot_acl") commands.appendln("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
it.execQuiet("$IPTABLES -X vpnhotspot_acl") commands.appendln("$IPTABLES -t nat -F vpnhotspot_masquerade")
it.execQuiet("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done") commands.appendln("$IPTABLES -t nat -X vpnhotspot_masquerade")
it.execQuiet("$IPTABLES -t nat -F vpnhotspot_masquerade") commands.appendln("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
it.execQuiet("$IPTABLES -t nat -X vpnhotspot_masquerade") commands.appendln("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
it.execQuiet("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done") commands.appendln("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
it.execQuiet("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done") commands.appendln("$IP6TABLES -F vpnhotspot_filter")
it.execQuiet("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done") commands.appendln("$IP6TABLES -X vpnhotspot_filter")
it.execQuiet("$IP6TABLES -F vpnhotspot_filter") commands.appendln("while ip rule del priority $RULE_PRIORITY_DNS; do done")
it.execQuiet("$IP6TABLES -X vpnhotspot_filter") commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_DNS; do done") commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done") commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; 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")
} }
suspend fun clean() {
TrafficRecorder.clean()
RootManager.use { it.execute(RoutingCommands.Clean()) }
} }
private fun RootSession.Transaction.iptables(command: String, revert: String) { private fun RootSession.Transaction.iptables(command: String, revert: String) {
val result = execQuiet(command, revert) 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 if (result.err.isNotEmpty()) Timber.i(message) // busy wait message
} }
private fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") = 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) { private fun RootSession.Transaction.ndc(name: String, command: String, revert: String? = null) {
val result = execQuiet(command, revert) val result = execQuiet(command, revert)
val log = RootSession.checkOutput(command, result, val suffix = "200 0 $name operation succeeded\n"
result.out.lastOrNull() != "200 0 $name operation succeeded") result.check(listOf(command), !result.out.endsWith(suffix))
if (result.out.size > 1) Timber.i(log) 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", transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
"ndc ipfwd disable vpnhotspot_$downstream") "ndc ipfwd disable vpnhotspot_$downstream")
return return
} catch (e: RootSession.UnexpectedOutputException) { } catch (e: RoutingCommands.UnexpectedOutputException) {
Timber.w(IOException("ndc ipfwd enable failure", e)) Timber.w(IOException("ndc ipfwd enable failure", e))
} }
transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward") 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 android.provider.Settings
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import be.mygod.vpnhotspot.App.Companion.app 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. * 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) @RequiresApi(27)
object TetherOffloadManager { object TetherOffloadManager {
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled" private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
var enabled: Boolean val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0 suspend fun setEnabled(value: Boolean) {
set(value) { val int = if (value) 0 else 1
RootSession.use { try {
it.exec("settings put global $TETHER_OFFLOAD_DISABLED ${if (value) 0 else 1}") 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 androidx.collection.SparseArrayCompat
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.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.broadcastReceiver
import be.mygod.vpnhotspot.util.callSuper import be.mygod.vpnhotspot.util.callSuper
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
import com.android.dx.stock.ProxyBuilder import com.android.dx.stock.ProxyBuilder
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import timber.log.Timber import timber.log.Timber
import java.io.File
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException import java.lang.reflect.InvocationTargetException
@@ -49,7 +57,10 @@ object TetheringManager {
*/ */
fun onTetheringFailed(error: Int? = null) { } 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. * Ncm local tethering type.
* *
* Requires NETWORK_SETTINGS permission, which is sadly not obtainable.
* @see [startTethering] * @see [startTethering]
*/ */
@RequiresApi(30) @RequiresApi(30)
@@ -149,7 +159,7 @@ object TetheringManager {
@get:RequiresApi(30) @get:RequiresApi(30)
private val instance by lazy @TargetApi(30) { private val instance by lazy @TargetApi(30) {
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4 @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 service
} }
@get:RequiresApi(30) @get:RequiresApi(30)
@@ -211,12 +221,64 @@ object TetheringManager {
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) } 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 * 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 * 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 * enabled immediately. If provisioning fails, tethering will not be enabled. It also
* schedules tether provisioning re-checks if appropriate. * 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 * @param type The type of tethering to start. Must be one of
* {@link ConnectivityManager.TETHERING_WIFI}, * {@link ConnectivityManager.TETHERING_WIFI},
* {@link ConnectivityManager.TETHERING_USB}, or * {@link ConnectivityManager.TETHERING_USB}, or
@@ -234,48 +296,57 @@ object TetheringManager {
*/ */
@RequiresApi(24) @RequiresApi(24)
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback, fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
handler: Handler? = null) { handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
val reference = WeakReference(callback) if (Build.VERSION.SDK_INT >= 30) try {
if (Build.VERSION.SDK_INT >= 30) { val proxy = proxy(callback)
val request = newTetheringRequestBuilder.newInstance(type).let { builder -> val executor = handler.makeExecutor()
// setting exemption requires TETHER_PRIVILEGED permission try {
if (app.checkSelfPermission("android.permission.TETHER_PRIVILEGED") == startTethering(type, true, showProvisioningUi, executor, proxy)
PackageManager.PERMISSION_GRANTED) setExemptFromEntitlementCheck(builder, true) } catch (e1: InvocationTargetException) {
setShouldShowEntitlementUi(builder, showProvisioningUi) if (e1.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
build(builder) 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)
} }
val proxy = Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader, return@launch
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 (result == null) callback.onTetheringStarted()
if (args?.size != 1) Timber.w("Unexpected args for $name: $args") else callback.onTetheringFailed(result.value)
callback?.onTetheringFailed(args?.getOrNull(0) as? Int?) } else callback.onException(e1)
} }
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
} }
} if (result) callback.onTetheringStarted() else callback.onTetheringFailed()
}) } else callback.onException(e)
startTethering(instance, request, handler.makeExecutor(), proxy) } catch (e: Exception) {
} else { callback.onException(e)
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)
} }
} }
@@ -290,7 +361,21 @@ object TetheringManager {
*/ */
@RequiresApi(24) @RequiresApi(24)
fun stopTethering(type: Int) { 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 * @return error The error code of the last error tethering or untethering the named
* interface * 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 // tether errors defined in ConnectivityManager up to Android 10
private val tetherErrors29 = arrayOf("TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE", private val tetherErrors29 = arrayOf("TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE",

View File

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

View File

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

View File

@@ -17,7 +17,7 @@ class IpLinkMonitor private constructor() : IpMonitor() {
monitor = IpLinkMonitor() monitor = IpLinkMonitor()
instance = monitor instance = monitor
} }
monitor.flush() monitor.flushAsync()
} }
fun unregisterCallback(owner: Any) = synchronized(this) { fun unregisterCallback(owner: Any) = synchronized(this) {
if (callbacks.remove(owner) == null || callbacks.isNotEmpty()) return@synchronized 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.ErrnoException
import android.system.OsConstants import android.system.OsConstants
import androidx.core.content.edit import androidx.core.content.edit
import be.mygod.librootkotlinx.RootServer
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.R 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 be.mygod.vpnhotspot.widget.SmartSnackbar
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.ReceiveChannel
import kotlinx.coroutines.channels.consumeEach
import timber.log.Timber import timber.log.Timber
import java.io.IOException import java.io.IOException
import java.io.InterruptedIOException 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.concurrent.thread
import kotlin.coroutines.EmptyCoroutineContext
abstract class IpMonitor : Runnable { abstract class IpMonitor {
companion object { companion object {
const val KEY = "service.ipMonitor" const val KEY = "service.ipMonitor"
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493 // https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
@@ -51,7 +56,7 @@ abstract class IpMonitor : Runnable {
@Volatile @Volatile
private var destroyed = false private var destroyed = false
private var monitor: Process? = null private var monitor: Process? = null
private var pool: ScheduledExecutorService? = null private val worker = Job()
private fun handleProcess(builder: ProcessBuilder) { private fun handleProcess(builder: ProcessBuilder) {
val process = try { val process = try {
@@ -79,8 +84,18 @@ abstract class IpMonitor : Runnable {
if ((e.cause as? ErrnoException)?.errno != OsConstants.EBADF) Timber.w(e) if ((e.cause as? ErrnoException)?.errno != OsConstants.EBADF) Timber.w(e)
} }
err.join() err.join()
process.waitFor() Timber.d("Monitor process exited with ${process.waitFor()}")
Timber.d("Monitor process exited with ${process.exitValue()}") }
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 { init {
@@ -92,17 +107,64 @@ abstract class IpMonitor : Runnable {
handleProcess(ProcessBuilder("ip", "monitor", monitoredObject)) handleProcess(ProcessBuilder("ip", "monitor", monitoredObject))
if (destroyed) return@thread 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 if (destroyed) return@thread
app.logEvent("ip_monitor_failure") app.logEvent("ip_monitor_failure")
} }
val pool = Executors.newScheduledThreadPool(1) GlobalScope.launch(Dispatchers.IO + worker) {
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS) var server: RootServer? = null
this.pool = pool 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() { private fun poll() {
val process = ProcessBuilder("ip", monitoredObject) 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() { fun destroy() {
destroyed = true destroyed = true
monitor?.destroy() monitor?.destroy()
pool?.shutdown() worker.cancel()
} }
} }

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
package be.mygod.vpnhotspot.net.monitor 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.MacAddressCompat
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
import be.mygod.vpnhotspot.room.AppDatabase import be.mygod.vpnhotspot.room.AppDatabase
@@ -63,10 +64,11 @@ object TrafficRecorder {
loop@ for (line in RootSession.use { loop@ for (line in RootSession.use {
val command = "$IPTABLES -nvx -L vpnhotspot_acl" val command = "$IPTABLES -nvx -L vpnhotspot_acl"
val result = it.execQuiet(command) 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) 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() } val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
try { try {
check(columns.size >= 9) check(columns.size >= 9)
@@ -104,7 +106,7 @@ object TrafficRecorder {
} }
if (oldRecord.id != null) { if (oldRecord.id != null) {
check(records.put(key, record) == oldRecord) check(records.put(key, record) == oldRecord)
oldRecords.put(oldRecord.id!!, oldRecord) oldRecords[oldRecord.id!!] = oldRecord
} }
} }
else -> check(false) else -> check(false)

View File

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

View File

@@ -1,24 +1,20 @@
package be.mygod.vpnhotspot.net.wifi package be.mygod.vpnhotspot.net.wifi
import android.net.wifi.p2p.WifiP2pGroup 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.RepeaterService
import be.mygod.vpnhotspot.net.MacAddressCompat 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 com.google.firebase.crashlytics.FirebaseCrashlytics
import java.io.File
/** /**
* This parser is based on: * 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/+/d2986c2/wpa_supplicant/config.c#488
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182 * 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 { companion object {
private const val TAG = "P2pSupplicantConfiguration" 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 const val PERSISTENT_MAC = "p2p_device_persistent_mac_addr="
private val networkParser = private val networkParser =
"^(bssid=(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})|psk=(ext:|\"(.*)\"|[0-9a-fA-F]{64}\$)?)".toRegex() "^(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") override fun toString() = joinToString("\n")
} }
private class Parser(val lines: List<String>) { private class Parser(val lines: Iterator<String>) {
private val iterator = lines.iterator()
lateinit var line: String lateinit var line: String
lateinit var trimmed: String lateinit var trimmed: String
fun next() = if (iterator.hasNext()) { fun next() = if (lines.hasNext()) {
line = iterator.next().apply { trimmed = trimStart('\r', '\t', ' ') } line = lines.next().apply { trimmed = trimStart('\r', '\t', ' ') }
true true
} else false } 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?, private data class Content(val lines: ArrayList<Any>, var target: NetworkBlock, var persistentMacLine: Int?,
var legacy: Boolean) var legacy: Boolean)
private val content = RootSession.use { private lateinit var content: Content
suspend fun init(ownerAddress: String? = null) {
val result = ArrayList<Any>() val result = ArrayList<Any>()
var target: NetworkBlock? = null var target: NetworkBlock? = null
var persistentMacLine: Int? = null var persistentMacLine: Int? = null
val command = "cat $CONF_PATH_TREBLE || cat $CONF_PATH_LEGACY" val (config, legacy) = RootManager.use { it.execute(RepeaterCommands.ReadP2pConfig()) }
val shell = it.execQuiet(command)
RootSession.checkOutput(command, shell, false, false)
val parser = Parser(shell.out)
try { try {
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress) var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
.distinct() .distinct()
@@ -68,6 +61,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
false false
} }
} }
val parser = Parser(config.lineSequence().iterator())
while (parser.next()) { while (parser.next()) {
if (parser.trimmed.startsWith("network={")) { if (parser.trimmed.startsWith("network={")) {
val block = NetworkBlock() val block = NetworkBlock()
@@ -129,22 +123,22 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
if (target == null) target = this if (target == null) target = this
}) })
} }
Content(result, target!!.apply { content = Content(result, target!!.apply {
RepeaterService.lastMac = bssid!! RepeaterService.lastMac = bssid!!
}, persistentMacLine, shell.err.isNotEmpty()) }, persistentMacLine, legacy)
} catch (e: RuntimeException) { } catch (e: Exception) {
FirebaseCrashlytics.getInstance().apply { FirebaseCrashlytics.getInstance().apply {
setCustomKey(TAG, parser.lines.joinToString("\n")) setCustomKey(TAG, config)
setCustomKey("$TAG.ownerAddress", ownerAddress.toString()) setCustomKey("$TAG.ownerAddress", ownerAddress.toString())
setCustomKey("$TAG.p2pGroup", group.toString()) setCustomKey("$TAG.p2pGroup", group.toString())
} }
throw e throw e
} }
} }
val psk = group?.passphrase ?: content.target.psk!! val psk by lazy { group?.passphrase ?: content.target.psk!! }
val bssid = MacAddressCompat.fromString(content.target.bssid!!) 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 val (lines, block, persistentMacLine, legacy) = content
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray() block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') } .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 } persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
block[block.bssidLine!!] = "\tbssid=$bssid" block[block.bssidLine!!] = "\tbssid=$bssid"
} }
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.deviceStorage.cacheDir) RootManager.use { it.execute(RepeaterCommands.WriteP2pConfig(lines.joinToString("\n"), legacy)) }
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()
}
} }
} }

View File

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

View File

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

View File

@@ -13,6 +13,7 @@ import androidx.core.content.getSystemService
import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.DefaultLifecycleObserver
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import be.mygod.vpnhotspot.App.Companion.app 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 * 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) 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") @SuppressLint("WakelockTimeout")
private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() } private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() }

View File

@@ -0,0 +1,185 @@
package be.mygod.vpnhotspot.root
import android.content.Context
import android.os.Build
import android.os.Parcelable
import android.os.RemoteException
import android.util.Log
import androidx.annotation.RequiresApi
import be.mygod.librootkotlinx.*
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.net.Routing
import be.mygod.vpnhotspot.net.TetheringManager
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.*
import kotlinx.coroutines.channels.produce
import java.io.File
import java.io.FileOutputStream
import java.io.InterruptedIOException
import java.util.concurrent.Executor
@Parcelize
class Dump(val path: String, val cacheDir: File = app.deviceStorage.codeCacheDir) : RootCommandNoResult {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
FileOutputStream(path, true).use { out ->
val process = ProcessBuilder("sh").redirectErrorStream(true).start()
process.outputStream.bufferedWriter().use { commands ->
// https://android.googlesource.com/platform/external/iptables/+/android-7.0.0_r1/iptables/Android.mk#34
val iptablesSave = if (Build.VERSION.SDK_INT >= 24) "iptables-save" else
File(cacheDir, "iptables-save").absolutePath.also {
commands.appendln("ln -sf /system/bin/iptables $it")
}
val ip6tablesSave = if (Build.VERSION.SDK_INT >= 24) "ip6tables-save" else
File(cacheDir, "ip6tables-save").absolutePath.also {
commands.appendln("ln -sf /system/bin/ip6tables $it")
}
commands.appendln("""
|echo dumpsys ${Context.WIFI_P2P_SERVICE}
|dumpsys ${Context.WIFI_P2P_SERVICE}
|echo
|echo dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|dumpsys ${Context.CONNECTIVITY_SERVICE} tethering
|echo
|echo iptables -t filter
|$iptablesSave -t filter
|echo
|echo iptables -t nat
|$iptablesSave -t nat
|echo
|echo ip6tables-save
|$ip6tablesSave
|echo
|echo ip rule
|ip rule
|echo
|echo ip neigh
|ip neigh
|echo
|echo iptables -nvx -L vpnhotspot_fwd
|${Routing.IPTABLES} -nvx -L vpnhotspot_fwd
|echo
|echo iptables -nvx -L vpnhotspot_acl
|${Routing.IPTABLES} -nvx -L vpnhotspot_acl
|echo
|echo logcat-su
|logcat -d
""".trimMargin())
}
process.inputStream.copyTo(out)
check(process.waitFor() == 0)
}
null
}
}
sealed class ProcessData : Parcelable {
@Parcelize
data class StdoutLine(val line: String) : ProcessData()
@Parcelize
data class StderrLine(val line: String) : ProcessData()
@Parcelize
data class Exit(val code: Int) : ProcessData()
}
@Parcelize
class ProcessListener(private val terminateRegex: Regex,
private vararg val command: String) : RootCommandChannel<ProcessData> {
override fun create(scope: CoroutineScope) = scope.produce(Dispatchers.IO, capacity) {
val process = ProcessBuilder(*command).start()
val parent = Job() // we need to destroy process before joining, so we cannot use coroutineScope
try {
launch(parent) {
try {
process.inputStream.bufferedReader().forEachLine {
check(offer(ProcessData.StdoutLine(it)))
if (terminateRegex.containsMatchIn(it)) process.destroy()
}
} catch (_: InterruptedIOException) { }
}
launch(parent) {
try {
process.errorStream.bufferedReader().forEachLine { check(offer(ProcessData.StderrLine(it))) }
} catch (_: InterruptedIOException) { }
}
launch(parent) { check(offer(ProcessData.Exit(process.waitFor()))) }
parent.join()
} finally {
parent.cancel()
if (Build.VERSION.SDK_INT < 26) process.destroy() else if (process.isAlive) process.destroyForcibly()
parent.join()
}
}
}
@Parcelize
class ReadArp : RootCommand<ParcelableString> {
override suspend fun execute() = withContext(Dispatchers.IO) {
ParcelableString(File("/proc/net/arp").bufferedReader().readText())
}
}
@Parcelize
@RequiresApi(30)
class StartTethering(private val type: Int, private val showProvisioningUi: Boolean) : RootCommand<ParcelableInt?> {
override suspend fun execute(): ParcelableInt? {
val future = CompletableDeferred<Int?>()
val callback = object : TetheringManager.StartTetheringCallback {
override fun onTetheringStarted() {
future.complete(null)
}
override fun onTetheringFailed(error: Int?) {
future.complete(error!!)
}
}
TetheringManager.startTethering(type, true, showProvisioningUi, Executor {
GlobalScope.launch(Dispatchers.Unconfined) { it.run() }
}, TetheringManager.proxy(callback))
return future.await()?.let { ParcelableInt(it) }
}
}
@Deprecated("Old API since API 30")
@Parcelize
@RequiresApi(24)
@Suppress("DEPRECATION")
class StartTetheringLegacy(private val cacheDir: File, private val type: Int,
private val showProvisioningUi: Boolean) : RootCommand<ParcelableBoolean> {
override suspend fun execute(): ParcelableBoolean {
val future = CompletableDeferred<Boolean>()
val callback = object : TetheringManager.StartTetheringCallback {
override fun onTetheringStarted() {
future.complete(true)
}
override fun onTetheringFailed(error: Int?) {
check(error == null)
future.complete(false)
}
}
TetheringManager.startTetheringLegacy(type, showProvisioningUi, callback, cacheDir = cacheDir)
return ParcelableBoolean(future.await())
}
}
@Parcelize
@RequiresApi(24)
class StopTethering(private val type: Int) : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
TetheringManager.stopTethering(type)
return null
}
}
@Parcelize
class SettingsGlobalPut(val name: String, val value: String) : RootCommandNoResult {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("settings", "put", "global", name, value).redirectErrorStream(true).start()
val error = process.inputStream.bufferedReader().readText()
check(process.waitFor() == 0)
if (error.isNotEmpty()) throw RemoteException(error)
null
}
}

View File

@@ -0,0 +1,78 @@
package be.mygod.vpnhotspot.root
import android.net.wifi.p2p.WifiP2pManager
import android.os.Looper
import android.os.Parcelable
import android.system.Os
import android.system.OsConstants
import android.text.TextUtils
import be.mygod.librootkotlinx.ParcelableInt
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.vpnhotspot.net.wifi.WifiP2pManagerHelper.setWifiP2pChannels
import be.mygod.vpnhotspot.util.Services
import eu.chainfire.librootjava.RootJava
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.CompletableDeferred
import java.io.File
object RepeaterCommands {
@Parcelize
class SetChannel(private val oc: Int, private val forceReinit: Boolean = false) : RootCommand<ParcelableInt?> {
override suspend fun execute() = Services.p2p!!.run {
if (forceReinit) channel = null
val uninitializer = object : WifiP2pManager.ChannelListener {
var target: WifiP2pManager.Channel? = null
override fun onChannelDisconnected() {
if (target == channel) channel = null
}
}
val channel = channel ?: initialize(RootJava.getSystemContext(),
Looper.getMainLooper(), uninitializer)
uninitializer.target = channel
RepeaterCommands.channel = channel // cache the instance until invalidated
val future = CompletableDeferred<Int?>()
setWifiP2pChannels(channel, 0, oc, object : WifiP2pManager.ActionListener {
override fun onSuccess() {
future.complete(null)
}
override fun onFailure(reason: Int) {
future.complete(reason)
}
})
future.await()?.let { ParcelableInt(it) }
}
}
@Parcelize
data class WriteP2pConfig(val data: String, val legacy: Boolean) : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
File(if (legacy) CONF_PATH_LEGACY else CONF_PATH_TREBLE).writeText(data)
for (process in File("/proc").listFiles { _, name -> TextUtils.isDigitsOnly(name) }!!) {
if (File(File(process, "cmdline").inputStream().bufferedReader().readText()
.split(Char.MIN_VALUE, limit = 2).first()).name == "wpa_supplicant") {
Os.kill(process.name.toInt(), OsConstants.SIGTERM)
}
}
return null
}
}
@Parcelize
class ReadP2pConfig : RootCommand<WriteP2pConfig> {
private fun test(path: String) = File(path).run {
if (canRead()) readText() else null
}
override suspend fun execute(): WriteP2pConfig {
test(CONF_PATH_TREBLE)?.let { return WriteP2pConfig(it, false) }
test(CONF_PATH_LEGACY)?.let { return WriteP2pConfig(it, true) }
throw IllegalStateException("p2p config file not found")
}
}
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 var channel: WifiP2pManager.Channel? = null
}

View File

@@ -0,0 +1,33 @@
package be.mygod.vpnhotspot.root
import android.os.Parcelable
import be.mygod.librootkotlinx.RootCommandNoResult
import be.mygod.librootkotlinx.RootServer
import be.mygod.librootkotlinx.RootSession
import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.BuildConfig
import be.mygod.vpnhotspot.util.Services
import eu.chainfire.librootjava.RootJava
import kotlinx.android.parcel.Parcelize
import timber.log.Timber
object RootManager : RootSession() {
@Parcelize
class RootInit : RootCommandNoResult {
override suspend fun execute(): Parcelable? {
Services.init(RootJava.getSystemContext())
return null
}
}
override fun createServer() = RootServer { Timber.w(it) }
override suspend fun initServer(server: RootServer) {
RootServer.DEBUG = BuildConfig.DEBUG
try {
server.init(app)
} finally {
server.readUnexpectedStderr()?.let { Timber.e(it) }
}
server.execute(RootInit())
}
}

View File

@@ -0,0 +1,59 @@
package be.mygod.vpnhotspot.root
import android.os.Parcelable
import android.util.Log
import be.mygod.librootkotlinx.RootCommand
import be.mygod.librootkotlinx.RootCommandOneWay
import be.mygod.vpnhotspot.net.Routing
import kotlinx.android.parcel.Parcelize
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
object RoutingCommands {
@Parcelize
class Clean : RootCommandOneWay {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder("sh").redirectErrorStream(true).start()
process.outputStream.bufferedWriter().use(Routing.Companion::appendCleanCommands)
when (val code = process.waitFor()) {
0 -> { }
else -> Log.d("RoutingCommands.Clean", "Unexpected exit code $code")
}
check(process.waitFor() == 0)
}
}
class UnexpectedOutputException(msg: String, val result: ProcessResult) : RuntimeException(msg)
@Parcelize
data class ProcessResult(val exit: Int, val out: String, val err: String) : Parcelable {
fun message(command: List<String>, out: Boolean = this.out.isNotEmpty(),
err: Boolean = this.err.isNotEmpty()): String? {
val msg = StringBuilder("${command.joinToString(" ")} exited with $exit")
if (out) msg.append("\n${this.out}")
if (err) msg.append("\n=== stderr ===\n${this.err}")
return if (exit != 0 || out || err) msg.toString() else null
}
fun check(command: List<String>, out: Boolean = this.out.isNotEmpty(),
err: Boolean = this.err.isNotEmpty()) = message(command, out, err)?.let { msg ->
throw UnexpectedOutputException(msg, this)
}
}
@Parcelize
class Process(val command: List<String>, private val redirect: Boolean = false) : RootCommand<ProcessResult> {
@Suppress("BlockingMethodInNonBlockingContext")
override suspend fun execute() = withContext(Dispatchers.IO) {
val process = ProcessBuilder(command).redirectErrorStream(redirect).start()
coroutineScope {
val output = async { process.inputStream.bufferedReader().readText() }
val error = async { if (redirect) "" else process.errorStream.bufferedReader().readText() }
ProcessResult(process.waitFor(), output.await(), error.await())
}
}
}
}

View File

@@ -0,0 +1,19 @@
package be.mygod.vpnhotspot.root
import be.mygod.librootkotlinx.ParcelableBoolean
import be.mygod.librootkotlinx.RootCommand
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat
import be.mygod.vpnhotspot.net.wifi.WifiApManager
import kotlinx.android.parcel.Parcelize
object WifiApCommands {
@Parcelize
class GetConfiguration : RootCommand<SoftApConfigurationCompat> {
override suspend fun execute() = WifiApManager.configuration
}
@Parcelize
data class SetConfiguration(val configuration: SoftApConfigurationCompat) : RootCommand<ParcelableBoolean> {
override suspend fun execute() = ParcelableBoolean(WifiApManager.setConfiguration(configuration))
}
}

View File

@@ -1,127 +1,50 @@
package be.mygod.vpnhotspot.util package be.mygod.vpnhotspot.util
import android.os.Looper import be.mygod.librootkotlinx.RootServer
import androidx.annotation.WorkerThread import be.mygod.vpnhotspot.root.RootManager
import com.topjohnwu.superuser.Shell import be.mygod.vpnhotspot.root.RoutingCommands
import kotlinx.coroutines.* import kotlinx.coroutines.runBlocking
import timber.log.Timber import timber.log.Timber
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
import kotlin.collections.ArrayList
import kotlin.concurrent.withLock import kotlin.concurrent.withLock
class RootSession : AutoCloseable { class RootSession : AutoCloseable {
companion object { companion object {
private val monitor = ReentrantLock() private val monitor = ReentrantLock()
private fun onUnlock() {
if (monitor.holdCount == 1) instance?.startTimeoutLocked()
}
private fun unlock() {
onUnlock()
monitor.unlock()
}
private var instance: RootSession? = null fun <T> use(operation: (RootSession) -> T) = monitor.withLock { operation(RootSession()) }
private fun ensureInstance(): RootSession {
var instance = instance
if (instance == null || !instance.isAlive) instance = RootSession().also { RootSession.instance = it }
return instance
}
fun <T> use(operation: (RootSession) -> T) = monitor.withLock {
val instance = ensureInstance()
instance.haltTimeoutLocked()
operation(instance).also { onUnlock() }
}
fun beginTransaction(): Transaction { fun beginTransaction(): Transaction {
monitor.lock() monitor.lock()
val instance = try { val instance = try {
ensureInstance() RootSession()
} catch (e: RuntimeException) { } catch (e: RuntimeException) {
unlock() monitor.unlock()
throw e throw e
} }
instance.haltTimeoutLocked()
return instance.Transaction() return instance.Transaction()
} }
@WorkerThread
fun trimMemory() = monitor.withLock {
val instance = instance ?: return
instance.haltTimeoutLocked()
instance.close()
} }
fun checkOutput(command: String, result: Shell.Result, out: Boolean = result.out.isNotEmpty(), private var server: RootServer? = runBlocking { RootManager.acquire() }
err: Boolean = result.err.isNotEmpty()): String {
val msg = StringBuilder("$command exited with ${result.code}")
if (out) result.out.forEach { msg.append("\n$it") }
if (err) result.err.forEach { msg.append("\nE $it") }
if (!result.isSuccess || out || err) throw UnexpectedOutputException(msg.toString(), result)
return msg.toString()
}
}
class UnexpectedOutputException(msg: String, val result: Shell.Result) : RuntimeException(msg)
init {
check(Looper.getMainLooper().thread != Thread.currentThread()) {
"Unable to initialize shell in main thread" // https://github.com/topjohnwu/libsu/issues/33
}
}
private val shell = Shell.newInstance("su")
private val stdout = ArrayList<String>()
private val stderr = ArrayList<String>()
private val isAlive get() = shell.isAlive
override fun close() { override fun close() {
shell.close() server = null
if (instance == this) instance = null server?.let { runBlocking { RootManager.release(it) } }
}
private var timeoutJob: Job? = null
private fun startTimeoutLocked() {
check(timeoutJob == null)
timeoutJob = GlobalScope.launch(start = CoroutineStart.UNDISPATCHED) {
delay(TimeUnit.MINUTES.toMillis(5))
monitor.withLock {
close()
timeoutJob = null
}
}
}
private fun haltTimeoutLocked() {
timeoutJob?.cancel()
timeoutJob = null
} }
/** /**
* Don't care about the results, but still sync. * Don't care about the results, but still sync.
*/ */
fun submit(command: String) { fun submit(command: String) = execQuiet(command).message(listOf(command))?.let { Timber.v(it) }
val result = execQuiet(command)
val err = result.err.joinToString("\n") { "E $it" }.trim()
val out = result.out.joinToString("\n").trim()
if (result.code != 0 || err.isNotEmpty() || out.isNotEmpty()) {
Timber.v("$command exited with ${result.code}")
if (err.isNotEmpty()) Timber.v(err)
if (out.isNotEmpty()) Timber.v(out)
}
}
fun execQuiet(command: String, redirect: Boolean = false): Shell.Result { fun execQuiet(command: String, redirect: Boolean = false) = runBlocking {
stdout.clear() server!!.execute(RoutingCommands.Process(listOf("sh", "-c", command), redirect))
return shell.newJob().add(command).to(stdout, if (redirect) stdout else {
stderr.clear()
stderr
}).exec()
} }
fun exec(command: String) = checkOutput(command, execQuiet(command)) fun exec(command: String) = execQuiet(command).check(listOf(command))
fun execOut(command: String): String { fun execOut(command: String): String {
val result = execQuiet(command) val result = execQuiet(command)
checkOutput(command, result, false) result.check(listOf(command), false)
return result.out.joinToString("\n") return result.out
} }
/** /**
@@ -130,13 +53,13 @@ class RootSession : AutoCloseable {
inner class Transaction { inner class Transaction {
private val revertCommands = LinkedList<String>() private val revertCommands = LinkedList<String>()
fun exec(command: String, revert: String? = null) = checkOutput(command, execQuiet(command, revert)) fun exec(command: String, revert: String? = null) = execQuiet(command, revert).check(listOf(command))
fun execQuiet(command: String, revert: String? = null): Shell.Result { fun execQuiet(command: String, revert: String? = null): RoutingCommands.ProcessResult {
if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails if (revert != null) revertCommands.addFirst(revert) // add first just in case exec fails
return this@RootSession.execQuiet(command) return this@RootSession.execQuiet(command)
} }
fun commit() = unlock() fun commit() = monitor.unlock()
fun revert() { fun revert() {
if (revertCommands.isEmpty()) return if (revertCommands.isEmpty()) return
@@ -145,15 +68,14 @@ class RootSession : AutoCloseable {
val shell = if (locked) this@RootSession else { val shell = if (locked) this@RootSession else {
monitor.lock() monitor.lock()
locked = true locked = true
ensureInstance() RootSession()
} }
shell.haltTimeoutLocked()
revertCommands.forEach { shell.submit(it) } revertCommands.forEach { shell.submit(it) }
} catch (e: RuntimeException) { // if revert fails, it should fail silently } catch (e: RuntimeException) { // if revert fails, it should fail silently
Timber.d(e) Timber.d(e)
} finally { } finally {
revertCommands.clear() revertCommands.clear()
if (locked) unlock() // commit if (locked) monitor.unlock() // commit
} }
} }

View File

@@ -0,0 +1,29 @@
package be.mygod.vpnhotspot.util
import android.annotation.SuppressLint
import android.content.Context
import android.net.ConnectivityManager
import android.net.wifi.WifiManager
import android.net.wifi.p2p.WifiP2pManager
import android.util.Log
import androidx.core.content.getSystemService
import timber.log.Timber
@SuppressLint("LogNotTimber")
object Services {
lateinit var context: Context
fun init(context: Context) {
this.context = context
}
val connectivity by lazy { context.getSystemService<ConnectivityManager>()!! }
val p2p by lazy {
try {
context.getSystemService<WifiP2pManager>()
} catch (e: RuntimeException) {
if (android.os.Process.myUid() == 0) Log.w("WifiP2pManager", e) else Timber.w(e)
null
}
}
val wifi by lazy { context.getSystemService<WifiManager>()!! }
}

View File

@@ -25,12 +25,15 @@ import be.mygod.vpnhotspot.net.MacAddressCompat
import be.mygod.vpnhotspot.widget.SmartSnackbar import be.mygod.vpnhotspot.widget.SmartSnackbar
import java.lang.invoke.MethodHandles import java.lang.invoke.MethodHandles
import java.lang.reflect.InvocationHandler import java.lang.reflect.InvocationHandler
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method import java.lang.reflect.Method
import java.net.InetAddress import java.net.InetAddress
import java.net.NetworkInterface import java.net.NetworkInterface
import java.net.SocketException import java.net.SocketException
val Throwable.readableMessage get() = localizedMessage ?: javaClass.name val Throwable.readableMessage: String get() = if (this is InvocationTargetException) {
targetException.readableMessage
} else localizedMessage ?: javaClass.name
/** /**
* This is a hack: we wrap longs around in 1 billion and such. Hopefully every language counts in base 10 and this works * This is a hack: we wrap longs around in 1 billion and such. Hopefully every language counts in base 10 and this works

View File

@@ -11,7 +11,6 @@ import androidx.lifecycle.findViewTreeLifecycleOwner
import be.mygod.vpnhotspot.App.Companion.app import be.mygod.vpnhotspot.App.Companion.app
import be.mygod.vpnhotspot.util.readableMessage import be.mygod.vpnhotspot.util.readableMessage
import com.google.android.material.snackbar.Snackbar import com.google.android.material.snackbar.Snackbar
import com.topjohnwu.superuser.NoShellException
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
sealed class SmartSnackbar { sealed class SmartSnackbar {
@@ -26,10 +25,7 @@ sealed class SmartSnackbar {
ToastWrapper(Toast.makeText(app, text, Toast.LENGTH_LONG)) ToastWrapper(Toast.makeText(app, text, Toast.LENGTH_LONG))
} else SnackbarWrapper(Snackbar.make(holder, text, Snackbar.LENGTH_LONG)) } else SnackbarWrapper(Snackbar.make(holder, text, Snackbar.LENGTH_LONG))
} }
fun make(e: Throwable) = make(when (e) { fun make(e: Throwable) = make(e.readableMessage)
is NoShellException -> e.cause ?: e
else -> e
}.readableMessage)
} }
class Register(private val view: View) : DefaultLifecycleObserver { class Register(private val view: View) : DefaultLifecycleObserver {