Magisk before v28 requires META-INF/com/google/android/update-binary
+ updater-script in module zips to extract them; without these the
manager fails with an unpack error (issue #23). Magisk v28+ removed
this requirement, which is why the bug only shows up on older managers.
Added the standard Magisk template (same one already used by the
zygisk module) to portshide and kmod. CI's `(cd module && zip -qr)`
step picks up the new files automatically.
Replace the bash update-version.sh with a python equivalent that also
rotates the changelog (previous top-level → history[0], new empty
top-level for the new VERSION) and regenerates update-json/changelog.md
from the JSON source of truth.
Add changelog-add.py for appending a bilingual entry (EN + RU) to the
upcoming version's section.
Both scripts are uv inline scripts; shared helpers live in _changelog.py.
Address second-pass review of PR #19:
apply.sh was feeding package names to grep as a regex. Package names
contain dots, which regex treats as "any char" — so resolving e.g.
\"com.x.y\" could land on a neighbouring \"comXxXy\" package and
silently block the wrong UID. Switch to awk with literal field
comparison, eliminating the class entirely.
While there, log explicitly when the boot-time pm readiness loop times
out after 30s, so apply.sh's \"0 observer(s)\" message is no longer
ambiguous between "user selected nothing" and "pm never came up".
Minor polish: drop the empty-body null-branch comment in PortsHidingScreen
and simplify body-string construction with joinToString(postfix).
Move UID resolution from Save-time to apply-time so app reinstalls
(which rotate UIDs) get picked up automatically without manual Save.
observers.txt now stores one package name per line — vpnhide_ports_apply.sh
reads it, queries \`pm list packages -U\`, and resolves to UIDs inline
when building the iptables ruleset. Same pattern kmod's service.sh
already uses for its targets.
Other review points:
- uninstall.sh loops \`-D OUTPUT -j …\` so duplicate jumps are fully
cleaned up, and redirects all probes to /dev/null
- PortsHidingScreen detects the module via \`cat module.prop\` rather
than \`[ -d \$dir ]\` — a KSU-Next remove-pending dir stays on disk
until reboot, so bare dir-check misreports install state
- Dashboard observer count now uses the existing countTargets helper,
consistent with kmod/zygisk line counting
- Dropped the awkward \`when (moduleInstalled) { null, true -> Unit;
false -> … return }\` in a Composable — plain if/else reads cleaner
- Comment on apply.sh explaining v4/v6 restores are per-family, not
transactional — if v6 fails, v4 rules are already live
- Simplified buildPortsSaveCommand: no more per-package UID resolver
shell-script construction, just a base64-encoded newline list
Mirror the kmod/zygisk plumbing so KernelSU-Next / Magisk pick up
portshide updates automatically:
- scripts/update-version.sh bumps portshide/module/module.prop along
with the other modules when VERSION changes
- scripts/update-json.sh writes update-json/update-ports.json pointing
at the current release zip
- CI appends updateJson=.../update-ports.json to the portshide
module.prop before zipping, matching kmod/zygisk
- Dashboard reports portshide version mismatches as issues, with the
same up/down/different wording the other modules use
Adds a portshide job mirroring the simple kmod zip packaging (no build
needed, just zip the module directory). Artifact lands next to the
other release zips so the gh-release step picks it up for tagged
builds.
Add a fourth module card under Modules showing ports module install
state, iptables chain active-ness, and configured observer count.
Detected via /data/adb/modules/vpnhide_ports presence plus a quick
`iptables -L vpnhide_out` probe to confirm service.sh has applied the
chain.
Add an issue card when the module is installed but observers.txt is
empty, pointing the user to Protection → Ports.
Replaces the ComingSoonPlaceholder in Protection → Ports with a real
picker. Lists installed apps with a single P chip per row — tapping it
toggles the app as a localhost-proxy-hide observer. On Save, resolves
package names to UIDs, writes /data/adb/vpnhide_ports/observers.txt,
then invokes the portshide module's apply script via su so iptables
rules update immediately (no reboot needed).
If the portshide module isn't installed, shows a hint card explaining
how to install vpnhide-ports.zip via KernelSU-Next or Magisk.
Help dialog becomes mode-aware for Ports too: explains the P role,
what connect() behavior observers see (ECONNREFUSED), and which app
categories are safe to mark as observers (banks / госуслуги /
marketplaces yes; Chromium-based browsers no).
New standalone Magisk / KernelSU-Next module that rejects TCP/UDP
connections from selected UIDs to 127.0.0.1 / ::1 via iptables
owner-match rules. Covers the VPN/proxy detection vector where an app
probes well-known localhost ports (7890, 1080, etc.) via
connect() — the observer gets ECONNREFUSED, indistinguishable from
a real closed port.
Rules live in a dedicated chain `vpnhide_out` / `vpnhide_out6` with a
jump from OUTPUT, applied atomically via iptables-restore. Configured
by /data/adb/vpnhide_ports/observers.txt (one UID per line, UID < 10000
filtered out for safety). service.sh re-applies at boot after netd
finishes its own chain setup. uninstall.sh flushes on module removal.
No C code, no per-kernel builds, no Rust FFI — just a shell script
leveraging the iptables binary that ships with every Android ≥ 4.
Verified on Pixel 8 Pro (Android 16) with iptables 1.8.11 legacy:
observer UID gets REJECT, non-observer UIDs are unaffected.
On GKI 5.10 kernels built with Clang LTO, dev_ifconf() gets inlined
into sock_do_ioctl(). The symbol remains in kallsyms as a dead stub,
so kretprobe registration succeeds but the probe never fires.
Confirmed by disassembly on Xiaomi 13 Lite (5.10.136) and Lenovo
Legion 2 Pro (5.10.101).
On 6.1+, SIOCGIFCONF was moved from sock_do_ioctl() into sock_ioctl()
directly, so hooking sock_do_ioctl would miss it on newer kernels.
sock_ioctl is the file_operations->unlocked_ioctl callback — used as
a function pointer, so LTO cannot inline it. SIOCGIFCONF passes through
it on every kernel version. After it returns, ifconf data is already in
userspace, so we filter uniformly via copy_from_user/copy_to_user with
no version-specific code paths.
Rename the Apps tab to Protection and put a segmented button on top
that selects between three sub-modes: Tun (existing VPN target picker),
Apps (new package-visibility picker with H/O chips per app), and Ports
(placeholder — coming soon).
App Hiding reads/writes vpnhide_hidden_pkgs.txt and vpnhide_observer_uids.txt,
resolves observer package names to UIDs at save time. Self-package is always
added to the hidden list both at app startup (ensureSelfInTargets) and on
Save — managed invisibly, not shown in the app list.
Help dialog is mode-aware: Tun shows the existing L/K/Z hints, Apps shows
the new H/O hints with Russian labels spelling out Hidden / Observer
explicitly, Ports has no help.
Hides selected packages from selected caller UIDs at the PackageManagerService
Binder stub. Filters getInstalled{Packages,Applications}, queryIntent*,
resolve{Intent,Service}, get{Package,Application}Info, getPackageUid,
getPackagesForUid, getInstaller{PackageName,SourceInfo}. Hooks
IPackageManagerBase with PackageManagerService fallback.
Config via /data/system/vpnhide_hidden_pkgs.txt and
/data/system/vpnhide_observer_uids.txt with inotify live-reload. Callers
with UID < 10000 are exempt to avoid breaking installd / LauncherApps.
logcat via su runs as root and can't see app's own log entries on some
devices. Use Runtime.exec("logcat") directly instead, which reads the
app's own log buffer without needing READ_LOGS permission.
- scripts/update-version.sh — updates source files (module.prop, Cargo.toml, etc.)
- scripts/update-json.sh — generates Magisk/KSU update-json files
update-json must be committed AFTER the release is published to avoid
a race where Magisk sees a new version but the zip doesn't exist yet.
dev_ifconf() changed its signature between 5.10 and 5.15:
5.10: dev_ifconf(struct net *, struct ifconf *ifc, int size)
x1 = kernel pointer (caller did copy_from_user)
5.15+: dev_ifconf(struct net *, struct ifconf __user *uifc)
x1 = userspace pointer
The kretprobe handler assumed 5.15+ (copy_from_user on x1), which
silently failed on 5.10 because copy_from_user on a kernel pointer
returns EFAULT. This left SIOCGIFCONF unfiltered — tun0 visible.
Use LINUX_VERSION_CODE to select the right access method at compile
time. Each kmod build already targets a specific GKI generation, so
this is safe.
Reported by users on Android 12 (5.10) and Android 14 (non-GKI 5.10).
On Android 10 and devices with permissive SELinux, netlink RTM_GETLINK
is not blocked by sepolicy. The existing recvmsg hook covers most
callers (bionic getifaddrs, Java NetworkInterface), but code using
recv() goes through recvfrom via a bare branch — a different syscall
path. Hooking recvfrom directly breaks recv (shadowhook overwrites the
branch target), so we hook recv instead (12 bytes, safe for island mode).
Also switch diagnostic checks from recv to recvmsg so they go through
the hooked path, and add a separate recv-based check for full coverage.
ensureSelfInTargets() and loadDashboardState() ran as independent
coroutines, so the dashboard could read selfNeedsRestart=false before
the init completed. On slower devices (Pixel 4a) this caused "Installed,
inactive" instead of the restart prompt.
- Make selfNeedsRestart nullable; show spinner until resolved
- Sync zygisk targets to module dir so next app fork picks them up
- Show "restart app to activate" instead of misleading "inactive"
- Add scripts/clean-device.sh for testing fresh installs via adb
KernelSU-Next and Magisk fetch the changelog URL and display it as text.
Pointing to the GitHub releases HTML page showed raw HTML tags.
Now points to a raw markdown file that renders correctly.
- Detect LSPosed in all known module paths (zygisk_vector, zygisk_lsposed, lsposed)
- Skip LSPosed config warnings when hooks are already active at runtime
- Check all modules for empty targets, not just LSPosed
- Bump version to v0.5.1
The writeToParcel hooks were mutating the real NetworkCapabilities,
NetworkInfo, and LinkProperties objects in beforeHookedMethod and
restoring them in afterHookedMethod. Between before and after, other
ConnectivityService threads could read the mutated object, causing
IllegalStateException in checkNrisConsistency ("This NRI is already
registered") and crashing system_server.
Fix: create a copy of each object, modify the copy, write the copy
to the Parcel, and skip the original writeToParcel via setResult(null).
The original object is never mutated. ThreadLocal re-entrancy guard
prevents infinite recursion when the copy's writeToParcel triggers
the same hook.