#!/bin/bash # fips-dns-setup — Configure DNS routing for the .fips domain. # # Detects the system's DNS resolver and configures it to forward .fips # queries to the FIPS DNS responder on 127.0.0.1:5354 (or [::1]:5354 # for the global drop-in backend). # # Backends (tried in order): # 1. systemd dns-delegate (systemd >= 258, declarative drop-in) # 2. systemd-resolved global drop-in via /etc/systemd/resolved.conf.d/ # (preferred for systemd-resolved hosts — DNS=[::1]:5354 routes # via standard loopback with no interface scoping, sidestepping # the IPV6_PKTINFO ifindex behaviour that causes self-destined # traffic to fips0's address to be silently dropped by the # mesh-interface filter) # 3. systemd-resolved via per-link resolvectl (legacy fallback — # historically failed silently due to the issue described above; # kept for hosts where the global drop-in is unsuitable) # 4. dnsmasq (standalone) # 5. NetworkManager with dnsmasq plugin # 6. Warning with manual instructions set -e FIPS_DNS_PORT="5354" FIPS_DNS_LOOPBACK_V6="::1" FIPS_INTERFACE="fips0" DNS_DELEGATE_DIR="/etc/systemd/dns-delegate.d" DNS_DELEGATE_FILE="${DNS_DELEGATE_DIR}/fips.dns-delegate" RESOLVED_DROPIN_DIR="/etc/systemd/resolved.conf.d" RESOLVED_DROPIN_FILE="${RESOLVED_DROPIN_DIR}/fips.conf" DNSMASQ_CONF="/etc/dnsmasq.d/fips.conf" NM_DNSMASQ_CONF="/etc/NetworkManager/dnsmasq.d/fips.conf" # Record which backend was configured for teardown. STATE_DIR="/run/fips" STATE_FILE="${STATE_DIR}/dns-backend" log() { echo "fips-dns: $*"; } save_backend() { mkdir -p "$STATE_DIR" echo "$1" > "$STATE_FILE" } # Wait for fips0 interface (up to 30s). wait_for_interface() { for i in $(seq 1 30); do ip link show "$FIPS_INTERFACE" >/dev/null 2>&1 && return 0 sleep 1 done log "ERROR: $FIPS_INTERFACE interface not found after 30s" return 1 } # Wait for fips0 to have a global IPv6 address assigned (up to 30s). # Returns the address on stdout, or empty string if timeout. wait_for_fips_addr() { for i in $(seq 1 30); do local addr addr=$(ip -6 -o addr show dev "$FIPS_INTERFACE" scope global 2>/dev/null \ | awk '{print $4}' | cut -d/ -f1 | head -1) if [ -n "$addr" ]; then echo "$addr" return 0 fi sleep 1 done return 1 } # Get systemd version number. systemd_version() { systemctl --version 2>/dev/null | head -1 | grep -oE '[0-9]+' | head -1 } # Check if a systemd service is active. is_active() { systemctl is-active --quiet "$1" 2>/dev/null } # Backend 1: systemd dns-delegate (>= 258) # # Targets [::1]:5354 to match the daemon's default IPv6 loopback # bind. v4-targeted DNS= lines do not reach a daemon bound on # explicit `::1` (Linux v6 sockets bound to ::1 don't accept # v4-mapped traffic). try_dns_delegate() { local ver ver=$(systemd_version) [ -z "$ver" ] && return 1 [ "$ver" -lt 258 ] && return 1 is_active systemd-resolved.service || return 1 log "Configuring via dns-delegate (systemd $ver)" mkdir -p "$DNS_DELEGATE_DIR" cat > "$DNS_DELEGATE_FILE" <= 258). try_global_drop_in() { is_active systemd-resolved.service || return 1 log "Configuring via global resolved.conf.d drop-in (DNS=[${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT})" mkdir -p "$RESOLVED_DROPIN_DIR" cat > "$RESOLVED_DROPIN_FILE" </dev/null 2>&1 || return 1 is_active systemd-resolved.service || return 1 local fips_addr fips_addr=$(wait_for_fips_addr) if [ -z "$fips_addr" ]; then log "ERROR: $FIPS_INTERFACE has no global IPv6 address after 30s" return 1 fi log "Configuring via resolvectl (DNS=[${fips_addr}]:${FIPS_DNS_PORT})" resolvectl dns "$FIPS_INTERFACE" "[${fips_addr}]:${FIPS_DNS_PORT}" resolvectl domain "$FIPS_INTERFACE" "~fips" save_backend "resolvectl" return 0 } # Backend 4: standalone dnsmasq # # dnsmasq's `server=//#` syntax accepts an IPv6 # literal directly (no brackets). Targets ::1#5354 to match the # daemon's default IPv6 loopback bind. try_dnsmasq() { is_active dnsmasq.service || return 1 [ -d /etc/dnsmasq.d ] || return 1 log "Configuring via dnsmasq" cat > "$DNSMASQ_CONF" <