#!/usr/bin/env bash # Injects an SOA record + canonical NS records into each raw Vultr zone # file, then writes the result to zones-prepared/. Source files in zones/ # are never modified. # # Corrections applied to each zone: # 1. Synthesize SOA — Vultr's export omits it. SOA mname is ns1.he.net # because Hurricane Electric secondaries serve the public face of # these zones (hidden-primary architecture). # 2. Strip the source's ns1.vultr.com / ns2.vultr.com NS records and # replace with the five HE nameservers, so AXFR-pulled zones at HE # advertise the correct delegation. # 3. Apex disambiguation: lines starting with leading-TAB-then-TTL get # "@" prepended (Vultr's apex convention vs. RFC 1035 inheritance). # 4. Dot-terminate NS/MX/CNAME rdata (Vultr exports unqualified names). set -euo pipefail SRC_DIR="${SRC_DIR:-zones}" DST_DIR="${DST_DIR:-zones-prepared}" ADMIN_EMAIL="${ADMIN_EMAIL:-admin}" # becomes admin.. # Serial number generation — YYYYMMDDNN format (RFC 1912 §2.2). # # Strategy: every `make prep` run produces a strictly-increasing serial # so that HE slaves notice the change on their next poll. If today's # previous serials exist in zones-prepared/, increment the 2-digit # counter. Otherwise start at NN=01. # # Honors an explicit override: `SERIAL=2026051699 make prep` skips the # auto-detection. TODAY=$(date +%Y%m%d) if [[ -z "${SERIAL:-}" ]]; then # Pull the highest YYYYMMDDNN serial from currently-prepared zones # that starts with today's date. If none, default to NN=01. # `|| true` so a zero-match grep doesn't trip `set -e`. Empty $highest # then triggers the "first run of the day" branch below. highest=$(grep -hE '^[[:space:]]+'"${TODAY}"'[0-9]{2}[[:space:]]+;' "$DST_DIR"/*.zone 2>/dev/null \ | awk '{print $1}' | sort -un | tail -1 || true) if [[ -n "$highest" ]]; then nn=$((10#${highest:8:2})) next_nn=$((nn + 1)) if (( next_nn > 99 )); then echo "ERROR: serial counter exhausted for ${TODAY} (NN=99 reached)." >&2 echo "Set SERIAL manually or wait until tomorrow." >&2 exit 1 fi SERIAL=$(printf "%s%02d" "$TODAY" "$next_nn") else SERIAL="${TODAY}01" fi fi # Public-facing nameservers (Hurricane Electric free secondary service). # These appear in NS records inside every zone so that recursive # resolvers fetching the zone learn the correct delegation. HE_NAMESERVERS=( "ns1.he.net." "ns2.he.net." "ns3.he.net." "ns4.he.net." "ns5.he.net." ) mkdir -p "$DST_DIR" count=0 for src in "$SRC_DIR"/*.zone; do fname=$(basename "$src") zone="${fname%.zone}" dst="$DST_DIR/$fname" { echo "; Auto-prepared by scripts/prepare-zones.sh on $(date -Iseconds)" echo "; Source: $src" echo "\$ORIGIN ${zone}." echo "\$TTL 3600" echo "@ 3600 IN SOA ns1.he.net. ${ADMIN_EMAIL}.${zone}. (" echo " ${SERIAL} ; serial — bump per change (SERIAL=YYYYMMDDNN make prep)" echo " 300 ; refresh (5 min) — slaves poll us this often;" echo " ; tightened from 3600 to nudge HE's internal" echo " ; puller→anycast replication" echo " 120 ; retry (2 min) — kept < refresh per RFC 1912" echo " 604800 ; expire (1 week)" echo " 60 ; minimum (1 min) — negative-cache TTL on public" echo " ; resolvers; shrinks the window when an old" echo " ; NXDOMAIN keeps showing after we add a name" echo " )" echo "" # Inject HE nameservers as the authoritative NS set. for ns in "${HE_NAMESERVERS[@]}"; do echo "@ 3600 IN NS ${ns}" done echo "" # Strip source's own $ORIGIN / $TTL / comments AND drop ns?.vultr.com # NS records (we just emitted HE's NS set above). Then run the awk # transformations for apex disambiguation and rdata dot-termination. grep -vE '^\$(ORIGIN|TTL)|^;' "$src" \ | grep -vE '[[:space:]]NS[[:space:]]+ns[12]\.vultr\.com\.?[[:space:]]*$' \ | awk ' NF == 0 { print; next } { # (a) Detect Vultr-style apex line: leading whitespace, then TTL, # then "IN". Prepend "@" so the owner is explicit. if ($0 ~ /^[[:space:]]+[0-9]+[[:space:]]+IN[[:space:]]/) { sub(/^[[:space:]]+/, "@\t", $0) } # (b) Dot-terminate trailing hostname for NS/CNAME/MX rdata. type = "" for (i = 1; i <= NF; i++) { if ($i == "NS" || $i == "CNAME" || $i == "MX") { type = $i; break } } if (type != "") { target = $NF if (index(target, ".") > 0 && substr(target, length(target), 1) != ".") { sub(/[[:space:]]+$/, "", $0) print $0 "." next } } print } ' } > "$dst" count=$((count + 1)) done echo "Prepared ${count} zone files in ${DST_DIR}/ (serial=${SERIAL})"