Skip to content

Repository files navigation

NetCloak 🛡️

A dual-layer (Java/ART + native libc) network-environment cloaking framework for Android.

Platform Engine ABI Version License

Author: @acessrdpgg · Repo: github.com/acessrdpgg/NetCloak-Xposed


Research disclaimer. NetCloak is intended for authorized security testing, interoperability research, and privacy analysis on devices and applications you own or are explicitly permitted to test. It does not guarantee that every application, SDK, kernel component, or security mechanism will observe the simulated environment. Do not use it to defeat access controls, fraud controls, or any security mechanism on systems you do not control.


📖 Table of Contents


🎯 What NetCloak Does

NetCloak makes a target process believe it is running on an ordinary phone connected to an ordinary Wi‑Fi network — even when a VPN tunnel, custom routing, or a hooking framework is active on the device.

It does this by presenting a single, internally consistent network reality across every observation surface an app can reach, from high-level Java APIs down to raw kernel syscalls:

  • VPN concealmenttun/tap/ppp/wg/ipsec/WireGuard interfaces, their routes, and their TRANSPORT_VPN capability are removed at every layer.
  • Wi‑Fi fabrication — a coherent Wi‑Fi environment (SSID, BSSID, RSSI, link speed, frequency, DHCP lease, scan neighbourhood) that behaves like a live radio, not a frozen constant.
  • Package concealment — VPN/proxy/circumvention apps and any app declaring android.net.VpnService become invisible to PackageManager.
  • Anti-fingerprinting — module markers (lspatch, magisk, zygisk, frida, …) are stripped from /proc/self/maps and mount tables; emulator/QEMU system properties read as absent.

The reference adversary during development was RKNHardering (com.notcvnt.rknhardering), a multi-vector VPN/environment detector that probes Java APIs, raw syscalls, getdents64 directory reads, and AF_NETLINK sockets simultaneously — precisely the surfaces single-layer modules leave exposed.

🧠 Why It Is Hard — and Why Most Modules Fail

A typical "hide VPN" Xposed module hooks a handful of Java methods (ConnectivityManager.getNetworkCapabilities, NetworkInterface.getNetworkInterfaces) and stops there. That is trivially defeated, because a determined detector never asks Java — it goes underneath it:

Detector technique Bypasses a Java-only module because… NetCloak's answer
getifaddrs() / if_indextoname() (libc) libc never calls back into ART Native interface_hooks drop VPN interfaces / return ENXIO
Raw read() of /proc/net/route, /proc/net/if_inet6, /proc/self/maps file bytes bypass every Java API read()/pread64() content filter via fd classification
Raw getdents64() on /sys/class/net the libc readdir() wrapper is skipped dedicated getdents64 entry filter
stat()/access() of /sys/class/net/tunX (sysfs-leak probe) bytes never flow through a name API fstatat/faccessat AT-variant hooks return ENOENT
AF_NETLINK RTM_GETLINK/RTM_GETADDR talks straight to the kernel, no libc name API netlink transform: getifaddrs dump passed through + name-pruned; raw-probe dumps physically compacted
svc #0 inline syscalls / the syscall() dispatcher no exported libc symbol to hook the syscall() dispatcher itself is proxied

The second, harder problem is coexistence. NetCloak is designed to run under rootless loaders (LSPatch/NPatch) whose VectorNative SigBypass already inline-hooks the hottest libc functionsopen/openat/stat/access/readlink*/fopenbefore NetCloak loads. Stacking a second inline trampoline on those functions hangs ART at startup (confirmed, reproducible freeze).

NetCloak's core design decision follows from this: never hook what SigBypass already owns. Instead it intercepts the functions SigBypass leaves alone — read, pread64, getdents64, socket, recv*, close, and the syscall dispatcher — and achieves the same concealment through a different, collision-free path. That is the capability most modules cannot replicate: they either hook open() and freeze, or they never reach the native layer at all.

The netlink path carries the project's most expensive lesson, in four generations. (1) An early build that compacted every datagram (returning a shorter length) corrupted bionic's getifaddrs parser and crashed the target with SIGSEGV. (2) A rewrite that renamed the VPN interface in place (e.g. tun0eth0) was length-safe but introduced a phantom eth0 the JVM view never reported — tripping NATIVE_JVM_MISMATCH and failing every follow-up ioctl(eth0). (3) Retyping the whole RTM_NEWLINK/RTM_NEWADDR message to NLMSG_NOOP ("skip me") looked length-safe, but inside getifaddrs it orphaned the interface's RTM_NEWADDR — bionic had already parsed the address, could no longer resolve it to a link by ifindex, and fell into an ioctl(SIOCGIFNAME) recovery that faulted at 0x0 (and hardened raw parsers with no NOOP handler mis-walked it). (4) The current build splits by call site, because the crash was never about the transform — it was about mutating the dump bionic itself is parsing:

  • Inside getifaddrs() (detected by a per-thread guard) the netlink dump is passed through byte-for-byte untouched — the VPN interfaces are only read to learn their ifindex. bionic builds its complete ifaddrs list unharmed, and that finished list is then pruned by name in the getifaddrs proxy. No orphaned address, no ioctl recovery, no crash.
  • On every other (raw-probe) path — a detector reading netlink directly, where no bionic parser is involved — the VPN messages are physically compacted out: survivors slide down and a genuinely shorter length is returned, exactly what a device with no VPN reports.

🏛️ Architecture at a Glance

MockEntry is the single Xposed entry point. On every handleLoadPackage it first applies the optional settings overlay, then routes the process into the dual-layer simulation.

flowchart TD
    A([Target process starts]) --> B[MockEntry.handleLoadPackage]
    B --> S["ModuleSettings.apply()<br/><i>overlay → Config, or defaults</i>"]
    S --> M{GLOBAL_MODE?}
    M -->|true| H[Hook this process]
    M -->|false| T{pkg ∈ TARGET_PACKAGES?}
    T -->|no| P[Pass through untouched]
    T -->|yes| H

    H --> J[["Layer 1 · Java / ART<br/>(XposedHelpers)"]]
    H --> N[["Layer 2 · Native / libc<br/>(NativeBridge → libmockenv.so → ShadowHook)"]]

    subgraph JAVA [ ]
      J --> J1[NetworkHooks<br/>WifiManager · Connectivity · NetworkInterface]
      J --> J2[PackageHooks<br/>hide VPN / proxy apps + VpnService]
    end

    subgraph NATIVE [ ]
      N --> N1[interface_hooks<br/>getifaddrs · ioctl · if_*index]
      N --> N2["fs_hooks<br/>opendir/readdir · (open* gated OFF)"]
      N --> N3["syscall_hooks<br/>read · getdents64 · netlink · fstatat/faccessat · syscall()"]
      N --> N4[property_hooks<br/>__system_property_*]
    end

    J1 --> R([One consistent network reality])
    J2 --> R
    N1 --> R
    N2 --> R
    N3 --> R
    N4 --> R
