docs: refresh installation/backend docs + 1.0.0 screenshots (#224)

* docs: refresh installation and backend docs

* docs: fix review findings and add KPM to update-json

Cross-checked the docs refresh against the code and corrected:
- protocol.md: config parser is vpnhide_parse_config (not the legacy
  vpnhide_parse_target_uids); path is kmod/shared/vpnhide_logic.h; drop the
  unverified 0x2026 KPatch-Next marker (activator drives it via the CLI).
- zygisk/README: module declares Zygisk API v2 (not v5); add the recv hook
  row and the if<N> renamed-netdev match.
- README (en/ru): live-check counts are 13 native / 12 Java; Diagnostics is
  under Settings, not a tab; mention the Statistics tab; note KPM beta parity.
- detection-vectors: KPM host-route helper is kpm_is_public_host_route{4,6};
  restore CONFIG_KPROBES and the Zygisk qualifiers; codegen renders four targets.
- storage.md: legacy files are no longer user-managed (migration inputs), not
  'gone'. CLAUDE.md: activator has four bins (kmod/kpm/zygisk/ports).
- development/ROADMAP/kmod/lsposed/portshide READMEs + AGENTS.md: assorted
  accuracy fixes (KPM needs a KernelPatch runtime, per-KMI kmod zip name, etc.).
- CONTRIBUTING.md: clang-format uses scripts/clang-format-c.sh (covers KPM src).
- portshide customize.sh: 'Hiding -> Ports' (renamed tab).
- update-json.sh: emit update-kpm.json so KPM gets Magisk/KSU update checks.

* docs: correct diagnostics check count (25, not 26 vectors)

* docs: refresh README screenshots for the 1.0.0 UI

Replace the v0.5.3-era screenshots (old per-tab Protection UI) with a
new 1.0.0 set: VPN-hidden dashboard, the unified Hiding list with
J/N/A/P, the Statistics tab + per-hook breakdown, per-app hook selection,
diagnostics, settings, and the community screen. Extra shots
(statistics-apps, settings-advanced, diagnostics-java, settings-developer)
are included for the release notes.

* docs: fix the how-it-works layer breakdown

The old 'Layer 3 — additional app-level controls' bucket was wrong:
interface hiding is what Layers 1+2 already do, app-hiding is done by the
LSPosed/Java layer (system_server PackageManager hooks), and ports is a
separate module. Fold app-hiding into Layer 1 (LSPosed) and make Layer 3
the standalone portshide module.

* docs: conceptual-coherence pass on the 5 core docs

Second review pass, this time for architectural coherence rather than
code-facts. Fixes:
- README (en/ru): 'What vpnhide hides' stopped reifying interface hiding
  as a standalone 'layer'/'role' and now ties the three hiding goals to
  the four J/N/A/P roles (interface = Java + Native, toggled independently)
- README (ru): add the KPM-beta hook-parity caveat (parity with the EN side)
- protocol.md: app-hiding is a normal lsposed mask bit, not a separate
  'package/observer records' entry in the §6 profile table
- detection-vectors.md: a complete install is two components (native + lsposed),
  not 'two roles'; name both lsposed jobs
- storage.md: scope the text protocol to the config wire (LSPosed reuses the
  format for its state file); 'by function' not 'by role'; note the Apps role
  shares the same resolver; heading no longer files ports under 'Native layer'
This commit is contained in:
Danila Gornushko 2026-07-02 00:03:17 +03:00 • committed by GitHub
parent 60bccf53a6
commit 1582efa067
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
42 changed files with 425 additions and 310 deletions

View file

@ -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 <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.

View file

@ -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`

View file

@ -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.
<details>
<summary><b>Shell configuration (advanced)</b></summary>
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.
</details>
@ -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 |
|:-:|:-:|:-:|
| <img src="assets/screenshots/dashboard-all-ok.png" width="250"> | <img src="assets/screenshots/dashboard-issues.jpg" width="250"> | <img src="assets/screenshots/dashboard-install-recommendation.jpg" width="250"> |
| <img src="assets/screenshots/dashboard-hidden.png" width="250"> | <img src="assets/screenshots/hiding-list.png" width="250"> | <img src="assets/screenshots/hiding-help.png" width="250"> |
| Protection — Tun | App hiding | Ports hiding |
| Statistics | Per-hook breakdown | Per-app hook selection |
|:-:|:-:|:-:|
| <img src="assets/screenshots/protection-tunnels-list.png" width="250"> | <img src="assets/screenshots/app-hiding-list.png" width="250"> | <img src="assets/screenshots/ports-hiding-list.png" width="250"> |
| <img src="assets/screenshots/statistics.png" width="250"> | <img src="assets/screenshots/statistics-breakdown.png" width="250"> | <img src="assets/screenshots/hook-picker.png" width="250"> |
| App hiding help | Ports hiding help | Diagnostics |
| Diagnostics | Settings | Community |
|:-:|:-:|:-:|
| <img src="assets/screenshots/app-hiding-help.png" width="250"> | <img src="assets/screenshots/ports-hiding-help.png" width="250"> | <img src="assets/screenshots/diagnostics-native.jpg" width="250"> |
| <img src="assets/screenshots/diagnostics-native.png" width="250"> | <img src="assets/screenshots/settings.png" width="250"> | <img src="assets/screenshots/community.png" width="250"> |
## 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

177
README.md
View file

@ -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.
<details>
<summary><b>Настройка через командную строку (для продвинутых)</b></summary>
Редактируйте `/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_*` больше не являются пользовательской настройкой и используются только для миграции старых конфигов.
</details>
@ -121,82 +138,87 @@ vpnhide — это не один переключатель, а три разн
## Скриншоты
| Обзор — всё ОК | Обзор — проблемы | Рекомендация установки |
| Обзор — VPN скрыт | Скрытие — единый список | Как это работает |
|:-:|:-:|:-:|
| <img src="assets/screenshots/dashboard-all-ok.png" width="250"> | <img src="assets/screenshots/dashboard-issues.jpg" width="250"> | <img src="assets/screenshots/dashboard-install-recommendation.jpg" width="250"> |
| <img src="assets/screenshots/dashboard-hidden.png" width="250"> | <img src="assets/screenshots/hiding-list.png" width="250"> | <img src="assets/screenshots/hiding-help.png" width="250"> |
| Защита — Tun | Скрытие приложений | Скрытие портов |
| Статистика | Разбор по хукам | Выбор хуков для приложения |
|:-:|:-:|:-:|
| <img src="assets/screenshots/protection-tunnels-list.png" width="250"> | <img src="assets/screenshots/app-hiding-list.png" width="250"> | <img src="assets/screenshots/ports-hiding-list.png" width="250"> |
| <img src="assets/screenshots/statistics.png" width="250"> | <img src="assets/screenshots/statistics-breakdown.png" width="250"> | <img src="assets/screenshots/hook-picker.png" width="250"> |
| Скрытие приложений — помощь | Скрытие портов — помощь | Диагностика |
| Диагностика | Настройки | Сообщество |
|:-:|:-:|:-:|
| <img src="assets/screenshots/app-hiding-help.png" width="250"> | <img src="assets/screenshots/ports-hiding-help.png" width="250"> | <img src="assets/screenshots/diagnostics-native.jpg" width="250"> |
| <img src="assets/screenshots/diagnostics-native.png" width="250"> | <img src="assets/screenshots/settings.png" width="250"> | <img src="assets/screenshots/community.png" width="250"> |
## Проверка
В приложении есть встроенная система диагностики, которая автоматически обнаруживает большинство проблем с настройкой.
**Обзор** (запускается при каждом открытии приложения):
- Статус модулей для всех трёх уровней (установлен, активен, версия, количество целей)
- Статус модулей и бэкендов (установлен, активен, версия, количество целей)
- Валидация конфигурации 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)
- Серверная детекция неисправима на стороне клиента — используйте раздельное туннелирование
## Лицензия

Binary file not shown.

Before

Width:  |  Height:  |  Size: 220 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 87 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 223 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 233 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 213 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 208 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

View file

@ -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

View file

@ -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/<iface>/*` | 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/<iface>/*` | 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<N>` 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<digits>` renamed-tunnel form from issue #86) are

View file

@ -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:<kmi>-<TAG>` 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-<kmi>.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)

View file

@ -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`.

View file

@ -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).
---

View file

@ -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-<kmi>.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).

View file

@ -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<T>`, `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`,

View file

@ -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

View file

@ -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:
```

View file

@ -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."

View file

@ -45,6 +45,16 @@ cat > "update-json/update-zygisk.json" <<EOJSON
EOJSON
echo " update-json/update-zygisk.json"
cat > "update-json/update-kpm.json" <<EOJSON
{
"version": "v${VERSION}",
"versionCode": ${VERSION_CODE},
"zipUrl": "${REPO}/releases/download/v${VERSION}/vpnhide-kpm.zip",
"changelog": "${RAW}/update-json/changelog.md"
}
EOJSON
echo " update-json/update-kpm.json"
cat > "update-json/update-ports.json" <<EOJSON
{
"version": "v${VERSION}",

View file

@ -14,6 +14,7 @@ All hooks are inline on `libc.so` via ByteDance shadowhook:
| `getifaddrs` | `NetworkInterface.getNetworkInterfaces()`, Dart VM, direct C/C++ | Unlinks VPN entries from the returned linked list. |
| `openat` | `/proc/net/{route,ipv6_route,if_inet6,tcp,tcp6}` | Returns a memfd with VPN entries stripped out. |
| `recvmsg` | Netlink `RTM_NEWADDR` / `RTM_NEWLINK` dump responses | Removes VPN interface entries from netlink messages. |
| `recv` | Netlink `RTM_NEWADDR` / `RTM_NEWLINK` dump responses | Same as `recvmsg`; hooked separately because bionic's `recv()` tail-calls `recvfrom`. |
## Architecture
@ -43,7 +44,7 @@ We carry a small fork at [okhsunrog/android-inline-hook](https://github.com/okhs
## Compatibility
The module declares Zygisk API v5 but only calls v1-era functions (`pre_app_specialize`, `post_app_specialize`, `args.nice_name`, `set_option(DlCloseModuleLibrary)`). The inline hooks happen via shadowhook inside the process, not through the Zygisk API.
The module declares Zygisk API v2 but only calls v1-era functions (`pre_app_specialize`, `post_app_specialize`, `args.nice_name`, `set_option(DlCloseModuleLibrary)`). The inline hooks happen via shadowhook inside the process, not through the Zygisk API.
| Setup | Works |
|-------|-------|
@ -113,11 +114,11 @@ cargo ndk -t arm64-v8a build --release \
## Filter logic
VPN interface prefixes: `tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, `l2tp`, `gre`, plus anything containing the substring `vpn`. Matches the list in the [LSPosed companion](../lsposed/).
VPN interface prefixes: `tun`, `ppp`, `tap`, `wg`, `ipsec`, `xfrm`, `utun`, `l2tp`, `gre`, plus anything containing the substring `vpn`, plus `if<N>` 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.