Files
serverAppConfigs/tests/dnsmasq.bats
T
Frank 55d062ea7b DNS names: flat franksdank.cloud scheme + dnsmasq LAN DNS on dankclaw
Phase 1 of the local-DNS-name work. Users type a name, not a port.

- dnsmasq quadlet on dankclaw (rootless, slirp4netns, port 53 published):
  serves the flat name scheme from addn-hosts, upstream 1.1.1.1/1.0.0.1.
  Image built from the in-repo Containerfile (localhost/dnsmasq:latest).
- Caddyfiles (both hosts): all *.dankclaw.local / *.dankserver.local vhosts
  renamed to flat <service>.franksdank.cloud. Explicit http:// site
  addresses for now; TLS (wildcard LE via DNS-01/Porkbun) lands in phase 2.
- Drift reconciliation (live /etc/caddy/Caddyfile on dankserver differs
  from the repo): added the live-only llms.franksdank.cloud vhost
  (OpenWebUI on :3000). Dropped the legacy search vhost from dankserver
  (the name belongs to dankclaw's gluetun-routed SearXNG; the legacy
  instance stays reachable at 192.168.0.235:8081).
- Homepage service links updated to the new names.
- New test seams (TDD): tests/dnsmasq.bats (quadlet boot-persistence,
  port 53, conf/record validity, Caddyfile<->addn-hosts cross-check on
  the right host IPs); caddyfile.bats naming-convention tests updated.

LAN clients get this DNS from the SURFboard G36 (DNS Relay -> statically
configured -> 192.168.0.143); the router change is a pending user step.
2026-08-21 16:31:42 -07:00

115 lines
3.7 KiB
Bash

#!/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 <caddyfile> — 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 <name> — 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
}
@test "every dankserver Caddyfile vhost has an A record on 192.168.0.235" {
local names n ip
names="$(vhost_names "$REPO_ROOT/dankserver/Caddyfile")"
[ -n "$names" ] || { echo "no .franksdank vhosts found in dankserver/Caddyfile"; return 1; }
while IFS= read -r n; do
[ -z "$n" ] && continue
ip="$(record_ip_for "$n")"
[ "$ip" = "192.168.0.235" ] || { echo "$n -> '$ip' (want 192.168.0.235)"; return 1; }
done <<< "$names"
}
@test "every dankclaw 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 secret material in the dnsmasq files" {
local f
for f in "$DNSMASQ_DIR"/*; do
has_no_leaked_secret "$f"
done
}