Loading

Both layers read the same Config object, so the Java view and the native view can never contradict each other — the mismatch that detectors specifically look for (NATIVE_JVM_MISMATCH) is designed out.


☕ Layer 1 — Java / ART Interception

Implemented in NetworkHooks.kt and PackageHooks.kt via the Xposed API. This layer answers every framework-level question with a coherent Wi‑Fi story.

Surface Hooked methods Behaviour
WifiManager getConnectionInfo, getDhcpInfo, getScanResults, startScan, isWifiEnabled, getWifiState, is5GHz/24GHz/6GHzBandSupported, isWpa3SaeSupported, isEnhancedOpenSupported Forged WifiInfo/DhcpInfo, a realistic scan neighbourhood, and honest band-capability reporting
ConnectivityManager getActiveNetworkInfo, getNetworkInfo, getNetworkCapabilities, getLinkProperties, isActiveNetworkMetered Reports a connected, unmetered Wi‑Fi transport; strips VPN routes from LinkProperties
NetworkCapabilities hasTransport, getTransportTypes, hasCapability Removes TRANSPORT_VPN, forces TRANSPORT_WIFI and NET_CAPABILITY_NOT_VPN
NetworkInterface getNetworkInterfaces, getByName, getByIndex, isUp, getHardwareAddress, getMTU, … Drops VPN interfaces (default) or exposes a synthetic wlan0 (opt-in fabrication)
LinkProperties getRoutes, getAllRoutes Filters out any route bound to a VPN interface (defence-in-depth)
PackageManager getInstalledPackages/Applications/Modules, getPackageInfo, getApplicationInfo, queryIntentServices, resolveService, getLaunchIntentForPackage, … Hides Config.HIDDEN_PACKAGES and VpnService-declaring apps

Route stripping matters most here. The detector's routing check reads LinkProperties.getRoutes() across every network; a lingering 0.0.0.0/1 via tun0 there is what trips the "dedicated routes / split tunnelling" findings — so VPN-bound RouteInfo entries are removed before the interface is renamed to wlan0.

⚙️ Layer 2 — Native libc Interception

Loaded as libmockenv.so and installed through ShadowHook 1.0.9 in SHARED mode (function-level PLT/inline hooks on exported libc symbols). Configuration is pushed once from Kotlin via a 21-parameter JNI bridge (the two most recent additions: show_logs, the diagnostics gate, and api_level, the running Build.VERSION.SDK_INT), so there is still a single source of truth.

Installation order (initShadowHooknetcloak.cpp): interface_hooksfs_hooksproperty_hookssyscall_hooks.

The fd-classification content filter (read / pread64)

Because open() cannot be hooked (SigBypass owns it), NetCloak classifies each file descriptor once, lazily, on first read() — using a raw readlinkat syscall on /proc/self/fd/N — and caches the class. Subsequent reads of a sensitive fd are served filtered bytes.

flowchart LR
    RD[read fd, buf, n] --> C{fd class cached?}
    C -->|no| CL["classify via raw<br/>readlinkat(/proc/self/fd/N)"]
    CL --> C
    C -->|yes| K{class}
    K -->|maps / mounts| FM[drop lines matching<br/>PROC_MAPS_MARKERS]
    K -->|/proc/net/*| FV[drop lines matching<br/>VPN_INTERFACE_TOKENS]
    K -->|plain| PASS[original bytes]
    FM --> OUT([filtered buffer])
    FV --> OUT
    PASS --> OUT
Loading

This removes the lspatch in /proc/self/maps and tun0 in /proc/net/route findings without ever touching open().

The call-site-split netlink transform (socket / recv / recvfrom / recvmsg)

getifaddrs() and hardened detectors both talk to the kernel over AF_NETLINK, bypassing every libc name API. NetCloak tags netlink sockets at socket() time, then transforms the RTM_NEWLINK/RTM_NEWADDR (and RTM_DELLINK/RTM_DELADDR) replies — but how it transforms them depends on who is reading, enforced by a per-thread in_getifaddrs guard:

  • bionic's own getifaddrs() dump is passed through byte-for-byte and only read, to learn which ifindex values belong to VPN interfaces (matched by IFLA_IFNAME/IFA_LABEL, or by that learned index so the label-less IPv6 RTM_NEWADDR messages are caught too). bionic finishes building its ifaddrs list intact; the VPN entries are then removed from that finished list by name inside the getifaddrs proxy. Touching the dump here is what orphaned addresses and crashed earlier builds — so here we never do.
  • Every other (raw-probe) recv is physically compacted: VPN messages are removed, survivors slide down, and a genuinely shorter length is returned — the honest shape of a device with no VPN.
flowchart LR
    S["socket(AF_NETLINK)"] --> TAG[mark fd class = NETLINK]
    RCV["recv / recvfrom / recvmsg"] --> G{fd is netlink?}
    G -->|no| ORIG[return original bytes]
    G -->|yes| GI{in getifaddrs?}
    GI -->|yes| RD["pass through UNTOUCHED<br/>learn VPN ifindexes only<br/><i>(name-prune the finished<br/>ifaddrs list afterwards)</i>"]
    GI -->|no| CP["COMPACT: drop VPN msgs,<br/>slide survivors, return<br/>SHORTER length"]
Loading

getdents64, the stat family & the syscall dispatcher

  • getdents64 — detectors enumerate /sys/class/net and /proc/net with the raw syscall, skipping libc readdir(). NetCloak filters VPN entries directly out of the dirent stream.
  • fstatat / fstatat64 / faccessat — the detectSysfsLeak probe stat()s / access()es /sys/class/net/tunX and /proc/sys/net/*/conf/tunX directly. bionic implements stat/lstat/access as thin forwarders to these AT-variants, which — unlike the stat/statx/access wrappers — SigBypass does not inline-hook, so stacking here is collision-free. The proxies return ENOENT for VPN net paths (a bounded string test only, no I/O, no allocation). Gated by HOOK_STAT_FAMILY; disable it only if a particular patcher is found to hook fstatat.
  • syscall() dispatcher — some probes issue syscall(SYS_read, …), syscall(SYS_getdents64, …), syscall(SYS_recvfrom, …) etc. directly. The syscall proxy re-dispatches these cases through the same filters, and additionally returns ENOENT for SYS_newfstatat/SYS_statx/SYS_faccessat/SYS_faccessat2 on VPN /sys//proc net paths — covering probes that skip the libc AT-wrapper entirely.
  • close — evicts the fd from the classification cache so descriptor numbers are never confused after reuse.

