KPM has been validated across the supported kernel families and on real hardware, so it is no longer experimental. Remove the beta/experimental framing everywhere it is presented as a current status: - app: drop the "KPM is experimental — fall back to kmod/Zygisk and contact the author" dashboard note shown while KPM is active, and the beta caveat under the KPM install recommendation; strip "(beta)" from the recommendation and capability strings (EN/RU/zh) - docs: drop "beta" from the KPM descriptions in the three READMEs, CLAUDE.md, docs/development.md, docs/detection-vectors.md, and kmod/README.md Historical changelog entries (changelog.json, update-json/changelog.md) keep their original wording — KPM was beta when those releases shipped. The `.ko` remains the default on supported GKI devices; that is about per-KMI QEMU test coverage, not KPM stability.
7.9 KiB
Development setup
How to build vpnhide from source.
Prerequisites
- JDK 17 or later — what the CI image installs (
openjdk-17-jdk-headless); local builds with JDK 21 also work. Thelsposed/appGradle build setssourceCompatibility = 17andjvmTarget = "17". - Android SDK — install
platforms;android-35,build-tools;35.0.0,platform-tools(via Android Studio orcmdline-tools). ExportANDROID_HOME. - Android NDK r28 or later — export
ANDROID_NDK_HOME(or drop it in$ANDROID_HOME/ndk/<version>/, the scripts auto-detect). Thelsposed/appbuildRustProbetask readsANDROID_NDK_HOME(falling back toANDROID_NDK_ROOT, then the SDK-managedndk/<version>) and passes it to cargo-ndk. r27c builds compile, but the resulting cdylibs trigger an Android 16 KiB-page-size compatibility warning at app start on Pixel 8 Pro / future hardware (сегмент LOAD не выровнен); r28+ aligns LOAD segments on 16 KiB by default. (zygisk/build.rsandlsposed/native/build.rsalso pass-Wl,-z,max-page-size=16384explicitly so older NDKs stay compatible — defence in depth.) - Rust (latest stable) with the Android target:
rustup target add aarch64-linux-android cargo install cargo-ndk - clang-format 18.x — kmod C formatting is pinned because clang-format
versions disagree on kernel-style C casts. The repo checks this via
scripts/clang-format-c.sh. On Arch:sudo pacman -S clang18 mkdir -p ~/.local/bin ln -sfn /usr/lib/llvm18/bin/clang-format ~/.local/bin/clang-format-18 clang-format-18 --version dockerorpodman— only for building the kernel module via DDK images. Docker is preferred when both are installed, matching CI and device-test workflows. See kmod/BUILDING.md.zip— packaging module zips.adb— installing builds on a device.
The Rust crate at lsposed/native/ is built via cargo-ndk by the buildRustProbe Gradle Exec task in lsposed/app/build.gradle.kts (wired into preBuild), which bundles the resulting libvpnhide_checks.so into the APK's jniLibs/ plus the root-exec'able vhprobe bin as an asset. There is no codegen and no extra Gradle plugin: the whole native surface is one JSON-returning JNI function (NativeProbe.runAllChecksJson), parsed on the Kotlin side — no manual cargo invocation needed.
Repository layout
| Path | Component |
|---|---|
zygisk/ |
Zygisk native module (Rust, inline libc hooks) |
lsposed/ |
LSPosed module + target-picker Android app (Kotlin, Compose) |
kmod/ |
Kernel-level native backends: GKI .ko (C, kretprobes) and KPM (KernelPatch inline hooks) |
portshide/ |
Localhost port blocker (shell + iptables) |
scripts/ |
Release & changelog tooling |
update-json/ |
Magisk/KSU update metadata |
docs/ |
Contributor documentation (this directory) |
Each module has its own README with architecture and design notes.
Signing keystore (required for lsposed)
lsposed/app/build.gradle.kts routes all APK build types (debug, rawDebug, and release) through a single signing config that reads lsposed/keystore.properties. Without that file, ./gradlew assembleDebug, :app:assembleRawDebug, and :app:assembleRelease fail with:
SigningConfig 'release' is missing required property 'storeFile'
Create lsposed/keystore.properties (git-ignored):
storeFile=/absolute/path/to/your.jks
keyAlias=yourAlias
password=yourPassword
Generate a keystore if you don't have one:
keytool -genkey -v -keystore ~/vpnhide.jks \
-keyalg RSA -keysize 4096 -validity 36500 -alias vpnhide
Build each module
zygisk module
cd zygisk && ./build.py
# → zygisk/target/vpnhide-zygisk.zip
The script auto-detects the NDK from $ANDROID_NDK_HOME or ~/Android/Sdk/ndk/*.
lsposed APK
cd lsposed && ./gradlew :app:assembleDebug
# → lsposed/app/build/outputs/apk/debug/app-debug.apk
The default debug APK is still debuggable and uses the same package id/signing as release, but it is R8/resource-shrunk so LSPosed/Vector has less dex to prepare after an APK update. This is the recommended local and agent-control build.
For a distribution build:
cd lsposed && ./gradlew :app:assembleRelease
# → lsposed/app/build/outputs/apk/release/app-release.apk
For the old unminified Studio/debugger path, build :app:assembleRawDebug
instead. Use it only when you specifically need to debug code before R8 has
shrunk it.
kernel module
./kmod/build.py --kmi android14-6.1 # one variant
./kmod/build.py --all # every supported GKI
# → vpnhide-kmod-<kmi>.zip at the repo root
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.
KPM backend
./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.
Install on device
# APK
adb install -r lsposed/app/build/outputs/apk/release/app-release.apk
# 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-<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 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)
CI runs the same checks. See .github/workflows/ci.yml for the authoritative list.
# Codegen drift — run after editing data/interfaces.toml; CI fails on diff
python3 scripts/codegen-interfaces.py
git diff --quiet # must be clean
# Python (ruff, config in pyproject.toml). uvx runs without installing anything global.
uvx ruff format --check
uvx ruff check
# Rust
cd zygisk && cargo fmt --check && cargo ndk -t arm64-v8a clippy -- -D warnings
cd ../lsposed/native && cargo fmt --check && cargo ndk -t arm64-v8a clippy -- -D warnings
cd ../zygisk && cargo test
cd ../lsposed/native && cargo test
# C (kernel module)
scripts/clang-format-c.sh --check
# Host-side test of the generated VPN-iface matcher used by the kernel module
gcc -O2 -Wall -Werror -o /tmp/test_iface_lists kmod/test_iface_lists.c && /tmp/test_iface_lists
# Kotlin
ktlint "lsposed/app/src/**/*.kt"
cd lsposed && ./gradlew :app:lintDebug :app:testDebugUnitTest
Build versions
Every module zip and the APK carry a version string derived from git at build time:
- on a release tag
vX.Y.Z→X.Y.Z - otherwise →
X.Y.Z-N-gSHA(commits since the nearest tag + short hash, plus-dirtyif the working tree has uncommitted changes)
So a locally-built dev APK shows up in Android Settings as e.g. 0.6.1-5-gabc1234-dirty, and the same string lands in module.prop inside the zip. The committed module.prop files themselves stay at the last release number — the version is stamped into a staging copy per build.
See releasing.md for details.
More docs
- releasing.md — version bump, tag, release flow
- changelog.md — how changelog entries flow from JSON → markdown
- kmod/BUILDING.md — kernel-module build deep dive