Remove every DNS routing file on purge and uninstall, and restart the resolver that used it

When the package was purged, or the tarball uninstalled, while fips-dns
was not running, fips-dns-teardown never ran and the DNS routing that
fips-dns-setup wrote stayed behind. The cleanup in postrm purge and in
uninstall.sh was meant to catch that case but removed the dns-delegate
file from the wrong directory (dns-delegate/ instead of dns-delegate.d/),
never removed the systemd-resolved global drop-in, and restarted no
resolver. systemd-resolved kept sending .fips queries to [::1]:5354,
where nothing listens any more, so .fips lookups timed out.

Both scripts now remove all four files fips-dns-setup can write, and
restart systemd-resolved, reload dnsmasq or reload NetworkManager when
they removed that resolver's file and it is running. A failed restart
prints a warning and does not fail the removal.

A packaging test pins both scripts to the paths fips-dns-setup and
fips-dns-teardown use, and the deb-install scenario now purges the
package with the routing file in place and fips-dns stopped, checking
the file is gone and systemd-resolved restarted. The CI comment on the
arm64 leg is corrected to say that leg now runs that purge.
No suite runs uninstall.sh, and the test's doc comment says so.
This commit is contained in:
Johnathan Corgan
2026-09-24 22:44:42 +00:00
parent ad021fab5e
commit c99e6d3908
5 changed files with 303 additions and 12 deletions
+6 -5
View File
@@ -1007,11 +1007,12 @@ jobs:
- type: deb-install
scenario: ubuntu26
arch: amd64
# The arm64 package on the oldest supported distribution: a fresh
# install and a daemon start. Deliberately GitHub-only (the local host
# is x86_64), and deliberately one leg: the upgrade, purge and
# conffile paths run under debian12 on amd64 only and stay
# unexercised on arm64.
# The arm64 package on the oldest supported distribution: the install
# scenario, which covers a fresh install, a daemon start and a purge
# of the DNS routing. Deliberately GitHub-only (the local host is
# x86_64), and deliberately one leg: the upgrade and conffile paths,
# including the upgrade scenario's own purge, run under debian12 on
# amd64 only and stay unexercised on arm64.
- type: deb-install
scenario: ubuntu22
arch: arm64
+36 -1
View File
@@ -13,10 +13,45 @@ case "$1" in
# Remove runtime directory
rm -rf /run/fips/
# Remove DNS config files that fips-dns-setup may have created
# Remove the DNS routing fips-dns-setup may have written, in case
# fips-dns-teardown did not run (prerm's stop runs it only when
# fips-dns.service was active), and make the resolver drop it. The
# paths match packaging/common/fips-dns-teardown, which dpkg has
# already removed, so it cannot be called from here.
restart_resolved=0
if [ -f /etc/systemd/dns-delegate.d/fips.dns-delegate ]; then
rm -f /etc/systemd/dns-delegate.d/fips.dns-delegate
restart_resolved=1
fi
if [ -f /etc/systemd/resolved.conf.d/fips.conf ]; then
rm -f /etc/systemd/resolved.conf.d/fips.conf
restart_resolved=1
fi
# Only pre-v0.3.0 development builds wrote this path, and systemd
# never read it.
rm -f /etc/systemd/dns-delegate/fips.dns-delegate
if [ "$restart_resolved" = 1 ] && [ -d /run/systemd/system ] \
&& systemctl is-active --quiet systemd-resolved.service; then
systemctl restart systemd-resolved \
|| echo "fips: warning: could not restart systemd-resolved; restart it to drop the .fips route"
fi
if [ -f /etc/dnsmasq.d/fips.conf ]; then
rm -f /etc/dnsmasq.d/fips.conf
if [ -d /run/systemd/system ] \
&& systemctl is-active --quiet dnsmasq.service; then
systemctl reload dnsmasq \
|| echo "fips: warning: could not reload dnsmasq; reload it to drop the .fips route"
fi
fi
if [ -f /etc/NetworkManager/dnsmasq.d/fips.conf ]; then
rm -f /etc/NetworkManager/dnsmasq.d/fips.conf
if [ -d /run/systemd/system ] \
&& systemctl is-active --quiet NetworkManager.service \
&& command -v nmcli >/dev/null 2>&1; then
nmcli general reload \
|| echo "fips: warning: could not reload NetworkManager; reload it to drop the .fips route"
fi
fi
# Remove fips system group
if getent group fips >/dev/null 2>&1; then
+47 -3
View File
@@ -42,10 +42,54 @@ rm -rf /usr/lib/fips/
systemctl daemon-reload
echo "systemd units and DNS scripts removed."
# Clean up DNS config files that fips-dns-setup may have created
# --- Remove DNS routing ---
# fips-dns-setup may have written one of these, and stopping fips-dns.service
# above runs fips-dns-teardown only when the unit was active. The paths match
# packaging/common/fips-dns-teardown.
restart_resolved=false
if [ -f /etc/systemd/dns-delegate.d/fips.dns-delegate ]; then
rm -f /etc/systemd/dns-delegate.d/fips.dns-delegate
echo "Removed /etc/systemd/dns-delegate.d/fips.dns-delegate."
restart_resolved=true
fi
if [ -f /etc/systemd/resolved.conf.d/fips.conf ]; then
rm -f /etc/systemd/resolved.conf.d/fips.conf
echo "Removed /etc/systemd/resolved.conf.d/fips.conf."
restart_resolved=true
fi
# Only pre-v0.3.0 development builds wrote this path, and systemd never read it.
rm -f /etc/systemd/dns-delegate/fips.dns-delegate
rm -f /etc/dnsmasq.d/fips.conf
rm -f /etc/NetworkManager/dnsmasq.d/fips.conf
if $restart_resolved && systemctl is-active --quiet systemd-resolved.service 2>/dev/null; then
if systemctl restart systemd-resolved; then
echo "systemd-resolved restarted."
else
echo "Warning: could not restart systemd-resolved; restart it to drop the .fips route." >&2
fi
fi
if [ -f /etc/dnsmasq.d/fips.conf ]; then
rm -f /etc/dnsmasq.d/fips.conf
echo "Removed /etc/dnsmasq.d/fips.conf."
if systemctl is-active --quiet dnsmasq.service 2>/dev/null; then
if systemctl reload dnsmasq; then
echo "dnsmasq reloaded."
else
echo "Warning: could not reload dnsmasq; reload it to drop the .fips route." >&2
fi
fi
fi
if [ -f /etc/NetworkManager/dnsmasq.d/fips.conf ]; then
rm -f /etc/NetworkManager/dnsmasq.d/fips.conf
echo "Removed /etc/NetworkManager/dnsmasq.d/fips.conf."
if systemctl is-active --quiet NetworkManager.service 2>/dev/null \
&& command -v nmcli >/dev/null 2>&1; then
if nmcli general reload; then
echo "NetworkManager reloaded."
else
echo "Warning: could not reload NetworkManager; reload it to drop the .fips route." >&2
fi
fi
fi
# --- Remove tmpfiles.d entry ---
+101
View File
@@ -161,6 +161,33 @@ fn logical_lines(sh: &str) -> Vec<String> {
out
}
/// Returns the logical lines of a shell script, trimmed at both ends, without
/// the lines that are comments.
fn code_lines(sh: &str) -> Vec<String> {
logical_lines(sh)
.into_iter()
.map(|l| l.trim().to_string())
.filter(|l| !l.starts_with('#'))
.collect()
}
/// Returns the code lines of the `case` branch `label)` in a shell script:
/// those after the line that trims to `label)`, up to the next line that trims
/// to `;;`.
fn case_branch(sh: &str, label: &str) -> Vec<String> {
let open = format!("{label})");
let lines = code_lines(sh);
let start = 1 + lines
.iter()
.position(|l| *l == open)
.unwrap_or_else(|| panic!("no `{open}` branch found"));
let len = lines[start..]
.iter()
.position(|l| l == ";;")
.unwrap_or_else(|| panic!("`{open}` branch is never closed with `;;`"));
lines[start..start + len].to_vec()
}
#[test]
fn deb_and_aur_packages_declare_nftables_for_the_firewall_units_nft() {
let unit = repo_file("packaging/debian/fips-firewall.service");
@@ -316,3 +343,77 @@ fn freebsd_newsyslog_entry_signals_the_daemon8_supervisor_started_with_sighup_re
"build-pkg.sh pkg-plist does not list etc/newsyslog.conf.d/fips.conf: {plist:?}"
);
}
/// Pins the DNS cleanup in `postrm purge` and `uninstall.sh` to the files
/// `fips-dns-setup` writes, so a purge after a `fips-dns` that never ran its
/// teardown does not leave the resolver sending `.fips` to a dead responder.
///
/// This is a text test. Each path must appear on an `rm -f` line, but a
/// resolver command passes wherever it appears on a code line, including in a
/// message. What `postrm` actually does is covered by the deb-install purge
/// check. No suite runs `uninstall.sh`: its two resolved paths were run once,
/// by hand in a container, and its dnsmasq and NetworkManager paths by nothing.
#[test]
fn dns_cleanup_in_postrm_purge_and_uninstall_removes_every_file_fips_dns_setup_writes_and_restarts_its_resolver()
{
let setup = rc_vars(&repo_file("packaging/common/fips-dns-setup"));
let teardown = rc_vars(&repo_file("packaging/common/fips-dns-teardown"));
let paths: Vec<&str> = [
"DNS_DELEGATE_FILE",
"RESOLVED_DROPIN_FILE",
"DNSMASQ_CONF",
"NM_DNSMASQ_CONF",
]
.into_iter()
.map(|name| {
let path = setup
.get(name)
.filter(|p| p.starts_with('/'))
.unwrap_or_else(|| panic!("fips-dns-setup sets no absolute {name}"));
assert_eq!(
teardown.get(name),
Some(path),
"fips-dns-teardown's {name} is not the file fips-dns-setup writes"
);
path.as_str()
})
.collect();
let commands = [
"restart systemd-resolved",
"reload dnsmasq",
"nmcli general reload",
];
let scripts = [
(
"packaging/debian/postrm purge)",
case_branch(&repo_file("packaging/debian/postrm"), "purge"),
),
(
"packaging/systemd/uninstall.sh",
code_lines(&repo_file("packaging/systemd/uninstall.sh")),
),
];
let mut missing = Vec::new();
for (script, lines) in &scripts {
for path in &paths {
if !lines
.iter()
.any(|l| l.contains("rm -f") && l.contains(path))
{
missing.push(format!("{script}: no `rm -f` of {path}"));
}
}
for command in commands {
if !lines.iter().any(|l| l.contains(command)) {
missing.push(format!("{script}: never runs `{command}`"));
}
}
}
assert!(
missing.is_empty(),
"DNS cleanup does not match the files fips-dns-setup writes and the resolvers \
fips-dns-teardown restarts:\n {}",
missing.join("\n ")
);
}
+111 -1
View File
@@ -8,13 +8,16 @@
# to come up, and verifies that `dig @127.0.0.53 AAAA <npub>.fips`
# returns a non-empty AAAA answer through the resolver backend that
# fips-dns-setup configured. Then exercises fips-gateway against the
# same daemon to verify the gateway/daemon default-pairing.
# same daemon to verify the gateway/daemon default-pairing. Finally it
# purges the package with the DNS routing file planted and fips-dns
# stopped, and checks the file is removed and systemd-resolved restarted.
#
# This is the most thorough test surface — it exercises:
# - cargo deb packaging (binary stripping, dependency declaration)
# - dpkg conffile placement (/etc/fips/fips.yaml)
# - postinst maintainer scripts (systemd unit enablement,
# fips-dns.service running fips-dns-setup)
# - postrm purge (removing the DNS routing fips-dns-setup wrote)
# - The fips, fips-dns, and (optionally) fips-gateway systemd units
# - End-to-end .fips resolution as a real user would experience it
#
@@ -301,6 +304,111 @@ EOF
return
}
# Purge the package with the DNS routing file planted and fips-dns stopped, and
# check that postrm removes the file and restarts systemd-resolved.
#
# Stopping fips-dns runs fips-dns-teardown, which removes the file; putting it
# back gives the state in which removal leaves it behind: a live delegation and
# an inactive fips-dns, so prerm's stop runs no teardown. Only postrm purge is
# left to clean up, and a file it misses keeps the resolver sending .fips to
# [::1]:5354 after nothing listens there.
#
# Args: <name> <expected_backend>, the backend the scenario expects
# fips-dns-setup to pick (dns-delegate or global-drop-in).
check_purge_clears_dns() {
local name="$1" backend="$2" file
case "$backend" in
dns-delegate) file=/etc/systemd/dns-delegate.d/fips.dns-delegate ;;
global-drop-in) file=/etc/systemd/resolved.conf.d/fips.conf ;;
*)
fail "purge: no DNS routing file known for backend '$backend'"
return
;;
esac
# The gateway-enable restart of fips.service is passed on to fips-dns
# (Requires=fips.service), whose setup waits for fips0 before it writes the
# file, and nothing since has waited for it. After=fips.service stops the
# old instance before the daemon, so active here means the new setup ran.
if ! wait_for_service_active "$name" fips-dns.service; then
fail "purge: fips-dns.service not active again after the gateway-enable restart"
echo " --- fips-dns.service journal ---"
docker exec "$name" journalctl -u fips-dns.service --no-pager 2>&1 | tail -20
return
fi
if ! cexec "$name" test -f "$file"; then
fail "purge: $file not written by fips-dns-setup before the purge"
return
fi
# Saved inside the container: cexec runs docker exec without -i, so a
# copy piped back from the host would arrive empty.
if ! cexec "$name" cp "$file" /root/fips-dns.saved; then
fail "purge: could not save $file"
return
fi
cexec "$name" systemctl stop fips-dns.service >/dev/null 2>&1
cexec "$name" cp /root/fips-dns.saved "$file"
cexec "$name" systemctl restart systemd-resolved >/dev/null 2>&1
local ok=1 status
if ! cexec "$name" sh -c "test -s '$file' && cmp -s '$file' /root/fips-dns.saved"; then
fail "purge: $file not restored before the purge"
ok=0
fi
if cexec "$name" systemctl is-active --quiet fips-dns.service; then
fail "purge: fips-dns.service still active before the purge"
ok=0
fi
# Captured rather than piped into grep -q: under pipefail, grep closing the
# pipe early can fail the pipeline on a match.
if ! status=$(cexec "$name" resolvectl status 2>&1); then
fail "purge: resolvectl status failed before the purge"
echo "$status" | tail -10
ok=0
elif ! grep -q ':5354' <<<"$status"; then
fail "purge: resolvectl status does not show $file in effect before the purge"
echo "$status" | tail -25
ok=0
fi
[ "$ok" = 1 ] || return
local before after
before=$(cexec "$name" systemctl show -p InvocationID --value systemd-resolved)
run_apt "$name" "$UPGRADE_APT_TIMEOUT" purge -y fips
echo " purge took ${APT_SECS}s"
if [ "$APT_RC" -ne 0 ]; then
fail "purge: apt-get purge exited $APT_RC"
echo "$APT_OUT" | tail -20
return
fi
# test exits 1 for a missing file; any other failure is docker exec's.
local rc=0
cexec "$name" test -e "$file" || rc=$?
case "$rc" in
1) pass "purge removed $file" ;;
0) fail "purge left $file behind" ;;
*) fail "purge: could not check for $file (exit $rc)" ;;
esac
after=$(cexec "$name" systemctl show -p InvocationID --value systemd-resolved)
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
pass "purge restarted systemd-resolved"
else
fail "purge did not restart systemd-resolved (InvocationID '$before' -> '$after')"
fi
if ! status=$(cexec "$name" resolvectl status 2>&1); then
fail "purge: resolvectl status failed after the purge"
echo "$status" | tail -10
elif grep -q ':5354' <<<"$status"; then
fail "purge: resolvectl status still routes to port 5354"
echo "$status" | tail -25
else
pass "purge: resolvectl status no longer routes to port 5354"
fi
return
}
# ─────────────────────────────────────────────────────────────────────
# Scenario runner
#
@@ -601,6 +709,8 @@ DOCKERFILE
docker exec "$name" journalctl -u fips-gateway.service --no-pager 2>&1 | tail -15
fi
check_purge_clears_dns "$name" "$expected_backend"
cleanup_container "$name"
}