Directory & property hooks

  • opendir/readdir/readdir64/closedir (fs_hooks) — cold libc paths SigBypass ignores; VPN entries are dropped from tracked net directories.
  • __system_property_get/_find/_read_callback/_read (property_hooks) — QEMU/goldfish/emulator property keys read back as absent, clearing markers like ro.kernel.qemu.gles across both the legacy and modern callback read paths.
  • open/openat/open64/openat64 (fs_hooks) — a memfd content-rewrite path that exists but is permanently gated OFF (HOOK_OPEN_FAMILY = false) precisely because of the SigBypass collision. read() filtering replaces it.

🧩 Version-Resilient Hooking (API 26 → latest)

Android's networking surface is not the same across releases. Methods appear late (WifiManager.is6GHzBandSupported — API 30, isEnhancedOpenSupported — API 33), fields are renamed or removed, whole introspection paths are closed by the OS (RTM_GETLINK for apps — API 30), and libc symbol sets shift between platforms. A module that assumes "every Android is the same" either crashes on the first missing symbol or silently stops installing the rest of its hooks after it.

NetCloak's rule is the opposite of a hardcoded SDK gate: attempt every hook on every platform, and wrap each one so a class/method/field that does not exist on the running device is caught, logged, and skipped — while every other hook still installs.

  • Java / ART layer resolves classes with XposedHelpers.findClassIfExists (returns null instead of throwing) and installs each method hook through a single guard, HookGuard.safe("Class.method") { … }. A NoSuchMethodError, NoSuchFieldError, or ClassNotFound* is classified as an expected absence on this API and logged as a ⏭️ skip at debug level; anything else is surfaced as a real error. One missing symbol never aborts the batch.
  • Native / libc layer pushes the running Build.VERSION.SDK_INT across the JNI bridge (api_level) and logs the regime up front; each shadowhook_hook_sym_name is checked for a null stub, so a libc export missing on an older bionic is a logged skip, not a hang.

This is why the same APK behaves correctly from Android 8.0 (API 26, minSdk) to the latest release without per-version builds — the platform decides which hooks can install, and NetCloak logs exactly which ones did.

What each release actually permits

The cloaking behaviour is identical everywhere; what differs is which observation surface exists and which the OS itself has already closed (so a given hook is either load-bearing or redundant on that platform):

Android (API) Netlink RTM_GETLINK for apps What is load-bearing here NetCloak's per-version logic
8.0–10 (26–29) ✅ Open to apps Raw netlink hook is fully load-bearing — a detector can dump every link itself Netlink transform does the heavy lifting; read()/getdents64/stat cover the file paths; __system_property_read_callback present since API 26
11 (30) ⛔ SELinux denies bind() + RTM_GETLINK to non-system apps (AOSP b/155595000) RTM_GETADDR (still allowed) + getifaddrs name-prune + the file/sysfs paths Netlink RTM_GETLINK dumps are already empty by OS policy — the transform now mainly prunes RTM_GETADDR and any pre-bound sockets; the banner logs this
12–13 (31–33) ⛔ As API 30 + tightened MAC/neighbour access Same as 30, plus late Wi‑Fi capability getters (is6GHzBandSupported, isEnhancedOpenSupported) exist and are hooked if present Version-guarded Wi‑Fi getters install via HookGuard; absent ones are skipped cleanly on lower APIs
14+ (34+) ⛔ As above Same surface; newer WifiInfo/NetworkCapabilities fields populated when the setters resolve Everything attempted; unresolved new symbols are logged skips, so the module stays forward-compatible without a rebuild

The api_level regime is logged once at init on both layers ([INIT] api_level=… regime=… natively, and the 🚀 NetCloak init … | API … (…) banner in Kotlin) when debug logging is on — so a field report immediately shows which platform story applied.


📡 Wi‑Fi Realism Engine

Hiding the VPN is only half the job; the replacement environment has to look like a real radio, not a hard-coded constant. NetCloak generates a fresh, self-consistent Wi‑Fi world once per process launch (frozen for the session so repeated reads agree) and then adds live micro-variation on top.

  • Organic SSIDs — three naming styles instead of a naïve prefix_suffix: vendor-default (TP-Link_A4F2, NETGEAR47), ISP-provisioned (JioFiber-8842, Xfinity), and human-named (The_Promised_LAN, SecondFloor), with realistic band tags (_5G, _2.4G).
  • Credible MAC/BSSID split — the AP BSSID is built from a real IEEE-registered vendor OUI (TP-Link/Netgear/ASUS/Cisco/Google…), because a locally-administered 02: AP is itself a tell; the device STA MAC keeps the 02: locally-administered bit, exactly matching Android per-network MAC randomization.
  • Band-consistent RFis5GHz (70% of sessions) drives a matching channel, frequencyMhz (correct 802.11 mapping), and rx/tx link-speed caps, so band, channel, and frequency never disagree.
  • Live jitter — RSSI wobbles ±3 dBm per poll around a stable baseline (a frozen signal is unnatural), clamped to a sane window.
  • Full WifiInfo — beyond SSID/BSSID/RSSI: mFrequency, mLinkSpeed, mTxLinkSpeed/mRxLinkSpeed, mMaxSupported*LinkSpeed, and mWifiStandard (11ac/11ax) are populated (each guarded for API-level availability).
  • Scan neighbourhoodgetScanResults() returns the connected AP as the strongest signal surrounded by 2–4 weaker neighbours on assorted channels/bands, generated once so scans stay consistent.
  • Coherent DHCPDhcpInfo (IP/gateway/netmask/DNS/lease) is derived from the same host IP as WifiInfo.

All generation lives in Config.kt and is disabled by a single switch (USE_DYNAMIC_GENERATION) for reproducible testing.


🚀 Deployment Modes: Rootless vs Root

NetCloak runs in two very different environments, and its behaviour is deliberately identical for the target app in both — the only difference is how it is configured.

flowchart TB
    subgraph ROOTLESS [Rootless · LSPatch / NPatch]
      direction TB
      L1[Module code merged into target APK]
      L1 --> L2[No LSPosed prefs bridge · no root]
      L2 --> L3["XSharedPreferences.file.canRead() == false"]
      L3 --> L4["ModuleSettings.apply() returns early<br/>→ compile-time Config defaults stand"]
    end
    subgraph ROOT [Root · LSPosed]
      direction TB
      R1[System-wide Xposed runtime]
      R1 --> R2["xposedsharedprefs bridge makes<br/>the settings file world-readable"]
      R2 --> R3["ModuleSettings.apply() reads overlay<br/>→ live-tunable Config"]
    end
