Commit graph

186 commits

Author SHA1 Message Date
okhsunrog
b21df83d6b feat(lsposed): auto-enable debug logging during diagnostic captures
The off-by-default toggle silenced VpnHide logs everywhere — which
silently broke the two diagnostic capture paths that bug reports
actually depend on: Start recording would share a logcat with zero
VpnHide-tagged lines, and Collect debug log would zip up a useless
`logcat.txt` stub.

Extract applyDebugLoggingRuntime() that pushes the flag to runtime
sinks (VpnHideLog.enabled + the two flag files) without writing
SharedPreferences. exportDebugZip and LogcatRecorder.start call it to
force-enable on entry when the persisted preference is OFF; on exit
they reconcile against the current SharedPreferences state so a user
who flipped the UI toggle mid-capture wins over the rollback.

Rewrites the toggle's description string to reflect that users don't
need to touch it for one-off bug reports — the capture buttons handle
it — and keep the toggle as an escape hatch for "emit logs
continuously" cases.
2026-04-18 01:23:21 +03:00
okhsunrog
412d78e599 feat: debug logging toggle, off by default
Users installing VPN Hide for stealth didn't want the app, LSPosed
hooks, and zygisk writing per-request lines to logcat that a forensic
analysis with root could read back. Add a single toggle in Diagnostics
that silences all three layers; errors still pass through so hook-
install failures stay diagnosable.

Why each piece:

- VpnHideLog + HookLog wrap Log.i/d/w and XposedBridge.log respectively,
  gated by @Volatile flags. i/d/w only; e always prints.
- SharedPreferences is the source of truth for the app process;
  system_server hooks read /data/system/vpnhide_debug_logging (inotify-
  watched, same pattern as vpnhide_uids.txt — flip is live without
  rebooting); zygisk reads debug_logging from its module dir via the
  existing dir fd in on_load, then calls log::set_max_level.
- Default Off matches the project's stealth-first stance. When disabled,
  zygisk stays at LevelFilter::Error so the rare hook-install failure
  is still visible.
- Zygisk-hooked apps need a restart for the change to take effect
  (flag is read at on_load). App and system_server hooks pick it up
  immediately. Documented in the toggle's description.

Also aligns lsposed/native/Cargo.lock with Cargo.toml (0.6.1 → 0.6.2)
— same stale-lock fix surfaced by the gradle build as in the previous PR.
2026-04-17 19:09:07 +03:00
okhsunrog
42407c6e90 fix(lsposed): strip 'v' prefix from module.prop version at parse time
Kernel-module, Zygisk and Ports module cards were showing
'vX.Y.Z' (from module.prop, Magisk convention), while the LSPosed
hook card showed 'X.Y.Z' (from APK versionName, Android convention) —
the same information rendered two different ways on one screen.

