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; package io.nekohasekai.sfa.bg;
import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor;
import io.nekohasekai.sfa.bg.INeighborTableCallback;
import io.nekohasekai.sfa.bg.ParceledListSlice; import io.nekohasekai.sfa.bg.ParceledListSlice;
interface IRootService { interface IRootService {
@@ -11,4 +12,8 @@ interface IRootService {
void installPackage(in ParcelFileDescriptor apk, long size, int userId) = 2; void installPackage(in ParcelFileDescriptor apk, long size, int userId) = 2;
String exportDebugInfo(String outputPath) = 3; 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.InterfaceUpdateListener
import io.nekohasekai.libbox.Libbox import io.nekohasekai.libbox.Libbox
import io.nekohasekai.libbox.LocalDNSTransport import io.nekohasekai.libbox.LocalDNSTransport
import io.nekohasekai.libbox.NeighborEntryIterator
import io.nekohasekai.libbox.NeighborUpdateListener
import io.nekohasekai.libbox.NetworkInterfaceIterator import io.nekohasekai.libbox.NetworkInterfaceIterator
import io.nekohasekai.libbox.PlatformInterface import io.nekohasekai.libbox.PlatformInterface
import io.nekohasekai.libbox.StringIterator import io.nekohasekai.libbox.StringIterator
import io.nekohasekai.libbox.TunOptions import io.nekohasekai.libbox.TunOptions
import io.nekohasekai.libbox.WIFIState import io.nekohasekai.libbox.WIFIState
import io.nekohasekai.sfa.Application import io.nekohasekai.sfa.Application
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.runBlocking
import java.net.Inet6Address import java.net.Inet6Address
import java.net.InetSocketAddress import java.net.InetSocketAddress
import java.net.InterfaceAddress import java.net.InterfaceAddress
@@ -24,8 +28,11 @@ import java.net.NetworkInterface
import java.security.KeyStore import java.security.KeyStore
import kotlin.io.encoding.Base64 import kotlin.io.encoding.Base64
import kotlin.io.encoding.ExperimentalEncodingApi import kotlin.io.encoding.ExperimentalEncodingApi
import io.nekohasekai.libbox.NeighborEntry as LibboxNeighborEntry
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
private var neighborCallback: INeighborTableCallback.Stub? = null
interface PlatformInterfaceWrapper : PlatformInterface { interface PlatformInterfaceWrapper : PlatformInterface {
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
@@ -172,6 +179,49 @@ interface PlatformInterfaceWrapper : PlatformInterface {
return StringArray(certificates.iterator()) 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 { private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) : NetworkInterfaceIterator {
override fun hasNext(): Boolean = iterator.hasNext() override fun hasNext(): Boolean = iterator.hasNext()

View File

@@ -133,4 +133,21 @@ object RootClient {
throw e.rethrowFromSystemServer() 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.Intent
import android.content.pm.PackageInfo import android.content.pm.PackageInfo
import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.ParcelFileDescriptor import android.os.ParcelFileDescriptor
import android.os.RemoteCallbackList
import android.util.Log
import com.topjohnwu.superuser.ipc.RootService 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.BuildConfig
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
import java.io.IOException import java.io.IOException
import java.lang.reflect.Proxy
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.Executors
class RootServer : RootService() { 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() { private val binder = object : IRootService.Stub() {
override fun destroy() { override fun destroy() {
stopSelf() stopSelf()
@@ -31,7 +52,174 @@ class RootServer : RootService() {
outputPath!!, outputPath!!,
BuildConfig.APPLICATION_ID, 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 onBind(intent: Intent): IBinder = binder
override fun onDestroy() {
stopTetheringMonitor()
neighborSubscription?.close()
neighborSubscription = null
neighborCallbacks.kill()
super.onDestroy()
}
} }