uvlava/services/dns-updater
Natalie 98cc0fa21d refactor(dns-updater): rename prospector.ct -> sales.ct (public name)
The always-on region-mobile surface is publicly "sales" (the node is still the
Prospector PWA internally). DNS host becomes sales.ct.uvlava.com; the joker.com
CNAME is sales.transquinnftw.com -> sales.ct.uvlava.com. Updated terraform
record, env grant, client examples, README, and tests (8 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-29 14:11:13 -04:00
..
client refactor(dns-updater): rename prospector.ct -> sales.ct (public name) 2026-06-29 14:11:13 -04:00
src refactor(dns-updater): rename prospector.ct -> sales.ct (public name) 2026-06-29 14:11:13 -04:00
test refactor(dns-updater): rename prospector.ct -> sales.ct (public name) 2026-06-29 14:11:13 -04:00
.gitignore feat(dns-updater): self-hosted dyndns2 service for region-mobile nodes 2026-06-29 13:57:20 -04:00
bun.lock feat(dns-updater): self-hosted dyndns2 service for region-mobile nodes 2026-06-29 13:57:20 -04:00
compose.yml feat(dns-updater): self-hosted dyndns2 service for region-mobile nodes 2026-06-29 13:57:20 -04:00
deploy.sh feat(dns-updater): self-hosted dyndns2 service for region-mobile nodes 2026-06-29 13:57:20 -04:00
Dockerfile feat(dns-updater): self-hosted dyndns2 service for region-mobile nodes 2026-06-29 13:57:20 -04:00
env.example refactor(dns-updater): rename prospector.ct -> sales.ct (public name) 2026-06-29 14:11:13 -04:00
package.json refactor(dns-updater): rename prospector.ct -> sales.ct (public name) 2026-06-29 14:11:13 -04:00
README.md refactor(dns-updater): rename prospector.ct -> sales.ct (public name) 2026-06-29 14:11:13 -04:00
tsconfig.json feat(dns-updater): self-hosted dyndns2 service for region-mobile nodes 2026-06-29 13:57:20 -04:00

dns-updater (dns.ct.uvlava.com)

A tiny dyndns2-compatible DNS updater for the uvlava.com zone, backed by the DigitalOcean DNS API.

It exists so region-mobile nodes can repoint their own A record automatically when they move regions — no registrar GUI, no manual dig/edit:

FQDN (moving) Node Lifecycle
live.ct.uvlava.com broadcast relay droplet ephemeral — per show, nearest the broadcaster, torn down after
sales.ct.uvlava.com "sales" surface = Prospector PWA node always-on — follows the operator across regions, stays up

The pretty *.transquinnftw.com names are static CNAMEs onto these (set once, below), so the only records that ever move are inside the zone we control via the DO API. The transquinnftw.com zone at joker.com never changes again.

Why not joker.com DynDNS?

Joker offers dyndns2 too, but with per-host credentials at the registrar and no fleet-wide auth. Since uvlava.com is delegated to DO, we own that zone via the API — so a single self-hosted service gives us one auth model (token → hostname allowlist), keeps transquinnftw.com static, and is the foundation for DNS-01 ACME / multi-zone later.

Endpoints

  • GET /healthz{ "ok": true, "domain": "uvlava.com" }
  • GET|POST /nic/update?hostname=<fqdn>&myip=<ipv4> → dyndns2 response: good <ip> · nochg <ip> · nohost · notfqdn · badauth · 911

myip is optional — omit it and the caller's IP (left-most X-Forwarded-For from the forge Caddy) is used. A token may update only the hostnames in its grant; it cannot touch forge.ct/npm.ct/another node's record.

Auth

Authorization: Bearer <token> (scripts) or HTTP Basic with the token as the password (Authorization: Basic base64(any:<token>), what generic dyndns2 clients send).

Configuration (env / .env)

Var Required Meaning
DO_TOKEN yes DO API token (read/write DNS). Use a dedicated token, not the TF one.
DNS_UPDATER_TOKENS yes JSON: [{"token":"…","hosts":["live.ct.uvlava.com"]}]
DNS_DOMAIN no Zone (default uvlava.com)
PORT no Bind port (default 8090)
TRUST_PROXY no Trust X-Forwarded-For (default true; Caddy fronts it)

Generate node tokens with openssl rand -hex 24. See env.example.

Deploy

Runs on the ct-forge droplet behind the existing Caddy, on a shared edge docker network.

# one-time: put secrets on the droplet
ssh root@forge.ct.uvlava.com 'mkdir -p /opt/dns-updater'
scp env.example root@forge.ct.uvlava.com:/opt/dns-updater/.env   # then edit + fill

./deploy.sh                      # rsync + build + start + wire Caddy vhost + reload

deploy.sh is idempotent: ensures the edge network, attaches Caddy, appends the dns.ct.uvlava.com vhost to /opt/forge/Caddyfile if missing, reloads Caddy, and health-checks the container. (The same wiring is declared in terraform/do/cloud-init/forge.yaml for clean reprovisions.)

One-time registrar setup (manual, at joker.com)

transquinnftw.com lives at joker.com (login natily). After the DO records exist (terraform apply of the records below), add two static CNAMEs there — these never change again:

live   CNAME  live.ct.uvlava.com.
sales  CNAME  sales.ct.uvlava.com.

transquinnftw.com is NOT delegated to DO, so these CNAMEs are added in the joker.com DNS panel by hand, once. The existing *.transquinnftw.com LE wildcard covers TLS for both names (or the node's own Caddy issues per-name).

terraform records

terraform/do/dns.tf creates dns.ct (→ forge) and seeds live.ct / sales.ct with lifecycle { ignore_changes = [value] } — TF makes them exist; this service owns their value at runtime.

Node-side usage

Broadcast relay (per show) — after provisioning the droplet, point live.ct at it (see lilith-platform.livescripts/provision-stream-droplet.sh, which calls):

curl -fsS -H "Authorization: Bearer $DNS_UPDATER_TOKEN" \
  "https://dns.ct.uvlava.com/nic/update?hostname=live.ct.uvlava.com&myip=$DROPLET_IP"

Sales node (always-on, on boot + every 5 min) — the public "sales" surface is the Prospector PWA node. 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:

# on the node, as root:
DNS_HOSTNAME=sales.ct.uvlava.com NODE_TOKEN=<token> \
  ./client/install-client.sh

It installs dyndns-update.sh + a dyndns-updater.timer (OnBootSec + every 5 min). Under the hood each tick is:

curl -fsS -H "Authorization: Bearer $TOKEN" \
  "https://dns.ct.uvlava.com/nic/update?hostname=sales.ct.uvlava.com"

Local dev

bun install
DO_TOKEN=DNS_UPDATER_TOKENS='[{"token":"dev","hosts":["live.ct.uvlava.com"]}]' \
  bun run dev
curl -H 'Authorization: Bearer dev' \
  'http://127.0.0.1:8090/nic/update?hostname=live.ct.uvlava.com&myip=1.2.3.4'