From 0cb6f1fb7d8e7705f7f993a9194e1e3cf044a373 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=96=E7=95=8C?= Date: Sat, 17 Jan 2026 03:16:53 +0800 Subject: [PATCH] Cache reflection results using lazy fields --- .../sfa/vendor/PrivilegedServiceUtils.kt | 96 +++++++++------ .../sfa/xposed/HookModuleVersion.kt | 2 +- .../sfa/xposed/PrivilegeChecker.kt | 49 ++++++-- .../sfa/xposed/PrivilegeSettingsStore.kt | 16 ++- .../io/nekohasekai/sfa/xposed/VpnAppStore.kt | 49 +++++--- .../io/nekohasekai/sfa/xposed/VpnSanitizer.kt | 34 ++++-- .../io/nekohasekai/sfa/xposed/XposedInit.kt | 9 +- .../hidevpn/ConnectivityServiceHookHelper.kt | 111 +++++++++++++++--- .../NetworkCapabilities+writeToParcel.kt | 34 ++++-- .../hooks/hidevpn/NetworkInterface+getName.kt | 9 +- .../PackageManager+getInstalledPackages.kt | 15 ++- 11 files changed, 312 insertions(+), 112 deletions(-) diff --git a/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt b/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt index 85e3fe8..6016fd3 100644 --- a/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt +++ b/app/src/main/java/io/nekohasekai/sfa/vendor/PrivilegedServiceUtils.kt @@ -19,30 +19,67 @@ import java.io.IOException object PrivilegedServiceUtils { + private val iPackageManagerStubClass by lazy { Class.forName("android.content.pm.IPackageManager\$Stub") } + private val asInterfaceMethod by lazy { iPackageManagerStubClass.getMethod("asInterface", IBinder::class.java) } + private val iPackageManagerClass by lazy { Class.forName("android.content.pm.IPackageManager") } + + private val getInstalledPackagesMethodLong by lazy { + iPackageManagerClass.getMethod( + "getInstalledPackages", + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + } + private val getInstalledPackagesMethodInt by lazy { + iPackageManagerClass.getMethod( + "getInstalledPackages", + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ) + } + private val getPackageInstallerMethod by lazy { iPackageManagerClass.getMethod("getPackageInstaller") } + + private val packageInstallerCtorS by lazy { + PackageInstaller::class.java.getConstructor( + IPackageInstaller::class.java, + String::class.java, + String::class.java, + Int::class.javaPrimitiveType + ) + } + private val packageInstallerCtorPre by lazy { + PackageInstaller::class.java.getConstructor( + IPackageInstaller::class.java, + String::class.java, + Int::class.javaPrimitiveType + ) + } + private val sessionCtor by lazy { + PackageInstaller.Session::class.java.getConstructor(IPackageInstallerSession::class.java) + } + private val intentSenderCtor by lazy { + IntentSender::class.java.getConstructor(IIntentSender::class.java) + } + private val installFlagsField by lazy { + PackageInstaller.SessionParams::class.java.getDeclaredField("installFlags").apply { isAccessible = true } + } + private val getListMethod by lazy { + Class.forName("android.content.pm.ParceledListSlice").getMethod("getList") + } + private fun getPackageManager(): Any { - val binder = SystemServiceHelperCompat.getSystemService("package") ?: throw IllegalStateException("package service not available") - val stubClass = Class.forName("android.content.pm.IPackageManager\$Stub") - val asInterface = stubClass.getMethod("asInterface", IBinder::class.java) - return asInterface.invoke(null, binder) ?: throw IllegalStateException("IPackageManager is null") + val binder = SystemServiceHelperCompat.getSystemService("package") + ?: throw IllegalStateException("package service not available") + return asInterfaceMethod.invoke(null, binder) + ?: throw IllegalStateException("IPackageManager is null") } fun getInstalledPackages(flags: Int, userId: Int): List { val iPackageManager = getPackageManager() - val iPackageManagerClass = Class.forName("android.content.pm.IPackageManager") val result = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { - val method = iPackageManagerClass.getMethod( - "getInstalledPackages", - Long::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - ) - method.invoke(iPackageManager, flags.toLong(), userId) + getInstalledPackagesMethodLong.invoke(iPackageManager, flags.toLong(), userId) } else { - val method = iPackageManagerClass.getMethod( - "getInstalledPackages", - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - ) - method.invoke(iPackageManager, flags, userId) + getInstalledPackagesMethodInt.invoke(iPackageManager, flags, userId) } return extractPackageList(result) } @@ -62,9 +99,6 @@ object PrivilegedServiceUtils { val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) params.setAppPackageName(BuildConfig.APPLICATION_ID) - // Set INSTALL_REPLACE_EXISTING flag (value = 2) - val installFlagsField = PackageInstaller.SessionParams::class.java.getDeclaredField("installFlags") - installFlagsField.isAccessible = true installFlagsField.setInt(params, installFlagsField.getInt(params) or 2) val sessionId = packageInstaller.createSession(params) @@ -106,9 +140,7 @@ object PrivilegedServiceUtils { private fun getPackageInstaller(): IPackageInstaller { val iPackageManager = getPackageManager() - val iPackageManagerClass = Class.forName("android.content.pm.IPackageManager") - val method = iPackageManagerClass.getMethod("getPackageInstaller") - val installer = method.invoke(iPackageManager) as IPackageInstaller + val installer = getPackageInstallerMethod.invoke(iPackageManager) as IPackageInstaller return IPackageInstaller.Stub.asInterface(installer.asBinder()) } @@ -119,23 +151,14 @@ object PrivilegedServiceUtils { userId: Int, ): PackageInstaller { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PackageInstaller::class.java.getConstructor( - IPackageInstaller::class.java, - String::class.java, - String::class.java, - Int::class.javaPrimitiveType, - ).newInstance(installer, installerPackageName, installerAttributionTag, userId) + packageInstallerCtorS.newInstance(installer, installerPackageName, installerAttributionTag, userId) } else { - PackageInstaller::class.java.getConstructor( - IPackageInstaller::class.java, - String::class.java, - Int::class.javaPrimitiveType, - ).newInstance(installer, installerPackageName, userId) + packageInstallerCtorPre.newInstance(installer, installerPackageName, userId) } } private fun createSession(session: IPackageInstallerSession): PackageInstaller.Session { - return PackageInstaller.Session::class.java.getConstructor(IPackageInstallerSession::class.java).newInstance(session) + return sessionCtor.newInstance(session) } private fun createIntentSender(onResult: (Intent) -> Unit): IntentSender { @@ -152,13 +175,12 @@ object PrivilegedServiceUtils { onResult(intent) } } - return IntentSender::class.java.getConstructor(IIntentSender::class.java).newInstance(sender) + return intentSenderCtor.newInstance(sender) } @Suppress("UNCHECKED_CAST") private fun extractPackageList(parceledListSlice: Any?): List { if (parceledListSlice == null) return emptyList() - val getListMethod = parceledListSlice.javaClass.getMethod("getList") val list = getListMethod.invoke(parceledListSlice) as? List<*> return list?.filterIsInstance() ?: emptyList() } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/HookModuleVersion.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/HookModuleVersion.kt index a72fdae..fa29f11 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/HookModuleVersion.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/HookModuleVersion.kt @@ -1,5 +1,5 @@ package io.nekohasekai.sfa.xposed object HookModuleVersion { - const val CURRENT = 2 + const val CURRENT = 3 } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt index bcf688f..7d41264 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeChecker.kt @@ -3,8 +3,7 @@ package io.nekohasekai.sfa.xposed import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Process -import de.robv.android.xposed.XposedHelpers -import io.nekohasekai.sfa.BuildConfig +import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap object PrivilegeChecker { @@ -21,6 +20,13 @@ object PrivilegeChecker { private val exemptCache = ConcurrentHashMap() private val privilegedCache = ConcurrentHashMap() + private val appGlobalsClass by lazy { Class.forName("android.app.AppGlobals") } + private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") } + private var getPackagesForUidMethod: Method? = null + private var checkUidPermissionMethod: Method? = null + private var getApplicationInfoMethodLong: Method? = null + private var getApplicationInfoMethodInt: Method? = null + fun isPrivilegedUid(uid: Int): Boolean { if (uid < Process.FIRST_APPLICATION_UID) { return true @@ -44,9 +50,16 @@ object PrivilegeChecker { return true } } + val checkMethod = checkUidPermissionMethod ?: run { + pm.javaClass.getMethod( + "checkUidPermission", + String::class.java, + Int::class.javaPrimitiveType + ).also { checkUidPermissionMethod = it } + } for (permission in privilegedPermissions) { val result = try { - XposedHelpers.callMethod(pm, "checkUidPermission", permission, uid) as? Int + checkMethod.invoke(pm, permission, uid) as? Int } catch (_: Throwable) { null } @@ -83,7 +96,11 @@ object PrivilegeChecker { private fun getPackagesForUid(uid: Int): List { val pm = getPackageManager() ?: return emptyList() return try { - val method = pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType) + val method = getPackagesForUidMethod ?: run { + pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType).also { + getPackagesForUidMethod = it + } + } val result = method.invoke(pm, uid) when (result) { is Array<*> -> result.filterIsInstance() @@ -97,9 +114,7 @@ object PrivilegeChecker { private fun getPackageManager(): Any? { return try { - val appGlobals = Class.forName("android.app.AppGlobals") - val method = appGlobals.getMethod("getPackageManager") - method.invoke(null) + getPackageManagerMethod.invoke(null) } catch (_: Throwable) { null } @@ -107,10 +122,26 @@ object PrivilegeChecker { private fun getApplicationInfo(pm: Any, pkg: String, userId: Int): ApplicationInfo? { return try { - XposedHelpers.callMethod(pm, "getApplicationInfo", pkg, 0, userId) as? ApplicationInfo + val method = getApplicationInfoMethodInt ?: run { + pm.javaClass.getMethod( + "getApplicationInfo", + String::class.java, + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ).also { getApplicationInfoMethodInt = it } + } + method.invoke(pm, pkg, 0, userId) as? ApplicationInfo } catch (_: Throwable) { try { - XposedHelpers.callMethod(pm, "getApplicationInfo", pkg, 0L, userId) as? ApplicationInfo + val method = getApplicationInfoMethodLong ?: run { + pm.javaClass.getMethod( + "getApplicationInfo", + String::class.java, + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType + ).also { getApplicationInfoMethodLong = it } + } + method.invoke(pm, pkg, 0L, userId) as? ApplicationInfo } catch (_: Throwable) { null } diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt index 6f3ebcd..0d06d4f 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/PrivilegeSettingsStore.kt @@ -1,8 +1,8 @@ package io.nekohasekai.sfa.xposed import java.io.File +import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap -import io.nekohasekai.sfa.xposed.HookErrorStore object PrivilegeSettingsStore { private const val SETTINGS_DIR = "/data/system/sing-box" @@ -17,6 +17,10 @@ object PrivilegeSettingsStore { private var interfacePrefix = "en" private val uidCache = ConcurrentHashMap() + private val appGlobalsClass by lazy { Class.forName("android.app.AppGlobals") } + private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") } + private var getPackagesForUidMethod: Method? = null + fun update( enabled: Boolean, packages: Set, @@ -110,7 +114,11 @@ object PrivilegeSettingsStore { private fun getPackagesForUid(uid: Int): List { val pm = getPackageManager() ?: return emptyList() return try { - val method = pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType) + val method = getPackagesForUidMethod ?: run { + pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType).also { + getPackagesForUidMethod = it + } + } val result = method.invoke(pm, uid) when (result) { is Array<*> -> result.filterIsInstance() @@ -125,9 +133,7 @@ object PrivilegeSettingsStore { private fun getPackageManager(): Any? { return try { - val appGlobals = Class.forName("android.app.AppGlobals") - val method = appGlobals.getMethod("getPackageManager") - method.invoke(null) + getPackageManagerMethod.invoke(null) } catch (e: Throwable) { HookErrorStore.e("PrivilegeSettingsStore", "getPackageManager failed", e) null diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt index df90f2f..4e4ccdf 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/VpnAppStore.kt @@ -7,6 +7,7 @@ import android.content.pm.PackageManager import android.os.Binder import android.os.SystemClock import io.nekohasekai.sfa.BuildConfig +import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap object VpnAppStore { @@ -20,6 +21,16 @@ object VpnAppStore { private val uidVpnCache = ConcurrentHashMap>() private val uidPackagesCache = ConcurrentHashMap>>() + private val appGlobalsClass by lazy { Class.forName("android.app.AppGlobals") } + private val getPackageManagerMethod by lazy { appGlobalsClass.getMethod("getPackageManager") } + + @Volatile + private var pmClass: Class<*>? = null + private var getPackagesForUidMethod: Method? = null + private var getInstalledPackagesMethodLong: Method? = null + private var getInstalledPackagesMethodInt: Method? = null + private var getListMethod: Method? = null + fun isVpnUid(uid: Int): Boolean { val now = SystemClock.uptimeMillis() val cached = uidVpnCache[uid] @@ -57,7 +68,11 @@ object VpnAppStore { val result = binderLocalScope { val pm = getPackageManager() ?: return@binderLocalScope emptyList() try { - val method = pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType) + val method = getPackagesForUidMethod ?: run { + pm.javaClass.getMethod("getPackagesForUid", Int::class.javaPrimitiveType).also { + getPackagesForUidMethod = it + } + } when (val raw = method.invoke(pm, uid)) { is Array<*> -> raw.filterIsInstance() is List<*> -> raw.filterIsInstance() @@ -108,19 +123,23 @@ object VpnAppStore { private fun getInstalledPackagesCompat(pm: Any, flags: Long, userId: Int): List { val result = try { - val method = pm.javaClass.getMethod( - "getInstalledPackages", - Long::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - ) + val method = getInstalledPackagesMethodLong ?: run { + pm.javaClass.getMethod( + "getInstalledPackages", + Long::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ).also { getInstalledPackagesMethodLong = it } + } method.invoke(pm, flags, userId) } catch (_: Throwable) { try { - val method = pm.javaClass.getMethod( - "getInstalledPackages", - Int::class.javaPrimitiveType, - Int::class.javaPrimitiveType, - ) + val method = getInstalledPackagesMethodInt ?: run { + pm.javaClass.getMethod( + "getInstalledPackages", + Int::class.javaPrimitiveType, + Int::class.javaPrimitiveType, + ).also { getInstalledPackagesMethodInt = it } + } method.invoke(pm, flags.toInt(), userId) } catch (e: Throwable) { HookErrorStore.e("VpnAppStore", "getInstalledPackages failed", e) @@ -137,9 +156,7 @@ object VpnAppStore { private fun getPackageManager(): Any? { return try { - val appGlobals = Class.forName("android.app.AppGlobals") - val method = appGlobals.getMethod("getPackageManager") - method.invoke(null) + getPackageManagerMethod.invoke(null) } catch (e: Throwable) { HookErrorStore.e("VpnAppStore", "getPackageManager failed", e) null @@ -161,7 +178,9 @@ object VpnAppStore { return raw.filterIsInstance() } return try { - val method = raw.javaClass.getMethod("getList") + val method = getListMethod ?: run { + raw.javaClass.getMethod("getList").also { getListMethod = it } + } val list = method.invoke(raw) if (list is List<*>) { list.filterIsInstance() diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/VpnSanitizer.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/VpnSanitizer.kt index 1aed0d5..1aa35f1 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/VpnSanitizer.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/VpnSanitizer.kt @@ -3,6 +3,7 @@ package io.nekohasekai.sfa.xposed import android.net.LinkProperties import android.net.NetworkCapabilities import android.net.NetworkInfo +import android.net.ProxyInfo import android.os.Build import android.os.Parcel import android.os.Process @@ -14,6 +15,22 @@ object VpnSanitizer { "tun", ) + private val getStackedLinksMethod by lazy { + LinkProperties::class.java.getMethod("getStackedLinks") + } + private val removeStackedLinkMethod by lazy { + LinkProperties::class.java.getMethod("removeStackedLink", String::class.java) + } + private val setHttpProxyMethod by lazy { + LinkProperties::class.java.getMethod("setHttpProxy", ProxyInfo::class.java) + } + private val removeTransportTypeMethod by lazy { + NetworkCapabilities::class.java.getMethod("removeTransportType", Int::class.javaPrimitiveType) + } + private val addCapabilityMethod by lazy { + NetworkCapabilities::class.java.getMethod("addCapability", Int::class.javaPrimitiveType) + } + fun shouldHide(uid: Int): Boolean { if (!PrivilegeSettingsStore.shouldHideUid(uid)) { return false @@ -47,13 +64,13 @@ object VpnSanitizer { lp.setInterfaceName(null) } @Suppress("UNCHECKED_CAST") - val stacked = XposedHelpers.callMethod(lp, "getStackedLinks") as? List + val stacked = getStackedLinksMethod.invoke(lp) as? List if (!stacked.isNullOrEmpty()) { for (link in stacked) { clearHttpProxy(link) - val name = link.interfaceName - if (isVpnInterface(name)) { - XposedHelpers.callMethod(lp, "removeStackedLink", name) + val iface = link.interfaceName + if (iface != null && isVpnInterface(iface)) { + removeStackedLinkMethod.invoke(lp, iface) } } } @@ -65,8 +82,7 @@ object VpnSanitizer { return true } @Suppress("UNCHECKED_CAST") - val stacked = XposedHelpers.callMethod(lp, "getStackedLinks") as? List - ?: return false + val stacked = getStackedLinksMethod.invoke(lp) as? List ?: return false return stacked.any { isVpnInterface(it.interfaceName) } } @@ -77,8 +93,8 @@ object VpnSanitizer { } private fun sanitizeTransport(caps: NetworkCapabilities) { - XposedHelpers.callMethod(caps, "removeTransportType", NetworkCapabilities.TRANSPORT_VPN) - XposedHelpers.callMethod(caps, "addCapability", NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + removeTransportTypeMethod.invoke(caps, NetworkCapabilities.TRANSPORT_VPN) + addCapabilityMethod.invoke(caps, NetworkCapabilities.NET_CAPABILITY_NOT_VPN) } private fun clearUnderlyingNetworks(caps: NetworkCapabilities) { @@ -110,7 +126,7 @@ object VpnSanitizer { } private fun clearHttpProxy(lp: LinkProperties) { - XposedHelpers.callMethod(lp, "setHttpProxy", null) + setHttpProxyMethod.invoke(lp, null as ProxyInfo?) } fun cloneLinkProperties(source: LinkProperties): LinkProperties { diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt index e150815..f854fc7 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/XposedInit.kt @@ -15,6 +15,10 @@ class XposedInit( param: XposedModuleInterface.ModuleLoadedParam, ) : XposedModule(base, param) { + private val activityThreadClass by lazy { Class.forName("android.app.ActivityThread") } + private val currentActivityThreadMethod by lazy { activityThreadClass.getMethod("currentActivityThread") } + private val getSystemContextMethod by lazy { activityThreadClass.getMethod("getSystemContext") } + override fun onSystemServerLoaded(param: XposedModuleInterface.SystemServerLoadedParam) { val systemContext = resolveSystemContext() HookErrorStore.i("XposedInit", "handleSystemServerLoaded") @@ -45,9 +49,8 @@ class XposedInit( private fun resolveSystemContext(): Context? { return try { - val activityThread = Class.forName("android.app.ActivityThread") - val currentThread = activityThread.getMethod("currentActivityThread").invoke(null) - activityThread.getMethod("getSystemContext").invoke(currentThread) as? Context + val currentThread = currentActivityThreadMethod.invoke(null) + getSystemContextMethod.invoke(currentThread) as? Context } catch (e: Throwable) { HookErrorStore.e("XposedInit", "resolveSystemContext failed", e) null diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt index de27be4..fda798b 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/ConnectivityServiceHookHelper.kt @@ -14,6 +14,7 @@ import io.nekohasekai.sfa.xposed.VpnAppStore import io.nekohasekai.sfa.xposed.VpnSanitizer import io.nekohasekai.sfa.xposed.hooks.SafeMethodHook import io.nekohasekai.sfa.xposed.hooks.XHook +import java.lang.reflect.Method import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.atomic.AtomicBoolean @@ -33,6 +34,17 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo lateinit var cls: Class<*> private set + private val serviceManagerClass by lazy { Class.forName("android.os.ServiceManager") } + private val checkServiceMethod by lazy { serviceManagerClass.getMethod("checkService", String::class.java) } + + private var getVpnForUidMethod: Method? = null + private lateinit var getVpnUnderlyingNetworksMethod: Method + private lateinit var getNetworkAgentInfoForNetworkMethod: Method + private var getFilteredNetworkInfoMethod: Method? = null + private lateinit var getDefaultNetworkMethod: Method + private lateinit var isVPNMethod: Method + private var networkMethod: Method? = null + override fun injectHook() { val foundClass = findConnectivityServiceClass() if (foundClass != null) { @@ -50,6 +62,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo } this.cls = cls connectivityClassLoader = cls.classLoader ?: classLoader + initMethodCache() HookErrorStore.i( SOURCE, "Installing ConnectivityService hooks ($source) cls=${cls.name} loader=${connectivityClassLoader.javaClass.name}", @@ -72,6 +85,38 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo HookErrorStore.i(SOURCE, "Hooked ConnectivityService ($source) cls=${cls.name}") } + private fun initMethodCache() { + val intType = Int::class.javaPrimitiveType!! + val booleanType = Boolean::class.javaPrimitiveType!! + val naiClass = resolveConnectivityModuleClass("NetworkAgentInfo", "connectivity") + if (sdkInt >= 31) { + getVpnForUidMethod = findDeclaredMethod(cls, "getVpnForUid", intType) + if (getVpnForUidMethod == null) { + HookErrorStore.w(SOURCE, "getVpnForUid not found; falling back to underlying networks") + } + } + getVpnUnderlyingNetworksMethod = requireDeclaredMethod(cls, "getVpnUnderlyingNetworks", intType) + getNetworkAgentInfoForNetworkMethod = requireDeclaredMethod(cls, "getNetworkAgentInfoForNetwork", Network::class.java) + if (sdkInt >= 31) { + getFilteredNetworkInfoMethod = findDeclaredMethod( + cls, + "getFilteredNetworkInfo", + naiClass, + intType, + booleanType + ) + if (getFilteredNetworkInfoMethod == null) { + HookErrorStore.w(SOURCE, "getFilteredNetworkInfo not found; network info sanitization disabled") + } + } + getDefaultNetworkMethod = requireDeclaredMethod(cls, "getDefaultNetwork") + isVPNMethod = requireDeclaredMethod(naiClass, "isVPN") + networkMethod = findDeclaredMethod(naiClass, "network") + if (networkMethod == null) { + HookErrorStore.w(SOURCE, "NetworkAgentInfo.network() not found; falling back to field access") + } + } + // region Service Discovery private fun findConnectivityServiceClass(): Class<*>? { @@ -229,9 +274,7 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo private fun tryHookFromServiceManager() { if (hooked.get()) return val binder = try { - val serviceManager = Class.forName("android.os.ServiceManager") - val checkService = serviceManager.getMethod("checkService", String::class.java) - checkService.invoke(null, Context.CONNECTIVITY_SERVICE) as? IBinder + checkServiceMethod.invoke(null, Context.CONNECTIVITY_SERVICE) as? IBinder } catch (_: Throwable) { null } @@ -365,56 +408,88 @@ class ConnectivityServiceHookHelper(private val classLoader: ClassLoader) : XHoo fun hasVpnForUid(connectivityService: Any, uid: Int): Boolean { if (sdkInt >= 31) { - return XposedHelpers.callMethod(connectivityService, "getVpnForUid", uid) != null + val vpnForUidMethod = getVpnForUidMethod + if (vpnForUidMethod != null) { + return vpnForUidMethod.invoke(connectivityService, uid) != null + } } @Suppress("UNCHECKED_CAST") - val networks = XposedHelpers.callMethod(connectivityService, "getVpnUnderlyingNetworks", uid) - as? Array + val networks = getVpnUnderlyingNetworksMethod.invoke(connectivityService, uid) as? Array return networks != null && networks.isNotEmpty() } fun isVpnNetwork(connectivityService: Any, network: Network): Boolean { - val nai = XposedHelpers.callMethod(connectivityService, "getNetworkAgentInfoForNetwork", network) - ?: return false + val nai = getNetworkAgentInfoForNetworkMethod.invoke(connectivityService, network) ?: return false return isVpnNai(nai) } fun isVpnNai(nai: Any): Boolean { - return XposedHelpers.callMethod(nai, "isVPN") as Boolean + return isVPNMethod.invoke(nai) as Boolean } fun getUnderlyingNetwork(connectivityService: Any, uid: Int): Network? { val nai = getUnderlyingNai(connectivityService, uid) ?: return null - return XposedHelpers.callMethod(nai, "network") as Network? + val method = networkMethod + return if (method != null) { + method.invoke(nai) as Network? + } else { + XposedHelpers.getObjectField(nai, "network") as? Network + } } fun getUnderlyingLinkProperties(connectivityService: Any, uid: Int): LinkProperties? { val nai = getUnderlyingNai(connectivityService, uid) ?: return null - val lp = XposedHelpers.getObjectField(nai, "linkProperties") as LinkProperties? - ?: return null + val lp = XposedHelpers.getObjectField(nai, "linkProperties") as LinkProperties? ?: return null return VpnSanitizer.cloneLinkProperties(lp) } fun getUnderlyingNetworkInfo(connectivityService: Any, uid: Int): NetworkInfo? { val nai = getUnderlyingNai(connectivityService, uid) ?: return null - return XposedHelpers.callMethod(connectivityService, "getFilteredNetworkInfo", nai, uid, false) - as NetworkInfo? + val method = getFilteredNetworkInfoMethod + if (method != null) { + return method.invoke(connectivityService, nai, uid, false) as NetworkInfo? + } + return XposedHelpers.getObjectField(nai, "networkInfo") as? NetworkInfo } fun getUnderlyingNai(connectivityService: Any, uid: Int): Any? { @Suppress("UNCHECKED_CAST") - val networks = XposedHelpers.callMethod(connectivityService, "getVpnUnderlyingNetworks", uid) - as? Array + val networks = getVpnUnderlyingNetworksMethod.invoke(connectivityService, uid) as? Array if (networks != null && networks.isNotEmpty()) { - return XposedHelpers.callMethod(connectivityService, "getNetworkAgentInfoForNetwork", networks[0]) + return getNetworkAgentInfoForNetworkMethod.invoke(connectivityService, networks[0]) } - val defaultNai = XposedHelpers.callMethod(connectivityService, "getDefaultNetwork") + val defaultNai = getDefaultNetworkMethod.invoke(connectivityService) if (defaultNai != null && !isVpnNai(defaultNai)) { return defaultNai } return null } + private fun findDeclaredMethod( + target: Class<*>, + name: String, + vararg parameterTypes: Class<*>, + ): Method? { + var current: Class<*>? = target + while (current != null) { + try { + return current.getDeclaredMethod(name, *parameterTypes).apply { isAccessible = true } + } catch (_: NoSuchMethodException) { + current = current.superclass + } + } + return null + } + + private fun requireDeclaredMethod( + target: Class<*>, + name: String, + vararg parameterTypes: Class<*>, + ): Method { + return findDeclaredMethod(target, name, *parameterTypes) + ?: throw NoSuchMethodException("${target.name}#$name") + } + /** * Resolves a class from the Connectivity module, handling APEX package rewriting. * diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt index 710e2c5..339c193 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkCapabilities+writeToParcel.kt @@ -17,6 +17,25 @@ class HookNetworkCapabilitiesWriteToParcel : XHook { private const val SOURCE = "HookNCWriteToParcel" } + private val copyCtor by lazy { + if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { + NetworkCapabilities::class.java.getDeclaredConstructor( + NetworkCapabilities::class.java, + Long::class.javaPrimitiveType + ).apply { isAccessible = true } + } else { + NetworkCapabilities::class.java.getDeclaredConstructor( + NetworkCapabilities::class.java + ).apply { isAccessible = true } + } + } + private val removeTransportTypeMethod by lazy { + NetworkCapabilities::class.java.getMethod("removeTransportType", Int::class.javaPrimitiveType) + } + private val addCapabilityMethod by lazy { + NetworkCapabilities::class.java.getMethod("addCapability", Int::class.javaPrimitiveType) + } + private val inWrite = ThreadLocal.withInitial { false } override fun injectHook() { @@ -58,22 +77,15 @@ class HookNetworkCapabilitiesWriteToParcel : XHook { private fun copyNetworkCapabilities(caps: NetworkCapabilities): NetworkCapabilities { return if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.S) { - val ctor = NetworkCapabilities::class.java.getDeclaredConstructor( - NetworkCapabilities::class.java, - Long::class.javaPrimitiveType!! - ) - ctor.isAccessible = true - ctor.newInstance(caps, 0L) + copyCtor.newInstance(caps, 0L) as NetworkCapabilities } else { - val ctor = NetworkCapabilities::class.java.getDeclaredConstructor(NetworkCapabilities::class.java) - ctor.isAccessible = true - ctor.newInstance(caps) + copyCtor.newInstance(caps) as NetworkCapabilities } } private fun sanitizeNetworkCapabilities(caps: NetworkCapabilities) { - XposedHelpers.callMethod(caps, "removeTransportType", NetworkCapabilities.TRANSPORT_VPN) - XposedHelpers.callMethod(caps, "addCapability", NetworkCapabilities.NET_CAPABILITY_NOT_VPN) + removeTransportTypeMethod.invoke(caps, NetworkCapabilities.TRANSPORT_VPN) + addCapabilityMethod.invoke(caps, NetworkCapabilities.NET_CAPABILITY_NOT_VPN) clearVpnTransportInfo(caps) clearUnderlyingNetworks(caps) clearOwnerUid(caps) diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt index 2402841..77cbfdb 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpn/NetworkInterface+getName.kt @@ -31,6 +31,11 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook private const val IFF_UP = 0x1 } + private val netlinkSocketAddressClass by lazy { Class.forName("android.system.NetlinkSocketAddress") } + private val netlinkSocketAddressCtor by lazy { + netlinkSocketAddressClass.getConstructor(Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) + } + private val seq = AtomicInteger(1) override fun injectHook() { @@ -223,9 +228,7 @@ class HookNetworkInterfaceGetName(private val classLoader: ClassLoader) : XHook } private fun buildNetlinkAddress(): SocketAddress { - val cls = Class.forName("android.system.NetlinkSocketAddress") - val ctor = cls.getConstructor(Int::class.javaPrimitiveType, Int::class.javaPrimitiveType) - return ctor.newInstance(0, 0) as SocketAddress + return netlinkSocketAddressCtor.newInstance(0, 0) as SocketAddress } private fun buildLinkMessage( diff --git a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt index 40caedd..28ee53a 100644 --- a/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt +++ b/app/src/main/java/io/nekohasekai/sfa/xposed/hooks/hidevpnapp/PackageManager+getInstalledPackages.kt @@ -13,6 +13,7 @@ import io.nekohasekai.sfa.xposed.PrivilegeSettingsStore import io.nekohasekai.sfa.xposed.VpnAppStore import io.nekohasekai.sfa.xposed.hooks.SafeMethodHook import io.nekohasekai.sfa.xposed.hooks.XHook +import java.lang.reflect.Method class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoader) : XHook { private companion object { @@ -20,6 +21,10 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade private const val PER_USER_RANGE = 100000 } + @Volatile + private var lastPackageNameClass: Class<*>? = null + private var getPackageNameMethod: Method? = null + override fun injectHook() { val hooked = ArrayList() val sdk = Build.VERSION.SDK_INT @@ -238,7 +243,15 @@ class HookPackageManagerGetInstalledPackages(private val classLoader: ClassLoade private fun extractPackageName(arg: Any?): String? { if (arg == null) return null try { - val method = arg.javaClass.getMethod("getPackageName") + val argClass = arg.javaClass + val method = if (lastPackageNameClass == argClass && getPackageNameMethod != null) { + getPackageNameMethod!! + } else { + argClass.getMethod("getPackageName").also { + lastPackageNameClass = argClass + getPackageNameMethod = it + } + } val result = method.invoke(arg) as String? if (!result.isNullOrEmpty()) { return result