# headscale-ddns — keep `headscale.phasefinal.com` pointed at the NH3 WAN v4 A user timer on nh3-dev, every 10 minutes: read the WAN v4, PATCH the Cloudflare A record if it moved. It is how every mesh client finds the control plane, so a silent failure here eventually costs the mesh. Installed at `~/.local/bin/headscale-ddns.sh` with user units `headscale-ddns.{service,timer}`. **Tracked here since 2026-09-22** — it was running untracked before that, so a fix to it lived on exactly one disk. ## The 2026-09-22 failure, and what it exposed The unit failed at 15:28 (exit 1) after succeeding all afternoon. Cause was transient — `icanhazip.com` did not answer inside its 10s cap, so `$IP` came back empty and the regex guard refused it. No DNS impact: the record already held the right address and the next timer run succeeded. **What actually mattered was that the alarm carried no cause.** Both failure paths were `|| exit 1` in silence, so the failed-START notifier fired correctly and said only "exit status 1". An alarm you cannot act on costs the same triage as no alarm. Two fixes, both verified by making them fail: - **Every exit path now says why** — a missing vault key names the key; an empty token is distinguished from a failed read; a dead WAN lookup says so and adds *"DNS left unchanged"*, which is the fact the reader needs. - **The WAN lookup retries 3×** with announced attempts. One third-party blip should not page a human, and a *silent* retry would hide a degrading dependency. ## ⚠ The vault read is 17 of the script's 18 seconds Measured 2026-09-22: `secret get` takes **~17s**, everything else ~1s. It runs every 10 minutes. That is not a fault — but it bounds any retry budget here, and it is a fleet-wide cost worth knowing: svos-dev's own alarm unit carries the same 17-second note. Any script in a timer that reads the vault pays it. ## Triage ```sh systemctl --user status headscale-ddns.service journalctl --user -u headscale-ddns.service -n 50 --no-pager dig +short headscale.phasefinal.com @1.1.1.1 # the thing that actually matters /home/lkraven/.local/bin/headscale-ddns.sh # safe to run by hand; idempotent ``` A failed unit clears itself on the next timer run; it does not need `reset-failed` unless you want the state gone immediately.