refactor(lsposed): hook ConnectivityService getters instead of writeToParcel#24
Draft
refactor(lsposed): hook ConnectivityService getters instead of writeToParcel#24
Conversation
Skeleton for migrating from Parcelable.writeToParcel hooks (which run in many contexts where Binder.getCallingUid() does not correspond to the eventual recipient) to direct hooks on ConnectivityService getters, where the calling UID is reliably the requester. No behavior change: skeleton is not wired into HookEntry yet.
…ooks Split the VPN-stripping logic out of HookEntry's writeToParcel handlers into a new NetworkSanitizers object with three pure functions (sanitizeNetworkCapabilities, sanitizeNetworkInfo, sanitizeLinkProperties). Each returns a modified copy or null when no change is needed — no in-place mutation of objects the ConnectivityService may be sharing across threads. Wire the previously dormant ConnectivityServiceHooks.install() from HookEntry now that the sanitizers are callable from both code paths. The writeToParcel hooks stay in place as a defensive fallback for callback/broadcast paths the server-side hooks may not yet cover. Array results (NetworkInfo[], Network[], NetworkCapabilities[]) and Network handle hiding are still TODO in ConnectivityServiceHooks. Basic coverage today: NetworkInfo getters, NetworkCapabilities, LinkProperties scalar getters.
9d56f17 to
a761db5
Compare
…le policy Fill in sanitizeArray so NetworkInfo[] (from getAllNetworkInfo) and NetworkCapabilities[] (from getDefaultNetworkCapabilitiesForUser) get per-element sanitization. Length is preserved and VPN elements are replaced with their disguised copies, matching the writeToParcel semantics callers rely on. Network[] and single Network handles are left untouched on purpose: handles are opaque references, and the follow-up getNetworkCapabilities(handle) / getLinkProperties(handle) calls go through our other hooks which do the real filtering. Swapping handles is both harder (need an NC lookup) and not actually required. Also refresh the file-level doc — it's no longer a skeleton.
Mastade
pushed a commit
to Mastade/vpnhide
that referenced
this pull request
Apr 27, 2026
…khsunrog#24 okhsunrog#31 okhsunrog#32 okhsunrog#37) Six small review-list items rolled together — all CI/dev-tooling, no runtime behaviour change. okhsunrog#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. okhsunrog#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. okhsunrog#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. okhsunrog#31 scripts/release.py::patch_file now hard-fails when a regex pattern doesn't match (was silently leaving stale versions). okhsunrog#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. okhsunrog#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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
The current LSPosed hooks live on
Parcelable.writeToParcel()ofNetworkCapabilities,NetworkInfo, andLinkProperties(HookEntry.kt:209-211). Inside each hook we look up the caller viaBinder.getCallingUid()and, if the caller is in the target list, write a sanitised copy into the parcel instead.This works for the common case (a target app calls
ConnectivityManager.getActiveNetworkInfo()over Binder; the response is serialised in system_server in the same Binder context;getCallingUid()returns the right UID). It is, however, architecturally fragile.Problem
writeToParcel()is called in many contexts where the current Binder calling UID is not the eventual recipient of the parcel:ConnectivityServicepostsCONNECTIVITY_ACTION(and friends) withEXTRA_NETWORK_INFO. Serialisation happens onConnectivityService's handler thread; whethergetCallingUid()returns the original requester,SYSTEM_UID, or stale state from the last incoming transaction depends on Android version and timing.NetworkCapabilitiesobject is sent to N subscribers. Modifying it once based on "some" caller's UID is semantically wrong.ConnectivityManagercaches results client-side. Server-sidewriteToParcelmodification interacts oddly with the cache invalidation protocol.In addition, the
writeToParcelhook fires for every serialisation of these objects across the system, even when the caller is not in our target list; for non-targets we exit early after aSet.contains(), but the dispatch itself plus reflection access in the entry block (getLongFieldetc. for the bail-fast check on NC) is still per-call overhead.Symptoms reported
#16 — multiple users on Magisk Kitsune report apps hanging on infinite loading (Ozon, Home Assistant, unofficial 4pda client, slow Chrome cold start) even when the affected app is excluded from the VPN Hide target list, or never added. I am not yet 100% sure this refactor fixes that specific report — it might be unrelated Magisk Kitsune zygisk overhead — but the underlying architecture is brittle regardless and worth fixing on its own.
Proposal
Move filtering to server-side getters in
com.android.server.ConnectivityService— specifically:getActiveNetworkInfo,getActiveNetworkInfoForUid,getNetworkInfo,getNetworkInfoForUid,getAllNetworkInfogetActiveNetwork,getActiveNetworkForUid,getAllNetworksgetNetworkCapabilities,getLinkProperties,getActiveLinkPropertiesgetDefaultNetworkCapabilitiesForUserEach of these runs strictly inside the incoming Binder transaction from the requesting app, so
Binder.getCallingUid()is the real caller without ambiguity. We replace the result viaparam.result = ...inafterHookedMethod. AOSP version differences in method signatures are handled by hooking by name and walkingdeclaredMethods.Open questions / TODO
NetworkCallback, callback delivery sendsNetworkCapabilities/LinkPropertiesto the app's process. Need to figure out the right hook for that. Options:IConnectivityManager#registerNetworkCallbackand wrap the messengergetActiveNetwork()/getAllNetworks()return opaqueNetworkhandles. Hiding a VPN here means returning null / filtering the array, which requires a cross-call lookup of which Network is the VPN. Skeleton has a TODO.ConnectivityManagerto drop its cache for the affected app(s).ConnectivityServiceHooks.install(...)fromHookEntrywith the actual sanitising lambdas (currently those lambdas live as private helpers inHookEntryand need to be extracted / made callable).declaredMethodswalking finds everything we expect on each.What this PR actually contains right now
A skeleton file
ConnectivityServiceHooks.ktwith the hook installation scaffolding, the list of methods to cover, and the dispatch table for sanitising results by type. It is not wired intoHookEntryyet, so there is no behaviour change. The skeleton is intentionally compile-only so this PR can be merged (or kept as draft) without risk while we work through the open questions above.