#!/usr/bin/env bash # Tailscale re-auth watchdog for Steam Deck. # Runs every 5 min via systemd timer. If tailscale is logged out / stopped, # refreshes the /etc/hosts pin for headscale.vish.gg (Deck's own DNS may fail # when off-LAN) and re-runs `tailscale up` with the stored reusable key. set -u LOG=/var/log/tailscale-watchdog.log AUTHKEY_FILE=/etc/tailscale/authkey HEADSCALE_HOST=headscale.vish.gg LOGIN_SERVER=https://${HEADSCALE_HOST}:8443 TS=/opt/tailscale/tailscale log() { printf '%s %s\n' "$(date -u +%FT%TZ)" "$*" >> "$LOG"; } get_backend_state() { "$TS" status --json 2>/dev/null | python3 -c ' import json, sys try: print(json.load(sys.stdin).get("BackendState", "")) except Exception: print("") ' } need_reauth() { if ! pidof tailscaled >/dev/null; then log "tailscaled not running" return 0 fi local state state=$(get_backend_state) case "$state" in Running) return 1 ;; NeedsLogin|Stopped|NoState|"") log "BackendState=$state"; return 0 ;; *) return 1 ;; esac } resolve_headscale_public() { # DNS-over-HTTPS via Google (then Cloudflare). Returns an A record or empty. python3 - "$HEADSCALE_HOST" <<'PY' import json, sys, urllib.request, urllib.error name = sys.argv[1] for url in ( f"https://dns.google/resolve?name={name}&type=A", f"https://1.1.1.1/dns-query?name={name}&type=A", ): try: req = urllib.request.Request(url, headers={"accept": "application/dns-json"}) with urllib.request.urlopen(req, timeout=4) as r: d = json.load(r) for a in d.get("Answer", []): if a.get("type") == 1: print(a["data"]) sys.exit(0) except Exception: continue sys.exit(1) PY } refresh_hosts_pin() { local ip current ip=$(resolve_headscale_public) || true if [[ -z "$ip" ]]; then log "could not resolve $HEADSCALE_HOST via DoH" return fi current=$(grep -E "[[:space:]]${HEADSCALE_HOST}$" /etc/hosts | awk '{print $1}' | head -1) if [[ "$current" != "$ip" ]]; then sed -i.bak "/[[:space:]]${HEADSCALE_HOST}\$/d" /etc/hosts printf '%s %s\n' "$ip" "$HEADSCALE_HOST" >> /etc/hosts log "pinned $HEADSCALE_HOST -> $ip (was ${current:-none})" fi } if need_reauth; then refresh_hosts_pin if [[ -r "$AUTHKEY_FILE" ]]; then AUTHKEY=$(cat "$AUTHKEY_FILE") if "$TS" up --login-server="$LOGIN_SERVER" --authkey="$AUTHKEY" --accept-routes=false --hostname=deck >> "$LOG" 2>&1; then log "tailscale up succeeded" else log "tailscale up failed (rc=$?)" fi else log "missing $AUTHKEY_FILE" fi fi