Add IPv6 addresses to connected devices
This commit is contained in:
@@ -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
|
||||
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")
|
||||
}, iface)
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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) })
|
||||
}
|
||||
|
||||
@@ -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,13 +36,14 @@ 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]) {
|
||||
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[9]}")
|
||||
Log.w(TAG, "Unknown state encountered: ${match.groupValues[10]}")
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user