#!/usr/bin/env bats # Seam: LAN DNS (dnsmasq) on dankclaw. # # The dnsmasq quadlet is the LAN's authoritative DNS for the flat # franksdank.cloud / franksdank.casa name scheme. Every Caddy vhost name # must have an A record pointing at the host that runs it, records must # stay inside the LAN, and the quadlet must be boot-persistent. load helpers DNSMASQ_DIR="$REPO_ROOT/dankclaw/dnsmasq" QUADLET="$DNSMASQ_DIR/dnsmasq.container" CONF="$DNSMASQ_DIR/dnsmasq.conf" HOSTS="$DNSMASQ_DIR/addn-hosts" @test "the dnsmasq quadlet container file exists" { [ -f "$QUADLET" ] } @test "the dnsmasq quadlet is installed for boot" { grep -q 'WantedBy=default.target' "$QUADLET" } @test "the dnsmasq quadlet publishes port 53 tcp and udp" { grep -qE '^PublishPort=53:53/tcp$' "$QUADLET" grep -qE '^PublishPort=53:53/udp$' "$QUADLET" } @test "the dnsmasq quadlet restarts on failure" { grep -q '^Restart=always' "$QUADLET" } @test "dnsmasq.conf exists and pins upstream resolvers" { [ -f "$CONF" ] grep -q '^no-resolv' "$CONF" grep -qE '^server=[0-9.]+$' "$CONF" } @test "dnsmasq.conf loads the addn-hosts record file" { grep -q '^addn-hosts=' "$CONF" } @test "every addn-hosts line is a valid IPv4 address plus hostnames" { local bad=0 line while IFS= read -r line; do [ -z "$line" ] && continue case "$line" in \#*) continue ;; esac if ! echo "$line" | grep -qE '^([0-9]{1,3}\.){3}[0-9]{1,3}[[:space:]]+[a-z0-9.-]+([[:space:]]+[a-z0-9.-]+)*$'; then echo "BAD LINE: $line" bad=1 fi done < "$HOSTS" [ "$bad" -eq 0 ] } @test "every addn-hosts IP is inside the homelab LAN" { ! awk '!/^[[:space:]]*#/ && NF {print $1}' "$HOSTS" \ | grep -vE '^(192\.168\.0\.[0-9]{1,3})$' \ | grep -q . } @test "every addn-hosts name is under franksdank.cloud or franksdank.casa" { local bad=0 line ip n while IFS= read -r line; do [ -z "$line" ] && continue case "$line" in \#*) continue ;; esac ip="$(echo "$line" | awk '{print $1}')" for n in $(echo "$line" | cut -d' ' -f2-); do echo "$n" | grep -qE '\.(franksdank\.(cloud|casa))$' \ || { echo "UNEXPECTED TLD on $ip: $n"; bad=1; } done done < "$HOSTS" [ "$bad" -eq 0 ] } # vhost_names — print site-block names under the flat scheme vhost_names() { grep -oE '^(http://)?[a-z0-9.-]+\.franksdank\.(cloud|casa)[[:space:]]*\{' "$1" \ | sed -E 's#^http://##; s/[[:space:]]*\{$//' } # record_ip_for — print the IP a name maps to in addn-hosts record_ip_for() { awk -v n="$1" '{ip=$1; for (i=2; i<=NF; i++) if ($i==n) print ip}' "$HOSTS" | head -1 } # Caddy is consolidated on dankclaw (192.168.0.143): it fronts every service # on both hosts, so every vhost name resolves to that one IP. @test "every Caddyfile vhost has an A record on 192.168.0.143" { local names n ip names="$(vhost_names "$REPO_ROOT/dankclaw/Caddyfile")" [ -n "$names" ] || { echo "no .franksdank vhosts found in dankclaw/Caddyfile"; return 1; } while IFS= read -r n; do [ -z "$n" ] && continue ip="$(record_ip_for "$n")" [ "$ip" = "192.168.0.143" ] || { echo "$n -> '$ip' (want 192.168.0.143)"; return 1; } done <<< "$names" } @test "no addn-hosts records point at the old per-host split (192.168.0.235)" { ! awk '!/^[[:space:]]*#/ && NF {print $1}' "$HOSTS" | grep -q '^192\.168\.0\.235$' } @test "the retired llms name has no A record" { ! grep -qE '\billms\.franksdank\.cloud\b' "$HOSTS" } @test "no secret material in the dnsmasq files" { local f for f in "$DNSMASQ_DIR"/*; do has_no_leaked_secret "$f" done }