- Add fips-wire-protocol.md: comprehensive packet dispatch design - Wire format: discriminator byte + session indices for O(1) dispatch - WireGuard-style roaming: crypto authority, not address - Security: rate limiting, replay protection, state machine strictness - Transport considerations for UDP, TCP, Tor - Rename fips-protocol-flow.md → fips-session-protocol.md - Update fips-design.md wire format section - Replace TLV with discriminator + index format - Cross-reference fips-wire-protocol.md for details - Update cross-references in all design docs
27 KiB
FIPS Protocol Traffic Flow
Design discussion from Session 39.
Overview
This document captures design considerations for FIPS protocol message flow, including peer discovery, authentication, tree announcements, and data routing.
1. Application-Initiated Traffic Flow
Traffic flow begins at the application layer with a DNS query, which triggers a cascade of events through the FIPS stack.
1.1 DNS as Entry Point
An application wants to send IPv6 traffic to another FIPS node, identified by an npub. The flow:
-
DNS Query: Application queries a local FIPS DNS server for the npub (format TBD - perhaps
npub1xxx...xxx.fipsor similar) -
FIPS DNS Server performs two functions:
- Address derivation: Converts the npub to an identity and derives the
corresponding
fd::/8IPv6 address - Cache priming: Stores the identity mapping (IPv6 address ↔ npub ↔ node_id) in the local FIPS routing cache
- Address derivation: Converts the npub to an identity and derives the
corresponding
-
DNS Response: Returns the derived IPv6 address to the application
-
Packet Transmission: Application sends IPv6 packet to the returned address, which routes to the TUN interface via the
fd::/8route -
TUN Processing: When the packet arrives at the TUN, FIPS already has the cached mapping from the DNS lookup, enabling immediate routing decisions
1.2 Design Rationale
Using DNS as the trigger point ensures the routing cache is populated before packets arrive. This avoids:
- Blocking packets while performing identity lookups
- Packet drops during cold-cache scenarios
- Complex async lookup machinery in the hot path
The DNS server acts as a "routing intent" signal - if an application queries for a destination, it likely intends to send traffic there.
1.3 DNS Name Format
NPUBs are represented as DNS names in the format:
npub1xxxxxx...xxxxx.fips
The FIPS DNS server recognizes names ending in .fips and extracts the npub
for address derivation.
1.4 Identity Cache Lifetime
The identity cache (IPv6 address ↔ npub ↔ node_id) has the following lifetime semantics:
- Configurable timeout: Cache entries expire after a configured duration
- Traffic refresh: Timer resets to zero whenever traffic is sent to that destination (LRU-style keep-alive)
- TTL relationship: Cache timeout MUST be longer than DNS TTL
The TTL constraint ensures that while an application believes its DNS resolution is valid (within TTL), the corresponding FIPS routing entry remains present. Example: DNS TTL = 300s, Cache timeout = 600s.
DNS query → cache entry created (timeout = 600s)
...traffic... → timeout reset to 600s
...traffic... → timeout reset to 600s
DNS TTL expires (300s) → app may re-query, but cache still valid
...no traffic for 600s...
Cache entry expires
1.5 Traffic Without Prior DNS Lookup
A packet may arrive at the TUN for an fd::/8 destination without a prior
DNS lookup. Scenarios:
- Application has cached/hardcoded address from previous session
- Manual configuration bypassing DNS
- Reply packets (covered separately - source already known from outbound)
Options for handling cold-cache outbound packets:
- Drop with ICMPv6: Return Destination Unreachable, require DNS lookup
- Reverse derive: Attempt to derive npub from address (not possible - address is hash of npub, not reversible)
- Query protocol: Initiate network query to discover identity for address
- Hold and query: Buffer packet while performing discovery
The address derivation is one-way (SHA-256), so reverse derivation is impossible. Without the npub, we cannot determine the node_id needed for routing.
Decision: Return ICMPv6 Destination Unreachable (Code 0: No route to destination) for packets to unknown addresses. The identity cache MUST be populated through some mechanism before traffic can be routed.
Known cache population mechanisms:
- DNS lookup (primary path, described above)
- Inbound traffic from authenticated peers (described in §X)
- Additional mechanisms TBD as design progresses
2. TUN Reader Processing
After DNS resolution, the application sends an IPv6 datagram to the destination
address. The kernel routes it to the TUN interface (via the fd::/8 route),
where the FIPS TUN reader receives it.
2.1 Packet Arrival
Application
│
▼
IPv6 datagram (src=local_addr, dst=target_addr)
│
▼
Kernel routing table: fd::/8 → fips0
│
▼
TUN reader receives raw IPv6 packet
2.2 TUN Reader Actions
On receiving a packet, the TUN reader:
-
Validate IPv6 header: Version = 6, payload length sane, etc.
-
Extract destination address: The
fd::/8address from the IPv6 header -
Identity cache lookup: Query cache for destination address
- Miss: Return ICMPv6 Destination Unreachable (see §1.5)
- Hit: Proceed with routing
-
Retrieve routing identity: Cache hit provides:
npub: The Nostr public key of the destinationnode_id: SHA-256(npub), used for spanning tree routing
-
Session lookup: Check for existing FIPS session with destination npub
- Hit: Use existing session for encryption/signing
- Miss: Initiate session establishment (see §3)
-
Route determination: Using node_id, determine next hop (covered in §X)
-
Packet forwarding: Encapsulate and send via appropriate transport
3. FIPS Sessions
A FIPS session represents a bidirectionally authenticated, encrypted channel between two FIPS nodes.
3.1 Session Properties
Each session contains:
- Peer identity: The remote node's npub and node_id
- Symmetric session keys: Directional keys for encryption (send_key, recv_key)
- Nonce counters: Per-direction counters for replay protection
Payloads within a session are:
- Encrypted with the session key (provides confidentiality)
- Authenticated via AEAD tag (session keys bound to npub identities)
Authentication derives from the Noise KK handshake binding session keys to both parties' static keys. See §6 for cryptographic details.
3.2 Session Establishment Trigger
When the TUN reader has a packet for a destination with no existing session:
TUN reader
│
├─► Identity cache lookup → node_id
│
├─► Session lookup (by npub) → MISS
│
└─► Initiate session establishment
The original packet that triggered session establishment must be handled:
Option A: Drop packet, let application retry (simple, may cause timeout)- Option B: Queue packet, send after session established
- Option C: Send packet optimistically during handshake (may fail)
Decision: Either queue or optimistic send. Dropping is not acceptable as it causes unnecessary latency and potential application timeouts.
Queuing is simpler to reason about but requires bounded buffer management. Optimistic send (0-RTT style) improves latency but requires careful replay protection.
3.3 Session Independence from Transport
FIPS sessions exist above the routing layer. A session between two npubs survives:
- Transport failover (UDP → Tor → back to UDP)
- Route changes (different intermediate hops)
- IP address changes on either end
The session is bound to npub identities, not network addresses or transport paths. This is similar to QUIC's connection migration but at the FIPS layer.
3.4 Session Establishment Flow
TBD - handshake protocol for mutual authentication and key exchange.
Considerations:
- Must work over unreliable transports (UDP)
- Must handle packet loss/reordering during handshake
- Should minimize round trips for latency
- Must bind session to both npub identities cryptographically
3.5 Simultaneous Session Initiation (Crossing Hellos)
When both nodes attempt to establish a session simultaneously, we have "crossing hellos" - two handshakes in flight at once.
Options:
-
Deterministic tie-breaker: Lower npub (lexicographically) is always the "initiator." When a node receives an initiation from a higher npub while it has an outbound initiation pending to that same npub, it defers to the lower npub's handshake.
-
Both complete, then deduplicate: Let both handshakes run to completion. Both sides end up with the same session key (if protocol is designed for this). Discard the "extra" session state using tie-breaker.
-
Detect and merge: When receiving an initiation while one is pending, recognize the crossing and merge into a single handshake with contributions from both sides.
Considerations:
- Option 1 is simplest but may add latency (one side backs off)
- Option 2 wastes bandwidth but is robust
- Option 3 is elegant but complex to implement correctly
The npub comparison provides a consistent, globally-agreed ordering without any coordination.
Decision: Option 1 - deterministic tie-breaker using npub ordering.
When a node detects a crossing hello (receives initiation while its own initiation to the same peer is pending):
- If local npub < remote npub: Continue as initiator, ignore incoming initiation
- If local npub > remote npub: Abort own initiation, switch to responder role
This ensures exactly one handshake completes with minimal wasted effort. The latency cost is bounded to one round-trip in the crossing case, which should be rare.
4. FIPS Mesh Routing
Below the session layer, all FIPS packets (session handshake messages, encrypted payloads, control traffic) must be routed through the mesh to their destination.
4.1 Routing Layer Entry Points
The routing layer handles packets from two sources:
- Session establishment: Handshake packets for new sessions
- Session data: Encrypted payloads over established sessions
Both require determining how to reach the destination node_id.
4.2 Route Cache
The routing layer maintains a route cache mapping:
node_id → (coordinates, next_hop_peer)
Where:
coordinates: The destination's spanning tree coordinatesnext_hop_peer: A direct peer for greedy forwarding toward those coordinates
4.3 Routing Decision Flow
When sending a packet to a destination node_id:
Packet to send (dest = node_id)
│
├─► Route cache lookup
│ ├─► HIT: Coordinates known → greedy route via next_hop
│ └─► MISS: Proceed to discovery
│
└─► Route discovery (see §4.4)
If the route cache has coordinates for the destination, greedy routing proceeds immediately - no discovery needed.
4.4 Route Discovery Protocol
When the route cache has no entry for the destination, discovery must determine how to reach node_id X.
Discovery flow:
Route discovery for node_id X
│
├─► Check peer bloom filters
│ ├─► Match in peer P's filter → query P for coordinates
│ └─► No match in any filter → proceed to flooding
│
└─► Send LookupRequest (flooding with TTL)
└─► Await LookupResponse with coordinates
Bloom filter role: Bloom filters don't provide routes directly - they indicate which peers might know about a destination. A bloom match triggers a targeted query to that peer rather than blind flooding.
LookupRequest flooding: When no bloom filter matches, flood the query through the spanning tree with bounded TTL. Nodes that know the destination (have it in their bloom filter or route cache) respond with coordinates.
4.5 Packet Handling During Discovery
Packets arriving while route discovery is in progress:
- Queue: Buffer packets while discovery completes (bounded queue)
- Drop with error: Return to session layer, which may retry
Decision needed: Packet handling during route discovery?
4.6 Route Cache Population
Routes are learned through:
- Successful route discovery (explicit)
- Receiving packets from a source (reverse path learning)
- Spanning tree announcements (implicit reachability for nearby nodes)
- Bloom filter updates combined with coordinate queries
4.7 Route Cache Lifetime
Route cache entries should:
- Expire after configurable timeout
- Refresh on successful packet delivery
- Invalidate when peer link goes down
- Invalidate on spanning tree topology changes affecting the path
5. Terminology Reconciliation
The existing design docs (fips-routing.md) and this document use "session" differently. This section clarifies the terminology.
5.1 Two Distinct Concepts
| Term | Layer | Purpose | Endpoints |
|---|---|---|---|
| Crypto Session (§3) | End-to-end | Authentication + encryption | Source ↔ Destination |
| Routing Session (existing doc §4) | Hop-by-hop | Cache coordinates at routers | Along the path |
Crypto Session (what §3 of this document describes):
- Established between two npub identities
- Provides confidentiality (encryption) and authenticity (signatures)
- Survives route changes and transport failover
- Keyed by:
(local_npub, remote_npub)
Routing Session (what fips-routing.md §Part 4 describes):
- Warms coordinate caches at intermediate routers
- Enables minimal 36-byte data packet headers
- Must be re-established when router caches expire
- Keyed by:
(src_addr, dest_addr)at each router
5.2 Relationship Between Sessions
These are complementary, not conflicting:
┌─────────────────────────────────────────────────────────────────────┐
│ Crypto Session (end-to-end) │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ Routing Session (hop-by-hop cache state) │ │
│ │ │ │
│ │ Source ──► Router1 ──► Router2 ──► ... ──► Destination │ │
│ │ (cache) (cache) │ │
│ └───────────────────────────────────────────────────────────────┘ │
│ │
│ Encrypted payload travels inside routing session │
└─────────────────────────────────────────────────────────────────────┘
5.3 Establishment Order
Option A: Sequential (Crypto first, then Routing)
1. Discover destination coordinates (LookupRequest/Response)
2. Establish crypto session (handshake for keys)
3. Send SessionSetup to warm router caches
4. Send encrypted data packets (minimal headers)
Pros: Clean separation, crypto session exists before any data flows Cons: Additional round-trips before first data
Option B: Combined (Routing carries Crypto handshake)
1. Discover destination coordinates (LookupRequest/Response)
2. Send SessionSetup carrying crypto handshake initiation
3. Routers cache coordinates; destination receives handshake
4. Destination responds with SessionAck + crypto response
5. Send encrypted data packets
Pros: Fewer round-trips, single establishment phase Cons: Couples two concerns, SessionSetup becomes more complex
Option C: Crypto-only (No Routing Session)
1. Discover destination coordinates
2. Establish crypto session
3. Every data packet carries full coordinates
Pros: Simplest, no router cache state Cons: Larger packets (~400 bytes vs 36 bytes), more bandwidth
5.4 Recommended Terminology
To avoid confusion going forward:
| Use This | Instead Of | Meaning |
|---|---|---|
| Crypto session | "FIPS session" | End-to-end authenticated encryption |
| Routing session | "Session" (from routing doc) | Router cache state for a flow |
| Route discovery | — | Finding destination coordinates |
| Session setup | — | Warming router caches (routing session) |
5.5 Decision: Option B - Combined Establishment
Decision: Use combined establishment where routing session setup carries the crypto handshake.
Combined Establishment Flow:
1. Route discovery (if needed)
└─► LookupRequest/Response → obtain destination coordinates
2. SessionSetup + Crypto Init
└─► Source sends SessionSetup containing:
- src/dest coordinates (for router caching)
- Crypto handshake initiation (for destination)
└─► Routers cache coordinates as packet transits
└─► Destination receives crypto init, begins handshake
3. SessionAck + Crypto Response
└─► Destination sends SessionAck containing:
- Its coordinates (for reverse path caching)
- Crypto handshake response
└─► Routers cache reverse path
└─► Source completes crypto handshake
4. Data flow
└─► Encrypted payloads with minimal 36-byte headers
└─► Both crypto session and routing session now active
Benefits of combined approach:
- Single round-trip establishes both sessions
- Router caches warm as handshake transits
- No additional latency vs crypto-only
- Bidirectional routing session from the start (SessionAck warms return path)
Message structure implications:
SessionSetup and SessionAck messages must carry both:
- Routing information (coordinates for router caching)
- Crypto payload (handshake messages, opaque to routers)
Routers process the routing portion and forward; only endpoints process the crypto portion.
6. Session-Layer Encryption
FIPS uses two independent Noise Protocol handshakes at different layers:
| Layer | Scope | Pattern | Purpose |
|---|---|---|---|
| Link | Hop-by-hop | Noise IK | Authenticate peers, encrypt link |
| Session | End-to-end | Noise IK | Authenticate endpoints, encrypt payload |
Both use Noise_IK_secp256k1_ChaChaPoly_SHA256 with the same cryptographic
primitives, but with separate keys and sessions.
6.1 Why Two Layers?
Link encryption protects against passive observers on each hop but allows intermediate nodes to see routing information (destination address).
Session encryption protects the actual payload end-to-end. Intermediate nodes forward opaque ciphertext without being able to read the contents.
6.2 Session Noise Handshake
The session-layer Noise IK handshake is carried inside SessionSetup/SessionAck
messages, which themselves travel through the link-encrypted channel:
Initiator knows destination npub (from DNS lookup)
│
▼
SessionSetup { coords, handshake_payload: Noise IK msg1 }
│
▼ (travels through link-encrypted hops)
│
Responder processes msg1, learns initiator identity
│
▼
SessionAck { coords, handshake_payload: Noise IK msg2 }
│
▼
Session keys established (independent of link keys)
6.3 Cryptographic Primitives
Both link and session layers use the same stack:
| 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 |
6.4 Cryptographic Primitives
Following Lightning's BOLT 8 adaptation:
| 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 |
6.5 Handshake Integration with SessionSetup
The Noise handshake messages embed in SessionSetup/SessionAck:
SessionSetup {
// Routing portion (processed by routers)
src_coords: Vec<NodeId>,
dest_coords: Vec<NodeId>,
src_addr: Ipv6Addr,
dest_addr: Ipv6Addr,
// Crypto portion (opaque to routers, processed by destination)
handshake_payload: Vec<u8>, // Noise KK message 1
}
SessionAck {
// Routing portion
src_coords: Vec<NodeId>, // Responder's coordinates
// Crypto portion
handshake_payload: Vec<u8>, // Noise KK message 2
}
6.6 Session Keys
After handshake completion, Noise produces two symmetric keys:
send_key: For encrypting outbound packetsrecv_key: For decrypting inbound packets
These are used with ChaCha20-Poly1305 for all subsequent data packets.
6.7 Nonce Management
ChaCha20-Poly1305 requires unique nonces. Options:
-
Counter-based: Each side maintains a 64-bit send counter, incremented per packet. Nonce = counter (no coordination needed since keys are directional).
-
Random nonces: 96-bit random nonce per packet, included in header. Simpler but adds 12 bytes per packet.
Recommendation: Counter-based nonces (like WireGuard/Lightning). The counter also enables replay detection - reject packets with nonce ≤ last seen.
6.8 Forward Secrecy
The ephemeral keys (e in Noise notation) provide forward secrecy:
- Compromise of static keys (npub/nsec) doesn't reveal past session keys
- Each session has unique ephemeral keys
- Session keys derived from ephemeral-ephemeral DH (
ee)
6.9 Reference: Lightning BOLT 8
Lightning's adaptation of Noise for secp256k1 (BOLT 8) provides a proven reference implementation:
- Uses Noise XK pattern (different from our KK)
- Same secp256k1 + ChaCha20-Poly1305 + SHA-256 stack
- Handles the secp256k1 ECDH correctly
- Open source implementations available in multiple languages
FIPS can reference BOLT 8's cryptographic details while using the KK pattern appropriate for our mutual-knowledge scenario.
6.10 Data Packet Authentication
Decision: Use AEAD authentication only (no per-packet signatures).
The Noise handshake binds session keys to both parties' static keys. After handshake completion:
- Session keys are cryptographically tied to both npubs
- AEAD (ChaCha20-Poly1305) provides integrity and authenticity
- Only the holder of the session key can produce valid ciphertext
- Session keys can only be derived by holders of the corresponding nsecs
Per-packet signatures would add:
- 64 bytes overhead per packet
- Signing CPU cost (secp256k1 Schnorr)
- Verification CPU cost at receiver
Since Noise already provides authentication through key binding, signatures are redundant. This matches WireGuard and Lightning's approach.
Reconciliation note: §3.1 mentions packets being "signed by source" - this should be updated to reflect AEAD-only authentication.
7. Peer Connection Establishment
Before any of the traffic flows described above can occur, nodes must establish authenticated peer connections using Noise IK. See fips-design.md §1 for full protocol details and fips-architecture.md for the startup sequence.
7.1 Connection Flow Summary (Noise IK)
All messages use TLV framing (see fips-design.md §6 Wire Format):
┌────────┬────────┬────────────────────────────────────┐
│ Type │ Length │ Payload │
│ 1 byte │ 2 bytes│ Variable │
└────────┴────────┴────────────────────────────────────┘
Outbound (to static peer):
Config: npub + transport hint (e.g., "udp:192.168.1.1:4000")
│
▼
Create link via transport
│
▼
Send: [0x01][0x00 0x52][82-byte Noise msg1]
Type=NoiseIKMsg1, Length=82
│
▼
Recv: [0x02][0x00 0x21][33-byte Noise msg2]
Type=NoiseIKMsg2, Length=33
│
▼
Noise session established → link encrypted → begins tree gossip
Inbound (peer connects to us):
Transport receives packet from unknown address
│
▼
Parse TLV: Type=0x01 (NoiseIKMsg1)
│
▼
Process msg1 → learn peer's identity from encrypted static key
│
▼
Send: [0x02][0x00 0x21][33-byte Noise msg2]
│
▼
Noise session established → link encrypted → begins tree gossip
7.2 Post-Authentication
After successful Noise handshake:
- Link encrypted: All subsequent messages use AEAD encryption
- TreeAnnounce exchange: Both peers send their current tree state
- FilterAnnounce exchange: Both peers send their bloom filters
- Peer is Active: Can now participate in routing and forwarding
The first TreeAnnounce from a new peer may trigger parent reselection if that peer offers a better path to root.
8. Document Reconciliation
This section tracks items that need reconciliation with existing design docs or earlier sections of this document.
8.1 Completed Updates (Session 47)
| Location | Status | Notes |
|---|---|---|
| fips-design.md §1 Peer Auth | ✓ Done | Replaced custom handshake with Noise IK |
| fips-design.md §6 Messages | ✓ Done | Split into LinkMessageType + SessionMessageType |
| protocol.rs | ✓ Done | Removed Hello/Challenge/Auth/AuthAck types |
| protocol.rs | ✓ Done | Added SessionDatagram for link-layer encapsulation |
| This document §6 | ✓ Done | Updated to two-layer Noise IK architecture |
| This document §7 | ✓ Done | Updated connection flow for Noise IK |
8.2 Previous Updates (Session 40)
| Location | Status | Notes |
|---|---|---|
| §3.1 | ✓ Done | Updated to AEAD authentication |
| fips-routing.md Part 4 | ✓ Done | Renamed to "Routing Session Establishment" |
| fips-routing.md SessionSetup/Ack | ✓ Done | Added handshake_payload for crypto handshake |
| fips-design.md §7 Encryption | ✓ Done | Updated to reference Noise instead of NIP-44 |
| fips-architecture.md Config | ✓ Done | Renamed to "Routing Session", added "Crypto Session" |
8.3 Design Doc Alignment Summary
The following decisions from this document have been propagated:
- Two-layer architecture: Link layer (Noise IK peer auth) and session layer (Noise IK end-to-end) operate independently with separate keys
- Session terminology (§5.4): "Routing Session" vs "Crypto Session" distinction now consistent across all docs
- Combined establishment (§5.5): SessionSetup/SessionAck carry optional
handshake_payloadfor session-layer Noise IK handshake - Message type split: LinkMessageType for hop-by-hop, SessionMessageType for end-to-end (carried inside SessionDatagram)