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.ConnectivityManagerHelper
|
||||||
import be.mygod.vpnhotspot.net.TetherType
|
import be.mygod.vpnhotspot.net.TetherType
|
||||||
import java.net.NetworkInterface
|
import java.net.NetworkInterface
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener, IpNeighbourMonitor.Callback {
|
class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickListener, IpNeighbourMonitor.Callback {
|
||||||
inner class Data : BaseObservable() {
|
inner class Data : BaseObservable() {
|
||||||
@@ -79,19 +80,21 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
|||||||
val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
|
val statusListener = broadcastReceiver { _, _ -> onStatusChanged() }
|
||||||
}
|
}
|
||||||
|
|
||||||
inner class Client(p2p: WifiP2pDevice? = null, private val neighbour: IpNeighbour? = null) {
|
inner class Client(p2p: WifiP2pDevice? = null, neighbour: IpNeighbour? = null) {
|
||||||
private val iface = neighbour?.dev ?: p2pInterface!!
|
val iface = neighbour?.dev ?: p2pInterface!!
|
||||||
val mac = neighbour?.lladdr ?: p2p!!.deviceAddress!!
|
val mac = p2p?.deviceAddress ?: neighbour?.lladdr!!
|
||||||
val ip = neighbour?.ip
|
val ip = TreeMap<String, IpNeighbour.State>()
|
||||||
|
|
||||||
val icon get() = TetherType.ofInterface(iface, p2pInterface).icon
|
val icon get() = TetherType.ofInterface(iface, p2pInterface).icon
|
||||||
val title get() = listOf(ip, mac).filter { !it.isNullOrEmpty() }.joinToString("\t\t")
|
val title get() = "$mac%$iface"
|
||||||
val description get() = getString(when (neighbour?.state) {
|
val description get() = ip.entries.joinToString("\n") { (ip, state) ->
|
||||||
IpNeighbour.State.INCOMPLETE, null -> R.string.connected_state_incomplete
|
getString(when (state) {
|
||||||
|
IpNeighbour.State.INCOMPLETE -> R.string.connected_state_incomplete
|
||||||
IpNeighbour.State.VALID -> R.string.connected_state_valid
|
IpNeighbour.State.VALID -> R.string.connected_state_valid
|
||||||
IpNeighbour.State.FAILED -> R.string.connected_state_failed
|
IpNeighbour.State.FAILED -> R.string.connected_state_failed
|
||||||
else -> throw IllegalStateException("Invalid IpNeighbour.State")
|
else -> throw IllegalStateException("Invalid IpNeighbour.State: $state")
|
||||||
}, iface)
|
}, ip)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
|
private class ClientViewHolder(val binding: ListitemClientBinding) : RecyclerView.ViewHolder(binding.root)
|
||||||
private inner class ClientAdapter : RecyclerView.Adapter<ClientViewHolder>() {
|
private inner class ClientAdapter : RecyclerView.Adapter<ClientViewHolder>() {
|
||||||
@@ -100,15 +103,20 @@ class RepeaterFragment : Fragment(), ServiceConnection, Toolbar.OnMenuItemClickL
|
|||||||
var neighbours = emptyList<IpNeighbour>()
|
var neighbours = emptyList<IpNeighbour>()
|
||||||
|
|
||||||
fun recreate() {
|
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()
|
clients.clear()
|
||||||
val p2p = HashMap(p2p.associateBy { it.deviceAddress })
|
clients.addAll(p2p.values)
|
||||||
for (neighbour in neighbours)
|
clients.sortWith(compareBy<Client> { it.iface }.thenBy { it.mac })
|
||||||
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 })
|
|
||||||
notifyDataSetChanged() // recreate everything
|
notifyDataSetChanged() // recreate everything
|
||||||
binding.swipeRefresher.isRefreshing = false
|
binding.swipeRefresher.isRefreshing = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -101,7 +101,10 @@ class TetheringService : Service(), VpnMonitor.Callback, IpNeighbourMonitor.Call
|
|||||||
}
|
}
|
||||||
override fun postIpNeighbourAvailable() {
|
override fun postIpNeighbourAvailable() {
|
||||||
val sizeLookup = neighbours.groupBy { it.dev }.mapValues { (_, neighbours) ->
|
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) })
|
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:
|
* Parser based on:
|
||||||
* https://android.googlesource.com/platform/external/iproute2/+/ad0a6a2/ip/ipneigh.c#194
|
* https://android.googlesource.com/platform/external/iproute2/+/ad0a6a2/ip/ipneigh.c#194
|
||||||
* https://people.cs.clemson.edu/~westall/853/notes/arpstate.pdf
|
* 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 =
|
private val parser = ("^(Deleted )?(.+?) (dev (.+?) )?(lladdr (.+?))?( router)?( proxy)?" +
|
||||||
"^(Deleted )?(.+?) (dev (.+?) )?(lladdr (.+?))?( proxy)?( ([INCOMPLET,RAHBSDYF]+))?\$".toRegex()
|
"( ([INCOMPLET,RAHBSDYF]+))?\$").toRegex()
|
||||||
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
|
private fun checkLladdrNotLoopback(lladdr: String) = if (lladdr == "00:00:00:00:00:00") "" else lladdr
|
||||||
|
|
||||||
fun parse(line: String): IpNeighbour? {
|
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 }
|
.filter { it[ARP_IP_ADDRESS] == ip && it[ARP_DEVICE] == dev }
|
||||||
.map { it[ARP_HW_ADDRESS] }
|
.map { it[ARP_HW_ADDRESS] }
|
||||||
.singleOrNull() ?: "")
|
.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
|
"", "INCOMPLETE" -> State.INCOMPLETE
|
||||||
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
"REACHABLE", "DELAY", "STALE", "PROBE", "PERMANENT" -> State.VALID
|
||||||
"FAILED" -> State.FAILED
|
"FAILED" -> State.FAILED
|
||||||
"NOARP" -> return null // skip
|
"NOARP" -> return null // skip
|
||||||
else -> {
|
else -> {
|
||||||
Log.w(TAG, "Unknown state encountered: ${match.groupValues[9]}")
|
Log.w(TAG, "Unknown state encountered: ${match.groupValues[10]}")
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ class IpNeighbourMonitor private constructor() {
|
|||||||
init {
|
init {
|
||||||
thread(name = TAG + "-input") {
|
thread(name = TAG + "-input") {
|
||||||
// monitor may get rejected by SELinux
|
// 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)
|
.redirectErrorStream(true)
|
||||||
.start()
|
.start()
|
||||||
this.monitor = monitor
|
this.monitor = monitor
|
||||||
@@ -85,7 +85,7 @@ class IpNeighbourMonitor private constructor() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun flush() = thread(name = TAG + "-flush") {
|
fun flush() = thread(name = TAG + "-flush") {
|
||||||
val process = ProcessBuilder("ip", "-4", "neigh")
|
val process = ProcessBuilder("ip", "neigh")
|
||||||
.redirectErrorStream(true)
|
.redirectErrorStream(true)
|
||||||
.start()
|
.start()
|
||||||
process.waitFor()
|
process.waitFor()
|
||||||
@@ -94,7 +94,11 @@ class IpNeighbourMonitor private constructor() {
|
|||||||
process.inputStream.bufferedReader().useLines {
|
process.inputStream.bufferedReader().useLines {
|
||||||
synchronized(neighbours) {
|
synchronized(neighbours) {
|
||||||
neighbours.clear()
|
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()
|
postUpdateLocked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,9 +36,9 @@
|
|||||||
<string name="tethering_manage">管理…</string>
|
<string name="tethering_manage">管理…</string>
|
||||||
|
|
||||||
<string name="connected_devices">已连接设备</string>
|
<string name="connected_devices">已连接设备</string>
|
||||||
<string name="connected_state_incomplete">正在通过 %s 连接</string>
|
<string name="connected_state_incomplete">%s (正在连接)</string>
|
||||||
<string name="connected_state_valid">已连接到 %s</string>
|
<string name="connected_state_valid">%s (已连上)</string>
|
||||||
<string name="connected_state_failed">已从 %s 断开</string>
|
<string name="connected_state_failed">%s (已断开)</string>
|
||||||
|
|
||||||
<string name="settings_service">服务</string>
|
<string name="settings_service">服务</string>
|
||||||
<string name="settings_service_dns">下游 DNS 服务器[:端口]</string>
|
<string name="settings_service_dns">下游 DNS 服务器[:端口]</string>
|
||||||
|
|||||||
@@ -38,9 +38,9 @@
|
|||||||
<string name="tethering_manage">Manage…</string>
|
<string name="tethering_manage">Manage…</string>
|
||||||
|
|
||||||
<string name="connected_devices">Connected devices</string>
|
<string name="connected_devices">Connected devices</string>
|
||||||
<string name="connected_state_incomplete">Connecting to %s</string>
|
<string name="connected_state_incomplete">%s (connecting)</string>
|
||||||
<string name="connected_state_valid">Connected to %s</string>
|
<string name="connected_state_valid">%s (reachable)</string>
|
||||||
<string name="connected_state_failed">Lost from %s</string>
|
<string name="connected_state_failed">%s (lost)</string>
|
||||||
|
|
||||||
<string name="settings_service">Service</string>
|
<string name="settings_service">Service</string>
|
||||||
<string name="settings_service_dns">Downstream DNS server[:port]</string>
|
<string name="settings_service_dns">Downstream DNS server[:port]</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user