diff --git a/CLAUDE.md b/CLAUDE.md index 8dedb760..96c4eab8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,9 +8,9 @@ tooling that follows the AGENTS-convention picks it up automatically. ## Project layout -Monorepo for hiding VPN interfaces from selected Android apps. Three runtime components plus tooling: +Monorepo for hiding VPN interfaces from selected Android apps. Runtime components plus tooling: -- `kmod/` — kernel module, kretprobes-based, per-GKI-generation builds +- `kmod/` — kernel-level native backends: `.ko` kretprobe module per GKI generation, plus KPM beta for KernelPatch - `zygisk/` — Rust Zygisk module, inline `libc` hooks via shadowhook - `lsposed/` — LSPosed module + Compose target-picker app - `portshide/` — localhost port blocker (shell + iptables) @@ -22,9 +22,9 @@ These short files cover everything specific to this repo. Skipping them leads to - [CONTRIBUTING.md](CONTRIBUTING.md) — PR process, commit conventions, changelog requirement - [docs/development.md](docs/development.md) — prereqs, per-module build quickstart, keystore setup, device install, CI lints - [docs/state.md](docs/state.md) — every persistent path / proc entry / iptables chain the project touches; who writes, who reads, lifetime -- [docs/storage.md](docs/storage.md) — **target** storage & activation design: the single JSON canonical, the activator (Rust workspace, three bins) that derives the runtime wire, LSPosed self-read, the APatch superkey, SELinux layout. Read before touching how config is stored/loaded. +- [docs/storage.md](docs/storage.md) — current storage & activation design: the single JSON canonical, the activator (Rust workspace, four thin bins: kmod/kpm/zygisk/ports) that derives runtime state, LSPosed self-read, the APatch superkey, SELinux layout. Read before touching how config is stored/loaded. - [docs/protocol.md](docs/protocol.md) — the frozen v1 control/stats **wire** between the app and the native backends (config/stats/status); the format every backend parser must agree on -- [docs/detection-vectors.md](docs/detection-vectors.md) — what an app can probe to detect the VPN (or a hidden package), which component (kmod / zygisk / lsposed / SELinux) covers each vector, how it manifests. Read before adding or changing a hook. +- [docs/detection-vectors.md](docs/detection-vectors.md) — what an app can probe to detect the VPN (or a hidden package), which component (kmod / KPM / zygisk / lsposed / SELinux) covers each vector, how it manifests. Read before adding or changing a hook. - [docs/adb-root-debugging.md](docs/adb-root-debugging.md) — how to make `adb shell su` useful on KernelSU/APatch/Magisk test devices; covers UID 0 without capabilities, SELinux denials on `/data/adb` and `/data/system`, and KPM/KPatch diagnostics. - [docs/changelog.md](docs/changelog.md) — changelog storage (`changelog.d/` fragments + history JSON), `./scripts/changelog.py` usage - [docs/releasing.md](docs/releasing.md) — `./scripts/release.py` usage, version-bump flow @@ -49,6 +49,7 @@ These short files cover everything specific to this repo. Skipping them leads to Single-command builds for both CI and local — the same scripts run in both places. - **kmod**: `./kmod/build.py --kmi ` (or `--all`). Auto-spawns the DDK Docker/podman container if you're not already inside it; CI passes `--inside-container` to skip the spawn. The DDK image tag is `DDK_IMAGE_TAG` in `kmod/build.py` — `.github/workflows/ci.yml` mirrors it, bump both together. +- **KPM**: `python3 kmod/kpm/build.py` — builds the KernelPatch module and flashable `vpnhide-kpm.zip`. - **zygisk**: `cd zygisk && ./build.py` — host-side cargo-ndk build, same script in CI image. - **lsposed APK**: `cd lsposed && ./gradlew :app:assembleRelease`. @@ -62,4 +63,4 @@ So when you need diagnostics in the logs and Debug logging is off: **stop and as ## Design notes -- **KPatch-Next KPM:** `kmod/` currently uses kretprobes and requires per-GKI-generation builds (kernel headers + `Module.symvers`). Porting to a [KPatch-Next](https://github.com/KernelSU-Next/KPatch-Next) KPM would produce a single binary for kernels 3.18–6.12, eliminating the multi-GKI build problem. Tradeoff: adds KPatch-Next as a hard dependency (already bundled in KernelSU-Next). +- **KPM backend:** `kmod/kpm/` ships a KernelPatch Module beta packaged as `vpnhide-kpm.zip`. It is a kernel-level Native backend like the `.ko`, but uses KernelPatch inline hooks and one cross-version artifact for supported 4.14–6.12 kernels. It requires APatch or KPatch-Next-Module, and must not be active together with the `.ko`; supported GKI devices should still prefer the QEMU-tested `.ko` unless there is a real load/signing problem. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d9ca2a30..ef9fbba8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -49,7 +49,7 @@ See [docs/changelog.md](docs/changelog.md) for the full changelog workflow. All style is enforced by CI — run the checks locally before pushing: - Rust: `cargo fmt` + `cargo clippy -- -D warnings` -- C: `clang-format` against `kmod/vpnhide_kmod.c` +- C: `scripts/clang-format-c.sh --check` (all tracked kmod C sources) - Kotlin: `ktlint` - Android: `./gradlew :app:lint` diff --git a/README.en.md b/README.en.md index 82ae4307..04a5db36 100644 --- a/README.en.md +++ b/README.en.md @@ -24,35 +24,36 @@ Existing modules like [NoVPNDetect](https://bitbucket.org/yuri-project/novpndete vpnhide solves both problems with a layered architecture: -**Layer 1 — Java API (lsposed module):** hooks `system_server`, not the target app. `NetworkCapabilities`, `NetworkInfo`, and `LinkProperties` are filtered at the Binder level *before* data reaches the app's process. The app receives clean data over IPC — no injection into its process, nothing for anti-tamper to detect. +**Layer 1 — Java API (lsposed module):** hooks `system_server`, not the target app. `NetworkCapabilities`, `NetworkInfo`, and `LinkProperties` are filtered at the Binder level *before* data reaches the app's process. The app receives clean data over IPC — no injection into its process, nothing for anti-tamper to detect. The same module also hides selected apps from observer apps at the `PackageManager` level (the **Apps** role). -**Layer 2 — Native (kmod or zygisk):** covers every native detection path: -- **kmod** (recommended) — kernel-level `kretprobe` hooks. Filters `ioctl` (SIOCGIFFLAGS, SIOCGIFNAME, SIOCGIFCONF), `getifaddrs`/netlink dumps (RTM_GETLINK, RTM_GETADDR), and `/proc/net/route` reads — all before the syscall returns to userspace. Other `/proc/net/*` files (`tcp*`, `udp*`, `dev`, `if_inet6`, etc.) are blocked by SELinux for untrusted apps on Android 10+, so no hook is needed there. Zero in-process footprint. No library injection. Nothing to detect. -- **zygisk** (alternative) — inline-hooks `libc.so` inside the app process. Same native coverage as kmod but runs in-process, so it's theoretically detectable by advanced anti-tamper. Use this if your kernel isn't supported by kmod. +**Layer 2 — Native (kmod, KPM, or Zygisk):** covers native detection paths. Exactly one native backend should be active: +- **kmod** (recommended for supported GKI kernels) — kernel-level `kretprobe` hooks. Filters `ioctl` (SIOCGIFFLAGS, SIOCGIFNAME, SIOCGIFCONF), `getifaddrs`/netlink dumps (RTM_GETLINK, RTM_GETADDR), routes, and `/proc/net/route` reads before the syscall returns to userspace. Zero footprint in the target process: no library injection, nothing to detect. +- **KPM** (beta) — a KernelPatch Module with the same purpose as kmod, but without a GKI-variant-specific `.ko`. Useful for old/non-GKI 4.14 / 4.19 / 5.4 kernels and cases where the `.ko` cannot load. Requires a KernelPatch runtime: APatch or KPatch-Next-Module. +- **Zygisk** — fallback when a kernel-level backend is not possible. It inline-hooks `libc.so` inside the target process, so banking and anti-fraud apps may detect it. For those apps, leave Native off and rely on the Java layer. -**Layer 3 — Additional app-level controls (integrated into the VPN Hide app):** -- **Interface hiding** — hide VPN interfaces, routes, and Java API VPN state from selected apps -- **Port hiding** — block selected apps from reaching `127.0.0.1` / `::1` so they cannot probe locally bound VPN / proxy daemons -- **App hiding** — hide selected apps from selected observer apps at the PackageManager level +**Layer 3 — Ports module (portshide):** a separate Magisk module. It blocks selected apps from reaching `127.0.0.1` / `::1` (via iptables), so they can't detect a locally bound VPN / proxy daemon by its open port (the **Ports** role). -The target app's process is completely untouched (with kmod + lsposed) — no Xposed, no inline hooks, no modified memory regions. Because of that, vpnhide works with banking and government apps that actively detect and block Xposed-based modules. +The target app's process is completely untouched when using LSPosed + a kernel-level Native backend (kmod or KPM) — no Xposed, no inline hooks, no modified memory regions. Because of that, vpnhide works with banking and government apps that actively detect and block Xposed-based modules. ## What vpnhide hides -vpnhide is not just one toggle. It combines three different protections that can be enabled per app: +vpnhide hides three things from selected apps, all configured per app via the four **J / N / A / P** roles (Java, Native, Apps, Ports): -1. **Interface hiding** — the core VPN-hiding layer. It removes VPN interfaces and routes from native APIs (`ioctl`, `getifaddrs`, `/proc/net/*`, `NetworkInterface`) and from Java APIs (`NetworkCapabilities`, `NetworkInfo`, `LinkProperties`). -2. **Port hiding** — blocks localhost access for selected apps so they cannot detect Clash, sing-box, V2Ray, Happ, and similar tools by probing local ports. -3. **App hiding** — hides selected installed apps from selected observer apps. Useful against package visibility checks, for example when an app tries to determine whether a VPN or proxy client is installed. +1. **Interface hiding** — the main goal. It removes VPN interfaces and routes from native APIs (`ioctl`, `getifaddrs`, `/proc/net/*`, `NetworkInterface`) and from Java APIs (`NetworkCapabilities`, `NetworkInfo`, `LinkProperties`). It is delivered by two roles together — **Java (J)** and **Native (N)** — toggled independently. +2. **Port hiding** — blocks localhost access for selected apps so they cannot detect Clash, sing-box, V2Ray, Happ, and similar tools by probing local ports (the **Ports (P)** role). +3. **App hiding** — hides selected installed apps from selected observer apps. Useful against package visibility checks, for example when an app tries to determine whether a VPN or proxy client is installed (the **Apps (A)** role). ## Which modules do I need? -You always need the **VPN Hide app** (`vpnhide.apk`) plus one native module for interface hiding. The app can also use the optional Ports module if you want localhost port blocking: +You always need the **VPN Hide app** (`vpnhide.apk`) + LSPosed/Vector for the Java layer + exactly one Native backend for native hiding. The app can also use the optional Ports module if you want localhost port blocking: -- **`kmod`** (recommended) — fully out-of-process, invisible to anti-tamper. Requires a supported GKI kernel. -- **`zygisk`** — use this if your kernel isn't supported by kmod. +- **`kmod`** (stable default) — fully out-of-process, invisible to anti-tamper. Requires a supported GKI kernel: 5.10, 5.15, 6.1, 6.6, or 6.12. +- **`KPM`** (beta) — kernel-level backend for 4.14 / 4.19 / 5.4 and other cases where the `.ko` does not fit. Requires APatch or KPatch-Next-Module. +- **`Zygisk`** — fallback if kmod/KPM are unavailable or you do not want to install a KernelPatch runtime. - **`portshide`** (optional) — install this if you want to block selected apps from probing localhost ports. +Do not install multiple Native backends at the same time. If more than one is installed, the app chooses the active one by priority: kmod, then KPM, then Zygisk; uninstall unused modules. + See [Install](#install) for step-by-step instructions. ## Install @@ -64,18 +65,20 @@ Download the latest release from [Releases](https://github.com/okhsunrog/vpnhide 1. Install `vpnhide.apk` as a regular app 2. In LSPosed manager, enable the VPN Hide module and add **"System Framework"** to its scope 3. Reboot the device (required — LSPosed hooks are injected into `system_server` at boot, so the module must be active before `system_server` starts) -4. Open the VPN Hide app and grant it root access (Magisk will prompt automatically; on KernelSU-Next, grant permission manually in the manager) +4. Open the VPN Hide app and grant it root access (Magisk usually prompts automatically; on KernelSU/KernelSU-Next/APatch, grant permission in the manager) ### Step 2 — Native module for interface hiding -Open the VPN Hide app. The **Dashboard** tab will detect your device and kernel, and tell you exactly which native module to install: +Open the VPN Hide app. The **Dashboard** tab will detect your device and kernel, and tell you which Native backend to install: -- If your kernel is supported, it will recommend a specific kmod file (e.g. `vpnhide-kmod-android14-6.1.zip`) -- If not, it will recommend the zygisk module (`vpnhide-zygisk.zip`) +- For a supported GKI kernel, it recommends a specific kmod file, e.g. `vpnhide-kmod-android14-6.1.zip`. +- For old/non-GKI 4.14 / 4.19 / 5.4 kernels, it recommends `vpnhide-kpm.zip` (beta). If no KernelPatch runtime is detected yet, the app asks you to install KPatch-Next-Module first or use Zygisk as a fallback. +- For other kernels, it recommends `vpnhide-zygisk.zip`. Install the recommended module: -- **kmod:** via KernelSU-Next manager → Modules → Install from storage -- **zygisk:** via KernelSU-Next or Magisk manager → Modules +- **kmod:** via KernelSU-Next / KernelSU / Magisk manager → Modules → Install from storage. +- **KPM:** install `vpnhide-kpm.zip`; under APatch the app may ask you to save the SuperKey in **Settings → Security** for boot-time activation. Under Magisk, KernelSU, and KernelSU-Next, install KPatch-Next-Module first if it is not already installed. +- **Zygisk:** via KernelSU-Next, KernelSU, or Magisk manager → Modules. Reboot the device after installing the native module. @@ -83,26 +86,40 @@ Reboot the device after installing the native module. If you want localhost port blocking, install `vpnhide-ports.zip` via KernelSU-Next or Magisk manager. -This module is independent from kmod / zygisk and is only needed for the **Ports** mode in the app. +This module is independent from the Native backend and is only needed for the **Ports** role in the app. -### Step 4 — Configure protections +### Step 4 — Configure hiding -Open the VPN Hide app → **Protection** tab. +Open the VPN Hide app → **Hiding** tab. -- **Tun** mode: use the **L** / **K** / **Z** toggles to control interface hiding layers for each app (LSPosed, Kernel module, Zygisk), or tap the row to toggle all layers at once -- **Apps** mode: choose which apps should be hidden and which apps should act as observers -- **Ports** mode: choose which apps should be blocked from accessing localhost ports +Each app row has roles: + +- **Java** — hide VPN through Android Java APIs at the LSPosed/system_server level. +- **Native** — the active Native backend: kmod, KPM, or Zygisk. VPN Hide stores one Native selection; only the active backend acts. +- **Apps** — the app becomes an observer that should receive a sanitized PackageManager view with selected VPN/proxy apps hidden. +- **Ports** — block this app from reaching localhost ports. + +Settings can switch from short **J / N / A / P** chips to full role labels. For Java, Native, and Ports, the settings icon next to the label opens per-hook or port-range settings. Tap Save after making changes. -After changing targets, force-stop and restart the affected apps — hooks take effect on the next app launch. +Java and kernel-level Native backends (kmod/KPM) apply immediately. Zygisk hooks and Ports rules are picked up by a selected app after force-stop and reopen. -> **Note:** some apps detect Zygisk hooks. For those apps, keep **Z** disabled and rely on kmod + LSPosed. +> **Note:** some apps detect Zygisk hooks when Native is enabled for them. Leave Native off for those apps and rely on the Java layer, or use kmod/KPM instead.
Shell configuration (advanced) -Edit `/data/adb/vpnhide_kmod/targets.txt`, `/data/adb/vpnhide_zygisk/targets.txt`, or `/data/adb/vpnhide_lsposed/targets.txt` directly (one package name per line). Force-stop and restart affected apps for changes to take effect. +The user-managed config lives in `/data/system/vpnhide_config.json`. Edit the JSON, then run the activator for the installed module: + +```sh +su -c /data/adb/modules/vpnhide_kmod/activator +su -c /data/adb/modules/vpnhide_kpm/activator +su -c /data/adb/modules/vpnhide_zygisk/activator +su -c /data/adb/modules/vpnhide_ports/activator +``` + +Run only activators for modules that are actually installed. LSPosed reads the JSON directly from `system_server`; it does not need an activator. Old `/data/adb/vpnhide_*` `targets.txt` files are no longer user config and are only used for migration from older releases.
@@ -121,82 +138,87 @@ Alternatively, run `adb shell uname -r` to see the kernel version string. ## Screenshots -| Dashboard — all OK | Dashboard — issues | Install recommendation | +| Dashboard — VPN hidden | Hiding — one app list | How it works | |:-:|:-:|:-:| -| | | | +| | | | -| Protection — Tun | App hiding | Ports hiding | +| Statistics | Per-hook breakdown | Per-app hook selection | |:-:|:-:|:-:| -| | | | +| | | | -| App hiding help | Ports hiding help | Diagnostics | +| Diagnostics | Settings | Community | |:-:|:-:|:-:| -| | | | +| | | | ## Verify The app has a built-in diagnostics system that catches most setup problems automatically. **Dashboard** (runs on every app launch): -- Module status for all three layers (installed, active, version, target count) +- Module and backend status (installed, active, version, target count) - LSPosed configuration validation — reads the LSPosed database to verify that VPN Hide is enabled, System Framework is in scope, and no extra apps are scoped (a common misconfiguration) - Version mismatch detection — compares installed module versions with the running app version and tells you exactly what to update -- Native module recommendation — detects your kernel and maps it to the right kmod artifact, or recommends zygisk if unsupported -- Live protection check (when VPN is active) — runs 16 native checks and 5 Java API checks to verify that VPN is actually hidden +- Native backend recommendation — detects your kernel and maps it to the right kmod, KPM, or Zygisk artifact +- Live hiding check (when VPN is active) — runs 13 native checks and 12 Java API checks to verify that VPN is actually hidden Any issues found are shown as actionable cards with specific instructions. -**Diagnostics** tab — detailed per-check breakdown with individual PASS/FAIL results for all 26 detection vectors. Useful for troubleshooting when the Dashboard shows partial protection. +**Statistics** tab — per-app breakdown of which apps probe for the VPN and how, showing which checks each app runs (counters reported by the active backends). + +**Settings → Diagnostics** (Detailed diagnostics) — detailed per-check breakdown with individual PASS/FAIL results for all 25 checks. Useful for troubleshooting when the Dashboard shows incomplete hiding. ## Components | Directory | What | How | |---|---|---| -| **[kmod/](kmod/)** | Kernel module (C) | `kretprobe` hooks in kernel space. Zero footprint in the target app's process. ([details](kmod/README.md)) | -| **[lsposed/](lsposed/)** | LSPosed module + app (Kotlin + Rust) | Hooks `writeToParcel` in `system_server` for per-UID Binder filtering. The APK provides a dashboard (module status, version checks, LSPosed config validation, install recommendations), Protection modes for interface / port / app hiding, and diagnostics. ([details](lsposed/README.md)) | +| **[kmod/](kmod/)** | `.ko` kernel module + KPM backend (C) | Two kernel-level Native backends: the stable GKI `.ko` using `kretprobe`, and the KPM beta using KernelPatch inline hooks. Both have zero footprint in the target app's process; only one should be active. ([details](kmod/README.md), [KPM](kmod/kpm/README.md)) | +| **[lsposed/](lsposed/)** | LSPosed module + app (Kotlin + Rust) | Hooks `writeToParcel` in `system_server` for per-UID Binder filtering. The APK provides a dashboard (module status, version checks, LSPosed config validation, install recommendations), the Hiding tab for Java / Native / Apps / Ports roles, and diagnostics. ([details](lsposed/README.md)) | | **[portshide/](portshide/)** | Ports module (Shell + iptables) | Blocks selected apps from reaching `127.0.0.1` / `::1`, hiding locally bound VPN / proxy daemons from localhost port probes. ([details](portshide/README.md)) | -| **[zygisk/](zygisk/)** | Zygisk module (Rust) | Inline-hooks `libc.so` in the target app's process. Alternative to kmod. ([details](zygisk/README.md)) | +| **[zygisk/](zygisk/)** | Zygisk module (Rust) | Inline-hooks `libc.so` in the target app's process. Fallback when a kernel-level backend is unavailable. ([details](zygisk/README.md)) | ## Detection coverage -| # | Detection vector | SELinux | kmod | zygisk | lsposed | -|---|---|---|---|---|---| -| 1 | `ioctl(SIOCGIFFLAGS)` on tun0 | | x | x | | -| 2 | `ioctl(SIOCGIFNAME)` resolve index to name | | x | x | | -| 3 | `ioctl(SIOCGIFMTU)` MTU fingerprinting | | x | x | | -| 4 | `ioctl(SIOCGIFCONF)` interface enumeration | | x | x | | -| 5 | All other `SIOCGIF*` (INDEX, HWADDR, ADDR, etc.) | | x | x | | -| 6 | `getifaddrs()` (uses netlink internally) | | x | x | | -| 7 | netlink `RTM_GETLINK` dump | | x | x | | -| 8 | netlink `RTM_GETADDR` dump (IPv4 + IPv6) | | x | x | | -| 9 | netlink `RTM_GETROUTE` dump | | x | x | | -| 10 | `/proc/net/route` | blocked | x | x | | -| 11 | `/proc/net/ipv6_route` | blocked | x | x | | -| 12 | `/proc/net/if_inet6` | blocked | | x | | -| 13 | `/proc/net/tcp`, `tcp6` | blocked | | x | | -| 14 | `/proc/net/udp`, `udp6` | blocked | | | | -| 15 | `/proc/net/dev` | blocked | | | | -| 16 | `/proc/net/fib_trie` | blocked | | | | -| 17 | `/sys/class/net/tun0/` | blocked | | | | -| 18 | `NetworkCapabilities` (hasTransport, NOT_VPN, transportInfo) | | | | x | -| 19 | `NetworkInfo` (getType, getTypeName) | | | | x | -| 20 | `ConnectivityManager.getActiveNetwork()` | | | | x | -| 21 | `ConnectivityManager.getAllNetworks()` + VPN scan | | | | x | -| 22 | `LinkProperties` (interfaceName) | | | | x | -| 23 | `LinkProperties` (routes via VPN interfaces) | | | | x | -| 24 | `NetworkInterface.getNetworkInterfaces()` | | x | x | | -| 25 | `System.getProperty` (proxy settings) | | | x | | -| 26 | `/proc/net/route` via Java `FileInputStream` | blocked | x | x | | +| # | Detection vector | SELinux | kmod | KPM | Zygisk | LSPosed | +|---|---|---|---|---|---|---| +| 1 | `ioctl(SIOCGIFFLAGS)` on tun0 | | x | x | x | | +| 2 | `ioctl(SIOCGIFNAME)` resolve index to name | | x | x | x | | +| 3 | `ioctl(SIOCGIFMTU)` MTU fingerprinting | | x | x | x | | +| 4 | `ioctl(SIOCGIFCONF)` interface enumeration | | x | x | x | | +| 5 | All other `SIOCGIF*` (INDEX, HWADDR, ADDR, etc.) | | x | x | x | | +| 6 | `getifaddrs()` (uses netlink internally) | | x | x | x | | +| 7 | netlink `RTM_GETLINK` dump | | x | x | x | | +| 8 | netlink `RTM_GETADDR` dump (IPv4 + IPv6) | | x | x | x | | +| 9 | netlink `RTM_GETROUTE` dump | | x | x | x | | +| 10 | `/proc/net/route` | blocked | x | x | x | | +| 11 | `/proc/net/ipv6_route` | blocked | x | x | x | | +| 12 | `/proc/net/if_inet6` | blocked | | | x | | +| 13 | `/proc/net/tcp`, `tcp6` | blocked | | | x | | +| 14 | `/proc/net/udp`, `udp6` | blocked | | | | | +| 15 | `/proc/net/dev` | blocked | | | | | +| 16 | `/proc/net/fib_trie` | blocked | | | | | +| 17 | `/sys/class/net/tun0/` | blocked | | | | | +| 18 | `NetworkCapabilities` (hasTransport, NOT_VPN, transportInfo) | | | | | x | +| 19 | `NetworkInfo` (getType, getTypeName) | | | | | x | +| 20 | `ConnectivityManager.getActiveNetwork()` | | | | | x | +| 21 | `ConnectivityManager.getAllNetworks()` + VPN scan | | | | | x | +| 22 | `LinkProperties` (interfaceName) | | | | | x | +| 23 | `LinkProperties` (routes via VPN interfaces) | | | | | x | +| 24 | `NetworkInterface.getNetworkInterfaces()` | | x | x | x | | +| 25 | `System.getProperty` (proxy settings) | | | | x | | +| 26 | `/proc/net/route` via Java `FileInputStream` | blocked | x | x | x | | **blocked** = on stock-enforcing builds (Android 10+) SELinux usually denies untrusted apps access to that `/proc/net/*` / `/sys` file. But **SELinux policy is configured differently across devices and ROMs** (OEM and custom ROMs, `permissive` builds), so the vpnhide layers filter these paths anyway and never rely on SELinux. -Important: **netlink dumps (rows 7-9) are not restricted by SELinux** — a regular app reads interfaces, addresses, and routes directly over `NETLINK_ROUTE`. This is exactly how detectors like RKNHardering bypass the `/proc/net/route` denial (see [issue #86](https://github.com/okhsunrog/vpnhide/issues/86)). So the vectors actually reachable by a regular app are rows 1-9 and 24, closed by kmod / zygisk; everything else is either often SELinux-blocked on stock (device-dependent) or goes through Java APIs and is covered by lsposed. +Important: **netlink dumps (rows 7-9) are not restricted by SELinux** — a regular app reads interfaces, addresses, and routes directly over `NETLINK_ROUTE`. This is exactly how detectors like RKNHardering bypass the `/proc/net/route` denial (see [issue #86](https://github.com/okhsunrog/vpnhide/issues/86)). So the vectors actually reachable by a regular app are rows 1-9 and 24; the active Native backend handles them. Kernel-level backends (kmod/KPM) do this with no target-process footprint; Zygisk covers libc-routed paths but remains detectable and raw-syscall-bypassable. Everything else is either often SELinux-blocked on stock (device-dependent) or goes through Java APIs and is covered by LSPosed. + +KPM is beta: its coverage mirrors the `.ko` columns above, but there are minor hook-parity gaps versus the stable `.ko`. The full vector map — per-layer breakdown, SELinux caveats, and known gaps — lives in [docs/detection-vectors.md](docs/detection-vectors.md). ## Building from source - **kmod**: `./kmod/build.py --kmi android14-6.1` (or `--all`) — auto-spawns the DDK container via podman/docker. Full guide: [kmod/BUILDING.md](kmod/BUILDING.md). +- **KPM**: `python3 kmod/kpm/build.py` — builds the universal `vpnhide-kpm.zip` through the KernelPatch submodule. Details: [kmod/kpm/README.md](kmod/kpm/README.md). - **zygisk**: `cd zygisk && ./build.py` (Rust + NDK + cargo-ndk) - **lsposed**: `cd lsposed && ./gradlew assembleDebug` (JDK 17 + Rust + NDK + cargo-ndk) @@ -208,10 +230,12 @@ If you're on Windows, there are some inconveniences with building some subprojec **portshide**: `cd .\portshide\; python .\build-zip.py` runs fine. -For the next two, you'll (unfortunately) need to install [Docker for Windows](https://docs.docker.com/desktop/setup/install/windows-install/). +For kmod and zygisk, you'll (unfortunately) need to install [Docker for Windows](https://docs.docker.com/desktop/setup/install/windows-install/). **kmod**: `python .\kmod\build.py --kmi android14-6.1` — the script picks up Docker and pulls the same `ddk-min` image that CI uses. +**KPM**: build from Linux or WSL. The script expects POSIX tools, `make`/`clang`, the KernelPatch submodule, and the Android NDK; a native Windows build path is not documented. + **zygisk**: ```powershell docker run --rm -it -v "${PWD}:/workspace" -v "vpnhide_cargo_cache:/usr/local/cargo/registry" -w /workspace ghcr.io/okhsunrog/vpnhide/ci:latest bash -c 'cd zygisk && python3 ./build.py' @@ -242,10 +266,11 @@ vpnhide hides an active VPN from specific apps. It is NOT designed for: ## Known limitations -- `kmod` requires a GKI kernel with `CONFIG_KPROBES=y` (standard on Android 12+ devices) +- `kmod` requires a supported GKI kernel with `CONFIG_KPROBES=y` (standard on Android 12+ devices) +- KPM is beta and requires a KernelPatch runtime (APatch or KPatch-Next-Module); do not install KPM together with the `.ko` - `lsposed` requires LSPosed, LSPosed-Next, or Vector - `zygisk` is arm64 only -- Direct `svc #0` syscalls bypass zygisk's libc hooks — that's what kmod is for +- Direct `svc #0` syscalls bypass Zygisk's libc hooks — use a kernel-level backend (kmod or KPM) for that - Server-side detection is unfixable client-side — use split tunneling ## License diff --git a/README.md b/README.md index cf26897f..3ccab3f8 100644 --- a/README.md +++ b/README.md @@ -24,35 +24,36 @@ vpnhide решает обе проблемы многослойной архитектурой: -**Уровень 1 — Java API (модуль lsposed):** хукает `system_server`, а не целевое приложение. `NetworkCapabilities`, `NetworkInfo` и `LinkProperties` фильтруются на уровне Binder *до того*, как данные попадут в процесс приложения. Приложение получает чистые данные через IPC — никаких инъекций в его процесс, нечего обнаруживать. +**Уровень 1 — Java API (модуль lsposed):** хукает `system_server`, а не целевое приложение. `NetworkCapabilities`, `NetworkInfo` и `LinkProperties` фильтруются на уровне Binder *до того*, как данные попадут в процесс приложения. Приложение получает чистые данные через IPC — никаких инъекций в его процесс, нечего обнаруживать. Этот же модуль скрывает выбранные приложения от приложений-наблюдателей на уровне `PackageManager` (роль Apps). -**Уровень 2 — нативный (kmod или zygisk):** покрывает все нативные пути обнаружения: -- **kmod** (рекомендуется) — хуки `kretprobe` на уровне ядра. Фильтрует `ioctl` (SIOCGIFFLAGS, SIOCGIFNAME, SIOCGIFCONF), `getifaddrs`/netlink-дампы (RTM_GETLINK, RTM_GETADDR) и чтение `/proc/net/route` — всё до возврата системного вызова в пользовательское пространство. Остальные файлы `/proc/net/*` (`tcp*`, `udp*`, `dev`, `if_inet6` и т.д.) на Android 10+ заблокированы SELinux для untrusted-приложений, так что отдельный хук не нужен. Нулевой след в процессе. Никаких инъекций библиотек. Нечего обнаруживать. -- **zygisk** (альтернатива) — inline-хуки `libc.so` внутри процесса приложения. То же нативное покрытие, что и kmod, но работает в процессе, поэтому теоретически обнаружим продвинутой anti-tamper защитой. Используйте, если ваше ядро не поддерживается kmod. +**Уровень 2 — нативный (kmod, KPM или Zygisk):** покрывает нативные пути обнаружения. Активным должен быть ровно один Native-бэкенд: +- **kmod** (рекомендуется для поддерживаемых GKI-ядер) — хуки `kretprobe` на уровне ядра. Фильтрует `ioctl` (SIOCGIFFLAGS, SIOCGIFNAME, SIOCGIFCONF), `getifaddrs`/netlink-дампы (RTM_GETLINK, RTM_GETADDR), маршруты и `/proc/net/route` до возврата системного вызова в пользовательское пространство. Нулевой след в процессе приложения: никаких инъекций библиотек, нечего обнаруживать. +- **KPM** (бета) — KernelPatch Module с тем же назначением, что и kmod, но без привязки к GKI-варианту. Полезен для старых/non-GKI ядер 4.14 / 4.19 / 5.4 и ситуаций, где `.ko` не может загрузиться. Требует KernelPatch runtime: APatch или KPatch-Next-Module. +- **Zygisk** — запасной вариант, если kernel-level backend поставить нельзя. Inline-хуки `libc.so` работают внутри процесса приложения, поэтому банковские и anti-fraud приложения могут их обнаруживать. Для таких приложений лучше оставлять Native выключенным и полагаться на Java-уровень. -**Уровень 3 — дополнительные механизмы в самом приложении VPN Hide:** -- **Скрытие интерфейса** — скрывает VPN-интерфейсы, маршруты и VPN-состояние Java API от выбранных приложений -- **Скрытие портов** — блокирует выбранным приложениям доступ к `127.0.0.1` / `::1`, чтобы они не могли проверять локально запущенные VPN / proxy-демоны -- **Скрытие приложений** — скрывает выбранные приложения от выбранных приложений-наблюдателей на уровне PackageManager +**Уровень 3 — модуль скрытия портов (portshide):** отдельный Magisk-модуль. Через iptables блокирует выбранным приложениям доступ к `127.0.0.1` / `::1`, чтобы они не могли обнаружить локально запущенный VPN / proxy-демон по открытому порту (роль Ports). -Процесс целевого приложения полностью нетронут (при использовании kmod + lsposed) — ни Xposed, ни inline-хуков, ни модифицированных регионов памяти. Благодаря этому vpnhide работает с банковскими и государственными приложениями, которые активно обнаруживают и блокируют модули на основе Xposed. +Процесс целевого приложения полностью нетронут при использовании LSPosed + kernel-level Native-бэкенда (kmod или KPM) — ни Xposed, ни inline-хуков, ни модифицированных регионов памяти. Благодаря этому vpnhide работает с банковскими и государственными приложениями, которые активно обнаруживают и блокируют модули на основе Xposed. ## Что именно скрывает vpnhide -vpnhide — это не один переключатель, а три разных типа защиты, которые можно включать по отдельности для каждого приложения: +vpnhide скрывает от выбранных приложений три вещи — всё настраивается пер-апп через четыре роли **J / N / A / P** (Java, Native, Apps, Ports): -1. **Скрытие интерфейса** — основной слой скрытия VPN. Убирает VPN-интерфейсы и маршруты из нативных API (`ioctl`, `getifaddrs`, `/proc/net/*`, `NetworkInterface`) и из Java API (`NetworkCapabilities`, `NetworkInfo`, `LinkProperties`). -2. **Скрытие портов** — блокирует доступ к localhost для выбранных приложений, чтобы они не могли обнаружить Clash, sing-box, V2Ray, Happ и подобные инструменты через проверку локальных портов. -3. **Скрытие приложений** — позволяет скрыть выбранные установленные приложения от выбранных приложений-наблюдателей. Полезно против проверок package visibility, например когда приложение пытается определить, установлен ли на устройстве VPN или proxy-клиент. +1. **Скрытие интерфейса** — главная задача. Убирает VPN-интерфейсы и маршруты из нативных API (`ioctl`, `getifaddrs`, `/proc/net/*`, `NetworkInterface`) и из Java API (`NetworkCapabilities`, `NetworkInfo`, `LinkProperties`). Его дают две роли сразу — **Java (J)** и **Native (N)**, они включаются независимо. +2. **Скрытие портов** — блокирует доступ к localhost для выбранных приложений, чтобы они не могли обнаружить Clash, sing-box, V2Ray, Happ и подобные инструменты через проверку локальных портов (роль **Ports (P)**). +3. **Скрытие приложений** — позволяет скрыть выбранные установленные приложения от выбранных приложений-наблюдателей. Полезно против проверок package visibility, например когда приложение пытается определить, установлен ли на устройстве VPN или proxy-клиент (роль **Apps (A)**). ## Какие модули нужны? -Всегда нужно **приложение VPN Hide** (`vpnhide.apk`) плюс один нативный модуль для скрытия интерфейса. Дополнительно приложение может использовать необязательный Ports-модуль для блокировки localhost-портов: +Всегда нужно **приложение VPN Hide** (`vpnhide.apk`) + LSPosed/Vector для Java-уровня + ровно один Native-бэкенд для нативного скрытия. Дополнительно приложение может использовать необязательный Ports-модуль для блокировки localhost-портов: -- **`kmod`** (рекомендуется) — полностью out-of-process, невидим для anti-tamper. Требуется поддерживаемое GKI-ядро. -- **`zygisk`** — используйте, если ваше ядро не поддерживается kmod. +- **`kmod`** (стабильный вариант по умолчанию) — полностью out-of-process, невидим для anti-tamper. Требуется поддерживаемое GKI-ядро: 5.10, 5.15, 6.1, 6.6 или 6.12. +- **`KPM`** (бета) — kernel-level backend для 4.14 / 4.19 / 5.4 и других случаев, где `.ko` не подходит. Нужен APatch или KPatch-Next-Module. +- **`Zygisk`** — fallback, если kmod/KPM недоступны или вы не хотите ставить KernelPatch runtime. - **`portshide`** (необязательно) — установите, если хотите блокировать выбранным приложениям доступ к localhost-портам. +Не ставьте несколько Native-бэкендов одновременно. Если они всё же установлены, приложение выбирает активный по приоритету: kmod, затем KPM, затем Zygisk; лишние модули лучше удалить. + См. [Установка](#установка) для пошаговой инструкции. ## Установка @@ -64,18 +65,20 @@ vpnhide — это не один переключатель, а три разн 1. Установите `vpnhide.apk` как обычное приложение 2. В менеджере LSPosed включите модуль VPN Hide и добавьте **«System Framework»** в его область действия 3. Перезагрузите устройство (обязательно — хуки LSPosed внедряются в `system_server` при загрузке, поэтому модуль должен быть активен до запуска `system_server`) -4. Откройте приложение VPN Hide и предоставьте ему root-доступ (Magisk запросит автоматически; на KernelSU-Next выдайте разрешение вручную в менеджере) +4. Откройте приложение VPN Hide и предоставьте ему root-доступ (Magisk обычно запросит автоматически; на KernelSU/KernelSU-Next/APatch выдайте разрешение в менеджере) ### Шаг 2 — Нативный модуль для скрытия интерфейса -Откройте приложение VPN Hide. На вкладке **«Обзор»** приложение определит ваше устройство и ядро и покажет, какой именно нативный модуль нужно установить: +Откройте приложение VPN Hide. На вкладке **«Обзор»** приложение определит устройство и ядро и покажет, какой Native-бэкенд лучше установить: -- Если ядро поддерживается — будет рекомендован конкретный файл kmod (например, `vpnhide-kmod-android14-6.1.zip`) -- Если нет — будет рекомендован модуль zygisk (`vpnhide-zygisk.zip`) +- Для поддерживаемого GKI-ядра будет рекомендован конкретный файл kmod, например `vpnhide-kmod-android14-6.1.zip`. +- Для non-GKI/старых ядер 4.14 / 4.19 / 5.4 будет рекомендован `vpnhide-kpm.zip` (бета). Если KernelPatch runtime ещё не найден, приложение попросит сначала установить KPatch-Next-Module или использовать Zygisk как fallback. +- Для остальных ядер будет рекомендован `vpnhide-zygisk.zip`. Установите рекомендованный модуль: -- **kmod:** через менеджер KernelSU-Next → Модули → Установить из хранилища -- **zygisk:** через менеджер KernelSU-Next или Magisk → Модули +- **kmod:** через KernelSU-Next / KernelSU / Magisk → Модули → Установить из хранилища. +- **KPM:** установите `vpnhide-kpm.zip`; под APatch приложение может попросить сохранить SuperKey в **Настройки → Безопасность** для активации при загрузке. Под Magisk, KernelSU и KernelSU-Next сначала установите KPatch-Next-Module, если он ещё не установлен. +- **Zygisk:** через KernelSU-Next, KernelSU или Magisk → Модули. Перезагрузите устройство после установки нативного модуля. @@ -83,26 +86,40 @@ vpnhide — это не один переключатель, а три разн Если вам нужна блокировка localhost-портов, установите `vpnhide-ports.zip` через KernelSU-Next или Magisk manager. -Этот модуль независим от kmod / zygisk и нужен только для режима **Ports** в приложении. +Этот модуль независим от Native-бэкенда и нужен только для роли **Ports** в приложении. -### Шаг 4 — Настройка защит +### Шаг 4 — Настройка скрытия -Откройте приложение VPN Hide → вкладка **«Защита»**. +Откройте приложение VPN Hide → вкладка **«Скрытие»**. -- Режим **Tun**: используйте переключатели **L** / **K** / **Z** для управления уровнями скрытия интерфейса для каждого приложения (LSPosed, модуль ядра, Zygisk), или нажмите на строку, чтобы переключить все уровни сразу -- Режим **Apps**: выберите, какие приложения нужно скрывать и какие приложения должны выступать наблюдателями -- Режим **Ports**: выберите, каким приложениям нужно запретить доступ к localhost-портам +В списке у каждого приложения есть роли: + +- **Java** — скрытие VPN через Android Java API на уровне LSPosed/system_server. +- **Native** — активный Native-бэкенд: kmod, KPM или Zygisk. VPN Hide сохраняет один выбор Native; работает только активный бэкенд. +- **Apps** — приложение становится наблюдателем, от которого нужно скрывать выбранные VPN/proxy-приложения через PackageManager. +- **Ports** — приложению блокируется доступ к localhost-портам. + +В настройках можно включить полные подписи ролей вместо коротких **J / N / A / P**. Для Java, Native и Ports значок настройки рядом с названием открывает индивидуальные хуки или диапазоны портов. После изменений нажмите «Сохранить». -После изменения списка принудительно остановите и перезапустите затронутые приложения — хуки вступают в силу при следующем запуске. +Java и kernel-level Native-бэкенды (kmod/KPM) применяются сразу. Zygisk-хуки и правила Ports применяются для выбранного приложения после принудительной остановки и повторного запуска. -> **Примечание:** некоторые приложения обнаруживают хуки Zygisk. Для таких приложений оставьте **Z** выключенным и используйте kmod + LSPosed. +> **Примечание:** некоторые приложения обнаруживают Zygisk-хуки, если для них включён Native. Для таких приложений оставьте Native выключенным и используйте Java-уровень, либо перейдите на kmod/KPM.
Настройка через командную строку (для продвинутых) -Редактируйте `/data/adb/vpnhide_kmod/targets.txt`, `/data/adb/vpnhide_zygisk/targets.txt` или `/data/adb/vpnhide_lsposed/targets.txt` напрямую (одно имя пакета на строку). Принудительно остановите и перезапустите затронутые приложения для применения изменений. +Пользовательская конфигурация хранится в `/data/system/vpnhide_config.json`. Отредактируйте JSON и запустите activator установленного модуля: + +```sh +su -c /data/adb/modules/vpnhide_kmod/activator +su -c /data/adb/modules/vpnhide_kpm/activator +su -c /data/adb/modules/vpnhide_zygisk/activator +su -c /data/adb/modules/vpnhide_ports/activator +``` + +Запускайте только activator тех модулей, которые действительно установлены. LSPosed читает JSON напрямую из `system_server`, отдельный activator для него не нужен. Старые `targets.txt` в `/data/adb/vpnhide_*` больше не являются пользовательской настройкой и используются только для миграции старых конфигов.
@@ -121,82 +138,87 @@ vpnhide — это не один переключатель, а три разн ## Скриншоты -| Обзор — всё ОК | Обзор — проблемы | Рекомендация установки | +| Обзор — VPN скрыт | Скрытие — единый список | Как это работает | |:-:|:-:|:-:| -| | | | +| | | | -| Защита — Tun | Скрытие приложений | Скрытие портов | +| Статистика | Разбор по хукам | Выбор хуков для приложения | |:-:|:-:|:-:| -| | | | +| | | | -| Скрытие приложений — помощь | Скрытие портов — помощь | Диагностика | +| Диагностика | Настройки | Сообщество | |:-:|:-:|:-:| -| | | | +| | | | ## Проверка В приложении есть встроенная система диагностики, которая автоматически обнаруживает большинство проблем с настройкой. **Обзор** (запускается при каждом открытии приложения): -- Статус модулей для всех трёх уровней (установлен, активен, версия, количество целей) +- Статус модулей и бэкендов (установлен, активен, версия, количество целей) - Валидация конфигурации LSPosed — читает базу данных LSPosed и проверяет, что VPN Hide включён, System Framework в scope, и нет лишних приложений в scope (частая ошибка при настройке) - Обнаружение несоответствия версий — сравнивает версии установленных модулей с версией приложения и подсказывает, что именно нужно обновить -- Рекомендация нативного модуля — определяет ядро устройства и подбирает нужный файл kmod, или рекомендует zygisk, если ядро не поддерживается -- Проверка защиты в реальном времени (при активном VPN) — выполняет 16 нативных и 5 Java API проверок, чтобы убедиться, что VPN действительно скрыт +- Рекомендация Native-бэкенда — определяет ядро устройства и подбирает нужный файл kmod, KPM или Zygisk +- Проверка скрытия в реальном времени (при активном VPN) — выполняет 13 нативных и 12 Java API проверок, чтобы убедиться, что VPN действительно скрыт Все обнаруженные проблемы показываются в виде карточек с конкретными инструкциями по исправлению. -**Диагностика** — детальная разбивка по каждой проверке с индивидуальными результатами PASS/FAIL для всех 26 векторов обнаружения. Полезна для отладки, когда «Обзор» показывает частичную защиту. +**Статистика** — счётчики перехватов по каждому приложению: какие приложения и какими методами проверяют VPN. + +**Настройки → Диагностика** — детальная разбивка по каждой проверке с индивидуальными результатами PASS/FAIL по всем 25 проверкам. Полезна для отладки, когда «Обзор» показывает неполное скрытие. ## Компоненты | Директория | Что | Как | |---|---|---| -| **[kmod/](kmod/)** | Модуль ядра (C) | Хуки `kretprobe` в пространстве ядра. Нулевой след в процессе приложения. ([подробнее](kmod/README.md)) | -| **[lsposed/](lsposed/)** | LSPosed-модуль + приложение (Kotlin + Rust) | Хуки `writeToParcel` в `system_server` для per-UID фильтрации Binder. APK предоставляет обзорную панель (статус модулей, проверка версий, валидация конфигурации LSPosed, рекомендации по установке), режимы защиты для скрытия интерфейса / портов / приложений и диагностику. ([подробнее](lsposed/README.md)) | +| **[kmod/](kmod/)** | Модуль ядра `.ko` + KPM backend (C) | Два kernel-level Native-бэкенда: стабильный GKI `.ko` на `kretprobe` и KPM-бета на KernelPatch inline hooks. Оба дают нулевой след в процессе приложения; активным должен быть только один. ([подробнее](kmod/README.md), [KPM](kmod/kpm/README.md)) | +| **[lsposed/](lsposed/)** | LSPosed-модуль + приложение (Kotlin + Rust) | Хуки `writeToParcel` в `system_server` для per-UID фильтрации Binder. APK предоставляет обзорную панель (статус модулей, проверка версий, валидация конфигурации LSPosed, рекомендации по установке), вкладку «Скрытие» для ролей Java / Native / Apps / Ports и диагностику. ([подробнее](lsposed/README.md)) | | **[portshide/](portshide/)** | Модуль скрытия портов (Shell + iptables) | Блокирует выбранным приложениям доступ к `127.0.0.1` / `::1`, скрывая локально запущенные VPN / proxy-демоны от проверок localhost-портов. ([подробнее](portshide/README.md)) | -| **[zygisk/](zygisk/)** | Zygisk-модуль (Rust) | Inline-хуки `libc.so` в процессе приложения. Альтернатива kmod. ([подробнее](zygisk/README.md)) | +| **[zygisk/](zygisk/)** | Zygisk-модуль (Rust) | Inline-хуки `libc.so` в процессе приложения. Fallback, когда kernel-level backend недоступен. ([подробнее](zygisk/README.md)) | ## Покрытие обнаружения -| # | Вектор обнаружения | SELinux | kmod | zygisk | lsposed | -|---|---|---|---|---|---| -| 1 | `ioctl(SIOCGIFFLAGS)` на tun0 | | x | x | | -| 2 | `ioctl(SIOCGIFNAME)` разрешение индекса в имя | | x | x | | -| 3 | `ioctl(SIOCGIFMTU)` фингерпринтинг MTU | | x | x | | -| 4 | `ioctl(SIOCGIFCONF)` перечисление интерфейсов | | x | x | | -| 5 | Все остальные `SIOCGIF*` (INDEX, HWADDR, ADDR и т.д.) | | x | x | | -| 6 | `getifaddrs()` (использует netlink внутри) | | x | x | | -| 7 | netlink `RTM_GETLINK` дамп | | x | x | | -| 8 | netlink `RTM_GETADDR` дамп (IPv4 + IPv6) | | x | x | | -| 9 | netlink `RTM_GETROUTE` дамп | | x | x | | -| 10 | `/proc/net/route` | блок. | x | x | | -| 11 | `/proc/net/ipv6_route` | блок. | x | x | | -| 12 | `/proc/net/if_inet6` | блок. | | x | | -| 13 | `/proc/net/tcp`, `tcp6` | блок. | | x | | -| 14 | `/proc/net/udp`, `udp6` | блок. | | | | -| 15 | `/proc/net/dev` | блок. | | | | -| 16 | `/proc/net/fib_trie` | блок. | | | | -| 17 | `/sys/class/net/tun0/` | блок. | | | | -| 18 | `NetworkCapabilities` (hasTransport, NOT_VPN, transportInfo) | | | | x | -| 19 | `NetworkInfo` (getType, getTypeName) | | | | x | -| 20 | `ConnectivityManager.getActiveNetwork()` | | | | x | -| 21 | `ConnectivityManager.getAllNetworks()` + VPN-сканирование | | | | x | -| 22 | `LinkProperties` (interfaceName) | | | | x | -| 23 | `LinkProperties` (маршруты через VPN-интерфейсы) | | | | x | -| 24 | `NetworkInterface.getNetworkInterfaces()` | | x | x | | -| 25 | `System.getProperty` (настройки прокси) | | | x | | -| 26 | `/proc/net/route` через Java `FileInputStream` | блок. | x | x | | +| # | Вектор обнаружения | SELinux | kmod | KPM | Zygisk | LSPosed | +|---|---|---|---|---|---|---| +| 1 | `ioctl(SIOCGIFFLAGS)` на tun0 | | x | x | x | | +| 2 | `ioctl(SIOCGIFNAME)` разрешение индекса в имя | | x | x | x | | +| 3 | `ioctl(SIOCGIFMTU)` фингерпринтинг MTU | | x | x | x | | +| 4 | `ioctl(SIOCGIFCONF)` перечисление интерфейсов | | x | x | x | | +| 5 | Все остальные `SIOCGIF*` (INDEX, HWADDR, ADDR и т.д.) | | x | x | x | | +| 6 | `getifaddrs()` (использует netlink внутри) | | x | x | x | | +| 7 | netlink `RTM_GETLINK` дамп | | x | x | x | | +| 8 | netlink `RTM_GETADDR` дамп (IPv4 + IPv6) | | x | x | x | | +| 9 | netlink `RTM_GETROUTE` дамп | | x | x | x | | +| 10 | `/proc/net/route` | блок. | x | x | x | | +| 11 | `/proc/net/ipv6_route` | блок. | x | x | x | | +| 12 | `/proc/net/if_inet6` | блок. | | | x | | +| 13 | `/proc/net/tcp`, `tcp6` | блок. | | | x | | +| 14 | `/proc/net/udp`, `udp6` | блок. | | | | | +| 15 | `/proc/net/dev` | блок. | | | | | +| 16 | `/proc/net/fib_trie` | блок. | | | | | +| 17 | `/sys/class/net/tun0/` | блок. | | | | | +| 18 | `NetworkCapabilities` (hasTransport, NOT_VPN, transportInfo) | | | | | x | +| 19 | `NetworkInfo` (getType, getTypeName) | | | | | x | +| 20 | `ConnectivityManager.getActiveNetwork()` | | | | | x | +| 21 | `ConnectivityManager.getAllNetworks()` + VPN-сканирование | | | | | x | +| 22 | `LinkProperties` (interfaceName) | | | | | x | +| 23 | `LinkProperties` (маршруты через VPN-интерфейсы) | | | | | x | +| 24 | `NetworkInterface.getNetworkInterfaces()` | | x | x | x | | +| 25 | `System.getProperty` (настройки прокси) | | | | x | | +| 26 | `/proc/net/route` через Java `FileInputStream` | блок. | x | x | x | | **блок.** = на сток-enforcing сборках (Android 10+) SELinux обычно запрещает обычным приложениям доступ к этому файлу `/proc/net/*` / `/sys`. Но политика SELinux настроена **по-разному на разных устройствах и прошивках** (OEM- и кастомные ROM, `permissive`-сборки), поэтому слои vpnhide всё равно фильтруют эти пути и не полагаются на SELinux. -Важно: **netlink-дампы (строки 7–9) SELinux не ограничивает** — обычное приложение читает интерфейсы, адреса и маршруты через `NETLINK_ROUTE` напрямую. Именно так детекторы вроде RKNHardering обходят блокировку `/proc/net/route` (см. [issue #86](https://github.com/okhsunrog/vpnhide/issues/86)). Поэтому векторы, реально доступные обычному приложению, — это строки 1–9 и 24; их закрывают kmod / zygisk. Остальное либо часто блокируется SELinux на стоке (но это зависит от устройства), либо идёт через Java API и покрывается lsposed. +Важно: **netlink-дампы (строки 7–9) SELinux не ограничивает** — обычное приложение читает интерфейсы, адреса и маршруты через `NETLINK_ROUTE` напрямую. Именно так детекторы вроде RKNHardering обходят блокировку `/proc/net/route` (см. [issue #86](https://github.com/okhsunrog/vpnhide/issues/86)). Поэтому векторы, реально доступные обычному приложению, — это строки 1–9 и 24; их закрывает активный Native-бэкенд. Kernel-level варианты (kmod/KPM) закрывают их без следа в процессе; Zygisk закрывает libc-пути, но остаётся обнаружимым и обходится raw syscall. Остальное либо часто блокируется SELinux на стоке (но это зависит от устройства), либо идёт через Java API и покрывается LSPosed. + +KPM в бете: его покрытие повторяет колонки `.ko` выше, но есть небольшие расхождения в паритете хуков относительно стабильного `.ko`. Полная карта векторов — с разбивкой по слоям, нюансами SELinux и известными пробелами — в [docs/detection-vectors.md](docs/detection-vectors.md). ## Сборка из исходников - **kmod**: `./kmod/build.py --kmi android14-6.1` (или `--all`) — авто-запускает DDK-контейнер через podman/docker. Подробнее: [kmod/BUILDING.md](kmod/BUILDING.md). +- **KPM**: `python3 kmod/kpm/build.py` — собирает универсальный `vpnhide-kpm.zip` через KernelPatch submodule. Подробнее: [kmod/kpm/README.md](kmod/kpm/README.md). - **zygisk**: `cd zygisk && ./build.py` (Rust + NDK + cargo-ndk) - **lsposed**: `cd lsposed && ./gradlew assembleDebug` (JDK 17 + Rust + NDK + cargo-ndk) @@ -208,10 +230,12 @@ vpnhide — это не один переключатель, а три разн **portshide**: `cd .\portshide\; python .\build-zip.py` выполняется без проблем. -Для следующих двух вам (к сожалению) потребуется установить [Docker for Windows](https://docs.docker.com/desktop/setup/install/windows-install/). +Для kmod и zygisk вам (к сожалению) потребуется установить [Docker for Windows](https://docs.docker.com/desktop/setup/install/windows-install/). **kmod**: `python .\kmod\build.py --kmi android14-6.1` — скрипт сам подберёт docker и поднимет образ `ddk-min` (тот же, что в CI). +**KPM**: собирайте из Linux или WSL. Скрипт ожидает POSIX-инструменты, `make`/`clang`, KernelPatch submodule и Android NDK; нативная сборка на Windows сейчас не документирована. + **zygisk**: ```powershell docker run --rm -it -v "${PWD}:/workspace" -v "vpnhide_cargo_cache:/usr/local/cargo/registry" -w /workspace ghcr.io/okhsunrog/vpnhide/ci:latest bash -c 'cd zygisk && python3 ./build.py' @@ -242,10 +266,11 @@ vpnhide скрывает активный VPN от конкретных прил ## Известные ограничения -- `kmod` требует GKI-ядро с `CONFIG_KPROBES=y` (стандарт на устройствах Android 12+) +- `kmod` требует поддерживаемое GKI-ядро с `CONFIG_KPROBES=y` (стандарт на устройствах Android 12+) +- KPM — бета и требует KernelPatch runtime (APatch или KPatch-Next-Module); не ставьте KPM одновременно с `.ko` - `lsposed` требует LSPosed, LSPosed-Next или Vector - `zygisk` — только arm64 -- Прямые системные вызовы `svc #0` обходят хуки libc в zygisk — для этого и нужен kmod +- Прямые системные вызовы `svc #0` обходят хуки libc в Zygisk — для этого нужен kernel-level backend (kmod или KPM) - Серверная детекция неисправима на стороне клиента — используйте раздельное туннелирование ## Лицензия diff --git a/assets/screenshots/app-hiding-help.png b/assets/screenshots/app-hiding-help.png deleted file mode 100644 index 233d503c..00000000 Binary files a/assets/screenshots/app-hiding-help.png and /dev/null differ diff --git a/assets/screenshots/app-hiding-list.png b/assets/screenshots/app-hiding-list.png deleted file mode 100644 index 3515ea36..00000000 Binary files a/assets/screenshots/app-hiding-list.png and /dev/null differ diff --git a/assets/screenshots/apps-filter-russian.jpg b/assets/screenshots/apps-filter-russian.jpg deleted file mode 100644 index 3e92c1a4..00000000 Binary files a/assets/screenshots/apps-filter-russian.jpg and /dev/null differ diff --git a/assets/screenshots/apps-help-dialog.jpg b/assets/screenshots/apps-help-dialog.jpg deleted file mode 100644 index c089a23d..00000000 Binary files a/assets/screenshots/apps-help-dialog.jpg and /dev/null differ diff --git a/assets/screenshots/community.png b/assets/screenshots/community.png new file mode 100644 index 00000000..0be2e396 Binary files /dev/null and b/assets/screenshots/community.png differ diff --git a/assets/screenshots/dashboard-all-ok.jpg b/assets/screenshots/dashboard-all-ok.jpg deleted file mode 100644 index e877a10d..00000000 Binary files a/assets/screenshots/dashboard-all-ok.jpg and /dev/null differ diff --git a/assets/screenshots/dashboard-all-ok.png b/assets/screenshots/dashboard-all-ok.png deleted file mode 100644 index 1c99a447..00000000 Binary files a/assets/screenshots/dashboard-all-ok.png and /dev/null differ diff --git a/assets/screenshots/dashboard-hidden.png b/assets/screenshots/dashboard-hidden.png new file mode 100644 index 00000000..f832125d Binary files /dev/null and b/assets/screenshots/dashboard-hidden.png differ diff --git a/assets/screenshots/dashboard-install-recommendation.jpg b/assets/screenshots/dashboard-install-recommendation.jpg deleted file mode 100644 index 21409375..00000000 Binary files a/assets/screenshots/dashboard-install-recommendation.jpg and /dev/null differ diff --git a/assets/screenshots/dashboard-issues.jpg b/assets/screenshots/dashboard-issues.jpg deleted file mode 100644 index f4e19f9d..00000000 Binary files a/assets/screenshots/dashboard-issues.jpg and /dev/null differ diff --git a/assets/screenshots/diagnostics-java.jpg b/assets/screenshots/diagnostics-java.jpg deleted file mode 100644 index 0c0e8940..00000000 Binary files a/assets/screenshots/diagnostics-java.jpg and /dev/null differ diff --git a/assets/screenshots/diagnostics-java.png b/assets/screenshots/diagnostics-java.png new file mode 100644 index 00000000..14ef5941 Binary files /dev/null and b/assets/screenshots/diagnostics-java.png differ diff --git a/assets/screenshots/diagnostics-native.jpg b/assets/screenshots/diagnostics-native.jpg deleted file mode 100644 index f5c95962..00000000 Binary files a/assets/screenshots/diagnostics-native.jpg and /dev/null differ diff --git a/assets/screenshots/diagnostics-native.png b/assets/screenshots/diagnostics-native.png new file mode 100644 index 00000000..6705f812 Binary files /dev/null and b/assets/screenshots/diagnostics-native.png differ diff --git a/assets/screenshots/hiding-help.png b/assets/screenshots/hiding-help.png new file mode 100644 index 00000000..487d7b1c Binary files /dev/null and b/assets/screenshots/hiding-help.png differ diff --git a/assets/screenshots/hiding-list.png b/assets/screenshots/hiding-list.png new file mode 100644 index 00000000..de072897 Binary files /dev/null and b/assets/screenshots/hiding-list.png differ diff --git a/assets/screenshots/hook-picker.png b/assets/screenshots/hook-picker.png new file mode 100644 index 00000000..d153bc8b Binary files /dev/null and b/assets/screenshots/hook-picker.png differ diff --git a/assets/screenshots/ports-hiding-help.png b/assets/screenshots/ports-hiding-help.png deleted file mode 100644 index e5f628c5..00000000 Binary files a/assets/screenshots/ports-hiding-help.png and /dev/null differ diff --git a/assets/screenshots/ports-hiding-list.png b/assets/screenshots/ports-hiding-list.png deleted file mode 100644 index f3e51d7b..00000000 Binary files a/assets/screenshots/ports-hiding-list.png and /dev/null differ diff --git a/assets/screenshots/protection-tunnels-list.png b/assets/screenshots/protection-tunnels-list.png deleted file mode 100644 index f67c50f2..00000000 Binary files a/assets/screenshots/protection-tunnels-list.png and /dev/null differ diff --git a/assets/screenshots/settings-advanced.png b/assets/screenshots/settings-advanced.png new file mode 100644 index 00000000..a773bcf8 Binary files /dev/null and b/assets/screenshots/settings-advanced.png differ diff --git a/assets/screenshots/settings-developer.png b/assets/screenshots/settings-developer.png new file mode 100644 index 00000000..9d4dec12 Binary files /dev/null and b/assets/screenshots/settings-developer.png differ diff --git a/assets/screenshots/settings.png b/assets/screenshots/settings.png new file mode 100644 index 00000000..60c5df27 Binary files /dev/null and b/assets/screenshots/settings.png differ diff --git a/assets/screenshots/statistics-apps.png b/assets/screenshots/statistics-apps.png new file mode 100644 index 00000000..910316ea Binary files /dev/null and b/assets/screenshots/statistics-apps.png differ diff --git a/assets/screenshots/statistics-breakdown.png b/assets/screenshots/statistics-breakdown.png new file mode 100644 index 00000000..24901833 Binary files /dev/null and b/assets/screenshots/statistics-breakdown.png differ diff --git a/assets/screenshots/statistics.png b/assets/screenshots/statistics.png new file mode 100644 index 00000000..7822badd Binary files /dev/null and b/assets/screenshots/statistics.png differ diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index a65544a0..9eb54b9a 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -84,12 +84,14 @@ Follow-up work (low priority): ### 32-bit (compat) SIOCGIFCONF enumeration (low priority) The kmod `.ko` filters `SIOCGIFCONF` interface enumeration via a kretprobe on -`sock_ioctl`. A **32-bit** app's `SIOCGIFCONF` enters the kernel through +`sock_ioctl`, including the 64-bit `ifc_req == NULL` size-query path. A +**32-bit** app's `SIOCGIFCONF` still enters the kernel through `compat_sock_ioctl` → `compat_dev_ifconf` and never reaches `sock_ioctl`, so its -enumeration is not filtered (it sees VPN interfaces). The size-query subcase of -the 64-bit path (the `ifc_req == NULL` length query) is likewise unfiltered. -Both are tracked in [detection-vectors.md](detection-vectors.md) (the SIOCGIFCONF -notes). +enumeration is not filtered (it sees VPN interfaces). KPM also compacts the +filled ifreq array but does not yet reduce the `ifc_req == NULL` size query — +a separate, 64-bit gap, not the 32-bit/compat case this heading names. +Both remaining cases are tracked in [detection-vectors.md](detection-vectors.md) +(the SIOCGIFCONF notes). This is **low priority**: most current Android apps are 64-bit, and modern interface enumeration uses netlink `getifaddrs` (covered by the rtnl/inet fill diff --git a/docs/detection-vectors.md b/docs/detection-vectors.md index eb316c9f..78d1586e 100644 --- a/docs/detection-vectors.md +++ b/docs/detection-vectors.md @@ -33,48 +33,51 @@ Two recurring assumptions, both documented in ROADMAP: --- -## 2. The four enforcement layers +## 2. The enforcement layers -Coverage comes from three vpnhide components plus the platform. They differ in +Coverage comes from vpnhide components plus the platform. They differ in *where* they sit, *which APIs* they can touch, *how* targets are selected, and *whether a raw-syscall detector can bypass them*. | Layer | Where it runs | Selects targets by | Sees | Bypassable by raw syscalls? | |---|---|---|---|---| | **kmod** | Linux kernel (kretprobes) | **UID** (`/proc/vpnhide_ctl`) | Everything below libc — the syscall/skb/seq_file source | **No** — filters at the source regardless of how userspace calls | +| **KPM** | Linux kernel (KernelPatch inline hooks) | **UID** (KPM `ctl0` supercall) | Same kernel sources as kmod, delivered through KernelPatch instead of `.ko` loading | **No** — filters at the source regardless of how userspace calls | | **zygisk** | target process, inline `libc` hooks (shadowhook) | **package** (canonical JSON -> module-dir runtime wire, per-fork) | Only calls routed through the hooked `libc` symbols | **Yes** — a direct `svc #0` / unhooked libc entry slips past | | **lsposed** | `system_server`, Binder hooks (Vector framework) | **package/appId** (`vpnhide_config.json` + `/data/system/packages.list`) | Java/framework Binder results *before* serialization to the app | N/A — the data is built in another process; the app only gets the sanitized parcel | | **SELinux** | platform policy | domain (`untrusted_app`) | n/a — it *denies* access rather than filtering | n/a — a denial is a denial | Key consequences: -- **kmod is the only bypass-proof layer.** A detector reading `/proc/net/route` - with a raw `openat` syscall, or driving netlink without libc, defeats zygisk - but not kmod. zygisk's value is that it works **without a custom kernel** - (stock GKI, Magisk/KSU) and reaches the same native APIs for libc-routed - callers. +- **The kernel backends (kmod `.ko` and KPM) are the bypass-proof native layers.** + A detector reading `/proc/net/route` with a raw `openat` syscall, or driving + netlink without libc, defeats Zygisk but not a kernel backend. kmod is the + stable default for supported GKI kernels; KPM is the beta KernelPatch path for + old/non-GKI kernels and `.ko` load failures. - **lsposed is the only layer that can fake the high-level Java network model** (`ConnectivityManager`, `LinkProperties`, capabilities, callbacks) and **package visibility**. Native layers cannot synthesize a coherent `NetworkCapabilities` parcel; framework layers cannot touch `getifaddrs`. -- **kmod and zygisk are two implementations of the *same* native layer — you - install one, not both.** They cover essentially the same native vectors - (interface enumeration, routes, procfs, netlink); the choice is a tradeoff, - not an addition: - - **kmod** is preferred — bypass-proof and out-of-process (zero footprint in - the app), but needs a supported GKI kernel with `CONFIG_KPROBES`. - - **zygisk** is the fallback for kernels kmod doesn't support — works on any - arm64 kernel, but only catches libc-routed calls (a raw `svc #0` slips - past) and runs inside the app process (visible to aggressive anti-tamper). - Running both at once is redundant, not additive (both would filter the same - data for the same target). The small deltas between them — e.g. zygisk also - filters `/proc/net/{if_inet6,tcp,tcp6}`, kmod also handles `RTM_GETRULE` and - the server host-route — are noted in the matrix; neither delta is a reason to - stack them. -- So a complete install is **two roles**: the native layer (kmod *or* zygisk) - **plus** lsposed for the Java vectors, with SELinux as an unreliable platform - backstop underneath. Where only one layer covers a vector, that is called out - below. +- **kmod, KPM, and Zygisk are implementations of the same Native role — you + install one active backend, not a stack.** The app prioritizes kmod > KPM > + Zygisk when more than one is installed. `.ko` + KPM together is the dangerous + case because they hook the same kernel functions and can freeze the device; + other overlaps are redundant and make setup harder to reason about. + - **kmod** is preferred on supported GKI kernels — bypass-proof and + out-of-process, with the most test coverage; needs `CONFIG_KPROBES`. + - **KPM** is also bypass-proof and out-of-process, but beta and dependent on + KernelPatch runtime (APatch or KPatch-Next-Module). + - **Zygisk** is the fallback where no kernel backend is practical — works on + any arm64 kernel, but only catches libc-routed calls (a raw `svc #0` slips + past) and runs in-process, visible to aggressive anti-tamper. + The small deltas between backends — e.g. Zygisk also filters + `/proc/net/{if_inet6,tcp,tcp6}`, kmod/KPM also handle `RTM_GETRULE` and the + server host-route — are noted in the matrix; neither delta is a reason to + stack native backends. +- So a complete install is **two components**: exactly one native backend (kmod, + KPM, or Zygisk) **plus** lsposed — which covers both the Java network vectors + and package visibility — with SELinux as an unreliable platform backstop + underneath. Where only one layer covers a vector, that is called out below. > **SELinux is a free but unreliable backstop.** On a Pixel 4a / Android 13, > `untrusted_app` is already *denied* `read` on `/proc/net/route` and `search` @@ -106,20 +109,20 @@ Legend: ✅ covered · ⚠️ partial / conditional · — not applicable to tha ### 3A. Interface enumeration — "is there a tun/wg/ppp interface?" -| Vector | How it manifests | kmod | zygisk | lsposed | SELinux | -|---|---|:--:|:--:|:--:|:--:| -| `getifaddrs()` | native list of ifaces+addrs | ✅ via netlink hooks | ✅ unlinks VPN nodes | — | | -| `NetworkInterface.getNetworkInterfaces()` (Java) | JNI → `getifaddrs` | ✅ | ✅ | — | | -| `ioctl(SIOCGIFNAME)` index→name | native | ✅ `dev_ioctl` | ✅ | — | | -| `ioctl(SIOCGIFCONF)` enumerate | native | ✅ `sock_ioctl` | ✅ `filter_ifconf` | — | | -| `ioctl(SIOCGIF{FLAGS,MTU,INDEX,HWADDR,ADDR})` by name | native | ✅ `dev_ioctl` | ✅ pre-screen | — | | -| netlink `RTM_GETLINK` dump | `recvmsg`/`recvfrom` of `RTM_NEWLINK` | ✅ `rtnl_fill_ifinfo` | ✅ filter by index | — | | -| netlink `RTM_GETADDR` dump | `RTM_NEWADDR` | ✅ `inet*_fill_ifaddr` | ✅ filter by index | — | | -| `/sys/class/net//*` | reads iface type/mtu/operstate | — | — | — | 🔒 usually denied | +| Vector | How it manifests | kmod | KPM | Zygisk | lsposed | SELinux | +|---|---|:--:|:--:|:--:|:--:|:--:| +| `getifaddrs()` | native list of ifaces+addrs | ✅ via netlink hooks | ✅ via netlink hooks | ✅ unlinks VPN nodes | — | | +| `NetworkInterface.getNetworkInterfaces()` (Java) | JNI → `getifaddrs` | ✅ | ✅ | ✅ | — | | +| `ioctl(SIOCGIFNAME)` index→name | native | ✅ `dev_ioctl` | ✅ `dev_ioctl` | ✅ | — | | +| `ioctl(SIOCGIFCONF)` enumerate | native | ✅ `sock_ioctl` | ✅ `sock_ioctl` | ✅ `filter_ifconf` | — | | +| `ioctl(SIOCGIF{FLAGS,MTU,INDEX,HWADDR,ADDR})` by name | native | ✅ `dev_ioctl` | ✅ `dev_ioctl` | ✅ pre-screen | — | | +| netlink `RTM_GETLINK` dump | `recvmsg`/`recvfrom` of `RTM_NEWLINK` | ✅ `rtnl_fill_ifinfo` | ✅ `rtnl_fill_ifinfo` | ✅ filter by index | — | | +| netlink `RTM_GETADDR` dump | `RTM_NEWADDR` | ✅ `inet*_fill_ifaddr` | ✅ `inet*_fill_ifaddr` | ✅ filter by index | — | | +| `/sys/class/net//*` | reads iface type/mtu/operstate | — | — | — | — | 🔒 usually denied | -Notes: bionic's `getifaddrs` itself runs over netlink, so the kmod +Notes: bionic's `getifaddrs` itself runs over netlink, so the kernel-backend `rtnl_fill_ifinfo` / `inet*_fill_ifaddr` hooks cover the Java and native paths -at once. zygisk additionally unlinks entries from the `getifaddrs` result list +at once. Zygisk additionally unlinks entries from the `getifaddrs` result list directly, so it works even if a future bionic stops using netlink. **`SIOCGIFCONF` size-query subcase — closed in the `.ko`.** The classic two-step @@ -151,46 +154,47 @@ items. ### 3B. Route table — "is the default route a tunnel?" -| Vector | How it manifests | kmod | zygisk | lsposed | SELinux | -|---|---|:--:|:--:|:--:|:--:| -| `/proc/net/route` (IPv4) | text, iface in col 1 | ✅ `fib_route_seq_show` | ✅ `filter_route_buf` | — | 🔒 often denied | -| `/proc/net/ipv6_route` | text, iface last field | ✅ `ipv6_route_seq_show` | ✅ | — | 🔒 | -| netlink `RTM_GETROUTE` **dump** | `RTA_OIF` index per route | ✅ `fib_dump_info` / `rt6_fill_node` | ✅ `RTM_NEWROUTE` filter (issue #86) | — | | -| netlink `RTM_GETROUTE` **single** (`ip route get`) | one `rt_fill_info` reply | ⚠️ intentionally unhooked (see ROADMAP) | ⚠️ not filtered | — | | -| netlink `RTM_GETRULE` (policy rules) | per-UID lookup tables | ✅ `fib_nl_fill_rule` | — | — | | -| host-route to the VPN **server** | `/32`·`/128` to a public IP via a *physical* iface | ✅ `is_public_host_route_via_physical` | — | — | | -| `LinkProperties.getRoutes()` (Java) | framework route list | — | — | ✅ filter `mRoutes` | | +| Vector | How it manifests | kmod | KPM | Zygisk | lsposed | SELinux | +|---|---|:--:|:--:|:--:|:--:|:--:| +| `/proc/net/route` (IPv4) | text, iface in col 1 | ✅ `fib_route_seq_show` | ✅ `fib_route_seq_show` | ✅ `filter_route_buf` | — | 🔒 often denied | +| `/proc/net/ipv6_route` | text, iface last field | ✅ `ipv6_route_seq_show` | ✅ `ipv6_route_seq_show` | ✅ | — | 🔒 | +| netlink `RTM_GETROUTE` **dump** | `RTA_OIF` index per route | ✅ `fib_dump_info` / `rt6_fill_node` | ✅ `fib_dump_info` / `rt6_fill_node` | ✅ `RTM_NEWROUTE` filter (issue #86) | — | | +| netlink `RTM_GETROUTE` **single** (`ip route get`) | one `rt_fill_info` reply | ⚠️ intentionally unhooked (see ROADMAP) | ⚠️ intentionally unhooked | ⚠️ not filtered | — | | +| netlink `RTM_GETRULE` (policy rules) | per-UID lookup tables | ✅ `fib_nl_fill_rule` | ✅ `fib_nl_fill_rule` | — | — | | +| host-route to the VPN **server** | `/32`·`/128` to a public IP via a *physical* iface | ✅ `is_public_host_route_via_physical` | ✅ `kpm_is_public_host_route{4,6}` | — | — | | +| `LinkProperties.getRoutes()` (Java) | framework route list | — | — | — | ✅ filter `mRoutes` | | The **`if` leak (issue #86)** lived here: a hidden tun still has an index, and a route dump exposes that index even when the *name* is hidden, so the detector -renders it as the synthetic `if33`. Fixed in zygisk by filtering `RTM_NEWROUTE` +renders it as the synthetic `if33`. Fixed in Zygisk by filtering `RTM_NEWROUTE` on `RTA_OIF` *and* hooking the FORTIFY'd `recvfrom`/`__recvfrom_chk` that fortified native code actually calls (plain `recv` is never emitted for a -fixed-size buffer). kmod already covered it via `fib_dump_info`. +fixed-size buffer). The kernel backends cover it via `fib_dump_info`. **Host-route to the server** is a desktop-VPN pattern: OpenVPN / kernel WireGuard / root VPNs pin a `/32` (or `/128`) route to the server's public IP out the physical interface so tunnel packets don't loop. That route names a -*physical* iface, so name-based hiding misses it — kmod matches it by *shape* +*physical* iface, so name-based hiding misses it — the kernel backends match it by *shape* (host prefix + public dst + physical dev) instead. **On Android `VpnService` apps this route usually does not exist**: they reach the server via `protect(socket)` (an `SO_MARK` fwmark that bypasses VPN routing), so no host -route is installed. The kmod logic is therefore *inert* on the typical Android +route is installed. This logic is therefore *inert* on the typical Android setup and matters for desktop-style / non-`VpnService` VPNs. Kept because it is cheap and correct when such a route is present; see issue discussion. ### 3C. Socket / address leaks — "any socket bound to a tunnel IP?" -| Vector | How it manifests | kmod | zygisk | lsposed | SELinux | -|---|---|:--:|:--:|:--:|:--:| -| `/proc/net/tcp` | local addr hex per socket | — | ✅ `filter_tcp4_buf` (by VPN addr) | — | 🔒 often denied | -| `/proc/net/tcp6` | 32-hex local addr | — | ✅ `filter_tcp6_buf` | — | 🔒 | -| `/proc/net/if_inet6` | IPv6 addrs, iface last field | — | ✅ `filter_if_inet6_buf` | — | 🔒 | +| Vector | How it manifests | kmod | KPM | Zygisk | lsposed | SELinux | +|---|---|:--:|:--:|:--:|:--:|:--:| +| `/proc/net/tcp` | local addr hex per socket | — | — | ✅ `filter_tcp4_buf` (by VPN addr) | — | 🔒 often denied | +| `/proc/net/tcp6` | 32-hex local addr | — | — | ✅ `filter_tcp6_buf` | — | 🔒 | +| `/proc/net/if_inet6` | IPv6 addrs, iface last field | — | — | ✅ `filter_if_inet6_buf` | — | 🔒 | -These three are **zygisk-only** today: kmod hides IPv6 addresses on the netlink -`RTM_GETADDR` path but does **not** hook `if_inet6_seq_show`, `tcp4_seq_show`, -or `tcp6_seq_show`, so the procfs equivalents leak under a raw-syscall reader -that SELinux happens to allow. Candidate kmod work if a real detector uses them. +These three are **Zygisk-only** today: kernel backends hide IPv6 addresses on +the netlink `RTM_GETADDR` path but do **not** hook `if_inet6_seq_show`, +`tcp4_seq_show`, or `tcp6_seq_show`, so the procfs equivalents leak under a +raw-syscall reader that SELinux happens to allow. Candidate kernel-backend work +if a real detector uses them. ### 3D. Framework network APIs (Java) — lsposed territory @@ -233,7 +237,8 @@ detectors actually probe: 1. **Interface enumeration + route dump (3A, 3B).** The first thing every native detector checks. Must be airtight across libc *and* raw-syscall readers → - needs kmod for the bypass-proof guarantee, zygisk for kmod-less installs. + needs a kernel backend (`.ko` or KPM) for the bypass-proof guarantee, Zygisk + for kernel-backend-less installs. 2. **Framework network model (3D).** What pure-Java apps and most anti-fraud SDKs use. lsposed-only; high value because it is the easy path for apps. 3. **Package visibility (3E).** Stops "you have a VPN app installed" heuristics. @@ -247,14 +252,17 @@ detectors actually probe: ## 5. Known gaps & assumptions -- **Raw-syscall native readers defeat zygisk.** Only kmod is bypass-proof. - Document any "covered" claim with the layer; a zygisk-only install is not - raw-syscall-proof. +- **Raw-syscall native readers defeat Zygisk.** Only the kernel backends + (`.ko`/KPM) are bypass-proof. Document any "covered" claim with the layer; a + Zygisk-only install is not raw-syscall-proof. - **zygisk netlink filtering is recv-family-scoped.** It hooks `recvmsg`, `recv`, `recvfrom`, `__recvfrom_chk`. A detector reading a netlink socket via plain `read()`/`readv()` or `recvmmsg` would slip past. Not seen in the wild yet; add hooks if it appears. -- **kmod procfs gap:** no `if_inet6` / `tcp` / `tcp6` seq-file hooks (3C). +- **Kernel-backend procfs gap:** no `if_inet6` / `tcp` / `tcp6` seq-file hooks + in `.ko` or KPM (3C). +- **KPM SIOCGIFCONF size-query gap:** KPM compacts the returned ifreq array but + does not yet reduce the `ifc_req == NULL` size query the `.ko` handles (3A). - **Single-lookup route** (`rt_fill_info`) is intentionally unhooked — no stable static-function ABI; low value under split tunnel. See [ROADMAP.md](ROADMAP.md). @@ -273,9 +281,10 @@ detectors actually probe: | Layer | Entry points | |---|---| | kmod | `kmod/vpnhide_kmod.c` (10 kretprobes); iface matcher `kmod/generated/iface_lists.h` | +| KPM | `kmod/kpm/vpnhide_kpm.c` (KernelPatch inline hooks + ctl0); offsets in `kmod/kpm/kver_offsets.h` | | zygisk | `zygisk/src/hooks.rs` (ioctl/getifaddrs/openat/recv*); `zygisk/src/filter.rs` (procfs + netlink filters) | | lsposed | `lsposed/app/.../HookEntry.kt`, `PackageVisibilityHooks.kt`; iface matcher `.../generated/IfaceLists.kt` | -| iface match rules | single source of truth `data/interfaces.toml` → `scripts/codegen-interfaces.py` renders all four languages | +| iface match rules | single source of truth `data/interfaces.toml` → `scripts/codegen-interfaces.py` renders all four targets (kmod/KPM C, zygisk Rust, lsposed native Rust, lsposed Kotlin) | The interface-name patterns (`tun`/`tap`/`wg`/`ppp`/`ipsec`/`xfrm`/`utun`/`l2tp`/`gre`, substring `vpn`, and the `if` renamed-tunnel form from issue #86) are diff --git a/docs/development.md b/docs/development.md index e101b1bc..f1e58743 100644 --- a/docs/development.md +++ b/docs/development.md @@ -33,7 +33,7 @@ How to build vpnhide from source. |---|---| | `zygisk/` | Zygisk native module (Rust, inline `libc` hooks) | | `lsposed/` | LSPosed module + target-picker Android app (Kotlin, Compose) | -| `kmod/` | Kernel module (C, kretprobes) | +| `kmod/` | Kernel-level native backends: GKI `.ko` (C, kretprobes) and KPM beta (KernelPatch inline hooks) | | `portshide/` | Localhost port blocker (shell + iptables) | | `scripts/` | Release & changelog tooling | | `update-json/` | Magisk/KSU update metadata | @@ -90,18 +90,36 @@ cd lsposed && ./gradlew :app:assembleRelease The script auto-spawns the `ghcr.io/ylarod/ddk-min:-` container via Docker or podman (same image CI uses; Docker is preferred when both are installed). For local kernel-source builds via `direnv` and the GKI matrix details, see [kmod/BUILDING.md](../kmod/BUILDING.md). +### KPM backend + +```sh +./kmod/kpm/build.py +# → vpnhide-kpm.zip at the repo root +``` + +The script builds the Android `kpm` activator, builds `vpnhide.kpm` against the +pinned KernelPatch submodule, stages `kmod/kpm/module/`, and packages the +flashable zip. For runtime requirements and safety notes, see +[kmod/kpm/README.md](../kmod/kpm/README.md). + ## Install on device ```sh # APK adb install -r lsposed/app/build/outputs/apk/release/app-release.apk -# zygisk / kmod: push to device, install via the Magisk or KernelSU manager app +# Native backend: push exactly one of these, then install via the Magisk, +# KernelSU, KernelSU-Next, or APatch manager path appropriate for the backend. adb push zygisk/target/vpnhide-zygisk.zip /sdcard/Download/ -adb push vpnhide-kmod.zip /sdcard/Download/ +adb push vpnhide-kmod-.zip /sdcard/Download/ +# The KPM zip additionally needs a KernelPatch runtime (built into APatch; +# KernelSU-Next/Magisk add it via KPatch-Next) — see kmod/kpm/README.md. +adb push vpnhide-kpm.zip /sdcard/Download/ ``` -After flashing kmod or zygisk, reboot the device. +After flashing a native backend, reboot the device. Do not keep multiple native +backends installed unless you are explicitly testing conflict handling; the app +uses priority `kmod > KPM > Zygisk`, and `.ko` + KPM together is unsafe. ## CI lints (run before pushing) diff --git a/docs/protocol.md b/docs/protocol.md index 3107550b..a2bf237d 100644 --- a/docs/protocol.md +++ b/docs/protocol.md @@ -7,8 +7,8 @@ decides who is wrong. Every implementation (C / Rust / Kotlin) and every test vector references it. Status: `version 1` **frozen** — all seven OPEN items (§10) are resolved. -Being implemented on `feat/control-protocol`. Key shape: one always-on Java -(LSPosed) layer + exactly one active native backend, priority `kmod > KPM > +Key shape: one always-on Java (LSPosed) layer + exactly one active native +backend, priority `kmod > KPM > Zygisk` (§1.5); three `kind`s — `config` (in), `stats` + `status` (out, §4.3); UID is the key on every channel incl. Zygisk; `u64` cumulative counters; `debug` folded into config; the `.ko` channel is one folded node `/proc/vpnhide_ctl` @@ -98,7 +98,7 @@ process: | Language | Process | Role in protocol | |---|---|---| -| C | kernel (`.ko` + KPM) | parse config; emit stats. Freestanding, `shared/vpnhide_logic.h`. | +| C | kernel (`.ko` + KPM) | parse config; emit stats. Freestanding, `kmod/shared/vpnhide_logic.h`. | | Rust | Zygisk process | parse its config profile (`targets.txt`). | | Kotlin | app | **serialise** all config snapshots into every channel; **parse** stats readback. The "thick" end. | | Kotlin | `system_server` (LSPosed hook) | parse its config profile from its file. Not removable — cannot inject a `.so` into `system_server`. | @@ -416,7 +416,7 @@ the only writer of all profiles; each backend reads its own. |---|---|---|---| | `.ko` / KPM | `debug`, `target` (kernel-owned mask bits) | yes | yes (§4.3) | | Zygisk | `debug`, `target` (zygisk-owned mask bits) | no, not yet (§7) | yes, via the app heartbeat | -| LSPosed | `debug`, `target` (lsposed-owned mask bits), package/observer records | yes | yes | +| LSPosed | `debug`, `target` (lsposed-owned mask bits, incl. package visibility) | yes | yes | A backend ignores `target` mask bits it does not own (`mask & own_hooks`), so the same `target 0x27fa 0x3ff` line is valid on every channel and each backend takes @@ -442,7 +442,7 @@ static long vpnhide_kpm_ctl0(const char *args, char *__user out_msg, int outlen) ``` The config parser is *already* shared with `.ko` (`apply_targets` → -`vpnhide_parse_target_uids` from `shared/`). KPM reuses the §4 format unchanged; +`vpnhide_parse_config` from `kmod/shared/`). KPM reuses the §4 format unchanged; only the transport differs and `out_msg`/`outlen` (currently `(void)`) become the read-back channel for `stats` and `status`. @@ -496,7 +496,7 @@ paginates. Therefore: On write, the whole snapshot must fit in `args`; bound it by `MAX_TARGET_UIDS` and have the parser truncate honestly at that ceiling (already the case for -`vpnhide_parse_target_uids(..., MAX_TARGET_UIDS)`). +`vpnhide_parse_config(..., MAX_TARGET_UIDS)`). ### 7.3 Delivery @@ -536,9 +536,10 @@ dispatch on those low bits, but the calling conventions differ: directly. The activator probes `SUPERCALL_HELLO` before load/control, so a missing KernelPatch runtime or stale SuperKey fails before a stray original syscall can run. -- **KPatch-Next:** syscall number `45`, arg0 is `NULL`, command word uses the - `0x2026` marker, and the kernel side gates calls by root UID rather than a - SuperKey. Use the runtime's own `kpatch kpm ...` CLI. With the standalone +- **KPatch-Next:** syscall number `45`, arg0 is `NULL`, and the kernel side gates + calls by root UID rather than a SuperKey. The activator issues no raw + command-word marker here — it drives KPatch-Next through the runtime's own + `kpatch kpm ...` CLI. With the standalone KPatch-Next-Module that binary lives under `/data/adb/modules/KPatch-Next/bin/kpatch`. diff --git a/docs/storage.md b/docs/storage.md index 1b975447..20e323a6 100644 --- a/docs/storage.md +++ b/docs/storage.md @@ -5,26 +5,26 @@ each backend. This is the layer *above* the wire protocol: [protocol.md](protoco defines the bytes exchanged with the kernel/native backends at runtime; this file defines the single on-disk source of truth those bytes are derived from. -> **Status.** This is the **target** design. The current code (the control-protocol -> migration, PR #164) is an interim step: it puts the text protocol on *every* -> backend with per-backend text files. This document is where that converges — -> one JSON canonical + activators that derive runtime state for the native and -> ports backends, with LSPosed reading the JSON directly. Sections below note -> where the current code differs. +> **Status.** This is the current storage/activation design: one canonical JSON +> desired-state file, Rust activators that derive runtime state for native and +> ports backends, and LSPosed reading the JSON directly from `system_server`. +> Older per-backend text files are migration inputs or derived runtime state, not +> user-managed config. --- ## 1. Two layers, one boundary -Two formats, by role — not one format stretched over everything: +Two formats, by function — not one format stretched over everything: - **On disk = JSON.** A single canonical "desired state" file. Package-keyed (so it survives reinstalls), rich (roles, per-hook selection, debug, app settings), trivially import/export-able. Read by the app (Kotlin), the LSPosed hook (Kotlin), and the Rust activators. - **Runtime IPC = the text protocol** ([protocol.md](protocol.md), v1 frozen). - uid-keyed, hand-parsed, kernel-safe. The runtime wire for the **native** - backends only (kmod / KPM / Zygisk): config in, stats/status out. + uid-keyed, hand-parsed, kernel-safe. The runtime *config* wire for the + **native** backends (kmod / KPM / Zygisk): config in, stats/status out. Its + text format is also reused for LSPosed's read-only state file (§3). - **The bridge = the activator** (Rust). It projects the JSON onto the wire for whichever native backend is installed. @@ -156,7 +156,10 @@ How it reads, carefully (this runs in the bootloop-critical `system_server`): them via **`/data/system/packages.list`** (the stable AOSP `pkg uid …` map) — **not** via `PackageManager`, because the hook *is* hooking PackageManager and a PM call could re-enter its own hooks (recursion → deadlock → bootloop). Reading - `packages.list` is a plain file read, hook-free. + `packages.list` is a plain file read, hook-free. The same self-read + `packages.list` + resolution also drives the **Apps** role (package visibility) — it just keys on the + observer UIDs and hidden packages instead of the target set, so both LSPosed jobs + share one hook-free resolver. - **Multi-profile for free:** match `callingUid % 100000 ∈ targetAppIds`. Any profile's instance of a target app matches, with no per-profile enumeration. - **State out:** the hook writes `/data/system/vpnhide_lsposed_state`, a single @@ -166,13 +169,12 @@ How it reads, carefully (this runs in the bootloop-critical `system_server`): `system_server` can't expose a `/proc`-style node, so it can't multiplex write config + read state on one channel the way the kmod node does. -> Current code (#164) differs: LSPosed reads a *derived protocol config* -> (`vpnhide_uids.txt`) that the native boot scripts populate. That is exactly the -> native-dependency this target design removes. +Older releases used a derived `vpnhide_uids.txt` file for this path. Current code +does not: the hook self-reads the canonical JSON and resolves UIDs in-process. --- -## 4. Native layer (kmod / KPM / Zygisk) + the activator +## 4. The activator and its backends (native + ports) The native backends are **mutually exclusive** — exactly one is installed (the app warns/errors and asks the user to remove the extra otherwise). So the activator @@ -191,11 +193,8 @@ wire. So: one shared core, thin per-target front-ends. ``` crates/ - protocol/ # lib, NO serde — the wire (parse/format). Shared by - # the Zygisk .so AND the activator. Today's - # zygisk/src/protocol.rs, promoted to a shared crate. - zygisk/ # cdylib — the injected .so. deps: protocol (+ shadowhook). - # Lean: no serde, no JSON in every app process. + protocol/ # lib, NO serde — the wire (parse/format), shared by + # the Zygisk .so and the activator. activator/ src/lib.rs # shared core: JSON schema (serde), pkg→uid resolution # (`pm`), project_native(json) -> String @@ -203,6 +202,8 @@ crates/ src/bin/kpm.rs # project_native(json) → APatch supercall / KPatch-Next `kpatch` src/bin/zygisk.rs # project_native(json) → write_atomic(module_dir file) src/bin/ports.rs # project_ports(json) → iptables-restore/ip6tables-restore +zygisk/ # cdylib — the injected .so. deps: protocol (+ shadowhook). + # Lean: no serde, no JSON in every app process. ``` - **Why a workspace, not feature-flags or one crate with `src/bin/`+cdylib:** @@ -377,7 +378,7 @@ exactly the threat model. --- -## 8. File inventory (target) +## 8. File inventory What you actually manage shrinks to **one** file; everything else is generated or optional. @@ -391,12 +392,15 @@ optional. | `/data/system/vpnhide_lsposed_state` | LSPosed status + stats (hook → dashboard) | per-boot | | `/data/adb/vpnhide/superkey` | APatch superkey (optional, flag-gated) | persistent, root-only | -Removed vs. the pre-redesign layout: +No longer user-managed config vs. the pre-redesign layout — kept only as migration +inputs (read when the canonical JSON is absent) and best-effort cleanup until +removed (see §9): - the four per-backend `targets.txt` lists → folded into the **one** canonical; -- `vpnhide_debug_logging` → **gone** (debug is a field in the canonical); +- `vpnhide_debug_logging` → **no longer user-managed** (debug is a field in the canonical); - `vpnhide_uids.txt` / `vpnhide_hidden_pkgs.txt` / `vpnhide_observer_uids.txt` → - **gone** (LSPosed reads the canonical + `packages.list` and derives them in-process). + **no longer user-managed** (LSPosed reads the canonical + `packages.list` and + derives them in-process). --- diff --git a/kmod/README.md b/kmod/README.md index e515712b..e52f2121 100644 --- a/kmod/README.md +++ b/kmod/README.md @@ -16,8 +16,7 @@ Zero footprint in the target app's process -- no modified function prologues, no | `fib_route_seq_show` | Forward-scans for VPN lines and compacts them out with `memmove` | `/proc/net/route` reads | | `ipv6_route_seq_show` | Forward-scans for VPN lines and compacts them out with `memmove` | `/proc/net/ipv6_route` reads | | `fib_dump_info` | Trims IPv4 VPN route entries and public physical-interface host-route hints from netlink route dumps via `skb_trim` | RTM_GETROUTE route table dumps | -| `rt_fill_info` | Trims IPv4 VPN route entries from single-route netlink lookups via `skb_trim` | RTM_GETROUTE lookups for specific destinations | -| `rt6_fill_node` | Trims IPv6 VPN route entries from netlink route replies via `skb_trim` | IPv6 RTM_GETROUTE dumps/lookups | +| `rt6_fill_node` | Trims IPv6 VPN route entries from netlink route dump replies via `skb_trim` | IPv6 RTM_GETROUTE dumps | | `fib_nl_fill_rule` | Trims target-UID policy rules and VPN interface rules from netlink rule dumps via `skb_trim` | RTM_GETRULE policy routing dumps | All filtering is **per-UID**: only processes whose UID is a `target` in the config written to `/proc/vpnhide_ctl` see the filtered view. Everyone else (system services, VPN client, NFC subsystem) sees the real data. @@ -34,6 +33,10 @@ All symbols used (`register_kretprobe`, `proc_create`, `seq_read`, etc.) are par CI builds are provided for all 7 GKI generations: `android12-5.10` through `android16-6.12`. +For old/non-GKI kernels or devices where the `.ko` cannot load, see the +[KPM backend](kpm/README.md). It covers the same kernel-level Native role through +KernelPatch inline hooks and is packaged as `vpnhide-kpm.zip` (beta). + ## Build See [BUILDING.md](BUILDING.md) for the full guide (DDK Docker build, kernel source preparation, toolchain setup, `Module.symvers` generation). @@ -44,7 +47,7 @@ See [BUILDING.md](BUILDING.md) for the full guide (DDK Docker build, kernel sour ## Install -1. `adb push vpnhide-kmod.zip /sdcard/Download/` +1. `adb push vpnhide-kmod-.zip /sdcard/Download/` (download the zip matching your device's GKI generation, e.g. `vpnhide-kmod-android14-6.1.zip`) 2. KernelSU-Next manager -> Modules -> Install from storage 3. Reboot @@ -79,6 +82,10 @@ For apps with aggressive anti-tamper SDKs, full VPN hiding requires covering bot Together they provide complete VPN hiding without any hooks in the target app's process. +KPM is the other kernel-level Native backend for this same role. Do not run KPM +and the `.ko` at the same time; choose one kernel backend, then pair it with +LSPosed for Java APIs. + ### Setup 1. Install **vpnhide-kmod** as a KSU module (this module). diff --git a/lsposed/AGENTS.md b/lsposed/AGENTS.md index a02bdde0..b78c1d54 100644 --- a/lsposed/AGENTS.md +++ b/lsposed/AGENTS.md @@ -10,11 +10,13 @@ reinvent them.** `grep` for an existing helper before writing a new one. - **Read path:** one batched root shell → `RootSnapshotCache` → typed snapshots (`DashboardState`, `TargetsSnapshot`) derived in pure functions → Compose. - Dashboard and Protection derive from the *same* snapshot so their counts - can't drift. -- **Write path (Save):** typed entries → `ShellCommandBuilders` → base64 → - root-owned text files in `/data/adb` + `/data/system` (the kmod/service.sh - contract). See `docs/state.md` for every path's owner/reader/lifetime. + Dashboard and Hiding derive from the *same* snapshot so their counts can't + drift. +- **Write path (Save):** typed entries → canonical JSON + (`/data/system/vpnhide_config.json`) → `ConfigChannels` / module activators + derive runtime state for the installed native and ports backends. LSPosed + reads the canonical JSON directly from `system_server`. See `docs/state.md` + for every path's owner/reader/lifetime. ## Load-bearing abstractions — reuse these @@ -22,27 +24,26 @@ reinvent them.** `grep` for an existing helper before writing a new one. (loading/error/value flows + single-flight job). A new cache **extends this**; never hand-roll `inflight`/`loading` again. - **`RootSnapshotCache`** — the single batched root read. Need new system state - on the Dashboard/Protection path? Add a section to its shell snapshot; don't + on the Dashboard/Hiding path? Add a section to its shell snapshot; don't add an ad-hoc `suExec` that races the snapshot. - **`ShellUtils`** — `suExec`/`suExecAsync`, and the parsers `parseConfigLines`, `parseKeyValueLines`, `parsePackageUidMap`. **Never write another `pm list` or `key=value` parser** — there used to be four; there is now one of each. -- **`ConfigChannels`** — the single serialiser of the `vpnhide 1 config` wire - (docs/protocol.md) and the one place that fans it to every runtime channel - (kmod `/proc/vpnhide_ctl`, the zygisk module dir, the system_server UID file). - Save, the debug toggle, and the startup reconcile all go through it. **Don't - hand-build a config snapshot anywhere else** — use `Protocol.formatConfig` via - this object. -- **`ShellCommandBuilders`** — `buildRawWriteCommand` (the base64 `echo | base64 - -d > path` primitive every file write uses), `buildConfigWriteCommand` / - `managedConfigBody` (the persistent **package-list** files, NOT the wire), - `systemDataFilePermsParts`, and `buildUidResolverCommand` (now only the - observer-UID file — the app-hiding "A" role; it is **not** the target-config - path anymore, that's `ConfigChannels`). The persistent package lists and the - resolved-UID runtime channels are deliberately separate (protocol §1.2). +- **`ConfigChannels`** — the one place that invokes the single active native + activator — folding `kmod > KPM > Zygisk` down to the first installed, + non-disabled backend — after the canonical JSON changes; the ports activator + is invoked alongside it. Save, the debug toggle, and startup reconcile go + through it. **Don't** hand-build per-backend runtime config in Kotlin; the + activators derive each backend's wire from the canonical JSON. +- **`StorageConfig` / `ShellCommandBuilders`** — canonical JSON schema, + migration helpers, and root-safe file writes (`buildCanonicalConfigWriteCommand` + in `StorageConfig` wraps the generic `buildAtomicSystemDataRawWriteCommand` in + `ShellCommandBuilders`). The canonical JSON is the persistent + target config; legacy `targets.txt` / UID files are migration inputs or + derived runtime state, not app-owned user config. - **`TargetPickerScaffold`** — `TargetPickerScreen`, `TargetRowShell`, - `TargetChip`, `AppListScrollbar`. All three picker screens (tun / hiding / - ports) are thin configs over this; a new picker is too. + `TargetChip`, `AppListScrollbar`. The unified Hiding list is built on these + primitives; a new picker/list view should be too. - **`StatusUi`** — `StatusColors` (pinned status palette — **never** use `MaterialTheme.colorScheme.errorContainer` etc. for status; Material You remixes them off-meaning), `StatusBanner`, `FileSaveShareRow`, diff --git a/lsposed/README.md b/lsposed/README.md index 2c082272..e6dc5a47 100644 --- a/lsposed/README.md +++ b/lsposed/README.md @@ -2,7 +2,10 @@ Hooks `writeToParcel()` in `system_server` to strip VPN data before Binder serialization reaches target apps. Part of [vpnhide](../README.md). -The APK also serves as the **target management UI** for the entire vpnhide project — it writes targets for both [kmod](../kmod/) and [zygisk](../zygisk/) modules. +The APK also serves as the **target management UI** for the entire vpnhide project. +It owns the canonical config at `/data/system/vpnhide_config.json`, then runs the +installed activators for the native and ports modules. LSPosed itself reads the +same JSON directly from `system_server`. Zero presence in the target app's process -- only "System Framework" is needed in the LSPosed scope. @@ -24,11 +27,13 @@ Filtering is controlled by `Binder.getCallingUid()` -- only apps whose UID appea ### Target management -Target UIDs are loaded from `/data/system/vpnhide_uids.txt`. A `FileObserver` (inotify) watches for changes and reloads the list immediately -- no reboot needed. +Targets are loaded from `/data/system/vpnhide_config.json`. A `FileObserver` +(inotify) watches that file and reloads immediately -- no reboot needed for +Java-layer target changes. -This file is written by: -- The **VPN Hide app** (this APK's target picker UI) -- The module's `service.sh` on boot +The hook resolves package names to appIds through `/data/system/packages.list`, +not PackageManager, because it runs inside PackageManager hooks and must avoid +re-entering them. ## Target picker app @@ -37,12 +42,14 @@ The APK includes a Compose UI for managing target apps across all vpnhide module - Lists all installed apps with icons, names, and package names - Text search filter - System apps toggle (selected system apps always visible) -- Save writes to all target locations via `su`: - - `/data/adb/vpnhide_kmod/targets.txt` (if kmod is installed) - - `/data/adb/vpnhide_zygisk/targets.txt` (if zygisk is installed) - - `/data/adb/modules/vpnhide_zygisk/targets.txt` (Magisk module dir copy) - - `/proc/vpnhide_ctl` (kmod live update, no reboot needed) - - `/data/system/vpnhide_uids.txt` (system_server hooks, live reload via inotify) +- One row per app with the current roles: + - **Java** — LSPosed Java/API hiding + - **Native** — the active native backend (kmod, KPM, or Zygisk) + - **Apps** — PackageManager observer mode + - **Ports** — localhost-port blocking observer +- Save writes the canonical JSON via `su`, runs the active native module's + activator (installed under `vpnhide_kmod`, `vpnhide_kpm`, or `vpnhide_zygisk`), + and runs the ports activator if present. The LSPosed hook reloads the JSON itself. Works on KernelSU, Magisk, and any other root solution. @@ -53,16 +60,18 @@ Works on KernelSU, Magisk, and any other root solution. 3. Open LSPosed/Vector manager, go to Modules, enable **VPN Hide**. 4. Add **"System Framework"** to the module's scope. No other apps should be in scope. 5. Reboot. -6. Open the VPN Hide app to manage target apps. +6. Open the VPN Hide app and use the **Hiding** tab to manage roles for target apps. -## Combined use with kmod +## Combined use with native backends For apps with aggressive anti-tamper SDKs, full VPN hiding requires covering both native and Java API paths without any hooks in the target app's process: -- **[kmod](../kmod/)** covers native: `ioctl`, `getifaddrs` (netlink), `/proc/net/route`. +- **[kmod](../kmod/)** and **[KPM](../kmod/kpm/)** cover native paths at the kernel level: `ioctl`, `getifaddrs`/netlink, routes, and `/proc/net/route`. - **This module** covers Java APIs: `NetworkCapabilities`, `NetworkInfo`, `LinkProperties` via `writeToParcel()` in `system_server`. Together they provide complete VPN hiding with zero footprint in the target process. +Zygisk can be used as the native fallback, but its libc hooks run inside the +target process and can be detected by aggressive apps. ## Debugging diff --git a/portshide/README.md b/portshide/README.md index e85847fd..8708d109 100644 --- a/portshide/README.md +++ b/portshide/README.md @@ -1,6 +1,6 @@ # portshide -Magisk / KernelSU-Next module that blocks selected apps from reaching +Magisk / KernelSU / KernelSU-Next / APatch module that blocks selected apps from reaching `localhost` ports. Used to hide locally-bound VPN / proxy daemons (Clash, Sing-box, V2Ray, Amnezia, etc.) from apps that probe for them via `connect(127.0.0.1, PORT)` / `connect(::1, PORT)`. @@ -32,14 +32,16 @@ beside them and is idempotently re-applied on every config change. ## Install -Pick `vpnhide-ports.zip` in the KernelSU-Next or Magisk manager and -install. Reboot not strictly required — rules apply on next boot via +Install `vpnhide-ports.zip` via your root manager's Modules screen +(Magisk / KernelSU / KernelSU-Next / APatch). Reboot not strictly +required — rules apply on next boot via `service.sh`, or immediately if you manage observers through the VPN Hide app (it invokes the ports activator via `su`). ## Configuration -Managed by the VPN Hide app (Protection -> P, tune icon). Direct shell +Managed by the VPN Hide app (**Hiding** tab -> **Ports** / **P**, settings icon +next to the role label). Direct shell alternative: ``` diff --git a/portshide/module/customize.sh b/portshide/module/customize.sh index 49fe4bde..8d6d0afc 100644 --- a/portshide/module/customize.sh +++ b/portshide/module/customize.sh @@ -9,4 +9,4 @@ set_perm "$MODPATH/service.sh" 0 0 0755 set_perm "$MODPATH/uninstall.sh" 0 0 0755 ui_print "- Config: /data/system/vpnhide_config.json (managed by the app)" -ui_print "- Pick apps via the VPN Hide app → Protection → Ports." +ui_print "- Pick apps via the VPN Hide app → Hiding → Ports." diff --git a/scripts/update-json.sh b/scripts/update-json.sh index 783436c6..3e3769ac 100755 --- a/scripts/update-json.sh +++ b/scripts/update-json.sh @@ -45,6 +45,16 @@ cat > "update-json/update-zygisk.json" < "update-json/update-kpm.json" < "update-json/update-ports.json" <` renamed/anonymous netdevs. Matches the list in the [LSPosed companion](../lsposed/). ## Known limitations -- **Direct `svc #0` syscalls bypass the hook.** Apps issuing raw syscalls skip libc entirely. Use [vpnhide-kmod](../kmod/) for these apps. +- **Direct `svc #0` syscalls bypass the hook.** Apps issuing raw syscalls skip libc entirely. Use a kernel-level backend, [vpnhide-kmod](../kmod/) or [KPM](../kmod/kpm/), 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.