Commit graph

71 commits

Author SHA1 Message Date
okhsunrog
38674c0fd6 ci: provide current bind probe to QEMU jobs 2026-07-13 22:01:17 +03:00
okhsunrog
bca81b80e7 fix: block socket binds to hidden VPN interfaces 2026-07-13 20:22:51 +03:00
okhsunrog
a29f26be7d refactor: separate activation and config responsibilities 2026-07-10 11:31:38 +03:00
okhsunrog
27d9bbd089 Pin C formatting to clang-format 18 2026-06-29 02:55:57 +03:00
okhsunrog
ce2c5de998 ci: format all project C sources 2026-06-28 22:56:00 +03:00
okhsunrog
fef99af08a fix: tighten kpm follow-up diagnostics 2026-06-28 22:53:37 +03:00
okhsunrog
69f0a92716 feat(storage): apply ports through activator 2026-06-28 19:01:44 +03:00
okhsunrog
b5790766e7 feat(storage): migrate app to canonical config 2026-06-28 18:27:51 +03:00
okhsunrog
dd2a8038e9 feat(storage): add native config activators 2026-06-28 17:47:45 +03:00
okhsunrog
d3127c5514 kpm: ship as a flashable Magisk/KSU/APatch module + CI publish
Package the cross-version .kpm as one flashable module (kmod/kpm/module +
kmod/kpm/build.py -> vpnhide-kpm.zip), analogous to the .ko and Zygisk
modules, per protocol §7.4:

- post-fs-data.sh loads the KPM via the KernelPatch runtime early in boot.
  Keyless KPatch-Next (Magisk/KSU) loads automatically; APatch (superkey)
  records awaiting_superkey for the app. Single-active guard: refuses to load
  when the .ko backend is installed (§1.5).
- service.sh resolves package->UID once PackageManager is up, pushes a
  'vpnhide 1 config' snapshot via 'kpatch kpm ctl0', and feeds the lsposed
  UID file (the always-active Java layer).
- customize.sh seeds the persistent /data/adb/vpnhide_kpm config dir.

CI gains a 'kpm' publish job (build + upload vpnhide-kpm.zip) wired into the
release job's needs; the new module scripts join the shellcheck list. The
kpm-qemu jobs remain the runtime test gate.

The boot-script CLI invocations follow §7.3 and are fail-safe (they record
load_status and exit rather than risk a bootloop); the exact on-device
kpatch CLI paths get confirmed in device testing.
2026-06-28 12:00:29 +03:00
okhsunrog
cf4d7ecbed protocol: shared C parser/serialiser + golden vectors
Implement the docs/protocol.md §4 wire format in the freestanding
shared/vpnhide_logic.h (shared verbatim by the .ko and KPM):

- vpnhide_parse_header / peek_kind — mandatory header, version fuse
- vpnhide_parse_config — debug + target uid/hookmask, 0x-hex u32 with
  width-overflow reject, dup-uid last-wins, unknown-keyword skip,
  ASCII-only (tab allowed as a separator), CRLF strip
- vpnhide_format_stats / format_status — lowercase-out hex, sparse stats
- vpnhide_clamp_to_line — KPM single-buffer truncation on a line boundary

Add the language-independent golden vectors (protocol_vectors.tsv) and the
C host harness (test_protocol.c) that pins every §8 Layer-0 corner; wire it
plus the previously-unrun test_vpnhide_logic.c into CI lint.
2026-06-28 11:42:18 +03:00
okhsunrog
e85f7cb401 codegen: hook-id registry (protocol §5 single source of truth)
data/hooks.toml is the global hook id space + status error codes shared by
the control/stats protocol: bit N of a config mask == hook id N == the id in
a stats line. scripts/codegen-hooks.py renders the matching id enums,
per-backend 'own' masks, and error codes for every language that touches the
protocol (kmod/KPM C, zygisk Rust, lsposed-native Rust, app Kotlin), so the
numbering can never diverge between backends. Wired into the CI drift-check
beside the iface-list codegen.

