Add IPv6 addresses to connected devices

This commit is contained in:
Mygod
2018-02-11 01:08:59 -08:00
parent bd34491ad4
commit d70acfa29e
6 changed files with 57 additions and 41 deletions

View File

@@ -29,6 +29,7 @@ import be.mygod.vpnhotspot.net.IpNeighbourMonitor
import be.mygod.vpnhotspot.net.ConnectivityManagerHelper
import be.mygod.vpnhotspot.net.TetherType
import java.net.NetworkInterface
import java.util.*
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener, IpNeighbourMonitor.Callback {
inner class Data : BaseObservable() {
@@ -79,19 +80,21 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
}
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
inner class Client(p2p: WifiP2pDevice? = null, neighbour: IpNeighbour? = null) {
val iface = neighbour?.dev ?: p2pInterface!!
val mac = p2p?.deviceAddress ?: neighbour?.lladdr!!
val ip = TreeMap<String, IpNeighbour.State>()
val icon get() = TetherType.ofInterface(iface, p2pInterface).icon
val title get() = listOf(ip, mac).filter { !it.isNullOrEmpty() }.joinToString("\t\t")
val description get() = getString(when (neighbour?.state) {
IpNeighbour.State.INCOMPLETE, null -> R.string.connected_state_incomplete
IpNeighbour.State.VALID -> R.string.connected_state_valid
IpNeighbour.State.FAILED -> R.string.connected_state_failed
else -> throw IllegalStateException("Invalid IpNeighbour.State")
}, iface)
val title get() = "$mac%$iface"
val description get() = ip.entries.joinToString("\n") { (ip, state) ->
getString(when (state) {
IpNeighbour.State.INCOMPLETE -> R.string.connected_state_incomplete
IpNeighbour.State.VALID -> R.string.connected_state_valid
IpNeighbour.State.FAILED -> R.string.connected_state_failed
else -> throw IllegalStateException("Invalid IpNeighbour.State: $state")
}, ip)
}
}
private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
private inner class ClientAdapter : RecyclerView.Adapter<ClientViewHolder>() {
@@ -100,15 +103,20 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
var neighbours = emptyList<IpNeighbour>()
fun recreate() {
val p2p = HashMap(p2p.associateBy({ Pair(p2pInterface, it.deviceAddress) }, { Client(it) }))
for (neighbour in neighbours) {
val key = Pair(neighbour.dev, neighbour.lladdr)
var client = p2p[key]
if (client == null) {
if (!tetheredInterfaces.contains(neighbour.dev)) continue
client = Client(neighbour = neighbour)
p2p[key] = client
}
client.ip += Pair(neighbour.ip, neighbour.state)
}
clients.clear()
val p2p = HashMap(p2p.associateBy { it.deviceAddress })
for (neighbour in neighbours)
if (neighbour.dev == p2pInterface) {
val client = p2p.remove(neighbour.lladdr)
if (client != null) clients.add(Client(client, neighbour))
} else if (tetheredInterfaces.contains(neighbour.dev)) clients.add(Client(neighbour = neighbour))
clients.addAll(p2p.map { Client(it.value) })
clients.sortWith(compareBy<Client> { it.ip }.thenBy { it.mac })
clients.addAll(p2p.values)
clients.sortWith(compareBy<Client> { it.iface }.thenBy { it.mac })
notifyDataSetChanged() // recreate everything
binding.swipeRefresher.isRefreshing = false
}

View File

@@ -101,7 +101,10 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
}
override fun postIpNeighbourAvailable() {
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
neighbours.count { it.state != IpNeighbour.State.FAILED }
neighbours
.filter { it.state != IpNeighbour.State.FAILED }
.distinctBy { it.lladdr }
.size
}
ServiceNotification.startForeground(this, routings.keys.associate { Pair(it, sizeLookup[it] ?: 0) })
}

View File

