@@ -2,6 +2,7 @@ package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
@@ -32,11 +33,11 @@ object DhcpWorkaround : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
RootSession.use {
|
||||
try {
|
||||
it.exec("ip rule $action iif lo uidrange 0-0 lookup local_network priority 11000")
|
||||
} catch (e: RootSession.UnexpectedOutputException) {
|
||||
if (e.result.out.isEmpty() && (e.result.code == 2 || e.result.code == 254) && if (enabled) {
|
||||
e.result.err.joinToString("\n") == "RTNETLINK answers: File exists"
|
||||
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
||||
if (e.result.out.isEmpty() && (e.result.exit == 2 || e.result.exit == 254) && if (enabled) {
|
||||
e.result.err == "RTNETLINK answers: File exists"
|
||||
} else {
|
||||
e.result.err.joinToString("\n") == "RTNETLINK answers: No such file or directory"
|
||||
e.result.err == "RTNETLINK answers: No such file or directory"
|
||||
}) return@use
|
||||
Timber.w(IOException("Failed to tweak dhcp workaround rule", e))
|
||||
SmartSnackbar.make(e).show()
|
||||
|
||||
@@ -3,7 +3,10 @@ package be.mygod.vpnhotspot.net
|
||||
import android.os.Build
|
||||
import android.system.ErrnoException
|
||||
import android.system.OsConstants
|
||||
import be.mygod.vpnhotspot.root.ReadArp
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.util.parseNumericAddress
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
@@ -35,6 +38,7 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
|
||||
|
||||
fun parse(line: String): List<IpNeighbour> {
|
||||
if (line.isBlank()) return emptyList()
|
||||
return try {
|
||||
val match = parser.matchEntire(line)!!
|
||||
val ip = parseNumericAddress(match.groupValues[2]) // by regex, ip is non-empty
|
||||
@@ -87,17 +91,24 @@ data class IpNeighbour(val ip: InetAddress, val dev: String, val lladdr: MacAddr
|
||||
private const val ARP_CACHE_EXPIRE = 1L * 1000 * 1000 * 1000
|
||||
private var arpCache = emptyList<List<String>>()
|
||||
private var arpCacheTime = -ARP_CACHE_EXPIRE
|
||||
private fun Sequence<String>.makeArp() = this
|
||||
.map { it.split(spaces) }
|
||||
.drop(1)
|
||||
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
||||
.toList()
|
||||
private fun arp(): List<List<String>> {
|
||||
if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try {
|
||||
arpCache = File("/proc/net/arp").bufferedReader().readLines()
|
||||
.asSequence()
|
||||
.map { it.split(spaces) }
|
||||
.drop(1)
|
||||
.filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() }
|
||||
.toList()
|
||||
arpCache = File("/proc/net/arp").bufferedReader().lineSequence().makeArp()
|
||||
} catch (e: IOException) {
|
||||
if (e !is FileNotFoundException || Build.VERSION.SDK_INT < 29 ||
|
||||
(e.cause as? ErrnoException)?.errno != OsConstants.EACCES) Timber.w(e)
|
||||
if (e is FileNotFoundException && Build.VERSION.SDK_INT >= 29 &&
|
||||
(e.cause as? ErrnoException)?.errno == OsConstants.EACCES) try {
|
||||
arpCache = runBlocking {
|
||||
RootManager.use { it.execute(ReadArp()) }
|
||||
}.value.lineSequence().makeArp()
|
||||
} catch (eRoot: Exception) {
|
||||
eRoot.addSuppressed(e)
|
||||
Timber.w(eRoot)
|
||||
} else Timber.w(e)
|
||||
}
|
||||
return arpCache
|
||||
}
|
||||
|
||||
@@ -11,9 +11,12 @@ import be.mygod.vpnhotspot.net.monitor.IpNeighbourMonitor
|
||||
import be.mygod.vpnhotspot.net.monitor.TrafficRecorder
|
||||
import be.mygod.vpnhotspot.net.monitor.UpstreamMonitor
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import timber.log.Timber
|
||||
import java.io.BufferedWriter
|
||||
import java.io.IOException
|
||||
import java.net.Inet4Address
|
||||
import java.net.InetAddress
|
||||
@@ -41,42 +44,38 @@ class Routing(private val caller: Any, private val downstream: String,
|
||||
private const val RULE_PRIORITY_UPSTREAM_FALLBACK = 17900
|
||||
private const val RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM = 17980
|
||||
|
||||
/**
|
||||
* -w <seconds> is not supported on 7.1-.
|
||||
* Fortunately there also isn't a time limit for starting a foreground service back in 7.1-.
|
||||
*
|
||||
* Source: https://android.googlesource.com/platform/external/iptables/+/android-5.0.0_r1/iptables/iptables.c#1574
|
||||
*/
|
||||
val IPTABLES = if (Build.VERSION.SDK_INT >= 26) "iptables -w 1" else "iptables -w"
|
||||
val IP6TABLES = if (Build.VERSION.SDK_INT >= 26) "ip6tables -w 1" else "ip6tables -w"
|
||||
const val IPTABLES ="iptables -w"
|
||||
const val IP6TABLES = "ip6tables -w"
|
||||
|
||||
fun clean() {
|
||||
fun appendCleanCommands(commands: BufferedWriter) {
|
||||
commands.appendln("$IPTABLES -t nat -F PREROUTING")
|
||||
commands.appendln("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
|
||||
commands.appendln("$IPTABLES -F vpnhotspot_fwd")
|
||||
commands.appendln("$IPTABLES -X vpnhotspot_fwd")
|
||||
commands.appendln("$IPTABLES -F vpnhotspot_acl")
|
||||
commands.appendln("$IPTABLES -X vpnhotspot_acl")
|
||||
commands.appendln("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
||||
commands.appendln("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
||||
commands.appendln("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
||||
commands.appendln("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
|
||||
commands.appendln("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
|
||||
commands.appendln("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
|
||||
commands.appendln("$IP6TABLES -F vpnhotspot_filter")
|
||||
commands.appendln("$IP6TABLES -X vpnhotspot_filter")
|
||||
commands.appendln("while ip rule del priority $RULE_PRIORITY_DNS; do done")
|
||||
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
||||
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
||||
commands.appendln("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; do done")
|
||||
}
|
||||
|
||||
suspend fun clean() {
|
||||
TrafficRecorder.clean()
|
||||
RootSession.use {
|
||||
it.execQuiet("$IPTABLES -t nat -F PREROUTING")
|
||||
it.execQuiet("while $IPTABLES -D FORWARD -j vpnhotspot_fwd; do done")
|
||||
it.execQuiet("$IPTABLES -F vpnhotspot_fwd")
|
||||
it.execQuiet("$IPTABLES -X vpnhotspot_fwd")
|
||||
it.execQuiet("$IPTABLES -F vpnhotspot_acl")
|
||||
it.execQuiet("$IPTABLES -X vpnhotspot_acl")
|
||||
it.execQuiet("while $IPTABLES -t nat -D POSTROUTING -j vpnhotspot_masquerade; do done")
|
||||
it.execQuiet("$IPTABLES -t nat -F vpnhotspot_masquerade")
|
||||
it.execQuiet("$IPTABLES -t nat -X vpnhotspot_masquerade")
|
||||
it.execQuiet("while $IP6TABLES -D INPUT -j vpnhotspot_filter; do done")
|
||||
it.execQuiet("while $IP6TABLES -D FORWARD -j vpnhotspot_filter; do done")
|
||||
it.execQuiet("while $IP6TABLES -D OUTPUT -j vpnhotspot_filter; do done")
|
||||
it.execQuiet("$IP6TABLES -F vpnhotspot_filter")
|
||||
it.execQuiet("$IP6TABLES -X vpnhotspot_filter")
|
||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_DNS; do done")
|
||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM; do done")
|
||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_FALLBACK; do done")
|
||||
it.execQuiet("while ip rule del priority $RULE_PRIORITY_UPSTREAM_DISABLE_SYSTEM; do done")
|
||||
}
|
||||
RootManager.use { it.execute(RoutingCommands.Clean()) }
|
||||
}
|
||||
|
||||
private fun RootSession.Transaction.iptables(command: String, revert: String) {
|
||||
val result = execQuiet(command, revert)
|
||||
val message = RootSession.checkOutput(command, result, err = false)
|
||||
val message = result.message(listOf(command), err = false)
|
||||
if (result.err.isNotEmpty()) Timber.i(message) // busy wait message
|
||||
}
|
||||
private fun RootSession.Transaction.iptablesAdd(content: String, table: String = "filter") =
|
||||
@@ -88,9 +87,9 @@ class Routing(private val caller: Any, private val downstream: String,
|
||||
|
||||
private fun RootSession.Transaction.ndc(name: String, command: String, revert: String? = null) {
|
||||
val result = execQuiet(command, revert)
|
||||
val log = RootSession.checkOutput(command, result,
|
||||
result.out.lastOrNull() != "200 0 $name operation succeeded")
|
||||
if (result.out.size > 1) Timber.i(log)
|
||||
val suffix = "200 0 $name operation succeeded\n"
|
||||
result.check(listOf(command), !result.out.endsWith(suffix))
|
||||
if (result.out.length > suffix.length) Timber.i(result.message(listOf(command), true))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -270,7 +269,7 @@ class Routing(private val caller: Any, private val downstream: String,
|
||||
transaction.ndc("ipfwd", "ndc ipfwd enable vpnhotspot_$downstream",
|
||||
"ndc ipfwd disable vpnhotspot_$downstream")
|
||||
return
|
||||
} catch (e: RootSession.UnexpectedOutputException) {
|
||||
} catch (e: RoutingCommands.UnexpectedOutputException) {
|
||||
Timber.w(IOException("ndc ipfwd enable failure", e))
|
||||
}
|
||||
transaction.exec("echo 1 >/proc/sys/net/ipv4/ip_forward")
|
||||
|
||||
@@ -3,7 +3,9 @@ package be.mygod.vpnhotspot.net
|
||||
import android.provider.Settings
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.root.SettingsGlobalPut
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
|
||||
/**
|
||||
* It's hard to change tethering rules with Tethering hardware acceleration enabled for now.
|
||||
@@ -16,11 +18,18 @@ import be.mygod.vpnhotspot.util.RootSession
|
||||
@RequiresApi(27)
|
||||
object TetherOffloadManager {
|
||||
private const val TETHER_OFFLOAD_DISABLED = "tether_offload_disabled"
|
||||
var enabled: Boolean
|
||||
get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||
set(value) {
|
||||
RootSession.use {
|
||||
it.exec("settings put global $TETHER_OFFLOAD_DISABLED ${if (value) 0 else 1}")
|
||||
val enabled get() = Settings.Global.getInt(app.contentResolver, TETHER_OFFLOAD_DISABLED, 0) == 0
|
||||
suspend fun setEnabled(value: Boolean) {
|
||||
val int = if (value) 0 else 1
|
||||
try {
|
||||
check(Settings.Global.putInt(Services.context.contentResolver, TETHER_OFFLOAD_DISABLED, int))
|
||||
} catch (e: SecurityException) {
|
||||
try {
|
||||
RootManager.use { it.execute(SettingsGlobalPut(TETHER_OFFLOAD_DISABLED, int.toString())) }
|
||||
} catch (eRoot: Exception) {
|
||||
eRoot.addSuppressed(e)
|
||||
throw eRoot
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,11 +15,19 @@ import androidx.annotation.RequiresApi
|
||||
import androidx.collection.SparseArrayCompat
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.root.StartTethering
|
||||
import be.mygod.vpnhotspot.root.StopTethering
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import be.mygod.vpnhotspot.util.broadcastReceiver
|
||||
import be.mygod.vpnhotspot.util.callSuper
|
||||
import be.mygod.vpnhotspot.util.ensureReceiverUnregistered
|
||||
import com.android.dx.stock.ProxyBuilder
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.lang.ref.WeakReference
|
||||
import java.lang.reflect.InvocationHandler
|
||||
import java.lang.reflect.InvocationTargetException
|
||||
@@ -49,7 +57,10 @@ object TetheringManager {
|
||||
*/
|
||||
fun onTetheringFailed(error: Int? = null) { }
|
||||
|
||||
fun onException() { }
|
||||
/**
|
||||
* ADDED: Called when a local Exception occurred.
|
||||
*/
|
||||
fun onException(e: Exception) { }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,7 +141,6 @@ object TetheringManager {
|
||||
/**
|
||||
* Ncm local tethering type.
|
||||
*
|
||||
* Requires NETWORK_SETTINGS permission, which is sadly not obtainable.
|
||||
* @see [startTethering]
|
||||
*/
|
||||
@RequiresApi(30)
|
||||
@@ -149,7 +159,7 @@ object TetheringManager {
|
||||
@get:RequiresApi(30)
|
||||
private val instance by lazy @TargetApi(30) {
|
||||
@SuppressLint("WrongConstant") // hidden services are not included in constants as of R preview 4
|
||||
val service = app.getSystemService(TETHERING_SERVICE)
|
||||
val service = Services.context.getSystemService(TETHERING_SERVICE)
|
||||
service
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
@@ -211,12 +221,64 @@ object TetheringManager {
|
||||
|
||||
private fun Handler?.makeExecutor() = Executor { if (this == null) it.run() else post(it) }
|
||||
|
||||
@Deprecated("Legacy API")
|
||||
@RequiresApi(24)
|
||||
fun startTetheringLegacy(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||
val reference = WeakReference(callback)
|
||||
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
||||
dexCache(cacheDir)
|
||||
handler { proxy, method, args ->
|
||||
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
|
||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||
when (method.name) {
|
||||
"onTetheringStarted" -> callback?.onTetheringStarted()
|
||||
"onTetheringFailed" -> callback?.onTetheringFailed()
|
||||
else -> ProxyBuilder.callSuper(proxy, method, args)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
startTetheringLegacy(Services.connectivity, type, showProvisioningUi, proxy, handler)
|
||||
}
|
||||
@RequiresApi(30)
|
||||
fun startTethering(type: Int, exemptFromEntitlementCheck: Boolean, showProvisioningUi: Boolean, executor: Executor,
|
||||
proxy: Any) {
|
||||
startTethering(instance, newTetheringRequestBuilder.newInstance(type).let { builder ->
|
||||
// setting exemption requires TETHER_PRIVILEGED permission
|
||||
if (exemptFromEntitlementCheck) setExemptFromEntitlementCheck(builder, true)
|
||||
setShouldShowEntitlementUi(builder, showProvisioningUi)
|
||||
build(builder)
|
||||
}, executor, proxy)
|
||||
}
|
||||
@RequiresApi(30)
|
||||
fun proxy(callback: StartTetheringCallback): Any {
|
||||
val reference = WeakReference(callback)
|
||||
return Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
|
||||
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||
return when (val name = method.name) {
|
||||
"onTetheringStarted" -> {
|
||||
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
|
||||
callback?.onTetheringStarted()
|
||||
}
|
||||
"onTetheringFailed" -> {
|
||||
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
|
||||
callback?.onTetheringFailed(args?.get(0) as Int)
|
||||
}
|
||||
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
/**
|
||||
* Runs tether provisioning for the given type if needed and then starts tethering if
|
||||
* the check succeeds. If no carrier provisioning is required for tethering, tethering is
|
||||
* enabled immediately. If provisioning fails, tethering will not be enabled. It also
|
||||
* schedules tether provisioning re-checks if appropriate.
|
||||
*
|
||||
* CHANGED BEHAVIOR: This method will not throw Exceptions, instead, callback.onException will be called.
|
||||
*
|
||||
* @param type The type of tethering to start. Must be one of
|
||||
* {@link ConnectivityManager.TETHERING_WIFI},
|
||||
* {@link ConnectivityManager.TETHERING_USB}, or
|
||||
@@ -234,48 +296,57 @@ object TetheringManager {
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun startTethering(type: Int, showProvisioningUi: Boolean, callback: StartTetheringCallback,
|
||||
handler: Handler? = null) {
|
||||
val reference = WeakReference(callback)
|
||||
if (Build.VERSION.SDK_INT >= 30) {
|
||||
val request = newTetheringRequestBuilder.newInstance(type).let { builder ->
|
||||
// setting exemption requires TETHER_PRIVILEGED permission
|
||||
if (app.checkSelfPermission("android.permission.TETHER_PRIVILEGED") ==
|
||||
PackageManager.PERMISSION_GRANTED) setExemptFromEntitlementCheck(builder, true)
|
||||
setShouldShowEntitlementUi(builder, showProvisioningUi)
|
||||
build(builder)
|
||||
handler: Handler? = null, cacheDir: File = app.deviceStorage.codeCacheDir) {
|
||||
if (Build.VERSION.SDK_INT >= 30) try {
|
||||
val proxy = proxy(callback)
|
||||
val executor = handler.makeExecutor()
|
||||
try {
|
||||
startTethering(type, true, showProvisioningUi, executor, proxy)
|
||||
} catch (e1: InvocationTargetException) {
|
||||
if (e1.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
|
||||
val result = try {
|
||||
RootManager.use { it.execute(StartTethering(type, showProvisioningUi)) }
|
||||
} catch (e2: Exception) {
|
||||
e2.addSuppressed(e1)
|
||||
try {
|
||||
// last resort: start tethering without trying to bypass entitlement check
|
||||
startTethering(type, false, showProvisioningUi, executor, proxy)
|
||||
Timber.w(e2)
|
||||
} catch (e3: Exception) {
|
||||
e3.addSuppressed(e2)
|
||||
Timber.w(e3)
|
||||
callback.onException(e3)
|
||||
}
|
||||
return@launch
|
||||
}
|
||||
if (result == null) callback.onTetheringStarted()
|
||||
else callback.onTetheringFailed(result.value)
|
||||
} else callback.onException(e1)
|
||||
}
|
||||
val proxy = Proxy.newProxyInstance(interfaceStartTetheringCallback.classLoader,
|
||||
arrayOf(interfaceStartTetheringCallback), object : InvocationHandler {
|
||||
override fun invoke(proxy: Any, method: Method, args: Array<out Any?>?): Any? {
|
||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||
return when (val name = method.name) {
|
||||
"onTetheringStarted" -> {
|
||||
if (!args.isNullOrEmpty()) Timber.w("Unexpected args for $name: $args")
|
||||
callback?.onTetheringStarted()
|
||||
}
|
||||
"onTetheringFailed" -> {
|
||||
if (args?.size != 1) Timber.w("Unexpected args for $name: $args")
|
||||
callback?.onTetheringFailed(args?.getOrNull(0) as? Int?)
|
||||
}
|
||||
else -> callSuper(interfaceStartTetheringCallback, proxy, method, args)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
callback.onException(e)
|
||||
} else @Suppress("DEPRECATION") try {
|
||||
startTetheringLegacy(type, showProvisioningUi, callback, handler, cacheDir)
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
|
||||
val result = try {
|
||||
val rootCache = File(cacheDir, "root")
|
||||
rootCache.mkdirs()
|
||||
check(rootCache.exists()) { "Creating root cache dir failed" }
|
||||
RootManager.use {
|
||||
it.execute(be.mygod.vpnhotspot.root.StartTetheringLegacy(
|
||||
rootCache, type, showProvisioningUi))
|
||||
}.value
|
||||
} catch (eRoot: Exception) {
|
||||
eRoot.addSuppressed(e)
|
||||
Timber.w(eRoot)
|
||||
callback.onException(eRoot)
|
||||
return@launch
|
||||
}
|
||||
})
|
||||
startTethering(instance, request, handler.makeExecutor(), proxy)
|
||||
} else {
|
||||
val proxy = ProxyBuilder.forClass(classOnStartTetheringCallback).apply {
|
||||
dexCache(app.deviceStorage.cacheDir)
|
||||
handler { proxy, method, args ->
|
||||
if (args.isNotEmpty()) Timber.w("Unexpected args for ${method.name}: $args")
|
||||
@Suppress("NAME_SHADOWING") val callback = reference.get()
|
||||
when (method.name) {
|
||||
"onTetheringStarted" -> callback?.onTetheringStarted()
|
||||
"onTetheringFailed" -> callback?.onTetheringFailed()
|
||||
else -> ProxyBuilder.callSuper(proxy, method, args)
|
||||
}
|
||||
}
|
||||
}.build()
|
||||
startTetheringLegacy(app.connectivity, type, showProvisioningUi, proxy, handler)
|
||||
if (result) callback.onTetheringStarted() else callback.onTetheringFailed()
|
||||
} else callback.onException(e)
|
||||
} catch (e: Exception) {
|
||||
callback.onException(e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -290,7 +361,21 @@ object TetheringManager {
|
||||
*/
|
||||
@RequiresApi(24)
|
||||
fun stopTethering(type: Int) {
|
||||
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type) else stopTetheringLegacy(app.connectivity, type)
|
||||
if (Build.VERSION.SDK_INT >= 30) stopTethering(instance, type)
|
||||
else stopTetheringLegacy(Services.connectivity, type)
|
||||
}
|
||||
@RequiresApi(24)
|
||||
fun stopTethering(type: Int, callback: (Exception) -> Unit) = try {
|
||||
stopTethering(type)
|
||||
} catch (e: InvocationTargetException) {
|
||||
if (e.targetException is SecurityException) GlobalScope.launch(Dispatchers.Unconfined) {
|
||||
try {
|
||||
RootManager.use { it.execute(StopTethering(type)) }
|
||||
} catch (eRoot: Exception) {
|
||||
eRoot.addSuppressed(e)
|
||||
callback(eRoot)
|
||||
}
|
||||
} else callback(e)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -517,7 +602,7 @@ object TetheringManager {
|
||||
* @return error The error code of the last error tethering or untethering the named
|
||||
* interface
|
||||
*/
|
||||
fun getLastTetherError(iface: String): Int = getLastTetherError(app.connectivity, iface) as Int
|
||||
fun getLastTetherError(iface: String): Int = getLastTetherError(Services.connectivity, iface) as Int
|
||||
|
||||
// tether errors defined in ConnectivityManager up to Android 10
|
||||
private val tetherErrors29 = arrayOf("TETHER_ERROR_NO_ERROR", "TETHER_ERROR_UNKNOWN_IFACE",
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.net.*
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import android.os.Build
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -23,7 +26,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||
.build()
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
val properties = app.connectivity.getLinkProperties(network)
|
||||
val properties = Services.connectivity.getLinkProperties(network)
|
||||
val ifname = properties?.interfaceName ?: return
|
||||
var switching = false
|
||||
synchronized(this@DefaultNetworkMonitor) {
|
||||
@@ -83,9 +86,9 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||
}
|
||||
} else {
|
||||
if (Build.VERSION.SDK_INT in 24..27) @TargetApi(24) {
|
||||
app.connectivity.registerDefaultNetworkCallback(networkCallback)
|
||||
Services.connectivity.registerDefaultNetworkCallback(networkCallback)
|
||||
} else try {
|
||||
app.connectivity.requestNetwork(networkRequest, networkCallback)
|
||||
Services.connectivity.requestNetwork(networkRequest, networkCallback)
|
||||
} catch (e: SecurityException) {
|
||||
// SecurityException would be thrown in requestNetwork on Android 6.0 thanks to Google's stupid bug
|
||||
if (Build.VERSION.SDK_INT != 23) throw e
|
||||
@@ -98,7 +101,7 @@ object DefaultNetworkMonitor : UpstreamMonitor() {
|
||||
|
||||
override fun destroyLocked() {
|
||||
if (!registered) return
|
||||
app.connectivity.unregisterNetworkCallback(networkCallback)
|
||||
Services.connectivity.unregisterNetworkCallback(networkCallback)
|
||||
registered = false
|
||||
currentNetwork = null
|
||||
currentLinkProperties = null
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.net.LinkProperties
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@@ -27,8 +27,8 @@ class InterfaceMonitor(val iface: String) : UpstreamMonitor() {
|
||||
private var registered = false
|
||||
override var currentIface: String? = null
|
||||
private set
|
||||
override val currentLinkProperties get() = app.connectivity.allNetworks
|
||||
.map { app.connectivity.getLinkProperties(it) }
|
||||
override val currentLinkProperties get() = Services.connectivity.allNetworks
|
||||
.map { Services.connectivity.getLinkProperties(it) }
|
||||
.singleOrNull { it?.interfaceName == iface }
|
||||
|
||||
override fun registerCallbackLocked(callback: Callback) {
|
||||
|
||||
@@ -17,7 +17,7 @@ class IpLinkMonitor private constructor() : IpMonitor() {
|
||||
monitor = IpLinkMonitor()
|
||||
instance = monitor
|
||||
}
|
||||
monitor.flush()
|
||||
monitor.flushAsync()
|
||||
}
|
||||
fun unregisterCallback(owner: Any) = synchronized(this) {
|
||||
if (callbacks.remove(owner) == null || callbacks.isNotEmpty()) return@synchronized
|
||||
|
||||
@@ -4,20 +4,25 @@ import android.os.Build
|
||||
import android.system.ErrnoException
|
||||
import android.system.OsConstants
|
||||
import androidx.core.content.edit
|
||||
import be.mygod.librootkotlinx.RootServer
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.BuildConfig
|
||||
import be.mygod.vpnhotspot.R
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.root.ProcessData
|
||||
import be.mygod.vpnhotspot.root.ProcessListener
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import be.mygod.vpnhotspot.root.RoutingCommands
|
||||
import be.mygod.vpnhotspot.widget.SmartSnackbar
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.channels.ReceiveChannel
|
||||
import kotlinx.coroutines.channels.consumeEach
|
||||
import timber.log.Timber
|
||||
import java.io.IOException
|
||||
import java.io.InterruptedIOException
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.concurrent.ScheduledExecutorService
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
|
||||
abstract class IpMonitor : Runnable {
|
||||
abstract class IpMonitor {
|
||||
companion object {
|
||||
const val KEY = "service.ipMonitor"
|
||||
// https://android.googlesource.com/platform/external/iproute2/+/7f7a711/lib/libnetlink.c#493
|
||||
@@ -51,7 +56,7 @@ abstract class IpMonitor : Runnable {
|
||||
@Volatile
|
||||
private var destroyed = false
|
||||
private var monitor: Process? = null
|
||||
private var pool: ScheduledExecutorService? = null
|
||||
private val worker = Job()
|
||||
|
||||
private fun handleProcess(builder: ProcessBuilder) {
|
||||
val process = try {
|
||||
@@ -79,8 +84,18 @@ abstract class IpMonitor : Runnable {
|
||||
if ((e.cause as? ErrnoException)?.errno != OsConstants.EBADF) Timber.w(e)
|
||||
}
|
||||
err.join()
|
||||
process.waitFor()
|
||||
Timber.d("Monitor process exited with ${process.exitValue()}")
|
||||
Timber.d("Monitor process exited with ${process.waitFor()}")
|
||||
}
|
||||
private suspend fun handleChannel(channel: ReceiveChannel<ProcessData>) {
|
||||
channel.consumeEach {
|
||||
when (it) {
|
||||
is ProcessData.StdoutLine -> if (errorMatcher.containsMatchIn(it.line)) {
|
||||
Timber.w(it.line)
|
||||
} else processLine(it.line)
|
||||
is ProcessData.StderrLine -> Timber.e(it.line)
|
||||
is ProcessData.Exit -> Timber.d("Root monitor process exited with ${it.code}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
@@ -92,17 +107,64 @@ abstract class IpMonitor : Runnable {
|
||||
handleProcess(ProcessBuilder("ip", "monitor", monitoredObject))
|
||||
if (destroyed) return@thread
|
||||
}
|
||||
handleProcess(ProcessBuilder("su", "-c", "exec ip monitor $monitoredObject"))
|
||||
try {
|
||||
runBlocking(EmptyCoroutineContext + worker) {
|
||||
RootManager.use { server ->
|
||||
// while we only need to use this server once, we need to also keep the server alive
|
||||
handleChannel(server.create(ProcessListener(errorMatcher, "ip", "monitor", monitoredObject),
|
||||
this))
|
||||
}
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
} catch (e: Exception) {
|
||||
Timber.w(e)
|
||||
}
|
||||
if (destroyed) return@thread
|
||||
app.logEvent("ip_monitor_failure")
|
||||
}
|
||||
val pool = Executors.newScheduledThreadPool(1)
|
||||
pool.scheduleAtFixedRate(this, 1, 1, TimeUnit.SECONDS)
|
||||
this.pool = pool
|
||||
GlobalScope.launch(Dispatchers.IO + worker) {
|
||||
var server: RootServer? = null
|
||||
try {
|
||||
while (isActive) {
|
||||
delay(1000)
|
||||
server = work(server)
|
||||
}
|
||||
} finally {
|
||||
if (server != null) RootManager.release(server)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun flush() = thread(name = "${javaClass.simpleName}-flush") { run() }
|
||||
/**
|
||||
* Possibly blocking. Should run in IO dispatcher or use [flushAsync].
|
||||
*/
|
||||
suspend fun flush() = work(null)?.let { RootManager.release(it) }
|
||||
fun flushAsync() = GlobalScope.launch(Dispatchers.IO) { flush() }
|
||||
|
||||
private suspend fun work(server: RootServer?): RootServer? {
|
||||
if (currentMode != Mode.PollRoot) try {
|
||||
poll()
|
||||
return server
|
||||
} catch (e: IOException) {
|
||||
app.logEvent("ip_poll_failure")
|
||||
Timber.d(e)
|
||||
}
|
||||
var newServer = server
|
||||
try {
|
||||
val command = listOf("ip", monitoredObject)
|
||||
val result = (server ?: RootManager.acquire().also { newServer = it })
|
||||
.execute(RoutingCommands.Process(command))
|
||||
result.check(command, false)
|
||||
val lines = result.out.lines()
|
||||
if (lines.any { errorMatcher.containsMatchIn(it) }) throw IOException(result.out)
|
||||
processLines(lines.asSequence())
|
||||
} catch (e: RuntimeException) {
|
||||
app.logEvent("ip_su_poll_failure") { param("cause", e.message.toString()) }
|
||||
Timber.d(e)
|
||||
}
|
||||
return newServer
|
||||
}
|
||||
|
||||
private fun poll() {
|
||||
val process = ProcessBuilder("ip", monitoredObject)
|
||||
@@ -125,32 +187,9 @@ abstract class IpMonitor : Runnable {
|
||||
}
|
||||
}
|
||||
|
||||
override fun run() {
|
||||
if (currentMode != Mode.PollRoot) try {
|
||||
return poll()
|
||||
} catch (e: IOException) {
|
||||
app.logEvent("ip_poll_failure")
|
||||
Timber.d(e)
|
||||
}
|
||||
try {
|
||||
val command = "ip $monitoredObject"
|
||||
RootSession.use { shell ->
|
||||
val result = shell.execQuiet(command)
|
||||
RootSession.checkOutput(command, result, false)
|
||||
if (result.out.any { errorMatcher.containsMatchIn(it) }) {
|
||||
throw IOException(result.out.joinToString("\n"))
|
||||
}
|
||||
processLines(result.out.asSequence())
|
||||
}
|
||||
} catch (e: RuntimeException) {
|
||||
app.logEvent("ip_su_poll_failure") { param("cause", e.message.toString()) }
|
||||
Timber.d(e)
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
destroyed = true
|
||||
monitor?.destroy()
|
||||
pool?.shutdown()
|
||||
worker.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ class IpNeighbourMonitor private constructor() : IpMonitor() {
|
||||
if (monitor == null) {
|
||||
monitor = IpNeighbourMonitor()
|
||||
instance = monitor
|
||||
monitor.flush()
|
||||
monitor.flushAsync()
|
||||
null
|
||||
} else monitor.neighbours.values
|
||||
}?.let { callback.onIpNeighbourAvailable(it) }
|
||||
|
||||
@@ -42,6 +42,7 @@ class TetherTimeoutMonitor(private val context: Context, private val onTimeout:
|
||||
var enabled
|
||||
get() = Settings.Global.getInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, 1) == 1
|
||||
set(value) {
|
||||
// TODO: WRITE_SECURE_SETTINGS permission
|
||||
check(Settings.Global.putInt(app.contentResolver, SOFT_AP_TIMEOUT_ENABLED, if (value) 1 else 0))
|
||||
}
|
||||
@Deprecated("Use SoftApConfigurationCompat instead")
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.util.LongSparseArray
|
||||
import androidx.collection.LongSparseArray
|
||||
import androidx.collection.set
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.net.Routing.Companion.IPTABLES
|
||||
import be.mygod.vpnhotspot.room.AppDatabase
|
||||
@@ -63,10 +64,11 @@ object TrafficRecorder {
|
||||
loop@ for (line in RootSession.use {
|
||||
val command = "$IPTABLES -nvx -L vpnhotspot_acl"
|
||||
val result = it.execQuiet(command)
|
||||
val message = RootSession.checkOutput(command, result, false, false)
|
||||
val message = result.message(listOf(command))
|
||||
if (result.err.isNotEmpty()) Timber.i(message)
|
||||
result.out.drop(2)
|
||||
result.out.lineSequence().drop(2)
|
||||
}) {
|
||||
if (line.isBlank()) continue
|
||||
val columns = line.split("\\s+".toRegex()).filter { it.isNotEmpty() }
|
||||
try {
|
||||
check(columns.size >= 9)
|
||||
@@ -104,7 +106,7 @@ object TrafficRecorder {
|
||||
}
|
||||
if (oldRecord.id != null) {
|
||||
check(records.put(key, record) == oldRecord)
|
||||
oldRecords.put(oldRecord.id!!, oldRecord)
|
||||
oldRecords[oldRecord.id!!] = oldRecord
|
||||
}
|
||||
}
|
||||
else -> check(false)
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
package be.mygod.vpnhotspot.net.monitor
|
||||
|
||||
import android.net.*
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import android.net.ConnectivityManager
|
||||
import android.net.LinkProperties
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.launch
|
||||
import timber.log.Timber
|
||||
@@ -21,7 +24,7 @@ object VpnMonitor : UpstreamMonitor() {
|
||||
}
|
||||
private val networkCallback = object : ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
val properties = app.connectivity.getLinkProperties(network)
|
||||
val properties = Services.connectivity.getLinkProperties(network)
|
||||
val ifname = properties?.interfaceName ?: return
|
||||
var switching = false
|
||||
synchronized(this@VpnMonitor) {
|
||||
@@ -88,14 +91,14 @@ object VpnMonitor : UpstreamMonitor() {
|
||||
callback.onAvailable(currentLinkProperties.interfaceName!!, currentLinkProperties)
|
||||
}
|
||||
} else {
|
||||
app.connectivity.registerNetworkCallback(request, networkCallback)
|
||||
Services.connectivity.registerNetworkCallback(request, networkCallback)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
|
||||
override fun destroyLocked() {
|
||||
if (!registered) return
|
||||
app.connectivity.unregisterNetworkCallback(networkCallback)
|
||||
Services.connectivity.unregisterNetworkCallback(networkCallback)
|
||||
registered = false
|
||||
available.clear()
|
||||
currentNetwork = null
|
||||
|
||||
@@ -1,24 +1,20 @@
|
||||
package be.mygod.vpnhotspot.net.wifi
|
||||
|
||||
import android.net.wifi.p2p.WifiP2pGroup
|
||||
import android.os.Build
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.RepeaterService
|
||||
import be.mygod.vpnhotspot.net.MacAddressCompat
|
||||
import be.mygod.vpnhotspot.util.RootSession
|
||||
import be.mygod.vpnhotspot.root.RepeaterCommands
|
||||
import be.mygod.vpnhotspot.root.RootManager
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import java.io.File
|
||||
|
||||
/**
|
||||
* This parser is based on:
|
||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/d2986c2/wpa_supplicant/config.c#488
|
||||
* https://android.googlesource.com/platform/external/wpa_supplicant_8/+/6fa46df/wpa_supplicant/config_file.c#182
|
||||
*/
|
||||
class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerAddress: String? = null) {
|
||||
class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null) {
|
||||
companion object {
|
||||
private const val TAG = "P2pSupplicantConfiguration"
|
||||
private const val CONF_PATH_TREBLE = "/data/vendor/wifi/wpa/p2p_supplicant.conf"
|
||||
private const val CONF_PATH_LEGACY = "/data/misc/wifi/p2p_supplicant.conf"
|
||||
private const val PERSISTENT_MAC = "p2p_device_persistent_mac_addr="
|
||||
private val networkParser =
|
||||
"^(bssid=(([0-9a-fA-F]{2}:){5}[0-9a-fA-F]{2})|psk=(ext:|\"(.*)\"|[0-9a-fA-F]{64}\$)?)".toRegex()
|
||||
@@ -36,12 +32,11 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
||||
override fun toString() = joinToString("\n")
|
||||
}
|
||||
|
||||
private class Parser(val lines: List<String>) {
|
||||
private val iterator = lines.iterator()
|
||||
private class Parser(val lines: Iterator<String>) {
|
||||
lateinit var line: String
|
||||
lateinit var trimmed: String
|
||||
fun next() = if (iterator.hasNext()) {
|
||||
line = iterator.next().apply { trimmed = trimStart('\r', '\t', ' ') }
|
||||
fun next() = if (lines.hasNext()) {
|
||||
line = lines.next().apply { trimmed = trimStart('\r', '\t', ' ') }
|
||||
true
|
||||
} else false
|
||||
}
|
||||
@@ -49,14 +44,12 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
||||
private data class Content(val lines: ArrayList<Any>, var target: NetworkBlock, var persistentMacLine: Int?,
|
||||
var legacy: Boolean)
|
||||
|
||||
private val content = RootSession.use {
|
||||
private lateinit var content: Content
|
||||
suspend fun init(ownerAddress: String? = null) {
|
||||
val result = ArrayList<Any>()
|
||||
var target: NetworkBlock? = null
|
||||
var persistentMacLine: Int? = null
|
||||
val command = "cat $CONF_PATH_TREBLE || cat $CONF_PATH_LEGACY"
|
||||
val shell = it.execQuiet(command)
|
||||
RootSession.checkOutput(command, shell, false, false)
|
||||
val parser = Parser(shell.out)
|
||||
val (config, legacy) = RootManager.use { it.execute(RepeaterCommands.ReadP2pConfig()) }
|
||||
try {
|
||||
var bssids = listOfNotNull(group?.owner?.deviceAddress, ownerAddress)
|
||||
.distinct()
|
||||
@@ -68,6 +61,7 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
||||
false
|
||||
}
|
||||
}
|
||||
val parser = Parser(config.lineSequence().iterator())
|
||||
while (parser.next()) {
|
||||
if (parser.trimmed.startsWith("network={")) {
|
||||
val block = NetworkBlock()
|
||||
@@ -129,22 +123,22 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
||||
if (target == null) target = this
|
||||
})
|
||||
}
|
||||
Content(result, target!!.apply {
|
||||
content = Content(result, target!!.apply {
|
||||
RepeaterService.lastMac = bssid!!
|
||||
}, persistentMacLine, shell.err.isNotEmpty())
|
||||
} catch (e: RuntimeException) {
|
||||
}, persistentMacLine, legacy)
|
||||
} catch (e: Exception) {
|
||||
FirebaseCrashlytics.getInstance().apply {
|
||||
setCustomKey(TAG, parser.lines.joinToString("\n"))
|
||||
setCustomKey(TAG, config)
|
||||
setCustomKey("$TAG.ownerAddress", ownerAddress.toString())
|
||||
setCustomKey("$TAG.p2pGroup", group.toString())
|
||||
}
|
||||
throw e
|
||||
}
|
||||
}
|
||||
val psk = group?.passphrase ?: content.target.psk!!
|
||||
val bssid = MacAddressCompat.fromString(content.target.bssid!!)
|
||||
val psk by lazy { group?.passphrase ?: content.target.psk!! }
|
||||
val bssid by lazy { MacAddressCompat.fromString(content.target.bssid!!) }
|
||||
|
||||
fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
||||
suspend fun update(ssid: String, psk: String, bssid: MacAddressCompat?) {
|
||||
val (lines, block, persistentMacLine, legacy) = content
|
||||
block[block.ssidLine!!] = "\tssid=" + ssid.toByteArray()
|
||||
.joinToString("") { (it.toInt() and 255).toString(16).padStart(2, '0') }
|
||||
@@ -153,25 +147,6 @@ class P2pSupplicantConfiguration(private val group: WifiP2pGroup? = null, ownerA
|
||||
persistentMacLine?.let { lines[it] = PERSISTENT_MAC + bssid }
|
||||
block[block.bssidLine!!] = "\tbssid=$bssid"
|
||||
}
|
||||
val tempFile = File.createTempFile("vpnhotspot-", ".conf", app.deviceStorage.cacheDir)
|
||||
try {
|
||||
tempFile.printWriter().use { writer ->
|
||||
lines.forEach { writer.println(it) }
|
||||
}
|
||||
// pkill not available on Lollipop. Source: https://android.googlesource.com/platform/system/core/+/master/shell_and_utilities/README.md
|
||||
RootSession.use {
|
||||
it.exec("cat ${tempFile.absolutePath} > ${if (legacy) CONF_PATH_LEGACY else CONF_PATH_TREBLE}")
|
||||
if (Build.VERSION.SDK_INT >= 23) it.exec("pkill wpa_supplicant") else {
|
||||
val result = try {
|
||||
it.execOut("ps | grep wpa_supplicant").split(whitespaceMatcher).apply { check(size >= 2) }
|
||||
} catch (e: Exception) {
|
||||
throw IllegalStateException("wpa_supplicant not found, please toggle Airplane mode manually", e)
|
||||
}
|
||||
it.exec("kill ${result[1]}")
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
if (!tempFile.delete()) tempFile.deleteOnExit()
|
||||
}
|
||||
RootManager.use { it.execute(RepeaterCommands.WriteP2pConfig(lines.joinToString("\n"), legacy)) }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,10 +38,10 @@ data class SoftApConfigurationCompat(
|
||||
/**
|
||||
* TODO
|
||||
*/
|
||||
const val BAND_ANY = -1
|
||||
const val BAND_2GHZ = 0
|
||||
const val BAND_5GHZ = 1
|
||||
const val BAND_6GHZ = 2
|
||||
const val BAND_ANY = 0
|
||||
const val BAND_2GHZ = 1
|
||||
const val BAND_5GHZ = 2
|
||||
const val BAND_6GHZ = 3
|
||||
const val CH_INVALID = 0
|
||||
|
||||
// TODO: localize?
|
||||
@@ -144,7 +144,9 @@ data class SoftApConfigurationCompat(
|
||||
classBuilder.getDeclaredMethod("setBssid", MacAddress::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setChannel by lazy { classBuilder.getDeclaredMethod("setChannel", Int::class.java) }
|
||||
private val setChannel by lazy {
|
||||
classBuilder.getDeclaredMethod("setChannel", Int::class.java, Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setClientControlByUserEnabled by lazy {
|
||||
classBuilder.getDeclaredMethod("setClientControlByUserEnabled", Boolean::class.java)
|
||||
@@ -156,7 +158,9 @@ data class SoftApConfigurationCompat(
|
||||
classBuilder.getDeclaredMethod("setMaxNumberOfClients", Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setPassphrase by lazy { classBuilder.getDeclaredMethod("setPassphrase", String::class.java) }
|
||||
private val setPassphrase by lazy {
|
||||
classBuilder.getDeclaredMethod("setPassphrase", String::class.java, Int::class.java)
|
||||
}
|
||||
@get:RequiresApi(30)
|
||||
private val setShutdownTimeoutMillis by lazy {
|
||||
classBuilder.getDeclaredMethod("setShutdownTimeoutMillis", Long::class.java)
|
||||
@@ -186,7 +190,7 @@ data class SoftApConfigurationCompat(
|
||||
}
|
||||
},
|
||||
preSharedKey,
|
||||
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) else BAND_ANY, // TODO
|
||||
if (Build.VERSION.SDK_INT >= 23) apBand.getInt(this) + 1 else BAND_ANY, // TODO
|
||||
if (Build.VERSION.SDK_INT >= 23) apChannel.getInt(this) else CH_INVALID, // TODO
|
||||
BSSID?.let { MacAddressCompat.fromString(it) }?.addr,
|
||||
0, // TODO: unsupported field should have @RequiresApi?
|
||||
@@ -275,10 +279,10 @@ data class SoftApConfigurationCompat(
|
||||
// TODO: can we always call copy constructor?
|
||||
val builder = if (sac == null) classBuilder.newInstance() else newBuilder.newInstance(sac)
|
||||
setSsid(builder, ssid)
|
||||
// TODO: setSecurityType
|
||||
setPassphrase(builder, passphrase)
|
||||
setBand(builder, band)
|
||||
setChannel(builder, channel)
|
||||
setPassphrase(builder, passphrase, securityType)
|
||||
// TODO: how to use these?
|
||||
// setBand(builder, band)
|
||||
// setChannel(builder, band, channel)
|
||||
setBssid(builder, bssid?.toPlatform())
|
||||
setMaxNumberOfClients(builder, maxNumberOfClients)
|
||||
setShutdownTimeoutMillis(builder, shutdownTimeoutMillis)
|
||||
|
||||
@@ -5,8 +5,8 @@ import android.net.wifi.SoftApConfiguration
|
||||
import android.net.wifi.WifiManager
|
||||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.net.wifi.SoftApConfigurationCompat.Companion.toCompat
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
|
||||
object WifiApManager {
|
||||
private val getWifiApConfiguration by lazy { WifiManager::class.java.getDeclaredMethod("getWifiApConfiguration") }
|
||||
@@ -22,22 +22,18 @@ object WifiApManager {
|
||||
WifiManager::class.java.getDeclaredMethod("setSoftApConfiguration", SoftApConfiguration::class.java)
|
||||
}
|
||||
|
||||
var configuration: SoftApConfigurationCompat
|
||||
get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
(getWifiApConfiguration(app.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
||||
?: SoftApConfigurationCompat.empty()
|
||||
} else (getSoftApConfiguration(app.wifi) as SoftApConfiguration).toCompat()
|
||||
set(value) = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
require(setWifiApConfiguration(app.wifi,
|
||||
value.toWifiConfiguration()) as Boolean) { "setWifiApConfiguration failed" }
|
||||
} else require(setSoftApConfiguration(app.wifi, value.toPlatform()) as Boolean) {
|
||||
"setSoftApConfiguration failed"
|
||||
}
|
||||
val configuration get() = if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
(getWifiApConfiguration(Services.wifi) as android.net.wifi.WifiConfiguration?)?.toCompat()
|
||||
?: SoftApConfigurationCompat.empty()
|
||||
} else (getSoftApConfiguration(Services.wifi) as SoftApConfiguration).toCompat()
|
||||
fun setConfiguration(value: SoftApConfigurationCompat) = (if (Build.VERSION.SDK_INT < 30) @Suppress("DEPRECATION") {
|
||||
setWifiApConfiguration(Services.wifi, value.toWifiConfiguration())
|
||||
} else setSoftApConfiguration(Services.wifi, value.toPlatform())) as Boolean
|
||||
|
||||
private val cancelLocalOnlyHotspotRequest by lazy {
|
||||
WifiManager::class.java.getDeclaredMethod("cancelLocalOnlyHotspotRequest")
|
||||
}
|
||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(app.wifi)
|
||||
fun cancelLocalOnlyHotspotRequest() = cancelLocalOnlyHotspotRequest(Services.wifi)
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
private val setWifiApEnabled by lazy {
|
||||
@@ -66,13 +62,13 @@ object WifiApManager {
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26, malfunctioning on API 25")
|
||||
fun start(wifiConfig: android.net.wifi.WifiConfiguration? = null) {
|
||||
app.wifi.isWifiEnabled = false
|
||||
app.wifi.setWifiApEnabled(wifiConfig, true)
|
||||
Services.wifi.isWifiEnabled = false
|
||||
Services.wifi.setWifiApEnabled(wifiConfig, true)
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
@Deprecated("Not usable since API 26")
|
||||
fun stop() {
|
||||
app.wifi.setWifiApEnabled(null, false)
|
||||
app.wifi.isWifiEnabled = true
|
||||
Services.wifi.setWifiApEnabled(null, false)
|
||||
Services.wifi.isWifiEnabled = true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.core.content.getSystemService
|
||||
import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import be.mygod.vpnhotspot.App.Companion.app
|
||||
import be.mygod.vpnhotspot.util.Services
|
||||
|
||||
/**
|
||||
* This mechanism is used to maximize profit. Source: https://stackoverflow.com/a/29657230/2245107
|
||||
@@ -91,7 +92,7 @@ class WifiDoubleLock(lockType: Int) : AutoCloseable {
|
||||
override fun onDestroy(owner: LifecycleOwner) = app.pref.unregisterOnSharedPreferenceChangeListener(this)
|
||||
}
|
||||
|
||||
private val wifi = app.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
||||
private val wifi = Services.wifi.createWifiLock(lockType, "vpnhotspot:wifi").apply { acquire() }
|
||||
@SuppressLint("WakelockTimeout")
|
||||
private val power = service.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "vpnhotspot:power").apply { acquire() }
|
||||
|
||||
|
||||
Reference in New Issue
Block a user