fix(pfsense): restart unbound after editing the DNS Resolver options

Symptom: on pfSense Plus 26.07 amd64, fips-dns-setup wrote the .fips
forward-zone into the DNS Resolver custom options, config.xml and the
regenerated /var/unbound/unbound.conf both carried it, and the helper
reported "DNS Resolver updated and restarted" — but `drill <npub>.fips`
through unbound answered NXDOMAIN, and `unbound-control lookup` showed
the query still going to the root servers. The daemon answered the same
query directly. fips-dns-teardown had the mirror problem: the block was
gone from the file while the running resolver kept forwarding.

Root cause: the helper called sync_unbound_service(), which regenerates
unbound.conf and then only *starts* unbound, a no-op while an instance
is already running, so the running resolver never saw the new file.

Fix: call services_unbound_configure(), which is what the GUI's Apply
runs (services_unbound.php): it TERMs the running unbound, waits, and
starts it on the regenerated configuration. Verified on the same VM:
teardown and setup each produce a new unbound pid, `unbound-control
lookup` reports "forwarding request", and the full chain answers
NOERROR; after a reboot the block is regenerated from config.xml and the
chain still answers.

Regression check: check-pfsense-pkg.sh now asserts, statically, that the
shipped helper calls services_unbound_configure() and not
sync_unbound_service(); the call itself needs pfSense's includes and
cannot run in CI. The README's test record gains the Plus 26.07 amd64 VM
run that found this.
This commit is contained in:
fr34aky
2026-09-18 13:13:52 +00:00
committed by Johnathan Corgan
parent a72c589342
commit 69a512f7e8
3 changed files with 51 additions and 16 deletions
+29 -13
View File
@@ -506,20 +506,36 @@ gate — nothing re-checks it when this code changes.
Known still-unexercised paths, from that same run: `fips-dns-setup`'s
refusal path (it has only ever run against a responder that was already
answering), its DNS Forwarder branch, and `pkg delete`.
answering) and its DNS Forwarder branch.
(`fips-dns-teardown` has since been run on the same box and restored
`custom_options` byte for byte.)
**No amd64 package has ever been installed.** The CE 2.8.1 (FreeBSD 15)
and the CE 2.9 / Plus 26.x (FreeBSD 16) amd64 packages are built and
pass the checker, and nothing more. The one hardware run was aarch64;
the amd64 packages share every script here and have had none of that
exposure, so read a passing check as "the package is well-formed", not
"it works".
The FreeBSD 16 amd64 package has been run once on pfSense Plus
26.07-RELEASE amd64, in a KVM virtual machine installed with the Netgate
installer (the same `FreeBSD:16:amd64` package serves CE 2.9.0). That
package was built outside `master`'s CI, which builds no FreeBSD 16
package, on the 16.0-CURRENT 20260907 snapshot:
`pkg add`, the boot script through start, re-entrant start, restart and
stop with the daemon answering `fipsctl` and DNS, `pfSctl -c 'service
reload packages'` (the WAN-address-change path) leaving the running
daemon alone, a reboot bringing up exactly one daemon with the DNS
Resolver block regenerated from `config.xml`, `fips-dns-teardown`
leaving `custom_options` empty as it was, and `pkg delete`. That run
found the defect fixed alongside this text: `fips-dns-setup` wrote the
block and reported "updated and restarted", but the running unbound was
never restarted and answered NXDOMAIN for `.fips` until it was. What the
VM did not cover: mesh traffic (no peer), the TUN datapath under pf, and
CE itself. The CE 2.8.1 (FreeBSD 15) package is still built and checked
only.
That matters because the aarch64 run found several defects, every one in
this packaging rather than the daemon — a boot script whose pid check
never succeeded, a DNS setup that reported success while nothing was
listening, and a static build that faulted at `posix_spawn`. The daemon
itself needed no changes. An untested path in the amd64 packages is
exactly where the next one would sit.
Left behind by `pkg delete`, by design or as known gaps:
`/usr/local/etc/fips/fips.key` if the daemon generated one (it may be the
node's identity), `/var/log/fips.log`, and the newsyslog entry under
`/var/etc`, which a RAM-disk `/var` drops at the next boot anyway.
The hardware and VM runs found several defects, every one in this
packaging rather than the daemon — a boot script whose pid check never
succeeded, a DNS setup that reported success while nothing was
listening, a static build that faulted at `posix_spawn`, and the
resolver restart above. The daemon itself needed no changes. An untested
path is exactly where the next one would sit.
+8 -3
View File
@@ -29,6 +29,7 @@ if (!getenv('FIPS_UNBOUND_HELPER_TEST')) {
require_once("config.inc");
require_once("util.inc");
require_once("unbound.inc");
require_once("services.inc");
}
define('FIPS_BEGIN', '# BEGIN FIPS - managed by fips-dns-setup, do not edit this block');
@@ -218,9 +219,13 @@ if (!fips_resolver_enabled()) {
}
/* Regenerate /var/unbound/unbound.conf from config.xml and restart the
* resolver. Nothing shorter works: the file is generated wholesale, so
* a reload alone would re-read the config we have not rewritten yet. */
sync_unbound_service();
* resolver, the way the GUI's Apply does (services_unbound.php). It has to
* be this function: sync_unbound_service() regenerates the file too, but
* then only *starts* unbound, which is a no-op while one is running, so
* the running resolver kept its old forward set and answered NXDOMAIN for
* .fips (found on Plus 26.07 amd64). services_unbound_configure() TERMs
* the running instance first. */
services_unbound_configure();
fips_log("DNS Resolver updated and restarted");
exit(0);
+14
View File
@@ -527,6 +527,20 @@ fi
# ── 6. PHP helper ───────────────────────────────────────────────────────────
echo "-- php helper"
HELPER="$PAYLOAD/libexec/fips/fips-unbound-custom.php"
# After editing config.xml the helper must restart the running resolver the
# way the GUI's Apply does, services_unbound_configure(): it TERMs unbound
# and starts it on the regenerated unbound.conf. sync_unbound_service()
# regenerates the file too but then only *starts* unbound, a no-op while one
# is running, so the resolver kept its old forward set and answered NXDOMAIN
# for .fips while setup reported success (Plus 26.07 amd64). A static check,
# since the call needs pfSense's includes to run.
if grep -qE '^[^*/#]*services_unbound_configure\(' "$HELPER" \
&& ! grep -qE '^[^*/#]*sync_unbound_service\(' "$HELPER"; then
pass "helper restarts unbound with services_unbound_configure()"
else
fail "helper does not restart unbound with services_unbound_configure()" \
"sync_unbound_service() leaves a running unbound on its old config."
fi
if ! command -v php >/dev/null 2>&1; then
skip "php -l and the fips_strip_block unit test (no php on this host)"
else