Normalize at parse: parseModuleProp now strips the 'v' prefix via
normalizeVersion, so every downstream consumer — dashboard cards,
mismatch-issue text, update checks — sees a plain semver string.
module.prop files themselves keep the 'v' prefix (Magisk ecosystem
still expects it, and Magisk's update flow reads versionCode anyway).
2026-04-17 16:48:13 +03:00
okhsunrog
b85673c81c chore: release v0.6.2 2026-04-17 16:24:53 +03:00
okhsunrog
0a9fcef3c0 ci: rename vpnhide APK artifact + publish as draft release
Two tweaks driven by the same goal — make the artifact list on the CI
run page less ambiguous and give the release step a review gate.

- The APK artifact was named `vpnhide`, which blends in with the other
  module-zip artifacts (`vpnhide-kmod-*`, `vpnhide-zygisk`,
  `vpnhide-ports`). Rename to `vpnhide-apk` so every entry in the
  Artifacts list names the thing you actually get when you download it.
- Release-on-tag job now creates a DRAFT GitHub release instead of
  publishing directly. Gives a chance to eyeball the release notes and
  attached binaries before they go public, and avoids racing
  update-json.sh against the assets becoming reachable.

docs/releasing.md and the release.py post-run hints updated to reflect
the manual Publish step and the fact that update-json still has to
wait for the release to be *published*, not just drafted (draft
release assets sit behind auth).
2026-04-17 15:54:32 +03:00
okhsunrog
637761e678 fix(ci): don't dirty committed module.prop when injecting updateJson
Yesterday's Phase 2 commit left the zygisk and portshide CI artifacts
carrying a "-dirty" suffix in their module.prop version: CI appended
`updateJson=...` to the committed module.prop *before* calling
build-zip.sh, so when build-version.sh ran inside the script it saw
the dirtied working tree and `git describe --dirty` appended "-dirty".

Move the updateJson injection into build-zip.sh itself, gated on an
UPDATE_JSON_URL env var. CI sets the env var via the job step `env:`
block; committed module.prop files are no longer touched. Local dev
builds leave the var unset and ship without updateJson, matching the
previous behaviour.

kmod CI already did things in the right order (version computed
before any module.prop edits); left that step as-is.
2026-04-17 14:54:33 +03:00
okhsunrog
3fc735572a build: stamp git-describe build version into every artifact
Add scripts/build-version.sh — a single source of truth for the
effective version string:

  * HEAD on tag vX.Y.Z          -> "X.Y.Z"
  * N commits past tag          -> "X.Y.Z-N-gSHA"
  * working tree dirty          -> additional "-dirty" suffix
  * no git / no matching tag    -> VERSION file fallback

Wired into every packaging path:

  * zygisk/build-zip.sh and portshide/build-zip.sh now stage a copy of
    module/ and sed-patch `version=` in the staging copy, so committed
    module.prop files stay at the last-released version.
  * kmod/build-zip.sh now builds into a staging copy too.
  * The kmod CI step runs build-version.sh and sed-patches module.prop
    before zipping (git installed in the DDK container).
  * lsposed/app/build.gradle.kts exec's build-version.sh at configure
    time and assigns the result to `versionName` (versionCode stays
    static, still bumped by release.py).

All actions/checkout@v6 gained `fetch-depth: 0` so git describe sees
the full tag history inside CI containers.

Result: a locally built or CI-from-main APK shows up in Android
Settings as e.g. `0.6.1-16-gf86e5e5`, and the zip inside carries the
same string in module.prop; the Magisk/KSU manager displays it in the
update list. Release tag builds are indistinguishable from before —
clean `X.Y.Z`. Diagnostic bug reports now carry the exact commit in
the App version line of device_info.txt.
2026-04-17 14:42:40 +03:00
okhsunrog
f86e5e5a4c feat(lsposed): save-to-file for logcat recording
Mirror the debug-zip collection UI: after a logcat capture is stopped,
show a small "Last recording · 0:23 · 3.1 MB" caption above a row of
Save + Share buttons, with Start for a new recording underneath.

Save opens the SAF picker (CreateDocument, text/plain) and writes the
capture to the chosen destination via ContentResolver, same pattern as
the debug-zip Save button — addresses the prior flow where users who
picked "Save to Downloads" from the Share sheet ended up with no file
actually persisted.

The size/duration moved from the Share button label into a shared
caption above both buttons, so it's clear the stats describe the file
both buttons act on. Reused R.string.btn_save and R.string.btn_share_debug
for the labels.

LogcatRecorder.State.Stopped gains lastDurationMs (computed from
startMs on stop) so the caption can show elapsed time even after the
Recording state is gone.
2026-04-17 14:35:16 +03:00
okhsunrog
ad2590a453 refactor(scripts): unreleased section + rename scripts
Rework the changelog and release flow to remove the aspirational
top-level version that made it unclear whether new entries were
landing in an already-released section.

Schema change: `changelog.json` now has an explicit `unreleased`
object instead of hoisting the upcoming version to the top level. The
old `{version, sections, history}` layout becomes
`{unreleased, history}`, with the previously-released version moved
into `history[0]`.

New entries always go into `unreleased` via `changelog.py`. Releasing
is a single atomic operation (`release.py X.Y.Z`) that promotes
`unreleased` into `history[0]` with the target version number,
propagates the version to every source file, and regenerates the
markdown artifacts.

Script renames:
- `_changelog.py` → `changelog_lib.py` (no more underscore-prefixed
  module that's imported by two siblings)
- `changelog-add.py` → `changelog.py`
- `update-version.py` → `release.py` (does more than just version
  propagation — the name now reflects the full release action)

CHANGELOG.md rendering follows Keep a Changelog: a `## [Unreleased]`
block appears on top only when there are unreleased entries; the
update-json/changelog.md shown in Magisk/KSU popups still skips
Unreleased (only released versions make sense there).

Docs (docs/changelog.md, docs/releasing.md, CONTRIBUTING.md, CLAUDE.md)
updated with the new commands and the clarified model.

CLAUDE.md additionally gains a "read these before doing any work"
section that lists the contributor docs — so future sessions load the
workflow rules into context instead of skipping them as optional.
2026-04-17 14:34:47 +03:00
okhsunrog
876829d9ad docs: add CONTRIBUTING.md and docs/ with build/release/changelog guides
Split the contributor-facing knowledge that used to live in the local
CLAUDE.md into versioned, public docs:

- CONTRIBUTING.md — PR process, commit conventions, required changelog
  entry for user-visible changes, code-style checks.
- docs/development.md — prereqs, keystore setup, per-module build
  commands, device install, CI lints.
- docs/releasing.md — VERSION bump → update-version.py → tag → CI →
  update-json.sh flow, with the rationale for why update-json is a
  separate post-release commit.
- docs/changelog.md — changelog.json as source of truth, how the two
  generated markdowns are regenerated, when to add an entry.

Extended kmod/BUILDING.md with a Podman variant of the DDK command,
covering rootless + SELinux (Fedora) where --userns=keep-id and :Z are
required. Kept the kmod build docs next to the code since the GKI /
DDK complexity is kmod-specific.

Component READMEs untouched — they document each module's architecture
and belong next to the code.
2026-04-17 13:51:33 +03:00
Danila Gornushko
0ac9b09473
Merge pull request #37 from okhsunrog/fix/zygisk-netlink-only-filter
fix(zygisk): only filter netlink sockets in recv/recvmsg hooks
2026-04-17 13:04:11 +03:00
okhsunrog
c74cf32002 fix(zygisk): only filter netlink sockets in recv/recvmsg hooks
hooked_recv and hooked_recvmsg used to run the netlink dump filter on
EVERY recv call regardless of socket type. The filter looked at bytes
[4..6] of the buffer for an RTM_NEWLINK (0x10) / RTM_NEWADDR (0x14)
magic; on a TCP/UDP/Unix socket those bytes are arbitrary user data
(TLS ciphertext, HTTP body, whatever). A random 2-byte coincidence was
enough to trip filter_netlink_dump, which then mutated the buffer
in-place via copy_within while trying to parse it as a linked list of
nlmsghdrs — corrupting the TLS stream and stalling the SSL socket on
top. Per-recv probability is low (~2/65536) but HTTPS-heavy cold-start
paths issue hundreds of recvs, so collision is near-certain.

Symptom reported in #16: target apps (Ozon, Home Assistant, Megafon,
Chrome) hang on infinite load with the zygisk module installed, and
work fine once the module is disabled. The ANR trace at reproduction
on Pixel 4a points at RuStorePushService.onStartCommand stuck on
conscrypt recv() reading from ozone.ru endpoints; the TLS stream never
completes because our hook intermittently mangled arriving bytes.

Gate both hooks on `getsockopt(SOL_SOCKET, SO_DOMAIN) == AF_NETLINK`.
TCP/UDP/Unix sockets now pass through untouched; netlink sockets still
get filtered as before. One extra syscall per recv, negligible vs the
network round-trip already in progress.

Also add the IN_GETIFADDRS thread-local guard to both hooks — bionic's
getifaddrs internally uses recvmsg on netlink, so without the guard we
would recurse through collect_vpn_iface_indices -> real_getifaddrs ->
recvmsg -> hooked_recvmsg -> collect_vpn_iface_indices and so on.
The recursion used to terminate on the netlink type check; now that
we might actually reach the netlink path on a netlink fd, the guard
becomes load-bearing.

Locally reproduced and fixed on Pixel 4a with the exact module stack
reported — Kitsune 27001 debug + ZN 1.3.4 + Vector 2.0.
2026-04-17 12:56:07 +03:00
Danila Gornushko
dc8df50a34
Merge pull request #31 from okhsunrog/fix/zygisk-close-module-dir-fd
fix(zygisk): close module-dir fd before zygote forks
2026-04-17 12:52:01 +03:00
okhsunrog
131fb16152 fix(zygisk): close module-dir fd before zygote forks
Zygisksu's get_module_dir() returns a raw fd to /data/adb/modules/<id>
that the module owns. Previously the fd was stored in a local RawFd
variable and leaked — every app forked from zygote after us inherited
it via normal fd inheritance.

Apps that scan /proc/self/fd for root-managed paths (Ozon anti-tamper,
likely others) detected a descriptor pointing inside /data/adb/ and
silently hung — even when our .so was dlclosed for non-target apps.

Wrap the raw fd in OwnedFd so Drop closes it at end of on_load, before
zygote forks any app process.

Bisect-verified on Pixel 4a with Magisk Kitsune 27001 + Zygisk Next
1.3.4 + Vector 2.0: Ozon 19.12.1 hung at loading spinner with any
vpnhide-zygisk build before this fix, loads to full catalog after.
2026-04-16 18:35:12 +03:00
Danila Gornushko
728ebc30c8
Merge pull request #25 from okhsunrog/feat/full-logcat-capture
Some checks are pending
CI / lint (push) Waiting to run
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / kmod (android13-5.15) (push) Waiting to run
CI / kmod (android14-5.15) (push) Waiting to run
CI / kmod (android14-6.1) (push) Waiting to run
CI / kmod (android15-6.6) (push) Waiting to run
CI / kmod (android16-6.12) (push) Waiting to run
CI / zygisk (push) Waiting to run
CI / lsposed (push) Waiting to run
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
feat(diagnostics): full system logcat recording
2026-04-16 16:00:10 +03:00
Danila Gornushko
66586309b2
Merge pull request #30 from okhsunrog/fix/service-sh-pm-race
fix(service.sh): wait for PackageManager to index user apps before resolving UIDs
2026-04-16 15:59:53 +03:00
okhsunrog
00ba398f36 fix(service.sh): wait for PackageManager to index user apps before resolving UIDs
`pm list packages` starts responding to IPC very early in boot but
returns only system packages for several more seconds. service.sh's
previous `pm list packages >/dev/null && break` loop exited as soon as
PM was alive — before user-installed packages (including the vpnhide
app itself and any chosen targets) were indexed. The subsequent
`pm list packages -U | grep "^package:$pkg "` returned nothing, so
/data/system/vpnhide_uids.txt was written empty, and the LSPosed hook
in system_server cached an empty target set for the session. Result:
all Java-level filtering silently disabled until the next reboot where
we got lucky on timing.

Gate the boot wait on our own package being visible in the list (with
a 60s budget instead of 30s). That guarantees PM has moved past the
system-only snapshot before we read target names.

Also add per-call diagnostic logs to the three writeToParcel hooks
(NC, NI, LP) — `VpnHide-NC`, `VpnHide-NI`, `VpnHide-LP` tags — so the
next "Java check fails with zygisk on / passes with zygisk off" style
report can be diagnosed from logs alone instead of a live instrumented
build. The per-call volume is modest compared to system_server's own
logging and the logs live inside the LSPosed bridge log.
2026-04-16 15:33:46 +03:00
okhsunrog
90d04078e2 feat(diagnostics): full system logcat recording button
Add a start/stop capture control on the Diagnostics screen that spawns
`logcat -b all -v threadtime` via root and pipes it to a file in the
app cache dir. Users can press Start, reproduce a bug (cold-start an
app, trigger a hang, etc.), then Stop; the captured file is then
shareable via the standard Android share sheet.

Motivation: the existing debug-zip collector only grabs log lines
tagged VpnHide*, which is useless when we need to see what other apps
(Ozon, Home Assistant, Chrome, system_server) are doing around a
zygisk-injected hang. This gives us the unfiltered system view.

Uses su because READ_LOGS is not granted to third-party apps. The
subprocess is spawned as `exec logcat ...` under su so destroy() kills
logcat directly rather than the wrapping shell.
2026-04-16 03:37:22 +03:00
okhsunrog
88af2ef2ea docs(changelog): backfill v0.1.0–v0.3.1 history
Some checks are pending
CI / lint (push) Waiting to run
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / kmod (android13-5.15) (push) Waiting to run
CI / kmod (android14-5.15) (push) Waiting to run
CI / kmod (android14-6.1) (push) Waiting to run
CI / kmod (android15-6.6) (push) Waiting to run
CI / kmod (android16-6.12) (push) Waiting to run
CI / zygisk (push) Waiting to run
CI / lsposed (push) Waiting to run
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
Reconstructed the four earliest release sections from git log (their
GitHub release bodies are empty). Appended to changelog.json history
and regenerated CHANGELOG.md so the full project history is now in the
canonical source-of-truth JSON.

No behavior change; the short update-json/changelog.md is unaffected
(still last 5 versions).
2026-04-16 00:44:46 +03:00
okhsunrog
a4426ec655 docs(changelog): add full CHANGELOG.md at repo root
Split the generated markdown into two files:

  - CHANGELOG.md at repo root — full history with the Keep a Changelog
    header. Human-facing, discoverable from the GitHub repo page.
  - update-json/changelog.md — still truncated to the last 5 versions,
    for the Magisk/KSU update popup.

Both are regenerated from changelog.json on every changelog-add.py
and update-version.py run.

Also switch the CI release-notes extraction to read CHANGELOG.md so
the body is future-proof once a tag ages out of the short popup file.
2026-04-16 00:42:59 +03:00
okhsunrog
9c13c761a3 ci(release): use changelog.md section as release notes body
Extract the current tag's section from update-json/changelog.md with
awk and pass it as body_path to softprops/action-gh-release. Keeping
generate_release_notes=true so GitHub still appends the auto PR list
and "Full Changelog" link below our handwritten summary.
2026-04-16 00:34:25 +03:00
okhsunrog
d99485a9fb ci: bump GitHub actions to Node.js 24 runtime
Bumps actions/checkout v4→v6, actions/cache v4→v5,
actions/upload-artifact v4→v7, actions/download-artifact v4→v8
to silence the Node.js 20 deprecation warnings GitHub is emitting
ahead of the June 2026 cutoff.

softprops/action-gh-release stays on v2 (third-party, wasn't in the
deprecation list and v3 would need a separate compatibility review).
2026-04-16 00:31:39 +03:00
okhsunrog
546dc44ed6 chore: update-json for v0.6.1 2026-04-16 00:31:05 +03:00
okhsunrog
53aacaa870 chore: release v0.6.1 2026-04-16 00:24:58 +03:00
okhsunrog
a37b1b7cd8 fix(modules): add META-INF for Magisk versions before v28
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.
2026-04-16 00:23:05 +03:00
okhsunrog
4b0a597860 chore(scripts): rewrite update-version in python, add changelog-add
Replace the bash update-version.sh with a python equivalent that also
rotates the changelog (previous top-level → history[0], new empty
top-level for the new VERSION) and regenerates update-json/changelog.md
from the JSON source of truth.

Add changelog-add.py for appending a bilingual entry (EN + RU) to the
upcoming version's section.

Both scripts are uv inline scripts; shared helpers live in _changelog.py.
2026-04-16 00:18:25 +03:00
okhsunrog
b7adc38813 feat: detect gb.sweetlifecl as Russian app 2026-04-15 23:26:45 +03:00
okhsunrog
820afa7a55 docs: refresh README screenshots and install docs 2026-04-15 20:21:00 +03:00
okhsunrog
e1d4859e7f docs: update README for protection modes 2026-04-15 20:09:36 +03:00
okhsunrog
54267d87d8 chore: update-json for v0.6.0
Some checks are pending
CI / lint (push) Waiting to run
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / kmod (android13-5.15) (push) Waiting to run
CI / kmod (android14-5.15) (push) Waiting to run
CI / kmod (android14-6.1) (push) Waiting to run
CI / kmod (android15-6.6) (push) Waiting to run
CI / kmod (android16-6.12) (push) Waiting to run
CI / zygisk (push) Waiting to run
CI / lsposed (push) Waiting to run
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
2026-04-15 18:54:31 +03:00
okhsunrog
a5c21f262c chore: release v0.6.0 2026-04-15 18:42:08 +03:00
Danila Gornushko
c62a5d5613
Merge pull request #20 from okhsunrog/fix/rknhardering-linkproperties-routes
Fix LinkProperties route filtering for app-visible snapshots
2026-04-15 18:40:16 +03:00
okhsunrog
2ed0ce7241 Fix LinkProperties route filtering for target apps 2026-04-15 18:34:37 +03:00
Danila Gornushko
0b0c9818c3
Merge pull request #19 from okhsunrog/feat/ports-hiding
Add portshide module for localhost port blocking
2026-04-15 17:30:58 +03:00
okhsunrog
529be11adf Fix pkg regex-injection + log pm-never-ready in apply.sh
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).
2026-04-15 17:04:44 +03:00
okhsunrog
b2510e3526 Address PR #19 review: pkg-name config + robustness fixes
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
2026-04-15 16:57:33 +03:00
okhsunrog
5341c07781 Wire portshide into release + update-json pipeline
Mirror the kmod/zygisk plumbing so KernelSU-Next / Magisk pick up
portshide updates automatically:

