Commit graph

83 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
Danila Gornushko
db77076d0d
fix(kmod): hide VPN ifaces from the SIOCGIFCONF size query, not just the fill (#213)
Some checks failed
CI / kmod-qemu (android12-5.10) (push) Blocked by required conditions
CI / kmod-qemu (android13-5.10) (push) Blocked by required conditions
CI / kmod-qemu (android13-5.15) (push) Blocked by required conditions
CI / kmod-qemu (android14-5.15) (push) Blocked by required conditions
CI / kmod-qemu (android14-6.1) (push) Blocked by required conditions
CI / kmod-qemu (android15-6.6) (push) Blocked by required conditions
CI / kmod-qemu (android16-6.12) (push) Blocked by required conditions
CI / kpm-qemu (android12-5.10) (push) Blocked by required conditions
CI / kpm-qemu (android13-5.10) (push) Blocked by required conditions
CI / kpm-qemu (android13-5.15) (push) Blocked by required conditions
CI / kpm-qemu (android14-5.15) (push) Blocked by required conditions
CI / kpm-qemu (android14-6.1) (push) Blocked by required conditions
CI / kpm-qemu (android15-6.6) (push) Blocked by required conditions
CI / kpm-qemu (android16-6.12) (push) Blocked by required conditions
CI / kpm-qemu-legacy (4.14) (push) Blocked by required conditions
CI / kpm-qemu-legacy (4.19) (push) Blocked by required conditions
CI / kpm-qemu-legacy (5.4) (push) Blocked by required conditions
CI / kpm (push) Blocked by required conditions
CI / zygisk (push) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Blocked by required conditions
CI / release (push) Blocked by required conditions
QEMU Test Images / build-push (android12-5.10) (push) Has been cancelled
QEMU Test Images / build-push (android13-5.10) (push) Has been cancelled
QEMU Test Images / build-push (android13-5.15) (push) Has been cancelled
QEMU Test Images / build-push (android14-5.15) (push) Has been cancelled
QEMU Test Images / build-push (android14-6.1) (push) Has been cancelled
QEMU Test Images / build-push (android15-6.6) (push) Has been cancelled
QEMU Test Images / build-push (android16-6.12) (push) Has been cancelled
QEMU Test Images / build-legacy (push) Has been cancelled
The classic two-step SIOCGIFCONF enumeration first calls with ifc_req == NULL
to learn the buffer size (the kernel returns ifc_len = N * sizeof(struct ifreq)
without copying anything), then calls again with a sized buffer. sock_ioctl_ret
filtered the fill but bailed on the NULL probe, so the size query returned the
unfiltered count — a target comparing the two back-to-back saw a one-interface
discrepancy that revealed a hidden VPN.

sock_ioctl_ret now also handles ifc_req == NULL: it rcu-enumerates the socket's
netns netdevs (net captured at entry from the socket, matching dev_ifconf's
sock_net) and subtracts sizeof(struct ifreq) per IPv4-addressed VPN iface,
matched by ifa_label — the exact set and naming inet_gifconf would have emitted
— so the size query agrees with the filtered fill.

Validated by a new harness vector (ifconf_size_probe, via kmod/test/ifconf-probe.c)
asserting a target's size query equals its filtered fill and drops below the
non-target size query: 19/19 PASS, no panic, on GKI 6.1 and 6.6 in QEMU; CI
covers the remaining KMIs once the ddk-qemu image rebuilds with the new probe.

The 32-bit compat SIOCGIFCONF path (compat_sock_ioctl) remains unhooked and is
tracked in docs as low priority. The KPM backend's filter_ifconf still bails on
the NULL probe (raw netdev walking by per-kver offset is not worth it for the
legacy probe) and is noted as WIP parity.
2026-06-30 15:52:13 +03:00
okhsunrog
27d9bbd089 Pin C formatting to clang-format 18 2026-06-29 02:55:57 +03:00
okhsunrog
37705e1ecf ci: trim qemu image cache exports 2026-06-29 00:50:34 +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
4fd0ab826a ci(kmod): disable BTF in test kernels; revert swap to 16G
Some checks are pending
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 / 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) Blocked by required conditions
CI / lsposed (push) Blocked by required conditions
CI / portshide (push) Waiting to run
CI / release (push) Blocked by required conditions
QEMU Test Images / build-push (android12-5.10) (push) Waiting to run
QEMU Test Images / build-push (android13-5.10) (push) Waiting to run
QEMU Test Images / build-push (android13-5.15) (push) Waiting to run
QEMU Test Images / build-push (android14-5.15) (push) Waiting to run
QEMU Test Images / build-push (android14-6.1) (push) Waiting to run
QEMU Test Images / build-push (android15-6.6) (push) Waiting to run
QEMU Test Images / build-push (android16-6.12) (push) Waiting to run
The android13-5.10 image bake failed at the BTFIDS step (resolve_btfids over
the full-LTO vmlinux, Error 255) — not an OOM at the LTO link as first assumed
(the link completed in every run). BTF (CONFIG_DEBUG_INFO_BTF) isn't needed for
these tests, so disable it: the failing BTFIDS step is dropped and the build
lightens. Verified locally — android13-5.10 now builds clean and the harness
passes 7/7 on it.

