FIPS Code Review – May 2026

This is an AI Assisted code review of FIPS following the Composite Design framework. It focuses on code quality moreso than the software’s functionality. Code quality has a direct and overbearing impact on long-term software maintainability and security. For a project as ambitious and potentially consequential as FIPS, I implore its authors to take it seriously.

What FIPS Is

FIPS (Free Internetworking Peering System) is a self-organizing encrypted mesh network built on Nostr identities. A machine running FIPS becomes a node with a self-generated secp256k1 keypair. It can reach any other FIPS node regardless of transport, whether UDP, TCP, Tor, raw Ethernet, or Bluetooth; without central infrastructure, VPN concentrators, or coordinating servers.

The system provides a TUN interface that maps each remote node’s Nostr public key to an fd00::/8 IPv6 address, so unmodified IPv6 software reaches mesh peers as <npub>.fips. Routing uses a spanning-tree coordinate system with Bloom-filter-guided discovery. Encryption runs at two independent layers: Noise IK between adjacent peers (hop-by-hop) and Noise XK between mesh endpoints (end-to-end), with periodic rekeying for forward secrecy.

FIPS is at v0.3.0-dev. The protocol and APIs are not yet stable, and the project’s own roadmap lists a cryptographic security audit as a near-term priority that has not yet occurred. This analysis applies a production-readiness lens not to render a verdict on the current release, but to identify where structural investment will matter most before the project stabilizes.

Discovery Process

Scale

The repository contains approximately 86,000 lines of code across production Rust, unit tests, and a Python/Shell integration and chaos test harness. The following is a fast, rough analysis of the breakdown among source, unit test, and integration tests across the repository. Actual counts may differ.

Category Code Lines
Production logic (src/) 54,712
Unit tests (src/) 11,622
Integration & chaos (testing/) 19,742
Total test code ~31,364
Test-to-production ratio ~1 : 1.75

Counts exclude blank lines and comments. Inline #[cfg(test)] blocks were separated from production logic using brace-depth tracking; dedicated tests.rs files were excluded entirely.

Module Structure

The production source is organized into the following top-level modules:

  • noise/ – Noise protocol implementation (IK and XK patterns)
  • tree/ – Spanning tree state and coordinate system
  • transport/ – UDP, TCP, Tor, Ethernet, BLE transports
  • peer/ – Peer connection and active peer state
  • node/ – Node orchestration (the subject of this review)
  • upper/ – TUN device, DNS, ICMP, TCP MSS clamping
  • discovery/ – Nostr overlay discovery and NAT traversal
  • identity/ – Cryptographic identity management
  • mmp/ – Mesh Measurement Protocol
  • bloom/ – Bloom filter state
  • cache/ – Coordinate cache
  • config/ – Configuration loading and validation
  • protocol/ – Wire format types

Test Patterns

Two test patterns coexist throughout the codebase:

  • Pattern A: Dedicated tests.rs files (5 files, concentrated in noise/ and tree/)
  • Pattern B: Inline #[cfg(test)] blocks within production files (77 files)

The chaos test harness in testing/ is a multi-language integration suite including Python simulation, shell scripts, and Rust integration tests that exercises the mesh under network impairment conditions (packet loss, latency, reordering) using Linux tc netem.

Composite Design Framework

Composite Design (Glenford Myers, 1975) evaluates software modules along two axes. The word “module” from back then aligns with the “function” or “method” today, but the concept is fractal and can, at a higher level, align with the common definition of a “module” today as well.

Module Strength (strongest to weakest):

  • Functional – all elements serve a single function
  • Informational – multiple functions operating on a single data structure
  • Communicational – elements share the same data set but perform distinct operations
  • Procedural – elements are grouped by execution sequence
  • Classical – elements are related in time (initialization, cleanup)
  • Logical – elements are related by category but not by data
  • Coincidental – no meaningful relationship among elements

Module Coupling (loosest to tightest):

  • Data – all arguments are primitive data elements, no shared structures
  • Stamp – both modules reference the same data structure
  • Control – one module passes flags that control another’s execution
  • External – both modules reference the same global variable
  • Common – both modules reference the same global data structure
  • Content – one module directly accesses another’s internals

The goal is the highest strength and loosest coupling achievable for each module given the problem it solves.

Strengths

noise/handshake.rs – Functional Strength / Data Coupling

This is the highest-quality module in the codebase from a structural standpoint. SymmetricState is a self-contained state machine encapsulating the chaining key, handshake hash, and cipher state. HandshakeState progresses through an enumerated finite sequence (Initial → Message1Done → Message2Done → Complete). Every public function performs exactly one protocol step. Inputs and outputs are primitive byte arrays or typed wrappers; nothing is shared globally.

A specific design decision worth noting: Nostr npubs encode x-only public keys without parity. The Noise IK pre-message must mix identical bytes on both sides of the handshake, but the initiator may only have the x-only form. The solution, which is to normalize to even parity (0x02 prefix) before hashing, documented at the call site where it matters, is an example of making a non-obvious protocol constraint visible in the code rather than hiding it in a comment elsewhere.

