Sanitized mirror from private repository - 2026-04-20 01:32:01 UTC
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
[Unit]
|
||||
Description=Tailscale re-auth watchdog
|
||||
After=network-online.target tailscaled.service
|
||||
Wants=network-online.target
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=/etc/tailscale/watchdog.sh
|
||||
Nice=5
|
||||
10
docs/infrastructure/hosts/deck/tailscale-watchdog.timer
Normal file
10
docs/infrastructure/hosts/deck/tailscale-watchdog.timer
Normal file
@@ -0,0 +1,10 @@
|
||||
[Unit]
|
||||
Description=Run tailscale watchdog every 5 min
|
||||
|
||||
[Timer]
|
||||
OnBootSec=2min
|
||||
OnUnitActiveSec=5min
|
||||
Unit=tailscale-watchdog.service
|
||||
|
||||
[Install]
|
||||
WantedBy=timers.target
|
||||
91
docs/infrastructure/hosts/deck/watchdog.sh
Normal file
91
docs/infrastructure/hosts/deck/watchdog.sh
Normal file
@@ -0,0 +1,91 @@
|
||||
#!/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
|
||||
Reference in New Issue
Block a user