diff --git a/CLAUDE.md b/CLAUDE.md index 6da64fd..6562d42 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ These short files cover everything specific to this repo. Skipping them leads to - [CONTRIBUTING.md](CONTRIBUTING.md) — PR process, commit conventions, changelog requirement - [docs/development.md](docs/development.md) — prereqs, per-module build quickstart, keystore setup, device install, CI lints +- [docs/state.md](docs/state.md) — every persistent path / proc entry / iptables chain the project touches; who writes, who reads, lifetime - [docs/changelog.md](docs/changelog.md) — changelog storage (`changelog.d/` fragments + history JSON), `./scripts/changelog.py` usage - [docs/releasing.md](docs/releasing.md) — `./scripts/release.py` usage, version-bump flow - [kmod/BUILDING.md](kmod/BUILDING.md) — kernel-module build (one-command DDK via `./kmod/build.py`, GKI matrix, troubleshooting) diff --git a/docs/state.md b/docs/state.md new file mode 100644 index 0000000..d8d1a6a --- /dev/null +++ b/docs/state.md @@ -0,0 +1,272 @@ +# Persistent state — every path the project touches + +Reference catalogue for everyone (humans, agents) trying to answer +"where is this stored?" / "who reads X?" / "what survives a reboot?". + +Grouped by **location prefix**, because the same path is usually +written by one component and read by another, and grouping by reader +or writer would split the same path across multiple places. + +For each entry: format, who writes, who reads, lifetime, mode/owner/ +SELinux label when relevant. File:line cites the source of truth. + +> **Heads-up:** SELinux contexts shown here are `setfiles`/AOSP +> defaults observed on a Pixel running Magisk. Custom kernels or +> heavily modified ROMs may relabel things; if a write fails, check +> `dmesg | grep avc` first. + +--- + +## 1. Module install dirs — `/data/adb/modules/vpnhide_*/` + +These are the standard Magisk/KSU module dirs. Anything inside is +**wiped on module reinstall** — the root manager replaces the whole +tree from the zip. So all *user-managed* state lives outside, in the +persistent dirs of section 2. + +### `/data/adb/modules/vpnhide_kmod/` +- `module.prop` — module metadata + stamped `gkiVariant=` and `version=`. Read by app dashboard (`DashboardData.kt:382` `parseModuleProp`). +- `post-fs-data.sh` — runs at boot, attempts `insmod vpnhide_kmod.ko`, writes diagnostics into the persistent dir. +- `service.sh` — runs after boot, reads `targets.txt`, resolves UIDs, writes `/proc/vpnhide_targets`. +- `vpnhide_kmod.ko` — the kernel module binary itself. + +### `/data/adb/modules/vpnhide_zygisk/` +- `module.prop` — module metadata. +- `customize.sh` — install-time hook; seeds persistent dir, migrates legacy targets. +- `service.sh` — boot script; copies persistent `targets.txt` into module dir so the Zygisk loader's `get_module_dir()` fd sees it. +- `zygisk/arm64-v8a.so` — Rust cdylib injected into every forked app by NeoZygisk. +- `targets.txt` — **boot-time copy** of the canonical persistent file (`/data/adb/vpnhide_zygisk/targets.txt`). Loader reads via fd, not path. (`zygisk/module/service.sh`, `zygisk/src/lib.rs`) +- `debug_logging` — `"0"` or `"1"`. Written by app (`DebugLoggingPrefs.kt:78-82`), read by zygisk module on init. + +### `/data/adb/modules/vpnhide_ports/` +- `module.prop`. +- `customize.sh` — seeds persistent dir on install. +- `service.sh` — calls `vpnhide_ports_apply.sh` after netd is up. +- `vpnhide_ports_apply.sh` — main runtime script. Resolves observers → UIDs, builds & applies iptables rules. Also re-invoked by the app via su when the user taps Save. +- `uninstall.sh` — flushes `vpnhide_out` / `vpnhide_out6` chains. + +--- + +## 2. Module persistent dirs — `/data/adb/vpnhide_*/` + +These dirs are **outside** `/data/adb/modules/`, so module reinstalls +don't touch them. They survive Magisk/KSU updates, kernel upgrades, +and even uninstalling+reinstalling the corresponding module. Wiped +only by factory reset. + +### `/data/adb/vpnhide_kmod/` +| File | Format | Writer | Reader | Lifetime | +|---|---|---|---|---| +| `targets.txt` | one pkg per line, `#` comments | app via su (Save in Protection); seeded by `kmod/module/customize.sh` | `kmod/module/service.sh` (boot, resolves to UIDs) | persistent | +| `load_status` | `key=value` per line: `timestamp`, `boot_id`, `uname_r`, `gki_variant`, `kmod_version`, `root_manager`, `kprobes`, `kretprobes`, `insmod_exit`, `loaded`, `insmod_stderr` | `kmod/module/post-fs-data.sh` | app dashboard (`KMOD_LOAD_STATUS_FILE` constant in `ShellUtils.kt`); `readKmodLoadStatus` in `DashboardData.kt:482` | overwritten each boot | +| `load_dmesg` | filtered `dmesg` excerpt (text) | `post-fs-data.sh` | dashboard (verbose error display) | overwritten each boot | + +`boot_id` in `load_status` is compared against the current +`/proc/sys/kernel/random/boot_id` so the app can tell "this status +was written this boot" vs. "stale from last boot". + +### `/data/adb/vpnhide_zygisk/` +| File | Format | Writer | Reader | Lifetime | +|---|---|---|---|---| +| `targets.txt` | one pkg per line | app via su; `zygisk/module/customize.sh` migrates from legacy in-module location | copied into module dir by `zygisk/module/service.sh` at boot | persistent | + +### `/data/adb/vpnhide_ports/` +| File | Format | Writer | Reader | Lifetime | +|---|---|---|---|---| +| `observers.txt` | one pkg per line | app via su; seeded by `portshide/module/customize.sh` | `vpnhide_ports_apply.sh` (boot + on Save) | persistent | + +### `/data/adb/vpnhide_lsposed/` +| File | Format | Writer | Reader | Lifetime | +|---|---|---|---|---| +| `targets.txt` | one pkg per line | app via su (`ensureSelfInTargets` in `ShellUtils.kt:198` + Protection screens) | `kmod/module/service.sh` and `zygisk/module/service.sh` migrate-from on first boot if their own `targets.txt` is empty | persistent | + +LSPosed has no module dir (it's not a Magisk module — it's +hooks installed into system_server by the Vector framework), so +everything lives in this persistent dir directly. + +--- + +## 3. system_server-readable files — `/data/system/vpnhide_*` + +This is the coordination channel between the app (writes via su) and +the LSPosed hooks running inside system_server. All files here are +**owned `root:system`, mode 0640, label `system_data_file`** — system_server +reads via the `system` group, untrusted apps fall to "other" and get +EACCES. The dir `/data/system/` itself is mode 0775 traversable by +all, so the per-file restriction matters; a plain 0644 here would be +enumerable + readable. + +| File | Format | Writer | Reader | Lifetime | +|---|---|---|---|---| +| `vpnhide_uids.txt` | one UID per line (integer) | `kmod/module/service.sh` and `zygisk/module/service.sh` resolve `targets.txt` → UIDs at boot; app via su after Save | LSPosed hook in system_server; `HookEntry.kt:174` first-call read + FileObserver (`HookEntry.kt:346`) for live reload | persistent (rewritten at boot + on save) | +| `vpnhide_hidden_pkgs.txt` | one pkg per line | app via su when user picks "Apps to hide from PackageManager" | `PackageVisibilityHooks.kt:124` + FileObserver | persistent | +| `vpnhide_observer_uids.txt` | one UID per line | app via su | `PackageVisibilityHooks.kt:111` + FileObserver | persistent | +| `vpnhide_hook_active` | `key=value`: `version`, `boot_id`, `timestamp`, `aosp_sdk`, optional `broken_fields` | `HookEntry.kt:312-339` `writeHookStatusFile` (in system_server) | app dashboard (`DashboardData.kt:805` reads via su); compares `boot_id` to detect stale records | per-boot | +| `vpnhide_debug_logging` | single byte `"0"` or `"1"` | app via su (`DebugLoggingPrefs.kt:69-73`) | LSPosed hooks via `HookLog.reload` + FileObserver (`HookLog.kt:30-43`); also surfaced as a Dashboard warning (`DashboardData.kt:991`) | persistent | + +**FileObservers** in `HookEntry.kt` and `PackageVisibilityHooks.kt` +watch `/data/system/` for `CREATE | CLOSE_WRITE | MOVED_TO | MODIFY` +events. Saves from the app trigger inotify, which invalidates the +in-process cache, so a Save propagates to running system_server +hooks **without any IPC or restart**. + +--- + +## 4. Kernel module ABI — `/proc/vpnhide_*` + +Created by `kmod/vpnhide_kmod.c` at module init via `proc_create()`, +removed at module exit. Mode `0600`, root-only. + +| Path | Format | Writer | Reader (kernel side) | +|---|---|---|---| +| `/proc/vpnhide_targets` | one UID per line; write replaces full set | root userspace: `kmod/module/service.sh:78` writes resolved UIDs at boot; LSPosed app via su after Save | `vpnhide_kmod.c` `targets_write` handler — caches UIDs in kernel memory, consulted by every kretprobe handler | +| `/proc/vpnhide_debug` | `"1"` enables, `"0"` disables verbose `pr_info` from kretprobes | LSPosed Diagnostics screen during Collect-debug-log capture (`DiagnosticsScreen.kt`) | `READ_ONCE(debug_enabled)` in every probe handler | + +Both files are **per-boot, in-kernel state only**. Unloading the +module (or rebooting) wipes the cached UIDs; service.sh re-seeds at +next boot from `/data/adb/vpnhide_kmod/targets.txt`. + +--- + +## 5. App-process state — SharedPreferences and `filesDir` + +### SharedPreferences `vpnhide_prefs` + +Accessed via `context.getSharedPreferences("vpnhide_prefs", MODE_PRIVATE)` +in Kotlin. Keys currently in use: +- `debug_logging: Boolean` — Diagnostics toggle (`DebugLoggingPrefs.kt:21,27-30`). +- `last_seen_version: String` — for "what's new" changelog dialog. +- `help_collapsed_apps_tun: Boolean` and similar — collapse state for help accordions. + +> ⚠️ **Vector LSPosed redirects this storage.** +> +> Because `dev.okhsunrog.vpnhide` is registered in LSPosed as its own +> module, the Vector framework hooks `Context.getSharedPreferences()` +> for the app's process and **transparently redirects reads/writes** +> to: +> +> ``` +> /data/misc//prefs/dev.okhsunrog.vpnhide/vpnhide_prefs.xml +> ``` +> +> The `` is a row in `/data/adb/lspd/config/modules_config.db`. +> Owner is the app uid, SELinux label `xposed_data`. +> +> **Consequence when debugging:** +> `/data/data/dev.okhsunrog.vpnhide/shared_prefs/` will be empty even +> when the user has touched the toggle. Writing a fake `vpnhide_prefs.xml` +> at `/data/data//shared_prefs/` from a root shell has **no +> effect** — Vector ignores that path. To inspect or seed prefs: +> +> ```sh +> su -c "find /data/misc -name vpnhide_prefs.xml" +> ``` +> +> This is Vector framework behaviour (the modern equivalent of the +> classic `XSharedPreferences` mechanism), not a vpnhide-specific +> quirk. + +### `filesDir` — `/data/user/0/dev.okhsunrog.vpnhide/files/` + +| File | Format | Writer | Reader | Lifetime | +|---|---|---|---|---| +| `vpnhide_zygisk_active` | `key=value`: `version`, `boot_id`, `pid`, `timestamp` | Zygisk module (`zygisk/src/lib.rs`) when the VPN Hide app itself is forked under zygisk hooks | App reads from its own `filesDir` to verify zygisk is hooking the app process; compared against current `boot_id` to detect stale heartbeats; `cleanupStaleZygiskStatus` (`ShellUtils.kt:113`) deletes if stale | per-app-launch (overwritten on each fork) | + +Owner is the app uid (no su involved on read; Zygisk runs in the +forked app process so it has DAC perms to write into the app's own +filesDir). + +### `cacheDir` — `/data/user/0/dev.okhsunrog.vpnhide/cache/` + +Scratch space for short-lived files. Currently used for: +- `vpnhide_lspd_modules_config.db` (`+ -wal`, `+ -shm`) — temporary copies of LSPosed's config DB pulled via su and SQLite-opened read-only, then deleted. See `readLsposedConfig` (`DashboardData.kt:529-612`). + +--- + +## 6. iptables — `vpnhide_out` and `vpnhide_out6` + +Two named chains in the `filter` / `OUTPUT` path, IPv4 and IPv6. +Defined in `portshide/module/vpnhide_ports_apply.sh:21-22`. + +- **Created/populated** by `vpnhide_ports_apply.sh` via `iptables-restore` + / `ip6tables-restore` (`--noflush` so other chains aren't touched). +- **Triggered**: at boot (`portshide/module/service.sh`), and on every + Save in the Ports tab of the app (re-runs the apply script via su). +- **Removed** on module uninstall (`portshide/module/uninstall.sh`). +- **Live in kernel memory only** — no persistence across reboot; + must be re-applied each boot. +- Per-UID rules: target observer apps' UIDs get REJECT for connections + to `127.0.0.0/8` and `::1`. + +The dashboard's "ports active" check is `iptables -L vpnhide_out -n` +in `DashboardData.kt:690` — chain existence implies the apply script +has run successfully this boot. + +--- + +## 7. External / third-party paths the app reads + +Not owned by us, but consulted for diagnostics or wiring. + +| Path | Owner / Source | Purpose | +|---|---|---| +| `/data/adb/lspd/config/modules_config.db` (+ `-wal`, `-shm`) | LSPosed framework (LSPosed-Next / Vector) | Module enabled state, scope packages — read by `readLsposedConfig` in `DashboardData.kt:529` for dashboard display | +| `/data/misc//prefs//` | Vector LSPosed | Redirected SharedPreferences — see § 5 | +| `/proc/sys/kernel/random/boot_id` | Linux kernel | Compared everywhere with stored `boot_id` fields to detect "is this record from the current boot?" | +| `/proc/version`, `/proc/modules`, `/proc/config.gz` | Linux kernel | Read by `kmod/module/post-fs-data.sh` for kernel diagnostics | +| `/proc/net/{route,ipv6_route,if_inet6,tcp,tcp6,udp,udp6,dev,fib_trie}` | Linux kernel | Read by `lsposed/native/src/` diagnostics + filtered by Zygisk hooks (`zygisk/src/hooks.rs`) — a memfd substitute is returned when a target app `openat`'s these | +| `/sys/class/net/`, `/sys/class/net//operstate` | Linux kernel | `isVpnActiveBlocking` (`ShellUtils.kt:96`) iterates interfaces and checks operstate to detect "VPN is up right now" | + +--- + +## 8. Boot-time write sequence (high-level) + +``` +post-fs-data.sh phase (root, before zygote): + kmod/module/post-fs-data.sh + → modprobe / insmod vpnhide_kmod.ko + → write /data/adb/vpnhide_kmod/load_status + → write /data/adb/vpnhide_kmod/load_dmesg + +service.sh phase (root, after boot_completed-ish): + kmod/module/service.sh + → resolve /data/adb/vpnhide_kmod/targets.txt → UIDs + → write /proc/vpnhide_targets + → write /data/system/vpnhide_uids.txt + zygisk/module/service.sh + → cp /data/adb/vpnhide_zygisk/targets.txt → /data/adb/modules/vpnhide_zygisk/targets.txt + → resolve /data/adb/vpnhide_lsposed/targets.txt → UIDs + → append/merge into /data/system/vpnhide_uids.txt + portshide/module/service.sh + → wait for netd + → /data/adb/modules/vpnhide_ports/vpnhide_ports_apply.sh + → resolve /data/adb/vpnhide_ports/observers.txt → UIDs + → iptables-restore --noflush (creates vpnhide_out / vpnhide_out6) + +system_server start (LSPosed framework injects): + HookEntry.handleLoadPackage + → install hooks + → write /data/system/vpnhide_hook_active (with current boot_id) + → start FileObservers on /data/system/ + +zygote forks an app (NeoZygisk): + zygisk/src/lib.rs::on_load (in forked process before specialize) + → read targets via module dir fd + zygisk/src/lib.rs::post_app_specialize + → if target: install libc hooks + → if VPN Hide app itself: write filesDir/vpnhide_zygisk_active +``` + +--- + +## 9. Lifetime cheat-sheet + +| Lifetime class | Examples | +|---|---| +| **In-kernel only (per-boot, volatile)** | `/proc/vpnhide_targets`, `/proc/vpnhide_debug`, iptables `vpnhide_out{,6}` chains | +| **Per-boot** (overwritten each boot by service scripts) | `/data/adb/vpnhide_kmod/load_status`, `/data/adb/vpnhide_kmod/load_dmesg`, `/data/system/vpnhide_uids.txt`, `/data/system/vpnhide_hook_active` | +| **Per-app-launch** (overwritten on each fork) | `/vpnhide_zygisk_active` | +| **Persistent — survives reboot, module reinstall, app reinstall** | `/data/adb/vpnhide_*/targets.txt`, `/data/adb/vpnhide_ports/observers.txt`, `/data/system/vpnhide_hidden_pkgs.txt`, `/data/system/vpnhide_observer_uids.txt`, `/data/system/vpnhide_debug_logging` | +| **Persistent — survives reboot but wiped on module reinstall** | everything inside `/data/adb/modules/vpnhide_*/` (Magisk/KSU replaces the tree from the zip) | +| **Persistent — survives reboot but wiped on app reinstall** | SharedPrefs `vpnhide_prefs` — but stored at the Vector-redirected path under `/data/misc//prefs/` | +| **Wiped only by factory reset** | `/data/adb/vpnhide_*/` persistent dirs + their contents |