* feat: upgrade diagnostic captures Collect richer debug and full-logcat bundles with device, backend, hook, portshide, and kernel partition evidence. Add an explicit kernel image export for maintainer-requested kernel analysis. * fix: harden diagnostic capture sessions * fix(lsposed): harden diagnostic export error paths from review Address correctness findings from a review of the diagnostic-capture upgrade, all validated on a Pixel 4a via the agent bridge (kernel/debug/logcat exports). - Kernel image export: wrap exportKernelImagesZip so a zip/IO failure returns null (and cleans the parts dir) instead of escaping and stranding the export button disabled; add try/finally at the DiagnosticsScreen call site. Cap the per-partition dd (skip unknown/oversized partitions, bound with count=) so a mislabelled or huge partition can't fill /data. - HookDiagnostics: drive counter deltas off an explicit "baseline captured" flag instead of baselineCounters.isEmpty(), so a fresh boot with an empty baseline no longer reports every delta as n/a. Add HookDiagnosticsTest. - DebugShellSnapshot: raise the 20s snapshot timeout to 60s and preserve+flag a timeout-truncated section instead of silently dropping it and every later one. - LogcatRecorder: delete the intermediate raw logcat file after zipping and on failed start, so repeated captures stop leaking multi-MB files into cache. - DebugExport: rethrow CancellationException before the generic catch so navigating away mid-capture cancels cleanly instead of logging a false error. - DebugCaptureLogging: when the on-disk canonical config is present but unparseable, refuse the targets-snapshot rebuild (which would reset settings and clear autoHiddenPackages); fall back to an app-process-only toggle. - Dashboard: don't warn "ports rules not active" for a module the user disabled via their manager (new ports_disabled probe, mirrors the activator's guard). - activator write_atomic: per-process temp name so a boot-time and app-triggered activation can't corrupt each other's load_status. - Drop the wasted multi-user pm enumeration from the counter baseline command.
4.7 KiB
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 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.
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
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.
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
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)
- 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 undervpnhide_kmod,vpnhide_kpm, orvpnhide_zygisk), and runs the ports activator if present. The LSPosed hook reloads the JSON itself.
Works on KernelSU, Magisk, and any other root solution.
Install
- Build the APK (
./gradlew assembleDebug). - Install:
adb install app/build/outputs/apk/debug/app-debug.apk. - Open LSPosed/Vector manager, go to Modules, enable VPN Hide.
- Add "System Framework" to the module's scope. No other apps should be in scope.
- Reboot.
- Open the VPN Hide app and use the Hiding tab to manage roles for target apps.
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 and KPM cover native paths at the kernel level:
ioctl,getifaddrs/netlink, routes, and/proc/net/route. - This module covers Java APIs:
NetworkCapabilities,NetworkInfo,LinkPropertiesviawriteToParcel()insystem_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
adb logcat | grep VpnHide
Build
./gradlew assembleDebug
Requires JDK 17 or later. Output: app/build/outputs/apk/debug/app-debug.apk.
The default debug APK is debuggable and signed like release, but R8/resource
shrinking is enabled so the installed Xposed module stays small enough for
LSPosed/Vector cold-start smoke tests.
Use ./gradlew assembleRawDebug only when you need the old unminified debug APK
for Studio/debugger work. That variant can be much larger and may make the first
launch after updating the Xposed module slow enough to hit Android's process
start timeout.
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.