* 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.
* 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'
Note the path to per-UID port-range blocking (iptables --dports) so observers
stop being all-localhost-blocked — keeps the backend-independent netfilter
approach instead of a kernel connect-hook. No behaviour change.
service.sh waits for netd's bw_OUTPUT chain to appear, then applies our
rules once. On slow boots netd has been observed to flush/rebuild its
own chains AFTER bw_OUTPUT first appears — wiping the rules we just
installed. The rules then stay missing until the user opens the app
and Save runs apply again.
Add a backgrounded second pass 30 seconds after the first apply. The
apply script is already idempotent (chains created with
`-N ... 2>/dev/null || true`, atomically rebuilt via
`iptables-restore --noflush`), so the extra pass is harmless when
nothing was wiped and self-healing when it was. Cost is one shell
process and one iptables-restore per boot.
shellcheck clean (no new lint).
Android user profiles (work profile, MIUI Second Space, Private Space,
secondary users) each give the same package its own UID in the
namespace `<user>*100000 + <app_id>`. Previously every pkg→UID
resolver used plain `pm list packages -U`, which only emits UIDs for
the primary user, so the work-profile copy of Telegram kept seeing
the VPN even though the user had marked Telegram as a target.
Switch every resolver to `pm list packages -U --user all`. The pm
output format for multi-profile apps is comma-separated on one line:
package:com.android.chrome uid:10187,1010187
Each call site now splits on `,` and emits one UID per line so every
profile's copy is individually matched by the hooks. No UI changes —
"mark Telegram as a target" just now means "in every profile it's
installed in".
Resolvers touched (all places found by an audit, no duplicates left):
Shell (boot-time):
kmod/module/service.sh
zygisk/module/service.sh
portshide/module/vpnhide_ports_apply.sh
Kotlin (save-time via suExec):
AppPickerScreen.kt — buildUidResolver
AppHidingScreen.kt — buildHidingUidResolver
ShellUtils.kt — ensureSelfInTargets
TargetsCache.kt — PM_LIST batch script + parser
Verified on a Pixel 4a with a managed profile (user 10):
- Chrome toggled in LZ on primary → both 10187 and 1010187 land in
/data/system/vpnhide_uids.txt.
- Primary-only apps (Ozon, etc.) still resolve to a single UID.
- ensureSelfInTargets correctly adds both UIDs when vpnhide is
installed across profiles.
Magisk before v28 requires META-INF/com/google/android/update-binary
+ updater-script in module zips to extract them; without these the
manager fails with an unpack error (issue #23). Magisk v28+ removed
this requirement, which is why the bug only shows up on older managers.
Added the standard Magisk template (same one already used by the
zygisk module) to portshide and kmod. CI's `(cd module && zip -qr)`
step picks up the new files automatically.
Address second-pass review of PR #19:
apply.sh was feeding package names to grep as a regex. Package names
contain dots, which regex treats as "any char" — so resolving e.g.
\"com.x.y\" could land on a neighbouring \"comXxXy\" package and
silently block the wrong UID. Switch to awk with literal field
comparison, eliminating the class entirely.
While there, log explicitly when the boot-time pm readiness loop times
out after 30s, so apply.sh's \"0 observer(s)\" message is no longer
ambiguous between "user selected nothing" and "pm never came up".
Minor polish: drop the empty-body null-branch comment in PortsHidingScreen
and simplify body-string construction with joinToString(postfix).
Move UID resolution from Save-time to apply-time so app reinstalls
(which rotate UIDs) get picked up automatically without manual Save.
observers.txt now stores one package name per line — vpnhide_ports_apply.sh
reads it, queries \`pm list packages -U\`, and resolves to UIDs inline
when building the iptables ruleset. Same pattern kmod's service.sh
already uses for its targets.
Other review points:
- uninstall.sh loops \`-D OUTPUT -j …\` so duplicate jumps are fully
cleaned up, and redirects all probes to /dev/null
- PortsHidingScreen detects the module via \`cat module.prop\` rather
than \`[ -d \$dir ]\` — a KSU-Next remove-pending dir stays on disk
until reboot, so bare dir-check misreports install state
- Dashboard observer count now uses the existing countTargets helper,
consistent with kmod/zygisk line counting
- Dropped the awkward \`when (moduleInstalled) { null, true -> Unit;
false -> … return }\` in a Composable — plain if/else reads cleaner
- Comment on apply.sh explaining v4/v6 restores are per-family, not
transactional — if v6 fails, v4 rules are already live
- Simplified buildPortsSaveCommand: no more per-package UID resolver
shell-script construction, just a base64-encoded newline list
New standalone Magisk / KernelSU-Next module that rejects TCP/UDP
connections from selected UIDs to 127.0.0.1 / ::1 via iptables
owner-match rules. Covers the VPN/proxy detection vector where an app
probes well-known localhost ports (7890, 1080, etc.) via
connect() — the observer gets ECONNREFUSED, indistinguishable from
a real closed port.
Rules live in a dedicated chain `vpnhide_out` / `vpnhide_out6` with a
jump from OUTPUT, applied atomically via iptables-restore. Configured
by /data/adb/vpnhide_ports/observers.txt (one UID per line, UID < 10000
filtered out for safety). service.sh re-applies at boot after netd
finishes its own chain setup. uninstall.sh flushes on module removal.
No C code, no per-kernel builds, no Rust FFI — just a shell script
leveraging the iptables binary that ships with every Android ≥ 4.
Verified on Pixel 8 Pro (Android 16) with iptables 1.8.11 legacy:
observer UID gets REJECT, non-observer UIDs are unaffected.