Support showing connected devices from system tethering
Also fixes IP address not showing up.
This commit is contained in:
43
mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
Normal file
43
mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.debugLog
|
||||
|
||||
data class IpNeighbour(val ip: String, val dev: String, val lladdr: String) {
|
||||
enum class State {
|
||||
INCOMPLETE, VALID, VALID_DELAY, FAILED, DELETING
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "IpNeighbour"
|
||||
|
||||
/**
|
||||
* Parser based on:
|
||||
* https://android.googlesource.com/platform/external/iproute2/+/ad0a6a2/ip/ipneigh.c#194
|
||||
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
|
||||
* Assumptions: IPv4 only, RTM_GETNEIGH is never used and show_stats = 0
|
||||
*/
|
||||
private val parser =
|
||||
"^(Deleted )?((.+?) )?(dev (.+?) )?(lladdr (.+?))?( proxy)?( ([INCOMPLET,RAHBSDYF]+))?\$".toRegex()
|
||||
fun parse(line: String): Pair<IpNeighbour, State>? {
|
||||
val match = parser.matchEntire(line)
|
||||
if (match == null) {
|
||||
if (!line.isBlank()) Log.w(TAG, line)
|
||||
return null
|
||||
}
|
||||
val neighbour = IpNeighbour(match.groupValues[3], match.groupValues[5], match.groupValues[7])
|
||||
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[10]) {
|
||||
"", "INCOMPLETE" -> State.INCOMPLETE
|
||||
"REACHABLE", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
||||
"DELAY" -> State.VALID_DELAY
|
||||
"FAILED" -> State.FAILED
|
||||
"NOARP" -> return null // skip
|
||||
else -> {
|
||||
Log.w(TAG, "Unknown state encountered: ${match.groupValues[10]}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
return Pair(neighbour, state)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.os.Build
|
||||
import android.os.Handler
|
||||
import android.util.Log
|
||||
import be.mygod.vpnhotspot.debugLog
|
||||
import java.io.InterruptedIOException
|
||||
|
||||
class IpNeighbourMonitor private constructor() {
|
||||
companion object {
|
||||
private const val TAG = "IpNeighbourMonitor"
|
||||
private val callbacks = HashSet<Callback>()
|
||||
var instance: IpNeighbourMonitor? = null
|
||||
|
||||
fun registerCallback(callback: Callback) {
|
||||
if (!callbacks.add(callback)) return
|
||||
var monitor = instance
|
||||
if (monitor == null) {
|
||||
monitor = IpNeighbourMonitor()
|
||||
instance = monitor
|
||||
monitor.flush()
|
||||
} else synchronized(monitor.neighbours) { callback.onIpNeighbourAvailable(monitor.neighbours) }
|
||||
}
|
||||
fun unregisterCallback(callback: Callback) {
|
||||
if (!callbacks.remove(callback) || callbacks.isNotEmpty()) return
|
||||
val monitor = instance ?: return
|
||||
instance = null
|
||||
monitor.monitor?.destroy()
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper for kotlin.concurrent.thread that silences uncaught exceptions.
|
||||
*/
|
||||
private fun thread(name: String? = null, start: Boolean = true, isDaemon: Boolean = false,
|
||||
contextClassLoader: ClassLoader? = null, priority: Int = -1, block: () -> Unit): Thread {
|
||||
val thread = kotlin.concurrent.thread(false, isDaemon, contextClassLoader, name, priority, block)
|
||||
thread.setUncaughtExceptionHandler { _, _ -> }
|
||||
if (start) thread.start()
|
||||
return thread
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
fun onIpNeighbourAvailable(neighbours: Map<IpNeighbour, IpNeighbour.State>)
|
||||
fun postIpNeighbourAvailable() { }
|
||||
}
|
||||
|
||||
private val handler = Handler()
|
||||
private var updatePosted = false
|
||||
val neighbours = HashMap<IpNeighbour, IpNeighbour.State>()
|
||||
/**
|
||||
* Using monitor requires using /proc/self/ns/net which would be problematic on Android 6.0+.
|
||||
*
|
||||
* Source: https://source.android.com/security/enhancements/enhancements60
|
||||
*/
|
||||
private var monitor: Process? = null
|
||||
|
||||
init {
|
||||
thread(name = TAG + "-input") {
|
||||
val monitor = (if (Build.VERSION.SDK_INT >= 23)
|
||||
ProcessBuilder("su", "-c", "ip", "-4", "monitor", "neigh") else
|
||||
ProcessBuilder("ip", "-4", "monitor", "neigh"))
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
this.monitor = monitor
|
||||
thread(name = TAG + "-error") {
|
||||
try {
|
||||
monitor.errorStream.bufferedReader().forEachLine { Log.e(TAG, it) }
|
||||
} catch (ignore: InterruptedIOException) { }
|
||||
}
|
||||
try {
|
||||
monitor.inputStream.bufferedReader().forEachLine {
|
||||
debugLog(TAG, it)
|
||||
synchronized(neighbours) {
|
||||
val (neighbour, state) = IpNeighbour.parse(it) ?: return@forEachLine
|
||||
val changed = if (state == IpNeighbour.State.DELETING) neighbours.remove(neighbour) != null else
|
||||
neighbours.put(neighbour, state) != state
|
||||
if (changed) postUpdateLocked()
|
||||
}
|
||||
}
|
||||
Log.w(TAG, if (Build.VERSION.SDK_INT >= 26 && monitor.isAlive) "monitor closed stdout" else
|
||||
"monitor died unexpectedly")
|
||||
} catch (ignore: InterruptedIOException) { }
|
||||
}
|
||||
}
|
||||
|
||||
fun flush() = thread(name = TAG + "-flush") {
|
||||
val process = ProcessBuilder("ip", "-4", "neigh")
|
||||
.redirectErrorStream(true)
|
||||
.start()
|
||||
process.waitFor()
|
||||
val err = process.errorStream.bufferedReader().readText()
|
||||
if (err.isNotBlank()) Log.e(TAG, err)
|
||||
process.inputStream.bufferedReader().useLines {
|
||||
synchronized(neighbours) {
|
||||
neighbours.clear()
|
||||
neighbours.putAll(it.map(IpNeighbour.Companion::parse).filterNotNull().toMap())
|
||||
postUpdateLocked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun postUpdateLocked() {
|
||||
if (updatePosted || instance != this) return
|
||||
handler.post {
|
||||
synchronized(neighbours) {
|
||||
for (callback in callbacks) callback.onIpNeighbourAvailable(neighbours)
|
||||
updatePosted = false
|
||||
}
|
||||
for (callback in callbacks) callback.postIpNeighbourAvailable()
|
||||
}
|
||||
updatePosted = true
|
||||
}
|
||||
}
|
||||
43
mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
Normal file
43
mobile/src/main/java/be/mygod/vpnhotspot/net/TetherType.kt
Normal file
@@ -0,0 +1,43 @@
|
||||
package be.mygod.vpnhotspot.net
|
||||
|
||||
import android.content.res.Resources
|
||||
import be.mygod.vpnhotspot.App
|
||||
import be.mygod.vpnhotspot.R
|
||||
|
||||
enum class TetherType {
|
||||
NONE, WIFI_P2P, USB, WIFI, WIMAX, BLUETOOTH;
|
||||
|
||||
val icon get() = when (this) {
|
||||
USB -> R.drawable.ic_device_usb
|
||||
WIFI_P2P, WIFI, WIMAX -> R.drawable.ic_device_network_wifi
|
||||
BLUETOOTH -> R.drawable.ic_device_bluetooth
|
||||
else -> R.drawable.ic_device_wifi_tethering
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* Source: https://android.googlesource.com/platform/frameworks/base/+/61fa313/core/res/res/values/config.xml#328
|
||||
*/
|
||||
private val usbRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_usb_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val wifiRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_wifi_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val wimaxRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_wimax_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
private val bluetoothRegexes = App.app.resources.getStringArray(Resources.getSystem()
|
||||
.getIdentifier("config_tether_bluetooth_regexs", "array", "android"))
|
||||
.map { it.toPattern() }
|
||||
|
||||
fun ofInterface(iface: String, p2pDev: String? = null) = when {
|
||||
iface == p2pDev -> WIFI_P2P
|
||||
usbRegexes.any { it.matcher(iface).matches() } -> USB
|
||||
wifiRegexes.any { it.matcher(iface).matches() } -> WIFI
|
||||
wimaxRegexes.any { it.matcher(iface).matches() } -> WIMAX
|
||||
bluetoothRegexes.any { it.matcher(iface).matches() } -> BLUETOOTH
|
||||
else -> NONE
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user