Add support for MAC and hostname rule items

This commit is contained in:
世界
2026-03-05 00:16:03 +08:00
parent b3b09454c0
commit 696976c4a0
7 changed files with 319 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
package io.nekohasekai.sfa.bg;
import io.nekohasekai.sfa.bg.ParceledListSlice;
interface INeighborTableCallback {
oneway void onNeighborTableUpdated(in ParceledListSlice entries);
}

View File

@@ -1,6 +1,7 @@
package io.nekohasekai.sfa.bg;
import android.os.ParcelFileDescriptor;
import io.nekohasekai.sfa.bg.INeighborTableCallback;
import io.nekohasekai.sfa.bg.ParceledListSlice;
interface IRootService {
@@ -11,4 +12,8 @@ interface IRootService {
void installPackage(in ParcelFileDescriptor apk, long size, int userId) = 2;
String exportDebugInfo(String outputPath) = 3;
void registerNeighborTableCallback(in INeighborTableCallback callback) = 4;
oneway void unregisterNeighborTableCallback(in INeighborTableCallback callback) = 5;
}

View File

@@ -0,0 +1,3 @@
package io.nekohasekai.sfa.bg;
parcelable NeighborEntry;

View File

@@ -0,0 +1,49 @@
package io.nekohasekai.sfa.bg;
import android.os.Parcel;
import android.os.Parcelable;
import androidx.annotation.NonNull;
public class NeighborEntry implements Parcelable {
@NonNull public final String address;
@NonNull public final String macAddress;
@NonNull public final String hostname;
public NeighborEntry(
@NonNull String address, @NonNull String macAddress, @NonNull String hostname) {
this.address = address;
this.macAddress = macAddress;
this.hostname = hostname;
}
protected NeighborEntry(Parcel in) {
address = in.readString();
macAddress = in.readString();
hostname = in.readString();
}
@Override
public void writeToParcel(@NonNull Parcel dest, int flags) {
dest.writeString(address);
dest.writeString(macAddress);
dest.writeString(hostname);
}
@Override
public int describeContents() {
return 0;
}
public static final Creator<NeighborEntry> CREATOR =
new Creator<>() {
@Override
public NeighborEntry createFromParcel(Parcel in) {
return new NeighborEntry(in);
}
@Override
public NeighborEntry[] newArray(int size) {
return new NeighborEntry[size];
}
};
}

View File

@@ -11,12 +11,16 @@ import io.nekohasekai.libbox.ConnectionOwner
import io.nekohasekai.libbox.InterfaceUpdateListener
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.LocalDNSTransport
import io.nekohasekai.libbox.NeighborEntryIterator
import io.nekohasekai.libbox.NeighborUpdateListener
import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.StringIterator
import io.nekohasekai.libbox.TunOptions
import io.nekohasekai.libbox.WIFIState
import io.nekohasekai.sfa.Application
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import java.net.Inet6Address
import java.net.InetSocketAddress
import java.net.InterfaceAddress
@@ -24,8 +28,11 @@ import java.net.NetworkInterface
import java.security.KeyStore
import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi
import io.nekohasekai.libbox.NeighborEntry as LibboxNeighborEntry
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
private var neighborCallback: INeighborTableCallback.Stub? = null
interface PlatformInterfaceWrapper : PlatformInterface {
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
@@ -172,6 +179,49 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return StringArray(certificates.iterator())
}
override fun startNeighborMonitor(listener: NeighborUpdateListener?) {
if (listener == null) return
val callback = object : INeighborTableCallback.Stub() {
override fun onNeighborTableUpdated(entries: ParceledListSlice<*>?) {
if (entries == null) return
@Suppress("UNCHECKED_CAST")
val list = entries.list as List<NeighborEntry>
listener.updateNeighborTable(
NeighborEntryArray(
list.map { entry ->
LibboxNeighborEntry().apply {
address = entry.address
macAddress = entry.macAddress
hostname = entry.hostname
}
}.iterator(),
),
)
}
}
neighborCallback = callback
runBlocking(Dispatchers.IO) {
RootClient.registerNeighborTableCallback(callback)
}
}
override fun registerMyInterface(name: String?) {
}
override fun closeNeighborMonitor(listener: NeighborUpdateListener?) {
val callback = neighborCallback ?: return
neighborCallback = null
runBlocking(Dispatchers.IO) {
RootClient.unregisterNeighborTableCallback(callback)
}
}
private class NeighborEntryArray(private val iterator: Iterator<LibboxNeighborEntry>) : NeighborEntryIterator {
override fun hasNext(): Boolean = iterator.hasNext()
override fun next(): LibboxNeighborEntry = iterator.next()
}
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) : NetworkInterfaceIterator {
override fun hasNext(): Boolean = iterator.hasNext()

View File

@@ -133,4 +133,21 @@ object RootClient {
throw e.rethrowFromSystemServer()
}
}
suspend fun registerNeighborTableCallback(callback: INeighborTableCallback) {
val svc = bindService()
try {
svc.registerNeighborTableCallback(callback)
} catch (e: RemoteException) {
throw e.rethrowFromSystemServer()
}
}
suspend fun unregisterNeighborTableCallback(callback: INeighborTableCallback) {
try {
service?.unregisterNeighborTableCallback(callback)
} catch (e: RemoteException) {
throw e.rethrowFromSystemServer()
}
}
}

