Commit graph

326 commits

Author SHA1 Message Date
okhsunrog
0d4cf09866 chore: CI + scripts cleanup (review items #12 #13 #24 #31 #32 #37)
Six small review-list items rolled together — all CI/dev-tooling, no
runtime behaviour change.

  #12  Dockerfile: pin Rust 1.95.0 and cargo-ndk 4.1.2 (was floating
       `stable` + latest cargo-ndk on monthly rebuild). Versions live
       in ENV vars to make the next bump a one-line edit.

  #13  Add shellcheck to lint job. SC2034/SC3043 excluded — Magisk
       reads SKIPUNZIP externally; Android's /system/bin/sh (mksh on
       Pixel) does support `local` despite POSIX. Verified locally
       that the 11 .sh files (module-side + dev tooling) pass.
       shellcheck baked into the CI image via apt; inline apt-get
       fallback covers the window before image rebuild.

  #24  ci.yml keystore.properties: replace heredoc with `printf '%s\n'`.
       Heredoc without single-quoted EOF re-expands $, backticks and
       backslashes in the password — printf takes the value verbatim.

  #31  scripts/release.py::patch_file now hard-fails when a regex
       pattern doesn't match (was silently leaving stale versions).

  #32  Split rotate_fragments_into_history into rotate + delete steps
       so release.py can save_json + write_md *before* unlinking the
       fragment files. If anything in between fails, fragments are
       still on disk and the run is retryable.

  #37  codegen-interfaces.py: emit `assert!(matches_vpn(…), msg)` /
       `assert!(!matches_vpn(…), msg)` instead of
       `assert_eq!(matches_vpn(…), true/false, msg)` —
       clippy::bool_assert_comparison was firing on every generated
       row under `cargo clippy --tests`. Both generated test modules
       regenerated. CI's clippy steps now also pass `--tests` so this
       class of regression is caught.
