Files
fips/docs/releases/release-notes-v0.4.0.md
Johnathan Corgan 15db6471db Set version to 0.4.1 and correct the v0.4.0 release date
Drop the -dev suffix from the package version and refresh the lockfile.
The version is single-sourced from CARGO_PKG_VERSION, so build.rs and
src/version.rs need no change.

Also fix the archived v0.4.0 release notes, which still read
"2026-06-21 (provisional)" while the changelog records the actual
release date of 2026-06-27. The date was confirmed in the changelog at
the time but not in the two release-notes files that carry it, so the
shipped notes have been showing a wrong date and the word provisional
since that release. The root mirror is regenerated for v0.4.1, so only
the archived copy needed correcting.
2026-07-19 17:59:37 +00:00

16 KiB

FIPS v0.4.0

Released: 2026-06-27

v0.4.0 is the throughput-and-observability release on the v0.3.x wire format. It adds two new ways for nodes to find and reach each other (the Nym mixnet transport and opt-in mDNS LAN discovery), overhauls the data plane for higher single-node throughput and lower per-packet CPU, moves the entire operator read surface off the data-plane hot path so observability stays responsive under load, ships a reworked fipstop TUI, and hardens FMP and FSP rekey to be hitless under packet loss in both directions. It also folds in the accumulated mesh-convergence, admission-control, and packaging fixes from the maintenance line.

v0.4.0 is wire-compatible with v0.3.0. Mixed meshes interoperate; there is no flag-day upgrade. A deployed v0.3.0 node and an upgraded v0.4.0 node peer, rekey, and route normally, so you can roll the upgrade out across a mesh in any order.

At a glance

  • New outbound Nym mixnet transport with a single-container demo and a new mixnet-relay example.
  • Opt-in mDNS / DNS-SD discovery on the local link.
  • Data-plane overhaul: off-task encrypt and decrypt worker pools, GSO, connected-UDP send path, copy-avoidance on receive, batched macOS receive.
  • The full show_* read surface now serves off the receive loop, so fipsctl and fipstop stay responsive on loaded nodes; a new counter-only show_metrics query enables a Prometheus scraper at no hot-path cost.
  • Reworked fipstop TUI on a machine-verified render-snapshot base.
  • Rekey is now hitless under loss and reordering in both directions.
  • New packaging targets: an OpenWrt .apk for OpenWrt 25+ and a Nix flake for reproducible from-source builds on Nix/NixOS.
  • Six route-class transit counters partition forwarded traffic by its tree relationship to the next hop, visible via show_routing and show_status.

What's new

Nym mixnet transport

FIPS can now peer over the Nym mixnet for metadata-resistant connectivity. The new transports.nym transport makes outbound connections through a nym-socks5-client SOCKS5 proxy that you run alongside the daemon (for example as a service running alongside the fips daemon, or as a sidecar container). The transport waits at startup for the nym-socks5-client to become ready before giving up.

This is a privacy and anonymity deployment mode chosen for its own properties. It mixes your FIPS traffic into the Nym cover-traffic network so that link-level observers cannot correlate which mesh peers are talking. A new examples/sidecar-nostr-mixnet-relay/ demonstrates a FIPS-reachable Nostr relay peered across the mixnet end to end, and a single-container demo ships with the transport.

Enable it by adding a transports.nym instance and pointing it at your running nym-socks5-client. See the transports reference for the field set.

mDNS LAN discovery

Nodes on a shared local link can now find each other with zero address configuration. The opt-in node.discovery.lan path runs an mDNS / DNS-SD responder and browser: each node advertises a FIPS service record on the link and adopts the peers it discovers. This complements the existing Nostr-mediated overlay discovery for the common case where the peers are simply on the same LAN.

Turn it on with node.discovery.lan.enabled: true. service_type and scope tune the advertised service record and which interfaces participate. Discovery on the local link needs no relay and no STUN.

Data-plane throughput overhaul

The receive and send paths were reworked for higher single-node throughput and lower per-packet CPU, building on the v0.3.0 crypto-backend swap:

  • Off-task encrypt and decrypt. Per-peer encrypt and decrypt now run on dedicated worker tasks rather than inline on the receive loop, so a single busy peer no longer serializes the whole node's crypto.
  • GSO and connected-UDP send. The Linux send path uses generic segmentation offload and a connected-UDP socket where available, cutting syscall overhead on bulk flows.
  • Copy-avoidance on receive. The receive hot path avoids buffer copies it previously made per packet.
  • Batched macOS receive. macOS gains a recvmsg_x batched receive, mirroring the Linux recvmmsg batching from v0.3.0.
  • Shared immutable-state context and an atomic metric registry. Immutable per-node state moved into a single shared context, and counters live in an atomic metric registry that the new show_metrics query reads without touching the hot path.

These are all internal to the data plane and require no operator action.

Observability off the hot path

Every read-only control query now renders from a snapshot published once per tick into a lock-free ArcSwap, served from the control accept task instead of round-tripping the data-plane receive loop. This covers show_status, show_stats_*, show_peers, show_sessions, show_links, show_connections, show_transports, show_mmp, show_tree, show_bloom, show_cache, show_routing, show_identity_cache, show_acl, show_listening_sockets, and the new show_metrics. Only the mutating connect and disconnect commands still reach the loop.

The practical effect: on a loaded node where the receive loop was busy, fipsctl and fipstop queries previously stalled or timed out (the five-second query pattern operators saw). They now answer promptly regardless of data-plane load. Per-entity snapshots reuse unchanged rows by pointer, so the per-tick publish cost stays bounded as peer and session counts grow.

A new show_metrics query (surfaced as fipsctl stats metrics) returns a counter-only snapshot of every metric family. It is the enabler for a Prometheus scraper that pulls node counters at no hot-path cost.

Six route-class transit counters partition transit-forwarded packets by their tree relationship to the chosen next hop — tree-up, tree-down, tree-down-cross, cross-link descend, cross-link ascend, and direct-peer — and the six classes sum to forwarded_packets. They surface through show_routing and show_status, and the fipstop routing tab is reorganized so its two columns separate own/endpoint traffic from forwarded/transit traffic with the tree-down-cross line visually flagged.

Reworked fipstop TUI

fipstop gets a rendering, navigation, and read-surface overhaul on a machine-verified base: a render-snapshot harness asserts the exact text grid and per-cell style of every view against canned control-socket output. New daemon-resolved fields surface through the snapshots, including effective persistence, root and is-root state, a per-transport-type peer-count map, per-peer effective depth, the root npub, and the last-sent uptree filter fill ratio with the subtree size estimate.

A separate fix clears a garbled-screen problem on startup and stray bytes on quit, most visible over SSH and inside tmux: startup now forces a full repaint before the first draw, and quit stops and joins the stdin-poll thread before restoring the terminal, so post-raw-mode keystrokes no longer echo onto the restored screen.

Rekey reliability

FMP and FSP session rekey are now hitless under packet loss and reordering in both directions:

  • Inbound frames are authenticated against the pending session before the K-bit cutover promotes it, so a spoofed or stale frame cannot derail a rekey in progress.
  • Rekey message-1 retransmission is bounded, and the link-dead heartbeat is rekey-aware so an in-flight rekey is not mistaken for a dead link.
  • FSP session rekey holds connectivity across the rekey window under loss and reordering.
  • Dual-initiation races (both peers starting a rekey at once on a high-latency link) are desynchronized with symmetric jitter so the two sides converge on one session rather than fighting.
  • An exhausted retransmission-budget abort, an expected and self-limiting outcome on lossy or high-latency links, is logged at debug rather than warn.

The net operator takeaway: rekey completes cleanly without dropping traffic, even on lossy or high-latency links, and the log no longer cries wolf when a rekey gives up and retries.

New packaging targets

  • OpenWrt .apk. A new .apk package targets OpenWrt 25+, where apk-tools is the mandatory package manager; the existing .ipk continues to cover OpenWrt 24.x and earlier. It is built SDK-free, reusing the .ipk cross-compile and installed-filesystem payload, and releases publish .apk artifacts and checksums alongside .ipk. Like the .ipk, the package is unsigned and installed with apk add --allow-untrusted.
  • Nix flake. A flake.nix at the project root builds all four binaries (fips, fipsctl, fips-gateway, fipstop) from source on Nix/NixOS, pinning the exact toolchain and wiring the native build dependencies so no host setup is needed beyond Nix with flakes enabled. It exposes nix build, nix run, a nix develop dev shell, and nix flake check, with flake.lock committed for reproducibility.

Behavior changes worth flagging

These affect operators on upgrade.

  • Bloom filter antipoison cap raised. node.bloom.max_inbound_fpr moves from 0.05 to 0.10, accepting filters with a higher derived false-positive rate before rejecting them. This reduces spurious filter rejections on larger meshes while keeping the antipoison protection in place.
  • TCP inbound cap honors max_connections. The TCP inbound accept ceiling now resolves from explicit per-transport max_inbound_connections, then node-wide node.limits.max_connections, then the built-in default of 256. Previously the TCP inbound ceiling was hardwired to 256 and ignored max_connections, so raising it had no effect on inbound TCP.
  • Static host aliases hot-reload. /etc/fips/hosts now reloads on mtime change once per tick rather than only at startup, so display names in fipsctl and fipstop reflect edits without a daemon restart. The peer ACL reloads through the same lock-free snapshot mechanism.
  • Quieter logs on busy public-mesh nodes. Routine per-peer connection-lifecycle and capacity-cap events, no-route session-datagram drops, and exhausted rekey-budget aborts are demoted to debug, so genuinely notable info and warn lines are no longer drowned out.
  • More visible drops. Receive-path silent rejections now flow through typed reject-reason counters, and discovery counts requests dropped when the dedup cache is full (req_dedup_cache_full, visible via show_routing). Drops that were previously silent are now countable.
  • Tor connect-refused accounting. The Tor transport increments its connect_refused statistic (the "Refused" line in fipstop) on an actively-refused SOCKS5 connect, instead of recording every connect failure as a generic SOCKS5 error.

Notable bug fixes

The CHANGELOG has the exhaustive list. This is the operator-relevant subset of fixes for behavior that shipped in v0.3.0.

  • Symmetric peer teardown on manual disconnect. A manual fipsctl disconnect now sends the peer a scoped Disconnect so both ends tear down and re-handshake cleanly. Previously a manual disconnect tore down only the local side, leaving the peer with a stale session that was never re-adopted as a child and whose bloom filter was never re-recorded.
  • Gateway holds long-lived and DNS-cached mappings. fips-gateway no longer drops a virtual-IP mapping while traffic is still flowing. The mapping TTL clock previously advanced only on DNS re-query, so a busy long-lived or DNS-cached client could have its mapping reclaimed mid-flow. The tick now refreshes the mapping whenever conntrack reports active sessions and recovers a draining mapping to active when traffic resumes; only genuinely idle mappings drain.
  • Accurate mesh-size estimate under filter overlap. The mesh-size estimator now estimates the cardinality of the OR-union of self plus every connected peer's inbound filter, instead of summing per-filter cardinalities of tree peers. Summing assumed the filters were disjoint, so a stale or oversized parent filter or a routing loop inflated the reported mesh size and a tree rebalance flapped the count. OR-union deduplicates overlap, equals the old result in the disjoint case, and removes the estimate's dependence on tree-declaration cache freshness.
  • Single-uplink node reattaches within a round-trip. A node with one tree peer, which has periodic parent re-evaluation disabled, was left self-rooted and unreachable if its one-shot attaching TreeAnnounce was lost, until the next periodic re-broadcast. Tree-position exchange is now self-healing on the receive path: a node that hears an announce advertising a strictly worse root echoes its own declaration back, provoking the better-rooted peer to re-push its real position immediately.
  • macOS self-connections work end to end (#117). Traffic a macOS node sends to its own <npub>.fips address is now delivered locally for full TCP/UDP, not just ping6. The point-to-point utun egresses self-addressed packets into the daemon with an unfinished transport checksum (macOS offloads it on the lo0 loopback route), so re-injecting them verbatim made the local stack drop every segment the MSS-clamp rewrite did not happen to fix and self-connections half-opened and hung. The hairpin path now recomputes the TCP/UDP checksum before re-injection. Linux was unaffected.

Upgrade notes

Operator-actionable items moving from v0.3.0 to v0.4.0:

  • Wire-compatible, no flag day. v0.4.0 peers with v0.3.0. Upgrade nodes in any order. During a rolling upgrade you may see some log lines on the upgraded side as it interacts with not-yet-upgraded peers; behavior is correct, log noise only.
  • Bloom antipoison cap default changed. node.bloom.max_inbound_fpr now defaults to 0.10 (was 0.05). If you set this explicitly, review whether you still want the old value.
  • New optional config surfaces. transports.nym (outbound Nym mixnet) and node.discovery.lan (mDNS LAN discovery) are both opt-in and off by default. Adding them is the only way to turn the new paths on.
  • TCP inbound cap. If you relied on the old hardwired 256 inbound-TCP ceiling, note it now honors max_inbound_connections then node.limits.max_connections then 256.
  • New observability query. fipsctl stats metrics (the show_metrics control query) returns a counter-only snapshot suitable for a scraper.

Getting v0.4.0

  • Linux x86_64 / aarch64: .deb and tarball at the v0.4.0 release page.
  • Arch Linux: fips from the AUR.
  • macOS: .pkg at the v0.4.0 release page.
  • Windows: ZIP at the v0.4.0 release page.
  • OpenWrt: .ipk (OpenWrt 24.x and earlier) or .apk (OpenWrt 25+) at the v0.4.0 release page.
  • From source: cargo build --release from a checkout of the v0.4.0 tag (Rust 1.94.1 per rust-toolchain.toml; libclang-dev is a required Linux build prerequisite).
  • Nix / NixOS: nix build .#fips from a checkout of the v0.4.0 tag builds the binaries from source with the pinned toolchain and no manual prerequisites (see the Nix section of packaging/README.md).

The full per-commit changelog lives in CHANGELOG.md. Issues and discussion at github.com/jmcorgan/fips.

Contributors

Thanks to everyone who contributed code, packaging work, bug reports, or reviews to this release.

  • @jcorgan: release shepherd, high-level design, control read plane, rekey hardening, admission, bug fixes, testing, packaging, PR coordination, and issue resolution.
  • @mmalmi: opt-in mDNS LAN discovery and data-plane performance work.
  • @Origami74: macOS packaging and website coordination.
  • @dskvr: AUR packaging.
  • @oleksky: Nym mixnet transport and the single-container mixnet demo.