Commit graph

693 commits

Author SHA1 Message Date
okhsunrog
38674c0fd6 ci: provide current bind probe to QEMU jobs 2026-07-13 22:01:17 +03:00
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
5cbe367787
Merge pull request #243 from okhsunrog/refactor/architecture-boundaries
Some checks failed
CI / kmod-qemu (android13-5.15) (push) Has been cancelled
CI / kmod-qemu (android14-6.1) (push) Has been cancelled
CI / kpm-qemu-legacy (4.19) (push) Has been cancelled
CI / kpm-qemu (android12-5.10) (push) Has been cancelled
CI / kpm-qemu (android14-5.15) (push) Has been cancelled
CI / kpm-qemu (android14-6.1) (push) Has been cancelled
CI / kpm-qemu (android15-6.6) (push) Has been cancelled
CI / kpm-qemu (android16-6.12) (push) Has been cancelled
CI / kmod-activator (push) Has been cancelled
CI / kmod (android12-5.10) (push) Has been cancelled
CI / kmod (android13-5.10) (push) Has been cancelled
CI / kmod (android13-5.15) (push) Has been cancelled
CI / kmod (android15-6.6) (push) Has been cancelled
CI / kmod (android16-6.12) (push) Has been cancelled
CI / kmod-qemu (android12-5.10) (push) Has been cancelled
CI / kmod-qemu (android13-5.10) (push) Has been cancelled
CI / kpm (push) Has been cancelled
CI / kmod-qemu (android14-5.15) (push) Has been cancelled
CI / kpm-qemu-legacy (4.14) (push) Has been cancelled
CI / kpm-qemu-legacy (5.4) (push) Has been cancelled
CI / kmod-qemu (android15-6.6) (push) Has been cancelled
CI / kmod-qemu (android16-6.12) (push) Has been cancelled
CI / kpm-qemu (android13-5.10) (push) Has been cancelled
CI / kpm-qemu (android13-5.15) (push) Has been cancelled
CI / kmod (android14-5.15) (push) Has been cancelled
CI / kmod (android14-6.1) (push) Has been cancelled
CI / zygisk (push) Has been cancelled
CI / lsposed (push) Has been cancelled
CI / portshide (push) Has been cancelled
CI / release (push) Has been cancelled
refactor: separate activation and config responsibilities
2026-07-13 19:07:15 +03:00
okhsunrog
a29f26be7d refactor: separate activation and config responsibilities 2026-07-10 11:31:38 +03:00
okhsunrog
04f772e7b0 fix: close native backend race and filtering gaps 2026-07-10 10:21:29 +03:00
okhsunrog
8d129236cd chore: update-json for v1.1.1
Some checks failed
CI / kmod-activator (push) Has been cancelled
CI / kmod (android12-5.10) (push) Has been cancelled
CI / kmod (android13-5.10) (push) Has been cancelled
CI / kmod (android13-5.15) (push) Has been cancelled
CI / kmod (android14-5.15) (push) Has been cancelled
CI / kmod (android14-6.1) (push) Has been cancelled
CI / kmod (android15-6.6) (push) Has been cancelled
CI / kmod (android16-6.12) (push) Has been cancelled
CI / kmod-qemu (android12-5.10) (push) Has been cancelled
CI / kmod-qemu (android13-5.10) (push) Has been cancelled
CI / kmod-qemu (android13-5.15) (push) Has been cancelled
CI / kmod-qemu (android14-5.15) (push) Has been cancelled
CI / kmod-qemu (android14-6.1) (push) Has been cancelled
CI / kmod-qemu (android15-6.6) (push) Has been cancelled
CI / kmod-qemu (android16-6.12) (push) Has been cancelled
CI / kpm-qemu (android12-5.10) (push) Has been cancelled
CI / kpm-qemu (android13-5.10) (push) Has been cancelled
CI / kpm-qemu (android13-5.15) (push) Has been cancelled
CI / kpm-qemu (android14-5.15) (push) Has been cancelled
CI / kpm-qemu (android14-6.1) (push) Has been cancelled
CI / kpm-qemu (android15-6.6) (push) Has been cancelled
CI / kpm-qemu (android16-6.12) (push) Has been cancelled
CI / kpm-qemu-legacy (4.14) (push) Has been cancelled
CI / kpm-qemu-legacy (4.19) (push) Has been cancelled
CI / kpm-qemu-legacy (5.4) (push) Has been cancelled
CI / kpm (push) Has been cancelled
CI / zygisk (push) Has been cancelled
CI / lsposed (push) Has been cancelled
CI / portshide (push) Has been cancelled
CI / release (push) Has been cancelled
2026-07-05 15:22:27 +03:00
okhsunrog
e6723861bf chore: release v1.1.1 2026-07-05 15:11:38 +03:00
Danila Gornushko
0a1870ce50
Merge pull request #240 from okhsunrog/fix/lsposed-connectivity-hook-attach
fix(lsposed): attach ConnectivityService hooks on OEM ROMs via deferred getService
2026-07-05 15:09:38 +03:00
okhsunrog
f4750b8a99 chore(lsposed): keep the nameResolvesSame classloader guard as permanent telemetry 2026-07-05 15:00:08 +03:00
okhsunrog
112a71cd35 docs: hook binder.javaClass directly (name-resolution can hit a parent-loader copy) 2026-07-05 14:53:32 +03:00
okhsunrog
d6b29e5d81 fix(lsposed): hook ConnectivityService by the binder's own class, not by name
On MediaTek A11 the live ConnectivityService is defined by a child classloader;
resolving the class by name through that loader follows delegation to a parent
copy — a different Class object with the same name. Hooks attached to it cleanly
(every method matched, mask full) yet never fired, because the live binder
dispatches to the child copy. Tell: NetworkInfo.writeToParcel still fired on the
result of getNetworkInfo(TYPE_VPN) while our getNetworkInfo method hook did not —
the served method was a different object than the hooked one.