- scripts/update-version.sh bumps portshide/module/module.prop along
  with the other modules when VERSION changes
- scripts/update-json.sh writes update-json/update-ports.json pointing
  at the current release zip
- CI appends updateJson=.../update-ports.json to the portshide
  module.prop before zipping, matching kmod/zygisk
- Dashboard reports portshide version mismatches as issues, with the
  same up/down/different wording the other modules use
2026-04-15 16:45:58 +03:00
okhsunrog
7faeb5ce9a CI: build vpnhide-ports.zip
Adds a portshide job mirroring the simple kmod zip packaging (no build
needed, just zip the module directory). Artifact lands next to the
other release zips so the gh-release step picks it up for tagged
builds.
2026-04-15 16:41:38 +03:00
okhsunrog
d8ffc24712 Show portshide module in Dashboard
Add a fourth module card under Modules showing ports module install
state, iptables chain active-ness, and configured observer count.
Detected via /data/adb/modules/vpnhide_ports presence plus a quick
`iptables -L vpnhide_out` probe to confirm service.sh has applied the
chain.

Add an issue card when the module is installed but observers.txt is
empty, pointing the user to Protection → Ports.
2026-04-15 16:40:42 +03:00
okhsunrog
c967040660 Add Ports mode UI for managing observers
Replaces the ComingSoonPlaceholder in Protection → Ports with a real
picker. Lists installed apps with a single P chip per row — tapping it
toggles the app as a localhost-proxy-hide observer. On Save, resolves
package names to UIDs, writes /data/adb/vpnhide_ports/observers.txt,
then invokes the portshide module's apply script via su so iptables
rules update immediately (no reboot needed).

