- Add .editorconfig with ktlint config (disable wildcard-import rule,
allow PascalCase for @Composable functions)
- Add kmod/.clang-format from upstream kernel tree
- Run clang-format on vpnhide_kmod.c (kernel coding style)
- Run ktlint --format on all Kotlin files (lsposed + test-app)
- Fix test assertions to match Option return type of read_u32_ne/read_u16_ne
- Cast c_char slices to u8 via from_raw_parts instead of pointer coercion,
fixing host-target compilation where c_char is i8
- Run rustfmt on all changed files
- WebUI: validate package names against [a-zA-Z0-9_.\-]+ before
interpolating into shell commands (both kmod and zygisk copies)
- zygisk hooks.rs: use RTM_NEWLINK/RTM_NEWADDR from filter.rs instead
of magic constants 16/20
- zygisk lib.rs: read /proc/self/maps via raw libc::open in
scrub_shadowhook_maps to bypass our own hooked_openat
- kmod: add comment explaining why seq->buf access without seq->lock
is safe in fib_route_ret (seq_read holds the mutex around ->show())
- kmod: add comment clarifying MODULE_LICENSE("GPL") vs MIT SPDX
kmod:
- Add explicit rcu_read_lock() around ifa->idev->dev->name dereferences
in inet6_fill_entry, inet_fill_entry, and rtnl_fill_entry
- Remove racy READ_ONCE fast-path in is_target_uid; uncontended spin_lock
is ~5ns on ARMv8 and the optimization had incorrect TOCTOU semantics
- Fix dev_ifconf_ret: return immediately on copy_from_user/copy_to_user
failure instead of breaking the loop and writing back a wrong ifc_len
- Fail module load if zero kretprobes register; warn on partial registration
lsposed:
- Fix isSystemServer check-then-set race: use AtomicBoolean.compareAndSet
to prevent duplicate hook installation from concurrent handleLoadPackage
- Fix NC hook partial state corruption: save all values before mutating,
restore on exception, only set ThreadLocals after all mutations succeed
- Fix NI/LP hooks: replace param.result=null (which skips writeToParcel
and corrupts the Parcel stream) with save-mutate-restore pattern
- Synchronize loadTargetUids() with double-checked locking; always cache
result (even empty) to avoid file I/O on every Binder call
- Fix suExec: drain stderr on background thread, destroy process in finally
zygisk:
- Use std::sync::Once for shadowhook initialization instead of AtomicBool
- Handle write() return value on memfd: loop on short writes, return error
- Make netlink parsers (read_u32_ne/read_u16_ne) return Option instead of
panicking on out-of-bounds access
The app needs root (su) to write target files. LSPosed hooks are
injected into system_server at boot, so a reboot is required after
enabling the module — system_server must restart with hooks active.
Single VERSION file in repo root as the source of truth. The script
update-version.sh propagates it to all 5 locations: kmod module.prop,
zygisk module.prop, zygisk Cargo.toml, lsposed build.gradle.kts,
test-app build.gradle.kts. versionCode = major*10000 + minor*100 + patch.
The lsposed APK now includes a Compose target picker UI that works
with both kmod and zygisk on any root solution. Update all READMEs
to recommend the app over WebUI (which is KernelSU-only).
The shell variable $UIDS accumulated UIDs with literal \n (two chars)
instead of actual newlines. printf "$UIDS" wrote garbage to
/proc/vpnhide_targets and vpnhide_uids.txt. The empty-targets case
used bare > redirect which never triggers the proc write handler.
Fix: accumulate UIDs with real newlines (like service.sh does), use
echo "$UIDS" for output, and echo (writes \n) for the empty case so
the kmod write handler fires and clears the UID list.
Affects: APK target picker, kmod WebUI, zygisk WebUI.
Migrate from AppCompat + XML to Jetpack Compose with Material3 and
dynamic colors. The new UI lists all installed apps with icons, names,
and package names. Supports text search, system app filter (selected
system apps always visible), and saves targets to all module paths
(kmod, zygisk, Magisk copy, /proc, UIDs file) via su.
- Add version catalog, upgrade Gradle 8.12, AGP 8.9.3, Kotlin 2.1.20
- Add QUERY_ALL_PACKAGES permission for Android 11+ visibility
- Remove old XML layout and AppCompat dependency
- /proc/vpnhide_targets: change from 0644 to 0600 (root only).
Apps could read the UID list and discover which apps are targeted.
- Remove /data/local/tmp/vpnhide_targets.txt copies from service.sh
and WebUI (no longer needed after get_module_dir() fix).
On Magisk, SELinux blocks all /data/adb/ access from forked processes.
Zygisk's get_module_dir() returns an fd opened with root privileges —
use openat() on it to read targets.txt regardless of SELinux context.
Removes /data/local/tmp workaround.
On Magisk, SELinux blocks zygote from reading /data/adb/vpnhide_zygisk/
(Permission denied). Fall back to /data/adb/modules/vpnhide_zygisk/targets.txt
which zygote can read on both Magisk and KernelSU.
- service.sh copies targets.txt to module dir on boot
- WebUI copies on save
- Rust code tries persistent path first, falls back to module dir
Replace custom kernel source cloning + prepare with pre-built DDK
Docker images (ghcr.io/ylarod/ddk-min). These include kernel headers,
Module.symvers, and AOSP clang for each GKI generation.
Now builds for: android12-5.10, android13-5.10, android13-5.15,
android14-5.15, android14-6.1, android15-6.6, android16-6.12.
Drop sha256 files from releases.
Port all 15 native VPN detection checks from C++ to Rust using
jni + libc crates. Gradle triggers cargo-ndk automatically via
a preBuild dependency — single `./gradlew installDebug` builds
everything.
- Remove CMakeLists.txt and native-lib.cpp
- Add test-app/native/ Rust crate with Cargo.toml and src/lib.rs
- Gradle buildRustNative task runs cargo-ndk, copies .so to jniLibs
- Update CI test-app job with Rust + NDK setup
- 23/23 checks pass on device
Replace separate kmod/zygisk/lsposed workflows with one ci.yml.
Four jobs run in parallel: kmod, zygisk, lsposed, test-app.
A release job depends on all four and runs only on tag pushes,
creating a GitHub release with all artifacts and checksums.
- Add Google's AOSP clang (clang-r487747c, same as Pixel kernel build)
to the CI Docker image via sparse checkout. Distro clang caused ABI
mismatches leading to bootloops on device.
- Update kmod workflow to use the Docker image + AOSP clang instead of
system clang from apt.
- Replace symvers with real vmlinux.symvers from Pixel kernel build
(8050 symbols vs 4060 from device .ko extraction).
- Add kmod build deps (bc, kmod, cpio, binutils-aarch64) to Docker image.
Add service.sh to zygisk module that resolves package names → UIDs
and writes /data/system/vpnhide_uids.txt on boot — same contract as
kmod's service.sh. This enables the lsposed system_server hooks to
work with zygisk (previously they only worked with kmod).
Also update the zygisk WebUI to resolve and write UIDs on save,
so changes apply immediately without reboot.
Remove all in-process app hooks (NetworkCapabilities, NetworkInfo,
ConnectivityManager, LinkProperties, NetworkInterface, System.getProperty).
These are redundant: system_server writeToParcel hooks cover all Java
API detection paths without touching the app's process, and native
detection is handled by vpnhide-kmod or vpnhide-zygisk.
In-process hooks were also counterproductive for anti-tamper SDK apps —
they modify the app's memory, which is exactly what those SDKs detect.
The module now only needs "System Framework" in LSPosed scope.
~600 lines removed.
The hookProcNetFiles() hook redirected FileInputStream/FileReader for
/proc/net/* paths to /dev/null inside the app process. This is
counterproductive: SELinux already blocks untrusted_app from reading
these files (EACCES), and the redirect changes the behavior from
"access denied" to "access succeeds, empty data" — a detectable
anomaly that anti-tamper SDKs could notice.
Also fix Java /proc/net/route check in test app to treat EACCES as
PASS, consistent with the native checks.
Add 9 new native checks covering all known VPN detection vectors:
netlink RTM_GETROUTE, /proc/net/ipv6_route, /proc/net/tcp{,6},
/proc/net/udp{,6}, /proc/net/dev, /proc/net/fib_trie, /sys/class/net.
Result: all new paths are blocked by SELinux for untrusted apps.
No additional kernel hooks needed — our 6 kretprobes already cover
every reachable detection vector. 23/23 checks pass.
Kernel module:
- Add dev_ifconf hook to filter SIOCGIFCONF interface enumeration
(goes through sock_ioctl -> dev_ifconf, not dev_ioctl)
- Add inet6_fill_ifaddr and inet_fill_ifaddr hooks to filter RTM_GETADDR
netlink responses. getifaddrs() was leaking tun0 via the address dump
even though RTM_GETLINK was filtered. Uses skb_trim to undo the fill
and return 0 (not -EMSGSIZE which causes infinite retry on empty skb).
- All 6 kretprobes now cover: ioctl, SIOCGIFCONF, netlink link dumps,
netlink address dumps (IPv4+IPv6), and /proc/net/route.
Test app:
- Treat SELinux EACCES/EPERM as PASS — if the app can't access the
resource, it can't detect VPN through it either.
- Test results: 14/14 passed with VPN active.