Files
fips/docs/design/fips-session-layer.md
T
Johnathan Corgan 04d9fd625d FSP wire format revision and session-layer MMP implementation
FSP wire format revision (TASK-2026-0007):

Introduce the FIPS Session Protocol (FSP) wire format with a 4-byte
common prefix [ver_phase:1][flags:1][payload_len:2 LE] replacing the
old 1-byte msg_type dispatch. All session messages share this prefix
with phase-based dispatch (Established, Setup, Ack, Unencrypted).

- New session_wire.rs: FSP constants, header types, parse/build helpers
- SessionMessageType enum: DataPacket (0x10), SenderReport (0x11),
  ReceiverReport (0x12), PathMtuNotification (0x13)
- FspFlags (CP/K/U) and FspInnerFlags (SP) for flag management
- SessionSenderReport, SessionReceiverReport, PathMtuNotification
  message structs with encode/decode
- FSP send pipeline: 12-byte header as AAD, 6-byte inner header
  (timestamp + msg_type + inner_flags), encrypt_with_aad()
- FSP receive pipeline: parse header, extract cleartext coords (CP),
  AEAD decrypt with AAD, strip inner header, msg_type dispatch
- Forwarding: transit nodes parse cleartext coords without decryption
- Removed DataPacket struct and associated types
- SessionEntry: session_start_ms, mark_established(), session_timestamp()
- FIPS_OVERHEAD: 144 → 150 bytes (+6 for FSP inner header)
- Design docs updated for new wire format

Session-layer MMP implementation (TASK-2026-0008):

Implement complete session-layer MMP reusing the link-layer algorithm
modules (SenderState, ReceiverState, MmpMetrics, SpinBitState) with
independent configuration and higher report interval clamps.

- SessionMmpConfig: separate config section (node.session_mmp.*)
- MmpSessionState: session-specific wrapper with PathMtuState tracking
- Session-layer constants (500ms-10s report intervals, 1s cold start)
- Parameterized interval methods (new_with_cold_start,
  update_report_interval_with_bounds) on SenderState/ReceiverState
- Bidirectional From conversions between link/session report types
- SessionEntry: mmp and is_initiator fields, initialized on Established
- send_session_msg() for reports/notifications
- Per-message RX recording with spin bit state tracking
- Handlers for SenderReport, ReceiverReport, PathMtuNotification
- path_mtu threaded from SessionDatagram envelope through to handlers
- check_session_mmp_reports() tick handler with collect-then-send pattern
- Periodic and teardown operator logging for session metrics
- PathMtuState: destination observes incoming MTU on all session messages,
  source seeded from outbound transport MTU, decrease-immediate /
  increase-requires-3-consecutive rules

Link-layer MMP fix:

- Stop feeding spin bit RTT samples into SRTT estimator; inter-frame
  timing in the mesh is irregular, inflating spin-bit RTT by variable
  processing delays; timestamp-echo provides accurate RTT

29 files changed, 602 tests pass, 0 clippy warnings.
2026-02-19 03:15:05 +00:00

408 lines
17 KiB
Markdown

# FIPS Session Protocol (FSP)
The FIPS Session Protocol is the top protocol layer in the FIPS stack. It sits
above the FIPS Link Protocol (FLP) 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 FLP. 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](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 FLP
FSP treats FLP 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
FLP 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
FLP signals routing failures asynchronously:
- **CoordsRequired**: A transit node lacks the destination's tree coordinates.
FSP responds by re-initiating discovery and resetting the coordinate warmup
strategy.
- **PathBroken**: Greedy routing reached a dead end. FSP responds by
re-discovering the destination's current coordinates and resetting warmup.
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, FLP 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:
1. **Initiator** sends SessionSetup containing Noise IK msg1 and both
parties' tree coordinates
2. **Responder** processes msg1, learns initiator identity, sends SessionAck
containing Noise IK msg2 and its own coordinates
3. 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 the responder's coordinates back along the reverse path,
warming caches in the other direction.
### 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.
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 FLP 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.
## CP (Coords Present) 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 warmup-then-reactive strategy to keep transit caches populated:
### Warmup Phase
After session establishment, the first N data packets (configurable, default 5)
include both 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.
### 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 Recovery
When FSP receives a CoordsRequired signal:
1. The warmup counter resets — subsequent data packets include coordinates again
2. A new LookupRequest may be initiated to rediscover the destination's
current coordinates
3. 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)
When FSP receives a PathBroken signal:
1. A LookupRequest is initiated to discover the destination's current
coordinates (which may have changed due to topology change)
2. The warmup counter resets
Both signals are rate-limited at transit nodes (100ms per destination) to
prevent storms during topology changes.
### Warmup State Machine
```text
┌──────────────┐
│ WARMUP │ ◄── Send first N packets with coords
└──────┬───────┘
│ N packets sent without CoordsRequired
┌──────────────┐
│ MINIMAL │ ◄── Send packets without coords
└──────┬───────┘
│ CoordsRequired or PathBroken received
┌──────────────┐
│ WARMUP │ ◄── Counter reset, send 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.fips` derives
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, FLP 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.
## Implementation Status
| Feature | Status |
| ------- | ------ |
| Session establishment (Noise IK) | **Implemented** |
| End-to-end encryption (ChaCha20-Poly1305) | **Implemented** |
| Explicit counter replay protection | **Implemented** |
| CP warmup-then-reactive | **Implemented** |
| FSP wire format (prefix, AAD, inner header) | **Implemented** |
| Session-layer MMP report types | **Implemented** (wire format) |
| 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 (FLP SessionDatagram field) | **Implemented** |
| Path MTU notification (end-to-end echo) | Planned |
## References
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture
- [fips-link-layer.md](fips-link-layer.md) — FLP specification (below FSP)
- [fips-ipv6-adapter.md](fips-ipv6-adapter.md) — IPv6 adaptation layer (above FSP)
- [fips-mesh-operation.md](fips-mesh-operation.md) — Routing, discovery, and
error recovery
- [fips-wire-formats.md](fips-wire-formats.md) — Wire format reference for all
session message types