View File

@@ -2,15 +2,36 @@ package io.nekohasekai.sfa.bg
import android.content.Intent
import android.content.pm.PackageInfo
import android.os.Build
import android.os.IBinder
import android.os.ParcelFileDescriptor
import android.os.RemoteCallbackList
import android.util.Log
import com.topjohnwu.superuser.ipc.RootService
import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.NeighborEntryIterator
import io.nekohasekai.libbox.NeighborSubscription
import io.nekohasekai.libbox.NeighborUpdateListener
import io.nekohasekai.sfa.BuildConfig
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
import java.io.IOException
import java.lang.reflect.Proxy
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
class RootServer : RootService() {
private val neighborCallbacks = RemoteCallbackList<INeighborTableCallback>()
private var neighborSubscription: NeighborSubscription? = null
private val hostnameByMAC = ConcurrentHashMap<String, String>()
@Volatile
private var lastNeighborEntries: List<Pair<String, String>>? = null
private var tetheringCallback: Any? = null
private var tetheringManager: Any? = null
private val binder = object : IRootService.Stub() {
override fun destroy() {
stopSelf()
@@ -31,7 +52,174 @@ class RootServer : RootService() {
outputPath!!,
BuildConfig.APPLICATION_ID,
)
override fun registerNeighborTableCallback(callback: INeighborTableCallback?) {
if (callback == null) return
neighborCallbacks.register(callback)
synchronized(neighborCallbacks) {
if (neighborSubscription == null) {
try {
neighborSubscription =
Libbox.subscribeNeighborTable(object : NeighborUpdateListener {
override fun updateNeighborTable(entries: NeighborEntryIterator?) {
if (entries == null) return
val rawList = mutableListOf<Pair<String, String>>()
while (entries.hasNext()) {
val entry = entries.next()
rawList.add(entry.address to entry.macAddress)
}
lastNeighborEntries = rawList
broadcastEnrichedEntries(rawList)
}
})
} catch (e: Exception) {
Log.e("RootServer", "subscribeNeighborTable failed", e)
}
startTetheringMonitor()
}
}
}
override fun unregisterNeighborTableCallback(callback: INeighborTableCallback?) {
if (callback == null) return
neighborCallbacks.unregister(callback)
synchronized(neighborCallbacks) {
if (neighborCallbacks.registeredCallbackCount == 0) {
neighborSubscription?.close()
neighborSubscription = null
stopTetheringMonitor()
}
}
}
}
private fun broadcastEnrichedEntries(rawList: List<Pair<String, String>>) {
val list = rawList.map { (address, mac) ->
NeighborEntry(address, mac, hostnameByMAC[mac.uppercase()] ?: "")
}
Log.d("RootServer", "neighborTable updated: ${list.size} entries")
val slice = ParceledListSlice(list)
val count = neighborCallbacks.beginBroadcast()
try {
repeat(count) { i ->
try {
neighborCallbacks.getBroadcastItem(i).onNeighborTableUpdated(slice)
} catch (_: Exception) {
}
}
} finally {
neighborCallbacks.finishBroadcast()
}
}
// TetheringManager reflection (API 30+)
private val classTetheredClient by lazy {
Class.forName("android.net.TetheredClient")
}
private val getMacAddress by lazy {
classTetheredClient.getDeclaredMethod("getMacAddress")
}
private val getAddresses by lazy {
classTetheredClient.getDeclaredMethod("getAddresses")
}
private val classAddressInfo by lazy {
Class.forName("android.net.TetheredClient\$AddressInfo")
}
private val getHostname by lazy {
classAddressInfo.getDeclaredMethod("getHostname")
}
private fun startTetheringMonitor() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
try {
val manager = getSystemService("tethering") ?: return
tetheringManager = manager
val callbackClass =
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
val registerMethod = manager.javaClass.getMethod(
"registerTetheringEventCallback",
java.util.concurrent.Executor::class.java,
callbackClass,
)
val proxy = Proxy.newProxyInstance(
callbackClass.classLoader,
arrayOf(callbackClass),
) { proxyObject, method, args ->
when (method.name) {
"hashCode" -> System.identityHashCode(proxyObject)
"equals" -> proxyObject === args?.get(0)
"toString" ->
proxyObject.javaClass.name + "@" +
Integer.toHexString(System.identityHashCode(proxyObject))
"onClientsChanged" -> {
if (args != null) {
@Suppress("UNCHECKED_CAST")
handleClientsChanged(args[0] as Collection<*>)
}
null
}
else -> null
}
}
tetheringCallback = proxy
registerMethod.invoke(manager, Executors.newSingleThreadExecutor(), proxy)
Log.d("RootServer", "TetheringManager monitor started")
} catch (e: Exception) {
Log.e("RootServer", "startTetheringMonitor failed", e)
}
}
private fun stopTetheringMonitor() {
val manager = tetheringManager ?: return
val callback = tetheringCallback ?: return
try {
val callbackClass =
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
val unregisterMethod = manager.javaClass.getMethod(
"unregisterTetheringEventCallback",
callbackClass,
)
unregisterMethod.invoke(manager, callback)
} catch (e: Exception) {
Log.e("RootServer", "stopTetheringMonitor failed", e)
}
tetheringCallback = null
tetheringManager = null
hostnameByMAC.clear()
}
private fun handleClientsChanged(clients: Collection<*>) {
hostnameByMAC.clear()
for (client in clients) {
if (client == null) continue
try {
val mac = getMacAddress.invoke(client).toString().uppercase()
@Suppress("UNCHECKED_CAST")
val addresses = getAddresses.invoke(client) as List<*>
for (info in addresses) {
if (info == null) continue
val hostname = getHostname.invoke(info) as? String
if (!hostname.isNullOrEmpty()) {
hostnameByMAC[mac] = hostname
}
}
} catch (e: Exception) {
Log.e("RootServer", "handleClientsChanged reflection error", e)
}
}
Log.d("RootServer", "tethered clients updated: ${hostnameByMAC.size} hostnames")
lastNeighborEntries?.let { broadcastEnrichedEntries(it) }
}
override fun onBind(intent: Intent): IBinder = binder
override fun onDestroy() {
stopTetheringMonitor()
neighborSubscription?.close()
neighborSubscription = null
neighborCallbacks.kill()
super.onDestroy()
}
}