Commit graph

10 commits

Author SHA1 Message Date
okhsunrog
c059c64582 fix(zygisk): cover libc socket-interface binds 2026-07-13 21:57:05 +03:00
okhsunrog
bca81b80e7 fix: block socket binds to hidden VPN interfaces 2026-07-13 20:22:51 +03:00
Danila Gornushko
1582efa067
docs: refresh installation/backend docs + 1.0.0 screenshots (#224)
* docs: refresh installation and backend docs

* docs: fix review findings and add KPM to update-json

Cross-checked the docs refresh against the code and corrected:
- protocol.md: config parser is vpnhide_parse_config (not the legacy
  vpnhide_parse_target_uids); path is kmod/shared/vpnhide_logic.h; drop the
  unverified 0x2026 KPatch-Next marker (activator drives it via the CLI).
- zygisk/README: module declares Zygisk API v2 (not v5); add the recv hook
  row and the if<N> renamed-netdev match.
- README (en/ru): live-check counts are 13 native / 12 Java; Diagnostics is
  under Settings, not a tab; mention the Statistics tab; note KPM beta parity.
- detection-vectors: KPM host-route helper is kpm_is_public_host_route{4,6};
  restore CONFIG_KPROBES and the Zygisk qualifiers; codegen renders four targets.
- storage.md: legacy files are no longer user-managed (migration inputs), not
  'gone'. CLAUDE.md: activator has four bins (kmod/kpm/zygisk/ports).
- development/ROADMAP/kmod/lsposed/portshide READMEs + AGENTS.md: assorted
  accuracy fixes (KPM needs a KernelPatch runtime, per-KMI kmod zip name, etc.).
- CONTRIBUTING.md: clang-format uses scripts/clang-format-c.sh (covers KPM src).
- portshide customize.sh: 'Hiding -> Ports' (renamed tab).
- update-json.sh: emit update-kpm.json so KPM gets Magisk/KSU update checks.

* docs: correct diagnostics check count (25, not 26 vectors)

* docs: refresh README screenshots for the 1.0.0 UI

Replace the v0.5.3-era screenshots (old per-tab Protection UI) with a
new 1.0.0 set: VPN-hidden dashboard, the unified Hiding list with
J/N/A/P, the Statistics tab + per-hook breakdown, per-app hook selection,
diagnostics, settings, and the community screen. Extra shots
(statistics-apps, settings-advanced, diagnostics-java, settings-developer)
are included for the release notes.

* docs: fix the how-it-works layer breakdown

The old 'Layer 3 — additional app-level controls' bucket was wrong:
interface hiding is what Layers 1+2 already do, app-hiding is done by the
LSPosed/Java layer (system_server PackageManager hooks), and ports is a
separate module. Fold app-hiding into Layer 1 (LSPosed) and make Layer 3
the standalone portshide module.

* docs: conceptual-coherence pass on the 5 core docs

Second review pass, this time for architectural coherence rather than
code-facts. Fixes:
- README (en/ru): 'What vpnhide hides' stopped reifying interface hiding
  as a standalone 'layer'/'role' and now ties the three hiding goals to
  the four J/N/A/P roles (interface = Java + Native, toggled independently)
- README (ru): add the KPM-beta hook-parity caveat (parity with the EN side)
- protocol.md: app-hiding is a normal lsposed mask bit, not a separate
  'package/observer records' entry in the §6 profile table
- detection-vectors.md: a complete install is two components (native + lsposed),
  not 'two roles'; name both lsposed jobs
- storage.md: scope the text protocol to the config wire (LSPosed reuses the
  format for its state file); 'by function' not 'by role'; note the Apps role
  shares the same resolver; heading no longer files ports under 'Native layer'
2026-07-02 00:03:17 +03:00
Danila Gornushko
db77076d0d
fix(kmod): hide VPN ifaces from the SIOCGIFCONF size query, not just the fill (#213)
Some checks failed
CI / kmod-qemu (android12-5.10) (push) Blocked by required conditions
CI / kmod-qemu (android13-5.10) (push) Blocked by required conditions
CI / kmod-qemu (android13-5.15) (push) Blocked by required conditions
CI / kmod-qemu (android14-5.15) (push) Blocked by required conditions
CI / kmod-qemu (android14-6.1) (push) Blocked by required conditions
CI / kmod-qemu (android15-6.6) (push) Blocked by required conditions
CI / kmod-qemu (android16-6.12) (push) Blocked by required conditions
CI / kpm-qemu (android12-5.10) (push) Blocked by required conditions
CI / kpm-qemu (android13-5.10) (push) Blocked by required conditions
CI / kpm-qemu (android13-5.15) (push) Blocked by required conditions
CI / kpm-qemu (android14-5.15) (push) Blocked by required conditions
CI / kpm-qemu (android14-6.1) (push) Blocked by required conditions
CI / kpm-qemu (android15-6.6) (push) Blocked by required conditions
CI / kpm-qemu (android16-6.12) (push) Blocked by required conditions
CI / kpm-qemu-legacy (4.14) (push) Blocked by required conditions
CI / kpm-qemu-legacy (4.19) (push) Blocked by required conditions
CI / kpm-qemu-legacy (5.4) (push) Blocked by required conditions
CI / kpm (push) Blocked by required conditions
CI / zygisk (push) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Blocked by required conditions
CI / release (push) Blocked by required conditions
QEMU Test Images / build-push (android12-5.10) (push) Has been cancelled
QEMU Test Images / build-push (android13-5.10) (push) Has been cancelled
QEMU Test Images / build-push (android13-5.15) (push) Has been cancelled
QEMU Test Images / build-push (android14-5.15) (push) Has been cancelled
QEMU Test Images / build-push (android14-6.1) (push) Has been cancelled
QEMU Test Images / build-push (android15-6.6) (push) Has been cancelled
QEMU Test Images / build-push (android16-6.12) (push) Has been cancelled
QEMU Test Images / build-legacy (push) Has been cancelled
The classic two-step SIOCGIFCONF enumeration first calls with ifc_req == NULL
to learn the buffer size (the kernel returns ifc_len = N * sizeof(struct ifreq)
without copying anything), then calls again with a sized buffer. sock_ioctl_ret
filtered the fill but bailed on the NULL probe, so the size query returned the
unfiltered count — a target comparing the two back-to-back saw a one-interface
discrepancy that revealed a hidden VPN.

sock_ioctl_ret now also handles ifc_req == NULL: it rcu-enumerates the socket's
netns netdevs (net captured at entry from the socket, matching dev_ifconf's
sock_net) and subtracts sizeof(struct ifreq) per IPv4-addressed VPN iface,
matched by ifa_label — the exact set and naming inet_gifconf would have emitted
— so the size query agrees with the filtered fill.

Validated by a new harness vector (ifconf_size_probe, via kmod/test/ifconf-probe.c)
asserting a target's size query equals its filtered fill and drops below the
non-target size query: 19/19 PASS, no panic, on GKI 6.1 and 6.6 in QEMU; CI
covers the remaining KMIs once the ddk-qemu image rebuilds with the new probe.

The 32-bit compat SIOCGIFCONF path (compat_sock_ioctl) remains unhooked and is
tracked in docs as low priority. The KPM backend's filter_ifconf still bails on
the NULL probe (raw netdev walking by per-kver offset is not worth it for the
legacy probe) and is noted as WIP parity.
2026-06-30 15:52:13 +03:00
Danila Gornushko
2ddab4124f
kmod kernel-behavior: host-route parity, config race, shared compactor, stats-slot race (#210)
* fix(kmod): bound KPM ctl0 reply and reject overflowing version

- KPM ctl0 stats/status: vpnhide_format_* report the full intended length,
  which can exceed the 4096-byte stack buffer; clamp to sizeof(buf) before
  clamp_to_line/copy_to_user so a generous outlen can't drive an out-of-bounds
  read of the kernel stack (info leak / panic). The .ko already bounds this way.
- vpnhide_tok_decimal: reject values that overflow unsigned long instead of
  wrapping mod 2^64 and slipping past the version fuse — same contract as the
  hex token parser.

Host shared-logic and protocol-vector tests pass.

* fix(kpm): hide the public IPv4 host-route; share the predicates with the .ko

The KPM route hooks only hid a route when its output device was itself a VPN
interface, missing the public /32 host-route a VPN client pins to the physical
uplink to reach the server — the .ko hides it, the KPM did not, breaking the
backend-parity contract and leaking the VPN server's address through a
routing-table dump even with the tun interface hidden.

Rather than duplicate the .ko's predicates into the KPM, lift the pure
address/iface logic into shared/vpnhide_logic.h — vpnhide_is_public_ipv4
(network-order bytes), vpnhide_is_public_ipv6, and vpnhide_iface_is_physical
(self-contained case-insensitive prefix match) — so BOTH backends share one
implementation, the way the rest of the filtering logic already is. The .ko and
the KPM now call the shared predicates; each keeps only its kernel-struct read.
Host unit tests cover the predicates (test_vpnhide_logic.c), and the QEMU
harness gains a public host-route vector on BOTH backends (init.sh + init-kpm.sh
/ run-kpm.sh).

IPv4 needs no per-kver offset: fib_rt_info.dst/dst_len sit at a constant +12/+16
on every 5.6+ kernel (verified against the GKI sources), and the fri is
stack-resident so the reads are in-bounds. Validated A/B in QEMU — the .ko on
android12-5.10 (freshly DDK-built) and the KPM on android12-5.10 + android16-6.12
(hostroute4 hidden for the target, 17/17, no panic).

The IPv6 /128 analogue in the KPM is left a documented TODO: it needs the
per-kver fib6_info.fib6_dst offset, which varies. The .ko already hides it and
now also uses the shared vpnhide_is_public_ipv6.

* feat(kpm): also hide the public IPv6 host-route via a physical uplink

Completes the host-route parity with the .ko. The KPM now hides a public /128
host-route pinned to a physical uplink (the IPv6 analogue of the /32 just added),
the way the .ko's is_public_host_route6_via_physical does, using the shared
vpnhide_is_public_ipv6 / vpnhide_iface_is_physical predicates.

Unlike IPv4, this needs a per-kver offset: fib6_info.fib6_dst (rt6key { addr@0;
int plen@16 }) sits at +64 on 5.10/5.15/6.1/6.6 and +80 on 6.12 (the new
gc_link), derived from the GKI sources and the existing fib6_info offsets in the
table. A new fib6_info_fib6_dst field carries it; it is 0 (disabled) on the
pre-fib6_info 4.14 rt6_info path and on the non-GKI 5.4/4.19 kernels the QEMU
matrix can't validate — a wrong offset would fault, and the table's doctrine is
proven-not-guessed.

Validated A/B in QEMU on every distinct GKI offset struct — KPM on
android12-5.10, android13-5.15, android14-6.1, android16-6.12 and the .ko on
android12-5.10 (new hostroute6 vector hidden for the target, 18/18, no panic).
android15-6.6 shares the 6.1 struct and is covered by CI's per-KMI matrix.

* fix(kpm): publish config via a lock-free double-buffer, not in place

vpnhide_kpm_ctl0 parsed a CONFIG payload straight into the live targets[] array
and only then updated nr_targets, while every hooked syscall reads that same
array lock-free via hook_active() on other CPUs. A hook firing mid-update could
read a torn target (new uid with stale hookmask, or a half-overwritten entry
against the old, larger count) — momentarily filtering the wrong app: a target's
VPN briefly visible, or a non-target briefly hidden, on every config replace. The
.ko avoids this by parsing into a staging buffer and publishing under a spinlock;
the KPM had dropped both.

KP has no kernel spinlock/RCU, so mirror it lock-free: hold the config in a
two-element double-buffer, parse each update into a stack-local staging set, fill
the inactive buffer, and publish it with a single release-store of the active
index. hook_active() acquire-loads that index once and reads only that snapshot,
so the mask gate and the per-uid scan are always from one consistent config —
never a mix. Writers are the serialized control path (ctl0 / init load-args), so
they never pick the same inactive buffer.

Validated in QEMU on android12-5.10 (kver-independent — no offsets involved):
all hide/keep vectors still pass (18/18, no panic), exercising the rewritten
hook_active hot path.

* refactor(kmod): share the seq compactor and add a lock-free hook_active gate

Two .ko changes for parity with the KPM, no behavior change.

#29: fib_route_ret / ipv6_route_ret hand-rolled the /proc/net/route line
compaction that shared/vpnhide_logic.h already provides as
vpnhide_compact_seq_lines — the exact function the KPM calls for the same hooks.
The header even documents the inline copies as "intentionally byte-identical" to
the shared one. Replace both with the shared call (FIELD_FIRST / FIELD_LAST), so
the compaction lives in one place instead of three.

#30: hook_active() called target_mask() unconditionally, taking the global
targets_lock spinlock on every hooked syscall (e.g. every socket ioctl) even
with no targets configured. Add an active_hook_mask — the OR of all targets'
hookmasks, recomputed under targets_lock on each config apply — and short-circuit
hook_active() on a lock-free read before locking, matching the KPM's fast path.

Validated A/B in QEMU on android12-5.10 (freshly DDK-built): proc_route_v4/v6
hidden for the target, non-VPN routes preserved exactly (compactor output
byte-identical), 18/18, no panic.

* fix(kpm): avoid double-allocating a stats slot for one uid under a CAS race

stats_slot_for_uid() could hand the same uid two slots, splitting its per-hook
counters across two stats lines. Two races: a concurrent claimer mid-init (slot
state 2, uid not yet readable) was skipped by the second scan, which then
allocated a fresh slot for the same uid; and on a lost CAS the code moved on to
the next slot instead of re-checking the one the winner took.

Wait out the transient init state (state 2) so the uid becomes readable before
deciding, and re-examine a slot after losing its CAS instead of skipping ahead.
A residual window remains only if two first-hits for one uid claim two different
free slots at the same instant — that just splits a counter, never a crash; a
perfect lock-free find-or-insert needs a lock KP deliberately does without.

Validated in QEMU on android12-5.10: stats vectors unchanged, 18/18, no panic.

* docs: record the two narrow SIOCGIFCONF gaps (size-query, 32-bit compat)

* fix(kpm): seqlock config instead of double-buffer; gate host-route on GKI 5.6+

CI surfaced three issues in the kmod-behavior batch:

- The lock-free double-buffer (two copies of targets[]) grew the .kpm enough that
  the patched android16-6.12 image stopped booting (KP embeds the KPM into a
  reserved kernel region with limited headroom; 6.12 had the least, 5.10 was
  fine). Replace it with a seqlock: a writer bumps cfg_seq odd, mutates targets[]
  in place, bumps it even; the hook reader retries while odd / on a concurrent
  change. Same torn-read protection (#26/#28/#31), one extra word instead of a
  second targets[]. In-place parse is safe — vpnhide_parse_config only writes
  after a valid header, so a reject-whole leaves the old config intact.
  Re-validated A/B in QEMU: KPM boots and passes on 5.10 and 6.12 (18/18).

- The new hostroute4/hostroute6 vectors failed on the non-GKI legacy kernels
  (4.14/4.19/5.4), where the host-route is intentionally disabled (IPv4 needs the
  5.6+ fib_rt_info path; IPv6 needs a per-kver fib6_dst offset only populated for
  GKI). Gate them in init-kpm.sh on the running kernel's version (>= 5.6) and
  emit a SKIP marker that run-kpm.sh honors otherwise.

- clang-format the two new hot-path blocks with the pinned 18.x formatter.
2026-06-30 10:10:12 +03:00
okhsunrog
31f51a81aa fix(lsposed): avoid app-hiding self-lookups 2026-06-29 16:12:58 +03:00
okhsunrog
b5790766e7 feat(storage): migrate app to canonical config 2026-06-28 18:27:51 +03:00
okhsunrog
16dfc70c5a docs + cleanup: legacy protocol references and node names
Finish the migration: docs/state.md, docs/protocol.md, detection-vectors.md and
the kmod/lsposed READMEs now describe the folded /proc/vpnhide_ctl node and the
`vpnhide 1 config` wire on every channel; the kmod ABI section documents
config-in / status+stats-out. The KPM exit drops the dead remove_proc_entry
calls (it has no /proc node — its channel is ctl0). The shared header notes that
the decimal vpnhide_parse_target_uids is the legacy load-args/host-test path,
not the protocol channel. Changelog fragment added.

Refs #23.
2026-06-28 14:54:35 +03:00
okhsunrog
ebd4e84ef6 Clarify kmod and zygisk are alternatives, not stacked
kmod and zygisk are two implementations of the same native layer (install
one, not both); a complete setup is one native layer plus lsposed. The
earlier wording said 'install all three', which is wrong.
2026-06-26 13:58:12 +03:00
okhsunrog
00135ddf8b Add detection-vector coverage map and fix README coverage table
Consolidate "which app probe is hidden by which component" into a single
authoritative doc, and correct the README coverage table, which marked the
netlink path as SELinux-blocked and predated the issue #86 fix.

- Add docs/detection-vectors.md: threat model, the kmod/zygisk/lsposed/SELinux
  layers, a per-vector coverage matrix (interface enum, routes, socket procfs,
  framework Java APIs, package visibility), importance ranking, known gaps, and
  where each layer is defined in code
- Fix README.md / README.en.md table: netlink RTM_GETLINK/GETADDR/GETROUTE are
  not SELinux-restricted; RTM_GETROUTE is now covered by kmod and zygisk;
  RTM_GETADDR/ipv6_route/tcp coverage corrected; rewrite the conclusion to
  explain the netlink path and per-device SELinux variance
- Note that SELinux policy differs across devices, ROMs, and Android versions,
  so the layers never rely on it
- Cross-link the new doc from CLAUDE.md, ROADMAP.md, and state.md
2026-06-26 13:11:57 +03:00