The script runs under uv (PEP 723, '#!/usr/bin/env -S uv run' so plain
./scripts/codegen-hooks.py works), matching the repo's other tooling; no
external deps. Generated outputs are rustfmt- and ktlint-clean by
construction. Registry is append-only: ids are permanent so old stats keep
meaning and old readers skip unknown ids.
2026-06-28 02:21:28 +03:00
okhsunrog
16e12f5bcb kmod/kpm: make the CI KPM jobs fail if the getifaddrs probe is missing
The gai_getifaddrs vector skips (still exit 0) when no bionic probe is
available — correct for a dev box without an NDK, but in CI the probe is
baked into the image, so a skip there means it's missing/broken and would
let the job go green with the addr-fill hook silently unchecked. Add a
VPNHIDE_GAI_REQUIRED gate (set by both kpm-qemu jobs): when set, an
unavailable probe is a hard failure instead of a silent coverage drop.
2026-06-27 20:57:12 +03:00
okhsunrog
03c18f2c3c ci: KPM legacy (pre-GKI) matrix — 4.14 / 4.19 / 5.4
Completes KPM CI coverage with the kernels the .ko backend can't serve. Adds
a single lightweight `kpm-qemu-legacy` image (debian + one Bootlin gcc, no
DDK, no kernel tree) that bakes all three from-source Images via
build-source-kernel.sh, plus the Alpine rootfs + KernelPatch tool/runtime +
clang. Built by a new `build-legacy` job in qemu-image.yml (triggered by the
new Dockerfile / build-source-kernel.sh paths).

The `kpm-qemu-legacy` job (matrix 4.14/4.19/5.4) builds the .kpm and runs
run-kpm.sh against each Image with `-cpu cortex-a57` (these kernels fault on
`-cpu max`). With software PAN compiled into them (qemu.config), this is where
the user/kernel pointer-domain bug class is actually caught — the dev_ioctl
fix's regression test now lives in CI, not just on a physical Pixel.

With this, both backends are gated across the whole supported range every PR:
.ko + KPM on GKI 5.10–6.12, and KPM on 4.14/4.19/5.4.
2026-06-27 17:13:20 +03:00
okhsunrog
99de696c82 ci: wire the KPM backend into the QEMU gate (GKI matrix)
Adds a `kpm-qemu` job that runs the KPM harness (run-kpm.sh) on the same
baked GKI kernels as `kmod-qemu`, across all 7 KMIs — KernelPatch-patched +
.kpm-embedded boot instead of insmod. The KPM was untested in CI until now;
this closes that gap for the GKI range (4.14/4.19/5.4 follow in a separate
legacy job + image).

Hermetic, reusing the ddk-qemu image:
- bake the KernelPatch tool + runtime (kptools-linux + kpimg-linux, pinned
  KP_VER) into /opt/qemu/kp and expose VPNHIDE_KP_BIN, so run-kpm.sh skips its
  per-run release download;
- add gcc-aarch64-linux-gnu so the harness's static getifaddrs probe builds;
- run-kpm.sh honors VPNHIDE_KP_BIN (falls back to the per-run cache locally).

The job builds the relocatable .kpm against the KernelPatch submodule
(submodules: recursive) with the DDK clang — no kernel tree needed — then
run-kpm.sh boots the baked Image with KernelPatch and runs the A/B vectors.
Touching the Dockerfile/qemu.config triggers the image rebuild that bakes
these in (qemu-image.yml).
2026-06-27 17:09:59 +03:00
okhsunrog
52c9d46a7d Bump dev tooling and CI toolchain versions
Routine dependency refresh after auditing every manifest against the
upstream registries — the app/Rust runtime deps were already on their
latest stable, so this only touches dev tooling and the CI image.

- PMD CPD toolVersion 7.8.0 -> 7.25.0 (cpdCheck still passes clean)
- ruff 0.15.18 -> 0.15.20 (pyproject required-version + CI RUFF_VERSION in sync)
- CI image rustc 1.95.0 -> 1.96.0
- CI image NDK r28b -> r28c, matching the Gradle ndkVersion 28.2.13676358
  that was already pinned (removes a CI/build NDK mismatch)
- actions/cache v5 -> v6
2026-06-26 14:16:10 +03:00
okhsunrog
9da882f1de ci(kmod): build gate module against baked tree; fix image ref + creds
- Build the test module against VPNHIDE_QEMU_KSRC (the baked kernel tree) so
  CFI/vermagic/struct layouts match the kernel it boots on — building against
  the GKI kdir mismatched and panicked on android16-6.12.
- Pull the ddk-qemu image via a lowercased base from the setup job (GHCR names
  must be lowercase) with a credentials block, matching the other repo-owned
  image jobs (lint/zygisk/lsposed).
2026-06-24 13:27:22 +03:00
okhsunrog
f78c4f54c0 ci(kmod): add kmod-qemu runtime gate
For each KMI, boot the baked GKI kernel (from the ddk-qemu image) in QEMU and
run kmod/test, asserting the module actually hides VPN state per vector — not
just that it compiled. Turns the build-only kmod matrix into a real per-version
runtime gate that catches register/inlining regressions (how rt_fill_info was
found). The module is built here against the GKI kdir and loads on the baked
kernel (same source/vermagic).
2026-06-24 13:25:32 +03:00
okhsunrog
fb1237f8f1 ci(kmod): bake per-KMI QEMU test images for the runtime gate
Adds the infrastructure to run the kmod QEMU harness in CI:

- .github/docker/ddk-qemu/Dockerfile: FROM ddk-min:<kmi> + qemu-system-aarch64
  + a virtio GKI kernel (kernel/common@<kmi> + qemu.config on gki_defconfig,
  built with the DDK clang) + the Alpine rootfs. The module is NOT baked: a
  kdir-built module loads on this kernel (same source/vermagic), so CI reuses
  the existing kmod build artifact.
- .github/workflows/qemu-image.yml: matrix build+push of
  ghcr.io/<owner>/ddk-qemu:<kmi> for all 7 KMIs; runs only on recipe changes,
  monthly, or on demand (kernels change seldom).
- run.sh: honor VPNHIDE_QEMU_IMAGE/ROOTFS/KO so the same script uses the baked
  artifacts in CI and the local cache otherwise.
- ci.yml: lint the new harness scripts via shellcheck.
- README: CI rollout (two steps) + ready-to-apply kmod-qemu gate job.

Verified the kdir module loads on the virtio kernel and passes 7/7 vectors
(android12-5.10). The kmod-qemu gate job itself lands after the images are
built (documented), so this commit doesn't reference not-yet-existing images.
2026-06-24 08:53:44 +03:00
okhsunrog
7b03e9c126 ci(lsposed): enforce detekt + CPD in the lint job
detekt now runs clean with no baseline and CPD is clean at its 100-token
threshold, so both can gate. Added to the lint job's single Gradle invocation
(:app:detekt cpdCheck :app:lintDebug :app:testDebugUnitTest); CPD flipped to
ignoreFailures=false. AGENTS.md updated: opt out inherent findings with
@Suppress at the call site, not a baseline.
2026-06-23 10:57:41 +03:00
okhsunrog
1fe58be9e4 Pin Ruff version in CI 2026-06-22 22:41:51 +03:00
okhsunrog
1b9fdc4e98 Improve GitHub workflow maintenance 2026-06-22 19:07:01 +03:00
okhsunrog
0394055f9a Fix UID matching and update dependencies 2026-06-22 18:55:03 +03:00
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
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
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
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
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
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
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
1ff35d3347 ci(lsposed/native): make cargo test build on host
`libc::ioctl`'s second arg is `Ioctl`, which is `c_int` on android-arm64
but `c_ulong` on linux x86_64. Hardcoding `as i32` made the host build
of the lsposed/native test harness fail with a type mismatch, so
`cargo test --lib` couldn't compile and the generated `iface_lists`
unit tests in this crate were silently dead.

Use `as _` so the cast picks the right width per target. Then add the
matching `cargo test (lsposed native)` step in CI for symmetry with the
zygisk crate, so the codegen tests actually run.
2026-04-26 04:06:40 +03:00
okhsunrog
38f9b56f5b feat(codegen): grammar forms digits_optional / any + generated tests for all 4 langs
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 / kmod (android12-5.10) (push) Waiting to run
CI / kmod (android13-5.10) (push) Waiting to run
CI / setup (push) Waiting to run
CI / lint (push) Blocked by required conditions
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
Two follow-ups to #90 in one PR:

1. Two new match forms in data/interfaces.toml grammar:
     suffix = "digits_optional"   prefix + 0+ ASCII digits
     suffix = "any"                prefix + 1+ any chars
   Needed by the upcoming whitelist (PR-B) for patterns like
   `seth_lte\d*` and `v4-.+`. Not used by any current [[vpn]] rule, but
   the helper functions are exercised by direct unit tests in the
   generated test modules so a bug would surface before whitelist lands.

2. [[test]] vectors in data/interfaces.toml that the codegen renders
   into per-language unit tests:
     - zygisk + lsposed/native: #[cfg(test)] mod tests inside the
       generated iface_lists.rs (run via `cargo test`)
     - lsposed/app: a separate IfaceListsGeneratedTest under
       src/test/kotlin (run via `:app:testDebugUnitTest`)
     - kmod: a userspace test driver test_iface_lists.c — the
       generated header now has __KERNEL__-guarded includes so the
       same matcher compiles against libc, and a new lint step builds
       and runs it via gcc.
   36 fixed vectors today; trivial to grow as new rules / corner cases
   come up. CI catches drift on the next push: any single matcher that
   disagrees with the toml fails its job.