Loading

Why this matters (and why it is safe). On rootless LSPatch/NPatch the module app and the patched app are separate sandboxes with no shared, world-readable storage and no root to relax permissions — so the settings UI simply cannot reach the patched process. NetCloak treats that as a first-class case: ModuleSettings.apply() checks canRead(), and when the overlay is unreadable it returns immediately, leaving every Config value at its compile-time default.

Invariant: For LSPatch/NPatch users, installing or ignoring the UI changes nothing. The module behaves exactly as a headless, pre-configured build. Tune defaults by editing Config.kt and rebuilding.

On LSPosed (root), the manifest's xposedsharedprefs flag lets LSPosed bridge the world-readable overlay into hooked processes, so the same toggles become live and per-install configurable.

What changes — and what doesn't — between the two

The cloaking engine is byte-for-byte the same in both modes: both the Java/ART layer and the native libc layer install and behave identically. What differs is reach and configurability, not concealment strength.

🔓 Rootless (LSPatch / NPatch) 🧩 Root (LSPosed)
How it deploys Module code is merged into the target APK; you install the patched APK Installed once as a system-wide Xposed module; enabled per-app via LSPosed scope
Which apps it can hook Only apps you can repackage & re-sign (patch yourself) Any user app in scope — no repackaging
System / framework apps ❌ Cannot patch android/system_server (not repackageable) ✅ Can be scoped (subject to SELinux)
Configuration Compile-time Config.kt defaults — the UI is inert (prefs unreadable across sandboxes) Live, per-install via the settings UI (world-readable overlay bridged in)
Changing behaviour Edit Config.kt → rebuild → re-patch Flip a toggle in the app, relaunch the target
VPN / route / interface cloaking ✅ Full (Java + native) ✅ Full (Java + native)
/proc maps + net content, getdents64, netlink, sysfs stat
Emulator-property hiding
Persistence Baked into the patched APK Survives as long as the module is enabled

What is impossible in no-root, specifically:

  • Live reconfiguration. The module app and the patched app are isolated sandboxes with no shared world-readable storage and no root to relax it, so on-screen toggles cannot reach a patched process. You tune by editing Config.kt and rebuilding — the UI honestly reports Standalone mode.
  • Hooking apps you can't repackage. Anything you cannot decode/re-sign (or that hard-rejects a re-signed build) is out of reach; root sidesteps this because nothing is repackaged.
  • Touching the framework. system_server and android are never merged into a patched user APK, so system-wide network facts can't be altered from rootless mode.

What neither mode can change (outside the interception boundary entirely): your public egress IP / GeoIP location, the SIM's MCC/MNC and carrier, kernel/hardware/firmware facts, and traffic captured off-device. See Honesty.


⚔️ How NetCloak Compares to Other Modules

Projects in this space split into two camps: detectors (the adversaries NetCloak is measured against) and hiders (the modules it competes with). NetCloak is a hider, and the closest serious hider is okhsunrog/vpnhide. It is an excellent module — and it is root-only. That single distinction is the reason NetCloak exists.

NetCloak vs okhsunrog/vpnhide (the closest competitor)

