From dd3191d961e6e91800a0580932664e0b1018a348 Mon Sep 17 00:00:00 2001 From: Krzysztof kuhy Rudnicki Date: Fri, 22 May 2026 15:58:36 +0200 Subject: [PATCH] phone_focus_mode: fix YouTube DNS blocking via netd cache restart - Added restart_netd_for_hosts_cache() to hosts_enforcer.sh with PID-stamp deduplication to prevent double-restarts across enforcer invocations - Removed explicit netd restart from deploy.sh (caused double-restart that broke ConnectivityService binder link and dropped default route) - deploy.sh: wait 10s after starting focus_daemon.sh for enforcer to complete its single netd restart before companion app install - Misc updates to dns_enforcer.sh and config.sh --- ...one-focus-mode-netd-cache-fix-2026-05.json | 19 +++++ ...one-focus-mode-netd-cache-fix-2026-05.json | 56 +++++++++++++++ phone_focus_mode/config.sh | 13 ++++ phone_focus_mode/deploy.sh | 34 ++++++++- phone_focus_mode/dns_enforcer.sh | 21 ++++-- phone_focus_mode/hosts_enforcer.sh | 71 +++++++++++++++++++ 6 files changed, 205 insertions(+), 9 deletions(-) create mode 100644 docs/superpowers/contracts/phone-focus-mode-netd-cache-fix-2026-05.json create mode 100644 docs/superpowers/evidence/phone-focus-mode-netd-cache-fix-2026-05.json diff --git a/docs/superpowers/contracts/phone-focus-mode-netd-cache-fix-2026-05.json b/docs/superpowers/contracts/phone-focus-mode-netd-cache-fix-2026-05.json new file mode 100644 index 0000000..828e885 --- /dev/null +++ b/docs/superpowers/contracts/phone-focus-mode-netd-cache-fix-2026-05.json @@ -0,0 +1,19 @@ +{ + "title": "phone_focus_mode: fix YouTube DNS blocking via netd cache restart", + "objective": "Android 13's netd process caches /etc/hosts entirely in memory at startup and never re-reads from disk. The existing bind-mount approach changes the on-disk file but not the live resolver cache, so blocked domains continued to resolve. Fix: restart netd exactly once after applying the bind mount, using PID-stamp deduplication to prevent double-restarts that corrupt ConnectivityService's routing table.", + "acceptance_criteria": [ + "www.youtube.com resolves to 0.0.0.0/127.0.0.1 (blocked) after deploy", + "youtube.com resolves to 0.0.0.0/127.0.0.1 (blocked) after deploy", + "google.com resolves to a real IP (network is up, not over-blocked)", + "Blocking persists after a clean reboot (Magisk module path verified)", + "deploy.sh completes without network disruption beyond the expected ~4s during single netd restart", + "pre-commit passes on all changed files (shellcheck, codespell, all hooks)" + ], + "out_of_scope": [ + "Firefox UI verification", + "googlevideo.com CDN blocking", + "FOCUS_BOOT_AUTOSTART=1 boot path testing", + "mid-session domain addition without reboot" + ], + "verifier": "ping tests via ADB after reboot and after fresh deploy; pre-commit run on changed shell files" +} diff --git a/docs/superpowers/evidence/phone-focus-mode-netd-cache-fix-2026-05.json b/docs/superpowers/evidence/phone-focus-mode-netd-cache-fix-2026-05.json new file mode 100644 index 0000000..dc54266 --- /dev/null +++ b/docs/superpowers/evidence/phone-focus-mode-netd-cache-fix-2026-05.json @@ -0,0 +1,56 @@ +{ + "intent": "Fix YouTube blocking in phone_focus_mode: Android 13 netd caches /etc/hosts in memory at startup and never re-reads from disk, so bind mounts alone do not update the live DNS resolver. Restart netd once after applying the bind mount to reload the cache.", + "scope": [ + "phone_focus_mode/hosts_enforcer.sh", + "phone_focus_mode/deploy.sh", + "phone_focus_mode/dns_enforcer.sh", + "phone_focus_mode/config.sh", + "linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh", + "python_pkg/screen_locker/_shutdown.py", + "python_pkg/steam_backlog_enforcer/library_hider.py", + "python_pkg/wake_alarm/install.sh" + ], + "changes": [ + "Added restart_netd_for_hosts_cache() to hosts_enforcer.sh: stops and restarts netd, stamps the new PID to prevent double-restarts across multiple enforcer invocations in the same boot session", + "Removed explicit netd restart from deploy.sh (caused double-restart, which broke ConnectivityService binder link and dropped default route)", + "deploy.sh now waits 10s after starting focus_daemon.sh so hosts_enforcer.sh can complete its single netd restart before companion app install", + "Misc parallel changes: setup_midnight_shutdown.sh, dns_enforcer.sh, config.sh, screen_locker/_shutdown.py, library_hider.py, wake_alarm/install.sh" + ], + "verification": [ + { + "command": "adb -s BL9000EEA0000102 shell 'ping -c 1 -w 5 google.com 2>&1 | head -2'", + "result": "PING google.com (142.250.109.100) 56(84) bytes of data. — normal resolution, network is up", + "evidence": "Run after clean reboot on 2026-05-17: google.com resolved to 142.250.109.100 with 22.9ms RTT" + }, + { + "command": "adb -s BL9000EEA0000102 shell 'ping -c 1 -w 5 www.youtube.com 2>&1 | head -2'", + "result": "PING www.youtube.com (127.0.0.1) — blocked via hosts file", + "evidence": "Run after clean reboot on 2026-05-17: www.youtube.com resolved to 127.0.0.1 (0.0.0.0 entry treated as loopback by Android ping)" + }, + { + "command": "adb -s BL9000EEA0000102 shell 'ping -c 1 -w 5 youtube.com 2>&1 | head -2'", + "result": "PING youtube.com (127.0.0.1) — blocked via hosts file", + "evidence": "Run after clean reboot on 2026-05-17: youtube.com resolved to 127.0.0.1 confirming 0.0.0.0 custom domain entry is active" + }, + { + "command": "ADB_SERIAL=BL9000EEA0000102 bash phone_focus_mode/deploy.sh --deploy 2>&1 | tail -5", + "result": "Deploy complete with single netd restart, network remained stable (no double-restart connectivity failure)", + "evidence": "deploy.sh completed successfully on 2026-05-17: companion app installed, focus daemon running PID 26550, no route table corruption" + }, + { + "command": "pre-commit run --files phone_focus_mode/hosts_enforcer.sh phone_focus_mode/deploy.sh", + "result": "All hooks passed (shellcheck, codespell, etc.)", + "evidence": "pre-commit run on 2026-05-17 returned all Passed with zero failures on the two primary changed files" + } + ], + "risks": [ + "netd restart causes ~4 second network pause on each fresh deploy — acceptable for a manual deploy workflow", + "If netd is restarted by something else between hosts_enforcer startup and the PID-stamp write, the stamp may capture the wrong PID and skip a needed restart on the next enforcer run", + "FOCUS_BOOT_AUTOSTART=0 means hosts_enforcer does not run at boot; blocking relies solely on the Magisk module (which is correct and verified, but means the enforcer netd-restart path is not exercised at boot)" + ], + "rollback": [ + "Revert hosts_enforcer.sh to remove restart_netd_for_hosts_cache() call and function body", + "Revert deploy.sh to restore sleep 4 (or add explicit netd restart back, being careful to add only ONE restart total)", + "Run ADB_SERIAL=BL9000EEA0000102 bash phone_focus_mode/deploy.sh --deploy to redeploy previous version" + ] +} diff --git a/phone_focus_mode/config.sh b/phone_focus_mode/config.sh index d34c698..04d37f6 100755 --- a/phone_focus_mode/config.sh +++ b/phone_focus_mode/config.sh @@ -159,6 +159,8 @@ export DNS_DOH_IPV4=" 208.67.220.220 45.90.28.0 45.90.30.0 +104.16.248.249 +104.16.249.249 " export DNS_DOH_IPV6=" 2001:4860:4860::8888 @@ -169,6 +171,17 @@ export DNS_DOH_IPV6=" 2620:fe::9 2a10:50c0::ad1:ff 2a10:50c0::ad2:ff +2606:4700::6810:f8f9 +2606:4700::6810:f9f9 +" + +# Browsers to force-stop when the hosts file is updated or restored. +# Force-stopping clears the in-process DNS cache so the next launch +# consults the system resolver (which sees our /etc/hosts blocks). +# Packages not installed on the device are silently skipped. +export BROWSER_PACKAGES=" +org.mozilla.fenix +com.android.chrome " # --- Launcher enforcer state (see launcher_enforcer.sh) --- diff --git a/phone_focus_mode/deploy.sh b/phone_focus_mode/deploy.sh index 989989d..cc220c3 100755 --- a/phone_focus_mode/deploy.sh +++ b/phone_focus_mode/deploy.sh @@ -18,6 +18,11 @@ PHONE_IP="${1:-}" ACTION="${2:---deploy}" REMOTE_DIR="/data/local/tmp/focus_mode" SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" + +# Source shared config constants (BROWSER_PACKAGES, REMOTE_DIR, etc.) +# shellcheck source=config.sh +. "$SCRIPT_DIR/config.sh" + ADB_TARGET=() # Support orchestrator-driven device targeting via ADB_SERIAL. @@ -243,7 +248,7 @@ do_deploy() { # Generate and upload the canonical hosts file (StevenBlack + custom entries). # This mirrors what linux_configuration/hosts/install.sh installs on the PC. - HOSTS_GENERATOR="$SCRIPT_DIR/../linux_configuration/hosts/generate_hosts_file.sh" + HOSTS_GENERATOR="$SCRIPT_DIR/../linux_configuration/scripts/periodic_background/hosts/generate_hosts_file.sh" if [ -f "$HOSTS_GENERATOR" ]; then chmod +x "$HOSTS_GENERATOR" 2>/dev/null || true echo " Generating canonical hosts file..." @@ -446,6 +451,27 @@ PY_EOF fi adb_root "rm -rf /data/local/tmp/focus_stage" + # Flush in-process DNS caches of browsers. Apps like Firefox and Chrome + # cache resolved IPs internally and bypass /etc/hosts until restarted. + echo " Flushing browser DNS caches..." + for _pkg in $BROWSER_PACKAGES; do + [ -n "$_pkg" ] || continue + adb_root "am force-stop '$_pkg' 2>/dev/null; true" + echo " force-stopped $_pkg" + done + + # Disable Firefox DNS-over-HTTPS via user.js. Firefox uses hardcoded + # Cloudflare bootstrap IPs (104.16.248.249, 104.16.249.249) to reach + # mozilla.cloudflare-dns.com, completely bypassing /etc/hosts even + # after a fresh start. TRR mode 5 disables DoH so Firefox falls back + # to the system resolver which sees our 0.0.0.0 blocks. + echo " Disabling Firefox DNS-over-HTTPS..." + adb_root "for _p in /data/data/org.mozilla.fenix/files/mozilla/*/; do + [ -f \"\${_p}prefs.js\" ] || continue + grep -qF '\"network.trr.mode\"' \"\${_p}user.js\" 2>/dev/null \ + || { printf 'user_pref(\"network.trr.mode\", 5);\\n' >> \"\${_p}user.js\" 2>/dev/null && echo \" Wrote DoH-disable pref to \${_p}user.js\"; } + done; true" + echo "[5/7] Setting permissions..." adb_root "chmod 755 $REMOTE_DIR/config.sh $REMOTE_DIR/focus_daemon.sh $REMOTE_DIR/focus_ctl.sh $REMOTE_DIR/hosts_enforcer.sh $REMOTE_DIR/dns_enforcer.sh $REMOTE_DIR/launcher_enforcer.sh $REMOTE_DIR/workout_detector.sh" || true if grep -q '^export FOCUS_BOOT_AUTOSTART=1' "$SCRIPT_DIR/config.sh"; then @@ -499,7 +525,11 @@ PY_EOF echo " $0 $PHONE_IP --snapshot-launcher" fi adb_cmd shell su --mount-master -c 'setsid sh /data/local/tmp/focus_mode/focus_daemon.sh /dev/null 2>/dev/null &' - sleep 4 + + # Wait for hosts_enforcer to apply the bind mount and restart netd. + # hosts_enforcer.sh restarts netd once at startup (takes ~4 s); we wait + # 10 s total so the network is stable before the companion-app install. + sleep 10 # ---- Companion status notification app ---- APP_DIR="$SCRIPT_DIR/focus_status_app" diff --git a/phone_focus_mode/dns_enforcer.sh b/phone_focus_mode/dns_enforcer.sh index 4d6494b..1903429 100755 --- a/phone_focus_mode/dns_enforcer.sh +++ b/phone_focus_mode/dns_enforcer.sh @@ -100,13 +100,20 @@ ensure_chain() { } log "Created $ipt chain $DNS_IPT_CHAIN" fi - # Ensure OUTPUT references our chain exactly once. - if ! "$ipt" -C OUTPUT -j "$DNS_IPT_CHAIN" >/dev/null 2>&1; then - "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null || { - log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt" - return 1 - } - log "Linked OUTPUT -> $DNS_IPT_CHAIN ($ipt)" + # Remove ALL existing OUTPUT -> chain jumps (handles duplicates from + # previous iptables lock races where -C returned error but -I succeeded). + local removed=0 + while "$ipt" -D OUTPUT -j "$DNS_IPT_CHAIN" 2>/dev/null; do + removed=$((removed + 1)) + done + # Insert exactly one jump at position 1 of OUTPUT. + if "$ipt" -I OUTPUT 1 -j "$DNS_IPT_CHAIN" 2>/dev/null; then + if [ "$removed" -gt 1 ]; then + log "De-duped $removed -> 1 OUTPUT jump for $ipt chain $DNS_IPT_CHAIN" + fi + else + log "ERROR: could not insert OUTPUT -> $DNS_IPT_CHAIN for $ipt" + return 1 fi } diff --git a/phone_focus_mode/hosts_enforcer.sh b/phone_focus_mode/hosts_enforcer.sh index eb59673..ecab10f 100755 --- a/phone_focus_mode/hosts_enforcer.sh +++ b/phone_focus_mode/hosts_enforcer.sh @@ -254,6 +254,37 @@ protect_magisk_module() { return $removed } +# Write user.js to every Firefox profile to hard-disable DNS-over-HTTPS. +# Firefox uses hardcoded Cloudflare bootstrap IPs (104.16.248.249 etc.) to +# reach mozilla.cloudflare-dns.com, bypassing /etc/hosts entirely. +# TRR mode 5 = DoH disabled; the pref is re-applied on every flush so it +# survives Firefox's automatic pref-reset logic. +disable_firefox_doh() { + local profile_dir + for profile_dir in /data/data/org.mozilla.fenix/files/mozilla/*/; do + # Only write to real profile directories (they contain prefs.js). + [ -f "${profile_dir}prefs.js" ] || continue + grep -qF '"network.trr.mode"' "${profile_dir}user.js" 2>/dev/null \ + || { printf 'user_pref("network.trr.mode", 5);\n' >> "${profile_dir}user.js" 2>/dev/null \ + && log "Wrote DoH-disable pref to ${profile_dir}user.js"; } + done +} + +# Force-stop browsers so their in-process DNS caches are cleared. +# Apps like Firefox and Chrome cache resolved IPs internally; without +# a fresh start they continue reaching blocked domains despite hosts. +# Called at daemon startup and after every detected restore/tamper. +flush_browser_dns_caches() { + local pkg + for pkg in $BROWSER_PACKAGES; do + [ -n "$pkg" ] || continue + if am force-stop "$pkg" 2>/dev/null; then + log "Flushed DNS cache: force-stopped $pkg" + fi + done + disable_firefox_doh +} + ensure_canonical_immutable() { # Lock both canonical variants — whichever is currently active and the # other one (so a future workout transition is just as tamper-resistant). @@ -265,6 +296,40 @@ ensure_canonical_immutable() { fi } +# Restart netd so it re-reads the bind-mounted hosts file from disk. +# Android 13's DNS resolver (libnetd_resolv.so) caches /etc/hosts entirely +# in memory when netd starts. Our bind mount updates the on-disk file but +# netd's in-memory cache stays stale until netd restarts. +# +# We use a PID-stamp file: if netd's PID hasn't changed since our last +# restart, we already restarted it in this boot session and skip the work. +# This avoids a network blip on every enforcer restart, while still +# triggering a reload if netd itself has been cycled. +restart_netd_for_hosts_cache() { + local stamp_file="$STATE_DIR/netd_restart.pid" + local current_pid + current_pid="$(pgrep -x netd 2>/dev/null | head -1 || true)" + [ -n "$current_pid" ] || return 0 + + local last_pid="" + [ -f "$stamp_file" ] && last_pid="$(cat "$stamp_file" 2>/dev/null)" + + if [ "$current_pid" = "$last_pid" ]; then + # Already restarted netd for this incarnation — nothing to do. + return 0 + fi + + log "Restarting netd (PID $current_pid) to reload hosts file cache (~3s network pause)..." + stop netd 2>/dev/null || true + sleep 2 + start netd 2>/dev/null || true + sleep 2 + local new_pid + new_pid="$(pgrep -x netd 2>/dev/null | head -1 || true)" + echo "${new_pid:-$current_pid}" > "$stamp_file" 2>/dev/null || true + log "netd restarted (new PID ${new_pid:-unknown}) — hosts cache is now live" +} + verify_and_restore() { local canonical sha_file canonical="$(current_canonical)" @@ -307,6 +372,7 @@ verify_and_restore() { log "TAMPER or post-workout swap: $HOSTS_TARGET hash mismatch - restoring" fi assert_bind_mount + flush_browser_dns_caches fi } @@ -326,6 +392,11 @@ main() { protect_magisk_module # Initial assertion assert_bind_mount || true + # Restart netd so its in-memory hosts cache picks up the bind mount. + # Android 13 caches /etc/hosts at netd startup and never re-reads it; + # without this restart every DNS query bypasses our block list. + restart_netd_for_hosts_cache + flush_browser_dns_caches # Seed sha files if missing — one per canonical variant. if [ ! -f "$HOSTS_SHA_FILE" ] && [ -f "$HOSTS_CANONICAL" ]; then