Skip to content

Instantly share code, notes, and snippets.

@Konfekt
Last active February 25, 2026 16:24
Show Gist options
  • Select an option

  • Save Konfekt/9296846b86cdb2e9ba3a1e755ae5c59e to your computer and use it in GitHub Desktop.

Select an option

Save Konfekt/9296846b86cdb2e9ba3a1e755ae5c59e to your computer and use it in GitHub Desktop.
Reverse tether a local VPN; requires gnirethet and adb
#!/usr/bin/env bash
# Ensure a single responsive adb server before running gnirehtet, and prevent
# hangs from stuck or duplicated adb server processes.
# Trace exit on error of program or pipe (or use of undeclared variable).
set -o errtrace -o errexit -o pipefail # -o nounset
# Optionally debug output by supplying TRACE=1.
[[ "${TRACE:-0}" == "1" ]] && set -o xtrace
if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 4))); then
shopt -s inherit_errexit
fi
PS4='+\t '
have() { command -v "$1" >/dev/null 2>&1; }
require_cmd() {
for cmd; do
have "$cmd" || { printf 'Error: missing required command: %s\n' "$cmd" >&2; exit 127; }
done
}
# Enable desktop notifications.
# Preserve existing behavior, and add NOTIFY=1 override.
notify=""
can_notify() { [[ -n "${DBUS_SESSION_BUS_ADDRESS:-}" ]] && have notify-send; }
if [[ "${NOTIFY:-0}" == "1" ]]; then
can_notify && notify=1
else
[[ ! -t 0 ]] && can_notify && notify=1
fi
notify_msg() {
local urgency="$1" summary="$2" body="${3:-}"
[[ -n "${notify:+x}" ]] && notify-send --urgency="$urgency" "$summary" "$body"
}
error_handler() {
local line="$1" bash_line="$2" cmd="$3" rc="$4"
local summary="Error: In ${BASH_SOURCE[0]}, Lines $line and $bash_line, Command $cmd exited with Status $rc"
printf '%s\n' "$summary" >&2
notify_msg critical "$summary"
exit "$rc"
}
trap 'error_handler "$LINENO" "${BASH_LINENO[0]:-?}" "$BASH_COMMAND" "$?"' ERR
if [[ "${1-}" =~ ^-*h(elp)?$ ]]; then
echo "Usage: $0"
exit
fi
cd -- "$(dirname -- "${BASH_SOURCE[0]}")" ||
{ echo "Failed to change directory to script location"; exit 1; }
DNS="10.0.0.0"
# Timeouts in seconds for adb server health checks and restarts.
ADB_PROBE_TIMEOUT="${ADB_PROBE_TIMEOUT:-5}"
ADB_START_TIMEOUT="${ADB_START_TIMEOUT:-8}"
ADB_KILL_TIMEOUT="${ADB_KILL_TIMEOUT:-3}"
KILL_ADB_ON_EXIT="${KILL_ADB_ON_EXIT:-1}"
run_with_timeout() {
local secs="$1"
shift
timeout --preserve-status --kill-after=1s "${secs}s" "$@"
}
adb_server_pids() {
# Return PIDs of the *actual* adb server.
# Prefer PID owning TCP:5037 (one listener by design), then fall back to ps filtering by comm.
if have lsof; then
lsof -nP -iTCP:5037 -sTCP:LISTEN -t 2>/dev/null | sort -u
return 0
fi
if have ss; then
ss -ltnp 2>/dev/null |
awk '
$4 ~ /:5037$/ {
s=$0
while (match(s, /pid=([0-9]+)/, m)) {
print m[1]
s=substr(s, RSTART+RLENGTH)
}
}
' | sort -u
return 0
fi
ps -eo pid=,comm=,args= 2>/dev/null |
awk '$2=="adb" && /fork-server/ && /server/ {print $1}' |
awk '$1 ~ /^[0-9]+$/' |
sort -u
}
adb_ensure_single_server() {
set +o errexit
run_with_timeout "$ADB_PROBE_TIMEOUT" adb devices >/dev/null 2>&1
local probe_rc="$?"
local pids pid_count
pids="$(adb_server_pids || true)"
pid_count="$(awk 'NF{n++} END{print n+0}' <<<"$pids")"
if [[ "$probe_rc" -eq 0 && "$pid_count" -le 1 ]]; then
set -o errexit
return 0
fi
local summary="Info: Restarting adb server"
local body="Probe rc: $probe_rc
Server pids: ${pids:-none}"
printf '%s\n%s\n' "$summary" "$body" >&2
notify_msg low "$summary" "$body"
run_with_timeout "$ADB_KILL_TIMEOUT" adb kill-server >/dev/null 2>&1 || true
pids="$(adb_server_pids || true)"
if [[ -n "${pids//[[:space:]]/}" ]]; then
for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done
sleep 0.5
for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true; done
fi
run_with_timeout "$ADB_START_TIMEOUT" adb start-server >/dev/null 2>&1
local start_rc="$?"
if [[ "$start_rc" -ne 0 ]]; then
set -o errexit
printf 'Error: adb start-server failed or timed out with rc=%s\n' "$start_rc" >&2
return 1
fi
run_with_timeout "$ADB_PROBE_TIMEOUT" adb devices >/dev/null 2>&1
local probe2_rc="$?"
if [[ "$probe2_rc" -ne 0 ]]; then
set -o errexit
printf 'Error: adb devices probe failed after restart with rc=%s\n' "$probe2_rc" >&2
return 1
fi
# Allow brief fork/exec races to settle.
pid_count=99
for _ in 1 2 3 4 5; do
pids="$(adb_server_pids || true)"
pid_count="$(awk 'NF{n++} END{print n+0}' <<<"$pids")"
[[ "$pid_count" -le 1 ]] && break
sleep 0.2
done
if [[ "$pid_count" -gt 1 ]]; then
set -o errexit
printf 'Error: multiple adb server processes detected after restart: %s\n' "$pids" >&2
return 1
fi
set -o errexit
}
find_gnirehtet_pids() {
pgrep -f gnirehtet 2>/dev/null || true
}
terminate_pids() {
local pids="$1" i pid alive
for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done
for ((i = 0; i < 5; i++)); do
alive=""
for pid in $pids; do kill -0 "$pid" 2>/dev/null && alive+="$pid "; done
[[ -z "${alive//[[:space:]]/}" ]] && return 0
sleep 1
done
for pid in $pids; do kill -0 "$pid" 2>/dev/null && kill -KILL "$pid" 2>/dev/null || true; done
}
terminate_job_tree() {
local pid="${1:-}"
[[ -z "$pid" ]] && return 0
kill -TERM "$pid" 2>/dev/null || true
kill -TERM -- "-$pid" 2>/dev/null || true
sleep 0.2
kill -KILL "$pid" 2>/dev/null || true
kill -KILL -- "-$pid" 2>/dev/null || true
}
get_connected_device_ids() {
adb devices 2>/dev/null | awk 'NR>1 && $2=="device" {print $1}'
}
select_device_id() {
local -a devs=()
mapfile -t devs < <(get_connected_device_ids)
((${#devs[@]} == 0)) && { echo ""; return 0; }
[[ -n "${DEVICE_ID:-}" ]] && { echo "$DEVICE_ID"; return 0; }
((${#devs[@]} == 1)) && { echo "${devs[0]}"; return 0; }
printf 'Error: multiple devices connected; set DEVICE_ID explicitly; detected: %s\n' "${devs[*]}" >&2
exit 2
}
adb_track_watchdog() {
# Run event-driven tracking via `adb track-devices`.
# Signal USR1 on disappearance or non-device state (offline, unauthorized, etc).
set +o errexit
set +o errtrace
trap - ERR
local dev="$1" main_pid="$2"
local -a cmd=(adb track-devices)
have stdbuf && cmd=(stdbuf -oL -eL adb track-devices)
local in_snapshot=0 present=0 line adb_pid
coproc ADBTRACK { "${cmd[@]}" 2>/dev/null; }
adb_pid="$ADBTRACK_PID"
while IFS= read -r line <&"${ADBTRACK[0]}"; do
if [[ "$line" == "List of devices attached"* ]]; then
in_snapshot=1
present=0
continue
fi
if ((in_snapshot)) && [[ -z "$line" ]]; then
if ((present == 0)); then
kill -USR1 "$main_pid" 2>/dev/null || true
kill -TERM "$adb_pid" 2>/dev/null || true
return 0
fi
in_snapshot=0
continue
fi
if ((in_snapshot)); then
local id state
read -r id state _ <<<"$line"
[[ "$id" == "$dev" && "$state" == "device" ]] && present=1
fi
done
kill -USR1 "$main_pid" 2>/dev/null || true
kill -TERM "$adb_pid" 2>/dev/null || true
}
gn_pid=""
wd_pid=""
DEVICE_ID=""
adb_cleanup() {
# Kill adb server processes to avoid stale or duplicated servers across runs.
[[ "${KILL_ADB_ON_EXIT:-1}" == "0" ]] && return 0
set +o errexit
run_with_timeout "$ADB_KILL_TIMEOUT" adb kill-server >/dev/null 2>&1 || true
local pids
pids="$(adb_server_pids || true)"
if [[ -n "${pids//[[:space:]]/}" ]]; then
for pid in $pids; do kill -TERM "$pid" 2>/dev/null || true; done
sleep 0.3
for pid in $pids; do kill -KILL "$pid" 2>/dev/null || true; done
fi
}
# Replace the existing cleanup() with this version.
cleanup() {
set +o errexit
set +o errtrace
trap - ERR
terminate_job_tree "${wd_pid:-}"
[[ -n "${gn_pid:-}" ]] && kill -TERM "$gn_pid" 2>/dev/null || true
[[ -n "${wd_pid:-}" ]] && wait "$wd_pid" 2>/dev/null || true
[[ -n "${gn_pid:-}" ]] && wait "$gn_pid" 2>/dev/null || true
adb_cleanup
}
disconnect_handler() {
set +o errexit
set +o errtrace
trap - ERR
[[ -n "${gn_pid:-}" ]] && ! kill -0 "$gn_pid" 2>/dev/null && return 0
local summary="Warning: Android device disconnected"
local body="Device: ${DEVICE_ID:-unknown}
Action: Stopping gnirehtet
Exit: 10"
printf '%s\n%s\n' "$summary" "$body" >&2
notify_msg normal "$summary" "$body"
gnirehtet stop >/dev/null 2>&1 || true
[[ -n "${gn_pid:-}" ]] && kill -TERM "$gn_pid" 2>/dev/null || true
terminate_job_tree "${wd_pid:-}"
exit 10
}
trap cleanup EXIT
trap 'cleanup; exit 130' INT
trap 'cleanup; exit 143' TERM
trap disconnect_handler USR1
require_cmd gnirehtet adb timeout pgrep ps awk tr sort
adb_ensure_single_server
pids="$(find_gnirehtet_pids | awk -v self="$$" -v ppid="${PPID:-0}" '$1 != self && $1 != ppid' | sort -u || true)"
if [[ -n "${pids//[[:space:]]/}" ]]; then
gnirehtet stop >/dev/null 2>&1 || true
terminate_pids "$pids"
fi
DEVICE_ID="$(select_device_id)"
if [[ -z "${DEVICE_ID:-}" ]]; then
summary="Warning: No Android device connected"
body="Action: exiting 10"
printf '%s\n%s\n' "$summary" "$body" >&2
notify_msg normal "$summary" "$body"
exit 10
fi
gnirehtet run -d "$DNS" &
gn_pid="$!"
adb_track_watchdog "$DEVICE_ID" "$$" &
wd_pid="$!"
# Prefer wait -n if available to react quickly to watchdog exit.
if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 3))); then
wait -n "$gn_pid" "$wd_pid" || true
else
wait "$gn_pid" || true
fi
wait "$gn_pid"
rc="$?"
terminate_job_tree "$wd_pid"
wait "$wd_pid" 2>/dev/null || true
exit "$rc"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment