From 60b3235dc0ed0a7fcb57e2a351876969433c42ac Mon Sep 17 00:00:00 2001 From: okhsunrog Date: Sat, 11 Apr 2026 16:20:33 +0300 Subject: [PATCH] docs: redesign README hierarchy for monorepo Root README is now the single source for shared content (verified against, split tunneling, threat model, component overview). Sub-READMEs focus on component-specific technical details and link back to root. - Remove ~700 lines of duplicated content across sub-READMEs - Update all cross-references to use monorepo relative paths - Add test-app to components table - Update zygisk README: mark openat/recvmsg/SIOCGIFCONF as implemented - Fix stale links to archived repos --- README.md | 62 +++++--- kmod/README.md | 316 +++++-------------------------------- lsposed/README.md | 382 ++++++++------------------------------------- zygisk/README.md | 388 ++++++++++------------------------------------ 4 files changed, 226 insertions(+), 922 deletions(-) diff --git a/README.md b/README.md index 37a676e..e016fdc 100644 --- a/README.md +++ b/README.md @@ -1,48 +1,70 @@ # vpnhide -Hide an active Android VPN connection from selected apps. Three components work together to cover all detection vectors — from Java APIs down to kernel syscalls. +Hide an active Android VPN connection from selected apps. + +Three components work together to cover all detection vectors -- from Java APIs down to kernel syscalls. A diagnostic test app verifies everything works. ## Components | Directory | What | How | |-----------|------|-----| -| **[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. | +| **[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. | +| **[lsposed/](lsposed/)** | LSPosed module (Kotlin) | Hooks Java network APIs (`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. | +| **[test-app/](test-app/)** | Diagnostic app (Kotlin + C++) | 15 checks (6 native + 9 Java) covering all hook vectors. Logs everything to logcat under tag `VPNHideTest` for automated verification. | ## Which modules do I need? -- **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. +- **Most apps**: `zygisk` + `lsposed`. Almost all apps check VPN status through both native and Java APIs, so both modules are needed. +- **Apps with aggressive anti-tamper SDKs**: `kmod` + `lsposed` (system_server mode). Some SDKs detect userspace hooks via raw `svc #0` syscalls and ELF integrity checks -- only kernel-level filtering is invisible to them. +- **To verify your setup**: install `test-app`, add it to target lists, run with VPN active -- all checks should be green. ## Configuration -All three modules share a target list. Use the WebUI (KernelSU/Magisk manager → module settings) to select which apps should not see the VPN. The WebUI writes to: -- `targets.txt` — package names (read by zygisk and lsposed) -- `/proc/vpnhide_targets` — resolved UIDs (read by kmod) -- `/data/system/vpnhide_uids.txt` — resolved UIDs (read by lsposed system_server hooks) +All three modules share a target list. Use the WebUI (KernelSU/Magisk manager -> module settings) to select which apps should not see the VPN. The WebUI writes to: +- `targets.txt` -- package names (read by zygisk and lsposed) +- `/proc/vpnhide_targets` -- resolved UIDs (read by kmod) +- `/data/system/vpnhide_uids.txt` -- resolved UIDs (read by lsposed system_server hooks) ## Building -Each component has its own build system: - -- **zygisk**: `cd zygisk && ./build-zip.sh` (requires Rust + Android NDK + cargo-ndk) -- **lsposed**: `cd lsposed && ./gradlew assembleDebug` (requires JDK 17) -- **kmod**: `cd kmod && ./build-zip.sh` (requires kernel source + clang cross-compiler). See [kmod/BUILDING.md](kmod/BUILDING.md) for details. +- **zygisk**: `cd zygisk && ./build-zip.sh` (Rust + NDK + cargo-ndk). See [zygisk/README.md](zygisk/README.md). +- **lsposed**: `cd lsposed && ./gradlew assembleDebug` (JDK 17). See [lsposed/README.md](lsposed/README.md). +- **kmod**: `cd kmod && ./build-zip.sh` (kernel source + cross-compiler). See [kmod/BUILDING.md](kmod/BUILDING.md). +- **test-app**: `cd test-app && ./gradlew assembleDebug` (JDK 17 + NDK for native checks). ## Verified against -- [RKNHardering](https://github.com/xtclovver/RKNHardering/) — all detection vectors clean -- [YourVPNDead](https://github.com/loop-uh/yourvpndead) — all detection vectors clean +- [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. +Both implement the official Russian Ministry of Digital Development VPN/proxy detection methodology ([source](https://t.me/ruitunion/893)). ## Split tunneling -Works correctly with split-tunnel VPN configurations. Only the apps in the target list are affected — all other apps see normal VPN state. +Works correctly with split-tunnel VPN configurations. Only the apps in the target list are affected -- all other apps see normal VPN state. + +Note: detection apps that compare device-reported public IP against external checkers require split tunneling -- the detection app's HTTPS requests must exit through the carrier, not the tunnel. That is a network-layer fact, not something any client-side hook can fix. + +## Threat model + +vpnhide is designed for one scenario: "I have a VPN running and certain apps refuse to work because they detect it. I want those specific apps to think the VPN isn't there." + +It is NOT designed for: +- Hiding root or custom ROM presence +- Bypassing Play Integrity +- Fooling server-side detection (DNS leakage, IP blocklists, latency fingerprinting, TLS fingerprinting) ## Known limitations -- `kmod` requires a GKI kernel with `CONFIG_KPROBES=y` (standard on Pixel 6–9a with `android14-6.1`) +- `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 -- Some anti-tamper SDKs could theoretically be updated to detect kernel-level filtering, but this hasn't been observed in practice +- `zygisk` is arm64 only +- Direct `svc #0` syscalls bypass zygisk's libc hooks (that is what kmod is for) +- Server-side detection is unfixable client-side -- use split tunneling + +## License + +- **zygisk**: 0BSD +- **lsposed**: unlicensed (do whatever you want) +- **kmod**: GPL-2.0 (required for kernel modules) diff --git a/kmod/README.md b/kmod/README.md index ecd7386..8c091b2 100644 --- a/kmod/README.md +++ b/kmod/README.md @@ -1,27 +1,8 @@ -# vpnhide-kmod +# vpnhide -- Kernel module -A Linux kernel module that hides VPN network interfaces from selected -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 aggressive -anti-tamper SDKs (such as those used in banking apps for NFC -contactless payments). +kretprobe-based kernel module that hides VPN interfaces from selected apps. Part of [vpnhide](../README.md). -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 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 - for apps without anti-tamper SDKs. -- **This module** — kernel-level kretprobes covering the same native - detection paths as vpnhide-zygisk, but with zero footprint in - userspace. Required for apps where userspace hooks trigger anti-tamper - crashes or silent NFC payment degradation. +Zero footprint in the target app's process -- no modified function prologues, no framework classes, no anonymous memory regions. Invisible to aggressive anti-tamper SDKs. ## What it hooks @@ -31,65 +12,27 @@ Part of a three-module suite for hiding VPN on Android: | `rtnl_fill_ifinfo` | Returns `-EMSGSIZE` for VPN devices during RTM_GETLINK netlink dumps, causing the kernel to skip them | `getifaddrs()` (which uses netlink internally), any netlink-based interface enumeration | | `fib_route_seq_show` | Rewinds `seq->count` to hide lines with VPN interface names | `/proc/net/route` reads | -All filtering is **per-UID**: only processes whose UID appears in -`/proc/vpnhide_targets` see the filtered view. Everyone else -(system services, VPN client, NFC subsystem) sees the real data. +All filtering is **per-UID**: only processes whose UID appears in `/proc/vpnhide_targets` see the filtered view. Everyone else (system services, VPN client, NFC subsystem) sees the real data. -## Why not just use vpnhide-zygisk? +## Why kernel-level? -Apps that bundle aggressive anti-tamper SDKs (common in banking apps) -have native anti-tamper that detects: +Some anti-tamper SDKs read `/proc/self/maps` via raw `svc #0` syscalls (bypassing any libc hook) and check ELF relocation integrity. No userspace interposition can hide from them. -- **LSPosed/Xposed** — ART method entry point trampolines → - hard crash in `LibContentProvider.attachInfo()` -- **shadowhook inline hooks** — modified function prologues in - libc.so + `[anon:shadowhook-*]` regions in `/proc/self/maps` → - silent NFC contactless payment degradation (no crash, payment - just stops working) - -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. - -A kernel module operates below the SDK's inspection capability: -kretprobes modify kernel function behavior, not userspace code. -The target app's process memory, ELF tables, and `/proc/self/maps` -are completely untouched. - -## Verified working - -Pixel 8 Pro, crDroid 12.8, Android 16 (API 36), kernel -6.1.145-android14-11: - -- **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-anti-tamper-SDK apps) +Kernel kretprobes modify kernel function behavior, not userspace code. The target app's process memory, ELF tables, and `/proc/self/maps` are completely untouched. ## GKI compatibility -The module is built against the Android Common Kernel (ACK) source -for `android14-6.1`. All symbols it uses (`register_kretprobe`, -`proc_create`, `seq_read`, etc.) are part of the stable GKI KMI, -so the same `Module.symvers` CRCs work across all devices running -the same GKI generation. +The module is built against the Android Common Kernel (ACK) source for `android14-6.1`. All symbols it uses (`register_kretprobe`, `proc_create`, `seq_read`, etc.) are part of the stable GKI KMI, so the same `Module.symvers` CRCs work across all devices running the same GKI generation. -KernelSU bypasses the kernel's vermagic check, so no runtime -patching is needed. `post-fs-data.sh` simply runs `insmod` directly. +KernelSU bypasses the kernel's vermagic check, so no runtime patching is needed. `post-fs-data.sh` simply runs `insmod` directly. ### Current build target -- **`android14-6.1`** — Pixel 8/9 series, Samsung Galaxy S24/S25, - OnePlus 12/13, Xiaomi 14/15, and most 2024 flagships on - Android 14/15. +- **`android14-6.1`** -- Pixel 8/9 series, Samsung Galaxy S24/S25, OnePlus 12/13, Xiaomi 14/15, and most 2024 flagships on Android 14/15. ### TODO: multi-generation support -The C source is the same across GKI generations — only the -`Module.symvers` CRCs and kernel headers differ. To support other -generations, build against the corresponding ACK branch: +The C source is the same across GKI generations -- only the `Module.symvers` CRCs and kernel headers differ. To support other generations, build against the corresponding ACK branch: | GKI generation | ACK branch | Devices | |---|---|---| @@ -99,165 +42,38 @@ generations, build against the corresponding ACK branch: | `android15-6.1` | `android15-6.1` | Pixel 8/9 on Android 15 QPR | | `android15-6.6` | `android15-6.6` | Future devices | -Each generation needs a separate `.ko`. The build steps are -identical — only the kernel source checkout and `Module.symvers` -change. A future CI matrix build could produce all variants from -one commit. +Each generation needs a separate `.ko`. The build steps are identical -- only the kernel source checkout and `Module.symvers` change. A future CI matrix build could produce all variants from one commit. ## Build -### Prerequisites +See [BUILDING.md](BUILDING.md) for the full guide (kernel source preparation, toolchain setup, `Module.symvers` generation). -- Android Common Kernel source for `android14-6.1`: - ```bash - # If you have the Pixel kernel tree: - # The source is at /aosp/ with remote - # https://android.googlesource.com/kernel/common - # - # Or clone directly: - git clone --depth=1 -b android14-6.1 \ - https://android.googlesource.com/kernel/common \ - /path/to/kernel-source - ``` - -- Android clang toolchain (ships with the kernel tree under - `prebuilts/clang/host/linux-x86/clang-r*`) - -- The kernel source must be **prepared** before building modules. - This requires several steps (documented below). - -### Preparing the kernel source - -The kernel source needs `.config`, generated headers, and -`Module.symvers` before out-of-tree modules can compile. - -**1. Set `.config`:** -Pull the running kernel's config from a device: -```bash -adb shell "su -c 'gzip -d < /proc/config.gz'" > /path/to/kernel-source/.config -``` -Or use the GKI defconfig: -```bash -cd /path/to/kernel-source -make ARCH=arm64 LLVM=1 gki_defconfig -``` - -**2. Generate headers (`make prepare`):** -```bash -CLANG=/path/to/prebuilts/clang/host/linux-x86/clang-r487747c/bin - -# Create empty abi_symbollist if missing (GKI build expects it) -touch abi_symbollist.raw - -make ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld AR=$CLANG/llvm-ar \ - NM=$CLANG/llvm-nm OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - olddefconfig prepare -``` - -If `make prepare` fails on `tools/bpf/resolve_btfids` (common with -system clang version mismatches), the module can still build — the -error only affects BTF generation which is optional. - -**3. Generate `Module.symvers`:** -The CRCs must match the running kernel. Extract them from existing -`.ko` modules shipped with your ROM: +Quick version: ```bash -# Get the prebuilt .ko files from your ROM's kernel package -# (e.g., from the device tree's -kernels repo) -for ko in /path/to/prebuilt/*.ko; do - modprobe --dump-modversions "$ko" 2>/dev/null -done | sort -u -k2 | \ - awk '{printf "%s\t%s\tvmlinux\tEXPORT_SYMBOL\t\n", $1, $2}' \ - > /path/to/kernel-source/Module.symvers +cd kmod && ./build-zip.sh ``` -**4. Generate `scripts/module.lds`:** -```bash -$CLANG/clang -E -Wp,-MD,scripts/.module.lds.d -nostdinc \ - -I arch/arm64/include -I arch/arm64/include/generated \ - -I include -I include/generated \ - -include include/linux/kconfig.h \ - -D__KERNEL__ -DCC_USING_PATCHABLE_FUNCTION_ENTRY \ - --target=aarch64-linux-gnu -x c scripts/module.lds.S \ - 2>/dev/null | grep -v '^#' > scripts/module.lds - -# Fix ARM64 page-size literal that ld.lld can't parse -sed -i 's/((1UL) << 12)/4096/g' scripts/module.lds -``` - -**5. Set vermagic:** -For a universal build with runtime vermagic patching, use a long -placeholder: -```bash -PLACEHOLDER="6.1.999-placeholder-$(printf 'x%.0s' {1..100})" -echo "#define UTS_RELEASE \"$PLACEHOLDER\"" \ - > include/generated/utsrelease.h -echo -n "$PLACEHOLDER" > include/config/kernel.release -``` - -For a device-specific build, use the exact running kernel version: -```bash -KVER="$(adb shell uname -r)" -echo "#define UTS_RELEASE \"$KVER\"" \ - > include/generated/utsrelease.h -``` - -### Building the module - -```bash -cd /path/to/vpnhide-kmod - -KSRC=/path/to/kernel-source \ -CLANG=/path/to/clang/bin \ -make -C $KSRC M=$(pwd) \ - ARCH=arm64 LLVM=1 LLVM_IAS=1 \ - CC=$CLANG/clang LD=$CLANG/ld.lld \ - AR=$CLANG/llvm-ar NM=$CLANG/llvm-nm \ - OBJCOPY=$CLANG/llvm-objcopy \ - OBJDUMP=$CLANG/llvm-objdump \ - STRIP=$CLANG/llvm-strip \ - CROSS_COMPILE=aarch64-linux-gnu- \ - modules -``` - -Output: `vpnhide_kmod.ko` - -### Building the KSU module zip - -```bash -cp vpnhide_kmod.ko module/ -./build-zip.sh -# Output: vpnhide-kmod.zip -``` +Requires kernel source for `android14-6.1` + clang cross-compiler. ## Install 1. `adb push vpnhide-kmod.zip /sdcard/Download/` -2. KernelSU-Next manager → Modules → Install from storage +2. KernelSU-Next manager -> Modules -> Install from storage 3. Reboot On boot: - `post-fs-data.sh` runs `insmod` to load the kernel module -- `service.sh` resolves package names from - `/data/adb/vpnhide_kmod/targets.txt` to UIDs via - `pm list packages -U` and writes them to `/proc/vpnhide_targets` +- `service.sh` resolves package names from `targets.txt` to UIDs via `pm list packages -U` and writes them to `/proc/vpnhide_targets` -### Picking target apps +### Target management -**WebUI (recommended):** open the module in KernelSU-Next manager -and tap the WebUI entry. Select apps, save. The WebUI writes to -**three places** simultaneously: -1. `targets.txt` — persistent package names (survives module updates) -2. `/proc/vpnhide_targets` — resolved UIDs for the kernel module -3. `/data/system/vpnhide_uids.txt` — resolved UIDs for the - [vpnhide](https://github.com/okhsunrog/vpnhide) LSPosed module's - system_server hooks (live reload via inotify) +**WebUI (recommended):** open the module in KernelSU-Next manager and tap the WebUI entry. Select apps, save. The WebUI writes to **three places** simultaneously: +1. `targets.txt` -- persistent package names (survives module updates) +2. `/proc/vpnhide_targets` -- resolved UIDs for the kernel module +3. `/data/system/vpnhide_uids.txt` -- resolved UIDs for the [lsposed](../lsposed/) module's system_server hooks (live reload via inotify) -All changes apply immediately — no reboot needed. +All changes apply immediately -- no reboot needed. **Shell:** ```bash @@ -268,102 +84,56 @@ adb shell su -c 'echo "com.example.targetapp" > /data/adb/vpnhide_kmod/targets.t adb shell su -c 'echo 10423 > /proc/vpnhide_targets' ``` -### Manual loading (without KSU module) - -```bash -adb push vpnhide_kmod.ko /data/local/tmp/ -adb shell su -c 'insmod /data/local/tmp/vpnhide_kmod.ko' -adb shell su -c 'echo 10423 > /proc/vpnhide_targets' -``` - ## Combined use with system_server hooks -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 target app's process: +For apps with aggressive anti-tamper SDKs, full VPN hiding requires covering both native and Java API detection paths -- without placing any hooks in the target app's process: -- **vpnhide-kmod** (this module) covers the native side: `ioctl` - (`SIOCGIFFLAGS` / `SIOCGIFNAME` / `SIOCGIFCONF`), `getifaddrs()` - (via `rtnl_fill_ifinfo`), and `/proc/net/route` (via - `fib_route_seq_show`). -- **[vpnhide](https://github.com/okhsunrog/vpnhide) system_server - hooks** cover the Java API side: `NetworkCapabilities.writeToParcel()`, - `NetworkInfo.writeToParcel()`, `LinkProperties.writeToParcel()` — - stripping VPN data before Binder serialization reaches the app. +- **vpnhide-kmod** (this module) covers the native side: `ioctl` (`SIOCGIFFLAGS` / `SIOCGIFNAME` / `SIOCGIFCONF`), `getifaddrs()` (via `rtnl_fill_ifinfo`), and `/proc/net/route` (via `fib_route_seq_show`). +- **[lsposed](../lsposed/) system_server hooks** cover the Java API side: `NetworkCapabilities.writeToParcel()`, `NetworkInfo.writeToParcel()`, `LinkProperties.writeToParcel()` -- stripping VPN data before Binder serialization reaches the app. -Together they provide complete VPN hiding without any hooks in the -target app's process. The anti-tamper SDK cannot detect either -component. +Together they provide complete VPN hiding without any hooks in the target app's process. The anti-tamper SDK cannot detect either component. ### Setup 1. Install **vpnhide-kmod** as a KSU module (this module). -2. Install **[vpnhide](https://github.com/okhsunrog/vpnhide)** as an - LSPosed/Vector module and add **"System Framework"** to its scope. -3. Pick target apps in vpnhide-kmod's WebUI — it manages targets for - 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 anti-tamper SDK apps — loading the module into the - target app's process will trigger the SDK's anti-tamper detection. +2. Install **[lsposed](../lsposed/)** as an LSPosed/Vector module and add **"System Framework"** to its scope. +3. Pick target apps in vpnhide-kmod's WebUI -- it manages targets for both the kernel module and the system_server hooks. +4. **Remove** banking apps from lsposed's LSPosed app-process scope (if they were added previously). Only "System Framework" should be 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 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. +For apps without aggressive anti-tamper SDKs, the standard combination of [lsposed](../lsposed/) (app-process hooks) + [zygisk](../zygisk/) provides more complete Java + native coverage and does not require this kernel module. ## Architecture notes ### Why kretprobes work here -kretprobes instrument kernel functions by replacing their return -address on the stack. Unlike userspace inline hooks (which modify -instruction bytes), kretprobes: +kretprobes instrument kernel functions by replacing their return address on the stack. Unlike userspace inline hooks (which modify instruction bytes), kretprobes: -- Don't modify the target function's code in a way visible to - userspace — `/proc/self/maps` and the function's ELF bytes are - unchanged -- Can't be detected by the target app — the app can only inspect - its own process memory, not kernel data structures -- Work on any function visible in `/proc/kallsyms`, including - static (non-exported) functions +- Don't modify the target function's code in a way visible to userspace -- `/proc/self/maps` and the function's ELF bytes are unchanged +- Can't be detected by the target app -- the app can only inspect its own process memory, not kernel data structures +- Work on any function visible in `/proc/kallsyms`, including static (non-exported) functions ### dev_ioctl calling convention (GKI 6.1, arm64) ```c int dev_ioctl(struct net *net, // x0 unsigned int cmd, // x1 - struct ifreq *ifr, // x2 — KERNEL pointer - void __user *data, // x3 — userspace pointer + struct ifreq *ifr, // x2 -- KERNEL pointer + void __user *data, // x3 -- userspace pointer bool *need_copyout) // x4 ``` -**Important:** `x2` is a kernel-space pointer (the caller already -did `copy_from_user`). Using `copy_from_user` on it will EFAULT on -ARM64 with PAN enabled. The return handler reads via direct pointer -dereference. +**Important:** `x2` is a kernel-space pointer (the caller already did `copy_from_user`). Using `copy_from_user` on it will EFAULT on ARM64 with PAN enabled. The return handler reads via direct pointer dereference. ### rtnl_fill_ifinfo trick -To skip a VPN interface during a netlink dump without corrupting -the message stream, the return handler sets the return value to -`-EMSGSIZE`. The dump iterator interprets this as "skb too small -for this entry" and moves to the next device without adding the -current one — effectively skipping it. The entry is never seen by -userspace. +To skip a VPN interface during a netlink dump without corrupting the message stream, the return handler sets the return value to `-EMSGSIZE`. The dump iterator interprets this as "skb too small for this entry" and moves to the next device without adding the current one -- effectively skipping it. The entry is never seen by userspace. ## TODO - [ ] Multi-GKI-generation CI build (see GKI compatibility section) -- [ ] `/proc/net/tcp`, `tcp6` filtering (`tcp4_seq_show` / - `tcp6_seq_show`) — low priority, only matters for proxy-based - VPN clients with open local ports -- [ ] `connect()` filter on localhost proxy ports (`__sys_connect`) - — same caveat as above -- [x] ~~system_server LSPosed hooks~~ — implemented in - [okhsunrog/vpnhide](https://github.com/okhsunrog/vpnhide) and - managed through this module's WebUI +- [ ] `/proc/net/tcp`, `tcp6` filtering (`tcp4_seq_show` / `tcp6_seq_show`) -- low priority, only matters for proxy-based VPN clients with open local ports +- [ ] `connect()` filter on localhost proxy ports (`__sys_connect`) -- same caveat as above ## License -GPL-2.0 (required for kernel modules using GPL-only symbols like -`register_kretprobe`). +GPL-2.0 (required for kernel modules using GPL-only symbols like `register_kretprobe`). diff --git a/lsposed/README.md b/lsposed/README.md index 676f2db..57fb1c9 100644 --- a/lsposed/README.md +++ b/lsposed/README.md @@ -1,194 +1,15 @@ -# VpnHide +# vpnhide -- LSPosed module -An LSPosed / Vector (JingMatrix fork) module that hides an active VPN -connection from selected Android apps, without needing to actually turn the -VPN off. +Hooks Java network APIs to hide VPN presence from selected apps. Part of [vpnhide](../README.md). -Useful if you run a split-tunnel VPN (where only some traffic goes through -the tunnel) but certain apps still refuse to work when they see that *any* -VPN interface is up on the device. +## Modes -> **Status:** experimental / personal-use. Tested baseline is crDroid 12.8 -> (Android 16) on a Pixel 8 Pro with KernelSU-Next (LKM) + NeoZygisk + -> JingMatrix Vector, but the module itself is just an LSPosed/Vector -> hook plugin and runs on any LSPosed/Vector v93+ deployment regardless -> of the underlying root provider — Magisk, KernelSU, KernelSU-Next, -> APatch, with or without ZygiskNext / NeoZygisk / Magisk's built-in -> Zygisk. - -> **Companion modules:** this LSPosed module covers the Java / Android -> framework side. For the **native** detection path — apps that check -> for a VPN from C/C++/JNI/Flutter via `libc::ioctl`, `getifaddrs()`, -> `/proc/net/*` and never enter ART — use the matching Zygisk module -> [okhsunrog/vpnhide-zygisk](https://github.com/okhsunrog/vpnhide-zygisk). -> The two modules are independent and share no runtime state; you can -> install either one alone, but for full coverage of both the Java and -> native stacks you want both installed together. -> -> 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 -> Java API coverage. Neither component places anything in the banking -> app's process. - -### 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 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?" -apps report **all clean**, with no direct or indirect signals -triggered: - -- [xtclovver/RKNHardering](https://github.com/xtclovver/RKNHardering) — - the Kotlin app implementing the Russian Ministry of Digital - Development's VPN-detection methodology. All GeoIP, IP comparison, - Direct signs (`TRANSPORT_VPN`, HTTP/SOCKS proxy), Indirect signs - (`NET_CAPABILITY_NOT_VPN`, interface enumeration, MTU, default route, - DNS servers, `dumpsys`), Location signals and Split-tunnel bypass - cards come back Clean. -- [loop-uh/yourvpndead](https://github.com/loop-uh/yourvpndead) — the - "no root, no permissions, standard Android API, under one second" - detector. Reports `VPN: Не активен`; the only visible interfaces are - `dummy0` / `lo` / `rmnet16`; no VPN signals in direct or indirect - checks. - -Neither module alone covers all of this: - -- **This** module handles the Java / Android framework side: - `NetworkCapabilities` (`hasTransport` / `hasCapability` / - `getTransportInfo`), `NetworkInterface.getNetworkInterfaces`, - `LinkProperties` (`getRoutes` / `getDnsServers` / `getHttpProxy`), - `System.getProperty` for proxy keys, and redirects `/proc/net/*` - reads done through `java.io.FileInputStream` / `FileReader` to - `/dev/null`. -- The **vpnhide-zygisk** companion closes the native side: - `libc::ioctl` (`SIOCGIFNAME` / `SIOCGIFFLAGS`) and `libc::getifaddrs`, - which is what Flutter / Dart apps and any JNI code would hit - bypassing ART entirely. - -Split-tunnel is a hard requirement for the cards that compare the -device-reported public IP against external checkers: the detection -app's own HTTPS requests must exit through the carrier, otherwise the -checkers see the VPN exit IP and flag a mismatch with GeoIP / ASN -databases. That is a network-layer fact, not something any client-side -hook can fix. - -### Source: official VPN/Proxy detection methodology - -Both detection apps above (and any future ones playing the same game) -implement the **official Russian Ministry of Digital Development -methodology for identifying VPN/Proxy on user devices**, published as -an OCR'd Markdown copy here: -. The Android sections (6.4 / 7.4 / 7.6 / -7.7) are the canonical reference for which Java APIs we hook and why. - -### TODO — methodology coverage gaps - -The methodology mentions a few additional vectors that we don't yet -handle. None of them are triggered by the two detection apps above on -a Pixel 8 Pro / Android 16, but they're documented signals and future -detectors will use them. Listed by descending priority: - -- [ ] **`Runtime.exec("dumpsys vpn_management")` and `dumpsys activity - services VpnService`** — sec. 7.4. On `untrusted_app` these - always return `Permission Denial`, which both audited apps treat - as a clean signal. Worth covering preemptively in case a future - detector decides to interpret a non-empty stdout as positive - proof. -- [ ] **`Runtime.exec("ip route" / "ip rule")` etc.** — same idea, - same caveat (also denied for untrusted_app). -- [ ] **`NetworkScore` / `Score(Policies: IS_VPN)`** — sec. 6.4. Only - reachable via reflection on system-internal API; normal apps - can't read it. Theoretical leak. Defer until something actually - tries. - -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 -anti-tamper SDK apps), not this module. +1. **App-process mode** (default) -- hooks installed directly in target app processes via LSPosed/Xposed. More complete coverage (all APIs below) but visible to anti-tamper SDKs that scan for Xposed artifacts. +2. **system_server mode** -- hooks `writeToParcel()` in `system_server` so VPN data is stripped before Binder serialization. Zero presence in the app process. Use with [kmod](../kmod/) for native coverage. --- -## system_server mode (for apps with anti-tamper SDKs) - -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 -`system_server` — the system process that serializes network state -over Binder to all apps. VPN-related data is stripped *before* Binder -serialization, so the banking app's process receives clean data -without any hooks loaded into it. Per-UID filtering via -`Binder.getCallingUid()` ensures only target apps see the filtered -view; everything else (VPN client, NFC subsystem, system services) -sees the real network state. - -### When to use - -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 - -1. In LSPosed/Vector manager, add **"System Framework"** to this - module's scope (in addition to or instead of individual apps). -2. Reboot so the module loads into `system_server`. -3. Install [vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) - for native-side coverage (kernel-level ioctl/getifaddrs/route - filtering). The kernel module's WebUI manages the target app list - for both components. - -### Target management - -Target UIDs are managed through -[vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod)'s WebUI. -The WebUI writes UIDs to `/proc/vpnhide_targets` (kernel module) and -`/data/system/vpnhide_uids.txt` (system_server hooks). This module -watches the directory via `FileObserver` (inotify) and reloads the -UID list immediately when the file changes — no reboot needed. - -**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. - ---- - -## What problem does this solve? - -A lot of Android apps run an "is a VPN active?" check on startup (or before -sensitive actions like payments, account login, etc.) and refuse to work if -they detect one. They typically do this with a handful of very standard -Android APIs: - -- Ask `ConnectivityManager` if any network has the `TRANSPORT_VPN` capability. -- Enumerate `NetworkInterface.getNetworkInterfaces()` and look for anything - named `tun0`, `ppp0`, `wg0`, etc. -- Read `/proc/net/route` directly and look for a default route through a - tunnel interface. -- Use the old `ConnectivityManager.getActiveNetworkInfo().getType() == TYPE_VPN`. - -VpnHide intercepts all of these at the Java/Kotlin level (via LSPosed) so -that when a scoped app calls them, it gets back a picture of the world with -no VPN present. The VPN itself keeps working normally — this only affects -what the hooked app *sees*. - ---- - -## What it hides - -Every detection path below has a corresponding hook. If your target app only -uses these, VpnHide should make it blind to the VPN. +## What it hooks (app-process mode) ### 1. `android.net.NetworkCapabilities` | Method | Behaviour with VpnHide | @@ -197,9 +18,9 @@ uses these, VpnHide should make it blind to the VPN. | `hasCapability(NET_CAPABILITY_NOT_VPN)` | always returns `true` | | `getTransportTypes()` | `TRANSPORT_VPN` stripped from the returned `int[]` | | `getTransportInfo()` | returns `null` whenever the real value is `VpnTransportInfo` | -| `toString()` | post-processed: `\|VPN` stripped from `Transports:`, `VpnTransportInfo{…}` replaced with `null`, stray `IS_VPN` flags dropped from `&`-joined lists. Uses string manipulation (not regex) to avoid `PatternSyntaxException` on edge cases. | +| `toString()` | post-processed: `\|VPN` stripped from `Transports:`, `VpnTransportInfo{...}` replaced with `null`, stray `IS_VPN` flags dropped from `&`-joined lists. Uses string manipulation (not regex) to avoid `PatternSyntaxException` on edge cases. | -### 2. `android.net.NetworkInfo` (legacy `ConnectivityManager.getActiveNetworkInfo()` path) +### 2. `android.net.NetworkInfo` | Method | Behaviour with VpnHide | |---|---| | `getType()` | returns `TYPE_WIFI` instead of `TYPE_VPN` | @@ -228,28 +49,12 @@ uses these, VpnHide should make it blind to the VPN. | Method | Behaviour with VpnHide | |---|---| | `getNetworkInterfaces()` | VPN tunnel interfaces removed from the enumeration | -| `getByName(name)` | returns `null` for names like `tun*`, `ppp*`, `tap*`, `wg*`, `ipsec*`, `xfrm*`, `utun*`, `l2tp*`, `gre*`, or anything containing `"vpn"` | +| `getByName(name)` | returns `null` for VPN-like names | | `getByIndex(int)` | returns `null` if the looked-up interface is a VPN tunnel | | `getByInetAddress(addr)` | returns `null` if the matched interface is a VPN tunnel | -### 7. `System.getProperty` - -Proxy-related system properties that can leak VPN presence: - -| Key | Behaviour with VpnHide | -|---|---| -| `http.proxyHost` | returns `null` | -| `http.proxyPort` | returns `null` | -| `https.proxyHost` | returns `null` | -| `https.proxyPort` | returns `null` | -| `socksProxyHost` | returns `null` | -| `socksProxyPort` | returns `null` | - -### 8. `/proc/net/*` file reads -`FileInputStream` and `FileReader` constructors (both `String` and `File` -variants) are hooked. When an app tries to open any of the following paths, -the open is transparently redirected to `/dev/null`, so reads return EOF -immediately and the app sees no routes / no interfaces / no sockets: +### 6. `/proc/net/*` file reads +`FileInputStream` and `FileReader` constructors (both `String` and `File` variants) are hooked. Opens to the following paths are redirected to `/dev/null`: ``` /proc/net/route @@ -268,123 +73,79 @@ immediately and the app sees no routes / no interfaces / no sockets: /proc/net/xfrm_stat* ``` -### VPN interface name prefixes considered "a VPN" +### 7. `System.getProperty` -`tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, `l2tp`, `gre`, plus -anything whose name contains the substring `vpn` (case-insensitive). +Proxy-related system properties that can leak VPN presence: + +| Key | Behaviour with VpnHide | +|---|---| +| `http.proxyHost` | returns `null` | +| `http.proxyPort` | returns `null` | +| `https.proxyHost` | returns `null` | +| `https.proxyPort` | returns `null` | +| `socksProxyHost` | returns `null` | +| `socksProxyPort` | returns `null` | + +### VPN interface name prefixes + +`tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, `l2tp`, `gre`, plus anything whose name contains the substring `vpn` (case-insensitive). --- -## What it does NOT cover (known gaps) +## system_server mode -Be honest with yourself about what's here and what isn't. If a target app -does any of the things below, VpnHide won't be enough on its own. +### When to use -### Native code path — not covered -Apps that enumerate interfaces or read `/proc` from C/C++ via JNI bypass -every Java hook in this module. The Linux syscalls that matter are: +For apps with aggressive anti-tamper SDKs that detect app-process hooks (crashes, NFC payment degradation, etc.). The default app-process hooks cannot be used for these apps. -- `getifaddrs()` / `freeifaddrs()` -- `ioctl(SIOCGIFCONF)` / `ioctl(SIOCGIFFLAGS)` -- raw `open("/proc/net/route", ...)` from libc, not java.io -- `sysconf(_SC_NPROCESSORS_ONLN)` is irrelevant, but you get the idea -- Direct `socket(AF_NETLINK, ...)` followed by `RTM_GETLINK` messages - (modern native code tends to use this) +### How to enable -To intercept those you need a **Zygisk native module** that inline-hooks -libc. That's exactly what the [vpnhide-zygisk](https://github.com/okhsunrog/vpnhide-zygisk) -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. +1. In LSPosed/Vector manager, add **"System Framework"** to this module's scope. +2. Reboot so the module loads into `system_server`. +3. Install [vpnhide-kmod](../kmod/) for native-side coverage. +4. Manage targets via kmod's WebUI, which writes UIDs to `/data/system/vpnhide_uids.txt`. -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 -without any footprint in the app's process. +### What it hooks -### Server-side detection — unfixable client-side -No client-side module can fix any of this: +`writeToParcel()` on `NetworkCapabilities`, `NetworkInfo`, and `LinkProperties`. Uses a ThreadLocal save/restore pattern so the original values are preserved for non-target callers. Per-UID filtering via `Binder.getCallingUid()` ensures only target apps see the filtered view. -- **DNS leakage.** If the app resolves a hostname through the VPN resolver, - the resolved IP comes from the VPN side and the server notices. -- **IP range blocklists.** Commercial "is this IP a known VPN exit?" - databases (IPQS, MaxMind, IPHub, etc.) flag the source IP of your traffic. - If your app talks to a service that uses them, they see the exit IP of - your VPN provider and block you. -- **Latency fingerprinting.** Some backends measure RTT to known endpoints - and notice the VPN hop. -- **TLS fingerprinting (JA3/JA4).** Completely orthogonal to VPN detection - but worth knowing — the TLS handshake leaks a lot even through a tunnel. +### Target management -The usual answer to all of these is **split tunnel**: make sure the target -app's traffic goes direct, not through the VPN, so the server only sees -your real ISP IP. VpnHide + split tunnel is the combo that tends to work. +A `FileObserver` (inotify) watches `/data/system/` and reloads the UID list immediately when the file changes. No reboot needed. -### Process spawning — not covered -An app that does `Runtime.exec("cat /proc/net/route")` or -`ProcessBuilder.start()` to shell out and read `/proc` won't be caught by -the `FileInputStream` hook. Rare in practice but possible. - -### `NetworkCallback` in-flight events — not deeply hooked -When an app calls `registerNetworkCallback()` and the system delivers an -`onCapabilitiesChanged(network, caps)` callback, the `caps` object it -receives still goes through our `hasTransport` hook — so checking -`caps.hasTransport(TRANSPORT_VPN)` in the callback returns `false`. But if -the app counts `onAvailable()` calls and infers VPN from "more than one -active network", that inference happens outside the method-level hooks. -Uncommon, but possible. - -### VPN detection via `VpnService.prepare()` -`VpnService.prepare(Context)` returning an `Intent` doesn't necessarily mean -another VPN is active — it returns an `Intent` whenever the *calling* app -hasn't been granted VPN permission yet. Some detectors misread this, but -the module does not currently hook it. Add if needed. - ---- - -## Threat model - -VpnHide is designed for one specific scenario: *"I have a VPN running on my -phone and certain apps refuse to work because they detect it. I want those -specific apps to think the VPN isn't there, so I don't have to keep turning -the VPN off every time I use them."* - -It is explicitly **not** designed for: - -- Hiding root or custom ROM presence (that's a different problem — use - Vector/LSPosed module scope, Tricky Store OSS, crDroid's built-in Play - Integrity spoof, etc.) -- Bypassing Play Integrity's `MEETS_DEVICE_INTEGRITY` (unrelated — Play - Integrity doesn't care whether a VPN is active) -- Fooling network-layer or server-side detection (client-side Java hooks - can't do that — see "what it doesn't cover" above) +**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. --- ## Install -1. Build the APK (`./gradlew assembleDebug` → `app/build/outputs/apk/debug/app-debug.apk`). -2. Install it: `adb install app-debug.apk`. -3. Open your LSPosed / Vector manager, go to Modules, enable **VPN Hide**. -4. Add your target apps to the module's scope and **force-stop** them so - they re-fork with hooks active (or reboot). -5. Open the **VPN Hide** launcher icon. You'll see a list of installed - apps — tick the ones you want VPN hidden from. Use the overflow menu - to toggle between "user apps only" and "show system apps". +1. Build the APK (`./gradlew assembleDebug`). +2. Install: `adb install app/build/outputs/apk/debug/app-debug.apk`. +3. Open LSPosed/Vector manager, go to Modules, enable **VPN Hide**. +4. Add target apps to the module's scope and **force-stop** them (or reboot). +5. Open the **VPN Hide** launcher icon. Tick the apps you want VPN hidden from. 6. Force-stop the target app again so it re-reads prefs on next launch. ### Double-gate logic -Two conditions must both be true for hooks to actually run inside an app: +Two conditions must both be true for hooks to run inside an app: 1. The app is in the module's scope in LSPosed manager. 2. The app is checked in VPN Hide's picker UI. -Gate (1) controls which processes LSPosed loads the module into at all -(performance). Gate (2) is the per-app allowlist read at hook time via -`XSharedPreferences`. This lets you enable/disable hiding per-app without -going into the LSPosed manager every time. +Gate (1) controls which processes LSPosed loads the module into (performance). Gate (2) is the per-app allowlist read at hook time via `XSharedPreferences`. + +--- + +## What it does NOT cover + +- **Native code path** -- apps checking VPN from C/C++/JNI/Flutter bypass all Java hooks. Use [zygisk](../zygisk/) or [kmod](../kmod/). +- **Server-side detection** -- DNS leakage, IP blocklists, latency/TLS fingerprinting. Unfixable client-side; use split tunneling. +- **`Runtime.exec` / `ProcessBuilder` shell-outs** -- e.g. `cat /proc/net/route` in a subprocess. +- **`NetworkCallback` event counting** -- inferring VPN from `onAvailable()` call count. +- **`VpnService.prepare()`** -- not currently hooked. + +See [the project README](../README.md) for the full threat model and split-tunnel requirements. --- @@ -400,46 +161,29 @@ On target app startup you should see: VpnHide: installing hooks for com.example.targetapp ``` -Any hook that fails to install is logged with the hook category and the -exception message, so you can tell whether a specific hook broke on a -newer Android version. +Any hook that fails to install is logged with the hook category and exception message. -If hooks installed cleanly but the app still detects VPN: +If hooks installed but the app still detects VPN: ```bash -# Grab a full trace around the detection event adb logcat -c -# ... trigger the app's VPN check, then: +# trigger the app's VPN check, then: adb logcat -d > /tmp/detect.log -# Look for: grep -iE "tun0|ppp0|wg0|vpn|TRANSPORT_VPN|NetworkInterface|/proc/net" /tmp/detect.log ``` -Anything suspicious that isn't routed through VpnHide's filtered paths is -a clue about what the app is actually doing — e.g. a native library call, -a `ProcessBuilder.start("cat")` shell-out, or a server-side check. - --- ## Build -Requires JDK 17 and Android SDK with `compileSdk = 35`. - ```bash -cd vpnhide -gradle wrapper --gradle-version 8.9 # one-time, bootstraps ./gradlew ./gradlew assembleDebug ``` -Output: `app/build/outputs/apk/debug/app-debug.apk`. - -For a release build with proper signing, uncomment the `signingConfigs` -block in `app/build.gradle.kts` and provide a keystore. +Requires JDK 17. Output: `app/build/outputs/apk/debug/app-debug.apk`. --- ## License -Personal / educational project. No explicit license — do whatever you want -with it but don't hold me responsible if a target app updates its detection -logic and breaks things. +Personal / educational project. No explicit license -- do whatever you want with it but don't hold me responsible if a target app updates its detection logic and breaks things. diff --git a/zygisk/README.md b/zygisk/README.md index af4e60f..d3e1d8d 100644 --- a/zygisk/README.md +++ b/zygisk/README.md @@ -1,271 +1,95 @@ -# vpnhide-zygisk +# vpnhide -- Zygisk module -A small Zygisk module written in Rust that hides an active VPN interface -from selected Android apps by inline-hooking libc's `ioctl`. +Native-layer VPN interface hiding via inline libc hooks. Part of [vpnhide](../README.md). -Companion to the Kotlin LSPosed / Vector module -[**okhsunrog/vpnhide**](https://github.com/okhsunrog/vpnhide), which -handles Java-level VPN detection (`ConnectivityManager`, -`NetworkInterface`, `LinkProperties`, `System.getProperty` for proxy -keys, `NetworkCapabilities.getTransportInfo`, …). This module covers -the **native** detection path instead — apps calling `ioctl(SIOCGIFNAME)` -/ `ioctl(SIOCGIFFLAGS)` / `getifaddrs()` from C/C++/JNI/Flutter -runtime code that never enters ART. The two modules are independent -and share no runtime state; install either one alone, or both together -for full coverage of the Java and native stacks. +## What it hooks -For apps with aggressive anti-tamper SDKs where userspace hooks -cause crashes or NFC payment degradation, use -[okhsunrog/vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) -instead — a kernel module that provides the same native filtering -without any footprint in the app's process. +All hooks are inline on `libc.so` via ByteDance shadowhook: -## Status - -Tested baseline: Android 16 (API 36) on a Pixel 8 Pro with KernelSU-Next -+ NeoZygisk. Verified against a Flutter-based app with native VPN -detection: the VPN-detection banner no longer appears when a WireGuard -tunnel is active. - -Should work on **any current Zygisk implementation** — see the -[Compatibility](#compatibility) section below. - -### Verified against third-party detection apps - -With this module **together with** the Kotlin LSPosed companion -[okhsunrog/vpnhide](https://github.com/okhsunrog/vpnhide) installed, and -WireGuard running in **split-tunnel** mode (so the detection apps' own -HTTPS probes go through the carrier, not the tunnel), the following -popular Russian "is there a VPN on this device?" apps report **all -clean**, with no direct or indirect signals triggered: - -- [xtclovver/RKNHardering](https://github.com/xtclovver/RKNHardering) — - the Kotlin app that implements the Russian Ministry of Digital - Development's VPN-detection methodology. All GeoIP, IP comparison, - Direct signs (`TRANSPORT_VPN`, HTTP/SOCKS proxy), Indirect signs - (`NET_CAPABILITY_NOT_VPN`, interface enumeration, MTU, default route, - DNS servers, `dumpsys`), Location signals and Split-tunnel bypass - cards come back Clean. -- [loop-uh/yourvpndead](https://github.com/loop-uh/yourvpndead) — the - "no root, no permissions, standard Android API, under one second" - detector. Reports `VPN: Не активен`, the only visible interfaces are - `dummy0`/`lo`/`rmnet16`, no VPN signals in direct or indirect checks. - -Neither module alone covers all of this: - -- The LSPosed companion handles the Java / Android framework side: - `NetworkCapabilities.hasTransport/hasCapability/getTransportInfo`, - `NetworkInterface.getNetworkInterfaces`, - `LinkProperties.getRoutes/getDnsServers`, `System.getProperty` for - proxy keys, and redirects `/proc/net/route|tcp|tcp6|…` reads done - through `java.io.FileInputStream` / `FileReader` to `/dev/null`. -- **This** module closes the native side: `libc::ioctl` (`SIOCGIFNAME` - / `SIOCGIFFLAGS`) and `libc::getifaddrs`, which is what Flutter / - Dart apps and any JNI code would hit bypassing ART entirely. - -Split-tunnel is a requirement for the cards that compare the -device-reported public IP against external checkers: the detection -app's HTTPS requests must exit through the carrier, otherwise the -checkers see the VPN exit IP and flag a mismatch with GeoIP / ASN -databases. That's a network-layer fact, not something any client-side -hook can fix. - -### Source: official VPN/Proxy detection methodology - -Both detection apps above implement the **official Russian Ministry -of Digital Development methodology for identifying VPN/Proxy on user -devices**, published as an OCR'd Markdown copy here: -. The native Android sections (6.4 / 7.4 -/ 7.6 / 8.5) are the canonical reference for which libc / kernel -interfaces this module hooks and why. - -### TODO — methodology coverage gaps (native side) - -The methodology mentions native paths that we don't yet hook. None of -them are triggered by RKNHardering or YourVPNDead today -(they're all Java-only callers, so the LSPosed companion already -covers them at the ART layer), but the gaps matter for any future -detector that drops into C/C++ / NDK code to bypass ART. Listed by -descending priority: - -- [ ] **`open` / `openat` filter for `/proc/net/route`, `/proc/net/tcp`, - `/proc/net/tcp6`, `/proc/net/udp`, `/proc/net/udp6`, - `/proc/net/dev`, `/proc/net/arp`, `/proc/net/fib_trie*`** — - sec. 7.4 / 7.6 / 7.7 / 7.8. Java-side `FileInputStream` / - `FileReader` constructors are already redirected to `/dev/null` - by the LSPosed companion, but a native caller (Flutter, JNI) - reaches procfs through libc directly. Plan: replace the fd with - a memfd containing a sanitized version of the file (drop routes - via VPN ifaces, drop TCP entries on known proxy ports), or fall - back to redirecting to `/dev/null` if sanitizing turns out - fragile. -- [ ] **`ioctl(SIOCGIFCONF)` bulk-query filter** — sec. 6.4. Currently - passthrough; we only handle `SIOCGIFNAME` / `SIOCGIFFLAGS`. - `SIOCGIFCONF` returns the whole interface table in one shot, so - a Flutter app calling it from Dart bypasses our per-name filter - entirely. -- [ ] **`recvmsg` filter on `NETLINK_ROUTE` sockets** — sec. 7.6. - Apps that read the routing table via netlink instead of - `/proc/net/route` would slip past the procfs hook. Lower - priority — netlink usage from Android user code is rare. -- [ ] **`connect()` filter for localhost proxy ports** — sec. 7.8. - The methodology lists "active connections to non-standard - ports" as a Proxy sign, and YourVPNDead probes 127.0.0.1 on - 10808 / 7890 / 9050 / 9090 / etc. Risky to hook indiscriminately - (breaks legitimate localhost services), so target-app-scoped - and port-allowlisted only. - -The complementary Java side -(`ConnectivityManager` / `NetworkCapabilities` / `NetworkInterface` / -`LinkProperties` / `System.getProperty` / `NetworkCapabilities.toString` -/ Java-side `/proc/net/*` constructor redirects) is the responsibility -of [vpnhide](https://github.com/okhsunrog/vpnhide), not this module. - -Current coverage (all hooks are inline on `libc.so`): -- `ioctl(SIOCGIFFLAGS)` — pre-screened; returns `-1 ENODEV` if the caller - hands us an `ifr_name` matching a VPN prefix. -- `ioctl(SIOCGIFNAME)` — called through; if the returned name is a VPN, - rewritten to `-1 ENODEV`. -- Any other `ioctl` request: passthrough. -- `getifaddrs` — called through; VPN entries are unlinked from the - returned linked list before it reaches the caller. This catches - `NetworkInterface.getNetworkInterfaces()` inside libcore, Dart's - `NetworkInterface.list()`, and any direct C/C++ call. - -Planned: -- `openat`/`open` filter on `/proc/net/route`, `/proc/net/tcp`, - `/proc/net/tcp6`, `/proc/net/dev` — catches native readers of the - networking procfs entries that bypass the LSPosed companion's - `java.io.File` constructor hooks. -- `ioctl(SIOCGIFCONF)` bulk-query filter. -- `recvmsg` filter on `NETLINK_ROUTE` sockets. -- Optional: `connect()` filter on localhost proxy ports, to defeat - YourVPNDead-style SOCKS5 port probing. - -## Compatibility - -The module declares itself as Zygisk API v5 (via the `zygisk-api` -crate's `V5` shape) but only actually calls v1-era functions -(`pre_app_specialize`, `post_app_specialize`, `args.nice_name`, -`set_option(DlCloseModuleLibrary)`). The inline libc hooks happen -inside the process via shadowhook and don't go through the Zygisk API -at all. The Zygisk side only needs to inject our `.so` into zygote -and dispatch the two specialize callbacks. - -That means we run on every modern Zygisk implementation: - -| Setup | Works | -|---|---| -| Stock Magisk (API v5 since topjohnwu's recent versions) + LSPosed | ✅ | -| Magisk + ZygiskNext + LSPosed | ✅ | -| Magisk + NeoZygisk + LSPosed | ✅ | -| KernelSU + ZygiskNext + LSPosed | ✅ | -| KernelSU-Next + NeoZygisk + LSPosed/Vector | ✅ (tested baseline) | -| APatch + any Zygisk implementation + LSPosed | ✅ (untested in CI) | - -Hard requirements: - -- arm64 / `aarch64-linux-android` only — `build.rs` hard-fails on - other targets. -- A Zygisk implementation that exposes API ≥ v1 (every shipping - Zygisk fork does — Magisk's own `ZYGISK_API_VERSION` has been at 5 - since well before this module existed). -- LSPosed/Vector for the [Java-side companion `vpnhide`](https://github.com/okhsunrog/vpnhide). +| Hook | Detection path | What it does | +|------|---------------|--------------| +| `ioctl` | `SIOCGIFFLAGS` | Returns `ENODEV` for VPN interfaces (pre-screens input name). | +| `ioctl` | `SIOCGIFNAME` | Calls through; rewrites result to `ENODEV` if returned name is VPN. | +| `ioctl` | `SIOCGIFCONF` | Calls through; compacts VPN entries out of the returned `ifreq` array. | +| `getifaddrs` | `NetworkInterface.getNetworkInterfaces()`, Dart VM, direct C/C++ | Unlinks VPN entries from the returned linked list. | +| `openat` | `/proc/net/{route,ipv6_route,if_inet6,tcp,tcp6}` | Returns a memfd with VPN entries stripped out. | +| `recvmsg` | Netlink `RTM_NEWADDR` / `RTM_NEWLINK` dump responses | Removes VPN interface entries from netlink messages. | ## Architecture -The module runs inside each forked app process via Zygisk. +### Why inline hooks instead of PLT -1. **`pre_app_specialize`** — runs on the zygote side before uid drop and - SELinux context transition. We read the package name from - `args.nice_name`, check it against `/data/adb/vpnhide_zygisk/targets.txt` - plus a small built-in allowlist, and either: - - set an internal `is_target` flag, or - - call `DlCloseModuleLibrary` so Zygisk unloads our `.so` from the - process on callback return (non-targeted apps pay zero cost). -2. **`post_app_specialize`** — on targeted processes only: initialize - ByteDance shadowhook and install a single inline hook on - `libc.so!ioctl`. From this point on, every caller in the process — - regardless of when its library was dlopen'd — ends up in our - `hooked_ioctl` replacement. +PLT hooks patch the caller library's procedure linkage table. At `post_app_specialize` time, `libflutter.so` / `libapp.so` / late-loaded JNI libraries are **not yet mapped** -- only ~350 Android system libraries are present, and none of them have PLT relocations for `ioctl` (the call sites are inside libc itself). Inline-hooking libc's entry points rewrites the function prologue in-place, so every caller in the process -- regardless of when it was loaded -- lands on our trampoline. -### Why inline hooking instead of PLT +### Flow -PLT hooks patch the caller library's procedure linkage table entry for a -given symbol. To intercept `libflutter.so`'s call to `ioctl` that way we'd -have to patch libflutter.so's PLT. +1. **`pre_app_specialize`** -- runs on zygote before uid drop. Reads `args.nice_name`, checks against `/data/adb/vpnhide_zygisk/targets.txt`. Non-targeted apps get `DlCloseModuleLibrary` (zero cost after unload). +2. **`post_app_specialize`** -- on targeted processes only: `shadowhook_init`, install four inline hooks (`ioctl`, `getifaddrs`, `openat`, `recvmsg`), then scrub maps. -At `post_app_specialize` — the last Zygisk callback before the app's Java -code runs — libflutter.so / libapp.so / the Dart VM's native code are -**not yet loaded**. Only the ~350 Android system libraries are mapped, and -none of them directly call `ioctl` from their own code (verified via -`readelf -r` on libandroid, libbinder, libutils, libmedia, libui, libgui, -libnetd_client, libbase, libcutils, libc++, libandroid_runtime, libart — -zero ioctl relocations across all of them). The ioctl call sites are -inside libc itself. +### Thread-local guard -Inline-hooking libc.so's `ioctl` entry point rewrites the first few -instructions of the function in-place. Any caller in the process — Flutter, -any JNI library, anything dlopen'd later — eventually jumps to that same -address and lands on our trampoline. Load order becomes irrelevant. The -only thing inline libc hooks don't catch is apps issuing `syscall(SYS_ioctl, …)` -directly, which is extremely rare outside of deliberate anti-hook code. +The ioctl hook uses a thread-local `IN_GETIFADDRS` flag to pass through without filtering while libc's internal `getifaddrs` implementation is running. Without this, our `SIOCGIFFLAGS` filter returns `ENODEV` for VPN interfaces during libc's own ifaddrs list construction, which corrupts the list and breaks downstream consumers (including NFC/HCE payment flows). + +### Maps scrubbing + +After hook installation, `scrub_shadowhook_maps()` renames `[anon:shadowhook-island]` and `[anon:shadowhook-enter]` regions via `prctl(PR_SET_VMA, PR_SET_VMA_ANON_NAME, ..., "")`. This makes them show as plain `[anon:]` in `/proc/self/maps`, indistinguishable from hundreds of other anonymous mappings -- even to anti-tamper SDKs that read maps via raw `svc #0` syscalls. ### shadowhook fork -Inline hooking is provided by [ByteDance shadowhook](https://github.com/bytedance/android-inline-hook). -We carry a small fork at [okhsunrog/android-inline-hook](https://github.com/okhsunrog/android-inline-hook) -(branch `vpnhide-zygisk`), vendored as a git submodule under -`third_party/android-inline-hook/`, with two changes on top of upstream: +We carry a small fork at [okhsunrog/android-inline-hook](https://github.com/okhsunrog/android-inline-hook) (branch `vpnhide-zygisk`), vendored as a git submodule under `third_party/android-inline-hook/`, with two changes on top of upstream: -1. **`SHADOWHOOK_STATIC=ON` CMake option** — builds `libshadowhook.a` - instead of a shared library so we can embed it directly into this - Rust cdylib, and drops the SHARED-only link options. -2. **`sh_linker_init()` stub** — upstream hooks the Android dynamic linker's - `soinfo::call_constructors` / `soinfo::call_destructors` so it can apply - "pending" hooks to libraries dlopen'd after init. On Android 16 (API 36) - the hardcoded symbol table in `sh_linker_hook_call_ctors_dtors()` no - longer matches the newer linker layout, and the call fails with - `SHADOWHOOK_ERRNO_INIT_LINKER`, blocking all subsequent hooks. We don't - use the pending-hook feature (our target libc.so is always preloaded), - so the stub skips this path entirely. +1. **`SHADOWHOOK_STATIC=ON`** -- builds `libshadowhook.a` instead of a shared library so it can be embedded directly into this Rust cdylib. +2. **`sh_linker_init()` stub** -- on Android 16 (API 36) the hardcoded symbol table in upstream's linker hook no longer matches the newer linker layout, causing `SHADOWHOOK_ERRNO_INIT_LINKER`. We don't need the deferred-hook feature (libc.so is always preloaded), so the stub skips this path entirely. + +## Compatibility + +The module declares Zygisk API v5 but only calls v1-era functions (`pre_app_specialize`, `post_app_specialize`, `args.nice_name`, `set_option(DlCloseModuleLibrary)`). The inline hooks happen via shadowhook inside the process, not through the Zygisk API. + +| Setup | Works | +|-------|-------| +| Stock Magisk (API v5) + LSPosed | Yes | +| Magisk + ZygiskNext + LSPosed | Yes | +| Magisk + NeoZygisk + LSPosed | Yes | +| KernelSU + ZygiskNext + LSPosed | Yes | +| KernelSU-Next + NeoZygisk + LSPosed/Vector | Yes (tested baseline) | +| APatch + any Zygisk implementation + LSPosed | Yes (untested in CI) | + +Hard requirements: + +- arm64 / `aarch64-linux-android` only -- `build.rs` hard-fails on other targets. +- A Zygisk implementation that exposes API >= v1. +- [LSPosed/Vector](../lsposed/) for the Java-side companion. ## Build Requirements: -- Rust ≥ 1.85 (edition 2024) +- Rust >= 1.85 (edition 2024) - `rustup target add aarch64-linux-android` - `cargo install cargo-ndk` -- Android NDK (auto-detected under `~/Android/Sdk/ndk/`; any recent NDK - that ships `libclang_rt.builtins-aarch64-android.a` works) -- CMake ≥ 3.22, Ninja +- Android NDK (auto-detected under `~/Android/Sdk/ndk/`; any recent NDK that ships `libclang_rt.builtins-aarch64-android.a` works) +- CMake >= 3.22, Ninja - `git submodule update --init --recursive` -Build & package: +Build and package: ```bash ./build-zip.sh # Output: target/vpnhide-zygisk.zip (~180 KB) ``` -`build.rs` invokes the NDK's CMake toolchain on the shadowhook submodule, -pulls in `libclang_rt.builtins-aarch64-android.a` for `__clear_cache`, -and statically links everything into `libvpnhide_zygisk.so`. +`build.rs` invokes the NDK's CMake toolchain on the shadowhook submodule, pulls in `libclang_rt.builtins-aarch64-android.a` for `__clear_cache`, and statically links everything into `libvpnhide_zygisk.so`. ### Log level -Logging goes through the [`log`](https://crates.io/crates/log) crate + -`android_logger`. The compile-time ceiling is controlled by a Cargo -feature on this crate; calls below the ceiling are statically elided -(zero code size, zero runtime cost). +Logging goes through the [`log`](https://crates.io/crates/log) crate + `android_logger`. The compile-time ceiling is controlled by a Cargo feature; calls below the ceiling are statically elided. -| feature | default | effect | -| ----------- | ------- | ------------------------------- | -| `log-off` | | no logs at all | -| `log-error` | | errors only | -| `log-warn` | | errors, warnings | -| `log-info` | ✓ | errors, warnings, info | +| Feature | Default | Effect | +|-------------|---------|--------------------------------| +| `log-off` | | No logs at all | +| `log-error` | | Errors only | +| `log-warn` | | Errors, warnings | +| `log-info` | Yes | Errors, warnings, info | | `log-debug` | | + debug (e.g. `on_load` traces) | | `log-trace` | | + trace | @@ -279,92 +103,36 @@ cargo ndk -t arm64-v8a build --release \ ## Install 1. `adb push target/vpnhide-zygisk.zip /sdcard/Download/` -2. KernelSU-Next manager → Modules → Install from storage → pick the zip -3. Reboot -4. Pick the target apps. Two ways: - - **WebUI (recommended):** open the module in the KernelSU-Next - manager and tap the WebUI entry. You get a searchable list of - user-installed packages with checkboxes; Save writes the selection - to `targets.txt`. See [`module/webroot/index.html`](module/webroot/index.html). - - **Shell:** edit `/data/adb/vpnhide_zygisk/targets.txt` - directly (one package name per line, `#` for comments). A line with - a base package name `com.example.app` also matches its - subprocesses like `com.example.app:background`. -5. Force-stop the target app(s) so they re-fork with the hooks active: - `adb shell am force-stop ` -6. Verify via `adb logcat | grep vpnhide-zygisk`. Expected lines: - - ``` - I vpnhide-zygisk: is_targeted: matched com.example.targetapp (main) - I vpnhide-zygisk: pre_app_specialize: targeting com.example.targetapp - E shadowhook_tag: shadowhook init(default_mode: UNIQUE, …), return: 0 - I vpnhide-zygisk: hooks installed (inline libc!ioctl) - ``` +2. KernelSU/Magisk manager -> Modules -> Install from storage -> pick the zip. +3. Reboot. +4. Pick target apps: + - **WebUI (recommended):** open the module in the manager and tap the WebUI entry. Searchable list of user-installed packages with checkboxes; Save writes to `targets.txt`. See [`module/webroot/index.html`](module/webroot/index.html). + - **Shell:** edit `/data/adb/vpnhide_zygisk/targets.txt` directly (one package name per line, `#` for comments). A base package name `com.example.app` also matches subprocesses like `com.example.app:background`. +5. Force-stop target apps: `adb shell am force-stop ` +6. Verify: `adb logcat | grep vpnhide-zygisk` ## Filter logic -VPN interface prefixes: `tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, -`l2tp`, `gre`, plus anything containing the substring `vpn`. Matches the -list in the Kotlin companion module. - -- **`SIOCGIFFLAGS`** (app already has a name, wants flags): if the input - `ifr_name` matches a VPN prefix, set `errno=ENODEV` and return `-1` - without touching the real ioctl. The kernel never tells the app that - `tun0` has `IFF_POINTOPOINT` / `IFF_RUNNING`. -- **`SIOCGIFNAME`** (app has an index, wants the name): call the real - ioctl, check the returned name. If it's a VPN, rewrite to `-1 ENODEV`. - The app sees an empty slot at that index and moves on. -- Any other request passes through unchanged. +VPN interface prefixes: `tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, `l2tp`, `gre`, plus anything containing the substring `vpn`. Matches the list in the [LSPosed companion](../lsposed/). ## Known limitations -- **Direct syscalls bypass the hook.** We patch `libc.so!ioctl`, so any - code that issues `syscall(SYS_ioctl, …)` directly (or a hand-rolled - `svc #0` in assembly) goes straight to the kernel without touching our - trampoline. Rare in normal apps, common in deliberate anti-hook code. -- **`ioctl` is only one of several detection paths.** Apps can enumerate - interfaces via `getifaddrs()` / `freeifaddrs()` (Dart VM's - `NetworkInterface.list()` uses this), `ioctl(SIOCGIFCONF)` bulk queries, - or raw `NETLINK_ROUTE` sockets read via `recvmsg`. None of these are - hooked yet — see the "Planned" list in the Status section. An app that - uses any of these will still see `tun0` / `wg0` / etc. -- **Java-level detection is out of scope.** `ConnectivityManager.getNetworkCapabilities(…).hasTransport(TRANSPORT_VPN)`, - `NetworkInterface.getNetworkInterfaces()`, and similar ART-side APIs - are handled by the [Kotlin LSPosed companion module `vpnhide`](https://github.com/okhsunrog/vpnhide). - You almost always want both modules installed together. -- **Apps with aggressive anti-tamper SDKs.** Some SDKs detect modified - function prologues in libc.so and silently disable NFC contactless - payments. For these apps, use - [vpnhide-kmod](https://github.com/okhsunrog/vpnhide-kmod) - (kernel-level filtering) instead. -- **arm64 only.** `aarch64-linux-android` is the only supported target. - `build.rs` hard-fails on other architectures; no 32-bit arm, no x86. -- **Tested only on Android 16 (API 36).** Should work back to the - `android-24` link target in principle, but nothing older has been - exercised. The shadowhook linker-hook workaround in our fork was - specifically motivated by API 36; older versions may or may not need it. -- **Logging.** Log level is compile-time selectable via the `log-*` - Cargo features (see below). The default release build is `info`, which - emits a handful of lines per targeted process (`pre_app_specialize`, - shadowhook init result, hook installation) and is silent for - non-targeted processes once they're unloaded. - -## Uninstall - -KernelSU-Next manager → Modules → VPN Hide (Zygisk native) → Remove. -Reboot. +- **Direct `svc #0` syscalls bypass the hook.** Apps issuing raw syscalls skip libc entirely. Use [vpnhide-kmod](../kmod/) for these apps. +- **arm64 only.** No 32-bit arm, no x86. +- **`getifaddrs` hook leaks a few bytes per call.** Unlinked VPN entries in the ifaddrs linked list are intentionally leaked rather than tracked with a shadow allocator. Acceptable tradeoff -- `getifaddrs` is called infrequently. +- **Tested on Android 16 (API 36).** Should work back to API 24 in principle, but nothing older has been exercised. ## Files -- `src/lib.rs` — module entry point, scope file handling, hook installer -- `src/hooks.rs` — `hooked_ioctl` replacement + errno helper -- `src/filter.rs` — VPN interface name matching (unit tested) -- `src/shadowhook.rs` — minimal FFI to shadowhook -- `build.rs` — drives CMake on the shadowhook submodule -- `third_party/android-inline-hook/` — submodule (our shadowhook fork) -- `module/` — KernelSU/Magisk module metadata -- `build-zip.sh` — cross-compile + package script +- `src/lib.rs` -- module entry point, target gating, hook installer, maps scrubbing +- `src/hooks.rs` -- hook replacements for ioctl, getifaddrs, openat, recvmsg +- `src/filter.rs` -- VPN interface name matching and proc/net content filters (unit tested) +- `src/shadowhook.rs` -- minimal FFI to shadowhook +- `build.rs` -- drives CMake on the shadowhook submodule +- `third_party/android-inline-hook/` -- submodule (our shadowhook fork) +- `module/` -- KernelSU/Magisk module metadata +- `build-zip.sh` -- cross-compile + package script ## License -0BSD — do whatever you want with it, no warranty. +0BSD -- do whatever you want with it, no warranty.