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'
11
CLAUDE.md
|
|
@ -8,9 +8,9 @@ tooling that follows the AGENTS-convention picks it up automatically.
|
|||
|
||||
## Project layout
|
||||
|
||||
Monorepo for hiding VPN interfaces from selected Android apps. Three runtime components plus tooling:
|
||||
Monorepo for hiding VPN interfaces from selected Android apps. Runtime components plus tooling:
|
||||
|
||||
- `kmod/` — kernel module, kretprobes-based, per-GKI-generation builds
|
||||
- `kmod/` — kernel-level native backends: `.ko` kretprobe module per GKI generation, plus KPM beta for KernelPatch
|
||||
- `zygisk/` — Rust Zygisk module, inline `libc` hooks via shadowhook
|
||||
- `lsposed/` — LSPosed module + Compose target-picker app
|
||||
- `portshide/` — localhost port blocker (shell + iptables)
|
||||
|
|
@ -22,9 +22,9 @@ These short files cover everything specific to this repo. Skipping them leads to
|
|||
- [CONTRIBUTING.md](CONTRIBUTING.md) — PR process, commit conventions, changelog requirement
|
||||
- [docs/development.md](docs/development.md) — prereqs, per-module build quickstart, keystore setup, device install, CI lints
|
||||
- [docs/state.md](docs/state.md) — every persistent path / proc entry / iptables chain the project touches; who writes, who reads, lifetime
|
||||
- [docs/storage.md](docs/storage.md) — **target** storage & activation design: the single JSON canonical, the activator (Rust workspace, three bins) that derives the runtime wire, LSPosed self-read, the APatch superkey, SELinux layout. Read before touching how config is stored/loaded.
|
||||
- [docs/storage.md](docs/storage.md) — current storage & activation design: the single JSON canonical, the activator (Rust workspace, four thin bins: kmod/kpm/zygisk/ports) that derives runtime state, LSPosed self-read, the APatch superkey, SELinux layout. Read before touching how config is stored/loaded.
|
||||
- [docs/protocol.md](docs/protocol.md) — the frozen v1 control/stats **wire** between the app and the native backends (config/stats/status); the format every backend parser must agree on
|
||||
- [docs/detection-vectors.md](docs/detection-vectors.md) — what an app can probe to detect the VPN (or a hidden package), which component (kmod / zygisk / lsposed / SELinux) covers each vector, how it manifests. Read before adding or changing a hook.
|
||||
- [docs/detection-vectors.md](docs/detection-vectors.md) — what an app can probe to detect the VPN (or a hidden package), which component (kmod / KPM / zygisk / lsposed / SELinux) covers each vector, how it manifests. Read before adding or changing a hook.
|
||||
- [docs/adb-root-debugging.md](docs/adb-root-debugging.md) — how to make `adb shell su` useful on KernelSU/APatch/Magisk test devices; covers UID 0 without capabilities, SELinux denials on `/data/adb` and `/data/system`, and KPM/KPatch diagnostics.
|
||||
- [docs/changelog.md](docs/changelog.md) — changelog storage (`changelog.d/` fragments + history JSON), `./scripts/changelog.py` usage
|
||||
- [docs/releasing.md](docs/releasing.md) — `./scripts/release.py` usage, version-bump flow
|
||||
|
|
@ -49,6 +49,7 @@ These short files cover everything specific to this repo. Skipping them leads to
|
|||
Single-command builds for both CI and local — the same scripts run in both places.
|
||||
|
||||
- **kmod**: `./kmod/build.py --kmi <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.
|
||||
|
|
|
|||
|
|
@ -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`
|
||||
|
||||
|
|
|
|||
177
README.en.md
|
|
@ -24,35 +24,36 @@ Existing modules like [NoVPNDetect](https://bitbucket.org/yuri-project/novpndete
|
|||
|
||||
vpnhide solves both problems with a layered architecture:
|
||||
|
||||
**Layer 1 — Java API (lsposed module):** hooks `system_server`, not the target app. `NetworkCapabilities`, `NetworkInfo`, and `LinkProperties` are filtered at the Binder level *before* data reaches the app's process. The app receives clean data over IPC — no injection into its process, nothing for anti-tamper to detect.
|
||||
**Layer 1 — Java API (lsposed module):** hooks `system_server`, not the target app. `NetworkCapabilities`, `NetworkInfo`, and `LinkProperties` are filtered at the Binder level *before* data reaches the app's process. The app receives clean data over IPC — no injection into its process, nothing for anti-tamper to detect. The same module also hides selected apps from observer apps at the `PackageManager` level (the **Apps** role).
|
||||
|
||||
**Layer 2 — Native (kmod or zygisk):** covers every native detection path:
|
||||
- **kmod** (recommended) — kernel-level `kretprobe` hooks. Filters `ioctl` (SIOCGIFFLAGS, SIOCGIFNAME, SIOCGIFCONF), `getifaddrs`/netlink dumps (RTM_GETLINK, RTM_GETADDR), and `/proc/net/route` reads — all before the syscall returns to userspace. Other `/proc/net/*` files (`tcp*`, `udp*`, `dev`, `if_inet6`, etc.) are blocked by SELinux for untrusted apps on Android 10+, so no hook is needed there. Zero in-process footprint. No library injection. Nothing to detect.
|
||||
- **zygisk** (alternative) — inline-hooks `libc.so` inside the app process. Same native coverage as kmod but runs in-process, so it's theoretically detectable by advanced anti-tamper. Use this if your kernel isn't supported by kmod.
|
||||
**Layer 2 — Native (kmod, KPM, or Zygisk):** covers native detection paths. Exactly one native backend should be active:
|
||||
- **kmod** (recommended for supported GKI kernels) — kernel-level `kretprobe` hooks. Filters `ioctl` (SIOCGIFFLAGS, SIOCGIFNAME, SIOCGIFCONF), `getifaddrs`/netlink dumps (RTM_GETLINK, RTM_GETADDR), routes, and `/proc/net/route` reads before the syscall returns to userspace. Zero footprint in the target process: no library injection, nothing to detect.
|
||||
- **KPM** (beta) — a KernelPatch Module with the same purpose as kmod, but without a GKI-variant-specific `.ko`. Useful for old/non-GKI 4.14 / 4.19 / 5.4 kernels and cases where the `.ko` cannot load. Requires a KernelPatch runtime: APatch or KPatch-Next-Module.
|
||||
- **Zygisk** — fallback when a kernel-level backend is not possible. It inline-hooks `libc.so` inside the target process, so banking and anti-fraud apps may detect it. For those apps, leave Native off and rely on the Java layer.
|
||||
|
||||
**Layer 3 — Additional app-level controls (integrated into the VPN Hide app):**
|
||||
- **Interface hiding** — hide VPN interfaces, routes, and Java API VPN state from selected apps
|
||||
- **Port hiding** — block selected apps from reaching `127.0.0.1` / `::1` so they cannot probe locally bound VPN / proxy daemons
|
||||
- **App hiding** — hide selected apps from selected observer apps at the PackageManager level
|
||||
**Layer 3 — Ports module (portshide):** a separate Magisk module. It blocks selected apps from reaching `127.0.0.1` / `::1` (via iptables), so they can't detect a locally bound VPN / proxy daemon by its open port (the **Ports** role).
|
||||
|
||||
The target app's process is completely untouched (with kmod + lsposed) — no Xposed, no inline hooks, no modified memory regions. Because of that, vpnhide works with banking and government apps that actively detect and block Xposed-based modules.
|
||||
The target app's process is completely untouched when using LSPosed + a kernel-level Native backend (kmod or KPM) — no Xposed, no inline hooks, no modified memory regions. Because of that, vpnhide works with banking and government apps that actively detect and block Xposed-based modules.
|
||||
|
||||
## What vpnhide hides
|
||||
|
||||
vpnhide is not just one toggle. It combines three different protections that can be enabled per app:
|
||||
vpnhide hides three things from selected apps, all configured per app via the four **J / N / A / P** roles (Java, Native, Apps, Ports):
|
||||
|
||||
1. **Interface hiding** — the core VPN-hiding layer. It removes VPN interfaces and routes from native APIs (`ioctl`, `getifaddrs`, `/proc/net/*`, `NetworkInterface`) and from Java APIs (`NetworkCapabilities`, `NetworkInfo`, `LinkProperties`).
|
||||
2. **Port hiding** — blocks localhost access for selected apps so they cannot detect Clash, sing-box, V2Ray, Happ, and similar tools by probing local ports.
|
||||
3. **App hiding** — hides selected installed apps from selected observer apps. Useful against package visibility checks, for example when an app tries to determine whether a VPN or proxy client is installed.
|
||||
1. **Interface hiding** — the main goal. It removes VPN interfaces and routes from native APIs (`ioctl`, `getifaddrs`, `/proc/net/*`, `NetworkInterface`) and from Java APIs (`NetworkCapabilities`, `NetworkInfo`, `LinkProperties`). It is delivered by two roles together — **Java (J)** and **Native (N)** — toggled independently.
|
||||
2. **Port hiding** — blocks localhost access for selected apps so they cannot detect Clash, sing-box, V2Ray, Happ, and similar tools by probing local ports (the **Ports (P)** role).
|
||||
3. **App hiding** — hides selected installed apps from selected observer apps. Useful against package visibility checks, for example when an app tries to determine whether a VPN or proxy client is installed (the **Apps (A)** role).
|
||||
|
||||
## Which modules do I need?
|
||||
|
||||
You always need the **VPN Hide app** (`vpnhide.apk`) plus one native module for interface hiding. The app can also use the optional Ports module if you want localhost port blocking:
|
||||
You always need the **VPN Hide app** (`vpnhide.apk`) + LSPosed/Vector for the Java layer + exactly one Native backend for native hiding. The app can also use the optional Ports module if you want localhost port blocking:
|
||||
|
||||
- **`kmod`** (recommended) — fully out-of-process, invisible to anti-tamper. Requires a supported GKI kernel.
|
||||
- **`zygisk`** — use this if your kernel isn't supported by kmod.
|
||||
- **`kmod`** (stable default) — fully out-of-process, invisible to anti-tamper. Requires a supported GKI kernel: 5.10, 5.15, 6.1, 6.6, or 6.12.
|
||||
- **`KPM`** (beta) — kernel-level backend for 4.14 / 4.19 / 5.4 and other cases where the `.ko` does not fit. Requires APatch or KPatch-Next-Module.
|
||||
- **`Zygisk`** — fallback if kmod/KPM are unavailable or you do not want to install a KernelPatch runtime.
|
||||
- **`portshide`** (optional) — install this if you want to block selected apps from probing localhost ports.
|
||||
|
||||
Do not install multiple Native backends at the same time. If more than one is installed, the app chooses the active one by priority: kmod, then KPM, then Zygisk; uninstall unused modules.
|
||||
|
||||
See [Install](#install) for step-by-step instructions.
|
||||
|
||||
## Install
|
||||
|
|
@ -64,18 +65,20 @@ Download the latest release from [Releases](https://github.com/okhsunrog/vpnhide
|
|||
1. Install `vpnhide.apk` as a regular app
|
||||
2. In LSPosed manager, enable the VPN Hide module and add **"System Framework"** to its scope
|
||||
3. Reboot the device (required — LSPosed hooks are injected into `system_server` at boot, so the module must be active before `system_server` starts)
|
||||
4. Open the VPN Hide app and grant it root access (Magisk will prompt automatically; on KernelSU-Next, grant permission manually in the manager)
|
||||
4. Open the VPN Hide app and grant it root access (Magisk usually prompts automatically; on KernelSU/KernelSU-Next/APatch, grant permission in the manager)
|
||||
|
||||
### Step 2 — Native module for interface hiding
|
||||
|
||||
Open the VPN Hide app. The **Dashboard** tab will detect your device and kernel, and tell you exactly which native module to install:
|
||||
Open the VPN Hide app. The **Dashboard** tab will detect your device and kernel, and tell you which Native backend to install:
|
||||
|
||||
- If your kernel is supported, it will recommend a specific kmod file (e.g. `vpnhide-kmod-android14-6.1.zip`)
|
||||
- If not, it will recommend the zygisk module (`vpnhide-zygisk.zip`)
|
||||
- For a supported GKI kernel, it recommends a specific kmod file, e.g. `vpnhide-kmod-android14-6.1.zip`.
|
||||
- For old/non-GKI 4.14 / 4.19 / 5.4 kernels, it recommends `vpnhide-kpm.zip` (beta). If no KernelPatch runtime is detected yet, the app asks you to install KPatch-Next-Module first or use Zygisk as a fallback.
|
||||
- For other kernels, it recommends `vpnhide-zygisk.zip`.
|
||||
|
||||
Install the recommended module:
|
||||
- **kmod:** via KernelSU-Next manager → Modules → Install from storage
|
||||
- **zygisk:** via KernelSU-Next or Magisk manager → Modules
|
||||
- **kmod:** via KernelSU-Next / KernelSU / Magisk manager → Modules → Install from storage.
|
||||
- **KPM:** install `vpnhide-kpm.zip`; under APatch the app may ask you to save the SuperKey in **Settings → Security** for boot-time activation. Under Magisk, KernelSU, and KernelSU-Next, install KPatch-Next-Module first if it is not already installed.
|
||||
- **Zygisk:** via KernelSU-Next, KernelSU, or Magisk manager → Modules.
|
||||
|
||||
Reboot the device after installing the native module.
|
||||
|
||||
|
|
@ -83,26 +86,40 @@ Reboot the device after installing the native module.
|
|||
|
||||
If you want localhost port blocking, install `vpnhide-ports.zip` via KernelSU-Next or Magisk manager.
|
||||
|
||||
This module is independent from kmod / zygisk and is only needed for the **Ports** mode in the app.
|
||||
This module is independent from the Native backend and is only needed for the **Ports** role in the app.
|
||||
|
||||
### Step 4 — Configure protections
|
||||
### Step 4 — Configure hiding
|
||||
|
||||
Open the VPN Hide app → **Protection** tab.
|
||||
Open the VPN Hide app → **Hiding** tab.
|
||||
|
||||
- **Tun** mode: use the **L** / **K** / **Z** toggles to control interface hiding layers for each app (LSPosed, Kernel module, Zygisk), or tap the row to toggle all layers at once
|
||||
- **Apps** mode: choose which apps should be hidden and which apps should act as observers
|
||||
- **Ports** mode: choose which apps should be blocked from accessing localhost ports
|
||||
Each app row has roles:
|
||||
|
||||
- **Java** — hide VPN through Android Java APIs at the LSPosed/system_server level.
|
||||
- **Native** — the active Native backend: kmod, KPM, or Zygisk. VPN Hide stores one Native selection; only the active backend acts.
|
||||
- **Apps** — the app becomes an observer that should receive a sanitized PackageManager view with selected VPN/proxy apps hidden.
|
||||
- **Ports** — block this app from reaching localhost ports.
|
||||
|
||||
Settings can switch from short **J / N / A / P** chips to full role labels. For Java, Native, and Ports, the settings icon next to the label opens per-hook or port-range settings.
|
||||
|
||||
Tap Save after making changes.
|
||||
|
||||
After changing targets, force-stop and restart the affected apps — hooks take effect on the next app launch.
|
||||
Java and kernel-level Native backends (kmod/KPM) apply immediately. Zygisk hooks and Ports rules are picked up by a selected app after force-stop and reopen.
|
||||
|
||||
> **Note:** some apps detect Zygisk hooks. For those apps, keep **Z** disabled and rely on kmod + LSPosed.
|
||||
> **Note:** some apps detect Zygisk hooks when Native is enabled for them. Leave Native off for those apps and rely on the Java layer, or use kmod/KPM instead.
|
||||
|
||||
<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
|
|
@ -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)
|
||||
- Серверная детекция неисправима на стороне клиента — используйте раздельное туннелирование
|
||||
|
||||
## Лицензия
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 220 KiB |
|
Before Width: | Height: | Size: 174 KiB |
|
Before Width: | Height: | Size: 123 KiB |
|
Before Width: | Height: | Size: 130 KiB |
BIN
assets/screenshots/community.png
Normal file
|
After Width: | Height: | Size: 205 KiB |
|
Before Width: | Height: | Size: 87 KiB |
|
Before Width: | Height: | Size: 111 KiB |
BIN
assets/screenshots/dashboard-hidden.png
Normal file
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 168 KiB |
|
Before Width: | Height: | Size: 178 KiB |
BIN
assets/screenshots/diagnostics-java.png
Normal file
|
After Width: | Height: | Size: 223 KiB |
|
Before Width: | Height: | Size: 184 KiB |
BIN
assets/screenshots/diagnostics-native.png
Normal file
|
After Width: | Height: | Size: 234 KiB |
BIN
assets/screenshots/hiding-help.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
assets/screenshots/hiding-list.png
Normal file
|
After Width: | Height: | Size: 204 KiB |
BIN
assets/screenshots/hook-picker.png
Normal file
|
After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 213 KiB |
|
Before Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 183 KiB |
BIN
assets/screenshots/settings-advanced.png
Normal file
|
After Width: | Height: | Size: 207 KiB |
BIN
assets/screenshots/settings-developer.png
Normal file
|
After Width: | Height: | Size: 216 KiB |
BIN
assets/screenshots/settings.png
Normal file
|
After Width: | Height: | Size: 209 KiB |
BIN
assets/screenshots/statistics-apps.png
Normal file
|
After Width: | Height: | Size: 219 KiB |
BIN
assets/screenshots/statistics-breakdown.png
Normal file
|
After Width: | Height: | Size: 208 KiB |
BIN
assets/screenshots/statistics.png
Normal file
|
After Width: | Height: | Size: 182 KiB |
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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`.
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
||||
```
|
||||
|
|
|
|||
|
|
@ -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."
|
||||
|
|
|
|||
|
|
@ -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}",
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||