mirror of
https://github.com/jmcorgan/fips.git
synced 2026-07-30 19:46:15 +00:00
Fix DNS responder silent-drop on systemd-resolved deployments
The previous default configured systemd-resolved with `resolvectl dns fips0 [<fips0_addr>]:5354`, intended to bypass an Ubuntu 22 systemd 249 interface-scoping bug. That target collides with the daemon's mesh-interface filter on Linux: when an IPv6 packet's destination belongs to a non-loopback interface, the kernel attributes the packet to that interface in IPV6_PKTINFO (ipi6_ifindex == fips0) even though loopback delivery is used (tcpdump shows lo). The mesh-interface filter sees arrival_ifindex == mesh_ifindex and silently drops every query at trace level — invisible to operators at the default debug level. Net effect on stock deployments: every .fips query on systemd-resolved hosts was silently dropped. Daemon side ----------- - Default `dns.bind_addr` changes from "::" to "::1" (IPv6 loopback only). The mesh-interface filter is then defanged on the default path because loopback isn't reachable from mesh peers. The filter remains in place defensively for operators who explicitly bind "::" to expose a mesh-reachable responder. fips-dns-setup backend unification ---------------------------------- - New `try_global_drop_in` backend writes /etc/systemd/resolved.conf.d/fips.conf with DNS=[::1]:5354 and Domains=~fips. Inserted ahead of `try_resolvectl` in the dispatch chain. The standard loopback path has no interface scoping, so ipi6_ifindex reports lo and the filter passes. - All other backends now target [::1]:5354 to match the daemon's default IPv6-loopback bind: - try_dns_delegate writes DNS=[::1]:5354 - try_dnsmasq writes server=/fips/::1#5354 - try_nm_dnsmasq writes server=/fips/::1#5354 - Fixed dns-delegate file path: was /etc/systemd/dns-delegate/, must be /etc/systemd/dns-delegate.d/ (with .d suffix). systemd-resolved silently ignored the previous path. - fips-dns-teardown handles the new global-drop-in backend in cleanup. - The legacy resolvectl per-link backend stays as a fallback, documented to require careful daemon bind_addr coordination. fips-gateway upstream pairing ----------------------------- - gateway.dns.upstream default changes from 127.0.0.1:5354 to [::1]:5354 to match the daemon's default bind. Linux IPv6 sockets bound to explicit ::1 do not accept v4-mapped traffic, so the old default would have caused the gateway's startup DNS reachability probe to time out and systemd to restart-loop the service. - Operators who set a non-default daemon `dns.bind_addr` must also set `gateway.dns.upstream` to match — documented inline. Documentation ------------- - packaging/common/fips.yaml and packaging/openwrt-ipk fips.yaml examples updated; rationale for the bind_addr choice and the daemon/gateway pairing recorded inline. Test coverage ------------- - testing/dns-resolver/test.sh: real-fipsd end-to-end scenario added. Builds fipsd in a Debian 12 builder image (cached), runs the daemon with a real TUN in a privileged container, configures DNS via the setup script, and asserts `dig @127.0.0.53 AAAA <npub>.fips` returns AAAA. Refactored as a parameterized helper running across Debian 12/13 and Ubuntu 22/24/26 (5 e2e scenarios). Backend-aware assertions: on systemd >= 258 the expected backend is dns-delegate; on older systemd it's global-drop-in. Strict content checks fail CI on any [::1]:5354 drift. fips-gateway also exercised in the debian12 scenario to lock the gateway-upstream pairing. Renamed all "fipsd" references to "fips" (project convention). - testing/deb-install/ (new harness): builds the actual .deb via cargo-deb in a Debian 12 builder image (cached), installs via apt across each target distro, verifies maintainer scripts, conffile placement, binary placement, and end-to-end .fips resolution after start. Also exercises fips-gateway against the installed daemon to verify the gateway/daemon default pairing on a real .deb path. - This is the test layer that was missing — the previous harness only verified config files were written, never that queries reached the daemon. Verified: dns-resolver 78/78 assertions, deb-install 55/55 assertions across all 5 distros (debian:12, debian:trixie, ubuntu:22.04, ubuntu:24.04, ubuntu:26.04).
This commit is contained in:
@@ -2,23 +2,34 @@
|
||||
# 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.
|
||||
# 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 via resolvectl (most systemd distros)
|
||||
# 3. dnsmasq (standalone)
|
||||
# 4. NetworkManager with dnsmasq plugin
|
||||
# 5. Warning with manual instructions
|
||||
# 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_ADDR="127.0.0.1"
|
||||
FIPS_DNS_PORT="5354"
|
||||
FIPS_DNS_LOOPBACK_V6="::1"
|
||||
FIPS_INTERFACE="fips0"
|
||||
|
||||
DNS_DELEGATE_DIR="/etc/systemd/dns-delegate"
|
||||
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"
|
||||
|
||||
@@ -70,6 +81,11 @@ is_active() {
|
||||
}
|
||||
|
||||
# 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)
|
||||
@@ -81,7 +97,7 @@ try_dns_delegate() {
|
||||
mkdir -p "$DNS_DELEGATE_DIR"
|
||||
cat > "$DNS_DELEGATE_FILE" <<EOF
|
||||
[Delegate]
|
||||
DNS=${FIPS_DNS_ADDR}:${FIPS_DNS_PORT}
|
||||
DNS=[${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT}
|
||||
Domains=fips
|
||||
EOF
|
||||
systemctl restart systemd-resolved
|
||||
@@ -89,14 +105,53 @@ EOF
|
||||
return 0
|
||||
}
|
||||
|
||||
# Backend 2: resolvectl
|
||||
# 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 — see ISSUE-2026-0002 in the
|
||||
# project tracker.
|
||||
#
|
||||
# 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). We must use fips0's own
|
||||
# global IPv6 address, which is locally delivered by the kernel regardless of
|
||||
# which interface resolved tries to route through.
|
||||
# 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
|
||||
@@ -115,7 +170,11 @@ try_resolvectl() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# Backend 3: standalone dnsmasq
|
||||
# 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.
|
||||
try_dnsmasq() {
|
||||
is_active dnsmasq.service || return 1
|
||||
[ -d /etc/dnsmasq.d ] || return 1
|
||||
@@ -123,14 +182,14 @@ try_dnsmasq() {
|
||||
log "Configuring via dnsmasq"
|
||||
cat > "$DNSMASQ_CONF" <<EOF
|
||||
# FIPS .fips domain forwarding (managed by fips-dns.service)
|
||||
server=/fips/${FIPS_DNS_ADDR}#${FIPS_DNS_PORT}
|
||||
server=/fips/${FIPS_DNS_LOOPBACK_V6}#${FIPS_DNS_PORT}
|
||||
EOF
|
||||
systemctl reload dnsmasq || log "WARNING: dnsmasq reload failed (config written, may need manual reload)"
|
||||
save_backend "dnsmasq"
|
||||
return 0
|
||||
}
|
||||
|
||||
# Backend 4: NetworkManager with dnsmasq plugin
|
||||
# Backend 5: NetworkManager with dnsmasq plugin
|
||||
try_nm_dnsmasq() {
|
||||
is_active NetworkManager.service || return 1
|
||||
|
||||
@@ -144,7 +203,7 @@ try_nm_dnsmasq() {
|
||||
log "Configuring via NetworkManager dnsmasq plugin"
|
||||
cat > "$NM_DNSMASQ_CONF" <<EOF
|
||||
# FIPS .fips domain forwarding (managed by fips-dns.service)
|
||||
server=/fips/${FIPS_DNS_ADDR}#${FIPS_DNS_PORT}
|
||||
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"
|
||||
@@ -156,16 +215,17 @@ EOF
|
||||
wait_for_interface || exit 1
|
||||
|
||||
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_ADDR} port ${FIPS_DNS_PORT}."
|
||||
log "the .fips domain to [${FIPS_DNS_LOOPBACK_V6}]:${FIPS_DNS_PORT} (matches the daemon's default bind)."
|
||||
log ""
|
||||
log "Examples:"
|
||||
log " systemd-resolved: sudo apt install systemd-resolved"
|
||||
log " dnsmasq: echo 'server=/fips/${FIPS_DNS_ADDR}#${FIPS_DNS_PORT}' | sudo tee /etc/dnsmasq.d/fips.conf"
|
||||
log " dnsmasq: echo 'server=/fips/${FIPS_DNS_LOOPBACK_V6}#${FIPS_DNS_PORT}' | sudo tee /etc/dnsmasq.d/fips.conf"
|
||||
save_backend "none"
|
||||
exit 0
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
set -e
|
||||
|
||||
FIPS_INTERFACE="fips0"
|
||||
DNS_DELEGATE_FILE="/etc/systemd/dns-delegate/fips.dns-delegate"
|
||||
DNS_DELEGATE_FILE="/etc/systemd/dns-delegate.d/fips.dns-delegate"
|
||||
RESOLVED_DROPIN_FILE="/etc/systemd/resolved.conf.d/fips.conf"
|
||||
DNSMASQ_CONF="/etc/dnsmasq.d/fips.conf"
|
||||
NM_DNSMASQ_CONF="/etc/NetworkManager/dnsmasq.d/fips.conf"
|
||||
STATE_FILE="/run/fips/dns-backend"
|
||||
@@ -35,6 +36,14 @@ teardown_resolvectl() {
|
||||
return 0
|
||||
}
|
||||
|
||||
teardown_global_drop_in() {
|
||||
[ -f "$RESOLVED_DROPIN_FILE" ] || return 0
|
||||
log "Removing global resolved.conf.d drop-in"
|
||||
rm -f "$RESOLVED_DROPIN_FILE"
|
||||
is_active systemd-resolved.service && systemctl restart systemd-resolved || true
|
||||
return 0
|
||||
}
|
||||
|
||||
teardown_dnsmasq() {
|
||||
[ -f "$DNSMASQ_CONF" ] || return 0
|
||||
log "Removing dnsmasq config"
|
||||
@@ -59,14 +68,16 @@ if [ -f "$STATE_FILE" ]; then
|
||||
fi
|
||||
|
||||
case "$backend" in
|
||||
dns-delegate) teardown_dns_delegate ;;
|
||||
resolvectl) teardown_resolvectl ;;
|
||||
dnsmasq) teardown_dnsmasq ;;
|
||||
nm-dnsmasq) teardown_nm_dnsmasq ;;
|
||||
none) ;; # Nothing was configured
|
||||
dns-delegate) teardown_dns_delegate ;;
|
||||
global-drop-in) teardown_global_drop_in ;;
|
||||
resolvectl) teardown_resolvectl ;;
|
||||
dnsmasq) teardown_dnsmasq ;;
|
||||
nm-dnsmasq) teardown_nm_dnsmasq ;;
|
||||
none) ;; # Nothing was configured
|
||||
*)
|
||||
# State unknown — clean up everything
|
||||
teardown_dns_delegate
|
||||
teardown_global_drop_in
|
||||
teardown_resolvectl
|
||||
teardown_dnsmasq
|
||||
teardown_nm_dnsmasq
|
||||
|
||||
@@ -42,10 +42,16 @@ tun:
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
# bind_addr defaults to "::" (all interfaces, dual-stack). Required so
|
||||
# systemd-resolved can reach the responder via fips0 when resolvectl
|
||||
# configures per-interface DNS forwarding.
|
||||
# bind_addr: "::"
|
||||
# bind_addr defaults to "::1" (IPv6 loopback). The shipped
|
||||
# fips-dns-setup script configures systemd-resolved with a global
|
||||
# /etc/systemd/resolved.conf.d/fips.conf drop-in pointing at
|
||||
# [::1]:5354.
|
||||
#
|
||||
# Set "::" to expose the responder to mesh peers as well (e.g. for
|
||||
# gateway hosts that resolve .fips on behalf of LAN clients). The
|
||||
# mesh-interface filter in src/upper/dns.rs will still defend
|
||||
# /etc/fips/hosts aliases from cross-mesh enumeration.
|
||||
# bind_addr: "::1"
|
||||
port: 5354
|
||||
|
||||
transports:
|
||||
@@ -85,7 +91,12 @@ transports:
|
||||
# lan_interface: "eth0"
|
||||
# dns:
|
||||
# listen: "[::]:53"
|
||||
# upstream: "127.0.0.1:5354"
|
||||
# # upstream must match the daemon's dns.bind_addr above. The
|
||||
# # default "[::1]:5354" matches the daemon's default. If you set
|
||||
# # the daemon to bind on a wildcard ("::") or specific address,
|
||||
# # update upstream to match — explicit ::1 sockets do not accept
|
||||
# # v4 traffic.
|
||||
# upstream: "[::1]:5354"
|
||||
# ttl: 60
|
||||
# pool_grace_period: 60
|
||||
# conntrack:
|
||||
|
||||
@@ -23,8 +23,15 @@ tun:
|
||||
|
||||
dns:
|
||||
enabled: true
|
||||
# bind_addr defaults to "::" (all interfaces, dual-stack).
|
||||
# bind_addr: "::"
|
||||
# bind_addr defaults to "::1" (IPv6 loopback). On OpenWrt, dnsmasq
|
||||
# owns port 53; fips-dns-setup adds a forwarder rule pointing at
|
||||
# 127.0.0.1#5354. Loopback bind is sufficient for that path.
|
||||
#
|
||||
# Set "::" to expose the responder to LAN clients directly (rare —
|
||||
# most installs let dnsmasq forward instead). The mesh-interface
|
||||
# filter in src/upper/dns.rs defends /etc/fips/hosts aliases from
|
||||
# cross-mesh enumeration when bound to a wildcard.
|
||||
# bind_addr: "::1"
|
||||
port: 5354
|
||||
|
||||
transports:
|
||||
@@ -79,6 +86,6 @@ gateway:
|
||||
lan_interface: "br-lan" # LAN-facing interface for proxy NDP
|
||||
dns:
|
||||
listen: "[::1]:5353" # gateway DNS listener (dnsmasq forwards here)
|
||||
upstream: "127.0.0.1:5354" # FIPS daemon DNS resolver
|
||||
upstream: "[::1]:5354" # FIPS daemon DNS resolver (matches daemon default)
|
||||
ttl: 60 # DNS TTL and mapping lifetime (seconds)
|
||||
pool_grace_period: 60 # seconds after last session before reclaiming
|
||||
|
||||
Reference in New Issue
Block a user