tree/state.rs – Informational Strength / Stamp Coupling (intentional)

The tree module manages spanning-tree coordinate state: parent selection, declaration signing, hold-down timers, flap dampening. It passes TreeCoordinate and ParentDeclaration structs across its boundaries, which is Stamp Coupling by definition. The coupling is defensible because those structs are the data the module exists to manage. There is no gratuitous sharing. The boundary between what the tree knows and what the node decides is clearly drawn: the tree module computes coordinates and evaluates parent candidates; the node module calls into it and acts on the results.

noise/session.rs and node/session.rs (EndToEndState) – Informational Strength / Stamp Coupling

The EndToEndState enum is a textbook correct state machine:

Initiating(HandshakeState)
AwaitingMsg3(HandshakeState)
Established(NoiseSession)

Transitions are explicit and enumerated. The impossible states are unrepresentable. The comment explaining why Established is not boxed (precomputed AEAD key material, avoiding double-indirection on the hot path) is honest engineering documentation, not defensive commentary. This layer is stable and auditable.

node/encrypt_worker.rs – Functional Strength / External Coupling (platform-conditional)

The encrypt worker achieves a narrow goal: receive a pre-cooked wire buffer, perform the FMP AEAD seal, issue the UDP syscall. Nothing else. That is Functional Strength. The performance reasoning behind the design is documented with measured numbers:

endpoint_send  ≈ 2170 ns/pkt
  fsp_encrypt  ≈  550 ns/pkt
  fmp_encrypt  ≈  550 ns/pkt
  udp_send     ≈  150 ns/pkt
  "other"      ≈  920 ns/pkt  (serial: mutates per-session/per-peer state)

