Any host able to query the LAN resolver could drain the gateway's 65,535-address virtual-IP pool one `.fips` name at a time, and each allocation rebuilt the whole nftables table in a way that could leave the host with no NAT at all. Four changes, each independently useful, close that off. Do not allocate for query types the gateway never answers with an address. handle_query minted a virtual IP for every query type and only then looked at what the client asked, answering an A or HTTPS query with NODATA after creating a mapping for it. The query type is now decided before the pool is touched, and only AAAA and ANY allocate. The refresh an existing mapping used to get from any query type is kept: it came from the reuse path in allocate, so a new pool method does that refresh alone and never creates anything, and the reuse path calls it. Rebuild the NAT table in one netlink transaction. rebuild() deleted the fips_gateway table in a batch of its own and discarded the result, then sent a second batch recreating the table, the chains, the fips0 masquerade and two rules per mapping. Between those sends the host had no NAT table, and a recreate the kernel refused left the table deleted, turning one failed mapping change into a total loss of forwarding until some later rebuild happened to succeed. The delete and the recreate now share one batch. A leading table add makes the delete legal on the first run, since rustables sends it with NLM_F_CREATE and no NLM_F_EXCL and the crate offers no flush. Deciding what to send is now separate from sending it, which is the seam the new unit tests use: they assert one batch, the add-delete-add prefix, and that every chain and rule follows the recreate, without a netlink socket or privileges. Read conntrack once per tick, off the runtime thread, and match by address. The session count searched each /proc/net/nf_conntrack line for `dst=` followed by the virtual IP's compressed Display form, while the kernel prints every tuple with `%pI6`, the full uncompressed form. That string cannot occur in that field, so the count was zero for every mapping on every kernel that has the file: nothing pinned an in-use mapping and one whose client did not re-query DNS was reclaimed about two minutes after its last DNS reference with traffic still flowing. Each `dst=` is now parsed and compared as an address. The read was also per mapping, under the pool lock, on the runtime thread that serves DNS; the tick now takes one snapshot in a blocking task before taking the lock. An unreadable source was silent, because read_to_string's error became zero through unwrap_or(0). Zero stays, since treating it as in-use would pin every mapping forever on a kernel without CONFIG_NF_CONNTRACK_PROCFS, but it is now reported at warn on the first failure and on each change of outcome, and at debug on a repeat. Ship the OpenWrt gateway disabled, and keep its state across upgrades. The generated postinst enabled and started fips-gateway on every install, against the init script's own header, the package README and the deployment tutorial, which all say the service ships disabled. A fresh install now leaves it alone. Upgrades are the awkward case: opkg runs the outgoing package's prerm first, and every released prerm disabled the gateway on its way out without recording whether it had been enabled. The new prerm stops the services on an upgrade but no longer disables them, and leaves a marker the incoming postinst reads. With the marker, enablement survived and the gateway starts only if it was enabled; without it, the outgoing package was a released one whose prerm destroyed that state, so the gateway is re-enabled rather than letting an upgrade turn off a working deployment. That re-enables a hand-disabled gateway once, which the CHANGELOG says. start_service now reads gateway.enabled from fips.yaml before touching anything, since starting a gateway the config disables used to take dnsmasq's `.fips` forwarding away from the daemon and hand it to a port whose daemon exits immediately. The four maintainer-script bodies move out of heredocs in the two build scripts into packaging/openwrt-ipk/scripts/, so the .ipk and the .apk install the same bodies and a test can run what ships. Coverage recorded rather than closed. The conntrack parser's first test builds its line from the kernel's own format string rather than a capture, because this host is built without CONFIG_NF_CONNTRACK_PROCFS and has no /proc/net/nf_conntrack, so the lab exercises only the unreadable path. Kernel acceptance of delete-then-recreate inside one transaction is not asserted by a unit test; the gateway suite is what proves it, since the manager rebuilds at startup and the daemon exits if that fails. The OpenWrt scenarios run the shipped script bodies under ash in a busybox container against stubbed init scripts, and assert their behaviour given opkg's call order, arguments and PKG_UPGRADE as read from opkg-lede's sources, not under a real opkg upgrade on a router image. Admission limits on the pool are deliberately not included here: they need a measurement run before their constants can be chosen.
FIPS Packaging
This directory contains packaging for all supported target platforms.
Most build outputs go to deploy/ at the project root; make ipk
and make apk write to dist/ instead.
Quick Start
make deb # Debian/Ubuntu .deb (built in the pinned container)
make tarball # systemd install tarball
make ipk # OpenWrt .ipk (opkg, OpenWrt 24.x and earlier)
make apk # OpenWrt .apk (apk-tools, mandatory on OpenWrt 25+)
make aur # Arch Linux AUR package (fips-git, local build + namcap)
make pkg # macOS .pkg installer
make freebsd # FreeBSD .pkg package (on FreeBSD; use gmake)
make zip # Windows .zip package
make all # deb + tarball (default)
The two Debian build paths
make deb builds in a container pinned to the oldest supported
distribution, named with the glibc floor in
build-floor.env, and checks the package it produced
against that floor before handing it back. Its only host prerequisite is
docker: the toolchain and the build dependencies live in the image. This
is the path the release workflow, the integration suite and the internal
builder all take, so a package that passes locally is built the way the
shipped one is.
make deb-host is the old path. It builds on the host, at whatever glibc
the host has, and it is checked against nothing. Use it for local
iteration only. A package built on a current distribution records a
version dependency that the loader refuses on Debian 12 and Ubuntu 22.04,
which is what shipped in every Linux artifact from v0.3.0 through v0.5.0,
so it must not produce anything anyone else installs.
Build Prerequisites
The prerequisites below apply to the host-build targets. make deb needs
docker and nothing else.
These targets build FIPS from source, so the host needs a build
environment in addition to a Rust toolchain (the version pinned in
rust-toolchain.toml is auto-installed by rustup).
On Linux, libclang is required: the LAN gateway's nftables
bindings are generated by bindgen at build time, which needs
libclang.so on the build host. Without it the build fails inside the
rustables crate with an "Unable to find libclang" error.
sudo apt install libclang-dev # Debian / Ubuntu
This is a build-time prerequisite only — it is not a runtime
dependency, so hosts installing a pre-built .deb do not need it.
BLE is not optional, and it is not universal either. build.rs sets
ble_available for glibc Linux or Android, which is the set of
platforms with a concrete backend: the transport is absent from musl
Linux, macOS, FreeBSD and Windows builds entirely. On glibc Linux
libdbus-1-dev and pkg-config are hard build prerequisites: there is
no probe that skips BLE when they are missing. The BlueZ daemon is a
runtime dependency and is not needed to build.
Directory Structure
packaging/
aur/ Arch Linux AUR packaging (PKGBUILD, supporting files)
common/ Shared assets (default config, hosts file)
debian/ Debian/Ubuntu .deb packaging via cargo-deb
freebsd/ FreeBSD .pkg packaging via pkg-create(8)
macos/ macOS .pkg installer via pkgbuild
nixos/ NixOS flake module (services.fips.*)
systemd/ Generic Linux systemd tarball packaging
openwrt-ipk/ OpenWrt .ipk packaging via cargo-zigbuild (opkg)
openwrt-apk/ OpenWrt .apk packaging via cargo-zigbuild + apk mkpkg
windows/ Windows .zip package with service scripts
Formats
Debian/Ubuntu (.deb)
Built with cargo-deb. Installs
fips, fipsctl, fipstop, and fips-gateway to /usr/bin/, ships
the fips, fips-dns, fips-firewall, and fips-gateway systemd
units, and enables the fips and fips-dns services.
The default configuration ships as an example at
/usr/share/fips/fips.yaml.example and is not a dpkg conf-file.
(It is deliberately not under /usr/share/doc, which minimal and
container installs path-exclude, since the postinst reads it at install
time.)
On install, postinst seeds /etc/fips/fips.yaml (mode 600) from the
example only if it does not already exist, so a configuration that
was rendered by configuration management or edited by an operator is
never prompted for or clobbered on upgrade. To reset to defaults, remove
/etc/fips/fips.yaml and reinstall, or copy the example back manually.
# Build
make deb
# Install
sudo dpkg -i deploy/fips_<version>_<arch>.deb
# Remove (preserves config and keys)
sudo dpkg -r fips
# Purge (removes config and identity keys)
sudo dpkg -P fips
systemd Tarball
A self-contained tarball with binaries and an install.sh script for
any systemd-based Linux distribution.
# Build
make tarball
# Install (on target host)
tar -xzf deploy/fips-<version>-linux-<arch>.tar.gz
sudo ./fips-<version>-linux-<arch>/install.sh
See systemd/README.install.md for full installation and configuration instructions.
OpenWrt (.ipk, opkg — OpenWrt 24.x and earlier)
Cross-compiled with cargo-zigbuild and assembled as a standard .ipk
archive. Supports aarch64, mipsel, mips, arm, and x86_64 targets.
# Build (default: aarch64)
make ipk
# Build for a specific architecture
bash packaging/openwrt-ipk/build-ipk.sh --arch mipsel
See openwrt-ipk/README.md for router-specific installation instructions.
OpenWrt (.apk, apk-tools — mandatory on OpenWrt 25+)
OpenWrt 25 makes apk-tools the mandatory package manager (it is opt-in on
24.10). Same SDK-free approach
(cargo-zigbuild), but the .apk container is assembled by apk mkpkg
rather than hand-rolled, so the build additionally needs an apk-tools v3
apk binary built from source. The installed-filesystem payload is shared
with the .ipk package.
# Build (default: aarch64; also x86_64)
make apk
# Build for a specific architecture
bash packaging/openwrt-apk/build-apk.sh --arch x86_64
Packages are unsigned; install with apk add --allow-untrusted. See
openwrt-apk/README.md for building apk-tools and
router-specific installation.
macOS (.pkg)
Built with pkgbuild (included with Xcode command-line tools). Installs
binaries to /usr/local/bin/, config to /usr/local/etc/fips/, sets up
the /etc/resolver/fips DNS resolver for .fips domains, and loads a
launchd daemon. The TUN device is named utun<N> (kernel-assigned)
rather than fips0.
# Build
make pkg
# Install
sudo installer -pkg deploy/fips-<version>-macos-<arch>.pkg -target /
# Remove
sudo packaging/macos/uninstall.sh
FreeBSD (.pkg)
Built natively on a FreeBSD host with pkg create. Ships fips,
fipsctl, and fipstop (fips-gateway is excluded — its NAT backend
is nftables, Linux-only), rc.d services, and .fips DNS integration
for local_unbound, unbound, or dnsmasq. Config installs
sample-style under /usr/local/etc/fips/ (edits survive upgrades).
# Build (on FreeBSD; this Makefile needs GNU make — pkg install gmake)
gmake freebsd
# or directly, no gmake needed:
./packaging/freebsd/build-pkg.sh
# Install
pkg add ./deploy/fips-<version>-freebsd-<arch>.pkg
sysrc fips_enable=YES fips_dns_enable=YES
service fips start
service fips_dns start
See freebsd/README.md for host resolver setup and field-tested caveats.
Windows (.zip)
A ZIP archive containing binaries, default config, and PowerShell service helper scripts. Requires the wintun driver for TUN support.
# Build
make zip
# Or directly
powershell -File packaging/windows/build-zip.ps1
# Extract and install as service (requires Administrator)
Expand-Archive deploy\fips-<version>-windows-x86_64.zip -DestinationPath fips
cd fips
powershell -File install-service.ps1
# Uninstall (preserves config)
powershell -File uninstall-service.ps1
# Uninstall and remove config
powershell -File uninstall-service.ps1 -RemoveAll
Arch Linux (AUR)
Two AUR packages are maintained: fips (release, builds from tagged
tarball) and fips-git (development, builds from latest git master).
# Build and validate locally (git variant)
make aur
# Install from AUR
yay -S fips-git # development build from master
yay -S fips # release build from latest tag
See aur/README.md for AUR publication instructions and maintainer guide.
Nix / NixOS (flake)
A flake at the project root builds all four binaries
(fips, fipsctl, fips-gateway, fipstop) from source. It pins the
exact toolchain from rust-toolchain.toml via
fenix and wires up the
build-time native dependencies (libclang for bindgen, plus dbus
and pkg-config for BLE), so it needs no system setup beyond Nix with
flakes enabled.
nix build .#fips # build the package (all four binaries)
nix run .#fips -- --help # run a binary directly
nix run .#fipsctl -- show status
nix develop # dev shell with the pinned toolchain + cargo-edit
nix flake check # build + validate the flake
The flake also exposes:
overlays.default— addspkgs.fipsto nixpkgsnixosModules.default— a NixOS module (packaging/nixos/) that providesservices.fips.enableand runs the daemon as a systemd service
As a package only (no service management):
environment.systemPackages = [ fips.packages.${system}.default ];
As a managed NixOS service (recommended — starts on boot, journalctl logs):
# flake.nix
{
inputs.fips = {
url = "github:jmcorgan/fips";
inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, fips, ... }@inputs: {
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
specialArgs = { inherit inputs; };
modules = [
./configuration.nix
fips.nixosModules.default
{ services.fips.enable = true; }
];
};
};
}
See packaging/nixos/README.md for the full option
reference (services.fips.enable, .package, .configFile,
.openFirewall).
Shared Assets
common/ contains assets used across packaging formats:
fips.yaml— default configuration (ephemeral identity, UDP/TCP/TUN/DNS)hosts— static hostname-to-npub mappings for.fipsDNS resolution