mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-30 07:37:36 +00:00
Each left a host misconfigured or without its daemon after an upgrade or install. Keep fips-dns working on a renamed TUN and respect a disabled fips-dns. fips-dns-setup waited for an interface named fips0 before trying any resolver backend, so on a host whose config sets tun.name to anything else it timed out after 30 seconds and exited 1. Debian's postinst starts fips-dns on upgrade and fails the configure when it does not come up, so upgrading such a host left the package half-configured with apt exiting 100. postinst also re-enabled fips-dns.service on every configure, undoing an operator's disable on each upgrade or reinstall. fips-dns-setup now reads the interface name from the daemon config (tun.name under the top-level tun: block, falling back to fips0) and waits for it only in the resolvectl backend, the one backend that uses the interface; that backend records the interface in its state file and fips-dns-teardown reverts it from there. postinst enables fips-dns only on first install, or when the package is installed again after a remove that disabled it while it was enabled: prerm's remove records under /var/lib/fips whether fips-dns was enabled, postinst consumes the record, and purge removes it. An older package's remove left no record, and its postinst enabled fips-dns on every configure, so preinst treats an install from config-files state with no record as enabled at remove; otherwise removing 0.5.3 and installing this package would leave .fips names unresolved. Tests: a dns-resolver scenario with the TUN renamed to mesh0, a container-free test of the name lookup run with the other static checks in both CI runners, and two Debian upgrade scenarios covering the renamed-TUN upgrade, a disable surviving a reinstall, and enablement across remove and reinstall, including a remove that left no record. Leave the running daemon alone on a FreeBSD package upgrade. On an upgrade the old package's pre-deinstall stopped fips and fips_dns, and the new package's post-install started them again when enabled. pkg 2.8.4 ends a process that a package script starts once pkg returns, even under daemon(8), so the started daemon was gone by the time pkg exited: every upgrade left the node with no daemon and without its .fips resolver drop-in until the operator ran service fips restart. pre-deinstall now stops the services and tears down the drop-in only on removal, and post-install no longer starts anything. The running daemon keeps its replaced binary until the operator restarts it, and the package prints an upgrade message saying to run service fips restart (which also starts a daemon that is not running) and service fips_dns restart if it is used. Upgrades from a package before this one still run that package's pre-deinstall, which stops the daemon, so they still need the restart; the README says so. packaging/freebsd/test-upgrade.sh upgrades over a running, enabled daemon to a package built from the same binaries at version 99.0.0 and checks that a daemon is still running up to 10 seconds after pkg returns, that the restart instruction was printed, and that the restart moves the daemon onto the new binary with one process. The FreeBSD Package workflow runs it in its VM after the log rotation check. Repoint an existing Windows service registration at the installed fips.exe. When the fips service already existed, --install-service opened it and set only its description, then reported success. A service registered from another folder, by hand or by an older install, therefore kept starting that folder's fips.exe after install-service.ps1 had copied the new release into Program Files. On ERROR_SERVICE_EXISTS, --install-service now reads the existing configuration and rewrites it with this executable and --service, keeping the service's start type and account, prints the old and new binary paths when they differ, and says "updated" rather than "installed". The service commands it, install-service.ps1 and the ZIP README print are now sc.exe: in PowerShell a bare sc is the Set-Content alias. The Windows Package workflow gains a step that registers the service from C:\fips-old, runs install-service.ps1 under Windows PowerShell, and requires the registration to name Program Files' fips.exe with --service, and the ZIP check now fails on a README line that names a bare sc.
288 lines
10 KiB
Bash
Executable File
288 lines
10 KiB
Bash
Executable File
#!/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
|
|
#
|
|
# Only the resolvectl backend uses the TUN interface. Its name is the daemon's
|
|
# tun.name, read from the daemon's config (FIPS_CONFIG), and fips0 when the
|
|
# config does not set one, as in the daemon.
|
|
|
|
FIPS_DNS_PORT="5354"
|
|
FIPS_DNS_LOOPBACK_V6="::1"
|
|
FIPS_CONFIG="${FIPS_CONFIG:-/etc/fips/fips.yaml}"
|
|
FIPS_DEFAULT_INTERFACE="fips0"
|
|
FIPS_INTERFACE="$FIPS_DEFAULT_INTERFACE"
|
|
|
|
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: $*"; }
|
|
|
|
# Record the backend on the state file's first line, and for a backend that
|
|
# configured an interface, that interface on the second.
|
|
save_backend() {
|
|
mkdir -p "$STATE_DIR"
|
|
printf '%s\n' "$@" > "$STATE_FILE"
|
|
}
|
|
|
|
# Print the TUN interface name the daemon uses: `name:` directly under the
|
|
# top-level `tun:` block of FIPS_CONFIG, unquoted and without a trailing
|
|
# comment, or fips0 when the file, the block or the key is absent. This reads
|
|
# the block layout the shipped config uses; it is not a YAML parser, and a
|
|
# layout it does not read (flow style, anchors) gives fips0.
|
|
tun_name() {
|
|
local name=""
|
|
if [ -r "$FIPS_CONFIG" ]; then
|
|
name=$(awk -v sq="'" '
|
|
function value(v, q, end) {
|
|
sub(/^[ \t]+/, "", v)
|
|
q = substr(v, 1, 1)
|
|
if (q == "\"" || q == sq) {
|
|
end = index(substr(v, 2), q)
|
|
return end ? substr(v, 2, end - 1) : ""
|
|
}
|
|
sub(/[ \t]+#.*$/, "", v)
|
|
sub(/[ \t\r]+$/, "", v)
|
|
return v
|
|
}
|
|
/^[ \t]*(#.*)?\r?$/ { next }
|
|
/^[^ \t]/ { in_tun = ($0 ~ /^tun:[ \t]*(#.*)?\r?$/); indent = -1; next }
|
|
in_tun {
|
|
match($0, /^[ \t]*/)
|
|
if (indent < 0) indent = RLENGTH
|
|
if (RLENGTH == indent && $0 ~ /^[ \t]*name:/) {
|
|
v = $0
|
|
sub(/^[ \t]*name:/, "", v)
|
|
print value(v)
|
|
exit
|
|
}
|
|
}
|
|
' "$FIPS_CONFIG" 2>/dev/null) || name=""
|
|
fi
|
|
echo "${name:-$FIPS_DEFAULT_INTERFACE}"
|
|
}
|
|
|
|
# Wait for the TUN 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 the TUN interface 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" <<EOF
|
|
[Delegate]
|
|
DNS=[${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT}
|
|
Domains=fips
|
|
EOF
|
|
systemctl restart systemd-resolved
|
|
save_backend "dns-delegate"
|
|
return 0
|
|
}
|
|
|
|
# Backend 2: systemd-resolved global drop-in (preferred for systemd-resolved hosts)
|
|
#
|
|
# Writes /etc/systemd/resolved.conf.d/fips.conf with DNS=[::1]:5354 and
|
|
# Domains=~fips. systemd-resolved routes the .fips zone via standard
|
|
# loopback (no per-link interface scoping), which sidesteps two issues:
|
|
#
|
|
# - Ubuntu 22 (systemd 249) interface-scoped routing of per-link DNS
|
|
# servers, which would otherwise cause `resolvectl dns fips0
|
|
# 127.0.0.1:5354` to route through fips0 and fail.
|
|
# - The IPV6_PKTINFO ifindex attribution behaviour where Linux reports
|
|
# a packet to fips0's own IPv6 address as arriving on fips0 (despite
|
|
# loopback delivery), which would otherwise be silently dropped by
|
|
# the daemon's mesh-interface filter.
|
|
#
|
|
# This backend is the recommended path on every systemd-resolved host
|
|
# that doesn't have native dns-delegate support (systemd >= 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" <<EOF
|
|
# Managed by fips-dns.service (fips-dns-setup, global-drop-in backend).
|
|
# Routes .fips queries to the fips daemon's local DNS responder.
|
|
[Resolve]
|
|
DNS=[${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT}
|
|
Domains=~fips
|
|
EOF
|
|
systemctl restart systemd-resolved \
|
|
|| log "WARNING: systemd-resolved restart failed (config written, may need manual reload)"
|
|
save_backend "global-drop-in"
|
|
return 0
|
|
}
|
|
|
|
# Backend 3: resolvectl (legacy fallback)
|
|
#
|
|
# systemd-resolved applies interface-scoped routing to per-link DNS servers.
|
|
# On Ubuntu 22 (systemd 249), configuring `resolvectl dns fips0 127.0.0.1:5354`
|
|
# causes resolved to route the DNS query through fips0 and fail (127.0.0.1 is
|
|
# not reachable via a TUN with only fd00::/8 routes). Using fips0's own global
|
|
# IPv6 address (locally delivered by the kernel) sidesteps that — but on hosts
|
|
# where the daemon binds wildcard `::`, IPV6_PKTINFO reports `ipi6_ifindex ==
|
|
# fips0` for those packets and the daemon's mesh-interface filter drops every
|
|
# query silently. The global-drop-in backend above avoids both issues; this
|
|
# resolvectl path is retained only as a fallback for environments where the
|
|
# global drop-in is unsuitable.
|
|
try_resolvectl() {
|
|
command -v resolvectl >/dev/null 2>&1 || return 1
|
|
is_active systemd-resolved.service || return 1
|
|
wait_for_interface || 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" "$FIPS_INTERFACE"
|
|
return 0
|
|
}
|
|
|
|
# Backend 4: standalone dnsmasq
|
|
#
|
|
# dnsmasq's `server=/<domain>/<addr>#<port>` syntax accepts an IPv6
|
|
# literal directly (no brackets). Targets ::1#5354 to match the
|
|
# daemon's default IPv6 loopback bind.
|
|
#
|
|
# dnsmasq must be restarted, not reloaded: a reload is a SIGHUP, which
|
|
# does not make dnsmasq re-read its configuration, so the drop-in's
|
|
# server= line would not be loaded.
|
|
try_dnsmasq() {
|
|
is_active dnsmasq.service || return 1
|
|
[ -d /etc/dnsmasq.d ] || return 1
|
|
|
|
log "Configuring via dnsmasq"
|
|
cat > "$DNSMASQ_CONF" <<EOF
|
|
# FIPS .fips domain forwarding (managed by fips-dns.service)
|
|
server=/fips/${FIPS_DNS_LOOPBACK_V6}#${FIPS_DNS_PORT}
|
|
EOF
|
|
systemctl restart dnsmasq || log "WARNING: dnsmasq restart failed (config written; restart dnsmasq to load it)"
|
|
save_backend "dnsmasq"
|
|
return 0
|
|
}
|
|
|
|
# Backend 5: NetworkManager with dnsmasq plugin
|
|
try_nm_dnsmasq() {
|
|
is_active NetworkManager.service || return 1
|
|
|
|
# Check if NM is configured to use dnsmasq
|
|
local nm_dns
|
|
nm_dns=$(grep -sh '^dns=' /etc/NetworkManager/NetworkManager.conf \
|
|
/etc/NetworkManager/conf.d/*.conf 2>/dev/null | tail -1 | cut -d= -f2)
|
|
[ "$nm_dns" = "dnsmasq" ] || return 1
|
|
[ -d /etc/NetworkManager/dnsmasq.d ] || return 1
|
|
|
|
log "Configuring via NetworkManager dnsmasq plugin"
|
|
cat > "$NM_DNSMASQ_CONF" <<EOF
|
|
# FIPS .fips domain forwarding (managed by fips-dns.service)
|
|
server=/fips/${FIPS_DNS_LOOPBACK_V6}#${FIPS_DNS_PORT}
|
|
EOF
|
|
nmcli general reload || log "WARNING: NetworkManager reload failed (config written, may need manual reload)"
|
|
save_backend "nm-dnsmasq"
|
|
return 0
|
|
}
|
|
|
|
# --- Main ---
|
|
|
|
main() {
|
|
FIPS_INTERFACE=$(tun_name)
|
|
|
|
try_dns_delegate && exit 0
|
|
try_global_drop_in && exit 0
|
|
try_resolvectl && exit 0
|
|
try_dnsmasq && exit 0
|
|
try_nm_dnsmasq && exit 0
|
|
|
|
log "WARNING: No supported DNS resolver detected."
|
|
log "To resolve .fips domains, configure your DNS resolver to forward"
|
|
log "the .fips domain to [${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT} (matches the daemon's default bind)."
|
|
log ""
|
|
log "Examples:"
|
|
log " systemd-resolved: install it if needed (a separate package on some distributions), run 'sudo systemctl enable --now systemd-resolved', then 'sudo systemctl restart fips-dns.service'"
|
|
log " dnsmasq: echo 'server=/fips/${FIPS_DNS_LOOPBACK_V6}#${FIPS_DNS_PORT}' | sudo tee /etc/dnsmasq.d/fips.conf"
|
|
save_backend "none"
|
|
exit 0
|
|
}
|
|
|
|
# Sourced (by testing/dns-resolver/tun-name-test.sh), only the functions are
|
|
# defined.
|
|
if [ "${BASH_SOURCE[0]}" = "$0" ]; then
|
|
set -e
|
|
main
|
|
fi
|