Last active
February 25, 2026 16:24
-
-
Save Konfekt/9296846b86cdb2e9ba3a1e755ae5c59e to your computer and use it in GitHub Desktop.
Reverse tether a local VPN; requires gnirethet and adb
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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