No production behavior change — generated matches_vpn / vpnhide_iface_is_vpn
/ IfaceLists.isVpnIface bodies are byte-identical to before; only the
helper functions and test modules grew.
2026-04-25 21:18:29 +03:00
Danila Gornushko
daa98f3138
refactor: drive VPN-iface matching from a single TOML source of truth (#90)
The kernel module, zygisk, lsposed-native, and the LSPosed Kotlin module
each had their own hand-written list of VPN interface name prefixes,
and the four had drifted: kmod/zygisk/HookEntry knew utun/l2tp/gre
while lsposed-native and DiagnosticsScreen only knew tun/wg/ppp/tap/
ipsec/xfrm. So the self-test could PASS while the hooks were actually
hiding more interfaces.

Move the rules to data/interfaces.toml and render four matchers from it
via scripts/codegen-interfaces.py — one per language target. A new lint
job re-runs the codegen and fails if anything drifts.

The match grammar is intentionally tiny so each codegen target
implements it without depending on regex (kernel C can't):
  exact / prefix / prefix+digits / contains.

Side effect: native diagnostics now agree with the hooks, so the
self-test in DiagnosticsScreen will recognize utun*, l2tp*, gre* and
*vpn* substrings as VPN tunnels (previously it would silently PASS on
those). The /proc/net/route check also moved from raw substring to
whitespace-tokenized matching, which avoids matching VPN-prefix
substrings that show up by chance inside hex-encoded IP addresses.

Existing zygisk filter unit tests still pass unchanged — public API of
is_vpn_iface_bytes / is_vpn_iface_cstr is preserved, only the body now
delegates to the generated matches_vpn().

Cargo.lock files updated incidentally (synced with Cargo.toml versions
that were already 0.7.1 in the manifests).
2026-04-25 20:53:11 +03:00
Horizon
cf4e72fa01
fix(build): port build scripts to Python to allow Windows contributors to build subprojects (#83)
* Rewrite build-version and all build-zip bash scripts to python

* Add executable permissions to python build scripts

* Use python build script for kmod in CI

* Fix

* Enhance kmod build script, add/fix docs, CI edits

* Delete remaining build-zip bash scripts

* Delete remaining build-zip bash scripts
2026-04-25 19:53:15 +03:00
okhsunrog
4ad2ba8c2d ci: fall back to ephemeral keystore when secrets are unavailable
GitHub Actions does not expose secrets to workflows triggered by PRs from
forks, so the lsposed job's `assembleRelease` was failing with a corrupt
release.jks for every external contributor. Generate a throwaway keystore
on the fly in that case so fork PRs get a green CI; signed-for-release
artifacts (push/tag runs) keep using the real secrets unchanged.
2026-04-25 19:42:44 +03:00
BlueGradientHorizon
e170a6e9df Simpler approach to lowercasing
Revert adding docker/metadata-action
2026-04-21 18:32:28 +03:00
BlueGradientHorizon
1f662b0489 Proper fix instead of dirty hack
Use docker/metadata-action to perform username lowercasing
2026-04-20 21:05:03 +03:00
BlueGradientHorizon
0830cd82fc Try to fix CI 2026-04-20 20:42:44 +03:00
okhsunrog
b09f05ce60 feat: diagnose wrong-variant kmod installs end-to-end
Users routinely installed the wrong GKI variant of the kmod zip and
saw no signal beyond "installed, inactive". This adds a full chain
from build to diagnostics so the wrong-variant case is both obvious
to the user and fully captured in bug reports.

Why each piece exists:

- CI stamps `gkiVariant=<kmi>` into each variant's `module.prop`
  so the app can identify what was installed without guessing.
- `post-fs-data.sh` records `/data/adb/vpnhide_kmod/load_status`
  (boot_id, uname -r, gki_variant, insmod exit+stderr, kprobes,
  root manager) and `load_dmesg` at every boot — this survives
  reboots and is the only record of insmod failures by the time
  the user opens the app.
- Dashboard reads both, always computes the kernel-based
  recommendation, and emits targeted issues: wrong-variant,
  unknown-variant (pre-stamp zip that also failed to load),
  kmod-on-unsupported-kernel, kprobes-missing, or generic
  load-failed with the captured stderr.
- Diagnostics screen adds a "Kmod load trace" card so bug
  reports can come in as a screenshot, and the debug zip
  includes load_status + load_dmesg for deeper analysis.

Also aligns `lsposed/native/Cargo.lock` with Cargo.toml (0.6.1
→ 0.6.2) — a real stale-lock fix surfaced by the gradle build.
2026-04-19 20:41:30 +03:00
okhsunrog
98b05c256f fix(lsposed): skip version-mismatch warning for dev builds
Dashboard compared module.prop versions (e.g. 0.6.2) directly against
BuildConfig.VERSION_NAME (which carries the git-describe suffix
0.6.2-14-g1f2205e on dev builds), always flagging mismatch. Now uses
baseVersion() to strip -N-gSHA before comparison; pre-release tags
(-rc1, -beta) are preserved.

Adds unit tests for normalizeVersion, compareSemver, baseVersion and
versionsMismatch, and wires :app:testDebugUnitTest into CI so they
actually run.
2026-04-19 18:51:32 +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
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