Support showing connected devices from system tethering

Also fixes IP address not showing up.
This commit is contained in:
Mygod
2018-01-20 22:52:54 -08:00
parent 6bffe54e58
commit 0660a20fcb
11 changed files with 324 additions and 107 deletions

View 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)
}
}
}

View File

@@ -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
}
}

View 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
}
}
}