Hook binder.javaClass directly (the live instance's actual class) instead of
re-resolving by name. Add a nameResolvesSame flag to cs_attempts to confirm the
delegation mismatch from a report.
2026-07-05 14:50:06 +03:00
okhsunrog
709297dee5 fix(lsposed): attach ConnectivityService only from the live binder
Path A (resolve + hook the class on the system_server classloader at install
time) reported success on MediaTek A11 — every method matched, mask full — yet
none of the connectivity hooks fired and all five checks still leaked. The
class is hooked before ConnectivityService is constructed, and ART replaces the
method entries when it later initialises/compiles the class, dropping the
hooks. It also grabbed the once-only latch, so path C — which had already seen
the addService registration — was skipped ("C:skipped(already-hooked)").

Drop path A entirely. Only ever attach from the live service binder (B/C/D),
which happens after the instance exists so the hooks stick. Confirmed on a
Pixel 4a (A13): C attaches cleanly, no A entry in the trail, all checks pass.
2026-07-05 14:26:24 +03:00
okhsunrog
6b34834163 docs: add LSPosed hook debugging guide 2026-07-05 14:12:17 +03:00
okhsunrog
2e10ace820 fix(lsposed): attach ConnectivityService hooks on OEM ROMs via deferred getService
On Android 13+ ConnectivityService lives in the Connectivity APEX, so its
method hooks can only be attached from the service binder's classloader. That
was done solely by catching ServiceManager.addService("connectivity", ...) —
which never fires on many MediaTek/OEM ROMs (they publish the service off that
path). Paths A (system_server classloader) and B (immediate getService) can't
cover A13+, so the ConnectivityService hooks never attached and
getNetworkForType, the active/all-networks handles, getNetworkInfo(TYPE_VPN)
and pushed NetworkCallbacks leaked the VPN, while the writeToParcel hooks kept
passing — 20/25 with five Java-API checks red.

Add path D: a deferred getService("connectivity") poll on a short-lived
background thread that retries until the live binder appears and attaches from
its classloader, independent of how the service was registered. In-process
getService returns the local ConnectivityService instance (not a proxy), so
its APEX classloader resolves the class. The thread is torn down as soon as
the hooks attach (any path) or the retry budget is exhausted.

Also make the published hook mask honest: the connectivity bits are set only
once those hooks actually attach, so a device where they don't shows
PARTIAL_HOOKS and lists them under missing owned hooks instead of a false
"8/8 installed". And record per-attempt install telemetry (resolved class,
classloader chain, attach path, per-method match counts, full attempt trail)
as cs_* keys in the LSPosed state file, so this class of attach failure is
diagnosable from a debug bundle without depending on logcat or the
debug-logging toggle.
2026-07-05 09:51:53 +03:00
Danila Gornushko
b3e5ef3aa2
fix(release): keep Cargo.lock in sync on version bump; build --locked (#239)
Some checks are pending
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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
release.py rewrites each crate's `version = "..."` in Cargo.toml but never
touched Cargo.lock, so the lock kept the previous version. The first
`cargo build` in CI then rewrote the lock to match, dirtying the working tree —
and get_build_version()'s `git describe --dirty` stamped every artifact
"X.Y.Z-dirty" (that's why the v1.1.0 KPM/Ports/etc. modules report 1.1.0-dirty).

Two layers so it cannot recur:
- release.py now runs `cargo update --offline --workspace` for both lockfiles
  after the Cargo.toml bump, so the release commit carries a matching lock.
- The Rust build steps (activator, zygisk cdylib, lsposed probe) pass --locked,
  so a drifted lock fails the build loudly instead of silently rewriting it.

Also commits the now-synced 1.1.0 lockfiles.
2026-07-05 00:10:14 +03:00
okhsunrog
7384199892 chore: update-json for v1.1.0 2026-07-04 23:40:49 +03:00
okhsunrog
6b6ed490ed chore: release v1.1.0 2026-07-04 23:32:51 +03:00
Danila Gornushko
0bf926080d
Single-row adaptive app bar; filters as list chips; airy Dashboard header (#238)
* feat(lsposed): single-row app bar with adaptive brand, filters as list chips

Replace the LargeTopAppBar with a compact single-line TopAppBar so the logo,
tab title and actions sit on one row, reclaiming the vertical space the
two-tier large bar wasted (worst on short / high-density screens). Wrap the
brand in BoxWithConstraints and scale the logo and text down proportionally
when the title slot is narrow, so "VPN Hide" never wraps or clips.

Move the Apps-list filters (show-system, RU-only, sort) out of the top-bar
dropdown into a single horizontally scrollable row of chips above the list;
"Configured first" is selected by default. Only Search stays as a bar action,
which is what frees the room for the brand + three actions on one line.

* feat(lsposed): airy Dashboard header that morphs to compact on tab switch

On tall screens the Dashboard header expands: the logo grows and drops below
the (fixed) action buttons, with a larger wordmark. Every other tab — and any
short / high-density screen — keeps the compact single-row bar shipped in the
previous commit. It's one adaptive header, not a per-tab swap: an animated
`headerProgress` (0 = compact, 1 = airy) drives the logo size, wordmark scale
and vertical drop, so switching tabs smoothly grows/shrinks the bar and the
content below follows. Replaces the fixed-height TopAppBar with a Surface+Box
so the header can take the extra height; the brand keeps its narrow-screen
down-scaling so nothing wraps.
2026-07-04 23:30:50 +03:00
Danila Gornushko
995f218f16
Fix FolkPatch KPM activation (#237)
* Fix FolkPatch KPM activation

* Keep APatch default supercall version
2026-07-04 20:10:02 +03:00
Danila Gornushko
779656bb6e
fix(lsposed): honest "nothing to leak", show root ground truth, drop two dead checks (#236)
On a device with no backend the /proc/net/route check read a green "OK" even
though the app's read was SELinux-blocked — because it classifies as NothingToLeak
(root also sees nothing: a split-tunnel VPN isn't in the main routing table), and
NothingToLeak was collapsed into the backend "OK". Two checks were also dead
weight.

- NothingToLeak gets its own neutral "nothing to hide" pill, distinct from the
  green "OK" a backend earns — so a surface that's empty for everyone no longer
  reads as active protection where there is none.
- Native check detail now shows the root ground-truth read (`root: …`) next to the
  app's own read (`app: …`). That is the basis for the verdict, so adjacent cards
  with the same "access denied by SELinux" app read now explain why one is
  "nothing to hide" (root: no VPN) and another is "SELinux" (root: VPN present).
  Plumbed through CheckResult.groundTruthDetail and the agent bridge.
- Drop the /proc/net/route (Java) probe: a duplicate of the native Rust probe with
  no root differential, so it could only report a misleading "OK" on the SELinux
  denial. The Rust probe covers the vector correctly.
- Drop getActiveNetworkInfo(): its .type reports the underlying transport
  (WIFI/mobile) for an active VPN, not TYPE_VPN, so it never surfaced the leak.
  getNetworkInfo(TYPE_VPN) covers the same LSPOSED_NETWORK_INFO hook and works.
  NetworkInterface enum is kept — the canonical Java iface-enum a detector uses.
2026-07-04 19:05:26 +03:00
Danila Gornushko
9960c93dfd
feat(lsposed): one-tap module downloads and clearer kmod-load wording (#235)
The install-recommendation card told the user to "download vpnhide-kmod-….zip"
but gave no link, and the KPM-alternative note said the kmod zip "won't load"
which reads in Russian as "won't download". This wires up actual downloads and
clarifies the wording.

- The recommendation card gets two buttons: a bright one that downloads the exact
  recommended zip via GitHub's /releases/latest/download/<asset> redirect (always
  the latest release, no hard-coded tag), and a plain "All releases" link for the
  ambiguous-variant case or picking anything else.
- Module-problem and version-mismatch banners that name a zip (wrong/unknown/
  unsupported variant, ambiguous load, outdated module) get a download button too,
  via a new DashboardMessage.downloadArtifact. The outdated-version download is
  offered only when the installed module is older than the app.
- Reword the kmod-load strings: "won't load" -> "won't load into your kernel
  (wrong variant, or the kernel enforces module signatures)", so it no longer
  reads as a download failure.
2026-07-04 18:08:06 +03:00
Danila Gornushko
50dd518953
Add Hidden apps settings page (#234)
Some checks are pending
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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
* feat(lsposed): add hidden apps settings page

* fix(lsposed): keep hidden apps list stable while editing
2026-07-04 16:51:03 +03:00
Danila Gornushko
fbf62e4c5e
fix(lsposed): keep hiding picker state stable (#233) 2026-07-04 15:07:18 +03:00
Danila Gornushko
dde3a58aa0
Diagnostics & dashboard redesign: honest who-hid-the-VPN attribution (#231)
* Replace gobley/UniFFI with a cargo-ndk + JNI/JSON native transport

The native check probes reached the app through UniFFI, built by the gobley
Gradle plugin — which needs an AGP-9 fork on a private maven repo to build at
all. Now that diagnostics also want a root-exec ground-truth probe (a second,
JSON-over-stdout transport), the whole native surface is just "run the checks,
return a blob", so a typed-FFI codegen framework no longer earns its keep. Both
paths now share one JSON schema from one crate: a cdylib with a single JNI
function for the in-process app-view run, and a vhprobe bin for the root-exec
run. Check logic and diagnostics behaviour are unchanged.

- native: drop uniffi; add serde + jni 0.22.4; crate-type cdylib+rlib plus a
  vhprobe bin; run_all_json() and one JNI export
- build: remove the gobley plugins/version and the maven fork repo; add a
  config-cache-safe cargo-ndk Exec task (cdylib -> jniLibs, bin -> assets)
  wired via preBuild
- app: NativeProbe (loadLibrary + JNI + JSON parse) replaces the generated
  bindings; native specs join probe results by id

* Add the root-differential CheckOutcome (who hid the VPN)

Each native check now gets an honest outcome — hidden by the backend, hidden by
SELinux, nothing to leak, a real leak, or not measured — instead of one green
"pass" that conflated all of them. The app runs the probes in-process (its own
filtered view) and execs the same vhprobe binary as root (uid 0 is not a hook
target, so the unfiltered ground truth), then diffs per check: root sees the VPN
and the app doesn't = backend hid it; the app was EACCES-blocked = SELinux hid
it; root also sees nothing = nothing to leak. The legacy passed: Boolean? path
and the dashboard are untouched for now; the outcome is exposed on the agent
bridge for validation.

- native: CheckStatus gains SelinuxBlocked; the four EACCES sites report it
  instead of masquerading as pass
- app: CheckOutcome + classifier, and GroundTruthProbe (extract the asset, stage
  to /data/local/tmp, su-exec, parse), computed per run and carried on CheckResults
- bridge: AgentCheckResult gains an outcome token for the native checks

* Rewire the dashboard onto LayerStatus (backend health, not pass-count)

The native/Java tiles were NativeResult/JavaResult rolled up from the raw
pass-count, so an unloaded backend read "Partial" (from SELinux-only passes) and
a couple of failing Java probes read a blanket "Not working". Replace both with
one LayerStatus { Absent, Inactive, Active(hidden, leaks) } and a verdict
Ok/Partial/Broken. Presence is gated before the checks, so an inactive backend
can only render "Not active", never Partial; and each backend is judged only on
the vectors it owns — a leak on a not-owned vector (e.g. /proc/net/dev under a
kernel backend) goes to the hero, not the tile. hidden/leaks come from the
root-differential CheckOutcome, so the counts are measured, not inferred.

- DashboardData: ProtectionCheck.Checked carries LayerStatus; drop
  NativeResult/JavaResult and toNativeResult/toJavaResult; hero, fully-passed,
  and the checks-failed warning read the verdict
- summarizeNativeLayer / summarizeJavaLayer / verdict in LayerStatus.kt
- DashboardScreen: native and Java tiles share one layerSummaryText/Accent
- bridge: AgentProtectionSummary reports the verdict + hidden/leaks
- strings: add "Not active"; unit tests updated (a few Java fails = Partial)

* feat(lsposed): add RTM_GETRULE policy-rule probe, fix if_inet6 attribution

Policy routing is where Android expresses per-app VPN membership: an
`ip rule ... uidrange N-N lookup tun0` steers a UID into the tunnel's own
table, invisible to /proc/net/route (main table only). The new native
probe mirrors the fib_nl_fill_rule kernel filter (VPN iface name, or this
UID steered into a non-standard table), so a working backend reads as
hidden_backend, SELinux-blocked as hidden_selinux, and root ground-truth
as a leak. Validated on the Pixel 4a across enforcing/permissive/root.

Also drop INET6_FILL_IFADDR from the proc_if_inet6 coverage set — no
kernel seq_show hook exists for /proc/net/if_inet6; only the zygisk
openat filter (or SELinux) owns that vector.

* feat(lsposed): rewire diagnostics screen onto CheckOutcome attribution

Each native check now renders its root-differential outcome instead of a
bare pass/fail: a green card with a 'hidden by backend' pill when a hook
did the work, a green card with a blue 'hidden by SELinux' pill when the
app was EACCES-blocked, 'nothing to hide' when nothing was there, red on
a real leak, grey when not measured. The who-hid-it split is what the
whole redesign is for, and it now reads at a glance on the screen.

CheckOutcome rides along on each native CheckResult so the card is a pure
function of the list; Java checks (no root ground truth) keep the
passed tri-state, shown as clean/leak/not-measured in the same vocabulary.

* feat(lsposed): probe legacy NetworkInfo APIs, drop system-proxy check

The NetworkInfo hook (lsposed_network_info) was installed but exercised by
no check — getActiveNetworkInfo() / getNetworkInfo(TYPE_VPN) were untested,
so a green diagnostics run did not actually verify that hook. Add both
probes: getActiveNetworkInfo() marshals a NetworkInfo across Binder (hits
the parcel hook), getNetworkInfo(TYPE_VPN) is the legacy type query the
result sanitizer nulls (issue #85). Together they close the coverage gap so
the Java layer verifies every VPN-hiding hook.

Drop the system-proxy check: no backend hooks the proxy properties, a
consumer VPN sets no system proxy, so it never fired and only added a
green row that proved nothing.

Note: device was in sideload mode at commit time; on-device verification
(both checks pass with LSPosed active) still pending.

* test(lsposed): cover the root-differential classifier and layer rollup

Pins the truth table verified on-device this cycle: classifyNativeOutcome
(leak / hidden-by-backend / hidden-by-SELinux / nothing-to-leak / not-measured,
incl. empty-ground-truth-wins), CheckStatus.toPassed's SELinux mapping, the
Ok/Partial/Broken verdict, and that a native tile is judged only on the vectors
its backend family owns (kernel vs zygisk masks) — an unowned leak surfaces via
unownedNativeLeaks, not the tile. No device needed.

* feat(lsposed): gate diagnostics on VPN Hide being in the tunnel

Diagnostics measure VPN Hide's own process. If VPN Hide is split-tunnelled
out of the VPN, its own traffic never touches the tunnel, so the checks have
nothing to hide from us and a clean run is meaningless. Gate on it: read the
policy routing rules as root (hook-inert ground truth) and ask whether this
app's uid is steered into the VPN's own routing table.

- vhprobe --uid <n>: report {uid, routed, detail}. Identifies the VPN egress
  table via an 'oif <vpn-iface>' rule (is_vpn_iface, not a hardcoded tun0),
  then checks uidrange membership against exactly that table — so a wlan/rmnet
  per-network uidrange rule no longer counts as 'routed through the VPN'.
- New DiagnosticsCache.State.SelfNotRouted + ProtectionCheck.SelfNotRouted;
  a SelfNotRoutedPrompt ('add VPN Hide to your tunnel') on both the diagnostics
  screen and the dashboard, distinct from 'VPN off'. A null (no-root) answer
  does not block. Agent bridge reports state 'self_not_routed'.

Verified on device: uid 10572 (full tunnel) -> routed, 999999 -> not; the
happy path runs the checks (no self_not_routed transition). The blocking path's
prompt still needs a split-tunnel config to see live.

* feat(lsposed): classify Java checks by outcome (hidden/leak)

With the self-in-tunnel gate guaranteeing a VPN artifact is present, a clean
Java-API check means the LSPosed hook removed it and a dirty one is a leak —
so the Java layer becomes binary Leak / HiddenByBackend (NotMeasured only as a
defensive edge for a probe that could not run). classifyJavaOutcome rides onto
each Java CheckResult via withJavaOutcomes; summarizeJavaLayer now reads
hidden/leaks off the outcome so Partial vs Broken is a measurement, mirroring
the native tile. The diagnostics screen renders the same outcome pills for
Java, and the agent bridge threads the Java outcome token.

On-device pill screenshot still pending (GUI nav flaky on this ROM); logic is
unit-tested and the gate round-trip is verified.

* fix(lsposed): probe IPv6 policy rules in RTM_GETRULE and the self-in-tunnel gate

The kernel's RTM_GETRULE dump is per-family, so requesting AF_INET only
returned IPv4 policy rules. Android installs the per-app VPN uidrange/oif
rules for both families, so an IPv6-only rule set was invisible to both the
netlink_getrule diagnostic and the self-in-tunnel gate.

Add a shared dump_fib_rules helper and dump AF_INET + AF_INET6 on the same
socket (distinct seq), merging the results. Verified on-device: the gate
reports routed=true/false correctly and the rule set now covers both families.

* fix(lsposed): tighten diagnostics attribution honesty

Three refinements so the who-hid-it view can't overclaim:

- Java checks that couldn't observe (no active network / capabilities / link
  properties, or getNetworkForType unreachable via reflection) now report
  passed=null so they classify as NotMeasured instead of a false "hidden by
  backend". The self-in-tunnel gate makes these edges near-impossible, but they
  must not paint a backend success.
- Fold the Java-implemented native-level probes (NetworkInterface enum,
  /proc/net/route via ART) into the unowned-native-leak count. They carry no
  root differential, so a leak there was previously invisible to the dashboard.
- Scope the native tile's hidden count to owned vectors like its leak count, so
  the two describe the same vector set and a cross-backend hidden can't mask an
  owned Broken verdict.

* feat(lsposed): compact the diagnostics screen to a status dot + word, detail on tap

The detailed diagnostics screen was a wall of monospace detail — on an enforcing
device 6-7 cards repeated the same "access denied by SELinux" string, and the long
"Hidden by backend / Hidden by SELinux" pills wrapped the check titles.

Each check now reads as one compact card: name + a coloured status dot + a short
word (OK / SELinux / Leak / No data), matching the status-dot idiom used on the
module rows. The card colour tracks current reality only (green when hidden by the
backend, by SELinux, or nothing to leak; red on a leak; neutral when not measured),
so a normal enforcing device stays all-green and the backend-vs-SELinux attribution
rides on the dot colour, not an alarm. The raw detail is collapsed by default and
revealed on tap; a leak is expanded up front. Drops the misleading "Clean" pill that
showed on access-denied fallback rows.

* ci: resolve the lsposed NDK from ANDROID_NDK_HOME, and rustfmt the probe

Two CI fixes for the branch:

- buildRustProbe hard-coded the NDK path as $SDK/ndk/28.2.13676358, but the CI
  image installs the NDK standalone at $ANDROID_NDK_HOME (r28c == that build
  number), not under the SDK. Prefer ANDROID_NDK_HOME/ANDROID_NDK_ROOT like the
  zygisk build already does, falling back to the SDK-managed path for local dev.
- Run rustfmt over the RTM_GETRULE/gate changes in the probe so `cargo fmt
  --check` passes.

* ci: drop orphaned string resources the diagnostics rewire left unused

Android lint (lintDebug, previously skipped because rustfmt failed first) flags
badge_pass/fail/info and dashboard_protection_hooks_inactive as UnusedResources —
they were left behind when the diagnostics screen moved to the outcome model and
the tiles unified onto LayerStatus. Remove them from both locales.

* docs: add docs/diagnostics.md — the self-diagnosis / attribution model

Curated reference for how the app self-tests hiding and attributes who hid the
VPN: the root-differential classifier, the CheckOutcome taxonomy, the
LayerStatus/verdict rollup, the self-in-tunnel gate, the empirical facts that
shape the checks (SELinux carries part of protection; the VPN lives in policy
tables not the main route table), and the native check→hook map. Indexed from
AGENTS.md; complements detection-vectors.md (the hiding side).

* fix(lsposed): show "not checked" on both tiles for every unmeasured state

VPN-off, self-not-routed and needs-restart all mean the layer's hiding checks
did not run, so the Native / Java API tiles now read a plain "not checked" (neutral
accent) in all three — instead of cramming the hero's "VPN is off" title into the
tile for the VPN-off case only. The hero and banner remain the single place that
says why (turn on VPN / add to tunnel / restart); the tiles just report that the
layer was not measured.
2026-07-04 14:42:04 +03:00
Danila Gornushko
589bd4cf0a
refactor(lsposed): canonical config is the single source of truth for debug logging (#229)
Some checks failed
CI / kmod (android15-6.6) (push) Has been cancelled
CI / kmod (android16-6.12) (push) Has been cancelled
CI / kmod-qemu (android12-5.10) (push) Has been cancelled
CI / kmod-qemu (android13-5.10) (push) Has been cancelled
CI / kmod-qemu (android13-5.15) (push) Has been cancelled
CI / kmod-qemu (android14-5.15) (push) Has been cancelled
CI / kmod-qemu (android14-6.1) (push) Has been cancelled
CI / kmod-qemu (android15-6.6) (push) Has been cancelled
CI / kmod-qemu (android16-6.12) (push) Has been cancelled
CI / kpm-qemu (android12-5.10) (push) Has been cancelled
CI / kpm-qemu (android13-5.10) (push) Has been cancelled
CI / kpm-qemu (android13-5.15) (push) Has been cancelled
CI / kpm-qemu (android14-5.15) (push) Has been cancelled
CI / kpm-qemu (android14-6.1) (push) Has been cancelled
CI / kpm-qemu (android15-6.6) (push) Has been cancelled
CI / kpm-qemu (android16-6.12) (push) Has been cancelled
CI / kpm-qemu-legacy (4.14) (push) Has been cancelled
CI / kpm-qemu-legacy (4.19) (push) Has been cancelled
CI / kpm-qemu-legacy (5.4) (push) Has been cancelled
CI / kpm (push) Has been cancelled
CI / zygisk (push) Has been cancelled
CI / lsposed (push) Has been cancelled
CI / portshide (push) Has been cancelled
CI / release (push) Has been cancelled
CI / kmod-activator (push) Has been cancelled
CI / kmod (android12-5.10) (push) Has been cancelled
CI / kmod (android13-5.10) (push) Has been cancelled
CI / kmod (android13-5.15) (push) Has been cancelled
CI / kmod (android14-5.15) (push) Has been cancelled
CI / kmod (android14-6.1) (push) Has been cancelled
* refactor(lsposed): canonical config is the single source of truth for debug logging

The debug flag lived in two stores — the app's SharedPreferences and the
canonical JSON — kept in sync by a startup reconcile plus a drift safety-net,
which was the source of a class of edge cases. Collapse it to one store: the
canonical JSON, with two distinct fields — `debug` (the effective flag every
sink gates on: app logger, system_server HookLog, native backends) and
`debugSwitch` (the user's toggle intent). The Settings toggle writes both; a
capture temporarily forces only `debug` and restores it to `debugSwitch` in a
finally; an interrupted capture self-heals on the next launch via a
`debug <- debugSwitch` reconcile within the same store — no cross-process drift,
no SharedPreferences.

- Add `debugSwitch` to the canonical config (migrates from `debug` when absent);
  serialize both. buildCanonicalConfig inherits `debugSwitch` from the existing
  config, so rebuild paths (Save / auto-hide / manual-hidden) never clobber the
  user's intent when it diverges from the effective flag during a capture.
- The app reads the effective `debug` from the canonical snapshot; the Settings
  toggle reflects `debugSwitch`.
- Captures force only `debug` via a targeted copy of the parsed canonical config
  (never a rebuild from the legacy targets snapshot), so settings/autoHiddenPackages
  are preserved, and restore to `debugSwitch`.
- Replace the prefs<->canonical reconcile and the drift safety-net with a single
  startup `debug <- debugSwitch` reconcile; delete SharedPreferences,
  runtimeReconcileCanonicalConfig, and reapplyDebugLoggingIfDrifted.
- VpnHideLog gains an always-on e() for errors; stray Log.e routed through it.
- Tests: debugSwitch migration/round-trip, capture session, and that the
  auto-hide rebuild preserves debugSwitch.

* refactor(lsposed): centralize log tags and gating, drop double-activator

- add LogTags registry so call sites, HookLog and the debug-capture
  logcat filter share one spelling instead of scattered literals
- extract the shared gate-and-level policy into GatedLogger; VpnHideLog
  and HookLog keep their own sinks and per-process flag sources
- startup debug reconcile no longer runs the native activator twice
  (the reconciled write already runs it; the no-op path runs it once)

* fix(lsposed): re-read canonical before writing the debug toggle

setDebugLoggingEnabled read the cached RootSnapshotCache StateFlow and
then invalidated it on success, with nothing on the Settings screen
repopulating it. A second toggle in the same visit read a null snapshot
and silently no-opped (no write; the switch snapped back). Read via
getOrLoad() so the write always sees fresh canonical state, matching the
capture and startup-reconcile sites.
2026-07-02 18:50:18 +03:00
Danila Gornushko
5902c0f86c
Upgrade diagnostic captures (#227)
Some checks are pending
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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
* 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.
2026-07-02 16:25:53 +03:00
Danila Gornushko
2fe3065920
feat(lsposed): diagnose broken KPM installs, surface kmod GKI variant (#228)
- classifyKpmProblem: new red-banner diagnosis for a KPM install whose
  activator failed to load (missing binary, or a generic activator
  exit failure), reusing the boot script's `detail` status field that
  was previously parsed only for the conflict/awaiting-superkey cases
- NativeBackendStates.anyInstalled/noneInstalled: single source of
  truth for the three call sites that used to hand-roll their own
  kmod/kpm/zygisk boolean combination
- kmod's stamped GKI variant is now shown on its dashboard card, and
  an outdated-kmod warning names the exact recommended zip — updating
  an old kmod install no longer means guessing which variant to grab

Fixes #225
2026-07-02 16:16:36 +03:00
Danila Gornushko
58769886a5
fix(lsposed): reconcile auto-hidden VPN apps on startup and Refresh (#226)
* fix(lsposed): reconcile auto-hidden VPN apps on startup and Refresh

The auto-hidden VPN-app set was only re-materialized on an explicit Save,
so a VPN app installed after AppListCache had loaded its snapshot stayed
visible to detector apps until the user opened the Hiding tab and saved
again. Toggling auto-hide off/on didn't help — it re-materializes from the
same stale cached app list.

Watch the installed-app list and re-derive settings.autoHiddenPackages
whenever it (re)loads — at cold start and on Hiding-tab Refresh — persisting
only when the set actually changed. The write is idempotent and preserves
every manual role, since it reuses applyAutoHiddenPackages, which touches
only the auto-hidden set and the hidden flags derived from it.

* fix(lsposed): skip startup debug-flag re-propagation when already in sync

The cold-start safety-net re-propagated the persisted debug flag to the on-disk
canonical config and the native activator on every launch, rewriting the config
with byte-identical content each time — an extra su invocation, a config write,
an activator run, and a system_server FileObserver re-read per cold start.

Guard it with reapplyDebugLoggingIfDrifted: only re-propagate when the on-disk
flag has actually drifted from the persisted preference. Capture paths
(LogcatRecorder / debug export) are unaffected — they gate their own
applyDebugLoggingRuntime calls on VpnHideLog.enabled and must still write both
directions, so the guard lives at the startup call site, not in
writeDebugFlagFiles.
2026-07-02 09:47:48 +03:00
okhsunrog
65cdf9a737 chore: update-json for v1.0.0
Some checks failed
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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
CI Image / build-push (push) Has been cancelled
2026-07-02 00:16:24 +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
okhsunrog
60bccf53a6 style: ruff format release.py 2026-07-01 23:15:25 +03:00
okhsunrog
3c7853d2d5 chore: bump internal crates (activator, protocol) to 1.0.0
Don't leave the shared vpnhide_activator / vpnhide_protocol crates on the
old version while the release is 1.0.0. release.py now bumps them too.
2026-07-01 22:48:01 +03:00
okhsunrog
7dfc506ce9 fix(release): bump the KPM module.prop version too
release.py patched kmod/zygisk/ports module.prop but not the KPM one
(kmod/kpm/module/module.prop), which was added after the script was
written — so the KPM module shipped with a stale versionCode=701. Add it
to the patched files and set it to v1.0.0/10000 for this release.
2026-07-01 22:42:28 +03:00
okhsunrog
6af9f89437 chore: sync Cargo.lock after the v1.0.0 version bump
release.py bumps the crate versions in Cargo.toml but not the lockfiles;
cargo rewrites them on first build, which left the tree dirty and stamped
artifacts as 1.0.0-dirty. Commit the synced locks so builds are clean 1.0.0.
2026-07-01 22:37:56 +03:00
okhsunrog
1bbfed6e70 chore: release v1.0.0 2026-07-01 22:30:34 +03:00
okhsunrog
07a1d167d4 chore(release): patch only versionCode in build.gradle.kts
build.gradle.kts sets versionName = buildVersion (computed from
build-version.py / the git tag), not a literal, so release.py's
versionName-literal patch never matched and aborted the release.
versionName is derived from the vX.Y.Z tag, so only versionCode needs
patching there.
2026-07-01 22:29:38 +03:00
Danila Gornushko
ea9fbd06a1
chore: consolidate changelog for 1.0.0 release (#223)
* feat(lsposed): localize Removed/Deprecated/Security changelog headers

The in-app What's-new dialog only mapped added/changed/fixed/notes to
localized labels and fell back to the raw type string for anything else.
1.0.0 introduces a Security section, so add localized headers for
removed/deprecated/security too.

* chore(changelog): consolidate dev-cycle fragments into 1.0.0 release notes

Collapse 105 changelog.d fragments accumulated since v0.7.1 into 28
curated, flagship-first bullets. Features born this cycle (KPM backend,
Statistics tab, per-app hook selection, M3 redesign, the unified Hiding
list, ...) are each stated once in their final state instead of the
add/change/fix iteration users never saw; the VPN-detection work is
grouped by surface with example apps; pure internal refactors are dropped.
Rendered/verified with preview-changelog.py; release.py rotates these at
release time.
2026-07-01 22:24:21 +03:00
Danila Gornushko
716a70d426
feat(lsposed): add background update checks (#221)
Some checks are pending
CI / kpm-qemu (android14-5.15) (push) Blocked by required conditions
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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-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
* feat(lsposed): add background update checks

* refactor(lsposed): show the update-checks prompt only after root is granted

The first-run prompt rendered above the root gate, so it could pop over
the loading and no-root screens where the app is inert. Move it into the
root-granted branch so it only appears once the app is actually usable.

* fix(lsposed): persist the update-notification dedup marker in DataStore

The 'last notified version' marker was written to a separate
SharedPreferences via async apply(). A WorkManager worker's process can
be torn down the moment doWork() returns, before that fire-and-forget
write flushes — verified on-device: the notification posted but the
marker never hit disk, so the daily check would re-notify for the same
release every run. Store it in the app's existing DataStore (like every
other setting) with a synchronous suspend write that completes before
the worker finishes.
2026-07-01 12:19:49 +03:00
Danila Gornushko
1d984a8ad9
feat(lsposed): drop the duplicate per-level protection status on the Dashboard (#222)
* feat(lsposed): drop the duplicate per-level protection status on the Dashboard

The «Статус скрытия» section restated the hero card's «Нативный уровень» /
«Java API уровень» tiles verbatim (same label, same status text, same colour —
both derive from nativeSummaryText/javaSummaryText), adding no information in
any state. It's removed. The critical branches that lived in that section —
the VPN-off prompt and the needs-restart banner — move to a slot right under
the hero, so they're more prominent, not buried mid-screen. In the all-good
state the dashboard is now just hero + Модули. Removes the now-dead
NativeProtectionCard / JavaProtectionCard / ProtectionCardShell and the unused
dashboard_protection string.

* feat(lsposed): drop the duplicate status pill in the Dashboard hero

The hero rendered its verdict twice — the big «VPN скрыт» title and a
coloured StatusPill on the right, both from visual.titleRes. The pill only
added colour, which the status-tinted shield icon already carries. Removed
the pill (and the now-dead private StatusPill composable); the verdict text
appears once.

* feat(lsposed): warn + link to diagnostics when hiding checks fail; reword debug-logging copy

- New Dashboard warning: when a layer is active but some runtime probes still
  leak (NativeResult.Fail / JavaResult.Fail) the state used to show only as an
  amber hero/tile with no explanation. Now it adds a WARNING message with a
  'Details' button that opens the full diagnostics screen.
- Centralised the message action→button mapping into one messageActionSlot()
  used by all three message loops, so a new action is an enum case + one branch
  (not an edit per loop). Added DashboardMessageAction.OpenDiagnostics + a
  top-level diagnostics overlay in MainActivity (reuses DiagnosticsSettingsScreen).
- Reworded settings_debug_logging_sub: off by default to keep logcat quiet and
  save resources — not 'for stealth'. Third-party apps can't read another app's
  logcat, so the stealth framing was a wrong threat model.

* fix(lsposed): drop the confusing cache note from the VPN-off prompt

The VPN-off prompt ended with 'Results are cached until VPN Hide is
restarted — no need to re-run later in the same session' — an internal
caching detail that answers a concern the user doesn't have, and on the
VPN-off screen it forward-references results that don't exist yet. Cut it;
the prompt is now just the actionable core (turn the VPN on, tap Retry).
2026-07-01 11:28:55 +03:00
Danila Gornushko
8fd0609d7e
refactor(lsposed): reframe Protection as Hiding, polish RU/EN copy (#220)
Some checks are pending
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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
Reframes the app's vocabulary around hiding the VPN and cleans up the
Russian and English UI copy. The Dashboard status now reads
'VPN hidden / VPN visible', the Protection tab is renamed to Hiding, and a
lot of anglicisms and awkward Russian phrasings are tightened. The
'targets/цели' concept and the Пресет / ИНФО / ПРОВАЛ / Squircle-углы
labels are kept. No logic changes — only strings and doc comments.
2026-07-01 08:06:14 +03:00
Danila Gornushko
c987a0444c
feat(lsposed): app icons + names in Statistics, live capture session (#219)
* feat(lsposed): app icons + names in Statistics, live capture session

The per-app probe list now resolves each UID to its app icon and
friendly label (with the package name in monospace below), the same way
the Protection tab does, instead of showing the bare package name. UIDs
with no installed match fall back to a neutral placeholder avatar.

The capture session is now live: while a session is active the screen
re-reads the backend counters every couple of seconds, so probes show up
on their own as the user exercises a target app — the manual Refresh step
is gone (the top-bar refresh stays as an optional manual reload). The
card now leads with a numbered three-step how-to, and the session list is
relabelled 'Probes this session' to set it apart from the all-time list.

* feat(lsposed): keep capture results after Stop until cleared

Stopping a capture session used to discard everything the user had just
gathered and snap back to the all-time list. Now Stop freezes the
session's per-app results and keeps them on screen: the card shows
'Captured · M:SS · N apps' with New capture / Clear actions, and the
probe list stays put until the user explicitly clears it or starts a
fresh capture. Introduces an explicit three-state model (Idle / Live /
Stopped) for the capture section.

* fix(lsposed): more natural capture copy (drop 'Live'/'Вживую')

The live-status label read 'Live'/'Вживую' — a literal calque that no
Russian app uses for an in-progress capture; it now reads 'Capturing' /
'Идёт захват', matching the section's 'захват' vocabulary (with the
finished label moving from 'Поймано' to 'Захвачено'). The third how-to
step answered a question users don't have ('do I need to come back and
refresh?'); it now states the useful, positive outcome instead — the
checks show up in the list below. Also drops the '— live' tail from the
intro line.

* feat(lsposed): fold the Backends section into the Statistics top card

The standalone 'Backends' section sat awkwardly between the capture card
and the app list. It's now folded into the hero: a big total-events
headline, an 'N apps · M methods' caption, and a compact per-backend
strip (health dot + name + 'OK · hooks: N' + event count) inside the same
card. The redundant 'active X/Y' metric is dropped — the strip already
lists the active backends. Tab flow is now [top card] -> [capture] ->
[apps] with no foreign mid-screen section. Removes BackendSummaryCard /
BackendBadge and the now-unused metric strings.

* docs: changelog for Statistics top-card consolidation

* fix(lsposed): lay the Statistics total on one baseline, fill the row

The total read as a big number with 'events' stranded on its own line and
a lot of empty space to the right. It's now one baseline-aligned line —
'1,337 events' together — with the 'N apps · M methods' summary pushed to
the right edge so the headline fills the card width.

* fix(lsposed): flow the Statistics headline so RU doesn't clip on narrow screens

The single baseline row clipped on a small screen in Russian, where
'1,337 события' + the longer 'приложений: N · способов: M' overran the
card width and collided. The headline is now a FlowRow: the number and
its unit stay together as one block, and the apps/methods summary flows
onto the same line when it fits (English) or wraps below when it doesn't
(Russian on a narrow screen). No clipping in any locale.

* feat(lsposed): editorial summary sentence for the Statistics headline

Replaces the number + stranded unit + apps/methods caption with a
centred editorial line: the grand total as a big blue figure, then
'<events> recorded across <apps> via <methods>' with the two scope
figures highlighted in green. Built as one wrapping AnnotatedString so it
never clips in any locale, and the words are resolved through plurals so
the Russian case/number forms agree with each count (событие/события/
событий, среди N приложений, N способом/способами).

* feat(lsposed): make the Statistics headline one inline sentence

The total and the rest of the summary were two separate Text elements, so
they always sat on different lines. They're now a single AnnotatedString
paragraph: the big blue total is inline at the start of the sentence (one
element, baseline-aligned by the paragraph itself), and the green scope
figures are enlarged (headlineSmall). Adds a second sentinel (TOTAL_MARK)
so the total and the scope figures can carry different styles in the one
string; the summary template gains the total as %1.

* refactor(lsposed): move resolveAppSummary to ProbeStats.kt with a unit test

Per lsposed/AGENTS.md, pure logic belongs in a top-level *Data function with
a unit test, not in a composable file. resolveAppSummary (a pure UID→package
lookup) moves from StatisticsScreen.kt to ProbeStats.kt next to its analogues
(buildAppProbeStats/diffCapture), and gains ProbeStatsTest coverage for the
installed-match, shared-uid first-match, and uninstalled/empty fallback.

* fix(lsposed): address review nits on the Statistics redesign

Six low/nit items from the branch review:
- EN capture chips read 'apps: N' instead of an ungrammatical '1 apps' in
  the common single-app case (mirrors the RU colon form).
- AppStatAvatar rasterises each icon once per Drawable via remember() instead
  of on every recomposition — the live-capture 1 Hz tick was re-rasterising
  every visible row's icon every second.
- The capture session (baseline / start / frozen result) lives in a
  process-scoped holder, so it survives an Activity recreation (rotation,
  day/night auto-switch, resize) and a tab switch instead of silently
  resetting. Verified on device: session + elapsed clock + probes persist
  across a day/night flip.
- Dropped the dead write-only HealthVisual.container field left after the
  BackendBadge removal.
- Capture stopwatch uses SystemClock.elapsedRealtime() (monotonic) instead of
  wall-clock System.currentTimeMillis(), so an NTP/manual time step can't skew
  the shown duration.
- Plural-word selection derives the category from the last two digits
  (totalCount % 100) rather than clamping to Int.MAX_VALUE, so the Russian
  word agrees with the figure above 2^31 events.
2026-07-01 00:15:13 +03:00
Danila Gornushko
78fe2537c6
feat(lsposed): modernize the no-root screen + add a 'Check again' recheck (#218)
Some checks are pending
CI / kmod-activator (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.10) (push) Blocked by required conditions
CI / kmod (android13-5.15) (push) Blocked by required conditions
CI / kmod (android14-5.15) (push) Blocked by required conditions
CI / kmod (android14-6.1) (push) Blocked by required conditions
CI / kmod (android15-6.6) (push) Blocked by required conditions
CI / kmod (android16-6.12) (push) Blocked by required conditions
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
The 'Root access required' gate (RootDeniedScreen) was the barest of the app's
'can't proceed' cards: no icon, no button, and an all-red top bar. Worst of all,
the root probe ran once in LaunchedEffect(Unit), so after granting root the user
had to fully kill and reopen the app.

- Extract a shared BlockingErrorCard (ui/components) so every full-screen
  blocking state renders through one composable — no-root and the startup-prep
  error now use it, so they stay consistent and a redesign can't miss one again.
- Modernize RootDeniedScreen: lock icon, neutral app-chrome top bar + screen
  background (only the card carries the error colour, like the rest of the app),
  and a 'Check again' button that re-probes root without relaunching.
- Migrate RootPreparationErrorScreen onto the same card.

Navigation stays blocked: the no-root branch shows only this screen; the tabs
live in MainScreen, reachable only when root is granted.

Verified on a real Pixel 4a in BOTH light and dark themes (forced the no-root
state): errorContainer/onErrorContainer adapt per theme, top bar and background
match the app chrome, icon + title + body + button render correctly. ktlint,
detekt, lintDebug, and unit tests pass.
2026-06-30 19:32:35 +03:00
Danila Gornushko
9b1573b93d
feat(lsposed): show the real cause on the startup-preparation error screen (#217)
The 'Startup preparation failed' card always rendered a static message telling
the user to 'check your root manager if permission was interrupted', even though
the real reason was carried all the way to the composable and then dropped at the
render call. For the common incomplete-snapshot case (a backend's section missing,
e.g. a garbled KPM reply -> 'missing sections: kpm_state') that message actively
misleads: root WAS granted, nothing was interrupted.

Model the failure cause (SelfTargetFailureKind: RootUnavailable / IncompleteData /
ConfigWriteFailed / Unknown) where it's actually known, thread it through
SelfTargetPreparation -> StartupSelfTargetState.Failed, and render a
cause-appropriate body plus the underlying detail string (monospace) in the card.
It's the app's own screen, so showing the technical detail leaks nothing.

Verified on-device (forced the IncompleteData path): the card shows the accurate
body and 'root snapshot incomplete, missing sections: kpm_state' verbatim.
ktlint + detekt + lintDebug + unit tests pass.
2026-06-30 18:51:12 +03:00
Danila Gornushko
1aa221c955
fix(kpm): make the ctl0 status/stats channel reliable on real devices (#216)
* fix(kpm): build with -mcmodel=large so .rodata addressing survives the loader

The KPM ctl0 status/stats replies came back corrupted on a real APatch
(KernelPatch) device — garbled/duplicated string literals ("vpnhide vpnhide",
"backendr", a stray "1 stats" prefix). That made the app's root snapshot
fail to parse the kpm_state section and show "Startup preparation failed",
with no working KPM control channel.

Root cause: the .kpm was built with -mcmodel=small, which emits ADRP
(ADR_PREL_PG_HI21, PC-relative ±4 GB) relocations for .rodata string literals.
KernelPatch loads the module in a vmalloc region that can sit more than 4 GB
from where those literals were assumed, so the relocations overflow and resolve
to garbage addresses. The string-formatting code (vpnhide_format_status/stats,
shared with the .ko) is correct — it's only the .kpm's address materialisation
that breaks. The .ko is unaffected because Kbuild handles module addressing.

Switch the KPM build to -mcmodel=large, which materialises absolute addresses
via MOVW_UABS (no ±4 GB range limit). Verified on a real Pixel 4a (APatch,
4.14): ctl0 status/stats now decode cleanly and the app starts. QEMU KPM
harness still passes 18/18 with no panic on android16-6.12 (the larger .kpm
still boots KernelPatch) and android14-6.1.

* fix(activator): trust kpatch ctl0-read stdout, not its byte-count exit status

The kpatch CLI prints a ctl0 reply to stdout and exits with the supercall's
return value, which for a READ is the reply byte count (e.g. 64), not 0.
run_kpatch_kpm_ctl0_read treated any non-zero exit as failure, so every KPM
status/stats read failed on KernelSU / KPatch-Next and the dashboard showed no
KPM counters. (On APatch the supercall is read directly, so that path was
unaffected — its bug was the .rodata corruption fixed by the -mcmodel change.)

Capture stdout regardless of exit code: the reply text is authoritative, and on
a real error the CLI leaves its buffer empty so stdout is empty. The config path
already tolerates the byte-count exit status (kpatch_ctl0_config_status_ok); this
brings reads in line. Verified on a real Pixel 8 Pro (KSU-Next + KPatch-Next):
'activator state' now returns the clean status/stats reply instead of erroring.
2026-06-30 18:09:26 +03:00
Danila Gornushko
262e53fbec
fix(kpm): install warning should respect a disabled .ko, and say disable not uninstall (#215)
customize.sh warned about the single-active conflict whenever the vpnhide_kmod
module directory merely existed, and told the user to *uninstall* it. But the
boot loader (post-fs-data.sh / service.sh) only refuses to load the KPM when
vpnhide_kmod is present AND not disabled — a disabled .ko lets the KPM load.

Make the install check mirror that runtime guard (present AND no `disable`
flag), and reword the message to say disabling is sufficient. Verified on a
Pixel 8 Pro: with vpnhide_kmod disabled (not uninstalled), the KPM loads and
configures, and the .ko is not in lsmod.
2026-06-30 18:08:34 +03:00
Danila Gornushko
8703faa9b4
feat(lsposed): keep configured apps visible under all Protection filters (#214)
The Protection tab's app filters used to be able to hide apps the user had
already configured. The system-apps filter already exempted configured apps
(showSystem || !isSystem || anySelected), but the 'Russian apps only' filter
did not — turning it on hid a configured non-Russian app. Make the Russian
filter consistent: it (like the system filter) narrows the discovery set for
new apps but never hides an already-configured one.

Also remove the now-redundant 'Configured only' filter. The default
ConfiguredFirst sort already groups configured apps into a 'Configured' section
at the top (alphabetised within the group), so 'Configured only' only collapsed
the 'Other apps' section below — staying on ConfiguredFirst gives the same
alphabetical configured list. This drops showConfiguredOnly threading through
ProtectionScreen -> AppPickerScreen -> TargetPickerScreen -> visibleTargetEntries,
the filter menu item, and both string resources.

Updated the unit test that asserted the old configured-only behaviour to instead
assert configured apps survive the Russian filter. detekt + ktlint + lintDebug +
unit tests pass.
2026-06-30 16:27:27 +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
52a95d9eac
refactor(zygisk): dedup line-filter loop, saved-original blocks, hook install table (#212)
Three behaviour-preserving cleanups in the Zygisk backend:

- filter.rs: extract a shared compact_lines(data, hide) helper that owns the
  read/write line-compaction skeleton every /proc/net/* filter repeated, and
  make the TCP filter generic over the address type so tcp6 reuses one path
  instead of a near-identical copy.
- hooks.rs: replace the seven hand-copied saved-original blocks (static slot +
  fn type + getter + setter) with a saved_original! macro.
- lib.rs: collapse the seven if/plan.push install blocks into one HookDesc
  table walked by a data-driven install loop.

Net -132 lines; all unit tests, clippy, rustfmt and the cargo-ndk cdylib build
pass unchanged.
2026-06-30 11:33:53 +03:00
Danila Gornushko
09e02bf625
kmod host-route: cover the legacy kernels (4.14/4.19/5.4), document the 32-bit SIOCGIFCONF gap (#211)
* feat(kpm): extend the public host-route concealment to the legacy kernels

I had disabled the host-route check on the non-GKI kernels (4.14/4.19/5.4) on the
mistaken belief they weren't QEMU-tested — but kpm-qemu-legacy covers exactly
those, and they ARE the kernels the KPM exists to serve. Enable it properly:

- IPv4 now works on every supported kernel. On <5.6 (5.4/4.19/4.14) fib_dump_info
  passes the __be32 dst and dst_len as args 6/7 instead of a fib_rt_info*, so
  kpm_is_public_host_route4 reads them from the right place per ABI. (The __be32
  value in the arg register stores to network-order bytes, so the same
  vpnhide_is_public_ipv4 applies.)
- IPv6 now works on 5.4 and 4.19 too: their fib6_info head matches 5.10, so
  fib6_dst is at +64 (derived from the AOSP/vanilla sources, QEMU-validated on
  the legacy images). 4.14 stays disabled — it uses rt6_info, whose
  cacheline-aligned rt6i_dst has no hand-derivable offset.
- Harness: test hostroute4 on every kernel and hostroute6 on every kernel except
  4.14 (init-kpm.sh gate by uname -r).

Validated A/B in QEMU locally on 5.4, 4.19, 4.14 (legacy images, cortex-a57) and
re-checked 5.10 GKI for no regression on the refactored 5.6+ path.

* feat(kpm): also hide the public IPv6 host-route on 4.14 (rt6_info path)

Completes host-route coverage across every supported kernel. 4.14 predates
fib6_info — rt6_fill_node takes a struct rt6_info*, and the route's destination
is rt6_info.rt6i_dst (a rt6key), not fib6_info.fib6_dst. Add a per-kver rt6_dst
offset and read the rt6key there on the rt6_via_dst path.

The offset is derivable, not guessed: struct dst_entry is 160 bytes (its __pads
keep the size constant regardless of CONFIG_XFRM/CLASSID), the rt6_info prefix
runs to +220, and rt6i_dst is ____cacheline_aligned_in_smp, so it rounds up to
+256 (same for a 64- or 128-byte cacheline). QEMU-validated A/B on the legacy
4.14 image (hostroute6 now hidden for the target, 18/18, no panic), and the
harness host-route gate is dropped — every KPM kver now covers v4 + v6.

* docs(roadmap): record the 32-bit compat SIOCGIFCONF gap as low-priority

* docs(changelog): note the legacy-kernel KPM host-route coverage
2026-06-30 11:32:26 +03:00