diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b70236..c1c8e30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,60 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +## [0.4.1] - 2026-07-19 + +### Changed + +- `node.bloom.max_inbound_fpr` default raised from `0.10` to `0.20`. The + cap rejects inbound `FilterAnnounce` whose FPR (`fill^k`) exceeds it. On + the fixed 1 KB / k=5 filter, `0.10` corresponds to fill 0.631 (~1,630 + reachable entries), and the busiest nodes' aggregates had again begun to + reach it as the mesh grew. `0.20` (fill 0.7248, ~2,114 entries) restores + headroom without materially weakening the antipoison gate: a saturated or + poisoned filter is ~100% FPR and still rejected. This is the second raise + of this cap in two releases; the fixed 1 KB filter is the underlying + constraint, and the structural remedy is the v2 filter work rather than a + further raise. A node running this default accepts announcements that a + v0.4.0 node drops, so during a rolling upgrade the two versions can + disagree about mesh size. +- Bloom filter probing computes its SHA-256 digest once per operation + rather than once per hash function. All k indices were already derived + from a single digest, but the digest was recomputed inside the + per-function loop, so every insert and membership test hashed the same + bytes `hash_count` times (5x at the default). Output is bit-for-bit + identical; this is the hottest path in packet forwarding and mesh-size + estimation. +- Identity operations reuse one shared `secp256k1` context instead of + constructing a fresh one at every sign, verify, and key-derive site. + Each construction allocated a context and ran randomization and blinding + table setup. Behavior is unchanged: the same API calls are made, only the + context lifetime differs, and the shared context still performs the + standard construction-time blinding. + +### Fixed + +- Spanning tree: the coordinate cache is now invalidated when the parent + link is lost through peer removal. That path reparents or self-roots the + node but omitted the invalidation every other position-change path + performs, so cached entries for downstream destinations kept the node's + now-stale coordinate prefix. Because routing access refreshes an entry's + TTL, an actively routed stale entry never self-expired and was corrected + only by a fresh insert. +- Discovery: applying a `LookupResponse` now keeps the tighter of the + cached and received `path_mtu` rather than overwriting unconditionally. + A looser estimate arriving in a later response could clobber a tighter + value already learned from a reactive `MtuExceeded` or + `PathMtuNotification`, loosening a clamp that had been correctly + tightened. + +### Removed + +- The `parent_switched` spanning-tree metric counter. It was incremented on + the line immediately before `parent_switches` at every site and never + independently, so the two were always identical. `parent_switches` + remains as the sole counter. Consumers reading `parent_switched` from the + control socket or `fipstop` should use `parent_switches`. + ## [0.4.0] - 2026-06-27 ### Added diff --git a/README.md b/README.md index 0200581..eaef88c 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ![banner](docs/logos/fips_banner.png) [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) [![Rust](https://img.shields.io/badge/rust-1.85%2B-orange.svg)](https://www.rust-lang.org/) -[![Status](https://img.shields.io/badge/status-v0.4.1--dev-green.svg)](#status--roadmap) +[![Status](https://img.shields.io/badge/status-v0.4.1-green.svg)](#status--roadmap) A self-organizing encrypted mesh network built on Nostr identities, capable of operating over arbitrary transports without central @@ -210,8 +210,8 @@ testing/ Docker-based integration test harnesses + chaos simulation ## Status & roadmap -FIPS is at **v0.4.1-dev** on the `maint` branch. -[v0.4.0](https://github.com/jmcorgan/fips/releases/tag/v0.4.0) has +FIPS is at **v0.4.1** on the `maint` branch. +[v0.4.1](https://github.com/jmcorgan/fips/releases/tag/v0.4.1) has shipped; this line carries patch-level fixes for the 0.4.x series. The core protocol works end-to-end over UDP, TCP, Ethernet, Tor, Nym, and Bluetooth on a global, public test diff --git a/RELEASE-NOTES.md b/RELEASE-NOTES.md index c7e1365..6bab562 100644 --- a/RELEASE-NOTES.md +++ b/RELEASE-NOTES.md @@ -1,302 +1,134 @@ -# FIPS v0.4.0 +# FIPS v0.4.1 -**Released**: 2026-06-21 (provisional) +**Released**: 2026-07-19 -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.1 is a maintenance release on the v0.4.x line. It raises the default +antipoison cap on inbound bloom filter announcements, removes a redundant +spanning-tree metric counter, fixes two convergence and path-MTU bugs, and +cuts per-packet CPU in the bloom and identity paths. There is no wire +format change and no new feature surface. -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. +v0.4.1 is wire-compatible with v0.4.0. Nodes can be upgraded one at a time +with no coordinated restart, though one behavior change below is worth +reading before you start a rolling upgrade. ## 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](https://nymtech.net/) 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. +- `node.bloom.max_inbound_fpr` default moves from `0.10` to `0.20`. +- The `parent_switched` metric counter is gone. Use `parent_switches`. +- Spanning tree no longer serves stale coordinates after a parent link is + lost through peer removal. +- Discovery no longer loosens a path MTU clamp it had correctly tightened. +- Bloom probing and identity operations do measurably less work per call, + with identical results. ## Behavior changes worth flagging -These affect operators on upgrade. +### The inbound filter FPR cap default doubles again -- **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. +`node.bloom.max_inbound_fpr` goes from `0.10` to `0.20`. The cap rejects +inbound `FilterAnnounce` frames whose advertised false positive rate +exceeds it. On the fixed 1 KB, k=5 filter, `0.10` corresponds to a fill of +0.631 and roughly 1,630 reachable entries, and the busiest nodes' +aggregates had started reaching that ceiling as the mesh grew. `0.20` +corresponds to a fill of 0.7248 and roughly 2,114 entries. + +Be aware that this is the second time in two releases that this default +has doubled, for the same reason both times. That is worth stating plainly +rather than repeating the previous release's framing: raising the cap buys +headroom, it does not fix anything. The real constraint is the fixed 1 KB +filter size, which is a protocol constant. The structural remedy is the v2 +filter work, where filter capacity scales with the mesh instead of being +pinned. This release is an interim step to keep legitimate aggregates from +being rejected until that lands. It is not the start of a pattern of +raising the cap once per release, and if you are sizing capacity planning +around this number, plan against the v2 work rather than against a third +raise. + +The antipoison property the cap exists for is preserved. A saturated or +deliberately poisoned filter still presents an FPR near 100% and is still +rejected. + +**This matters during a rolling upgrade.** A v0.4.1 node accepts a +`FilterAnnounce` with a derived FPR between 0.10 and 0.20; a v0.4.0 node +drops the same frame, and the drop is silent on the wire with no NACK. The +cap also gates the mesh size estimator, which declines to produce a value +when any contributing filter is over the cap. So while a mesh is partly +upgraded, upgraded and not-yet-upgraded nodes can legitimately report +different mesh sizes, or one can report a size while the other reports +unknown. This resolves once every node is on v0.4.1. If you want to avoid +the window entirely, set `node.bloom.max_inbound_fpr: 0.10` explicitly in +your config before upgrading and remove it after the last node is done. + +### The `parent_switched` counter is removed + +`parent_switched` was incremented on the line immediately before +`parent_switches` at every site and never independently, so the two +counters always held the same value. `parent_switched` is now gone from +the tree metrics, the control socket snapshot, and the `fipstop` tree +view. `parent_switches` remains and is unchanged. + +If you scrape the control socket, or have dashboards or alerts referencing +`parent_switched`, point them at `parent_switches`. Anything still asking +for `parent_switched` will find nothing rather than a zero. ## 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. +### Stale coordinates after losing a parent through peer removal -- **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 `.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. +When a node's parent link dropped via peer removal, the node correctly +reparented or self-rooted, but skipped the coordinate cache invalidation +that every other position-change path performs. Cached entries for +downstream destinations kept the node's old coordinate prefix. This did +not self-correct the way a stale cache entry normally would: routing +access refreshes an entry's TTL, so an entry that was actively being +routed through never expired, and was only fixed by an unrelated fresh +insert. Both invalidation classes now run on this path, matching the +loop-detection branch. + +### Discovery could loosen a tightened path MTU clamp + +An originator handling a `LookupResponse` overwrote its cached path MTU +unconditionally. If a reactive `MtuExceeded` or `PathMtuNotification` had +already taught it a tighter value, a later, looser discovery estimate +would clobber that and re-loosen the clamp, risking a return to dropped +oversized packets. The cached and received values are now compared and the +tighter one is kept. ## Upgrade notes -Operator-actionable items moving from v0.3.0 to v0.4.0: +This is a drop-in upgrade from v0.4.0 with no wire format change, no +config migration, and no coordinated restart. Upgrade nodes in whatever +order you like. -- **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. +Two things to do rather than assume: -## Getting v0.4.0 +1. If you monitor `parent_switched`, move to `parent_switches` before + upgrading, or your dashboards will go blank rather than error. +2. During the rolling window, expect upgraded and not-yet-upgraded nodes + to potentially disagree about mesh size, per the FPR cap section above. + This is expected and self-resolves. Do not chase it as a bug unless it + persists after every node reports `0.4.1`. + +If you have pinned `node.bloom.max_inbound_fpr` explicitly in your config, +your setting is honored and nothing changes for you. The change only +affects nodes taking the default. + +Downgrading to v0.4.0 is supported and needs no special handling. + +## Getting v0.4.1 - **Linux x86_64 / aarch64**: `.deb` and tarball at the - [v0.4.0 release page](https://github.com/jmcorgan/fips/releases/tag/v0.4.0). + [v0.4.1 release page](https://github.com/jmcorgan/fips/releases/tag/v0.4.1). - **Arch Linux**: `fips` from the AUR. -- **macOS**: `.pkg` at the v0.4.0 release page. -- **Windows**: ZIP at the v0.4.0 release page. +- **macOS**: `.pkg` at the v0.4.1 release page. +- **Windows**: ZIP at the v0.4.1 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 + at the v0.4.1 release page. +- **From source**: `cargo build --release` from a checkout of the v0.4.1 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 +- **Nix / NixOS**: `nix build .#fips` from a checkout of the v0.4.1 tag builds the binaries from source with the pinned toolchain and no manual prerequisites (see the Nix section of `packaging/README.md`). @@ -309,13 +141,6 @@ The full per-commit changelog lives in Thanks to everyone who contributed code, packaging work, bug reports, or reviews to this release. -- [@jcorgan](https://github.com/jmcorgan): release shepherd, high-level - design, control read plane, rekey hardening, admission, bug fixes, - testing, packaging, PR coordination, and issue resolution. -- [@mmalmi](https://github.com/mmalmi): opt-in mDNS LAN discovery and - data-plane performance work. -- [@Origami74](https://github.com/Origami74): macOS packaging and - website coordination. -- [@dskvr](https://github.com/dskvr): AUR packaging. -- [@oleksky](https://github.com/oleksky): Nym mixnet transport and the - single-container mixnet demo. +- [@jcorgan](https://github.com/jmcorgan): release shepherd, spanning-tree + and discovery fixes, bloom and identity performance work, antipoison cap + change, and testing. diff --git a/docs/releases/release-notes-v0.4.1.md b/docs/releases/release-notes-v0.4.1.md new file mode 100644 index 0000000..6bab562 --- /dev/null +++ b/docs/releases/release-notes-v0.4.1.md @@ -0,0 +1,146 @@ +# FIPS v0.4.1 + +**Released**: 2026-07-19 + +v0.4.1 is a maintenance release on the v0.4.x line. It raises the default +antipoison cap on inbound bloom filter announcements, removes a redundant +spanning-tree metric counter, fixes two convergence and path-MTU bugs, and +cuts per-packet CPU in the bloom and identity paths. There is no wire +format change and no new feature surface. + +v0.4.1 is wire-compatible with v0.4.0. Nodes can be upgraded one at a time +with no coordinated restart, though one behavior change below is worth +reading before you start a rolling upgrade. + +## At a glance + +- `node.bloom.max_inbound_fpr` default moves from `0.10` to `0.20`. +- The `parent_switched` metric counter is gone. Use `parent_switches`. +- Spanning tree no longer serves stale coordinates after a parent link is + lost through peer removal. +- Discovery no longer loosens a path MTU clamp it had correctly tightened. +- Bloom probing and identity operations do measurably less work per call, + with identical results. + +## Behavior changes worth flagging + +### The inbound filter FPR cap default doubles again + +`node.bloom.max_inbound_fpr` goes from `0.10` to `0.20`. The cap rejects +inbound `FilterAnnounce` frames whose advertised false positive rate +exceeds it. On the fixed 1 KB, k=5 filter, `0.10` corresponds to a fill of +0.631 and roughly 1,630 reachable entries, and the busiest nodes' +aggregates had started reaching that ceiling as the mesh grew. `0.20` +corresponds to a fill of 0.7248 and roughly 2,114 entries. + +Be aware that this is the second time in two releases that this default +has doubled, for the same reason both times. That is worth stating plainly +rather than repeating the previous release's framing: raising the cap buys +headroom, it does not fix anything. The real constraint is the fixed 1 KB +filter size, which is a protocol constant. The structural remedy is the v2 +filter work, where filter capacity scales with the mesh instead of being +pinned. This release is an interim step to keep legitimate aggregates from +being rejected until that lands. It is not the start of a pattern of +raising the cap once per release, and if you are sizing capacity planning +around this number, plan against the v2 work rather than against a third +raise. + +The antipoison property the cap exists for is preserved. A saturated or +deliberately poisoned filter still presents an FPR near 100% and is still +rejected. + +**This matters during a rolling upgrade.** A v0.4.1 node accepts a +`FilterAnnounce` with a derived FPR between 0.10 and 0.20; a v0.4.0 node +drops the same frame, and the drop is silent on the wire with no NACK. The +cap also gates the mesh size estimator, which declines to produce a value +when any contributing filter is over the cap. So while a mesh is partly +upgraded, upgraded and not-yet-upgraded nodes can legitimately report +different mesh sizes, or one can report a size while the other reports +unknown. This resolves once every node is on v0.4.1. If you want to avoid +the window entirely, set `node.bloom.max_inbound_fpr: 0.10` explicitly in +your config before upgrading and remove it after the last node is done. + +### The `parent_switched` counter is removed + +`parent_switched` was incremented on the line immediately before +`parent_switches` at every site and never independently, so the two +counters always held the same value. `parent_switched` is now gone from +the tree metrics, the control socket snapshot, and the `fipstop` tree +view. `parent_switches` remains and is unchanged. + +If you scrape the control socket, or have dashboards or alerts referencing +`parent_switched`, point them at `parent_switches`. Anything still asking +for `parent_switched` will find nothing rather than a zero. + +## Notable bug fixes + +### Stale coordinates after losing a parent through peer removal + +When a node's parent link dropped via peer removal, the node correctly +reparented or self-rooted, but skipped the coordinate cache invalidation +that every other position-change path performs. Cached entries for +downstream destinations kept the node's old coordinate prefix. This did +not self-correct the way a stale cache entry normally would: routing +access refreshes an entry's TTL, so an entry that was actively being +routed through never expired, and was only fixed by an unrelated fresh +insert. Both invalidation classes now run on this path, matching the +loop-detection branch. + +### Discovery could loosen a tightened path MTU clamp + +An originator handling a `LookupResponse` overwrote its cached path MTU +unconditionally. If a reactive `MtuExceeded` or `PathMtuNotification` had +already taught it a tighter value, a later, looser discovery estimate +would clobber that and re-loosen the clamp, risking a return to dropped +oversized packets. The cached and received values are now compared and the +tighter one is kept. + +## Upgrade notes + +This is a drop-in upgrade from v0.4.0 with no wire format change, no +config migration, and no coordinated restart. Upgrade nodes in whatever +order you like. + +Two things to do rather than assume: + +1. If you monitor `parent_switched`, move to `parent_switches` before + upgrading, or your dashboards will go blank rather than error. +2. During the rolling window, expect upgraded and not-yet-upgraded nodes + to potentially disagree about mesh size, per the FPR cap section above. + This is expected and self-resolves. Do not chase it as a bug unless it + persists after every node reports `0.4.1`. + +If you have pinned `node.bloom.max_inbound_fpr` explicitly in your config, +your setting is honored and nothing changes for you. The change only +affects nodes taking the default. + +Downgrading to v0.4.0 is supported and needs no special handling. + +## Getting v0.4.1 + +- **Linux x86_64 / aarch64**: `.deb` and tarball at the + [v0.4.1 release page](https://github.com/jmcorgan/fips/releases/tag/v0.4.1). +- **Arch Linux**: `fips` from the AUR. +- **macOS**: `.pkg` at the v0.4.1 release page. +- **Windows**: ZIP at the v0.4.1 release page. +- **OpenWrt**: `.ipk` (OpenWrt 24.x and earlier) or `.apk` (OpenWrt 25+) + at the v0.4.1 release page. +- **From source**: `cargo build --release` from a checkout of the v0.4.1 + 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.1 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`](../../CHANGELOG.md). Issues and discussion at +[github.com/jmcorgan/fips](https://github.com/jmcorgan/fips). + +## Contributors + +Thanks to everyone who contributed code, packaging work, bug reports, or +reviews to this release. + +- [@jcorgan](https://github.com/jmcorgan): release shepherd, spanning-tree + and discovery fixes, bloom and identity performance work, antipoison cap + change, and testing.