diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt index 945215fe..f5e6196c 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/RepeaterFragment.kt @@ -73,15 +73,14 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL val statusListener = broadcastReceiver { _, _ -> onStatusChanged() } } - inner class Client(p2p: WifiP2pDevice? = null, - private val pair: Map.Entry? = null) { - val iface = pair?.key?.dev ?: p2pInterface!! - val mac = pair?.key?.lladdr ?: p2p!!.deviceAddress - val ip = pair?.key?.ip + inner class Client(p2p: WifiP2pDevice? = null, private val neighbour: IpNeighbour? = null) { + private val iface = neighbour?.dev ?: p2pInterface!! + val mac = neighbour?.lladdr ?: p2p!!.deviceAddress!! + val ip = neighbour?.ip val icon get() = TetherType.ofInterface(iface, p2pInterface).icon val title: CharSequence get() = listOf(ip, mac).filter { !it.isNullOrEmpty() }.joinToString(", ") - val description: CharSequence get() = when (pair?.value) { + val description: CharSequence get() = when (neighbour?.state) { IpNeighbour.State.INCOMPLETE, null -> "Connecting to $iface" IpNeighbour.State.VALID -> "Connected to $iface" IpNeighbour.State.VALID_DELAY -> "Connected to $iface (losing)" @@ -93,16 +92,16 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL private inner class ClientAdapter : RecyclerView.Adapter() { private val clients = ArrayList() var p2p: Collection = emptyList() - var neighbours = emptyMap() + var neighbours = emptyList() fun recreate() { clients.clear() val map = HashMap(p2p.associateBy { it.deviceAddress }) val tethered = (tetheredInterfaces + p2pInterface).filterNotNull() - for (pair in neighbours) { - val client = map.remove(pair.key.lladdr) - if (client != null) clients.add(Client(client, pair)) - else if (tethered.contains(pair.key.dev)) clients.add(Client(pair = pair)) + for (neighbour in neighbours) { + val client = map.remove(neighbour.lladdr) + if (client != null) clients.add(Client(client, neighbour)) + else if (tethered.contains(neighbour.dev)) clients.add(Client(neighbour = neighbour)) } clients.addAll(map.map { Client(it.value) }) clients.sortWith(compareBy { it.ip }.thenBy { it.mac }) @@ -210,8 +209,8 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL else -> false } - override fun onIpNeighbourAvailable(neighbours: Map) { - adapter.neighbours = neighbours.toMap() + override fun onIpNeighbourAvailable(neighbours: Map) { + adapter.neighbours = neighbours.values.toList() } override fun postIpNeighbourAvailable() = adapter.recreate() } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt index 5073bb17..7b8c5a62 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbour.kt @@ -1,9 +1,8 @@ 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) { +data class IpNeighbour(val ip: String, val dev: String, val lladdr: String, val state: State) { enum class State { INCOMPLETE, VALID, VALID_DELAY, FAILED, DELETING } @@ -15,29 +14,36 @@ data class IpNeighbour(val ip: String, val dev: String, val lladdr: String) { * 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 + * Assumptions: IP addr (key) always present, 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? { + "^(Deleted )?(.+?) (dev (.+?) )?(lladdr (.+?))?( proxy)?( ([INCOMPLET,RAHBSDYF]+))?\$".toRegex() + fun parse(line: String): IpNeighbour? { 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]) { + val ip = match.groupValues[2] + val dev = match.groupValues[4] + var lladdr = match.groupValues[6] + // use ARP as fallback + if (dev.isNotBlank() && lladdr.isBlank()) lladdr = (NetUtils.arp() + .filter { it[NetUtils.ARP_IP_ADDRESS] == ip && it[NetUtils.ARP_DEVICE] == dev } + .map { it[NetUtils.ARP_HW_ADDRESS] } + .singleOrNull() ?: "") + val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[9]) { "", "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]}") + Log.w(TAG, "Unknown state encountered: ${match.groupValues[9]}") return null } } - return Pair(neighbour, state) + return IpNeighbour(ip, dev, lladdr, state) } } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt index 16e5d08f..c61ee471 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/IpNeighbourMonitor.kt @@ -41,13 +41,13 @@ class IpNeighbourMonitor private constructor() { } interface Callback { - fun onIpNeighbourAvailable(neighbours: Map) + fun onIpNeighbourAvailable(neighbours: Map) fun postIpNeighbourAvailable() { } } private val handler = Handler() private var updatePosted = false - val neighbours = HashMap() + val neighbours = HashMap() /** * Using monitor requires using /proc/self/ns/net which would be problematic on Android 6.0+. * @@ -72,9 +72,10 @@ class IpNeighbourMonitor private constructor() { 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 + val neighbour = IpNeighbour.parse(it) ?: return@forEachLine + val changed = if (neighbour.state == IpNeighbour.State.DELETING) + neighbours.remove(neighbour.ip) != null + else neighbours.put(neighbour.ip, neighbour) != neighbour if (changed) postUpdateLocked() } } @@ -94,7 +95,7 @@ class IpNeighbourMonitor private constructor() { process.inputStream.bufferedReader().useLines { synchronized(neighbours) { neighbours.clear() - neighbours.putAll(it.map(IpNeighbour.Companion::parse).filterNotNull().toMap()) + neighbours.putAll(it.map(IpNeighbour.Companion::parse).filterNotNull().associateBy { it.ip }) postUpdateLocked() } } diff --git a/mobile/src/main/java/be/mygod/vpnhotspot/net/NetUtils.kt b/mobile/src/main/java/be/mygod/vpnhotspot/net/NetUtils.kt index 6e536514..2e0ff423 100644 --- a/mobile/src/main/java/be/mygod/vpnhotspot/net/NetUtils.kt +++ b/mobile/src/main/java/be/mygod/vpnhotspot/net/NetUtils.kt @@ -4,6 +4,7 @@ import android.os.Build import android.os.Bundle import android.support.annotation.RequiresApi import java.io.File +import java.io.IOException object NetUtils { // hidden constants from ConnectivityManager @@ -27,12 +28,24 @@ object NetUtils { extras.getStringArrayList(EXTRA_ACTIVE_TETHER).toSet() + extras.getStringArrayList(EXTRA_ACTIVE_LOCAL_ONLY) else extras.getStringArrayList(EXTRA_ACTIVE_TETHER_LEGACY).toSet() - fun arp(iface: String? = null) = File("/proc/net/arp").bufferedReader().useLines { - // IP address HW type Flags HW address Mask Device - it.map { it.split(spaces) } - .drop(1) - .filter { it.size >= 4 && (iface == null || it.getOrNull(5) == iface) && - mac.matcher(it[3]).matches() } - .associateBy({ it[3] }, { it[0] }) + // IP address HW type Flags HW address Mask Device + const val ARP_IP_ADDRESS = 0 + const val ARP_HW_ADDRESS = 3 + const val ARP_DEVICE = 5 + private const val ARP_CACHE_EXPIRE = 1L * 1000 * 1000 * 1000 + private var arpCache = emptyList>() + private var arpCacheTime = -ARP_CACHE_EXPIRE + fun arp(): List> { + if (System.nanoTime() - arpCacheTime >= ARP_CACHE_EXPIRE) try { + arpCache = File("/proc/net/arp").bufferedReader().useLines { + it.map { it.split(spaces) } + .drop(1) + .filter { it.size >= 6 && mac.matcher(it[ARP_HW_ADDRESS]).matches() } + .toList() + } + } catch (e: IOException) { + e.printStackTrace() + } + return arpCache } }