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;
|
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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.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()
|
||||||
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user