Skip to content

Commit d4f963e

Browse files
committed
Add support for MAC and hostname rule items
1 parent 3692d54 commit d4f963e

7 files changed

Lines changed: 312 additions & 0 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package io.nekohasekai.sfa.bg;
2+
3+
import io.nekohasekai.sfa.bg.ParceledListSlice;
4+
5+
interface INeighborTableCallback {
6+
oneway void onNeighborTableUpdated(in ParceledListSlice entries);
7+
}

app/src/main/aidl/io/nekohasekai/sfa/bg/IRootService.aidl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package io.nekohasekai.sfa.bg;
22

33
import android.os.ParcelFileDescriptor;
4+
import io.nekohasekai.sfa.bg.INeighborTableCallback;
45
import io.nekohasekai.sfa.bg.ParceledListSlice;
56

67
interface IRootService {
@@ -11,4 +12,8 @@ interface IRootService {
1112
void installPackage(in ParcelFileDescriptor apk, long size, int userId) = 2;
1213

1314
String exportDebugInfo(String outputPath) = 3;
15+
16+
void registerNeighborTableCallback(in INeighborTableCallback callback) = 4;
17+
18+
oneway void unregisterNeighborTableCallback(in INeighborTableCallback callback) = 5;
1419
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package io.nekohasekai.sfa.bg;
2+
3+
parcelable NeighborEntry;
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package io.nekohasekai.sfa.bg;
2+
3+
import android.os.Parcel;
4+
import android.os.Parcelable;
5+
import androidx.annotation.NonNull;
6+
7+
public class NeighborEntry implements Parcelable {
8+
@NonNull public final String address;
9+
@NonNull public final String macAddress;
10+
@NonNull public final String hostname;
11+
12+
public NeighborEntry(@NonNull String address, @NonNull String macAddress, @NonNull String hostname) {
13+
this.address = address;
14+
this.macAddress = macAddress;
15+
this.hostname = hostname;
16+
}
17+
18+
protected NeighborEntry(Parcel in) {
19+
address = in.readString();
20+
macAddress = in.readString();
21+
hostname = in.readString();
22+
}
23+
24+
@Override
25+
public void writeToParcel(@NonNull Parcel dest, int flags) {
26+
dest.writeString(address);
27+
dest.writeString(macAddress);
28+
dest.writeString(hostname);
29+
}
30+
31+
@Override
32+
public int describeContents() {
33+
return 0;
34+
}
35+
36+
public static final Creator<NeighborEntry> CREATOR =
37+
new Creator<>() {
38+
@Override
39+
public NeighborEntry createFromParcel(Parcel in) {
40+
return new NeighborEntry(in);
41+
}
42+
43+
@Override
44+
public NeighborEntry[] newArray(int size) {
45+
return new NeighborEntry[size];
46+
}
47+
};
48+
}

app/src/main/java/io/nekohasekai/sfa/bg/PlatformInterfaceWrapper.kt

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,28 @@ import io.nekohasekai.libbox.ConnectionOwner
1111
import io.nekohasekai.libbox.InterfaceUpdateListener
1212
import io.nekohasekai.libbox.Libbox
1313
import io.nekohasekai.libbox.LocalDNSTransport
14+
import io.nekohasekai.libbox.NeighborEntryIterator
15+
import io.nekohasekai.libbox.NeighborUpdateListener
1416
import io.nekohasekai.libbox.NetworkInterfaceIterator
1517
import io.nekohasekai.libbox.PlatformInterface
1618
import io.nekohasekai.libbox.StringIterator
1719
import io.nekohasekai.libbox.TunOptions
1820
import io.nekohasekai.libbox.WIFIState
1921
import io.nekohasekai.sfa.Application
22+
import kotlinx.coroutines.Dispatchers
23+
import kotlinx.coroutines.runBlocking
2024
import java.net.Inet6Address
2125
import java.net.InetSocketAddress
2226
import java.net.InterfaceAddress
2327
import java.net.NetworkInterface
2428
import java.security.KeyStore
2529
import kotlin.io.encoding.Base64
2630
import kotlin.io.encoding.ExperimentalEncodingApi
31+
import io.nekohasekai.libbox.NeighborEntry as LibboxNeighborEntry
2732
import io.nekohasekai.libbox.NetworkInterface as LibboxNetworkInterface
2833

34+
private var neighborCallback: INeighborTableCallback.Stub? = null
35+
2936
interface PlatformInterfaceWrapper : PlatformInterface {
3037
override fun usePlatformAutoDetectInterfaceControl(): Boolean = true
3138

@@ -172,6 +179,45 @@ interface PlatformInterfaceWrapper : PlatformInterface {
172179
return StringArray(certificates.iterator())
173180
}
174181

182+
override fun startNeighborMonitor(listener: NeighborUpdateListener?) {
183+
if (listener == null) return
184+
val callback = object : INeighborTableCallback.Stub() {
185+
override fun onNeighborTableUpdated(entries: ParceledListSlice<*>?) {
186+
if (entries == null) return
187+
@Suppress("UNCHECKED_CAST")
188+
val list = entries.list as List<NeighborEntry>
189+
listener.updateNeighborTable(NeighborEntryArray(list.map { entry ->
190+
LibboxNeighborEntry().apply {
191+
address = entry.address
192+
macAddress = entry.macAddress
193+
hostname = entry.hostname
194+
}
195+
}.iterator()))
196+
}
197+
}
198+
neighborCallback = callback
199+
runBlocking(Dispatchers.IO) {
200+
RootClient.registerNeighborTableCallback(callback)
201+
}
202+
}
203+
204+
override fun registerMyInterface(name: String?) {
205+
}
206+
207+
override fun closeNeighborMonitor(listener: NeighborUpdateListener?) {
208+
val callback = neighborCallback ?: return
209+
neighborCallback = null
210+
runBlocking(Dispatchers.IO) {
211+
RootClient.unregisterNeighborTableCallback(callback)
212+
}
213+
}
214+
215+
private class NeighborEntryArray(private val iterator: Iterator<LibboxNeighborEntry>) : NeighborEntryIterator {
216+
override fun hasNext(): Boolean = iterator.hasNext()
217+
218+
override fun next(): LibboxNeighborEntry = iterator.next()
219+
}
220+
175221
private class InterfaceArray(private val iterator: Iterator<LibboxNetworkInterface>) : NetworkInterfaceIterator {
176222
override fun hasNext(): Boolean = iterator.hasNext()
177223

app/src/main/java/io/nekohasekai/sfa/bg/RootClient.kt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,21 @@ object RootClient {
133133
throw e.rethrowFromSystemServer()
134134
}
135135
}
136+
137+
suspend fun registerNeighborTableCallback(callback: INeighborTableCallback) {
138+
val svc = bindService()
139+
try {
140+
svc.registerNeighborTableCallback(callback)
141+
} catch (e: RemoteException) {
142+
throw e.rethrowFromSystemServer()
143+
}
144+
}
145+
146+
suspend fun unregisterNeighborTableCallback(callback: INeighborTableCallback) {
147+
try {
148+
service?.unregisterNeighborTableCallback(callback)
149+
} catch (e: RemoteException) {
150+
throw e.rethrowFromSystemServer()
151+
}
152+
}
136153
}

app/src/main/java/io/nekohasekai/sfa/bg/RootServer.kt

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,36 @@ package io.nekohasekai.sfa.bg
22

33
import android.content.Intent
44
import android.content.pm.PackageInfo
5+
import android.os.Build
56
import android.os.IBinder
67
import android.os.ParcelFileDescriptor
8+
import android.os.RemoteCallbackList
9+
import android.util.Log
710
import com.topjohnwu.superuser.ipc.RootService
11+
import io.nekohasekai.libbox.Libbox
12+
import io.nekohasekai.libbox.NeighborEntryIterator
13+
import io.nekohasekai.libbox.NeighborSubscription
14+
import io.nekohasekai.libbox.NeighborUpdateListener
815
import io.nekohasekai.sfa.BuildConfig
916
import io.nekohasekai.sfa.vendor.PrivilegedServiceUtils
1017
import java.io.IOException
18+
import java.lang.reflect.Proxy
19+
import java.util.concurrent.ConcurrentHashMap
20+
import java.util.concurrent.Executors
1121

1222
class RootServer : RootService() {
1323

24+
private val neighborCallbacks = RemoteCallbackList<INeighborTableCallback>()
25+
private var neighborSubscription: NeighborSubscription? = null
26+
27+
private val hostnameByMAC = ConcurrentHashMap<String, String>()
28+
29+
@Volatile
30+
private var lastNeighborEntries: List<Pair<String, String>>? = null
31+
32+
private var tetheringCallback: Any? = null
33+
private var tetheringManager: Any? = null
34+
1435
private val binder = object : IRootService.Stub() {
1536
override fun destroy() {
1637
stopSelf()
@@ -31,7 +52,172 @@ class RootServer : RootService() {
3152
outputPath!!,
3253
BuildConfig.APPLICATION_ID,
3354
)
55+
56+
override fun registerNeighborTableCallback(callback: INeighborTableCallback?) {
57+
if (callback == null) return
58+
neighborCallbacks.register(callback)
59+
synchronized(neighborCallbacks) {
60+
if (neighborSubscription == null) {
61+
try {
62+
neighborSubscription =
63+
Libbox.subscribeNeighborTable(object : NeighborUpdateListener {
64+
override fun updateNeighborTable(entries: NeighborEntryIterator?) {
65+
if (entries == null) return
66+
val rawList = mutableListOf<Pair<String, String>>()
67+
while (entries.hasNext()) {
68+
val entry = entries.next()
69+
rawList.add(entry.address to entry.macAddress)
70+
}
71+
lastNeighborEntries = rawList
72+
broadcastEnrichedEntries(rawList)
73+
}
74+
})
75+
} catch (e: Exception) {
76+
Log.e("RootServer", "subscribeNeighborTable failed", e)
77+
}
78+
startTetheringMonitor()
79+
}
80+
}
81+
}
82+
83+
override fun unregisterNeighborTableCallback(callback: INeighborTableCallback?) {
84+
if (callback == null) return
85+
neighborCallbacks.unregister(callback)
86+
synchronized(neighborCallbacks) {
87+
if (neighborCallbacks.registeredCallbackCount == 0) {
88+
neighborSubscription?.close()
89+
neighborSubscription = null
90+
stopTetheringMonitor()
91+
}
92+
}
93+
}
94+
}
95+
96+
private fun broadcastEnrichedEntries(rawList: List<Pair<String, String>>) {
97+
val list = rawList.map { (address, mac) ->
98+
NeighborEntry(address, mac, hostnameByMAC[mac.uppercase()] ?: "")
99+
}
100+
Log.d("RootServer", "neighborTable updated: ${list.size} entries")
101+
val slice = ParceledListSlice(list)
102+
val count = neighborCallbacks.beginBroadcast()
103+
try {
104+
repeat(count) { i ->
105+
try {
106+
neighborCallbacks.getBroadcastItem(i).onNeighborTableUpdated(slice)
107+
} catch (_: Exception) {
108+
}
109+
}
110+
} finally {
111+
neighborCallbacks.finishBroadcast()
112+
}
113+
}
114+
115+
// TetheringManager reflection (API 30+)
116+
117+
private val classTetheredClient by lazy {
118+
Class.forName("android.net.TetheredClient")
119+
}
120+
private val getMacAddress by lazy {
121+
classTetheredClient.getDeclaredMethod("getMacAddress")
122+
}
123+
private val getAddresses by lazy {
124+
classTetheredClient.getDeclaredMethod("getAddresses")
125+
}
126+
private val classAddressInfo by lazy {
127+
Class.forName("android.net.TetheredClient\$AddressInfo")
128+
}
129+
private val getHostname by lazy {
130+
classAddressInfo.getDeclaredMethod("getHostname")
131+
}
132+
133+
private fun startTetheringMonitor() {
134+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) return
135+
try {
136+
val manager = getSystemService("tethering") ?: return
137+
tetheringManager = manager
138+
val callbackClass =
139+
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
140+
val registerMethod = manager.javaClass.getMethod(
141+
"registerTetheringEventCallback",
142+
java.util.concurrent.Executor::class.java,
143+
callbackClass,
144+
)
145+
val proxy = Proxy.newProxyInstance(
146+
callbackClass.classLoader,
147+
arrayOf(callbackClass),
148+
) { proxyObject, method, args ->
149+
when (method.name) {
150+
"hashCode" -> System.identityHashCode(proxyObject)
151+
"equals" -> proxyObject === args?.get(0)
152+
"toString" -> proxyObject.javaClass.name + "@" +
153+
Integer.toHexString(System.identityHashCode(proxyObject))
154+
"onClientsChanged" -> {
155+
if (args != null) {
156+
@Suppress("UNCHECKED_CAST")
157+
handleClientsChanged(args[0] as Collection<*>)
158+
}
159+
null
160+
}
161+
else -> null
162+
}
163+
}
164+
tetheringCallback = proxy
165+
registerMethod.invoke(manager, Executors.newSingleThreadExecutor(), proxy)
166+
Log.d("RootServer", "TetheringManager monitor started")
167+
} catch (e: Exception) {
168+
Log.e("RootServer", "startTetheringMonitor failed", e)
169+
}
170+
}
171+
172+
private fun stopTetheringMonitor() {
173+
val manager = tetheringManager ?: return
174+
val callback = tetheringCallback ?: return
175+
try {
176+
val callbackClass =
177+
Class.forName("android.net.TetheringManager\$TetheringEventCallback")
178+
val unregisterMethod = manager.javaClass.getMethod(
179+
"unregisterTetheringEventCallback",
180+
callbackClass,
181+
)
182+
unregisterMethod.invoke(manager, callback)
183+
} catch (e: Exception) {
184+
Log.e("RootServer", "stopTetheringMonitor failed", e)
185+
}
186+
tetheringCallback = null
187+
tetheringManager = null
188+
hostnameByMAC.clear()
189+
}
190+
191+
private fun handleClientsChanged(clients: Collection<*>) {
192+
hostnameByMAC.clear()
193+
for (client in clients) {
194+
if (client == null) continue
195+
try {
196+
val mac = getMacAddress.invoke(client).toString().uppercase()
197+
@Suppress("UNCHECKED_CAST")
198+
val addresses = getAddresses.invoke(client) as List<*>
199+
for (info in addresses) {
200+
if (info == null) continue
201+
val hostname = getHostname.invoke(info) as? String
202+
if (!hostname.isNullOrEmpty()) {
203+
hostnameByMAC[mac] = hostname
204+
}
205+
}
206+
} catch (e: Exception) {
207+
Log.e("RootServer", "handleClientsChanged reflection error", e)
208+
}
209+
}
210+
Log.d("RootServer", "tethered clients updated: ${hostnameByMAC.size} hostnames")
211+
lastNeighborEntries?.let { broadcastEnrichedEntries(it) }
34212
}
35213

36214
override fun onBind(intent: Intent): IBinder = binder
215+
216+
override fun onDestroy() {
217+
stopTetheringMonitor()
218+
neighborSubscription?.close()
219+
neighborSubscription = null
220+
neighborCallbacks.kill()
221+
super.onDestroy()
222+
}
37223
}

0 commit comments

Comments
 (0)