diff --git a/app/src/main/aidl/io/nekohasekai/sfa/bg/INeighborTableCallback.aidl b/app/src/main/aidl/io/nekohasekai/sfa/bg/INeighborTableCallback.aidl new file mode 100644 index 0000000..a2ed3cf --- /dev/null +++ b/app/src/main/aidl/io/nekohasekai/sfa/bg/INeighborTableCallback.aidl @@ -0,0 +1,7 @@ +package io.nekohasekai.sfa.bg; + +import io.nekohasekai.sfa.bg.ParceledListSlice; + +interface INeighborTableCallback { + oneway void onNeighborTableUpdated(in ParceledListSlice entries); +} diff --git a/app/src/main/aidl/io/nekohasekai/sfa/bg/IRootService.aidl b/app/src/main/aidl/io/nekohasekai/sfa/bg/IRootService.aidl index fc58161..382c192 100644 --- a/app/src/main/aidl/io/nekohasekai/sfa/bg/IRootService.aidl +++ b/app/src/main/aidl/io/nekohasekai/sfa/bg/IRootService.aidl @@ -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; } diff --git a/app/src/main/aidl/io/nekohasekai/sfa/bg/NeighborEntry.aidl b/app/src/main/aidl/io/nekohasekai/sfa/bg/NeighborEntry.aidl new file mode 100644 index 0000000..8c3cf81 --- /dev/null +++ b/app/src/main/aidl/io/nekohasekai/sfa/bg/NeighborEntry.aidl @@ -0,0 +1,3 @@ +package io.nekohasekai.sfa.bg; + +parcelable NeighborEntry; diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/NeighborEntry.java b/app/src/main/java/io/nekohasekai/sfa/bg/NeighborEntry.java new file mode 100644 index 0000000..97c97ad --- /dev/null +++ b/app/src/main/java/io/nekohasekai/sfa/bg/NeighborEntry.java @@ -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 CREATOR = + new Creator<>() { + @Override + public NeighborEntry createFromParcel(Parcel in) { + return new NeighborEntry(in); + } + + @Override + public NeighborEntry[] newArray(int size) { + return new NeighborEntry[size]; + } + }; +} diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt b/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt index 7a0be3c..78b3888 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt @@ -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 + 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) : NeighborEntryIterator { + override fun hasNext(): Boolean = iterator.hasNext() + + override fun next(): LibboxNeighborEntry = iterator.next() + } + private class InterfaceArray(private val iterator: Iterator) : NetworkInterfaceIterator { override fun hasNext(): Boolean = iterator.hasNext() diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt b/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt index a7b24b9..3cdc8f3 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt @@ -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() + } + } } diff --git a/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt b/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt index 352d159..13b0bcc 100644 --- a/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt +++ b/app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt @@ -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() + private var neighborSubscription: NeighborSubscription? = null + + private val hostnameByMAC = ConcurrentHashMap() + + @Volatile + private var lastNeighborEntries: List>? = 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>() + 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>) { + 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() + } }