From 69a512f7e80c58ec61619b7c35ffffc1f94af1f0 Mon Sep 17 00:00:00 2001 From: fr34aky <162515565+fr34aky@users.noreply.github.com> Date: Thu, 17 Sep 2026 20:10:43 +0000 Subject: [PATCH] fix(pfsense): restart unbound after editing the DNS Resolver options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 .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. --- packaging/pfsense/README.md | 42 ++++++++++++++++------- packaging/pfsense/fips-unbound-custom.php | 11 ++++-- testing/check-pfsense-pkg.sh | 14 ++++++++ 3 files changed, 51 insertions(+), 16 deletions(-) diff --git a/packaging/pfsense/README.md b/packaging/pfsense/README.md index f8835c6e..abecc780 100644 --- a/packaging/pfsense/README.md +++ b/packaging/pfsense/README.md @@ -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. diff --git a/packaging/pfsense/fips-unbound-custom.php b/packaging/pfsense/fips-unbound-custom.php index 978d50d4..c01a292d 100644 --- a/packaging/pfsense/fips-unbound-custom.php +++ b/packaging/pfsense/fips-unbound-custom.php @@ -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); diff --git a/testing/check-pfsense-pkg.sh b/testing/check-pfsense-pkg.sh index 943ef3db..bf18d115 100755 --- a/testing/check-pfsense-pkg.sh +++ b/testing/check-pfsense-pkg.sh @@ -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