vpnhide/lsposed/README.md
okhsunrog 60b3235dc0 docs: redesign README hierarchy for monorepo
Root README is now the single source for shared content (verified
against, split tunneling, threat model, component overview). Sub-READMEs
focus on component-specific technical details and link back to root.

- Remove ~700 lines of duplicated content across sub-READMEs
- Update all cross-references to use monorepo relative paths
- Add test-app to components table
- Update zygisk README: mark openat/recvmsg/SIOCGIFCONF as implemented
- Fix stale links to archived repos
2026-04-11 16:20:33 +03:00

7 KiB

vpnhide -- LSPosed module

Hooks Java network APIs to hide VPN presence from selected apps. Part of vpnhide.

Modes

  1. App-process mode (default) -- hooks installed directly in target app processes via LSPosed/Xposed. More complete coverage (all APIs below) but visible to anti-tamper SDKs that scan for Xposed artifacts.
  2. system_server mode -- hooks writeToParcel() in system_server so VPN data is stripped before Binder serialization. Zero presence in the app process. Use with kmod for native coverage.

What it hooks (app-process mode)

1. android.net.NetworkCapabilities

Method Behaviour with VpnHide
hasTransport(TRANSPORT_VPN) always returns false
hasCapability(NET_CAPABILITY_NOT_VPN) always returns true
getTransportTypes() TRANSPORT_VPN stripped from the returned int[]
getTransportInfo() returns null whenever the real value is VpnTransportInfo
toString() post-processed: |VPN stripped from Transports:, VpnTransportInfo{...} replaced with null, stray IS_VPN flags dropped from &-joined lists. Uses string manipulation (not regex) to avoid PatternSyntaxException on edge cases.

2. android.net.NetworkInfo

Method Behaviour with VpnHide
getType() returns TYPE_WIFI instead of TYPE_VPN
getTypeName() returns "WIFI" instead of "VPN"
getSubtypeName() any string containing "VPN" becomes empty

3. android.net.ConnectivityManager

Method Behaviour with VpnHide
getAllNetworks() VPN networks removed from the returned array
getActiveNetwork() if the active one is a VPN, substitute the first non-VPN network
getActiveNetworkInfo() if VPN, substitute the first non-VPN NetworkInfo
getAllNetworkInfo() VPN entries removed
getNetworkInfo(int type) returns null when asked about TYPE_VPN
getNetworkInfo(Network) returns null if the result would be a VPN

4. android.net.LinkProperties

Method Behaviour with VpnHide
getInterfaceName() rewrites VPN interface names (tun0, ppp0, wg0, etc.) to "wlan0"
getRoutes() routes whose interface is a VPN tunnel are dropped
getDnsServers() returns empty list for VPN LinkProperties
getHttpProxy() returns null for VPN LinkProperties

5. java.net.NetworkInterface

Method Behaviour with VpnHide
getNetworkInterfaces() VPN tunnel interfaces removed from the enumeration
getByName(name) returns null for VPN-like names
getByIndex(int) returns null if the looked-up interface is a VPN tunnel
getByInetAddress(addr) returns null if the matched interface is a VPN tunnel

6. /proc/net/* file reads

FileInputStream and FileReader constructors (both String and File variants) are hooked. Opens to the following paths are redirected to /dev/null:

/proc/net/route
/proc/net/ipv6_route
/proc/net/if_inet6
/proc/net/tcp
/proc/net/tcp6
/proc/net/udp
/proc/net/udp6
/proc/net/dev
/proc/net/arp
/proc/net/route_cache
/proc/net/rt_cache
/proc/net/fib_trie*
/proc/net/fib_triestat*
/proc/net/xfrm_stat*

7. System.getProperty

Proxy-related system properties that can leak VPN presence:

Key Behaviour with VpnHide
http.proxyHost returns null
http.proxyPort returns null
https.proxyHost returns null
https.proxyPort returns null
socksProxyHost returns null
socksProxyPort returns null

VPN interface name prefixes

tun, ppp, tap, wg, ipsec, xfrm, utun, l2tp, gre, plus anything whose name contains the substring vpn (case-insensitive).


system_server mode

When to use

For apps with aggressive anti-tamper SDKs that detect app-process hooks (crashes, NFC payment degradation, etc.). The default app-process hooks cannot be used for these apps.

How to enable

  1. In LSPosed/Vector manager, add "System Framework" to this module's scope.
  2. Reboot so the module loads into system_server.
  3. Install vpnhide-kmod for native-side coverage.
  4. Manage targets via kmod's WebUI, which writes UIDs to /data/system/vpnhide_uids.txt.

What it hooks

writeToParcel() on NetworkCapabilities, NetworkInfo, and LinkProperties. Uses a ThreadLocal save/restore pattern so the original values are preserved for non-target callers. Per-UID filtering via Binder.getCallingUid() ensures only target apps see the filtered view.

Target management

A FileObserver (inotify) watches /data/system/ and reloads the UID list immediately when the file changes. No reboot needed.

Important: apps with aggressive anti-tamper SDKs must NOT be added to this module's LSPosed app-process scope. Only "System Framework" should be in scope for these apps.


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 target apps to the module's scope and force-stop them (or reboot).
  5. Open the VPN Hide launcher icon. Tick the apps you want VPN hidden from.
  6. Force-stop the target app again so it re-reads prefs on next launch.

Double-gate logic

Two conditions must both be true for hooks to run inside an app:

  1. The app is in the module's scope in LSPosed manager.
  2. The app is checked in VPN Hide's picker UI.

Gate (1) controls which processes LSPosed loads the module into (performance). Gate (2) is the per-app allowlist read at hook time via XSharedPreferences.


What it does NOT cover

  • Native code path -- apps checking VPN from C/C++/JNI/Flutter bypass all Java hooks. Use zygisk or kmod.
  • Server-side detection -- DNS leakage, IP blocklists, latency/TLS fingerprinting. Unfixable client-side; use split tunneling.
  • Runtime.exec / ProcessBuilder shell-outs -- e.g. cat /proc/net/route in a subprocess.
  • NetworkCallback event counting -- inferring VPN from onAvailable() call count.
  • VpnService.prepare() -- not currently hooked.

See the project README for the full threat model and split-tunnel requirements.


Debugging

adb logcat | grep VpnHide

On target app startup you should see:

VpnHide: installing hooks for com.example.targetapp

Any hook that fails to install is logged with the hook category and exception message.

If hooks installed but the app still detects VPN:

adb logcat -c
# trigger the app's VPN check, then:
adb logcat -d > /tmp/detect.log
grep -iE "tun0|ppp0|wg0|vpn|TRANSPORT_VPN|NetworkInterface|/proc/net" /tmp/detect.log

Build

./gradlew assembleDebug

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


License

Personal / educational project. No explicit license -- do whatever you want with it but don't hold me responsible if a target app updates its detection logic and breaks things.