The platform-conditional code (#[cfg(target_os = "macos")]) is extensive. The macOS path requires a fully custom ordered-sender thread is essentially a mini-scheduler inside a security daemon. This is a maintenance surface that will grow as macOS kernel behavior evolves. It is correctly isolated from the security-critical path, but it is not free.

The Integration and Chaos Test Harness

The Python simulation harness in testing/chaos/sim/ is among the best-structured code in the repository. Topology generation (topology.py) achieves Functional Strength and Data Coupling. It represents pure mathematical graph construction with no side effects. Network impairment management (netem.py) wraps Linux tc kernel state with Informational Strength, making external system state explicit rather than hidden. Scenario orchestration (scenario.py) cleanly separates test configuration from execution.

The chaos suite exercises real failure modes, such as packet loss, latency injection, link flap, partition and heal against a live multi-node mesh running in Docker. The harness reflects the same design discipline as the cryptographic primitives: clear boundaries, explicit state, and observable behavior. It represents a meaningful investment in understanding how the system actually behaves under impairment, not just how it behaves on a clean network.

The Central Problem: node/

Scale and Structure

The node/ module is the largest in the codebase by a significant margin. The Node struct has approximately 60 fields. Its implementation is spread across 19 impl blocks in 20 source files totaling over 14,000 lines, with approximately 594 methods across the handler files.

For an alpha-stage project, a large orchestration module accumulating coordination responsibilities is expected and not inherently alarming. The concern here is not the current size in isolation, but the specific structural patterns that will make decomposition progressively more expensive as the project moves toward stability. Identifying them now, while boundaries are still being negotiated, is lower-cost than identifying them after wire format stability is declared.

The handler files by size:

File Lines
handlers/session.rs 2,300
mod.rs 2,189
encrypt_worker.rs 2,408
lifecycle.rs 2,075
handlers/handshake.rs 1,165
handlers/discovery.rs 708
handlers/rekey.rs 576
handlers/mmp.rs 589
session.rs 1,265
stats_history.rs 1,218

The Node’s Responsibilities

The Node struct and its methods are currently responsible for 31 separate tasks.

  1. Cryptographic identity and startup epoch
  2. Configuration ownership and validation
  3. Peer aliases and display names
  4. Transport creation, startup, and shutdown
  5. Per-transport operational state and drop tracking
  6. Link allocation, the link table, and the reverse address-to-link map
  7. Pending transport connects for connection-oriented transports
  8. Connection retry state and backoff
  9. Noise IK handshake initiation and processing (link layer)
  10. Pre-authentication connection tracking
  11. Authenticated peer table and session index dispatch
  12. Link heartbeats and peer restart detection
  13. End-to-end XK session handshake (session layer)
  14. Session epoch management and rekeying
  15. TUN packet queuing pending session establishment
  16. FMP encrypt and decrypt worker pool ownership and dispatch
  17. Spanning tree state and parent selection driving
  18. Bloom filter aggregation and mesh size estimation
  19. Coordinate cache and path MTU tracking
  20. In-mesh discovery flooding, deduplication, and reverse-path forwarding
  21. Nostr overlay discovery, NAT traversal, and bootstrap transport management
  22. Rate limiting (handshake, routing errors, discovery, ICMP)
  23. Peer ACL loading and hot-reloading
  24. MMP report driving for link peers and sessions
  25. TUN device creation, thread spawning, and shutdown
  26. ICMPv6 generation (Destination Unreachable, Packet Too Big)
  27. DNS responder socket binding and task spawning
  28. Identity cache population from DNS resolution
  29. Statistics counters and time-series history
  30. Control socket query and command dispatch
  31. The tokio::select! event loop and periodic tick orchestration

A module with Informational Strength performs multiple functions that all operate on a single, coherent data structure. What Node holds is not a single data structure; it is every data structure in the system. The module strength is Coincidental: the fields and methods are grouped together because they all touch the same self, not because they are conceptually unified.

The Coupling Consequence

Every handler file in node/handlers/ takes &mut self: the entire Node. This is Common Coupling: all handlers share access to the same global data structure. The practical consequence is that a change to any field’s semantics requires reading every handler to determine whether it is affected. There is no boundary that limits the blast radius of a modification. Every change directly affects every other line of code defining the Node because of this coupling.

This manifests concretely in two places.

The dual cross-init invariant. When both sides of a NAT traversal complete simultaneously, a tie-breaking rule determines which node tears down its in-flight handshake and adopts the fresh socket. The rule (“smaller NodeAddr wins as adopter”) is the correct decision. But it lives as a conditional block in lifecycle.rs:poll_nostr_discovery, is not referenced from the handshake handler that processes the same peer’s msg1, and nothing in the type system prevents a future contributor from adding a new connection path that omits the check. The invariant is procedurally enforced, not structurally enforced.

The session-to-worker wire contract. The pipelined fast path in handlers/session.rs constructs a wire_buf with a specific layout ([16-byte FMP header][inner plaintext][TAG_SIZE capacity]) that the encrypt worker depends on by array offset arithmetic. The contract is documented in comments, but it is not expressed in a type. Any change to the wire layout requires coordinating both files manually.

The Constructor Duplication Signal

Node::new and Node::with_identity are two constructors that are structurally identical across approximately 80 lines each, diverging in only two places. When a struct requires two nearly-identical 80-line constructors to initialize correctly, the initialization contract has grown beyond what a single function can hold cleanly. This is a concrete signal that the struct is doing too many things.

What the Test Suite Can and Cannot Do

The integration and chaos harness described in the Strengths section is doing real work precisely because node/ lacks internal structure. The tests are compensating for the absence of structural invariants. They detect known-bad interactions that the module boundaries do not prevent. This is a reasonable approach for an alpha, but it has a ceiling.

The specific risk is interaction density. Both Nostr discovery backoff and session rekey timing are fields on the same struct, accessible from the same &mut self. A contributor modifying one has no structural indication that the other exists or matters. The chaos suite will catch cases that someone thought to write a scenario for. It will not catch interactions that nobody anticipated, which is precisely where security-relevant bugs tend to appear as systems mature. As field count grows beyond 60 and handler count beyond 594, the cognitive load required to hold all implicit invariants simultaneously will exceed what an individual reviewer or even an LLM can manage during a pre-merge audit.

The Structural Divergence

The most telling observation in the codebase is the contrast between the modules written against an external specification and the module that has no equivalent.

noise/handshake.rs was written against the Noise Protocol Framework specification. The spec dictated the boundaries, the state transitions, and the invariants. It is stable, auditable, and replaceable in isolation.

node/ has no external specification. It is the place where all hard coordination problems land, a collection of the things that do not fit cleanly into any other module. Every transport negotiation edge case, every NAT traversal failure mode, every rate limiter, every new observability hook ends up here because there is no other home for it. The module grows not by design but by gravity.

The result is a codebase where the security-critical primitives are excellently structured and the orchestration layer that wires them together is structurally opaque. For a project whose threat model may include hostile actors, that asymmetry is the primary structural debt to retire before stability is claimed. The domain of a hostile attacker isn’t in the well-structured building blocks of the system, it is in the orchestration layer. Extract more building blocks, and the attack surface shrinks.

The most valuable investment ahead of a v1.0 stability declaration would be decomposing node/ into bounded subsystems, each with an explicit interface contract. That work does not require touching the cryptographic primitives, which are already in good shape. It requires drawing boundaries where none currently exist.

Summary

Module Strength Coupling Notes
noise/handshake.rs Functional Data Freeze candidate; auditable today
tree/state.rs Informational Stamp Stable; coupling is justified
noise/session.rs / EndToEndState Informational Stamp Stable; state machine is correct
encrypt_worker.rs Functional External (platform) Isolated; macOS path is a slow-growth surface
handlers/rx_loop.rs Communicational Common Moderate complexity
handlers/session.rs Communicational Common Will grow with protocol development
lifecycle.rs Procedural Common Sequential init; expected at this stage
node/mod.rs (Node struct) Coincidental Common Primary structural debt; decompose before v1.0

The cryptographic and routing foundations are well-bounded and reflect serious design discipline. The chaos test harness reflects the same discipline applied to operational verification. The orchestration layer, that is, everything that touches Node, is where the project’s structural debt is concentrated, and where decomposition effort will have the highest return before the system is declared stable.

Write a comment
No comments yet.