Rename FIPS Link Protocol (FLP) to FIPS Mesh Protocol (FMP)
The "Link Protocol" name understated the layer's scope — spanning tree
construction, bloom filter routing, greedy forwarding, and mesh-wide
coordination go well beyond link-level concerns. Rename fips-link-layer.md
to fips-mesh-layer.md, update FLP→FMP throughout docs and source code
(FLP_VERSION→FMP_VERSION, wire.rs, rx_loop.rs, spanning_tree.rs).
New SVG illustrations
- Protocol stack: color-coded layer diagram replacing ASCII art
- OSI mapping: side-by-side comparison with traditional networking layers
- Bloom filter propagation: 6-node tree with sender-colored filter boxes
showing split-horizon computation per link
- Routing decision flowchart: 5-step priority chain with candidate ranking
by tree distance and link performance
- Coordinate discovery: sequence diagram showing LookupRequest propagation,
response caching, and SessionSetup cache warming
Redesigned existing SVGs
- Architecture overview: uniform node layout, U-shaped encrypted link
connectors, separate end-to-end session line
- Node architecture: split Router Core into FSP and FMP layers, reorganize
transports into Overlay/Shared Medium/Point-to-Point categories
- Identity derivation: wider boxes, visible encode arrow, dashed npub line
fips-intro.md revisions
- Add inline references to prior work: Yggdrasil/Ironwood for coordinate
routing, Noise Protocol Framework for IK handshakes, WireGuard for
index-based session dispatch, Wikipedia for bloom filters, split-horizon,
and greedy embedding
- Add explanatory paragraphs after bloom filter diagram describing
split-horizon filter computation and candidate selection behavior
- Simplify transport abstraction language, remove I2P/LoRa references
- Fix LookupRequest wording ("propagates" not "floods"), note intermediate
node coordinate caching on lookup responses
- Rewrite architecture overview prose to match redesigned diagrams
22 KiB
FIPS Session Protocol (FSP)
The FIPS Session Protocol is the top protocol layer in the FIPS stack. It sits above the FIPS Mesh Protocol (FMP) and below applications (native FIPS API or IPv6 adapter). FSP provides end-to-end authenticated, encrypted datagram delivery between any two FIPS nodes, regardless of how many intermediate hops separate them.
Role
FSP manages end-to-end communication sessions between FIPS nodes identified by their public keys (npubs). Each session provides:
- End-to-end encryption: Payload confidentiality independent of how many intermediate nodes handle the traffic
- Mutual authentication: Both parties prove they control the private key for their claimed identity
- Replay protection: Counter-based nonces with sliding window, tolerant of UDP packet loss and reordering
- Transport independence: Sessions survive transport changes, route changes, and address changes — they are bound to npub identities, not to transport paths
FSP is a datagram session protocol. It provides encrypted datagrams, not reliable streams. There is no FIPS equivalent of TCP; if applications need reliability, ordering, or flow control, they provide it themselves (typically by running TCP over the FIPS IPv6 adapter).
Services Provided to Applications
Applications access the FIPS mesh through two interfaces, both served by FSP:
Native FIPS API
Applications address destinations directly by npub or public key. The FIPS stack resolves the destination's node_addr, establishes or reuses a session, encrypts the payload, and routes through FMP. No DNS involvement.
IPv6 Adapter
Unmodified IPv6 applications use a TUN device with fd::/8 routing. A local
DNS service maps npub → IPv6 address and primes the identity cache. Packets
arriving at the TUN are translated to FIPS datagrams and routed through FSP.
See fips-ipv6-adapter.md for the IPv6 adaptation layer.
What Applications Get
- Authenticated datagram delivery: Each datagram is encrypted and authenticated with session keys bound to both parties' npubs
- Session transparency: Sessions are established on demand and maintained automatically. Applications send packets; FSP handles session setup, encryption, and teardown.
- Endpoint identity: Applications address destinations by npub. The FIPS address is the public key.
What Applications Do Not Get
- Reliability: Datagrams may be lost, duplicated, or delivered out of order. FSP provides no retransmission or ordering.
- Path MTU discovery: FSP does not signal MTU to applications. The IPv6 adapter handles MTU enforcement via ICMP Packet Too Big and TCP MSS clamping.
- Congestion control: FSP does not throttle traffic. Applications running TCP over IPv6 get TCP's congestion control; native API applications must manage their own sending rate.
Services Required from FMP
FSP treats FMP as a black box providing three services. FSP knows nothing about transports, transport addresses, links, peers, spanning trees, coordinates, bloom filters, hop counts, or network topology.
SessionDatagram Forwarding
FMP accepts a SessionDatagram (source node_addr, destination node_addr, TTL, path MTU, payload) and delivers it best-effort toward the destination. Delivery may traverse multiple hops, each with independent link encryption.
Error Signaling
FMP signals routing failures asynchronously:
- CoordsRequired: A transit node lacks the destination's tree coordinates. FSP responds by sending a standalone CoordsWarmup (0x14) message (rate-limited), re-initiating discovery, and resetting the coordinate warmup counter.
- PathBroken: Greedy routing reached a dead end. FSP responds by sending a standalone CoordsWarmup (rate-limited), re-discovering the destination's current coordinates, and resetting the warmup counter.
Both signals are generated by transit nodes (not the destination) and travel back to the source inside a new SessionDatagram. They are plaintext (not end-to-end encrypted) because transit nodes have no session with the source.
Local Delivery
When a SessionDatagram arrives with a destination node_addr matching the local node, FMP delivers it to FSP for session-layer processing.
Session Lifecycle
Session Establishment
Sessions are established on demand when the first datagram needs to be sent to a destination with no existing session.
FSP uses Noise IK for session key agreement. The initiator knows the destination's npub (from DNS lookup or native API); the responder learns the initiator's identity from the handshake. This is the same asymmetry as link-layer peer connections.
The handshake is carried in SessionSetup and SessionAck messages:
- Initiator sends SessionSetup containing Noise IK msg1 and both parties' tree coordinates
- Responder processes msg1, learns initiator identity, sends SessionAck containing Noise IK msg2 and both parties' tree coordinates
- Both parties derive identical symmetric session keys
Packets that trigger session establishment are queued (with bounded buffer) and transmitted after the session is established.
Self-Bootstrapping
SessionSetup is self-bootstrapping for routing. It carries the source's and destination's tree coordinates in the clear (not inside the Noise payload). As the message transits intermediate nodes, each node caches these coordinates, warming the path for subsequent data packets that carry only addresses (no coordinates).
SessionAck carries both the responder's and initiator's coordinates back along the reverse path, warming caches in the other direction. This ensures return-path transit nodes can route even when the reverse path diverges from the forward path (e.g., after tree reconvergence).
Simultaneous Initiation
When both nodes attempt to establish a session simultaneously ("crossing hellos"), a deterministic tie-breaker resolves the conflict:
- If
local_node_addr < remote_node_addr: Continue as initiator, ignore incoming setup - If
local_node_addr > remote_node_addr: Abort own initiation, switch to responder role
This ensures exactly one handshake completes.
Data Transfer
Once established, sessions carry encrypted data using the FSP pipeline. Each encrypted message includes:
- A 12-byte cleartext header (used as AEAD AAD) containing the counter and flags (including the CP flag for coordinate cache warming)
- Optional cleartext coordinates when the CP flag is set
- An AEAD-encrypted payload containing a 6-byte inner header (session-relative timestamp, message type, inner flags) followed by the application data
Session Idle Timeout
Sessions that see no traffic for a configurable duration (default 90s) are torn down. When traffic resumes, a new session is established automatically.
Only DataPacket (message type 0x10) send/receive and session establishment
reset last_activity. MMP traffic — SenderReport, ReceiverReport, and
PathMtuNotification — does not reset the idle timer. This means a session
carrying only MMP reports and no application data will still tear down after
node.session.idle_timeout_secs. MMP reports continue flowing until the
session is torn down, providing final measurement data for the teardown log.
The idle timeout is deliberately shorter than the coordinate cache TTL (300s). This ordering ensures that when traffic stops and the session tears down, the transit node coordinate caches are still warm when a new session is established. The fresh SessionSetup re-warms the caches, maintaining routing continuity.
Session Independence from Transport
Sessions exist above the routing layer and are bound to npub identities, not transport addresses or routing paths. A session survives:
- Transport failover (UDP → Ethernet → back to UDP)
- Route changes (different intermediate hops)
- Transport address changes (IP address or port changes)
- Topology changes (direct peer becomes multi-hop or vice versa)
End-to-End Encryption
Noise IK Pattern
FSP uses the same Noise IK pattern as FMP link encryption, but with
independent keys and sessions. The full Noise descriptor is
Noise_IK_secp256k1_ChaChaPoly_SHA256.
The IK pattern:
- msg1 (
→ e, es, s, ss): Initiator sends ephemeral key, encrypts static key to responder. Four DH operations establish session keys. - msg2 (
← e, ee, se): Responder sends ephemeral key. Both parties now share identical session keys.
After the handshake, Noise produces two directional symmetric keys
(send_key, recv_key) used with ChaCha20-Poly1305 for all subsequent data.
Cryptographic Primitives
| Component | Choice | Notes |
|---|---|---|
| Curve | secp256k1 | Nostr-native |
| DH | ECDH on secp256k1 | Standard EC Diffie-Hellman |
| Cipher | ChaCha20-Poly1305 | AEAD, same as NIP-44 |
| Hash | SHA-256 | Nostr-native |
| Key derivation | HKDF-SHA256 | Standard Noise KDF |
These choices prioritize compatibility with the Nostr cryptographic stack.
secp256k1 Parity Normalization
Nostr npubs encode x-only public keys (32 bytes, no y-coordinate parity). The Noise IK pre-message mixes the responder's static key as a 33-byte compressed key, and the default secp256k1 ECDH hash includes a parity-dependent version byte.
Both operations are normalized to be parity-independent: the pre-message hash
uses even parity (0x02 prefix), and ECDH hashes only the x-coordinate of the
result point. This ensures handshakes succeed regardless of the responder's
actual key parity.
Privacy Note
Noise IK does not provide initiator anonymity if the responder's static key is compromised. An attacker who obtains the responder's nsec can decrypt the initiator's identity from captured handshake messages. Noise XK would protect initiator identity in this scenario but requires an additional round-trip (3 handshake messages vs. 2). The privacy/latency tradeoff may be revisited with deployment experience.
Data Packet Authentication
FSP uses AEAD authentication only — no per-packet signatures. The Noise handshake binds session keys to both parties' static keys, so only holders of the corresponding nsecs can derive the session keys. This provides implicit authentication for every packet, matching WireGuard and Lightning's approach.
Forward Secrecy
Ephemeral keys in the Noise handshake provide forward secrecy. Compromise of
static keys (nsec) does not reveal past session keys, because session keys are
derived in part from ephemeral-ephemeral DH (ee), and ephemeral keys are
discarded after the handshake.
Replay Protection
FSP uses explicit 8-byte counters on the wire for replay protection. Each side maintains a monotonically increasing send counter, included in the 12-byte cleartext header of every encrypted message. The receiver maintains a sliding window (2048-entry bitmap) tracking which counters have been seen.
This design is critical for operation over unreliable transports. Under UDP
packet loss or reordering, implicit nonce counters (where the receiver
increments on each decrypt attempt) would desynchronize permanently — a failed
decrypt() increments the nonce, and the desync grows with each lost packet.
Explicit counters allow the receiver to decrypt any packet independently,
regardless of what packets were lost or reordered.
The same ReplayWindow and decrypt_with_replay_check() implementation is
used at both the link and session layers.
Hybrid Coordinate Warmup Strategy
Session establishment (SessionSetup/SessionAck) warms transit node coordinate caches along the path. But coordinate caches have a finite TTL (default 300s), and entries may be evicted under memory pressure. When a transit node's cache entry expires, it cannot forward data packets (which carry only addresses, not coordinates) and sends a CoordsRequired error.
FSP uses a hybrid warmup strategy combining proactive piggybacking with reactive standalone messages to keep transit caches populated:
Proactive Warmup Phase
After session establishment, the first N data packets (configurable, default 5) per session attempt to piggyback source and destination coordinates via the CP flag in the FSP common prefix. The coordinates appear in cleartext between the 12-byte header and the ciphertext, allowing transit nodes to cache them without decryption.
If piggybacking coordinates would cause the total packet to exceed the transport MTU, the source sends a standalone CoordsWarmup message (0x14) first, followed by the data packet without the CP flag. This ensures transit caches are warmed even when data packets are near the MTU limit.
CoordsWarmup Message (0x14)
CoordsWarmup is an encrypted FSP message (phase 0x0) with the CP flag set and inner msg_type 0x14. It carries cleartext source and destination coordinates between the FSP header and the AEAD ciphertext, using the same format as CP-flagged data packets. The inner body is empty — the message exists solely to deliver coordinates to transit nodes.
Transit nodes extract coordinates via the existing try_warm_coord_cache()
code path with zero changes to the transit forwarding logic. CoordsWarmup is
indistinguishable from any other CP-flagged message at the transit layer.
Wire format:
FSP header (12 bytes, AAD): ver=0, phase=0, flags=CP, counter, payload_len
Cleartext coords: src_coords + dst_coords (same encoding as CP flag)
AEAD ciphertext: inner_header(6) + Poly1305 tag(16) = 22 bytes
Total FSP payload: 12 + coords + 22
Steady State
After the warmup count is reached, FSP clears the CP flag and sends minimal data packets (12-byte header + ciphertext). Transit nodes serve from their coordinate caches.
Reactive Re-Warm
When FSP receives a CoordsRequired or PathBroken signal:
- A standalone CoordsWarmup message is sent immediately to re-warm transit
caches, rate-limited at one per destination per configurable interval
(default 2000ms,
node.session.coords_response_interval_ms) - The warmup counter resets — subsequent data packets piggyback coordinates again when possible (or send additional CoordsWarmup messages when the data packet would exceed the MTU)
- A new LookupRequest may be initiated to rediscover the destination's current coordinates (always for PathBroken; optionally for CoordsRequired)
- When the LookupResponse arrives for an established session, the warmup counter resets again (handling the timing gap where warmup packets might fire before transit caches are repopulated by discovery)
The source-side rate limiting prevents amplification: at most one standalone
CoordsWarmup response per destination per coords_response_interval_ms
(default 2s). This is independent of the transit-node rate limiting on error
signal generation (100ms per destination).
Warmup State Machine
┌──────────────┐
│ WARMUP │ ◄── Send first N packets with coords (CP or CoordsWarmup)
└──────┬───────┘
│ N packets sent without CoordsRequired
▼
┌──────────────┐
│ MINIMAL │ ◄── Send packets without coords
└──────┬───────┘
│ CoordsRequired or PathBroken received
▼
┌──────────────────────────┐
│ SEND CoordsWarmup (0x14) │ ◄── Immediate standalone warmup (rate-limited)
└──────────┬───────────────┘
│
▼
┌──────────────┐
│ WARMUP │ ◄── Counter reset, piggyback coords again
└──────────────┘
Identity Cache
The identity cache maps FIPS address prefix (15 bytes, the fd::/8 IPv6
address minus the fd prefix) to (NodeAddr, PublicKey). This cache is
needed only when using the IPv6 adapter — the native FIPS API provides the
public key directly.
The mapping is deterministic (derived from the public key via SHA-256) and never becomes stale. The cache uses LRU-only eviction bounded by a configurable size (default 10K entries). There is no TTL — entries are evicted only when the cache is full and space is needed for a new entry.
Cache population mechanisms:
- DNS lookup: The primary path. Resolving
npub1xxx...xxx.fipsderives the IPv6 address and populates the identity cache. - Inbound traffic: Authenticated sessions from other nodes populate the cache with their identity information.
Coordinate Cache
The coordinate cache maps NodeAddr → TreeCoordinate and is the critical
data structure that enables efficient multi-hop routing. Without cached
coordinates for a destination, FMP cannot make forwarding decisions and must
either fall back to bloom-filter-only routing or signal CoordsRequired.
Unified Cache
The coordinate cache is a single unified cache (merged from previously separate coord_cache and route_cache). All coordinate sources — SessionSetup transit, CP-flagged data packets, LookupResponse — write to the same cache.
Eviction Policy
- TTL-based expiration: Entries expire after a configurable duration (default 300s)
- Refresh on use: Active routing through a cache entry resets its TTL, keeping hot entries alive
- LRU eviction: When the cache is full, least recently used entries are evicted first
- Flush on parent change: When the local node's tree parent changes, the entire coordinate cache is flushed. Tree parent changes mean the node's own coordinates have changed, making cached coordinates for other nodes potentially stale for routing purposes.
Timer Ordering
Cache and session timers are ordered so that idle sessions tear down before transit caches expire:
| Timer | Default | Purpose |
|---|---|---|
| Session idle timeout | 90s | Tear down unused sessions |
| Coordinate cache TTL | 300s | Expire stale coordinates |
| DNS TTL | 300s | Expire DNS resolutions |
When traffic stops: the session tears down at 90s. When traffic resumes: DNS re-resolves the identity, a fresh SessionSetup carries coordinates, and transit node caches (still within their 300s TTL) are re-warmed.
Session-Layer MMP
Each established session runs its own Metrics Measurement Protocol instance, providing end-to-end quality metrics independent of the number of hops.
Relationship to Link-Layer MMP
Session-layer MMP uses the same report wire format (SenderReport 0x11, ReceiverReport 0x12) and identical algorithms as link-layer MMP, but with two key differences:
- End-to-end routing: Session reports are encrypted and forwarded through every transit link. A 3-hop session generates report traffic on all 3 links, making bandwidth cost proportional to path length.
- Independent configuration: The
node.session_mmp.*parameters are separate fromnode.mmp.*, allowing operators to run a lighter mode for sessions (e.g., Lightweight) while keeping Full mode on links.
Metrics
The same metrics as link-layer MMP: SRTT, loss rate, jitter, goodput, OWD trend, ETX, and dual EWMA trends. Session MMP additionally tracks observed path MTU.
Report Intervals
Session-layer report intervals are higher than link-layer to account for bandwidth cost: clamped to [500ms, 10s] with a cold-start interval of 1s (vs. link-layer [100ms, 2s] with 500ms cold-start).
Path MTU Tracking
PathMtuNotification (message type 0x13) provides end-to-end path MTU feedback:
- The source sets
path_mtuin each SessionDatagram envelope to its outbound link MTU. - Each transit node applies
min(current, transport.mtu())before forwarding. - The destination receives the forward-path minimum and sends a PathMtuNotification (2-byte body: u16 LE path_mtu) back to the source.
- The source applies the notification with hysteresis:
- Decrease: immediate (take lower value).
- Increase: requires 3 consecutive higher-value notifications spanning at least 2 × notification interval.
- Notifications are sent on first measurement, on any decrease, and
periodically at
max(10s, 5 × SRTT).
Idle Timeout Interaction
MMP reports (SenderReport, ReceiverReport) and PathMtuNotification do not
reset the session idle timer. Only application data (DataPacket, type 0x10)
resets last_activity. This ensures sessions with no application traffic
tear down after node.session.idle_timeout_secs (default 90s), while MMP
continues providing measurement data up to the teardown moment.
Operator Logging
Session metrics are logged at info level (configurable via
node.session_mmp.log_interval_secs, default 30s):
MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms goodput=71.3MB/s mtu=1472 tx_pkts=1234 rx_pkts=5678
Implementation Status
| Feature | Status |
|---|---|
| Session establishment (Noise IK) | Implemented |
| End-to-end encryption (ChaCha20-Poly1305) | Implemented |
| Explicit counter replay protection | Implemented |
| Hybrid coordinate warmup (CP + CoordsWarmup) | Implemented |
| FSP wire format (prefix, AAD, inner header) | Implemented |
| Session-layer MMP | Implemented |
| Identity cache (LRU-only) | Implemented |
| Coordinate cache (unified, TTL + refresh) | Implemented |
| Session idle timeout | Implemented |
| CoordsRequired handling | Implemented |
| PathBroken handling | Implemented |
| Simultaneous initiation tie-breaker | Implemented |
| Flush coord cache on parent change | Implemented |
| Rekey | Planned |
| Path MTU tracking (FMP SessionDatagram field) | Implemented |
| Path MTU notification (end-to-end echo) | Implemented |
References
- fips-intro.md — Protocol overview and architecture
- fips-mesh-layer.md — FMP specification (below FSP)
- fips-ipv6-adapter.md — IPv6 adaptation layer (above FSP)
- fips-mesh-operation.md — Routing, discovery, and error recovery
- fips-wire-formats.md — Wire format reference for all session message types