mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-10 21:47:02 +01:00
a438652aaa
9 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
| 82e206116a |
Make the geofence tell the truth, and the phone explain itself
The enforcer failed in both directions: nothing blocked at home, and everything blocked at the office. The comparison was never inverted -- `metres <= threshold` was always right. Three separate causes: Only ACCESS_COARSE_LOCATION was declared. Android 12+ fuzzes coarse to a ~1-2 km grid against a 150 m fence, so a fix taken at home routinely landed outside it, read as AWAY, and restored every app the geofence exists to hide. FINE is now declared and self-granted by the device owner, along with coarse and background, re-asserted every pass so a reinstall cannot silently drop it. There were three states but two behaviours. LOCATION_UNKNOWN applies the identical sweep to AT_HOME -- correct fail-closed design, indistinguishable from outside. And nothing ever requested a fix: the pass read whatever another app happened to cache, so at the office with no navigation running the cache was empty. Passes now acquire a fix inside a location-typed foreground service, bounded at 20 s, and treat anything older than 30 min as no fix. That window is deliberately longer than the 15-minute cadence; a shorter one marks the newest cache stale on every pass of an idle phone and blocks everything. None of it was diagnosable. `adb logcat -d -s FocusOwner:V` came back empty while dumpsys showed 82 alarms had fired, and `run-as` is refused on the release build device owner requires. So the pass now writes a ring-buffered JSONL record read back in-app: reason, distance vs the fence, fix age/provider/accuracy, curfew state, and every hidden app with the rule that hid it. Coordinates are never recorded -- a distance is enough to debug a geofence, and this is a home address. Also fixed along the way: - Hysteresis keyed off `isEnforcing`, which is true under AWAY too because YouTube stays hidden, so the threshold was permanently radius+hysteresis. It now keys off whether the previous pass was inside the fence. - The scheduler's curfew-boundary alarms were documented but never scheduled; both callers invoked `scheduleNext()` bare, so 05:00 could land 15 min late. - Installing over the app cancelled its pending alarms, so `adb install -r` silently stopped enforcement until someone opened the app. MY_PACKAGE_REPLACED now re-arms it, verified on device without launching the app. - A wrong-typed log record threw instead of being skipped, and the exception escaped far enough to leave the page stuck busy -- which disables the release-device-owner button, the one control that must work when everything else has gone wrong. - `lib/enforcement.dart` was a second decision layer with no always-blocked concept, cited by KDoc as pinned by a `PolicyParityTest` that never existed. It would have made the new screen report YouTube visible while it is hidden. Deleted; the UI renders the Kotlin record rather than re-deciding. - Anchoring home accepted a stale or unknown-precision fix. A wrong home offsets the fence everywhere and forever, which is what produced a 766 m reading while standing at home. Policy: Play Store is allowed by day (not during curfew) so infakt can be installed and updated; the sweep is default-deny for third-party packages, so anything installed from Play is hidden on the next at-home pass. Tachiyomi's per-source extension apks are covered by a new prefix allowlist rather than an exact list that goes stale whenever an extension is installed. `export.py` stopped subtracting the allowlist from `blockable_system_packages`. Sweepable is not hideable: that list decides which system packages are eligible for a decision, and `isAllowed` is what protects them. Removing an allowed system package from the sweep freezes it in its last state -- it would have stranded Play hidden permanently the moment it was allowlisted. Verified on the Pixel 6a: FINE granted POLICY_FIXED, `applied AT_HOME` with a 0 m distance from a +/-11 m GPS fix, YouTube/Chrome/Firefox absent from `pm list packages -e`, Play Store, infakt, RunnerUp and all five Tachiyomi packages present. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HjxL9kdAzZmGqQA5tMNrZ8 |
|||
| 2d4d2a2eb2 |
Block by hiding apps, which the phone cannot undo
The network layer was the wrong place for this. Tested remotely today: opening RethinkDNS and switching off its device blocklists un-blocked YouTube in a few taps, and no device owner API can stop that -- an on-device filter app always owns its own rules. App hiding is different. 'pm unhide' from adb shell throws a SecurityException and the package stays hidden; only the device owner can reverse it, and reaching that costs a factory reset. So the block moves there, scoped to an explicit allowlist and the home geofence. Both whitelists are rewritten from the apps that actually exist on this phone. The old ones described the rooted Blackview. Every browser is excluded now, so at home a link does not open at all -- confirmed as intended. LAUNCHER_PACKAGE was still the Blackview's launcher, which is not installed here. The exporter refused to render the policy twice over it -- precisely what that check is for. The network layer stays a soft deterrent by choice: pinned and uninstall-blocked, but without lockdown, and its blocklists remain switchable. Private DNS is implemented but ships disabled. stunnel logged zero connections from the handset across two pin attempts, so the router does not forward 853. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| 62862c01ab |
Close the two holes that made the network block advisory
Both were measured, not theorised. `pm uninstall --user 0 com.celzero.bravedns` succeeded while the VPN was pinned and DISALLOW_CONFIG_VPN was set -- I ran it to check whether the provider was protected, and it removed the app, took the DNS filter with it, and left the always-on pin aimed at a package that no longer existed. The second hole was quieter: without lockdown, stopping the tunnel let traffic flow normally, so the filter only held while it was running. So the provider now gets setUninstallBlocked, re-asserted every pass, and the pin runs with lockdown. Lockdown is a real hazard and is deliberate: if the VPN app breaks, the device has no connectivity until ownership is released. That is only survivable because the release path lives in the enforcer app and needs no network. releaseDeviceOwner reads the pinned package before unpinning it, then unblocks its uninstall. Reversed, getAlwaysOnVpnPackage returns null after the unpin and the provider would stay unremovable forever -- the same trap the self-uninstall ordering already avoids. Home coordinates are provisioned. The pass now logs "applied AT_HOME" rather than LOCATION_UNKNOWN, so enforcement is finally geofenced rather than permanent-everywhere, driven by a real fix. `pm uninstall --user 0` on the provider now returns DELETE_FAILED_OWNER_BLOCKED. What this does NOT fix: RethinkDNS's blocklists are gone with the uninstall, since they live in app-private storage. The filter is running with no rules and youtube.com resolves normally until they are re-entered. And nothing here stops those rules being switched off from inside the app -- no device owner API can. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| db1d8f8247 |
Pin RethinkDNS as always-on VPN, the layer that reaches every app
Hiding the YouTube package stops the app. It does nothing about youtube.com in Firefox, in a webview, or in any client nobody has enumerated. This is that layer. setAlwaysOnVpnPackage from the device owner is not the mechanism that failed on 2026-08-09 -- that was `settings put secure always_on_vpn_app`, which writes successfully, survives a reboot, and is ignored by VpnManager. The DPM call takes effect: dumpsys now reports mAlwaysOnVpnPackage=com.celzero.bravedns. Lockdown stays off. It drops all traffic whenever the tunnel is down, and the wipe checklist names it together with the DISALLOW_* restrictions as the combination most likely to leave an unrecoverable device. DISALLOW_CONFIG_VPN is deferred for the same reason: locking VPN configuration before the filter is confirmed working would pin a broken setup in place. The exporter emits always_on_vpn_package only when the provider is also in never_disable_prefixes, and RethinkDNS was added there. Pinning a package the sweep can hide is the worst case -- the restriction would point at a hidden app and cost general connectivity, not just the filter. The pin is re-asserted every pass and, unlike the uninstall block, does not track the decision: the network filter must not lift when the geofence says AWAY. releaseDeviceOwner now unpins the VPN and clears the restriction before clearing ownership, for the same reason it already unblocked uninstall. Not yet active, and the commit does not pretend otherwise: the tunnel needs a one-time in-app VPN consent tap that adb cannot grant, and RethinkDNS is at first-run state because its wildcard rules live in app-private storage that the backup never reached. They have to be re-entered by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| b1de6f8d38 |
Take the geofence out of YouTube's reach, and pin infakt on
Leaving the house was an off switch. The AWAY branch returned early with packagesToShow = every installed package, so walking 150m from home restored YouTube -- which is the exact affordance device owner was provisioned to remove. always_blocked_packages is exempt from all four branches: AWAY hides them, the allowlist loop cannot show them, and the workout release skips them, because otherwise "go for a run" is the off switch instead. Chrome is included as a second route to the same content; Firefox stays, and carries the uBlock filters. Play is deliberately left geofenced so apps can still be installed away from home -- and a hidden package cannot be reinstalled from Play anyway. infakt moves from the allowlist to SYSTEM_NEVER_DISABLE. Allowlisting only held while it stayed in BOTH the day and night lists, and dropping it from either would silently make it hideable; isAllowed() consults the never-disable list before the curfew split, so it now holds under every condition. It is device-paired to a bank over SMS, so losing it strands the same re-authentication chain a hidden Messages app would. The exporter now refuses an always-blocked package that is not also blockable: the runner filters unopted system packages out of installedPackages, so such an entry would never be hidden and nothing would say so. Verified: 128 Python tests at 100% branch coverage, 16 Kotlin tests, the new ones canary-checked by breaking an assertion and confirming the suite actually fails. On device, youtube/music/chrome/vending are hidden=true, infakt hidden=false, uninstall still refused. The AWAY path itself is unit-tested but NOT verified on hardware: with no home_location.json every pass decides LOCATION_UNKNOWN, so the branch is unreachable on this phone until coordinates can be provisioned. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| eeaf9590da |
Let the sweep hide preinstalled apps, one named package at a time
The Device Owner sweep skipped every FLAG_SYSTEM package, which meant it could not touch the apps it exists to block: YouTube ships at /product/app/YouTube as a system app, and so does Chrome. Deleting the filter is not the fix. Measured on the Pixel 6a, that would expose 320 system packages of which 243 match no allowlist entry and no never_disable_prefix -- including com.android.cellbroadcastreceiver (emergency alerts), com.android.credentialmanager and com.android.devicelockcontroller. So system apps stay default-deny and blockable_system_packages is the opt-in. com.android.vending is on the list deliberately: leaving Play reachable makes every other removal a one-tap undo. The list lives in the Python exporter rather than config.sh because it is a safety boundary, and a shell typo should not be able to widen it. The exporter subtracts anything already allowed, so an asset can never state that a package is both allowed and blockable. Kotlin reads the field with a new optionalStringSet: an asset predating the field parses to the empty set instead of throwing, because a throw makes decide() skip the pass and silently disable enforcement. Verified on-device under a profile owner: hid 1 for YouTube, restored 1 when temporarily allowed, and cellbroadcastreceiver, credentialmanager, devicelockcontroller and settings all left hidden=false. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014DqEd1uUhyT52KjaNeLanC |
|||
| f33fd03a16 |
focus_owner: keep the unhide path reachable, never hide the enforcer
setApplicationHidden drops a package from the default PackageManager enumeration even though it stays installed=true, so a sweep querying with flags 0 could never see anything it had already hidden. That made packagesToShow permanently empty: measured on device, a pass following one that hid three apps reported "hid 0, restored 0" and left them hidden with no way back. Query with MATCH_UNINSTALLED_PACKAGES instead. Hiding the enforcer itself is the other unrecoverable case, since it takes the escape hatch and the trigger with it. The exporter now injects com.kuhy.focus_owner into both the day and night allowlists so the decision layer cannot propose it, and the runner keeps its point-of-use skip to cover a policy asset rendered before this fix or hand-edited. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YQLwdquDCNmWtvgi3Lk72J |
|||
| e3d5e1dd18 |
Serve the blocklist over DoT and hand the policy to the app as an asset
Two gaps stopped the unrooted path working end to end. The phone had no way to read the focus policy, and the blocklist was only reachable as a hosts file or over plain UDP/53 on the LAN -- while the rooted enforcer actively rejected every DoT connection, so a self-hosted resolver could not have worked even if one existed. focus_owner now loads a generated policy.json instead of hardcoding anything, so config.sh stays the single source of truth across Python, shell and Kotlin. The committed asset is rendered with --redact-home and a test fails if home coordinates ever appear in it. An unknown schema_version is rejected outright: silently accepting a newer schema means enforcing a policy the app has misread, and a misread allowlist is one that blocks the dialer. dns_enforcer.sh rejected ALL port 853 traffic and pinned Private DNS off, every 20 seconds. That is right when DoT is a bypass channel and exactly wrong when DoT is the enforcement channel, which is what an unrooted phone needs -- it has no hosts file to fall back to. With DNS_TRUSTED_DOT_HOST set it now ACCEPTs the trusted resolver before the blanket REJECT, and pins Private DNS to that hostname so the user cannot pick an unfiltered one. Rule order is load-bearing: iptables takes the first match, so an ACCEPT appended after the REJECT would never be reached. expected_rule_count() counts the new rules, because a mismatch there makes chain_intact() rebuild the chain every 20s -- a silent fork storm that also drops the connection each time. With the variable empty the behaviour is byte-identical, so the rooted phone is untouched until this is deployed. setup_dot_resolver.sh terminates DoT with stunnel rather than Caddy: DoT is a raw TLS stream and stock caddy:2.8 has no layer4 module, so using Caddy would have meant an xcaddy rebuild of a container serving five live sites. It reuses the certificate Caddy already renews, so nothing competes for port 80. It binds to the WireGuard address, not 0.0.0.0 -- a public DoT endpoint is an open resolver, and over the existing tunnel it needs no port-forward, no firewall rule and no rate limiting. Verified: example.com resolves, pornhub.com returns 0.0.0.0, TLS 1.3 against the LE cert, and both the LAN and public addresses refuse. The Device Owner checklist is planning only. Checking the device rather than the config settled the one open risk: Google Wallet is not installed and no NFC payment default is set, so the unconfirmable tap-to-pay question does not apply here. The same check found that Aegis, MS Authenticator, Oracle IDM and PKO IKO are absent -- they are in config.sh's whitelist, which describes the rooted phone, not this one. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL |
|||
| f636d2b21b |
Make focus policy and blocklist usable without root; fix workout CDN unblock
phone_focus_mode is built for a rooted phone: the hosts layer is a read-only
bind mount over /system/etc/hosts, chosen so that even root-in-a-terminal
cannot overwrite it. None of that ports to a stock locked device. This splits
out the parts that are device-independent so one policy can drive both.
python_pkg/focus_policy parses config.sh rather than copying it, so the shell
config stays the source of truth and the two cannot drift. It rejects the
shipped REDACTED_* coordinate placeholder instead of defaulting to (0,0),
which would move "home" to the Atlantic and quietly disable the location gate.
blocklist.py converts the hosts file to domains for a DoT resolver or a local
VpnService. Three properties of the real 194k-line file make the obvious parser
wrong, and all three are handled: generate_hosts_file.sh *comments out*
deliberate unblocks rather than deleting them, so stripping '#' re-blocks
facebook/messenger/linkedin/4chan; the StevenBlack header carries loopback and
IPv6 housekeeping that would blackhole localhost; and ~1060 upstream annotation
tokens ('#[adware.zeno]', '"spam') render as address=/"spam/, which makes
dnsmasq refuse to start -- taking DNS down rather than blocking anything.
Verified with dnsmasq --test over all 185681 generated domains.
strip_workout_hosts.py had the same class of bug on the live rooted phone:
exact set membership meant the allowlisted googlevideo.com never matched the
dynamic r*---sn-*.googlevideo.com hosts YouTube actually streams from, so the
workout unblock was only partly working. Now suffix-matched (17 lines stripped,
was 14). The three new tests were checked against the original code in a
throwaway worktree; two of them fail there, so they pin the fix rather than
passing vacuously.
focus_owner is a device-owner app that deliberately does nothing yet. It ships
clearDeviceOwnerApp() implemented and tested first, because dpm
remove-active-admin does not work on a device owner: an app that locks the
device down before proving it can let go leaves a soft-brick recoverable only
through fastboot on a locked bootloader. Built, installed and launched on the
Pixel 6a; dumpsys afterwards confirms Device Owner Type -1 and no user
restrictions, i.e. the phone is unchanged.
Measured while scoping this, and not yet reflected in any enforcement code:
on Android 16 both pm suspend and pm disable-user are cleared by a reboot, so
shell-level app blocking is per-session only. setApplicationHidden is assumed
to persist under Device Owner but is untested.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AwepyL5Lxs5QKbKkMpxxxL
|