@@ -16,10 +16,10 @@ data class IpNeighbour(val ip: String, val dev: String, val lladdr: String, val
* 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: IP addr (key) always present, IPv4 only, RTM_GETNEIGH is never used and show_stats = 0
* Assumptions: IP addr (key) always present, RTM_GETNEIGH is never used and show_stats = 0
*/
private val parser =
"^(Deleted )?(.+?) (dev (.+?) )?(lladdr (.+?))?( proxy)?( ([INCOMPLET,RAHBSDYF]+))?\$".toRegex()
private val parser = ("^(Deleted )?(.+?) (dev (.+?) )?(lladdr (.+?))?( router)?( proxy)?" +
"( ([INCOMPLET,RAHBSDYF]+))?\$").toRegex()
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
fun parse(line: String): IpNeighbour? {
@@ -36,16 +36,17 @@ data class IpNeighbour(val ip: String, val dev: String, val lladdr: String, val
.filter { it[ARP_IP_ADDRESS] == ip && it[ARP_DEVICE] == dev }
.map { it[ARP_HW_ADDRESS] }
.singleOrNull() ?: "")
val state = if (match.groupValues[1].isNotEmpty()) State.DELETING else when (match.groupValues[9]) {
"", "INCOMPLETE" -> State.INCOMPLETE
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
"FAILED" -> State.FAILED
"NOARP" -> return null // skip
else -> {
Log.w(TAG, "Unknown state encountered: ${match.groupValues[9]}")
return null
val state = if (match.groupValues[1].isNotEmpty() || lladdr.isEmpty()) State.DELETING else
when (match.groupValues[10]) {
"", "INCOMPLETE" -> State.INCOMPLETE
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
"FAILED" -> State.FAILED
"NOARP" -> return null // skip
else -> {
Log.w(TAG, "Unknown state encountered: ${match.groupValues[10]}")
return null
}
}
}
return IpNeighbour(ip, dev, lladdr, state)
}

View File

@@ -58,7 +58,7 @@ class IpNeighbourMonitor private constructor() {
init {
thread(name = TAG + "-input") {
// monitor may get rejected by SELinux
val monitor = ProcessBuilder("sh", "-c", "ip -4 monitor neigh || su -c ip -4 monitor neigh")
val monitor = ProcessBuilder("sh", "-c", "ip monitor neigh || su -c ip monitor neigh")
.redirectErrorStream(true)
.start()
this.monitor = monitor
@@ -85,7 +85,7 @@ class IpNeighbourMonitor private constructor() {
}
fun flush() = thread(name = TAG + "-flush") {
val process = ProcessBuilder("ip", "-4", "neigh")
val process = ProcessBuilder("ip", "neigh")
.redirectErrorStream(true)
.start()
process.waitFor()
@@ -94,7 +94,11 @@ class IpNeighbourMonitor private constructor() {
process.inputStream.bufferedReader().useLines {
synchronized(neighbours) {
neighbours.clear()
neighbours.putAll(it.map(IpNeighbour.Companion::parse).filterNotNull().associateBy { it.ip })
neighbours.putAll(it
.map(IpNeighbour.Companion::parse)
.filterNotNull()
.filter { it.state != IpNeighbour.State.DELETING } // skip entries without lladdr
.associateBy { it.ip })
postUpdateLocked()
}
}

View File

@@ -36,9 +36,9 @@
<string name="tethering_manage">管理…</string>
<string name="connected_devices">已连接设备</string>
<string name="connected_state_incomplete">正在通过 %s 连接</string>
<string name="connected_state_valid">已连接到 %s</string>
<string name="connected_state_failed">已从 %s 断开</string>
<string name="connected_state_incomplete">%s (正在连接)</string>
<string name="connected_state_valid">%s (已连上)</string>
<string name="connected_state_failed">%s (已断开)</string>
<string name="settings_service">服务</string>
<string name="settings_service_dns">下游 DNS 服务器[:端口]</string>

View File

@@ -38,9 +38,9 @@
<string name="tethering_manage">Manage…</string>
<string name="connected_devices">Connected devices</string>
<string name="connected_state_incomplete">Connecting to %s</string>
<string name="connected_state_valid">Connected to %s</string>
<string name="connected_state_failed">Lost from %s</string>
<string name="connected_state_incomplete">%s (connecting)</string>
<string name="connected_state_valid">%s (reachable)</string>
<string name="connected_state_failed">%s (lost)</string>
<string name="settings_service">Service</string>
<string name="settings_service_dns">Downstream DNS server[:port]</string>