vpnhide/lsposed
okhsunrog 0d4cf09866 chore: CI + scripts cleanup (review items #12 #13 #24 #31 #32 #37)
Six small review-list items rolled together — all CI/dev-tooling, no
runtime behaviour change.

  #12  Dockerfile: pin Rust 1.95.0 and cargo-ndk 4.1.2 (was floating
       `stable` + latest cargo-ndk on monthly rebuild). Versions live
       in ENV vars to make the next bump a one-line edit.

  #13  Add shellcheck to lint job. SC2034/SC3043 excluded — Magisk
       reads SKIPUNZIP externally; Android's /system/bin/sh (mksh on
       Pixel) does support `local` despite POSIX. Verified locally
       that the 11 .sh files (module-side + dev tooling) pass.
       shellcheck baked into the CI image via apt; inline apt-get
       fallback covers the window before image rebuild.

  #24  ci.yml keystore.properties: replace heredoc with `printf '%s\n'`.
       Heredoc without single-quoted EOF re-expands $, backticks and
       backslashes in the password — printf takes the value verbatim.

  #31  scripts/release.py::patch_file now hard-fails when a regex
       pattern doesn't match (was silently leaving stale versions).

  #32  Split rotate_fragments_into_history into rotate + delete steps
       so release.py can save_json + write_md *before* unlinking the
       fragment files. If anything in between fails, fragments are
       still on disk and the run is retryable.

  #37  codegen-interfaces.py: emit `assert!(matches_vpn(…), msg)` /
       `assert!(!matches_vpn(…), msg)` instead of
       `assert_eq!(matches_vpn(…), true/false, msg)` —
       clippy::bool_assert_comparison was firing on every generated
       row under `cargo clippy --tests`. Both generated test modules
       regenerated. CI's clippy steps now also pass `--tests` so this
       class of regression is caught.
2026-04-27 01:14:03 +03:00
..
app ci: shave another ~80s off lint/lsposed jobs 2026-04-27 00:28:25 +03:00
gradle refactor(lsposed): swap hand-rolled JNI for Gobley uniffi bindings 2026-04-26 04:40:39 +03:00
native chore: CI + scripts cleanup (review items #12 #13 #24 #31 #32 #37) 2026-04-27 01:14:03 +03:00
build.gradle.kts feat: replace lsposed status screen with Compose target picker UI 2026-04-12 02:42:23 +03:00
gradle.properties ci: shave another ~80s off lint/lsposed jobs 2026-04-27 00:28:25 +03:00
gradlew monorepo: combine vpnhide-zygisk, vpnhide (lsposed), and vpnhide-kmod 2026-04-11 15:01:49 +03:00
gradlew.bat monorepo: combine vpnhide-zygisk, vpnhide (lsposed), and vpnhide-kmod 2026-04-11 15:01:49 +03:00
README.md docs: align README and dev guide with current code and CI 2026-04-26 15:32:30 +03:00
settings.gradle.kts monorepo: combine vpnhide-zygisk, vpnhide (lsposed), and vpnhide-kmod 2026-04-11 15:01:49 +03:00

vpnhide -- LSPosed module + target picker app

Hooks writeToParcel() in system_server to strip VPN data before Binder serialization reaches target apps. Part of vpnhide.

The APK also serves as the target management UI for the entire vpnhide project — it writes targets for both kmod and zygisk modules.

Zero presence in the target app's process -- only "System Framework" is needed in the LSPosed scope.

What it hooks

writeToParcel() on three classes inside system_server:

Class Effect
NetworkCapabilities VPN transport and capability flags stripped before serialization. Covers hasTransport(VPN), getAllNetworks() + VPN scan, getTransportInfo().
NetworkInfo VPN type rewritten to WIFI before serialization
LinkProperties VPN interface name and routes stripped before serialization

Uses a ThreadLocal save/restore pattern so the original values are preserved for non-target callers.

Per-UID filtering

Filtering is controlled by Binder.getCallingUid() -- only apps whose UID appears in the target list see the filtered view. System services, VPN clients, and everything else see real data.

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.

This file is written by:

  • The VPN Hide app (this APK's target picker UI)
  • The module's service.sh on boot

Target picker app

The APK includes a Compose UI for managing target apps across all vpnhide modules:

  • 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_targets (kmod live update, no reboot needed)
    • /data/system/vpnhide_uids.txt (system_server hooks, live reload via inotify)

Works on KernelSU, Magisk, and any other root solution.

Install

  1. Build the APK (./gradlew assembleDebug).
  2. Install: adb install app/build/outputs/apk/debug/app-debug.apk.
  3. Open LSPosed/Vector manager, go to Modules, enable VPN Hide.
  4. Add "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.

Combined use with kmod

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 covers native: ioctl, getifaddrs (netlink), /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.

Debugging

adb logcat | grep VpnHide

Build

./gradlew assembleDebug

Requires JDK 17 or later. Output: app/build/outputs/apk/debug/app-debug.apk.

The build cross-compiles lsposed/native/ (Rust crate) for aarch64-linux-android via cargo-ndk and bundles the resulting libvpnhide_checks.so into the APK's jniLibs/, plus auto-generated UniFFI Kotlin bindings under package dev.okhsunrog.vpnhide.checks. Both steps are driven by Gobley Gradle plugins (dev.gobley.cargo + dev.gobley.uniffi) — no manual cargo invocation needed. See ../docs/development.md for the full prereq list.

License

MIT. See LICENSE.