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) <noreply@anthropic.com>
This commit is contained in:
Krzysztof Rudnicki 2026-08-03 13:33:45 +02:00
parent ed1a224ce7
commit 287512f011
3 changed files with 263 additions and 0 deletions

View File

@ -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 <ephemeral-port> (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"
}

View File

@ -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."
]
}

View File

@ -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'" <<EOF
#!/system/bin/sh
# Installed by phone_focus_mode/enable_persistent_adb.sh.
# Pin adb-over-Wi-Fi to a fixed port so the laptop can always connect without
# anyone tapping through Settings -> 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 "$@"