Add support for MAC and hostname rule items
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
package io.nekohasekai.sfa.bg;
|
||||
|
||||
import io.nekohasekai.sfa.bg.ParceledListSlice;
|
||||
|
||||
interface INeighborTableCallback {
|
||||
oneway void onNeighborTableUpdated(in ParceledListSlice entries);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
package io.nekohasekai.sfa.bg;
|
||||
|
||||
parcelable NeighborEntry;
|
||||
49
app/src/main/java/io/nekohasekai/sfa/bg/NeighborEntry.java
Normal file
49
app/src/main/java/io/nekohasekai/sfa/bg/NeighborEntry.java
Normal 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];
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user