92 lines
2.5 KiB
Bash
92 lines
2.5 KiB
Bash
#!/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
|