diff --git a/README.md b/README.md index 231de0a..37a676e 100644 --- a/README.md +++ b/README.md @@ -8,13 +8,12 @@ Hide an active Android VPN connection from selected apps. Three components work |-----------|------|-----| | **[zygisk/](zygisk/)** | Zygisk module (Rust) | Inline-hooks `libc.so` via [shadowhook](https://github.com/nicknisi/nicknisi): `ioctl`, `getifaddrs`, `openat` (`/proc/net/*`), `recvmsg` (netlink). Catches every caller regardless of load order — including Flutter/Dart and late-loaded native libs. | | **[lsposed/](lsposed/)** | LSPosed/Xposed module (Kotlin) | Hooks Java network APIs in app processes (`NetworkCapabilities`, `NetworkInterface`, `LinkProperties`, etc.) and `writeToParcel` in `system_server` for cross-process Binder filtering. | -| **[kmod/](kmod/)** | Kernel module (C) | `kretprobe` hooks on `dev_ioctl`, `rtnl_fill_ifinfo`, `fib_route_seq_show`. Invisible to any userspace anti-tamper SDK — including MIR HCE (Russian banking NFC payments). | +| **[kmod/](kmod/)** | Kernel module (C) | `kretprobe` hooks on `dev_ioctl`, `rtnl_fill_ifinfo`, `fib_route_seq_show`. Invisible to any userspace anti-tamper SDK. | ## Which modules do I need? -- **Most apps** (Шоколадница, Flutter apps, simple VPN checks): `zygisk` alone is enough. -- **Apps using Java network APIs** (connectivity checks via `NetworkCapabilities`): add `lsposed`. -- **Banking apps with MIR SDK** (Alfa-Bank, T-Bank, Yandex Bank): use `kmod` + `lsposed`. These apps detect userspace hooks via raw `svc #0` syscalls and ELF integrity checks — only kernel-level filtering is invisible to them. +- **Most apps**: `zygisk` + `lsposed`. Almost all apps check VPN status through Java network APIs (`NetworkCapabilities`, `NetworkInterface`, etc.), so both modules are needed for full coverage. +- **Apps with aggressive anti-tamper SDKs**: use `kmod` + `lsposed`. Some SDKs detect userspace hooks via raw `svc #0` syscalls and ELF integrity checks — only kernel-level filtering is invisible to them. ## Configuration @@ -33,9 +32,10 @@ Each component has its own build system: ## Verified against -- **RKNHardering** — all detection vectors clean -- **YourVPNDead** — all detection vectors clean -- Both implement the official Russian Ministry of Digital Development VPN/proxy detection methodology. +- [RKNHardering](https://github.com/xtclovver/RKNHardering/) — all detection vectors clean +- [YourVPNDead](https://github.com/loop-uh/yourvpndead) — all detection vectors clean + +Both implement the official Russian Ministry of Digital Development VPN/proxy detection methodology. ## Split tunneling @@ -45,4 +45,4 @@ Works correctly with split-tunnel VPN configurations. Only the apps in the targe - `kmod` requires a GKI kernel with `CONFIG_KPROBES=y` (standard on Pixel 6–9a with `android14-6.1`) - `lsposed` requires LSPosed or a compatible Xposed framework -- MIR SDK's custom VM bytecode engine could theoretically be updated to detect kernel-level filtering, but this hasn't been observed in practice +- Some anti-tamper SDKs could theoretically be updated to detect kernel-level filtering, but this hasn't been observed in practice diff --git a/kmod/BUILDING.md b/kmod/BUILDING.md index 6627d7a..899888b 100644 --- a/kmod/BUILDING.md +++ b/kmod/BUILDING.md @@ -281,7 +281,7 @@ adb push vpnhide-kmod.zip /sdcard/Download/ - The target app's UID changes on reinstall — re-resolve via WebUI. **NFC payment broken with module active** -- Remove the banking app from targets. The kernel module's ioctl - filtering can trigger MIR SDK's silent integrity degradation on - some apps. Use system_server hooks (vpnhide LSPosed) for Java-side - coverage instead. +- Remove the target app from targets. The kernel module's ioctl + filtering can trigger some anti-tamper SDKs' silent integrity + degradation. Use system_server hooks (vpnhide LSPosed) for + Java-side coverage instead. diff --git a/kmod/README.md b/kmod/README.md index c423713..ecd7386 100644 --- a/kmod/README.md +++ b/kmod/README.md @@ -5,15 +5,15 @@ Android apps by intercepting kernel-level network operations via kretprobes. Unlike userspace hooking (Zygisk inline hooks, LSPosed), this approach leaves **zero footprint** in the target app's process — no modified function prologues, no Xposed framework classes, no -anonymous memory regions — making it invisible to anti-tamper SDKs -such as NSPK's MIR HCE (used in Russian banking apps for NFC +anonymous memory regions — making it invisible to aggressive +anti-tamper SDKs (such as those used in banking apps for NFC contactless payments). Part of a three-module suite for hiding VPN on Android: - [okhsunrog/vpnhide](https://github.com/okhsunrog/vpnhide) — LSPosed module for Java API hooks. Has app-process mode (default) and - system_server mode (for MIR SDK apps, managed through this module's + system_server mode (for anti-tamper SDK apps, managed through this module's WebUI). - [okhsunrog/vpnhide-zygisk](https://github.com/okhsunrog/vpnhide-zygisk) — Zygisk module for libc inline hooks (`ioctl`, `getifaddrs`). Works @@ -37,9 +37,8 @@ All filtering is **per-UID**: only processes whose UID appears in ## Why not just use vpnhide-zygisk? -Banking apps that bundle NSPK's MIR HCE SDK (Alfa-Bank, T-Bank, -Yandex Bank, and likely others) have aggressive native anti-tamper -that detects: +Apps that bundle aggressive anti-tamper SDKs (common in banking apps) +have native anti-tamper that detects: - **LSPosed/Xposed** — ART method entry point trampolines → hard crash in `LibContentProvider.attachInfo()` @@ -48,7 +47,7 @@ that detects: silent NFC contactless payment degradation (no crash, payment just stops working) -The MIR SDK reads `/proc/self/maps` via **raw `svc #0` syscalls** +The anti-tamper SDK reads `/proc/self/maps` via **raw `svc #0` syscalls** (bypassing any libc hook) and checks ELF relocation integrity. No userspace interposition can hide from it. @@ -62,12 +61,12 @@ are completely untouched. Pixel 8 Pro, crDroid 12.8, Android 16 (API 36), kernel 6.1.145-android14-11: -- **Шоколадница** (Flutter, libc ioctl detection): VPN hidden ✅ -- **Yandex Bank** (MIR HCE SDK): launches without crash, NFC - contactless payment works ✅ +- **Flutter app with native VPN detection** (libc ioctl): VPN hidden ✅ +- **Banking app with aggressive anti-tamper SDK**: launches without + crash, NFC contactless payment works ✅ - Both **RKNHardering** and **YourVPNDead** detection apps report clean (when combined with LSPosed companion for Java API coverage - on non-MIR-SDK apps) + on non-anti-tamper-SDK apps) ## GKI compatibility @@ -263,7 +262,7 @@ All changes apply immediately — no reboot needed. **Shell:** ```bash # Write package names to the persistent config -adb shell su -c 'echo "com.yandex.bank" > /data/adb/vpnhide_kmod/targets.txt' +adb shell su -c 'echo "com.example.targetapp" > /data/adb/vpnhide_kmod/targets.txt' # Or write UIDs directly to the kernel module adb shell su -c 'echo 10423 > /proc/vpnhide_targets' @@ -279,9 +278,9 @@ adb shell su -c 'echo 10423 > /proc/vpnhide_targets' ## Combined use with system_server hooks -For banking apps with MIR HCE SDK (Alfa-Bank, T-Bank, Yandex Bank), +For apps with aggressive anti-tamper SDKs (common in banking apps), full VPN hiding requires covering both native and Java API detection -paths — without placing any hooks in the banking app's process: +paths — without placing any hooks in the target app's process: - **vpnhide-kmod** (this module) covers the native side: `ioctl` (`SIOCGIFFLAGS` / `SIOCGIFNAME` / `SIOCGIFCONF`), `getifaddrs()` @@ -292,8 +291,8 @@ paths — without placing any hooks in the banking app's process: `NetworkInfo.writeToParcel()`, `LinkProperties.writeToParcel()` — stripping VPN data before Binder serialization reaches the app. -Together they provide complete VPN hiding for banking apps without any -hooks in the bank's process. The MIR SDK cannot detect either +Together they provide complete VPN hiding without any hooks in the +target app's process. The anti-tamper SDK cannot detect either component. ### Setup @@ -305,12 +304,12 @@ component. both the kernel module and the system_server hooks. 4. **Remove** banking apps from vpnhide's LSPosed app-process scope (if they were added previously). Only "System Framework" should be - in scope for MIR SDK apps — loading the module into the banking - app's process will trigger MIR SDK's anti-tamper detection. + in scope for anti-tamper SDK apps — loading the module into the + target app's process will trigger the SDK's anti-tamper detection. -For non-MIR-SDK apps, the standard combination of vpnhide (app-process -hooks) + vpnhide-zygisk provides more complete Java + native coverage -and does not require this kernel module. +For apps without aggressive anti-tamper SDKs, the standard combination +of vpnhide (app-process hooks) + vpnhide-zygisk provides more complete +Java + native coverage and does not require this kernel module. ## Architecture notes diff --git a/lsposed/README.md b/lsposed/README.md index 46287bf..676f2db 100644 --- a/lsposed/README.md +++ b/lsposed/README.md @@ -25,8 +25,8 @@ VPN interface is up on the device. > install either one alone, but for full coverage of both the Java and > native stacks you want both installed together. > -> For **banking apps with MIR HCE SDK** (Alfa-Bank, T-Bank, Yandex Bank) -> where app-process hooks cause crashes or NFC payment degradation, use +> For **apps with aggressive anti-tamper SDKs** where app-process hooks +> cause crashes or NFC payment degradation, use > [okhsunrog/vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) > (kernel module) for native-side coverage instead of vpnhide-zygisk, > and enable this module's **system_server mode** (see below) for @@ -36,7 +36,7 @@ VPN interface is up on the device. ### Verified against third-party detection apps With **this module + [okhsunrog/vpnhide-zygisk](https://github.com/okhsunrog/vpnhide-zygisk)** -(or [okhsunrog/vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) for MIR SDK apps) +(or [okhsunrog/vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) for anti-tamper SDK apps) installed, and WireGuard running in **split-tunnel** mode (so the detection apps' own HTTPS probes go out through the carrier, not the tunnel), the following popular Russian "is there a VPN on this device?" @@ -110,17 +110,16 @@ The complementary native side (`getifaddrs`, `ioctl`, `/proc/net/*` read by C/C++/JNI/Flutter that bypasses ART entirely) is the responsibility of [vpnhide-zygisk](https://github.com/okhsunrog/vpnhide-zygisk) or [vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) (for -MIR SDK apps), not this module. +anti-tamper SDK apps), not this module. --- -## system_server mode (for banking apps) +## system_server mode (for apps with anti-tamper SDKs) -Banking apps that bundle NSPK's MIR HCE SDK (Alfa-Bank, T-Bank, -Yandex Bank) crash when LSPosed loads any module into their process -and silently lose NFC contactless payments when vpnhide-zygisk's -inline hooks are present. The default app-process hooks cannot be -used for these apps. +Apps that bundle aggressive anti-tamper SDKs (common in banking apps) +crash when LSPosed loads any module into their process and silently +lose NFC contactless payments when vpnhide-zygisk's inline hooks are +present. The default app-process hooks cannot be used for these apps. **system_server mode** solves this by hooking `writeToParcel()` on `NetworkCapabilities`, `NetworkInfo`, and `LinkProperties` inside @@ -134,8 +133,8 @@ sees the real network state. ### When to use -Use system_server mode when the target app has MIR HCE SDK or other -anti-tamper that detects app-process hooks. For apps without such +Use system_server mode when the target app has an aggressive +anti-tamper SDK that detects app-process hooks. For apps without such SDKs, the default app-process hooks provide more complete coverage. ### How to enable @@ -157,10 +156,11 @@ The WebUI writes UIDs to `/proc/vpnhide_targets` (kernel module) and watches the directory via `FileObserver` (inotify) and reloads the UID list immediately when the file changes — no reboot needed. -**Important:** banking apps with MIR SDK must NOT be added to this -module's LSPosed app-process scope. Only "System Framework" should be -in scope for these apps. The app-process scope is still used for -non-MIR-SDK apps where the default hooks provide better coverage. +**Important:** apps with aggressive anti-tamper SDKs must NOT be added +to this module's LSPosed app-process scope. Only "System Framework" +should be in scope for these apps. The app-process scope is still used +for apps without such SDKs where the default hooks provide better +coverage. --- @@ -297,7 +297,7 @@ companion does — `libc::ioctl` and `libc::getifaddrs` patched in place via ByteDance shadowhook. Install both modules together for full coverage of the Java + native stacks. -For **MIR SDK apps** (banking apps where vpnhide-zygisk's inline hooks +For **anti-tamper SDK apps** (where vpnhide-zygisk's inline hooks cause NFC payment degradation), use [vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) instead — a kernel module that provides the same native filtering via kretprobes diff --git a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt index 5bf89ad..c303332 100644 --- a/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt +++ b/lsposed/app/src/main/kotlin/dev/okhsunrog/vpnhide/HookEntry.kt @@ -89,16 +89,18 @@ class HookEntry : IXposedHookLoadPackage { return } - // MIR SDK apps: skip entirely. - val hasMirSdk = try { + // Apps with aggressive anti-tamper SDKs: skip in-process hooks entirely. + // These SDKs detect modified function prologues and hooking framework + // memory regions. Use vpnhide-kmod + system_server hooks instead. + val hasAntiTamperSdk = try { lpparam.classLoader.loadClass("ru.nspk.mir.hce.sdk.LibContentProvider") true } catch (_: ClassNotFoundException) { false } - if (hasMirSdk) { - XposedBridge.log("VpnHide: MIR SDK in ${lpparam.packageName}, skipping (use vpnhide-kmod + system_server hooks)") + if (hasAntiTamperSdk) { + XposedBridge.log("VpnHide: anti-tamper SDK in ${lpparam.packageName}, skipping (use kmod + system_server hooks)") return } diff --git a/test-app/app/build.gradle.kts b/test-app/app/build.gradle.kts index 42d1311..5acabaa 100644 --- a/test-app/app/build.gradle.kts +++ b/test-app/app/build.gradle.kts @@ -1,6 +1,7 @@ plugins { alias(libs.plugins.android.application) alias(libs.plugins.kotlin.android) + alias(libs.plugins.compose.compiler) } android { @@ -40,6 +41,10 @@ android { jvmTarget = "17" } + buildFeatures { + compose = true + } + externalNativeBuild { cmake { path = file("src/main/cpp/CMakeLists.txt") @@ -50,6 +55,10 @@ android { dependencies { implementation(libs.core.ktx) - implementation(libs.appcompat) - implementation(libs.recyclerview) + implementation(libs.activity.compose) + implementation(platform(libs.compose.bom)) + implementation(libs.compose.ui) + implementation(libs.compose.material3) + implementation(libs.compose.ui.tooling.preview) + debugImplementation(libs.compose.ui.tooling) } diff --git a/test-app/app/src/main/AndroidManifest.xml b/test-app/app/src/main/AndroidManifest.xml index c8fc2bb..76c80bf 100644 --- a/test-app/app/src/main/AndroidManifest.xml +++ b/test-app/app/src/main/AndroidManifest.xml @@ -7,7 +7,7 @@ + android:theme="@android:style/Theme.Material.Light.NoActionBar"> vpn_found; struct ifreq* it = ifc.ifc_req; int count = ifc.ifc_len / sizeof(struct ifreq); + std::vector all_ifaces; + std::vector vpn_found; for (int i = 0; i < count; i++) { const char* name = it[i].ifr_name; - LOGI("SIOCGIFCONF: found interface '%s'", name); + all_ifaces.push_back(name); + LOGI(" SIOCGIFCONF: interface '%s'", name); if (is_vpn_iface(name)) { vpn_found.push_back(name); } } + std::string all_list; + for (auto& n : all_ifaces) { if (!all_list.empty()) all_list += ", "; all_list += n; } + + std::string result; if (vpn_found.empty()) { - return to_jstring(env, "PASS"); + result = "PASS: " + std::to_string(count) + " interfaces visible: [" + all_list + "], none are VPN"; + } else { + std::string vpn_list; + for (auto& n : vpn_found) { if (!vpn_list.empty()) vpn_list += ", "; vpn_list += n; } + result = "FAIL: VPN interfaces found: [" + vpn_list + "] in full list: [" + all_list + "]"; } - std::string detail = "FAIL: VPN interfaces found:"; - for (auto& n : vpn_found) detail += " " + n; - return to_jstring(env, detail); + LOGI("RESULT: %s", result.c_str()); + return to_jstring(env, result); } // 3. getifaddrs extern "C" JNIEXPORT jstring JNICALL Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkGetifaddrs(JNIEnv* env, jobject) { + LOGI("=== CHECK: getifaddrs() enumeration ==="); struct ifaddrs* addrs = nullptr; if (getifaddrs(&addrs) != 0) { - return to_jstring(env, "FAIL: getifaddrs error: " + std::string(strerror(errno))); + std::string r = "FAIL: getifaddrs error: " + std::string(strerror(errno)); + LOGI("RESULT: %s", r.c_str()); + return to_jstring(env, r); } + std::vector all_ifaces; std::vector vpn_found; for (struct ifaddrs* ifa = addrs; ifa; ifa = ifa->ifa_next) { - if (ifa->ifa_name) { - LOGI("getifaddrs: found interface '%s'", ifa->ifa_name); - if (is_vpn_iface(ifa->ifa_name)) { - // Deduplicate - bool dup = false; - for (auto& n : vpn_found) if (n == ifa->ifa_name) { dup = true; break; } - if (!dup) vpn_found.push_back(ifa->ifa_name); - } + if (!ifa->ifa_name) continue; + // Deduplicate for display + bool seen = false; + for (auto& n : all_ifaces) if (n == ifa->ifa_name) { seen = true; break; } + if (!seen) { + all_ifaces.push_back(ifa->ifa_name); + LOGI(" getifaddrs: interface '%s' (family=%d, flags=0x%x)", + ifa->ifa_name, ifa->ifa_addr ? ifa->ifa_addr->sa_family : -1, ifa->ifa_flags); + } + if (is_vpn_iface(ifa->ifa_name)) { + bool dup = false; + for (auto& n : vpn_found) if (n == ifa->ifa_name) { dup = true; break; } + if (!dup) vpn_found.push_back(ifa->ifa_name); } } freeifaddrs(addrs); + std::string all_list; + for (auto& n : all_ifaces) { if (!all_list.empty()) all_list += ", "; all_list += n; } + + std::string result; if (vpn_found.empty()) { - return to_jstring(env, "PASS"); + result = "PASS: " + std::to_string(all_ifaces.size()) + " unique interfaces: [" + all_list + "], none are VPN"; + } else { + std::string vpn_list; + for (auto& n : vpn_found) { if (!vpn_list.empty()) vpn_list += ", "; vpn_list += n; } + result = "FAIL: VPN interfaces: [" + vpn_list + "] in full list: [" + all_list + "]"; } - std::string detail = "FAIL: VPN interfaces found:"; - for (auto& n : vpn_found) detail += " " + n; - return to_jstring(env, detail); + LOGI("RESULT: %s", result.c_str()); + return to_jstring(env, result); } -// Helper: read a proc file and check for VPN interface lines +// Helper: read a proc file and return detailed info static std::string check_proc_file(const char* path) { + LOGI("=== CHECK: %s (native read) ===", path); int fd = open(path, O_RDONLY); if (fd < 0) { - return "FAIL: cannot open " + std::string(path) + ": " + strerror(errno); + std::string r = "FAIL: cannot open " + std::string(path) + ": " + strerror(errno); + LOGI("RESULT: %s", r.c_str()); + return r; } char buf[8192]; @@ -148,6 +191,7 @@ static std::string check_proc_file(const char* path) { } close(fd); + int total_lines = 0; std::vector vpn_lines; size_t pos = 0; while (pos < content.size()) { @@ -155,22 +199,28 @@ static std::string check_proc_file(const char* path) { if (eol == std::string::npos) eol = content.size(); std::string line = content.substr(pos, eol - pos); pos = eol + 1; + if (line.empty()) continue; + total_lines++; + + LOGI(" %s line: %s", path, line.substr(0, 120).c_str()); - // Check if line contains a VPN interface name for (int i = 0; i < NUM_PREFIXES; i++) { if (line.find(VPN_PREFIXES[i]) != std::string::npos) { - vpn_lines.push_back(line.substr(0, 40)); + vpn_lines.push_back(line.substr(0, 80)); break; } } } + std::string result; if (vpn_lines.empty()) { - return "PASS"; + result = "PASS: " + std::to_string(total_lines) + " lines in " + path + ", no VPN entries"; + } else { + result = "FAIL: " + std::to_string(vpn_lines.size()) + " VPN lines in " + path + ":"; + for (auto& l : vpn_lines) result += "\n " + l; } - std::string detail = "FAIL: VPN lines found in " + std::string(path) + ":"; - for (auto& l : vpn_lines) detail += "\n " + l; - return detail; + LOGI("RESULT: %s", result.c_str()); + return result; } // 4. /proc/net/route @@ -188,9 +238,12 @@ Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkProcNetIfInet6(JNIEnv* env, jo // 6. Netlink RTM_GETLINK extern "C" JNIEXPORT jstring JNICALL Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink(JNIEnv* env, jobject) { + LOGI("=== CHECK: netlink RTM_GETLINK dump ==="); int fd = socket(AF_NETLINK, SOCK_RAW, NETLINK_ROUTE); if (fd < 0) { - return to_jstring(env, "FAIL: cannot create netlink socket: " + std::string(strerror(errno))); + std::string r = "FAIL: cannot create netlink socket: " + std::string(strerror(errno)); + LOGI("RESULT: %s", r.c_str()); + return to_jstring(env, r); } struct sockaddr_nl sa{}; @@ -198,10 +251,11 @@ Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink(JNIEnv* env, jo if (bind(fd, (struct sockaddr*)&sa, sizeof(sa)) < 0) { int err = errno; close(fd); - return to_jstring(env, "FAIL: bind error: " + std::string(strerror(err))); + std::string r = "FAIL: bind error: " + std::string(strerror(err)); + LOGI("RESULT: %s", r.c_str()); + return to_jstring(env, r); } - // Build RTM_GETLINK request struct { struct nlmsghdr nlh; struct ifinfomsg ifm; @@ -216,11 +270,13 @@ Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink(JNIEnv* env, jo if (send(fd, &req, req.nlh.nlmsg_len, 0) < 0) { int err = errno; close(fd); - return to_jstring(env, "FAIL: send error: " + std::string(strerror(err))); + std::string r = "FAIL: send error: " + std::string(strerror(err)); + LOGI("RESULT: %s", r.c_str()); + return to_jstring(env, r); } - // Receive and parse char buf[32768]; + std::vector all_ifaces; std::vector vpn_found; bool done = false; @@ -232,14 +288,8 @@ Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink(JNIEnv* env, jo NLMSG_OK(nh, (size_t)len); nh = NLMSG_NEXT(nh, len)) { - if (nh->nlmsg_type == NLMSG_DONE) { - done = true; - break; - } - if (nh->nlmsg_type == NLMSG_ERROR) { - done = true; - break; - } + if (nh->nlmsg_type == NLMSG_DONE) { done = true; break; } + if (nh->nlmsg_type == NLMSG_ERROR) { done = true; break; } if (nh->nlmsg_type != RTM_NEWLINK) continue; struct ifinfomsg* ifi = (struct ifinfomsg*)NLMSG_DATA(nh); @@ -249,7 +299,9 @@ Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink(JNIEnv* env, jo while (RTA_OK(rta, rta_len)) { if (rta->rta_type == IFLA_IFNAME) { const char* name = (const char*)RTA_DATA(rta); - LOGI("netlink RTM_GETLINK: found interface '%s'", name); + all_ifaces.push_back(name); + LOGI(" netlink RTM_NEWLINK: interface '%s' (index=%d, flags=0x%x)", + name, ifi->ifi_index, ifi->ifi_flags); if (is_vpn_iface(name)) { vpn_found.push_back(name); } @@ -258,13 +310,19 @@ Java_dev_okhsunrog_vpnhide_test_NativeChecks_checkNetlinkGetlink(JNIEnv* env, jo } } } - close(fd); + std::string all_list; + for (auto& n : all_ifaces) { if (!all_list.empty()) all_list += ", "; all_list += n; } + + std::string result; if (vpn_found.empty()) { - return to_jstring(env, "PASS"); + result = "PASS: " + std::to_string(all_ifaces.size()) + " interfaces via netlink: [" + all_list + "], none are VPN"; + } else { + std::string vpn_list; + for (auto& n : vpn_found) { if (!vpn_list.empty()) vpn_list += ", "; vpn_list += n; } + result = "FAIL: VPN interfaces: [" + vpn_list + "] in netlink dump: [" + all_list + "]"; } - std::string detail = "FAIL: VPN interfaces found:"; - for (auto& n : vpn_found) detail += " " + n; - return to_jstring(env, detail); + LOGI("RESULT: %s", result.c_str()); + return to_jstring(env, result); } diff --git a/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt b/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt index e0751b9..54c96d4 100644 --- a/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt +++ b/test-app/app/src/main/java/dev/okhsunrog/vpnhide/test/MainActivity.kt @@ -1,273 +1,411 @@ package dev.okhsunrog.vpnhide.test -import android.graphics.Color -import android.graphics.Typeface import android.net.ConnectivityManager import android.net.NetworkCapabilities -import android.os.Build import android.os.Bundle import android.util.Log -import android.util.TypedValue -import android.view.Gravity -import android.widget.Button -import android.widget.LinearLayout -import android.widget.TextView -import androidx.appcompat.app.AppCompatActivity +import androidx.activity.ComponentActivity +import androidx.activity.SystemBarStyle +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.foundation.isSystemInDarkTheme +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp import java.io.BufferedReader import java.io.InputStreamReader import java.net.NetworkInterface -class MainActivity : AppCompatActivity() { - - private lateinit var resultsLayout: LinearLayout - private lateinit var summaryText: TextView - private val resultViews = mutableListOf>() - - companion object { - private const val TAG = "VPNHideTest" - private val VPN_PREFIXES = listOf("tun", "wg", "ppp", "tap") - } - +class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - resultsLayout = findViewById(R.id.resultsLayout) - summaryText = findViewById(R.id.summaryText) - val runButton = findViewById