If the portshide module isn't installed, shows a hint card explaining
how to install vpnhide-ports.zip via KernelSU-Next or Magisk.

Help dialog becomes mode-aware for Ports too: explains the P role,
what connect() behavior observers see (ECONNREFUSED), and which app
categories are safe to mark as observers (banks / госуслуги /
marketplaces yes; Chromium-based browsers no).
2026-04-15 16:26:51 +03:00
okhsunrog
2b2b144cd7 Add portshide module for localhost port blocking
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.
2026-04-15 16:17:00 +03:00
Danila Gornushko
60213cf237
Merge pull request #17 from okhsunrog/feat/package-visibility
Add app-to-app package visibility hooks
2026-04-15 16:03:48 +03:00
Danila Gornushko
92e7eac07e
Merge pull request #18 from okhsunrog/fix/ifconf-sock-do-ioctl
Hook sock_ioctl instead of dev_ifconf for SIOCGIFCONF
2026-04-15 16:03:37 +03:00
okhsunrog
c882586032 Hook sock_ioctl instead of dev_ifconf for SIOCGIFCONF filtering
On GKI 5.10 kernels built with Clang LTO, dev_ifconf() gets inlined
into sock_do_ioctl(). The symbol remains in kallsyms as a dead stub,
so kretprobe registration succeeds but the probe never fires.
Confirmed by disassembly on Xiaomi 13 Lite (5.10.136) and Lenovo
Legion 2 Pro (5.10.101).

