mirror of
https://github.com/jmcorgan/fips.git
synced 2026-10-05 11:08:25 +00:00
588 lines
28 KiB
Markdown
588 lines
28 KiB
Markdown
# FIPS pfSense packaging
|
|
|
|
Builds a `.pkg` that installs FIPS on pfSense: `fips`, `fipsctl`,
|
|
`fipstop`, a boot script pfSense actually runs, and helpers that wire
|
|
the `.fips` zone into the DNS Resolver. `fips-gateway` is excluded (its
|
|
NAT backend is nftables, Linux-only; pfSense has pf for that).
|
|
|
|
This is **not a Netgate-supported package** and has no GUI. Netgate
|
|
[documents third-party packages as unsupported][netgate-freebsd-pkg]
|
|
and warns they can break upgrades; treat it accordingly.
|
|
|
|
[netgate-freebsd-pkg]: https://docs.netgate.com/pfsense/en/latest/recipes/freebsd-pkg-repo.html
|
|
|
|
## Maintenance and reports
|
|
|
|
This package is maintained by **fr34aky** (via the project's issue
|
|
tracker). pfSense-specific problems — a boot script that does not start,
|
|
DNS wiring, an upgrade that misbehaves — are best reported there; the
|
|
package manifest's maintainer field points at the project, so reports
|
|
reach it either way. The ABI-to-product table below tracks Netgate's
|
|
releases and needs updating when a new pfSense version ships or an old
|
|
one goes end-of-life; that is part of maintaining this package.
|
|
|
|
## Why this is separate from `packaging/freebsd/`
|
|
|
|
pfSense is FreeBSD underneath, but the FreeBSD package does not work
|
|
here — not "works worse", does not work — in three ways that all fail
|
|
silently:
|
|
|
|
| | FreeBSD package | pfSense |
|
|
|---|---|---|
|
|
| Boot | `rc.d/fips`, an `rc.conf`-gated rc.subr service | pfSense's `rc.start_packages` globs `/usr/local/etc/rc.d/*.sh` and runs each as `<script> start`, at boot and again on a WAN IP change. A suffixless script is never run; the `.sh` script must make a re-run a quiet no-op. |
|
|
| DNS | drop-in at `/var/unbound/conf.d/fips.conf` | `unbound.conf` is generated from `config.xml` and includes no `conf.d` directory. The drop-in is simply never read. |
|
|
| Responder bind | `::1` | pfSense writes `do-ip6: no` unless "Allow IPv6" is set, and then cannot reach `[::1]` at all. This package binds `127.0.0.1`. |
|
|
|
|
So: `fips.sh` instead of `fips`, DNS Resolver custom options instead of
|
|
a drop-in, and knobs in `/usr/local/etc/fips/fips.conf` instead of
|
|
`/etc/rc.conf`.
|
|
|
|
The mechanics that are *not* different are shared rather than copied:
|
|
`packaging/common/pkg-lib.sh` holds version derivation, the stage
|
|
layout, the manifest fields both packages agree on, the @sample
|
|
install-if-absent scripts, and `pkg create` itself. Both builders
|
|
source it. What stays per-package is only what the two systems
|
|
genuinely disagree about — boot, DNS, linkage, ABI and naming — since
|
|
folding those into one file behind flags would hide the differences
|
|
this table exists to explain.
|
|
|
|
## Which pfSense this matches
|
|
|
|
`pkg` refuses a package whose ABI does not match the running system, in
|
|
**both** the FreeBSD major and the architecture. Netgate's ARM
|
|
appliances are `aarch64`, so "which pfSense" is two questions, not one.
|
|
Ask the appliance rather than guessing:
|
|
|
|
```sh
|
|
pkg config abi # e.g. FreeBSD:16:aarch64
|
|
uname -m
|
|
```
|
|
|
|
The supported releases, from [Netgate's version
|
|
table](https://docs.netgate.com/pfsense/en/latest/releases/versions.html)
|
|
as of September 2026:
|
|
|
|
| Release | FreeBSD base | pkg ABI | Build host |
|
|
|---|---|---|---|
|
|
| pfSense CE 2.8.1 | 15.0-CURRENT | `FreeBSD:15:amd64` | FreeBSD 15, amd64 |
|
|
| pfSense CE 2.9.0 | 16.0-CURRENT | `FreeBSD:16:amd64` | FreeBSD 15 build, relabelled (see below) |
|
|
| pfSense Plus 26.03.1 / 26.07, Intel | 16.0-CURRENT | `FreeBSD:16:amd64` | FreeBSD 15 build, relabelled (see below) |
|
|
| pfSense Plus 26.03.1 / 26.07, ARM | 16.0-CURRENT | `FreeBSD:16:aarch64` | FreeBSD 16, **aarch64** |
|
|
|
|
CE 2.8.1 can no longer be installed: the 2.8 line shipped only through
|
|
the Netgate installer, which offers the current release, and the public
|
|
mirror stops at the 2.7.2 ISOs. Its package serves existing 2.8.1
|
|
installs and can only be tested on plain FreeBSD 15. Every pfSense a new
|
|
user can install runs FreeBSD 16.
|
|
|
|
CE has only ever shipped for amd64; Netgate has said there are no plans
|
|
for an ARM CE image. Plus 24.x and 25.x are end-of-life and deliberately
|
|
not in the build's table: a package named for an unsupported release
|
|
invites installing it there. The base moves between releases — CE 2.9
|
|
moved to FreeBSD 16 in August 2026 — so check the table before building.
|
|
|
|
The filename names the **pfSense product(s)**, not the FreeBSD ABI —
|
|
`fips-<version>-pfsense-<products>-<arch>.pkg` — because that is what
|
|
someone choosing a download knows. You know you run "Plus 26.03 on a
|
|
4200"; you do not necessarily know that means `FreeBSD:16:aarch64`.
|
|
|
|
One ABI can serve more than one product. CE 2.9 and Plus 26.x on Intel
|
|
are both `FreeBSD:16:amd64` and the artifact is byte-identical, so its
|
|
name carries both: `…-pfsense-ce2.9-plus26-amd64.pkg`. Architecture
|
|
alone would not do either way — CE 2.8 and CE 2.9 are both `amd64` and
|
|
`pkg` refuses each on the other's base.
|
|
|
|
The mapping is ABI → products, declared in one place in `build-pkg.sh`,
|
|
because the ABI is what gets built and the products are what people look
|
|
for. The build **refuses** an ABI that no supported release runs on
|
|
(`FreeBSD:15:aarch64`: only end-of-life Plus versions), and `--product`
|
|
is an assertion rather than a selector: pass it to say "I believe I am
|
|
building for CE 2.8", and the build refuses if that product does not run
|
|
on the ABI. The ABI stays in the manifest, where `pkg` reads it, and the
|
|
products in a `pfsense_products` annotation, so a renamed file can still
|
|
identify itself:
|
|
|
|
```sh
|
|
pkg info -A -F <file>.pkg # pfsense_products: ce2.9 plus26
|
|
pkg info -F <file>.pkg # Architecture: FreeBSD:16:amd64
|
|
```
|
|
|
|
| Artifact | Installs on |
|
|
|---|---|
|
|
| `…-pfsense-ce2.8-amd64.pkg` | pfSense CE 2.8.1 |
|
|
| `…-pfsense-ce2.9-plus26-amd64.pkg` | pfSense CE 2.9.0, and Plus 26.x on Intel |
|
|
| `…-pfsense-plus26-aarch64.pkg` | pfSense Plus 26.x on ARM |
|
|
|
|
Which of those can be built, and with what provenance, differs — and the
|
|
difference decides which may be published:
|
|
|
|
| Artifact | linkage | toolchain pin | CI |
|
|
|---|---|---|---|
|
|
| `…-pfsense-ce2.8-amd64.pkg` | static | honoured | built, checked, install-smoked; release asset |
|
|
| `…-pfsense-ce2.9-plus26-amd64.pkg` | static | honoured | built (the CE 2.8 binaries, relabelled), checked; release asset |
|
|
| `…-pfsense-plus26-aarch64.pkg` | dynamic | **not** honoured | not built — build it yourself |
|
|
|
|
Both Intel packages are release assets: each tagged release carries
|
|
them next to the FreeBSD package, with their SHA-256 in
|
|
`checksums-freebsd.txt`. They are built and checked in their own CI job,
|
|
so a pfSense-only failure reds that job by name, and the release job
|
|
needs it, so such a failure holds the release rather than shipping
|
|
without them. On every other ref the job's output is a 30-day workflow
|
|
artifact for anyone to test. "Install-smoked" means
|
|
`testing/pfsense-install-smoke.sh` ran it on the plain FreeBSD VM of the
|
|
same major: `pkg add`, the boot script's start, re-entrant start, restart
|
|
and stop with the real daemon answering `fipsctl` and DNS queries, a
|
|
forced `newsyslog` rotation on the shipped entry with the daemon's open
|
|
log following to the new `/var/log/fips.log`, then `pkg delete`. That is
|
|
the same script to run first on a real box; its header says what a
|
|
plain-FreeBSD pass does not prove.
|
|
|
|
The FreeBSD 16 Intel package is the FreeBSD 15 package's binaries under
|
|
a `FreeBSD:16:amd64` label: the CI job builds once on FreeBSD 15.1 and
|
|
runs `build-pkg.sh --no-build --abi FreeBSD:16:amd64` for the second
|
|
package. That is the direction FreeBSD's binary compatibility runs,
|
|
older binaries on a newer kernel, so no FreeBSD 16 build host is needed
|
|
while 16 has no release (the CI VM is 15.1 and `vmactions/freebsd-vm`
|
|
offers nothing newer; a 16.0-CURRENT snapshot would also be *newer*
|
|
than any Netgate base, the direction that is not promised). The
|
|
relabelled package has been run on pfSense Plus 26.03.1 and 26.07; see
|
|
the test record at the end. What CI cannot do with it is `pkg add`, since
|
|
pkg refuses a package whose ABI major differs from the host's: the
|
|
install smoke runs on the FreeBSD 15 package, which carries the same
|
|
bytes, and the checker verifies the label against the binaries from the
|
|
outside. This holds as long as the code builds on 15.1 without needing
|
|
something only 16 provides; if that changes, a FreeBSD 16 build host is
|
|
needed again. ARM cannot honour the pin at all, so it stays
|
|
build-it-yourself regardless of infrastructure.
|
|
|
|
### There is no cross-compiling out of this
|
|
|
|
The build host must genuinely be the target's architecture. Two reasons,
|
|
and the first is the one that wastes an afternoon:
|
|
|
|
- **`--abi` only relabels.** It changes the string `pkg` checks, not the
|
|
binaries. A package labelled `FreeBSD:16:aarch64` full of x86-64
|
|
binaries installs perfectly and then cannot exec.
|
|
`testing/check-pfsense-pkg.sh` compares the label against the real
|
|
binaries for exactly this reason, and fails the package.
|
|
- **Rust ships no toolchain for FreeBSD/ARM, in either direction.**
|
|
`rustup target add aarch64-unknown-freebsd` on an x86-64 FreeBSD host
|
|
fails with "no prebuilt artifacts available for target" — only
|
|
`i686-unknown-freebsd` and `x86_64-unknown-freebsd` are offered — and
|
|
rustup has no installer for the platform natively either. What makes a
|
|
native aarch64 builder the path of least resistance is not rustup but
|
|
the ports Rust (`pkg install rust`), with the pin consequences below.
|
|
|
|
`build-pkg.sh --target <triple>` exists for a builder that is already
|
|
the right architecture (it reads binaries from `target/<triple>/release`
|
|
and cross-checks the triple against `--abi`). It does not conjure a
|
|
toolchain that Rust does not distribute.
|
|
|
|
### ARM builds are build-it-yourself, and are not released
|
|
|
|
**No aarch64 package is published as a release artifact.** Build one
|
|
yourself with the recipe above, on your own aarch64 FreeBSD 16 host.
|
|
|
|
The reason is the toolchain, not the architecture. `rust-toolchain.toml`
|
|
pins an exact compiler, and every published artifact for every other
|
|
platform is built with it. On aarch64 FreeBSD that is impossible:
|
|
|
|
```
|
|
$ rustup target add aarch64-unknown-freebsd
|
|
error: toolchain '1.94.1-x86_64-unknown-freebsd' has no prebuilt
|
|
artifacts available for target 'aarch64-unknown-freebsd'
|
|
|
|
$ (on an aarch64 FreeBSD host)
|
|
error: installer for platform 'aarch64-unknown-freebsd' not found
|
|
```
|
|
|
|
So an ARM build uses the ports Rust, and the ports cargo ignores
|
|
`rust-toolchain.toml` outright. Publishing such a package alongside the
|
|
others would quietly imply a provenance it does not have.
|
|
|
|
The alternatives were considered and rejected: `RUSTC_BOOTSTRAP=1` with
|
|
`-Z build-std` nominally satisfies the pin, but only by disabling the
|
|
stable/unstable boundary and rebuilding `std` through a path upstream
|
|
does not support — a *less* visible deviation than a different version
|
|
number, for a daemon that terminates encrypted tunnels on a firewall.
|
|
Building rustc 1.94.1 from source is a multi-hour bootstrap that still
|
|
yields an unofficial, unverifiable compiler.
|
|
|
|
Every package records what produced it, so this is answerable from the
|
|
artifact rather than from whoever remembers building it:
|
|
|
|
```sh
|
|
pkg info -A fips
|
|
# built_with : rustc <ports version>
|
|
# toolchain_pin : 1.94.1
|
|
# pin_honoured : no <- not a release artifact
|
|
# linkage : static
|
|
# rust_pkg : rust-<ports version>
|
|
```
|
|
|
|
`build-pkg.sh` prints a loud notice whenever the compiler is not the
|
|
pinned one, and refuses outright below the edition-2024 floor (1.85).
|
|
`testing/check-pfsense-pkg.sh` fails a package that has lost these
|
|
annotations, and flags `pin_honoured: no` in its output.
|
|
|
|
### FreeBSD 16 is not released
|
|
|
|
pfSense CE 2.9 and Plus 26.x are built from FreeBSD **16.0-CURRENT**, a development
|
|
branch; 16.0-RELEASE does not exist yet. A FreeBSD 16 build host would
|
|
therefore be a [16.0-CURRENT
|
|
snapshot](https://download.freebsd.org/snapshots/), not a release image,
|
|
and one that is months *newer* than any Netgate base (their releases
|
|
track a `main` commit from several months earlier). Binaries built there
|
|
would run on the appliance in the direction FreeBSD does not promise.
|
|
That is why the FreeBSD 16 amd64 package is not built on 16 at all but is
|
|
the FreeBSD 15.1 static build relabelled, which runs in the promised
|
|
direction and has been verified on Plus 26.03.1 and 26.07.
|
|
|
|
The drift is real for a `--dynamic` build, on either major: Netgate's
|
|
`16.0-CURRENT@<hash>` and a FreeBSD tree from another date are different
|
|
trees, and a binary can reference a symbol the appliance's `libc` does
|
|
not export. It installs and then fails to start. If `fips` exits
|
|
immediately with a linker error, that is this. Build from a base no newer
|
|
than the appliance's, and check what the binary actually needs:
|
|
|
|
```sh
|
|
pkg info -F <the .pkg> | grep -A5 "Shared Libs" # on the build host
|
|
ldd /usr/local/bin/fips # on the appliance
|
|
```
|
|
|
|
## Build
|
|
|
|
```sh
|
|
gmake -C packaging pfsense # or:
|
|
./packaging/pfsense/build-pkg.sh # cargo build --release + pkg create
|
|
./packaging/pfsense/build-pkg.sh --no-build # package existing release binaries
|
|
./packaging/pfsense/build-pkg.sh --no-build --abi FreeBSD:16:amd64 # the same binaries, labelled for FreeBSD 16
|
|
./packaging/pfsense/build-pkg.sh --dynamic # link against libc.so.7 (see below)
|
|
```
|
|
|
|
Output: `deploy/fips-<version>-pfsense-<products>-<arch>.pkg`. Validate it before
|
|
shipping it anywhere:
|
|
|
|
```sh
|
|
./testing/check-pfsense-pkg.sh deploy/fips-<version>-pfsense-ce2.8-amd64.pkg
|
|
```
|
|
|
|
### Static linking is the default
|
|
|
|
Unlike every other platform's package, this one links statically unless
|
|
you ask otherwise. The reason is specific to pfSense: **it runs a
|
|
FreeBSD base you cannot obtain.** Netgate builds Plus from a
|
|
16.0-CURRENT snapshot of their own, and download.freebsd.org keeps only
|
|
the last two CURRENT builds — so there is usually no way to build
|
|
against the appliance's libraries even if you want to.
|
|
|
|
In practice the build host's `libc` ends up *newer* than the
|
|
appliance's, which is the direction that breaks: the binary references a
|
|
versioned symbol the appliance does not export, installs cleanly, and
|
|
then will not start. A dynamic package needs `libc.so.7`, `libm.so.5`,
|
|
`libthr.so.3` and `libgcc_s.so.1` to agree with it; a static one
|
|
declares no shared libraries at all. What is left is the kernel's
|
|
binary compatibility, which FreeBSD promises in one direction only:
|
|
binaries from an older release run on a newer kernel. Build on a base no
|
|
newer than the appliance's.
|
|
|
|
That is also why a static package survives a pfSense firmware upgrade's
|
|
change of base, where a dynamic one is pinned to the image it was built
|
|
against.
|
|
|
|
It is viable here because nothing in this codebase uses `dlopen` or
|
|
`libloading`, and FreeBSD compiles `files`/`dns` resolution into `libc`
|
|
— so a static binary still resolves hostnames. (This is where static
|
|
*glibc* would defeat you; FreeBSD is not glibc.) Verified end to end: a
|
|
static build resolves a peer hostname, completes the Noise handshake,
|
|
joins the spanning tree and answers `.fips` queries.
|
|
|
|
`crt-static` is a request a target may silently ignore, so
|
|
`build-pkg.sh` checks the produced binaries and fails if any came out
|
|
dynamic — shipping a dynamic binary while believing it static would
|
|
quietly reinstate the exposure this default exists to remove. The
|
|
package records which it is, in the `linkage` annotation.
|
|
|
|
**aarch64 is the exception, and the build refuses rather than
|
|
downgrades.** A statically linked aarch64 FreeBSD binary faults at
|
|
`addr=0x0` where `posix_spawn` should be, so the daemon dies the
|
|
first time it shells out — `sysctl`, from `is_ipv6_disabled()` at the
|
|
top of `TunDevice::create`. It presents as a TUN bug, and with
|
|
`tun.enabled: false` the daemon never spawns anything and looks
|
|
healthy. The same trace on static amd64 reaches `rfork(RFSPAWN)` and
|
|
spawns normally, so this is the architecture, not static linking.
|
|
ARM builds must pass `--dynamic`; `ldd` on the appliance then tells
|
|
you whether the drift this default exists to avoid is real.
|
|
|
|
Use `--dynamic` if you specifically want the smaller binaries and know
|
|
your build host's base matches the appliance's.
|
|
|
|
## Install
|
|
|
|
Copy the package to the firewall and, as root:
|
|
|
|
```sh
|
|
pkg add ./fips-<version>-pfsense-ce2.8-amd64.pkg
|
|
vi /usr/local/etc/fips/fips.yaml # identity and peers
|
|
/usr/local/etc/rc.d/fips.sh start
|
|
fipsctl show status
|
|
```
|
|
|
|
To upgrade an existing install, use `pkg install ./<file>.pkg`, not
|
|
`pkg add`: only `pkg install` runs the upgrade path (the old package's
|
|
pre-deinstall then the new post-install, with `PKG_UPGRADE=true`), which
|
|
stops the daemon before its binary is replaced and starts it after.
|
|
`pkg add` on an installed package refuses without `-f` and, with `-f`,
|
|
reinstalls without those hooks — so follow a `pkg add -f` with
|
|
`/usr/local/etc/rc.d/fips.sh restart` by hand. `pkg upgrade` does not
|
|
apply: these packages are in no repository.
|
|
|
|
Then, separately and deliberately (it edits `config.xml`):
|
|
|
|
```sh
|
|
/usr/local/libexec/fips/fips-dns-setup
|
|
```
|
|
|
|
The daemon starts at boot from then on. To keep it installed but
|
|
dormant, set `fips_enable="NO"` in `/usr/local/etc/fips/fips.conf`;
|
|
`fips.sh onestart` still starts it by hand.
|
|
|
|
### "Allow IPv6" and the responder bind
|
|
|
|
`Allow IPv6` (System > Advanced > Networking) is **on in the factory
|
|
configuration**, so most installs need nothing here. The notes below are
|
|
for a firewall where it has been turned off.
|
|
|
|
|
|
**System > Advanced > Networking > Allow IPv6.** The mesh is IPv6
|
|
(`fd00::/8`) end to end. With that setting off, pfSense emits
|
|
|
|
```
|
|
block in quick inet6 all
|
|
block out quick inet6 all
|
|
```
|
|
|
|
and a `quick` rule matches immediately — **no rule you add can override
|
|
it**. The mesh is dead in both directions.
|
|
|
|
The trap is that this does not look like a failure. The loopback IPv6
|
|
pass rule is unconditional, so the DNS responder keeps answering and
|
|
`.fips` names keep resolving; the outer UDP and TCP transports are IPv4
|
|
and keep peering happily. `fipsctl show status` looks healthy while
|
|
nothing crosses the mesh. **`.fips` resolving is not evidence that the
|
|
mesh carries traffic** — ping the address it returns.
|
|
|
|
Verify:
|
|
|
|
```sh
|
|
pfctl -sr | grep -c "Block all IPv6" # must be 0
|
|
pfctl -sr | grep "let out anything IPv6" # must be present
|
|
```
|
|
|
|
## .fips DNS integration
|
|
|
|
`fips-dns-setup` adds a marked block to **Services > DNS Resolver >
|
|
Custom options**, which is the only operator-writable surface in the
|
|
generated `unbound.conf`:
|
|
|
|
```
|
|
# BEGIN FIPS - managed by fips-dns-setup, do not edit this block
|
|
server:
|
|
domain-insecure: "fips."
|
|
do-not-query-localhost: no
|
|
|
|
forward-zone:
|
|
name: "fips."
|
|
forward-addr: 127.0.0.1@5354
|
|
forward-first: no
|
|
# END FIPS
|
|
```
|
|
|
|
Each line earns its place:
|
|
|
|
- `domain-insecure` — the `.fips` zone is unsigned and pfSense validates
|
|
DNSSEC by default, so without it every answer is discarded as bogus.
|
|
- `do-not-query-localhost: no` — unbound refuses loopback forwarders by
|
|
default, which SERVFAILs every `.fips` query rather than asking the
|
|
daemon. pfSense's `unbound.inc` never sets this. Note this applies to
|
|
the **whole resolver**, not only the `fips.` zone: after this, unbound
|
|
will also forward other loopback-directed queries it would otherwise
|
|
refuse. On a firewall whose only loopback listener is the FIPS
|
|
responder that changes nothing, but it is a resolver-wide setting.
|
|
- `forward-first: no` — never fall back to the public resolvers for a
|
|
name the daemon declined. `.fips` does not exist outside the mesh, and
|
|
leaking the query would publish which npubs this firewall talks to.
|
|
|
|
It is stored (base64-encoded) in `config.xml`, which is the point:
|
|
`config.xml` is pfSense's durable store — it survives reboots, config
|
|
restores, and removal of this package — so the `fips.` zone is not tied
|
|
to the package's own files. If the package is ever removed without
|
|
`fips-dns-teardown`, the zone keeps pointing at `127.0.0.1:5354` and
|
|
`.fips` fails loudly with SERVFAIL rather than resolving to something
|
|
else.
|
|
|
|
The block is written between markers and everything outside them is left
|
|
byte-for-byte alone, so your own custom options are safe. `write_config()`
|
|
records a config-history entry, so the edit is revertable from
|
|
**Diagnostics > Backup & Restore > Config History**.
|
|
|
|
To undo it while keeping the daemon:
|
|
|
|
```sh
|
|
/usr/local/libexec/fips/fips-dns-teardown
|
|
```
|
|
|
|
If this firewall uses the **DNS Forwarder (dnsmasq)** rather than the
|
|
DNS Resolver, the script says so; the equivalent single line under
|
|
Services > DNS Forwarder > Advanced Options is:
|
|
|
|
```
|
|
server=/fips/127.0.0.1#5354
|
|
```
|
|
|
|
## Firewall rules and the TUN interface
|
|
|
|
The daemon creates a `tun` interface for the mesh. Left unassigned, the
|
|
default pfSense ruleset gives it the posture most people want:
|
|
|
|
- `pass out ... all keep state` ("let out anything from firewall host
|
|
itself") is not interface-scoped, so it covers `tun` — **outbound
|
|
passes and creates state, replies return on that state**;
|
|
- the default deny covers inbound, so **unsolicited inbound is blocked**.
|
|
|
|
The sample `fips.yaml` binds its UDP and TCP transports to `0.0.0.0`,
|
|
i.e. every interface including WAN. Nothing is reachable from outside
|
|
only because pfSense's default WAN policy passes no unsolicited inbound;
|
|
if you add a WAN pass rule for another service, make sure it does not
|
|
cover the FIPS ports (2121/udp, 8443/tcp by default), or set
|
|
`transports.udp.bind_addr` / `transports.tcp.bind_addr` to the LAN
|
|
address. To accept inbound mesh connections deliberately, assign the
|
|
interface (Interfaces > Assignments) and add pass rules. Two caveats:
|
|
|
|
- **The interface name is kernel-assigned.** On FreeBSD the daemon reads
|
|
back whatever the kernel gave it (`tun0`, `tun1`, ...); `tun.name` in
|
|
`fips.yaml` is silently ignored on this platform. A pfSense assignment
|
|
pins a name, so confirm it is stable across daemon restarts and
|
|
reboots before building rules on it.
|
|
- **Match the daemon's posture to pf's.** pf dropping inbound does not
|
|
stop the daemon advertising itself as reachable — peers keep dialing
|
|
an endpoint that drops. If you are not allowing inbound, set
|
|
`transports.udp.accept_connections: false` (refuses inbound `msg1` at
|
|
the protocol level) or `outbound_only: true` (pure client) in
|
|
`fips.yaml`.
|
|
|
|
LAN clients reaching the mesh through the firewall rely on state from
|
|
the LAN pass rule. That holds under the default *floating* state policy;
|
|
if System > Advanced > Firewall is set to interface-bound states, the
|
|
`tun` side needs its own pass rule.
|
|
|
|
## Upgrades and removal
|
|
|
|
A pfSense **firmware upgrade** does not remove this package.
|
|
`pfSense-upgrade` deletes and reinstalls only `pfSense-pkg-*` packages;
|
|
`fips` is a plain pkg and is left in place. This was confirmed on a live
|
|
**Plus 26.03.1 → 26.07** upgrade (aarch64): the package survived, the
|
|
daemon restarted at boot, and `.fips` still resolved. That is a *minor*
|
|
base change (FreeBSD 16 → 16). At a **major** change (CE 2.8.1 on
|
|
FreeBSD 15 → CE 2.9.0 on FreeBSD 16) a FreeBSD-15 binary runs on a
|
|
FreeBSD-16 kernel only through that kernel's compat layer — not tested —
|
|
so after a major upgrade rebuild and reinstall the package for the new
|
|
base (the `ce2.9-plus26-amd64` one)
|
|
rather than trusting compat indefinitely. (The minor-upgrade survival
|
|
above is from the hardware run; the cross-major compat behaviour is only
|
|
what `pfSense-upgrade`'s source implies — see "What is and is not
|
|
tested".)
|
|
|
|
A **package upgrade** (`pkg install ./<newer>.pkg`) stops the daemon
|
|
before replacing its binary and starts it again afterwards only if it
|
|
had been running. **`pkg delete`** stops it, takes the `.fips` block back
|
|
out of the DNS Resolver, and deletes the config files only if they are
|
|
still byte-identical to the shipped samples — an edited `fips.yaml`, and
|
|
the identity key it may hold, is left in place.
|
|
|
|
## Debugging
|
|
|
|
```sh
|
|
/usr/local/etc/rc.d/fips.sh status
|
|
tail -f /var/log/fips.log
|
|
|
|
drill -p 5354 <npub>.fips @127.0.0.1 AAAA # the daemon directly
|
|
drill <npub>.fips AAAA # the full chain via unbound
|
|
ping6 <the AAAA it returned> # proves the mesh, not just DNS
|
|
|
|
unbound-checkconf /var/unbound/unbound.conf
|
|
grep -A8 "Unbound custom options" /var/unbound/unbound.conf
|
|
pfctl -ss | grep tun # mesh state entries
|
|
```
|
|
|
|
`ifconfig <tun-name>` prints `Opened by PID <n>` for the process holding
|
|
a tun device. After the daemon exits the interface stays listed, down,
|
|
without an address and with nobody holding it (observed on Plus 26.03.1
|
|
and 26.07); the next start opens it again.
|
|
|
|
## What is and is not tested
|
|
|
|
`testing/check-pfsense-pkg.sh` validates the package contents, the boot
|
|
script's behaviour and the config it ships, on any FreeBSD host. It runs
|
|
in CI. What it cannot cover — installing on pfSense, the `config.xml`
|
|
edit (which needs pfSense's PHP and `config.inc`), unbound answering
|
|
`.fips`, and pf passing mesh traffic — has no pfSense CI image to
|
|
automate against and remains a manual step.
|
|
|
|
Those manual steps have been exercised once, on pfSense Plus 26.03.1
|
|
aarch64: package install, boot script lifecycle, `fips-dns-setup`
|
|
writing the DNS Resolver block, `.fips` resolving through unbound, the
|
|
TUN interface coming up, and the mesh carrying traffic (304 packets
|
|
delivered, no loss, no drops). That is one run on one appliance, not a
|
|
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) and its DNS Forwarder branch.
|
|
(`fips-dns-teardown` has since been run on the same box and restored
|
|
`custom_options` byte for byte.)
|
|
|
|
**amd64 on pfSense.** The FreeBSD 16 amd64 package has been run on
|
|
pfSense Plus in KVM virtual machines installed with the Netgate
|
|
installer, so on Netgate's kernel. First a package built on the
|
|
16.0-CURRENT 20260907 snapshot, on Plus 26.07; that run found that
|
|
`fips-dns-setup` never restarted a running unbound. Then the package CI
|
|
now produces, the FreeBSD 15.1 build relabelled, on both Plus 26.03.1
|
|
(`plus-RELENG_26_03_1-n256546-1d1bfd578383`, `kern.osreldate` 1600011)
|
|
and Plus 26.07 (`plus-RELENG_26_07-n256584-8183aef9d019`, 1600018), with
|
|
the same result on each: `testing/pfsense-install-smoke.sh` (45 checks),
|
|
the TUN interface up with its mesh address, the responder answering the
|
|
node's own name directly, `fips-dns-setup` writing the block and `.fips`
|
|
resolving through unbound once the resolver was restarted (that package
|
|
predates the fix that makes `fips-dns-setup` do it), `pfSctl -c 'service
|
|
reload packages'` leaving the running daemon alone, a reboot bringing up
|
|
exactly one daemon with the DNS Resolver block regenerated and resolving,
|
|
`fips-dns-teardown` leaving `custom_options` as it was, and `pkg delete`
|
|
against the running daemon. The two boxes were then peered with each
|
|
other over UDP (a pass-in rule on the tun interface loaded into pf's
|
|
`userrules` anchor, the "accept inbound deliberately" posture above):
|
|
the link authenticated, `ping6` across the mesh ran 200 packets of 56
|
|
bytes and 100 of 1100 bytes each way with no loss, and 10 MiB by TCP each
|
|
way arrived byte-exact. Packets above the daemon's effective MTU (1203
|
|
bytes over the 1280-byte UDP transport) are answered with ICMPv6 Packet
|
|
Too Big and TCP is MSS-clamped, as the no-fragmentation policy in
|
|
`docs/design/fips-mtu.md` says, so a fixed-size `ping6 -s 1160` or larger
|
|
shows loss by design on every platform. What the VMs did not cover:
|
|
physical hardware, and CE 2.9.0 (no installer at hand).
|
|
The CE 2.8.1 (FreeBSD 15) package carries the same binaries and is
|
|
install-smoked on plain FreeBSD 15.1 in CI; it cannot be run on pfSense
|
|
because CE 2.8.1 media no longer exists.
|
|
|
|
Left behind by `pkg delete`, by design or as known gaps:
|
|
`/usr/local/etc/fips/fips.key` if the daemon generated one in persistent
|
|
mode (it is the node's identity), or `fips.key.unused` if it moved a key
|
|
aside in ephemeral mode, `/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.
|