Capability 🛡️ NetCloak 🧩 okhsunrog/vpnhide
Runs with no root Yes — merged into the target APK via LSPatch / NPatch No — root is mandatory
Runs with root ✅ Yes — LSPosed module ✅ Yes — LSPosed / LSPosed-Next / Vector
Root manager required None (rootless), or just LSPosed Magisk / KernelSU / APatch and one native backend
Native backend Userspace libc inline hooks (ShadowHook), engineered to coexist with LSPatch/NPatch's SigBypass Kernel module (kprobe/kretprobe), KPM, or Zygisk libc hooks — pick exactly one
Catches inline svc #0 / raw syscalls ❌ Documented as out of reach for any userspace module Yes, but only via the kmod/KPM kernel backend (root + supported GKI kernel)
Install footprint One APK (a patched app, or the module) App + LSPosed/Vector + exactly one of kmod/KPM/Zygisk + optional iptables module for loopback ports
System-wide / system_server Rootless: ❌ (can't touch the framework); LSPosed: ✅ per-app scope ✅ hooks system_server at the Binder level
Server-side (GeoIP / DNS / IP reputation) Out of scope — documented honestly Out of scope — documented honestly ("Серверная детекция неисправима на стороне клиента")

The honest verdict. For a rooted device with a supported GKI kernel, vpnhide's kernel backend is genuinely powerful: a kprobe in the kernel intercepts even the inline svc #0 raw syscalls that NetCloak — like any userspace module — documents as beyond its boundary (see bucket C). NetCloak does not claim to out-hook a kernel module on a rooted phone; this project does not overclaim.

What no configuration of vpnhide can do is run on a device without root. It requires a root manager (Magisk/KernelSU/APatch), an LSPosed-class runtime, and a separately-installed kernel/Zygisk backend. NetCloak's decisive differentiator is that the exact same dual-layer engine spans both worlds from one codebase: it deploys rootless by merging into a target APK (LSPatch/NPatch) or rooted as an LSPosed module — with no root manager, no kernel module, and no separate iptables package. On the vast population of phones that will never be rooted, NetCloak is available where vpnhide structurally cannot be.

Both projects converge on the same honest limit — client-side software cannot rewrite a server-side fact (egress-IP GeoIP, DNS resolution, IP reputation). NetCloak states it under Honesty; vpnhide states it as "server-side detection is unfixable on the client." Any module claiming otherwise is overpromising.

The detectors NetCloak is tested against (adversaries, not competitors)

These are detection apps — the yardsticks NetCloak is measured with, not rival hiders:

  • RKNHardering (com.notcvnt.rknhardering) — the primary development adversary: a multi-vector, Roskomnadzor-oriented detector that simultaneously probes Java APIs, raw syscalls, getdents64, AF_NETLINK, sysfs stat(), and a dedicated native probe (libnative_signs_probe.so). NetCloak clears every VPN, interface, and native check it runs — the only residue is region/SIM signals it cannot reach (see the RKN section).
  • cherepavel/VPN-Detector — an open-source detector that reads NetworkCapabilities.TRANSPORT_VPN, enumerates tun0/wg0 interfaces via "native + Java network enumeration," compares active-vs-global VPN state, and queries installed packages (QUERY_ALL_PACKAGES) for known VPN clients — explicitly built to "detect VPN presence even with split tunneling." With NetCloak active the tunnel is concealed on both the Java and the native enumeration paths (before/after).
  • Fing — a mainstream consumer network inspector. Beyond hiding the VPN, NetCloak makes the connection present as a named Wi‑Fi (Home_2.4G) instead of a bare cellular / no-Wi‑Fi state — the Wi‑Fi Realism Engine in action (before/after).

🎛️ The Configuration UI

NetCloak ships a redesigned "liquid glass" configuration app (SettingsActivity, Material 3) — a monospace, iOS-style frosted interface over a soft gradient backdrop, organised as a three-tab shell:

  • Settings — every runtime toggle in Config.kt, laid out as frosted MaterialCardView sections (mirroring the About tab) that cascade in with a staggered entrance animation. Each toggle is a full-width row — title + summary on the left, a MaterialSwitch on the right — that flips when tapped anywhere:
    • Appearance — a twelve-swatch accent-colour picker (violet / blue / indigo / cyan / teal / green / lime / amber / orange / red / pink / magenta) and a segmented theme mode selector (follow system / light / dark). These are cosmetic only.
    • Module Mode — Global vs scoped (TARGET_PACKAGES) hooking.
    • Wi‑Fi Simulation — spoofing master switch, dynamic generation, optional wlan0 fabrication.
    • VPN & Package Hiding — interface hiding, VPN/proxy app hiding, VpnService declaration hiding.
    • Native (libc) Layer — a master switch plus per-hook toggles: read() filter, getdents64 filter, netlink VPN compaction, sysfs-node stat hiding (fstatat/faccessat), /proc/self/maps sanitizer, /proc+/sys net filter, emulator-property hiding. The seven sub-toggles dim and disable whenever the native master switch is off.
    • Diagnostics — a single Show debug logs switch (SHOW_LOGS, off by default) that gates the detailed NcLog.d / NCLOGD diagnostics on both layers at once. See Diagnostics & Logging.
  • Apps — an interactive scoped-mode package picker (search, user/system/selected filters, ABI/split/signature metadata) that writes the target_packages set consumed by ModuleSettings when Global mode is off.
  • About — what the module does, how it works, creator and source links.

A persistent status banner replaces the old Toast: it reports Module active when a real Xposed framework loaded the module (self-activation probe) or the LSPosed world-readable prefs bridge is present, and Standalone mode otherwise — honestly telling rootless LSPatch/NPatch users that on-screen changes cannot reach patched apps.

Design system. The accent is swapped at runtime by applying one of twelve ThemeOverlay.NetCloak.Accent.* overlays via theme.applyStyle(...) before setContentView (plus Activity.recreate() on change); light/dark is driven by AppCompatDelegate.setDefaultNightMode(...). The whole Material 3 type scale is remapped to a bundled monospace face (@font/moni, shipped in res/font/ — no dependency on a system "monospace" alias), cards are translucent (nc_glass_surface) with hairline strokes, section rows reveal with a staggered layoutAnimation, tab swipes carry a subtle fade+scale ViewPager2 page transformer, and the brand mark is a domino mask (disguise / anonymity) that tints to the active accent. The appearance prefs live in the same netcloak_settings file but are never read by ModuleSettings, so they stay rootless-invariant and never affect cloaking.

The world-readable verdict is probed once and memoized as the very first prefs touch in the process (ThemePrefs.isWorldReadable), before anything can open the file MODE_PRIVATE. This closes a SharedPreferences cache-poisoning race — ContextImpl only runs its world-readable checkMode() on the first, uncached open, so a MODE_PRIVATE open that won the race used to make a later MODE_WORLD_READABLE open silently return the cached instance without throwing, falsely reporting Module active on a plain no-root device. HOOK_OPEN_FAMILY is intentionally not exposed — it is a permanently-false const val (see the SigBypass collision above) and must never be toggled on.


🔎 Diagnostics & Logging

By default NetCloak is silent. A shipped or headless patch prints nothing to logcat, so the module leaves no noisy trail in a target app's logs. When you are debugging a device or a stubborn detector, a single switch turns on rich, structured diagnostics across both layers at once.

  • One gate, both layers. Config.SHOW_LOGS (Diagnostics card in the UI, key show_logs) is pushed to the native layer over the JNI bridge, so it governs the Kotlin NcLog.d gate and the libc NCLOGD gate simultaneously. Flip it once; the whole stack starts (or stops) talking.
  • Debug vs error. Diagnostic logs (NcLog.d / NCLOGD) are emitted only when the switch is on. Errors (NcLog.e / NCLOGE) — a hook that fails to install, a null original, a caught exception — are emitted always, regardless of the switch, because they are real faults you must be able to see even on a "silent" build.
  • Everything is tagged. All messages carry a layer prefix — [NetCloak-Java] or [NetCloak-Native] — under the standard Xposed logcat tag, so logcat -s Xposed shows both layers interleaved in one stream.
  • The logs actually say something. Instead of bare "print" markers, the diagnostics record decisions and quantities: which hook fired and on which class, how many packages/interfaces/lines/messages were dropped and from which path, each fd's resolved classification, per-finding stat/access outcomes, the netlink compaction before/after lengths, the detected api_level + version regime, and every version-skipped hook (⏭️ skip Class.method — method absent on API N).
adb logcat -s Xposed

Turn it off for normal use (the default). Leave it on only while diagnosing — then read the [INIT] banner first: it prints the API level, the version regime, the active toggles, and the blocklist sizes, which is usually enough to explain any behaviour before you scroll further.


📸 Screenshots & Showcase

Images live in docs/screenshots/. Filenames below are the exact names the README expects — drop your captures in with these names and the grid fills in automatically.

The proof — RKNHardering, before vs after

❌ Without NetCloak
VPN + interfaces exposed → DETECTED
✅ With NetCloak
every VPN/interface/native check clean → REVIEW
RKNHardering detecting the VPN without the module — verdict DETECTED RKNHardering after NetCloak — every VPN and interface check clean, verdict downgraded to REVIEW on region signals only

Read the verdict carefully. Without NetCloak the verdict is a hard DETECTED driven by live VPN/interface signals. With NetCloak every one of those signals reads clean — Direct signs, Indirect signs, and the native probe all pass — and the verdict downgrades to REVIEW. That remaining REVIEW is not an interface leak: RKNHardering is a Russia-context tool, and it returns REVIEW for any device outside Russia — including a completely clean phone with no VPN installed at all. See why RKNHardering returns REVIEW for the full breakdown.

Real-world scenario — VPN active, but the app sees a clean connection

VPN‑Detector over mobile data — the tunnel is plainly visible without the module, and concealed with it:

Baseline (no hook)
VPN up on mobile data — visible
NetCloak active
same VPN — interface concealed
VPN-Detector showing the VPN tunnel with no hook VPN-Detector after NetCloak conceals the tunnel

Fing network inspector — with the module active the connection even presents as a named Wi‑Fi (Home_2.4G):

Baseline (no hook)
no Wi‑Fi connection reported
NetCloak active
presents as Wi‑Fi Home_2.4G
Fing with no hook — no Wi-Fi connection Fing with NetCloak active — shows Wi-Fi Home_2.4G

Tested against — RKNHardering (com.notcvnt.rknhardering), VPN‑Detector, and Fing. The screenshots above were captured from these apps.

The configuration app — "liquid glass" UI

Settings tab Package picker About tab Accent + theme picker
Settings · card sections Apps · scoped picker About Accents & theme

What each screenshot shows

Capture Source app What it demonstrates
rkn_before.jpg RKNHardering Baseline, no module. Full VPN/interface exposure across Java, syscalls, netlink, and the native probe → a hard DETECTED verdict. This is the bar the module has to clear.
rkn_after.jpg RKNHardering NetCloak active. Direct signs, Indirect signs, and the entire native probe read clean; the verdict downgrades to REVIEW on region/SIM signals alone — the same REVIEW a clean, VPN-less phone outside Russia produces (see the RKN section).
before_hook_vpn_on_mobile_data.jpg VPN-Detector No hook. A VPN is up over mobile data and the tun tunnel is plainly enumerated by the detector.
after_hook_vpn_on_mobile_data.jpg VPN-Detector NetCloak active. The same VPN — the tunnel is gone from both the Java and the native enumeration paths; the app reads a clean, tunnel-free connection.
fing_tools_without_hook.jpg Fing No hook. Fing reports no Wi‑Fi connection (bare cellular / tunnelled state).
fing_tools_with_hook.jpg Fing NetCloak active. The connection presents as a named Wi‑Fi (Home_2.4G) — the Wi‑Fi Realism Engine's fabricated-but-self-consistent radio, not a frozen constant.
ui_settings.jpg NetCloak The Settings tab — frosted MaterialCardView sections, whole-row MaterialSwitch toggles, and the native-layer sub-toggles that dim when the native master switch is off.
ui_apps.jpg NetCloak The Apps tab — the scoped-mode package picker (search + user/system/selected filters, ABI/split/signature metadata) that writes target_packages.
ui_about.jpg NetCloak The About tab — what the module does, how it works, and creator/source links.
ui_accent_theme.jpg NetCloak The Appearance controls — the twelve-swatch accent picker and segmented theme-mode selector. Cosmetic only; these prefs are never read by the hook path, so they stay rootless-invariant.

⚖️ Honesty: What NetCloak Can and Cannot Change

NetCloak is engineered to be maximal at the interface layer, but it does not overpromise:

  • A foreign VPN egress IP is a geolocation fact, not an interface fact. Detectors that resolve your public IP to a country (e.g. RKNHardering's R5_MATRIX verdict engine, where geoHit = geo.outsideRu && !expectedRoamingExit) will still see foreign egress. When geoHit latches true, a clean NOT_DETECTED verdict is mathematically unreachable regardless of how perfectly the interfaces are cloaked; the best attainable is NEEDS_REVIEW. NetCloak cannot relocate your traffic — only a correctly-located exit node can.
  • Residual native interface leaks are verdict-cosmetic for such engines (they fall under indirect/NATIVE_INTERFACE sources), but NetCloak still hardens them because "do everything possible" means leaving no interface-level tell behind.
  • Out-of-scope surfaces remain outside the interception boundary: the kernel and system_server, hardware/firmware, traffic captured off-device, statically-linked custom network stacks, and isolated processes the module never enters.

In short: NetCloak wins every interface-consistency check it can reach, and is transparent about the geolocation check it cannot.


🕵️ RKNHardering: Residual Detections & Why They Are Rootless-Unfixable

The before/after capture tells the real story. With NetCloak active, RKNHardering's Direct signs (TRANSPORT_VPN, HTTP/SOCKS/ProxyInfo proxies, known VPN/proxy apps and VpnService providers) all read clean, its Indirect signs (VPN-interface presence, NOT_VPN capability, MTU, default route → rmnet_data1/rmnet_data2, DNS, technical proxy signs) all read clean, and its IP comparison block passes. Every surface the module can actually reach — Java APIs, libc-routed syscalls, /proc + /sys, netlink, PackageManager — is cloaked.

The native probe (libnative_signs_probe.so) deserves special mention, because it is the surface most single-layer modules leak on and the one this project spent the most effort closing. All of its interface checks now read clean: getifaddrs(), /proc/net/if_inet6, the if_indextoname() 1..N sweep, the sysfs stat() of /sys/class/net/tunX and /proc/sys/net/{ipv4,ipv6}/{conf,neigh}/tunX, the ifindex-consistency cross-check, and the emulator-property reads. The last native tell to fall was native.vpnhide.ifconf_tail — a SIOCGIFCONF interface-list buffer check that does not read the returned list at all but scans the stale tail past ifc_len for a dropped VPN ifreq left behind by a naive length-only prune. NetCloak now zeroes that freed tail (on both the libc ioctl() and the raw syscall(SYS_ioctl) paths), so the buffer is byte-clean, indistinguishable from a device with one fewer IPv4 interface.

So why isn't the verdict a green OK? Because RKNHardering downgrades to REVIEW — and it is more honest to explain that residue than to hide it. What survives is not an interface leak; it is a set of region, network-path, and SIM facts that a rootless userspace module (a rooted one, too) deliberately does not forge.

Why the verdict reads REVIEW — RKNHardering assumes you are in Russia

RKNHardering is not a generic "is there a VPN?" checker; it is a Roskomnadzor-oriented tool whose R5_MATRIX verdict engine blends VPN-interface signals with region signals that assume the device sits on a Russian network. Several of those region checks fire on any device outside Russia — with or without a VPN. The proof is simple, and worth stating plainly:

Install RKNHardering on a stock phone outside Russia with no VPN at all, and it still returns REVIEW.

That is exactly what makes REVIEW a geography verdict rather than an interface verdict. The region signals driving it:

  • Reachability / ICMP expectation — RKN expects hosts that are censored inside Russia (e.g. instagram.com) to be unreachable on a genuine Russian route. On a normal Indian/European/US route they answer immediately, which does not match RKN's "inside-Russia" baseline.
  • GeoIP — it resolves your public egress IP to a country and expects Russia; any non-RU exit (here, a US hosting ASN — Zenlayer) latches geoHit.
  • Location / SIM — it reads the modem's MCC/MNC and expects a Russian carrier; MCC 405 (India / Jio) is not it.

None of these name a tun/wg interface, a VPN route, a TRANSPORT_VPN capability, or a VPN package — every one of which reads clean with NetCloak active. They are properties of where your traffic exits the internet and which SIM is in the phone, which is not something a network-interface cloak forges. In short: NetCloak flips every check it can actually reach from DETECTED to clean; what is left is RKNHardering telling you the phone is not in Russia. The categories below detail exactly those region/policy signals.

A. Network-path and SIM facts, not interface facts — GeoIP + Location

The categories RKN still flags here are GeoIP and Location, and both are properties of your connection and hardware, not of any interface NetCloak can hook:

  • GeoIP — RKN resolves your public egress IP to a country and latches when the exit is outside the expected region (geoHit = geo.outsideRu && !expectedRoamingExit). A hosting-provider or proxy-listed egress (e.g. a US datacenter ASN) reads as "not the final server address / public-IP-only." No tun/route/getifaddrs hook changes where packets actually leave the internet — that is a routing/geolocation truth outside the interception boundary. Only a correctly-located exit node produces a clean result.
  • Location — the SIM's MCC/MNC and carrier (e.g. MCC 405 = India / Jio) are read from the modem; they are what they are. A network-cloaking module does not — and should not — forge the SIM identity, so a mismatch between the SIM's country and the claimed exit region is visible by design.

B. RKN's own privileged probes are sandboxed away — and that works for us

RKN itself reports that "checks for processes, iptables/pf, and system certificates are limited without root/privileged access," and that dumpsys vpn_management and dumpsys activity services VpnService are unavailable. Those lines are the Android app sandbox denying the detector: an untrusted_app-domain process cannot ptrace other apps, read another uid's /proc, or invoke privileged dumpsys (Android SELinux). The same sandbox that stops NetCloak from touching the kernel also stops RKN from reaching the evidence — so these are not NetCloak leaks; they are checks the OS refuses to run for an unprivileged app.

C. Kernel-policy truths and off-boundary footprints

The theoretical residual surface — the reason a rootless module can never promise 100% — is that NetCloak hooks userspace libc, so anything that steps around libc, or that the OS decides on the app's behalf, steps around NetCloak too:

  • bind_probe vs RTM_GETLINK mismatch (flagged needs-review, not a hard VPN tell) — on API 30+ the OS blocks RTM_GETLINK for non-system apps (AOSP b/155595000), so the netlink link-count reads 0 while a bind-based count sees the real interfaces. This discrepancy is present on a stock, unmodified device at the same API level; it is an OS-policy artifact, names no VPN interface, and is not something a rootless module can reconcile (it can neither un-block RTM_GETLINK nor should it forge bind semantics).
  • A local proxy on loopback — if you run a proxy/VPN client app (e.g. Happ listening on 127.0.0.1:8080 / SOCKS 127.0.0.1:44966), RKN can connect-probe or enumerate that loopback listener. That socket belongs to a different process than the one NetCloak hooks; concealing it is a separate problem (resetting loopback connect probes and filtering those ports from /proc/net/tcp{,6}), not VPN-interface cloaking, and it is not what drives the verdict.
  • Inline svc #0 syscalls / direct mmap of /dev/__properties__ / system_server decisions — reach the kernel without touching the libc wrapper or the libc syscall() dispatcher NetCloak hooks. Intercepting those needs seccomp-BPF (the zygote owns that policy; seccomp filters) or ptrace (blocked between apps by Yama ptrace_scope) — both root-only.

Closing any of bucket C requires being the process launcher, a kernel LSM, or ptrace-capable — i.e. root. On the devices NetCloak targets (rootless NPatch/LSPatch, SELinux enforcing) it is architecturally out of reach, and pretending otherwise would be the kind of overclaim this project deliberately avoids.


🔧 Configuration Reference

All defaults live in Config.kt. Runtime toggles are var (compile-time value = default; overridable by the UI on root); the one permanently-disabled switch is a const val.

Key Default Purpose
GLOBAL_MODE true Hook every process, or only TARGET_PACKAGES
MOCK_NETWORK_WIFI true Forge the Wi‑Fi environment (Java layer)
MOCK_VPN_HIDE true Hide VPN interfaces/routes/transports
ENABLE_NATIVE_HOOKS true Master switch for the libc layer
HIDE_VPN_PACKAGES true Hide VPN/proxy apps from PackageManager
HIDE_VPN_SERVICE_DECLARATIONS true Hide VpnService-declaring apps
HOOK_READ_FILTER true read()-based content sanitization
HOOK_GETDENTS true getdents64() VPN entry filter
HOOK_NETLINK true Call-site-split netlink transform: getifaddrs() dumps pass through untouched (name-pruned afterward), raw-probe dumps physically compacted
HOOK_STAT_FAMILY true Hide sysfs VPN nodes via fstatat/faccessat (ENOENT)
SANITIZE_PROC_MAPS true Strip module markers from maps/mounts
FILTER_PROC_NET true Hide VPN from /proc+/sys net paths
SPOOF_EMULATOR_PROPS true Report QEMU/emulator props as absent
SHOW_LOGS false Emit detailed debug logs on both layers (NcLog.d/NCLOGD); errors (NcLog.e/NCLOGE) are always logged regardless
USE_DYNAMIC_GENERATION true Fresh realistic Wi‑Fi values per launch
FABRICATE_WIFI_INTERFACE false Inject a synthetic wlan0. Off by design — on any device that already exposes a real wlan0, a second fabricated one creates a native-vs-JVM index mismatch and an unnatural interface count (both are tells). Enable only for a genuinely Wi‑Fi‑less environment (e.g. an emulator with no wlan0).
HOOK_OPEN_FAMILY false (const) Permanently off — collides with SigBypass

🔨 Building From Source

Requirements: Android Studio, JDK 11+, Android SDK (min 26 / target 35), Android NDK, CMake 3.22+.

git clone https://github.com/acessrdpgg/NetCloak-Xposed.git
cd NetCloak-Xposed
./gradlew assembleDebug     # or assembleRelease

The build is ARM-only by design — ShadowHook's inline engine supports thumb/arm32/arm64, and x86 is excluded via abiFilters:

ndk { abiFilters.addAll(listOf("arm64-v8a", "armeabi-v7a")) }

Output: app/build/outputs/apk/debug/app-debug.apk. Install as an Xposed module in LSPosed/LSPatch/NPatch and (LSPosed only) open NetCloak · Configuration to tune it.

Which APK to ship. The debug APK (~8 MB) is unminified — use it for local testing. The release APK is what you publish: assembleRelease runs R8 tree-shaking + resource shrinking (obfuscation is deliberately disabled so string/JNI-resolved entry points are never renamed) and lands at ~4 MB. It is unsigned by default — sign it in Android Studio (Build ▸ Generate Signed App Bundle / APK ▸ APK) before attaching it to a release.

📊 Compatibility Matrix

Environment Java / ART Native libc Configuration
LSPosed (root) Live UI (world-readable overlay)
LSPatch (rootless) Compile-time defaults (UI inert)
NPatch (rootless) Compile-time defaults (UI inert)
arm64-v8a / armeabi-v7a
x86 / x86_64 ⚠️ excluded Native lib not built

Native concealment is contingent on a supported ARM ABI, successful libmockenv.so load, and ShadowHook init. Individual OEM builds or apps may require additional work.

📝 Changelog

v2.0.0 — The first major release since v1.0.0 — a near-total rebuild of the project. Native-layer correctness pass: fixed the root cause behind every lingering native-probe leak: each ShadowHook SHARED-mode proxy now calls SHADOWHOOK_STACK_SCOPE() as its first statement. Without it the hub pushed each hook point onto a per-thread reentrancy stack that was never popped, so a proxy ran once and then every later call was routed straight to the real libc original — a silent self-disable (not a crash) that let getifaddrs//proc/net/if_inet6/sysfs-stat/if_indextoname/ifindex/emulator-property checks keep leaking after the first hit. With the guard in place the whole native interface surface reads clean. Also closed native.vpnhide.ifconf_tail: the SIOCGIFCONF prune shortened ifc_len but left the dropped VPN ifreq bytes in the freed tail, which the probe recovers by scanning past ifc_len; the freed region is now zeroed, and the prune is shared between the libc ioctl() hook and a new raw syscall(SYS_ioctl) case so a raw-syscall variant can't dodge it. Hardened that same ioctl() hook against a crash in ioctl-heavy apps (network scanners such as Fing): the VPN-name check now runs only for the SIOCGIF* requests that actually carry a struct ifreq, and reads the name through a bounded IFNAMSIZ copy — so requests whose argument is a bare int* (FIONREAD, FIONBIO, the TCP_* family, …) are no longer mis-read as an interface name, eliminating the out-of-bounds read that previously SIGSEGV'd those apps on launch. Added an ungated [INIT] libc hooks live: N/M install-coverage summary so a single startup capture proves how many symbols patched in the target process. Documented the remaining rootless-unfixable residues (egress-IP GeoIP, SIM MCC/carrier, the API 30+ bind/RTM_GETLINK OS-policy mismatch, and any local loopback proxy client). Confirmed on-device: with the guard in place RKNHardering's Direct signs, Indirect signs, and the entire native probe read clean and the verdict drops from DETECTED to REVIEW — the residual REVIEW being RKN's Russia-context region checks (GeoIP/MCC/reachability), which fire even on a clean, VPN-less phone outside Russia. Documentation expanded: a head-to-head comparison against the root-only okhsunrog/vpnhide, VPN-Detector and Fing adversary walkthroughs, per-screenshot descriptions, and a full breakdown of why the verdict reads REVIEW.

Also in v2.0.0 — the architecture rebuild. Native strategy rebuilt around SigBypass coexistence: read/getdents64/netlink/syscall-dispatcher hooks replace the frozen open() path; the netlink layer settled on a call-site-split transform — inside getifaddrs() the dump is passed through byte-for-byte and the finished ifaddrs list is name-pruned afterward, while every raw-probe path is physically compacted (survivors slide down, a genuinely shorter length is returned) — after two earlier dead ends were retired: a rename-to-eth0 that manufactured a phantom eth0 (native-vs-JVM mismatch), then a length-preserving NLMSG_NOOP retype that orphaned the tunnel's RTM_NEWADDR and faulted in the ioctl(SIOCGIFNAME) recovery path; a MSG_TRUNC netlink over-read that faulted (nl_walk_len now clamps the walk to buffer capacity) is fixed; fstatat/faccessat sysfs-node hiding closes the stat()-on-tunX leak; emulator-property hiding; full Wi‑Fi realism engine (organic SSIDs, vendor-OUI BSSIDs, band-consistent RF, live RSSI jitter, scan neighbourhood, extended WifiInfo) with wlan0 fabrication kept off by default (a synthetic wlan0 on a device that already has a real one manufactures a native-vs-JVM index mismatch and an unnatural interface count — the exact tells earlier builds regressed on). UI overhaul: a "liquid glass" tabbed shell (Settings / Apps / About), the Settings screen rebuilt as hand-authored MaterialCardView sections with whole-row MaterialSwitch toggles and native-master dependency dimming, an interactive scoped-mode package picker that now surfaces each app's on-disk APK size (base + splits) alongside its signature, ABI/bitness and split layout, twelve runtime accent colours, a segmented theme-mode selector, a bundled monospace face (@font/moni) app-wide, an About tab that reads its version straight from BuildConfig (live versionName · build versionCode) and carries a new Key Features summary card, and staggered section + page-transition animations. A memoized world-readable probe fixes a SharedPreferences cache-poisoning race that falsely reported "Module active" on no-root devices. Release-engineering pass: dead Instagram-mock leftovers and their DexKit dependency were excluded from the build, unused deps (gson, androidx.preference) removed, R8 shrinking + resource shrinking enabled (obfuscation deliberately off so the string/JNI-resolved entry points can never be renamed), and native code compiled -Oz with dead-section GC — cutting the shipped release APK to ~4 MB — down from the 93.81 MB v1.0.0 release. Documentation rewrite.

v1.0.0 — Initial dual-layer release (Java hooks + native getifaddrs/ioctl/if_*).

📚 References

Tooling & runtimes

  • ShadowHook — ByteDance inline-hooking engine (thumb/arm32/arm64).
  • LSPosed · LSPatch — Xposed-compatible runtimes (rooted and rootless).
  • XposedBridge — original Xposed Java API.

Detectors used as adversaries

  • RKNHardering — the primary multi-vector development adversary (Java + syscall + netlink + native probe).
  • cherepavel/VPN-Detector — open-source TRANSPORT_VPN + interface-enumeration detector.
  • Fing — mainstream consumer network inspector.

Comparable modules

⚖️ License

Released under the MIT License — see LICENSE.

Disclaimer. NetCloak is provided for education, authorized security auditing, application testing, and network interoperability research. The author (@acessrdpgg) assumes no liability for misuse. Do not use it to bypass authorization, access controls, fraud controls, or any security mechanism on systems you do not own or lack explicit permission to test.

About

Dual-layer (ART + native libc) Android Xposed module that hides VPN interfaces, forges a Wi-Fi environment, and cloaks VPN/proxy apps — rootless (LSPatch/NPatch) or rooted (LSPosed).

Topics

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages