From 287512f0117f4556a612f3b538d5773e462192fb Mon Sep 17 00:00:00 2001 From: Krzysztof Rudnicki Date: Mon, 3 Aug 2026 13:33:45 +0200 Subject: [PATCH] phone_focus_mode: reach the phone over adb without touching it Android's Wireless debugging is deliberately ephemeral: the listener dies when you leave the settings screen or the device sleeps, and it picks a new random port each time. In one session it offered 35545, then 40401, then 43781, and vanished entirely twice. Every deploy therefore started with tapping through Settings and reading a port off the screen. enable_persistent_adb.sh waits for any such transient window, then uses it to pin adbd to port 5555 - the port deploy.sh has always assumed - and installs a Magisk service.d boot script so the same happens on every boot. The phone is on DHCP with no reservation, so the script identifies it by MAC rather than trusting a hardcoded address: a ping sweep populates the neighbour table and the address is looked up there. That beats a router reservation because it needs no router access and works on any network. One bug worth naming: the first version took the first `device` line from `adb devices`. Pairing also leaves an mDNS _adb-tls-connect._tcp entry for the same phone, so it reported port 5555 as reachable while nmap showed it closed. It now matches the exact ip:port. Verified: nmap showed 5555 closed then open across the switch; a run against a deliberately wrong IP reported "phone moved" and connected; and after `adb reboot` the script connected with no arguments and no interaction, with /proc/uptime at 149s and service.adb.tcp.port reading 5555, so the boot script really ran. shellcheck clean. Trade-off accepted by the user: adbd on a fixed port is reachable to anything on the LAN, though it still requires an authorised key. Co-Authored-By: Claude Opus 5 (1M context) --- .../contracts/persistent-adb-2026-08-03.json | 19 ++ .../evidence/persistent-adb-2026-08-03.json | 51 +++++ phone_focus_mode/enable_persistent_adb.sh | 193 ++++++++++++++++++ 3 files changed, 263 insertions(+) create mode 100644 docs/superpowers/contracts/persistent-adb-2026-08-03.json create mode 100644 docs/superpowers/evidence/persistent-adb-2026-08-03.json create mode 100755 phone_focus_mode/enable_persistent_adb.sh diff --git a/docs/superpowers/contracts/persistent-adb-2026-08-03.json b/docs/superpowers/contracts/persistent-adb-2026-08-03.json new file mode 100644 index 00000000..069f6887 --- /dev/null +++ b/docs/superpowers/contracts/persistent-adb-2026-08-03.json @@ -0,0 +1,19 @@ +{ + "title": "phone_focus_mode/enable_persistent_adb.sh: make adb-over-Wi-Fi reachable without anyone touching the phone", + "objective": "Android's Wireless debugging is deliberately ephemeral: the listener dies when you leave the settings screen or the device sleeps, and it picks a new random port every time it comes back (observed three different ports in one session: 35545, 40401, 43781). Every deploy therefore began with tapping through Settings on the phone. Pin adbd to a fixed port that comes back on its own after a reboot, and make the laptop able to find the phone even if DHCP moves it. Success: with the phone merely on the same network - screen off, nothing opened, nothing tapped - `adb connect` reaches it, and it still does after a reboot.", + "acceptance_criteria": [ + "adbd listens on the fixed port 5555, which is the port deploy.sh has always assumed", + "A Magisk service.d boot script re-applies the port on every boot, verified by an actual reboot rather than inferred from the mechanism", + "The laptop finds the phone by MAC when the configured IP is wrong, since the phone is on DHCP and has no reservation; verified by running the script against a deliberately wrong address", + "The script never reports success for a connection it does not actually have - it matches the exact ip:port, not the first `device` line, because pairing also leaves an mDNS _adb-tls-connect._tcp entry for the same phone that made an earlier version report a closed port as reachable", + "It fails loudly and bounded rather than spinning forever when no window ever appears", + "shellcheck clean, set -uo pipefail, no inline logic in another language" + ], + "out_of_scope": [ + "A router DHCP reservation - it would work but needs router access and only helps on one network; MAC discovery is strictly better and needs nothing", + "USB debugging - the point is to not touch the phone at all", + "Turning off Android's Wireless debugging UI - it stays available as the bootstrap path for a phone that has never been set up", + "Any change to what deploy.sh does once connected" + ], + "verifier": "./enable_persistent_adb.sh 192.168.1.64 (installs the boot script and switches the port); nmap -p 5555 -Pn 192.168.1.64 shows open; ./enable_persistent_adb.sh 192.168.1.99 (wrong IP on purpose) reports 'phone moved' and still connects; adb reboot, then ./enable_persistent_adb.sh with no arguments connects with zero interaction; shellcheck enable_persistent_adb.sh" +} diff --git a/docs/superpowers/evidence/persistent-adb-2026-08-03.json b/docs/superpowers/evidence/persistent-adb-2026-08-03.json new file mode 100644 index 00000000..a2a20f8b --- /dev/null +++ b/docs/superpowers/evidence/persistent-adb-2026-08-03.json @@ -0,0 +1,51 @@ +{ + "intent": "Let the laptop reach the phone over adb at any time without anyone opening Settings, tapping a pairing dialog, or reading a port off the screen.", + "scope": [ + "phone_focus_mode/enable_persistent_adb.sh - new script", + "On-device: /data/adb/service.d/99-adb-tcp-5555.sh - new Magisk boot script", + "Non-goal: no change to deploy.sh, which already assumes port 5555", + "Non-goal: no router DHCP reservation - MAC discovery replaces the need for one" + ], + "changes": [ + "Wait for any transient Wireless debugging window (trying a known port first, then an nmap sweep of the ephemeral range), then use it to setprop service.adb.tcp.port=5555 and restart adbd.", + "Install a Magisk service.d boot script that re-applies the port on every boot, so the phone comes back reachable without interaction.", + "Find the phone by MAC when the configured IP is wrong: the phone is on DHCP with no reservation, so a hardcoded address is a latent failure. A ping sweep populates the neighbour table and the MAC is looked up there.", + "Match the exact ip:port when deciding whether a connection exists. The first version took the first `device` line, and since pairing also leaves an mDNS _adb-tls-connect._tcp entry for the same phone, it reported port 5555 as reachable while nmap showed it closed." + ], + "verification": [ + { + "command": "./enable_persistent_adb.sh 192.168.1.64 40401", + "result": "pass", + "evidence": "Installed the boot script (-rwxr-xr-x root root 373) and switched the port; nmap -p 5555 then showed '5555/tcp open' where it had shown 'closed' immediately before." + }, + { + "command": "./enable_persistent_adb.sh 192.168.1.99 # deliberately wrong IP", + "result": "pass", + "evidence": "'phone moved: 192.168.1.99 -> 192.168.1.64' then 'reachable on 5555 at the new address'. MAC discovery works." + }, + { + "command": "adb reboot; ./enable_persistent_adb.sh # no arguments, no interaction", + "result": "pass", + "evidence": "'already reachable on 5555 - nothing to do'. /proc/uptime was 149s so the reboot was real, and getprop service.adb.tcp.port returned 5555, proving the boot script ran rather than a stale adbd surviving." + }, + { + "command": "shellcheck enable_persistent_adb.sh", + "result": "pass", + "evidence": "No findings." + }, + { + "command": "Observed port drift during one session", + "result": "pass", + "evidence": "Wireless debugging offered 35545, then 40401, then 43781, and the listener vanished entirely twice when the screen slept. This is the failure mode the fixed port removes." + } + ], + "risks": [ + "adbd on a fixed port is reachable to anything on the LAN. It still requires an authorised key and an unknown key prompts on-device, but this is a wider surface than ephemeral Wireless debugging. Accepted explicitly by the user.", + "PHONE_MAC is hardcoded to this handset; a new phone needs that constant updated. The script would otherwise silently fail to rediscover a moved device.", + "Credential-encrypted storage means a freshly rebooted, still-locked phone accepts adb but cannot screencap or read app data until it is unlocked once - relevant when scripting post-reboot verification." + ], + "rollback": [ + "rm /data/adb/service.d/99-adb-tcp-5555.sh and reboot, or setprop service.adb.tcp.port -1 followed by stop/start adbd to drop back to USB-only.", + "Confirm with nmap -p 5555 that the port is closed afterwards, and that Wireless debugging still works as the bootstrap path." + ] +} diff --git a/phone_focus_mode/enable_persistent_adb.sh b/phone_focus_mode/enable_persistent_adb.sh new file mode 100755 index 00000000..0d9814db --- /dev/null +++ b/phone_focus_mode/enable_persistent_adb.sh @@ -0,0 +1,193 @@ +#!/bin/bash + +# ============================================================================ +# Make adb-over-Wi-Fi permanent on the phone (port 5555). +# +# Android's "Wireless debugging" is deliberately ephemeral: the listener dies +# when you leave the settings screen or the device sleeps, and it picks a new +# random port every time it comes back. That is why every deploy so far has +# needed someone to tap through the phone first. +# +# This script waits for ANY transient wireless-debugging window, then uses it +# to switch adbd to a fixed port 5555 and installs a Magisk boot script so the +# same thing happens on every boot. After one successful run the phone is +# reachable at $PHONE_IP:5555 forever, with no tapping. +# +# Requires: root (Magisk) on the device, and the host already paired. +# +# Usage: +# ./enable_persistent_adb.sh [PHONE_IP] [KNOWN_PORT] +# PHONE_IP defaults to 192.168.1.64. +# KNOWN_PORT, if given, is tried first before falling back to a port scan. +# ============================================================================ + +set -uo pipefail + +PHONE_IP="${1:-192.168.1.64}" +readonly KNOWN_PORT="${2:-}" +readonly FIXED_PORT=5555 +# The phone is on DHCP, so its address can move. Its MAC cannot, so that is +# what we actually identify it by; PHONE_IP is only a first guess. +readonly PHONE_MAC="00:08:22:68:81:fc" +# Android allocates the wireless-debugging port from the ephemeral range. +readonly SCAN_RANGE="30000-65535" +# Give up rather than spin forever if nobody ever wakes the phone. +readonly DEADLINE_SECONDS=1800 + +readonly BOOT_SCRIPT=/data/adb/service.d/99-adb-tcp-5555.sh + +log() { printf '[persistent-adb] %s\n' "$1"; } + +require() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "Error: $1 is required but not installed" >&2 + exit 1 + fi +} + +# Echo the serial iff THIS endpoint is connected and not offline. +# +# Deliberately matches the exact ip:port rather than the first `device` line. +# After pairing, adb also auto-connects an mDNS `_adb-tls-connect._tcp` entry +# for the same phone; taking the first line made `try_connect 5555` report +# success while port 5555 was in fact closed. +connected_serial() { + local endpoint="$1" + adb devices 2>/dev/null | + awk -v want="$endpoint" '$1 == want && $2 == "device" {print $1; exit}' +} + +# Try to reach adbd. Returns 0 and sets REPLY to the serial when connected. +try_connect() { + local port="$1" endpoint + endpoint="${PHONE_IP}:${port}" + adb connect "$endpoint" >/dev/null 2>&1 || return 1 + # `connect` succeeds optimistically; the device can still be offline. + sleep 2 + REPLY="$(connected_serial "$endpoint")" + [[ -n "$REPLY" ]] +} + +# Locate the phone on the current subnet by MAC and echo its IP. +# +# DHCP can hand the phone a different address at any time, which would break a +# hardcoded PHONE_IP. A ping sweep populates the neighbour table, then we look +# the MAC up in it. Prefer this over a router DHCP reservation because it needs +# no router access and keeps working on any network the laptop joins. +discover_ip_by_mac() { + local subnet + subnet="$(ip -4 route show dev "$(ip -4 route show default | + awk '/default/ {print $5; exit}')" 2>/dev/null | + awk '/proto kernel/ {print $1; exit}')" + [[ -n "$subnet" ]] || return 1 + + nmap -sn -T4 "$subnet" >/dev/null 2>&1 + ip neigh show 2>/dev/null | + awk -v mac="$PHONE_MAC" 'tolower($5) == tolower(mac) {print $1; exit}' +} + +# Find the current ephemeral wireless-debugging port, if one is listening. +scan_for_port() { + nmap -p "$SCAN_RANGE" --open -T4 -Pn --min-rate 3000 "$PHONE_IP" 2>/dev/null | + awk -F/ '/^[0-9]+\/tcp[[:space:]]+open/ {print $1}' | head -1 +} + +# Switch adbd to the fixed port and persist it across reboots. +make_permanent() { + local serial="$1" + + if ! adb -s "$serial" shell 'su -c id' 2>/dev/null | grep -q 'uid=0'; then + echo "Error: no root on the device; cannot install the boot script." >&2 + echo " adb tcpip $FIXED_PORT alone would not survive a reboot." >&2 + return 1 + fi + + log "installing Magisk boot script at $BOOT_SCRIPT" + # service.d runs late in boot, after the network is up, which is what adbd + # needs. Restarting adbd is what actually makes it re-read the port. + adb -s "$serial" shell "su -c 'cat > $BOOT_SCRIPT'" < Wireless debugging first. +setprop service.adb.tcp.port $FIXED_PORT +stop adbd +start adbd +EOF + adb -s "$serial" shell "su -c 'chmod 755 $BOOT_SCRIPT'" + + log "switching adbd to port $FIXED_PORT now" + adb -s "$serial" shell "su -c 'setprop service.adb.tcp.port $FIXED_PORT; stop adbd; start adbd'" + # adbd restarting drops every current connection, including this one. + sleep 4 +} + +main() { + require adb + require nmap + + log "waiting for a wireless-debugging window on $PHONE_IP (up to $((DEADLINE_SECONDS / 60))m)" + log "wake the phone and open Settings -> Developer options -> Wireless debugging" + + local deadline=$((SECONDS + DEADLINE_SECONDS)) + local serial="" + + while ((SECONDS < deadline)); do + # Cheapest first: the fixed port may already be live from a prior run. + if try_connect "$FIXED_PORT"; then + serial="$REPLY" + log "already reachable on $FIXED_PORT - nothing to do" + printf '%s\n' "$serial" + return 0 + fi + + # Not at the expected address — DHCP may have moved it. Re-find the + # phone by MAC before concluding that adbd is down. + local moved + moved="$(discover_ip_by_mac)" + if [[ -n "$moved" && "$moved" != "$PHONE_IP" ]]; then + log "phone moved: $PHONE_IP -> $moved" + PHONE_IP="$moved" + if try_connect "$FIXED_PORT"; then + serial="$REPLY" + log "reachable on $FIXED_PORT at the new address" + printf '%s\n' "$serial" + return 0 + fi + fi + + if [[ -n "$KNOWN_PORT" ]] && try_connect "$KNOWN_PORT"; then + serial="$REPLY" + break + fi + + local found + found="$(scan_for_port)" + if [[ -n "$found" ]] && try_connect "$found"; then + serial="$REPLY" + break + fi + + sleep 5 + done + + if [[ -z "$serial" ]]; then + echo "Error: no wireless-debugging window appeared before the deadline." >&2 + return 1 + fi + + log "connected as $serial" + make_permanent "$serial" || return 1 + + if try_connect "$FIXED_PORT"; then + log "SUCCESS: phone is now reachable at ${PHONE_IP}:${FIXED_PORT}" + log "it will come back on this port after every reboot" + printf '%s\n' "${PHONE_IP}:${FIXED_PORT}" + return 0 + fi + + echo "Error: adbd did not come back on $FIXED_PORT after the switch." >&2 + return 1 +} + +main "$@"