On 6.1+, SIOCGIFCONF was moved from sock_do_ioctl() into sock_ioctl()
directly, so hooking sock_do_ioctl would miss it on newer kernels.

sock_ioctl is the file_operations->unlocked_ioctl callback — used as
a function pointer, so LTO cannot inline it. SIOCGIFCONF passes through
it on every kernel version. After it returns, ifconf data is already in
userspace, so we filter uniformly via copy_from_user/copy_to_user with
no version-specific code paths.
2026-04-15 15:54:37 +03:00
okhsunrog
692c8dcd33 Add Protection tab UI with VPN / Apps / Ports modes
Rename the Apps tab to Protection and put a segmented button on top
that selects between three sub-modes: Tun (existing VPN target picker),
Apps (new package-visibility picker with H/O chips per app), and Ports
(placeholder — coming soon).

App Hiding reads/writes vpnhide_hidden_pkgs.txt and vpnhide_observer_uids.txt,
resolves observer package names to UIDs at save time. Self-package is always
added to the hidden list both at app startup (ensureSelfInTargets) and on
Save — managed invisibly, not shown in the app list.

Help dialog is mode-aware: Tun shows the existing L/K/Z hints, Apps shows
the new H/O hints with Russian labels spelling out Hidden / Observer
explicitly, Ports has no help.
2026-04-15 15:30:15 +03:00
okhsunrog
23e2ca292b Add package visibility hooks in system_server
Hides selected packages from selected caller UIDs at the PackageManagerService
Binder stub. Filters getInstalled{Packages,Applications}, queryIntent*,
resolve{Intent,Service}, get{Package,Application}Info, getPackageUid,
getPackagesForUid, getInstaller{PackageName,SourceInfo}. Hooks
IPackageManagerBase with PackageManagerService fallback.

Config via /data/system/vpnhide_hidden_pkgs.txt and
/data/system/vpnhide_observer_uids.txt with inotify live-reload. Callers
with UID < 10000 are exempt to avoid breaking installd / LauncherApps.
2026-04-15 14:55:26 +03:00
okhsunrog
cffb52776b Fix logcat collection — read directly without su
logcat via su runs as root and can't see app's own log entries on some
devices. Use Runtime.exec("logcat") directly instead, which reads the
app's own log buffer without needing READ_LOGS permission.
2026-04-15 14:17:32 +03:00
okhsunrog
3d138f914c chore: update-json for v0.5.3 2026-04-15 00:56:23 +03:00
okhsunrog
00a40db04c chore: bump version to v0.5.3 2026-04-15 00:49:37 +03:00
okhsunrog
1a1b69a3e3 Fix clang-format violations in kmod 2026-04-15 00:40:45 +03:00