Revert the swap bump (32G -> 16G): the LTO link was never the failure, so the
extra swap was treating the wrong cause; keep a modest 16G as headroom for the
full-LTO link on a 16 GB runner.
2026-06-24 11:22:38 +03:00
okhsunrog
6c91e2d7a9 ci(kmod): bump qemu-image swap to 32G for the full-LTO link
16 GB swap (16+16 = 32 GB total) was marginal — one of four full-LTO kernel
bakes (android13-5.10) OOM-killed ld.lld at the vmlinux link. Bump to 32 GB
(16+32 = 48 GB total; /mnt has ~70 GB free) so full-LTO links fit reliably.
2026-06-24 09:59:03 +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
b8682882c2 ci(claude-review): pass --comment so the plugin actually posts the review
The `code-review` plugin is dry-run by default — it formats the review
into the Job Summary but does NOT publish a PR comment unless `--comment`
is passed in the slash invocation. That's why every auto-review run so
far finished green with permission_denials_count > 0 and zero comments
on the PR: the action ran, Claude reviewed, but the plugin's last
sentence was "No --comment argument was provided, so no GitHub comment
will be posted."

Adding `--comment` to the prompt. Permissions were never the blocker —
the `claude[bot]` GitHub App already has write on issues + PRs at
install time, and the on-demand workflow (`@claude` mentions) has been
posting fine without any of our workflow-level changes.
2026-04-27 02:57:31 +03:00
okhsunrog
4a8f5e3495 ci(claude): revert workflow-level write perms, enable display_report
Earlier commit e977af0 raised pull-requests/issues to `write` in both
workflows on the assumption that workflow-level permissions gate the
Claude action's GitHub API. They don't — the action uses an OIDC token
exchanged for the `claude[bot]` GitHub App installation token, whose
permissions are configured at App install time and already include
read+write on issues + pull requests. Workflow `permissions:` only
control the default `secrets.GITHUB_TOKEN`, which the action doesn't
use unless `github_token: ${{ secrets.GITHUB_TOKEN }}` is passed in.

Revert the perms back to read-only (now matches reality) and add a
short comment explaining why — so the next reader doesn't try the
same dead end.

Also enable `display_report: true` on the auto-review job so Claude's
full review text shows up in the Actions log, even when it classifies
its inline findings as low-confidence and skips publishing them. Right
now we couldn't tell whether the action had nothing to flag or whether
the inline classifier filtered everything out — display_report makes
that visible without leaking secrets (it's the same review body that
would have been posted as a comment).
2026-04-27 02:43:43 +03:00
okhsunrog
e977af0064 ci(claude): grant pull-requests/issues write so reviews are actually posted
Both Claude workflows had only read-level scopes, so the auto-review
job ran for ~3 minutes per PR and silently dropped its findings — the
post-buffered-inline-comments step reported "No buffered inline
comments" because GitHub denied the API write. Same with the on-demand
@claude responder.

  claude-code-review.yml: pull-requests read -> write
  claude.yml:             pull-requests read -> write
                          issues read -> write

Deliberately keeping `contents: read` everywhere — Claude advises,
doesn't commit. If we ever want @claude-driven code edits, we'd add
that scope explicitly to the on-demand workflow only.
2026-04-27 02:29:12 +03:00
Danila Gornushko
5b21f3a331 "Claude Code Review workflow" 2026-04-27 02:02:08 +03:00
Danila Gornushko
29e871d43c "Claude PR Assistant workflow" 2026-04-27 02:02:07 +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