diff --git a/services/dns-updater/README.md b/services/dns-updater/README.md index 883b2d5..6d2f089 100644 --- a/services/dns-updater/README.md +++ b/services/dns-updater/README.md @@ -101,11 +101,21 @@ curl -fsS -H "Authorization: Bearer $DNS_UPDATER_TOKEN" \ "https://dns.ct.uvlava.com/nic/update?hostname=live.ct.uvlava.com&myip=$DROPLET_IP" ``` -**Prospector node (on boot / region move)** — a systemd oneshot self-reports its -public IP (`myip` omitted → caller IP): +**Prospector node (always-on, on boot + every 5 min)** — install the reusable +client (`client/`), which self-reports the node's public IP (`myip` omitted → +caller IP) via a systemd timer so the record tracks the node across moves: ```bash -curl -fsS -H "Authorization: Bearer $DNS_UPDATER_TOKEN" \ +# on the node, as root: +DNS_HOSTNAME=prospector.ct.uvlava.com NODE_TOKEN= \ + ./client/install-client.sh +``` + +It installs `dyndns-update.sh` + a `dyndns-updater.timer` (OnBootSec + every +5 min). Under the hood each tick is: + +```bash +curl -fsS -H "Authorization: Bearer $TOKEN" \ "https://dns.ct.uvlava.com/nic/update?hostname=prospector.ct.uvlava.com" ``` diff --git a/services/dns-updater/client/dyndns-update.sh b/services/dns-updater/client/dyndns-update.sh new file mode 100755 index 0000000..08a59fc --- /dev/null +++ b/services/dns-updater/client/dyndns-update.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# Self-report this node's public IP to the dns-updater (dyndns2), repointing its +# own A record. For always-on, region-mobile nodes (e.g. the Prospector PWA on +# lime): run on boot and on a timer so the record tracks the node across moves. +# +# Config: /etc/dyndns-updater/dyndns.conf +# DNS_UPDATER_URL=https://dns.ct.uvlava.com +# DNS_HOSTNAME=prospector.ct.uvlava.com +# DNS_TOKEN_FILE=/etc/dyndns-updater/token # file containing the node token +# +# myip is omitted, so the updater uses the caller's observed public IP. +set -euo pipefail + +CONF="${DYNDNS_CONF:-/etc/dyndns-updater/dyndns.conf}" +[[ -f "$CONF" ]] || { echo "missing config: $CONF" >&2; exit 1; } +# shellcheck disable=SC1090 +source "$CONF" + +: "${DNS_UPDATER_URL:?DNS_UPDATER_URL not set in $CONF}" +: "${DNS_HOSTNAME:?DNS_HOSTNAME not set in $CONF}" +TOKEN_FILE="${DNS_TOKEN_FILE:-/etc/dyndns-updater/token}" +[[ -f "$TOKEN_FILE" ]] || { echo "missing token file: $TOKEN_FILE" >&2; exit 1; } + +TOKEN="$(tr -d '[:space:]' < "$TOKEN_FILE")" +[[ -n "$TOKEN" ]] || { echo "empty token in $TOKEN_FILE" >&2; exit 1; } + +resp="$(curl -fsS --retry 3 --retry-delay 2 --max-time 20 \ + -H "Authorization: Bearer ${TOKEN}" \ + "${DNS_UPDATER_URL}/nic/update?hostname=${DNS_HOSTNAME}")" + +echo "dyndns ${DNS_HOSTNAME}: ${resp}" +# dyndns2 success bodies start with "good" or "nochg"; anything else is a failure. +case "$resp" in + good*|nochg*) exit 0 ;; + *) echo "unexpected response" >&2; exit 1 ;; +esac diff --git a/services/dns-updater/client/dyndns-updater.service b/services/dns-updater/client/dyndns-updater.service new file mode 100644 index 0000000..44fffe8 --- /dev/null +++ b/services/dns-updater/client/dyndns-updater.service @@ -0,0 +1,14 @@ +[Unit] +Description=Self-report public IP to dns.ct.uvlava.com (dyndns2) +Documentation=https://forge.ct.uvlava.com/quinn/uvlava/src/branch/main/services/dns-updater +After=network-online.target +Wants=network-online.target + +[Service] +Type=oneshot +ExecStart=/usr/local/bin/dyndns-update.sh +# Don't let a transient DNS/API failure mark the node degraded; the timer retries. +SuccessExitStatus=0 1 + +[Install] +WantedBy=multi-user.target diff --git a/services/dns-updater/client/dyndns-updater.timer b/services/dns-updater/client/dyndns-updater.timer new file mode 100644 index 0000000..504d158 --- /dev/null +++ b/services/dns-updater/client/dyndns-updater.timer @@ -0,0 +1,14 @@ +[Unit] +Description=Periodically refresh this node's A record via dns.ct.uvlava.com +Documentation=https://forge.ct.uvlava.com/quinn/uvlava/src/branch/main/services/dns-updater + +[Timer] +# Fire shortly after boot, then every 5 minutes so the record tracks the node +# across IP changes / region moves. +OnBootSec=30s +OnUnitActiveSec=5min +AccuracySec=30s +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/services/dns-updater/client/install-client.sh b/services/dns-updater/client/install-client.sh new file mode 100755 index 0000000..315f74d --- /dev/null +++ b/services/dns-updater/client/install-client.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Install the dyndns self-report client on an always-on, region-mobile node +# (e.g. the Prospector PWA node on lime). Run as root ON THE NODE. +# +# Usage: +# DNS_HOSTNAME=prospector.ct.uvlava.com NODE_TOKEN= ./install-client.sh +# +# After install the node refreshes its A record on boot and every 5 minutes. +set -euo pipefail + +DNS_UPDATER_URL="${DNS_UPDATER_URL:-https://dns.ct.uvlava.com}" +DNS_HOSTNAME="${DNS_HOSTNAME:?set DNS_HOSTNAME, e.g. prospector.ct.uvlava.com}" +NODE_TOKEN="${NODE_TOKEN:?set NODE_TOKEN to this node dyndns token}" + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +install -m 0755 "${HERE}/dyndns-update.sh" /usr/local/bin/dyndns-update.sh +install -d -m 0700 /etc/dyndns-updater + +cat >/etc/dyndns-updater/dyndns.conf </etc/dyndns-updater/token +chmod 0600 /etc/dyndns-updater/token + +install -m 0644 "${HERE}/dyndns-updater.service" /etc/systemd/system/dyndns-updater.service +install -m 0644 "${HERE}/dyndns-updater.timer" /etc/systemd/system/dyndns-updater.timer + +systemctl daemon-reload +systemctl enable --now dyndns-updater.timer +# Fire once now so the record is correct immediately. +systemctl start dyndns-updater.service || true + +echo "Installed. Status:" +systemctl --no-pager status dyndns-updater.service | tail -n 5 || true +echo "Timer:" +systemctl list-timers dyndns-updater.timer --no-pager || true