Files
fips/packaging/freebsd/README.md
T
fr34akyandJohnathan Corgan 429d77731b add a pfSense package
pfSense is FreeBSD underneath, but the FreeBSD package does not work
there, failing in three silent ways. pfSense runs only
/usr/local/etc/rc.d/*.sh at boot and re-runs them when WAN gets a new
address, so a suffixless rc script never starts; unbound.conf is
generated from config.xml with no conf.d, so a drop-in is never read;
and on a firewall where the default-on "Allow IPv6" has been turned off,
unbound is then generated with do-ip6: no and a responder on ::1 is
unreachable. So this ships fips.sh, wires the fips. zone into the DNS
Resolver through config.xml, and binds the responder on 127.0.0.1 for
robustness against that last case.

The rc script is plain sh: what pfSense imposes is the .sh name and that
a re-run leave a running daemon alone and exit 0. It identifies the
daemon by process name and recovers an orphaned daemon(8) supervisor
found via fstat, since a locked empty pidfile makes daemon(8) report
pid -1. The DNS setup is a manual step, never run from post-install, and
validates the merged options with unbound-checkconf (pfSense's
test_unbound_config) before touching config.xml, so a bad merge cannot
take DNS from every client behind the firewall. The daemon runs under
daemon(8) -H so newsyslog can rotate its log by signalling a reopen.

Packages link statically by default: pfSense runs a FreeBSD base that
cannot be obtained to link against. A firmware upgrade keeps the package
(pfSense-upgrade removes only pfSense-pkg-*; confirmed on a live Plus
26.03.1 -> 26.07 upgrade, aarch64 — the package survived and the daemon
restarted at boot. That is a minor, FreeBSD 16 -> 16 change; the
cross-major compat case is still only source-reasoned). aarch64 is
refused, where a static binary faults at posix_spawn.

The mechanics the two builders share — version derivation, the stage
layout, the manifest fields, the @sample scripts and pkg create — live
in packaging/common/pkg-lib.sh, which both source; the FreeBSD package
is byte-identical before and after that extraction.

One ABI can serve more than one product: CE 2.9 and Plus 26.x on Intel
are both FreeBSD:16:amd64 with a byte-identical artifact, named
...-ce2.9-plus26-amd64.pkg. The pfSense package is built and checked in
its own CI job — separate from the FreeBSD package, and not a dependency
of the release job, so a pfSense-only failure reds that job alone and is
never a release asset. It is kept as a workflow artifact until it has
been installed on a real pfSense box. CI produces the CE 2.8.1
(FreeBSD:15:amd64) package; CE 2.9, Plus 26.x Intel and ARM need a
FreeBSD 16 build host the CI does not have, and ARM stays
build-it-yourself because rustup ships no toolchain for it.

testing/check-pfsense-pkg.sh validates a built package on any FreeBSD
host and runs in that CI job: contents, modes, a positive boot-script
lifecycle against a stub daemon, php -l and a fips_strip_block unit test
of the config.xml helper. Installing on a real pfSense box, and the
firmware-upgrade behaviour, are covered only by an aarch64 hardware run
and pfSense-upgrade's source; the README records what is and is not
tested.

Co-authored-by: Johnathan Corgan <johnathan@corganlabs.com>
2026-09-12 14:41:38 +00:00

107 lines
4.3 KiB
Markdown

# FIPS FreeBSD packaging
Builds a native FreeBSD `.pkg` shipping `fips`, `fipsctl`, `fipstop`,
rc.d services, and `.fips` DNS integration. `fips-gateway` is excluded
(its NAT backend is nftables, Linux-only).
**On pfSense?** Use [`packaging/pfsense/`](../pfsense/README.md)
instead. pfSense is FreeBSD underneath, but it boots packages, wires up
DNS and handles upgrades differently enough that this package does not
work there; the pfSense one addresses each difference.
Platform notes: the Ethernet and BLE transports are not available on
FreeBSD (UDP, TCP, Tor, and Nym are). The UDP datapath deliberately
uses the portable single-packet receive loop — FreeBSD's `recvmmsg(2)`
is a libc loop over `recvmsg`, not a kernel batch, so the Linux/macOS
batched arm would gain nothing — and the connected-UDP fast path is
not compiled pending FreeBSD-specific `SO_REUSEPORT` validation.
## Build
```sh
./packaging/freebsd/build-pkg.sh # cargo build --release + pkg create
./packaging/freebsd/build-pkg.sh --no-build # package existing release binaries
```
Output: `deploy/fips-<version>-freebsd-<arch>.pkg`. pkg versions cannot
contain `-` or `+`, so both are mapped to `.`: a Cargo version of
`<x.y.z>-dev` becomes `fips-<x.y.z>.dev-freebsd-amd64.pkg`.
## Install
```sh
pkg add ./deploy/fips-<version>-freebsd-<arch>.pkg
# post-install seeds this from the sample if absent, at mode 0600
vi /usr/local/etc/fips/fips.yaml
sysrc fips_enable=YES fips_dns_enable=YES
service fips start
service fips_dns start
fipsctl show status
```
Config installs sample-style (`@sample` semantics via manifest
scripts), so an edited `fips.yaml` survives upgrade/removal.
`fips.yaml` is installed `0600` — it may hold the node's private key
(`nsec:`). The daemon runs under daemon(8) with pidfile
`/var/run/fips/fips.pid` and logs to `/var/log/fips.log` (rc.conf
knobs: `fips_config`, `fips_flags`, `fips_logfile`).
The package creates a `fips` group; members can run `fipsctl` and
`fipstop` without root (`pw groupmod fips -m <user>`, then re-login).
On `pkg upgrade` the services are stopped before the binaries are
replaced and started again afterwards if enabled; on `pkg delete` they
are stopped and the `.fips` resolver drop-in is removed.
## .fips DNS integration
The daemon answers `.fips` queries on `[::1]:5354`. `fips_dns` points
the system resolver's `fips.` zone there; backends tried in order:
base `local_unbound` (drop-in `/var/unbound/conf.d/fips.conf`), pkg
`unbound`, pkg `dnsmasq`. The unbound drop-in must (and does) set:
- `do-not-query-localhost: no` — unbound's default silently refuses
loopback forwarders, SERVFAILing every `.fips` query.
- `do-ip6: yes` — the daemon binds `::1` only.
- `domain-insecure: "fips."` — the zone is unsigned.
### One-time host resolver setup (NOT automated)
The package configures the `fips.` zone only; making the local
resolver the *system* resolver is an operator decision. On a typical
box:
```sh
sysrc local_unbound_enable=YES
local-unbound-setup 1.1.1.1 9.9.9.9 # explicit upstreams — see below
service local_unbound restart
```
Field-tested caveats (`fips-dns-setup` detects and warns about each):
- `/etc/resolv.conf` must list a loopback nameserver (ideally only
`127.0.0.1`), or nothing ever queries unbound and `.fips` cannot
resolve.
- **Do not use a home-router DNS proxy as unbound's upstream.** Many
CPE forwarders are EDNS-broken; unbound always sends EDNS, so every
public query SERVFAILs. Forward to real resolvers (ISP or public).
- Remove `options edns0` from `/etc/resolv.conf` if present, and set
`resolv_conf_options=""` in `/etc/resolvconf.conf` so resolvconf(8)
does not re-add it — against an EDNS-broken router it breaks libc
resolution outright.
- Never run `local-unbound-setup` with no arguments while resolv.conf
already points at 127.0.0.1 — it snapshots that as upstream and
forwards unbound to itself.
## Debugging
```sh
drill -p 5354 <npub>.fips @::1 AAAA # daemon directly (bypasses unbound)
drill <npub>.fips AAAA # full chain; SERVER: must be 127.0.0.1
cat /var/run/fips/dns-backend # which backend fips_dns configured
```
The TUN interface gets a kernel-assigned name (`tun0`, `tun1`, ...),
like `utun` on macOS, and is destroyed automatically when the daemon
exits. `ifconfig <name>` prints `Opened by PID <n>` for the process
holding a tun device.