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> |
||
|---|---|---|
| .. | ||
| client | ||
| src | ||
| test | ||
| .gitignore | ||
| bun.lock | ||
| compose.yml | ||
| deploy.sh | ||
| Dockerfile | ||
| env.example | ||
| package.json | ||
| README.md | ||
| tsconfig.json | ||
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.comis NOT delegated to DO, so these CNAMEs are added in the joker.com DNS panel by hand, once. The existing*.transquinnftw.comLE 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.live →
scripts/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'