2026-04-27 01:14:03 +03:00
okhsunrog
e57417b686 ci: drop broken uniffi-bindgen pre-install from CI image
Some checks are pending
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 / setup (push) Waiting to run
CI / lint (push) Blocked by required conditions
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 / zygisk (push) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
#106 added \`cargo install uniffi-bindgen --version "^0.29" --locked\`,
which fails ci-image rebuilds:

  error: could not find \`uniffi-bindgen\` in registry \`crates-io\`
         with version \`^0.29\`

Two errors in the original change:
  1. The crate Gobley installs is \`gobley-uniffi-bindgen\` (its own
     fork on crates.io at 0.3.7), not upstream \`uniffi-bindgen\`.
  2. Gobley installs the binary into \`app/build/gobley-tools-install/
     uniffi-bindgen/\`, not \`~/.cargo/bin\`. A globally pre-installed
     binary wouldn't satisfy the task's UP-TO-DATE check anyway.

\`org.gradle.caching=true\` from #106 already makes
\`installUniffiBindgen\` go UP-TO-DATE on warm runs (verified locally),
so the optimisation is in effect via the build cache instead.
2026-04-27 00:41:09 +03:00
Danila Gornushko
005a54a55f
Merge pull request #106 from okhsunrog/ci/more-gradle-speedups
ci: shave another ~80s off lint/lsposed jobs
2026-04-27 00:34:56 +03:00
okhsunrog
5350f8e2f6 ci: shave another ~80s off lint/lsposed jobs
Profiling the warm-cache run on PR #105 showed three remaining hot spots
in the Gradle phase:

  installUniffiBindgen          52s   ← cargo install on every CI build
  cargoBuildAndroidArm64Debug   30s   ← Rust crate compile
  lintAnalyze* (3 variants)     43s   ← AGP Lint × main + unit + androidTest

This PR cuts the first one entirely and trims the third.

  - Dockerfile: pre-install uniffi-bindgen 0.29.x in the CI image so
    Gobley's :app:installUniffiBindgen task finds it ready instead of
    rebuilding it from sources on every run. Triggers a ci-image
    rebuild on merge — wait for that workflow to finish before merging
    consumers (or the first lint/lsposed run will still hit the old
    image and behave as before).
  - lsposed/gradle.properties: enable build cache + configuration
    cache. Verified locally: `./gradlew :app:assembleDebug
    --configuration-cache` reports "Configuration cache entry stored"
    cleanly with Gobley 0.3.7 + AGP 8.9.3 + Kotlin 2.1.20.
  - lsposed/app/build.gradle.kts: `lint { checkTestSources = false }`.
    Skips lintAnalyzeDebugUnitTest / lintAnalyzeDebugAndroidTest. Test
    sources here are pure JVM unit-test logic — functional bugs caught
    by :app:testDebugUnitTest, no Android-lifecycle code to lint.
    Deliberately leave `checkReleaseBuilds` at its default so ad-hoc
    `./gradlew :app:lint` still catches R8/ProGuard issues.
  - .github/workflows/ci.yml: `:app:lint` -> `:app:lintDebug`. Lints
    the debug variant only on PRs; release-variant Lint stays
    available locally / for future tag-time CI.
  - docs/development.md: refresh local-lint snippet.

Expected effect on warm cache (cumulative on top of PR #105):
  lint     286s -> ~190s  (3m10s, -32%)
  lsposed  227s -> ~130s  (2m10s, -42%)
2026-04-27 00:28:25 +03:00
Danila Gornushko
7fd63f5e22
Merge pull request #105 from okhsunrog/ci/speed-up-gradle-jobs
ci: speed up gradle jobs + add ruff for python scripts
2026-04-27 00:01:36 +03:00
okhsunrog
71538b04a1 ci: replace bash [[ with POSIX case in lsposed Build APK step
Container jobs default to /bin/sh, not bash, so '[[ ... ]]' fails with
'[[: not found'. POSIX 'case' supports glob patterns natively.
2026-04-26 23:54:26 +03:00
okhsunrog
ecaddaa21d ci: pin ruff-action to v4.0.0 (no v4 alias — immutable releases) 2026-04-26 23:50:12 +03:00
okhsunrog
91013acb54 ci+chore: add ruff (format + lint) for python scripts
Repo had ~1800 lines of Python (kmod/build.py, scripts/*, zygisk/build.py,
portshide/build-zip.py) with no formatter or linter. Long-lived scripts
like scripts/release.py and scripts/codegen-interfaces.py benefit from
catching unused-import / undefined-name / outdated-syntax issues early.

  pyproject.toml — ruff config, target-py312, line-length 100,
                   rules E F W I B UP SIM. Excludes zygisk/third_party,
                   target/, .claude/.
  ci.yml — astral-sh/ruff-action@v4 for `format --check` and `check`,
           ahead of the slow Rust/Gradle steps so it fails fast.
  docs/development.md — add `uvx ruff …` to the local-lint snippet.

Cleanup applied (`ruff format` + `ruff check --fix`):
  - reformat: kmod/build.py, scripts/{changelog_lib,codegen-interfaces,
    release,stats}.py, zygisk/build.py
  - I001: split multi-name imports onto separate lines after the
    sys.path.insert prelude (kmod/build.py, zygisk/build.py)
  - E501 manual: wrap one console.print line in scripts/release.py

Stdlib-only invariant from scripts/build_lib.py is preserved — ruff is
a dev/CI tool, not imported at runtime.
2026-04-26 23:48:37 +03:00
okhsunrog
9986100a77 ci: speed up gradle jobs (cache + assembleDebug for PRs)
The two slowest CI jobs were both Gradle:
  lint     6m41s (Android lint = 264s of it)
  lsposed  6m44s (assembleRelease = 307s of it)
Cargo was already cached; Gradle was not.

Changes:

  - gradle/actions/setup-gradle@v6 in lint and lsposed jobs. Caches
    ~/.gradle/caches, wrapper, configuration cache. cache-read-only on
    PRs so only main pushes write it.
  - lint job now has a cargo cache too (was missing). Combined key for
    both Cargo.lock files.
  - lint: Android lint and Kotlin unit tests run in one Gradle
    invocation (./gradlew :app:lint :app:testDebugUnitTest). Saves a
    second Gobley/AGP configuration phase + JVM startup.
  - lsposed: assembleDebug for PRs and main pushes, assembleRelease only
    for v* tags. R8/ProGuard runs only when the artifact actually goes
    into a release.
  - Drop --no-daemon: with one invocation per job (or one warm daemon
    between two), keeping the daemon is cheaper than killing it.
  - Drop the manual `export ANDROID_NDK_ROOT="$ANDROID_NDK_HOME"`. The
    CI image's Dockerfile already sets ANDROID_NDK_ROOT (line 63), so
    the workaround is redundant.
2026-04-26 23:39:50 +03:00
Danila Gornushko
447c97a8a2
Merge pull request #104 from okhsunrog/chore/16kb-alignment-build-scripts
build: 16 KiB cdylib alignment + unified kmod/zygisk build scripts
2026-04-26 23:37:41 +03:00
okhsunrog
35b3dcdf50 build: align native cdylib on 16 KiB; unify kmod/zygisk build scripts
Two related changes that ship together because they touch the same
build-script + docs surface and were verified together on-device.

16 KiB alignment
  - zygisk/build.rs: pass `-Wl,-z,max-page-size=16384` to lld so the
    cdylib's LOAD segments line up on 16 KiB pages. NDK r28+ already
    does this by default, but the flag keeps r27 builds compatible.
  - lsposed/native/build.rs: new file, same flag, for libvpnhide_checks.so.
  - docs/development.md: bumped the NDK requirement to r28+ and noted
    the 16 KiB rationale.

Verified via `llvm-readelf -l`: both libvpnhide_zygisk.so and
libvpnhide_checks.so now show `Align 0x4000` on every LOAD segment.

Unified build entry points
  - kmod/build.py replaces kmod/build-zip.py. Single script that
    auto-detects whether to build natively (we're inside the DDK image
    or `--kdir` was passed) or to spawn `ghcr.io/ylarod/ddk-min` via
    podman/docker. CI uses the same script with `--inside-container`.
  - zygisk/build-zip.py renamed to zygisk/build.py for symmetry; logic
    unchanged.
  - kmod/BUILDING.md rewritten — local build is now one command:
    `./kmod/build.py --kmi android14-6.1` (or `--all`). The old
    hand-rolled podman/docker recipes are gone.
  - .github/workflows/ci.yml updated to call the new entry points.
    The DDK image tag in CI now has a comment pointing at
    `DDK_IMAGE_TAG` in kmod/build.py as the source of truth.
  - README.{md,en.md}, kmod/README.md, zygisk/README.md, docs/releasing.md,
    scripts/build_lib.py: reference updates.
  - README.en.md: also fixes a "bacame" typo and tightens the Windows
    zygisk-build note (the aux.rs / libgit2 issue is still real).

Verified end-to-end on Pixel 8 Pro (husky, android14-6.1, Android 16):
APK installs, kmod + zygisk modules load, all 26 self-checks PASS in
Enforcing, 22/26 PASS in Permissive (the same 4 by-design FAILs as
before — kmod doesn't cover those paths in Permissive).
2026-04-26 23:26:30 +03:00
Danila Gornushko
14f4765048
Merge pull request #103 from okhsunrog/fix/kmod-rtnl-fill-emsgsize-infinite-retry
fix(kmod): rtnl_fill_ifinfo skb_trim instead of -EMSGSIZE
2026-04-26 23:02:16 +03:00
okhsunrog
a0e645f506 fix(kmod): replace EMSGSIZE return with skb_trim in rtnl_fill_ifinfo
Returning -EMSGSIZE from rtnl_fill_ifinfo for VPN devs hangs RTM_GETLINK
dumps on android14-6.1: the dump iterator retries the same entry forever
on an empty skb. inet6_fill_ifaddr in this same file already documented
and worked around the issue using skb_trim + return-0; mirror that for
rtnl_fill_ifinfo so RTM_GETLINK skips VPN ifaces cleanly.

Reproduced on Pixel 8 Pro (husky, android14-6.1) with SELinux Permissive:
the netlink_getlink check inside dev.okhsunrog.vpnhide hung on splash
screen. After the fix all three netlink-backed checks (getifaddrs,
netlink_getlink, netlink_getroute) PASS in Permissive in <1s.
2026-04-26 22:54:40 +03:00
Danila Gornushko
e39e05c8eb
Merge pull request #102 from okhsunrog/fix/proc-net-tcp-truncation
fix(zygisk): grow /proc/net read buffer past 64 KiB instead of truncating
2026-04-26 21:15:03 +03:00
okhsunrog
ebe0e3676e fix(zygisk): grow /proc/net read buffer past 64 KiB instead of truncating
`open_filtered_proc_net` used a fixed 65 536-byte stack array as the
read buffer. On devices with several hundred concurrent TCP sockets
(p2p clients, browsers with many tabs, dev devices with tethering),
`/proc/net/tcp6` exceeds 64 KiB — the previous code silently dropped
the tail and handed the truncated content back to the caller via
memfd. An app reading its own socket list could miss real entries,
which is a correctness bug, not a security one (the truncated tail
might contain non-VPN sockets the app needs).

Replace the stack array with a thread-local growable Vec<u8>:

* Initial capacity 64 KiB matches the previous fixed buffer, so the
  first call (and most subsequent ones) does no reallocation.
* When the read fills capacity, `Vec::reserve(8 * 1024)` triggers
  amortised doubling — large files cost O(log size) reallocations.
* `clear()` between calls keeps capacity intact: the second and later
  calls in the same thread are zero-allocation.
* Bounded memory: per-thread overhead is `max-observed-size`. On
  Android only one or two threads ever open /proc/net/* (network-
  info worker, diagnostics probe), so the steady state is well under
  a megabyte of RAM.

Trade-off: had to drop the `const { ... }` block on the thread_local
because `Vec::with_capacity` is not const fn. The per-`with()` lazy-
init flag check is negligible relative to the syscalls we're about
to issue.

Empirically: on the device used for testing /proc/net/tcp6 stays at
~15 KiB (no truncation triggered with the old buffer either). The
fix is preventive for power-user scenarios where the limit is
actually reached. SELinux on stock Android blocks /proc/net/* for
untrusted_app domain entirely, so the new path is exercised only on
permissive ROMs or by privileged callers — testing was therefore
limited to verifying no regression on the happy path.
2026-04-26 17:46:13 +03:00
Danila Gornushko
7c7d725991
Merge pull request #101 from okhsunrog/fix/data-system-files-not-world-readable
fix: tighten /data/system/vpnhide_*.txt to 0640 root:system
2026-04-26 16:59:57 +03:00
okhsunrog
c571e59afd fix: tighten /data/system/vpnhide_*.txt to 0640 root:system
The three files written under /data/system/ to coordinate state between
the LSPosed system_server hook and the app — vpnhide_uids.txt,
vpnhide_hidden_pkgs.txt, vpnhide_observer_uids.txt — were chmodded
0644 root:root. /data/system/ itself is mode 0775 system:system,
traversable by untrusted apps, so any "other"-readable file there is
both enumerable (`ls /data/system/`) and openable by name. Untrusted
apps could:

  cat /data/system/vpnhide_uids.txt          # all target UIDs
  cat /data/system/vpnhide_hidden_pkgs.txt   # the hide list
  cat /data/system/vpnhide_observer_uids.txt # observer UIDs

If the reader's own UID is in vpnhide_uids.txt, that's a positive
"vpnhide is filtering me right now" detection — strictly stronger than
the presence-of-marker fingerprint we already closed for
vpnhide_hook_active in PR #100.

Switch every write site to mode 0640 + chown root:system. system_server
runs as UID 1000 with `system` (GID 1000) in its supplementary groups,
so it still gets read via the group bit. Untrusted apps fall to the
"other" octet (now ---) and get EACCES on open.

Empirically verified on Pixel:
  before: 644 root:root → `cat` from untrusted shell succeeds
  after:  640 root:system → untrusted shell EACCES;
                            `su system -c cat` (uid=1000) reads fine,
                            mirroring what system_server sees

Boot-time service.sh in both kmod and zygisk modules also include an
idempotent migration block that re-stamps any pre-PR files left at
0644 by an older version on the next boot. Closes #36 in REVIEW.
2026-04-26 16:41:06 +03:00
Danila Gornushko
750a2b13db
Merge pull request #100 from okhsunrog/fix/review-cheap-batch
Some checks are pending
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 / setup (push) Waiting to run
CI / lint (push) Blocked by required conditions
CI / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / zygisk (push) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
fix: code-review cheap-batch — 10 small correctness/hardening fixes
2026-04-26 16:24:43 +03:00
okhsunrog
cd46097991 ci: narrow workflow contents permission to read; grant write only on release
Workflow-level `contents: write` was granted to every job — lint,
zygisk build, lsposed build, portshide build, kmod matrix — even
though only the release job needs it (to create the draft GitHub
release via softprops/action-gh-release@v2). Tighten to the
least-privilege default of `contents: read` at the workflow level
and override with `permissions: contents: write` on the release job
alone. Reduces blast radius if any of the lint/build jobs ever runs
untrusted code from a PR.
2026-04-26 16:04:43 +03:00
okhsunrog
91204f596a chore(lsposed): include vpnhide-zygisk tag in diagnostics logcat capture
The Diagnostics → Export debug bundle ran logcat with -s filtering
on VPNHideTest, VpnHide, and VpnHide-Dashboard tags only. zygisk
writes under `vpnhide-zygisk` (zygisk/src/lib.rs LOG_TAG), so the
zip the user ends up sharing was missing every native-side hook
trace — exactly the half that's hardest to reproduce locally.
2026-04-26 16:04:43 +03:00
okhsunrog
3c2ea4c1ff fix(lsposed): surface zygisk module-dir copy errors instead of swallowing them
`cp $ZYGISK_TARGETS $ZYGISK_MODULE_TARGETS 2>/dev/null; true` hid
both stderr and the exit code, so a read-only mount or SELinux
denial silently broke "edits in the app aren't picked up by zygisk
on next app launch" — the user had no signal.

Rewrite as `if [ -d ... ]; then cp ... 2>&1; fi`, capture exit and
output, and log a warn() only when both indicate a real failure
(non-zero exit + non-empty stderr). The dir-not-installed case
naturally produces exit=0 + empty output and stays quiet.
2026-04-26 16:04:43 +03:00
okhsunrog
d7ef5c7b8f refactor(lsposed): mStackedLinks rewrite only on actual changes
The else-branch in sanitizeLinkProperties' stacked-links loop set
`modified = true` whenever `stackedCopy !== value`, i.e. after any
successful clone. Cloning is the common case (the catch fallback is
just a defensive shim for a missing copy ctor), so the flag was
effectively always-true: every non-empty stacked map triggered the
trailing `stacked.clear() + putAll(filtered)`, even when no VPN data
was present and no modification happened.

Drop the `stackedCopy !== value` disjunct. `modified` now reflects
only real sanitization changes — the rewrite is skipped when there's
nothing to write back, and the clones in `filtered` are discarded
along with the never-mutated original entries staying in place. No
behavioural change for VPN-stripping callers; just less wasted work
on the non-VPN-bearing path.
2026-04-26 16:04:43 +03:00
okhsunrog
b9eacc63bc fix(lsposed): drop MODIFY from FileObserver mask on config files
inotify MODIFY fires on every write(2), not just on close. With the
current single-write `echo $b64 | base64 -d > $file` writers (see
ShellUtils.ensureSelfInTargets and the AppPicker save path) it
effectively always fires before CLOSE_WRITE, causing the hook to
invalidate the cache and re-read the file. If a writer ever switches
to a multi-write pattern (`echo line >> file` repeated, or anything
that flushes mid-buffer), the hook would see a partially-written
file. CLOSE_WRITE + MOVED_TO covers both single-write and atomic-
rename writers without the mid-write race.
2026-04-26 16:04:43 +03:00
okhsunrog
adec082804 fix(lsposed): stop making hook-status file world-readable
`/data/system/vpnhide_hook_active` was being chmodded to 0644 via
`setReadable(true, false)` so the VPN Hide app could read it
directly. But the app already runs every other read of files under
/data/system/ via `suExec` (see DashboardData.kt's read of this
exact file), so the world-readable bit was redundant — and a
discoverable marker for anti-tamper SDKs that scan /data/system/
for known filenames.

Drop the line; the file stays at the default 0640 system:system
mode, readable by system_server (which writes it) and by anything
running as root (which is how the app reads it).
2026-04-26 16:04:43 +03:00
okhsunrog
a0bec24576 chore(codegen): drop dead c == "" branch in test-name validation
`c` iterates one character at a time over the test name string;
a single character is never the empty string, so the disjunct was
always False. The remaining `0x20 <= ord(c) < 0x7F` already
correctly rejects non-ASCII names, so behaviour is unchanged.
2026-04-26 16:04:43 +03:00
okhsunrog
87a2041b88 chore(zygisk): drop redundant allow(dead_code) on is_vpn_iface_cstr
The function is called from hooks.rs (hooked_getifaddrs and
walk_getifaddrs_vpn). It's been used since the helper was added —
the allow attribute is leftover from before its callers landed.
clippy --release and cargo test both clean without it.
2026-04-26 16:04:43 +03:00
okhsunrog
5969ed9173 fix(kmod): abort init when /proc/vpnhide_targets cannot be created
`proc_create()` returns NULL on failure (typically OOM at boot or
/proc not yet mounted). The previous code stored the NULL into
`targets_entry` and continued — `pr_info(": loaded")` fired, the
kretprobes were registered, but userspace had no way to write the
target UID list, so the module silently filtered nothing.

Treat /proc/vpnhide_targets failure as fatal: log an error,
unregister any probes that did succeed, and return -ENOMEM so
insmod surfaces the failure to the caller. /proc/vpnhide_debug
stays best-effort — losing the debug toggle just means no verbose
logging, the rest of the module is still useful.
2026-04-26 16:04:43 +03:00
okhsunrog
fccc0387a2 fix: use awk literal match instead of grep regex for pm-list parsing
Six places parsed `pm list packages -U` output with
`grep "^package:${pkg} "`, which treats `pkg` as a regex — dots in
package names cross-match, in theory mapping `com.x.y` to a
hypothetical `comXxXy` package. In practice Android won't let two
such packages coexist, so this has never bit anyone, but the fix is
free and unifies with the literal `awk '$1 == p'` pattern that
portshide/vpnhide_ports_apply.sh has been using all along.

Touched:
* kmod/module/service.sh, zygisk/module/service.sh — boot-time UID
  resolution for kmod and lsposed/zygisk targets.
* lsposed/.../{AppPickerScreen,AppHidingScreen,ShellUtils}.kt — three
  call-sites that build shell pipelines from Kotlin to resolve UIDs
  for /proc/vpnhide_targets, the system_server hook uids file, and
  the package-visibility observer uids file.
* lsposed/.../DashboardData.kt — the self-multi-profile detection
  that warns when vpnhide is installed in more than one profile.
2026-04-26 15:44:19 +03:00
okhsunrog
8025be14cc docs: align README and dev guide with current code and CI
Six unrelated drift fixes that accumulated since they were last
synced. Each is independent of the rest:

* README{.en,}.md — kmod claim "filters /proc/net/*" trimmed to
  /proc/net/route. The other /proc/net files are SELinux-blocked
  for untrusted apps and the coverage table already says so.

* kmod/README.md — hook table and architecture note updated from
  dev_ifconf to sock_ioctl. dev_ifconf gets inlined by Clang LTO
  on GKI 5.10 so the kretprobe silently never fires; sock_ioctl
  has been the actual hook target since the vpnhide_kmod.c fix.

* zygisk/README.md — five inline hooks now, not four (recv was
  added separately because bionic's recv tail-calls recvfrom).
  Also clarified pre_app_specialize runs in the forked child, not
  zygote, matching the lifecycle block in lib.rs.

* docs/development.md — JDK requirement matches CI image (17, not
  21); document ANDROID_NDK_ROOT quirk for Gobley; CI lint list
  expanded to match what ci.yml actually runs.

* docs/development.md + lsposed/README.md — explain Gobley (the
  Gradle plugin pair that builds lsposed/native/ and bundles the
  .so + UniFFI Kotlin bindings into the APK). Previously absent
  from all *.md.
2026-04-26 15:32:30 +03:00
okhsunrog
f1479442f0 docs(zygisk): document module lifecycle and per-fork state
The previous comments around `dir_fd` and `CACHED_TARGETS` were ambiguous
about execution context ("before zygote forks any app", "every app
forked after us"), reading as if the module ran in zygote. It doesn't —
NeoZygisk dlopen's the .so in the already-forked child, so every Rust
static is fresh per app launch and `targets.txt` is reread on every
force-stop + restart with no zygote-side cache to invalidate.

Replace with an explicit lifecycle block in the module doc citing the
canonical sources (Magisk api.hpp v2, zygisk-api-rs trait docs,
NeoZygisk loader) plus a section spelling out the implications for
state. Tighten the dir_fd and CACHED_TARGETS comments to match.
2026-04-26 15:08:09 +03:00
Danila Gornushko
729ed47f3f
Merge pull request #99 from okhsunrog/refactor/dead-code-cleanup-bundle
Some checks are pending
CI Image / build-push (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 / setup (push) Waiting to run
CI / lint (push) Blocked by required conditions
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 / zygisk (push) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
refactor: dead-code cleanup bundle (zygisk hooks + lsposed/native ioctl)
2026-04-26 05:50:19 +03:00
okhsunrog
6753214d50 refactor(zygisk): extract maybe_filter_netlink_buf shared by recvmsg/recv hooks 2026-04-26 05:28:37 +03:00
okhsunrog
f0387eedf6 refactor(zygisk): extract walk_getifaddrs_vpn helper for collect_vpn_{addrs,iface_indices}
Both call sites duplicated the IN_GETIFADDRS guard wiring around
the real_getifaddrs lookup + ifaddrs walk. Extract the wiring so
a guard fix lands in one place — the kind of drift that hit the
closed PR #92.
2026-04-26 05:28:33 +03:00
okhsunrog
511c135605 style(zygisk): drop vestigial clippy::missing_const_for_thread_local allow
Verified that all thread-locals in hooks.rs are already declared
const { Cell::new(...) }; the allow had nothing to suppress.
2026-04-26 05:28:33 +03:00
okhsunrog
ac9a8c8f06 refactor(lsposed/native): collapse ioctl socket boilerplate into with_inet_dgram_socket helper 2026-04-26 05:28:27 +03:00
okhsunrog
6067d8eea8 chore(lsposed): drop dead check_netlink_getlink_recv probe
It was added with comment "Temporary check to verify the recvfrom
hook works." The hook is verified in production; the probe carries
no diagnostic value beyond what check_netlink_getlink already gives.
2026-04-26 05:28:23 +03:00
Danila Gornushko
762c88d30e
Merge pull request #98 from okhsunrog/chore/codegen-drop-unused-grammar
chore(codegen): drop unused suffix forms `digits_optional` / `any`
2026-04-26 05:27:51 +03:00
Danila Gornushko
724e07bb76
Merge pull request #97 from okhsunrog/ci/cache-lsposed-cargo
ci: cache cargo registry + lsposed/native target
2026-04-26 05:21:42 +03:00
Danila Gornushko
57dbb7a86f
Merge pull request #96 from okhsunrog/fix/dashboard-isvpnactive-codegen
fix(lsposed/app): unify VPN-active detection on codegen matcher
2026-04-26 05:21:27 +03:00
okhsunrog
9d8a54cfaa chore(codegen): drop unused suffix forms digits_optional / any
Both forms came in with the codegen split (#91) but no [[vpn]]
rule has ever used them — the only `suffix=` rules are `digits`
(`wlan` test vector + `if` from #93). The grammar surface paid
for itself in ~150 lines of dead C/Rust helpers + their tests.

Drop them from VALID_KINDS, the parser, the C/Rust/Kotlin
emitters, and the helper test cases. If a future rule needs
either form, reintroduce alongside the rule that needs it.

Re-ran the codegen; tests pass for all four targets.
2026-04-26 05:16:18 +03:00
okhsunrog
64b69c5cfe ci: cache cargo registry + lsposed/native target
The zygisk job has had this for a while; lsposed/native was rebuilding
the uniffi/serde/quinn deps from scratch every run. Same shape as the
zygisk cache, separate cache key so the two jobs don't fight over a
shared `target/` (different crate, different artifacts).
2026-04-26 05:12:45 +03:00
okhsunrog
3f1d551174 fix(lsposed/app): unify VPN-active detection on codegen matcher
Dashboard's `isVpnActiveSync` and `runJavaProtectionCheck` both
maintained their own `listOf("tun", "wg", "ppp", "tap", "ipsec",
"xfrm")` + `startsWith` checks, which missed names the kmod/zygisk
filter actually hides — `if<N>` from issue #86, `MyVPN`, `wg-client`,
substring catch-all. The dashboard would say "VPN not active" while
the filter was happily suppressing the renamed tunnel.

Move the `/sys/class/net + operstate` walk into a single
`isVpnActiveBlocking()` in ShellUtils.kt that uses
`IfaceLists.isVpnIface` (the same matcher fed to all three modules
from data/interfaces.toml). DiagnosticsScreen.isVpnActive becomes a
thin `withContext(IO)` wrapper around it. The link-properties
ifname check in `runJavaProtectionCheck` switches to the same
matcher.
2026-04-26 05:12:02 +03:00
okhsunrog
3dd7130308 fix(changelog): drop stray "gi" prefix from notification fragment
The date line in changelog.d/fixed-notification-time-increased-to-make-it-fc9a.md
read `gi_2026-04-25_` instead of `_2026-04-25_`. _DATE_LINE in changelog_lib.py
anchors with `^_…_$` under re.MULTILINE, so parse_fragment would have raised
ValueError and blocked the next release. No effect on rendered output — date
isn't emitted into CHANGELOG.md, only used for fragment sort order.
2026-04-26 05:03:05 +03:00
okhsunrog
0213fdc3bb fix(scripts/stats): authenticate via gh/GITHUB_TOKEN and scale bars to terminal width
Unauthenticated GitHub API hits rate-limit the moment you re-run; the
script then iterated the {"message": ...} error dict as a list of releases
and crashed on release["assets"]. Now uses GITHUB_TOKEN or `gh auth token`
when available and raise_for_status surfaces the real HTTP error.

Bar length was `count // 2`, which overflowed the terminal for any
non-trivial release and rich silently truncated with an ellipsis. Now
normalized: `count * bar_w // max_count` where bar_w is computed from
console.width minus the name and count columns plus padding.
2026-04-26 04:59:35 +03:00
Danila Gornushko
0faa0f0db4
Merge pull request #77 from okhsunrog/feat/lsposed-gobley-uniffi
refactor(lsposed): migrate Rust↔Kotlin FFI from hand-rolled JNI to Gobley + UniFFI
2026-04-26 04:52:58 +03:00
okhsunrog
025b39ace9 fix(lsposed/native): adapt to main after rebase
- SIOCGIF{FLAGS,MTU,CONF} casts use `as _` so the host build
  picks the right Ioctl type (matches PR #94).
- check_proc_file uses the codegen-backed `is_vpn_iface` over
  whitespace tokens; the local VPN_PREFIXES array is gone now
  that lib.rs imports `matches_vpn` from `generated::iface_lists`.
2026-04-26 04:44:17 +03:00
okhsunrog
ecf8f5cd98 ci: export ANDROID_NDK_ROOT for Gobley plugin
Real cause of the lsposed/lint NPE on CI: Gobley's
RustAndroidTarget.ndkToolchainDir resolves the NDK by checking, in
order, the explicit `ndkRoot` parameter, `<sdkRoot>/ndk/<latestVersion>`,
then `$ANDROID_NDK_ROOT`. The CI image installs the NDK as a separate
tree at /opt/android-ndk and exports `ANDROID_NDK_HOME`, not
`ANDROID_NDK_ROOT` — so all three lookups return null and Gobley's `!!`
produces a bare `NullPointerException` during `:app` configuration.

Locally my shell exports `ANDROID_NDK_ROOT` (Android Studio convention),
which is why the issue only surfaces in CI.

Bake `ANDROID_NDK_ROOT` into the CI Dockerfile and export it inline in
the lint / lsposed gradle steps so this PR's CI passes before the image
rebuilds. Revert the prior `rustup target add x86_64-unknown-linux-gnu`
and `--stacktrace` debug additions — that was a wrong-hypothesis
workaround (the host target is already installed by `rustup-init`).
2026-04-26 04:41:24 +03:00
okhsunrog
c24aeccb4b ci: add --stacktrace to lsposed gradle invocations (debug) 2026-04-26 04:41:24 +03:00
okhsunrog
33fa5574c3 ci: install x86_64-unknown-linux-gnu rust target for lsposed jobs
Gobley's cargo plugin enumerates Kotlin targets at gradle configure
time and queries rustup for each one — including the JVM host target,
even though we never build for it (`androidUnitTest = false` skips
wiring the JVM cargo build into Android unit tests, but the build
entry is still created at configure time).

Without `x86_64-unknown-linux-gnu` installed, that lookup returns
null and `:app:lint` / `assembleRelease` die with a bare
`NullPointerException` during project configuration.

Add the target as a workflow step in the lint and lsposed jobs so
this PR's CI passes immediately, and bake it into the CI Dockerfile
so subsequent image rebuilds carry it.
2026-04-26 04:41:24 +03:00
okhsunrog
429b265df0 style(lsposed): cargo fmt 2026-04-26 04:41:24 +03:00