mirror of
https://github.com/jmcorgan/fips.git
synced 2026-08-09 16:24:45 +00:00
Reorganize FIPS design documentation from implementation-centric structure (routing, gossip protocol, wire protocol, transports) to protocol-layer organization with clear service boundaries. New documents (8): - fips-transport-layer.md — transport layer spec - fips-link-layer.md — FLP spec (peer auth, link encryption, forwarding) - fips-session-layer.md — FSP spec (end-to-end encryption, sessions) - fips-ipv6-adapter.md — IPv6 adaptation (TUN, DNS, MTU enforcement) - fips-mesh-operation.md — routing, discovery, error recovery - fips-wire-formats.md — consolidated wire format reference - fips-spanning-tree.md — tree algorithm reference - fips-bloom-filters.md — bloom filter math reference Rewritten (2): - fips-intro.md — breadth-first intro with layer model diagrams - fips-software-architecture.md — slimmed to stable decisions Updated (3): - spanning-tree-dynamics.md — removed stale root refresh, aligned terminology - fips-configuration.md — fixed priority type (u16 → u8) - fips-state-machines.md — synced code examples with codebase Deleted (6): fips-transports.md, fips-wire-protocol.md, fips-gossip-protocol.md, fips-session-protocol.md, fips-routing.md, fips-tun-driver.md (content absorbed into new structure)
380 lines
16 KiB
Markdown
380 lines
16 KiB
Markdown
# FIPS Link Protocol (FLP)
|
|
|
|
The FIPS Link Protocol is the middle layer of the FIPS protocol stack. It sits
|
|
between the transport layer below and the FIPS Session Protocol (FSP) above.
|
|
FLP is where anonymous transport addresses become authenticated peers, where
|
|
the mesh self-organizes, and where forwarding decisions are made.
|
|
|
|
## Role
|
|
|
|
FLP manages direct peer connections over transports. When a transport delivers
|
|
a datagram from an unknown address, FLP authenticates the sender through a
|
|
Noise IK handshake, establishing a cryptographic link. Once authenticated, the
|
|
link carries all inter-peer communication: spanning tree gossip, bloom filter
|
|
updates, coordinate discovery, and forwarded session datagrams — all encrypted
|
|
per-hop.
|
|
|
|
FLP is the boundary between opaque transport addresses and identified peers.
|
|
Below FLP, everything is transport-specific addresses (IP:port, MAC, .onion).
|
|
Above FLP, everything is peers identified by public keys and routable by
|
|
node_addr. The transport layer never sees FIPS-level structure; FSP never sees
|
|
transport addresses or routing details.
|
|
|
|
## Services Provided to FSP
|
|
|
|
From the session layer's perspective, FLP is a black box providing three
|
|
services. FSP knows nothing about transports, links, peers, spanning trees,
|
|
coordinates, bloom filters, hop counts, or network topology.
|
|
|
|
### Datagram Forwarding
|
|
|
|
FLP accepts a datagram addressed by source and destination node_addr and
|
|
delivers it best-effort toward the destination. The datagram travels hop by
|
|
hop — at each node, FLP decrypts the link layer, reads the destination
|
|
node_addr, makes a local forwarding decision, and re-encrypts onto the
|
|
next-hop link.
|
|
|
|
FSP provides: source node_addr, destination node_addr, hop limit, and an
|
|
opaque payload (the session-layer encrypted message).
|
|
|
|
FLP provides: best-effort delivery. No acknowledgment, no retransmission, no
|
|
ordering guarantee. Datagrams may be dropped, duplicated, or delivered out of
|
|
order.
|
|
|
|
### Error Signaling
|
|
|
|
When forwarding fails, FLP signals the source endpoint asynchronously:
|
|
|
|
- **CoordsRequired**: A transit node lacks the destination's tree coordinates
|
|
and cannot make a forwarding decision. The source should re-initiate
|
|
discovery and reset its coordinate warmup strategy.
|
|
- **PathBroken**: Greedy routing reached a dead end — no peer is closer to the
|
|
destination than the current node. The source should re-discover the
|
|
destination's current coordinates.
|
|
|
|
Both signals travel inside the SessionDatagram envelope (using the existing
|
|
src/dest/hop_limit addressing) but are generated by transit nodes and are not
|
|
end-to-end encrypted. They are rate-limited at 100ms per destination to prevent
|
|
storms during topology changes.
|
|
|
|
### Local Delivery
|
|
|
|
When a datagram arrives with a destination node_addr matching the local node,
|
|
FLP delivers it up to FSP for session-layer processing.
|
|
|
|
## Services Required from Transport Layer
|
|
|
|
FLP requires the following from each transport:
|
|
|
|
### Datagram Delivery
|
|
|
|
Send and receive raw datagrams to/from transport addresses. The transport
|
|
handles all medium-specific details. FLP sees only "send bytes to address" and
|
|
"bytes arrived from address."
|
|
|
|
### MTU Reporting
|
|
|
|
The maximum datagram size for a given link. FLP needs this to determine how
|
|
much payload fits in a single packet after link encryption overhead (29 bytes
|
|
for the encrypted frame wrapper).
|
|
|
|
### Connection Lifecycle
|
|
|
|
For connection-oriented transports, the transport must establish the underlying
|
|
connection before FLP can begin the Noise IK handshake. For connectionless
|
|
transports, datagrams can flow immediately.
|
|
|
|
### Endpoint Discovery (Optional)
|
|
|
|
When a transport discovers a FIPS-capable endpoint (via beacon, query, or
|
|
other transport-specific mechanism), it notifies FLP so that link setup can
|
|
be initiated. Transports without discovery support provide peer addresses
|
|
through configuration.
|
|
|
|
## Peer Authentication
|
|
|
|
### Noise IK Handshake
|
|
|
|
Every peer connection begins with a Noise IK handshake that mutually
|
|
authenticates both parties and establishes symmetric keys for link encryption.
|
|
|
|
The IK pattern is chosen because:
|
|
- The **initiator** knows the responder's static public key from configuration
|
|
or discovery, and sends their own static key encrypted in the first message
|
|
- The **responder** learns the initiator's identity from the first message,
|
|
then responds with their own ephemeral key
|
|
|
|
After the two-message handshake completes, both parties share symmetric
|
|
session keys derived from four DH operations (es, ss, ee, se). The handshake
|
|
provides mutual authentication, forward secrecy, and identity hiding for the
|
|
initiator.
|
|
|
|
### Identity Binding
|
|
|
|
The Noise handshake binds the link to the peer's cryptographic identity. After
|
|
handshake completion:
|
|
|
|
- The peer's public key (FIPS identity) is confirmed
|
|
- The node_addr is computed from the public key (SHA-256, truncated to 128 bits)
|
|
- All subsequent traffic on the link is authenticated by the Noise session —
|
|
successful decryption proves the sender is the authenticated peer
|
|
- The link is registered in the dispatch table for O(1) packet routing
|
|
|
|
### Reconnection
|
|
|
|
When a Noise IK msg1 arrives from a peer that already has an authenticated
|
|
link, FLP accepts the new handshake alongside the existing session. If the new
|
|
handshake completes successfully, it replaces the old session. This handles
|
|
legitimate reconnection (network change, process restart, NAT rebinding)
|
|
without disrupting ongoing traffic until the new session is confirmed.
|
|
|
|
## Link Encryption
|
|
|
|
All traffic between authenticated peers is encrypted. Every packet on a link
|
|
— gossip messages, routing queries, forwarded session datagrams, disconnect
|
|
notifications — passes through Noise's ChaCha20-Poly1305 AEAD.
|
|
|
|
### Encrypted Frame Structure
|
|
|
|
Post-handshake packets are wrapped in an encrypted frame consisting of:
|
|
- A discriminator byte identifying the frame type
|
|
- A receiver index for O(1) session lookup
|
|
- An explicit counter used as the AEAD nonce
|
|
- The ciphertext with a Poly1305 authentication tag
|
|
|
|
The plaintext inside the encrypted frame begins with a message type byte
|
|
followed by the message-specific payload.
|
|
|
|
See [fips-wire-formats.md](fips-wire-formats.md) for the complete wire format
|
|
specification.
|
|
|
|
### What Encryption Provides
|
|
|
|
- **Confidentiality**: An observer on the underlying transport sees only
|
|
encrypted packets, packet timing, and packet sizes
|
|
- **Integrity**: Any modification to a packet is detected by the AEAD tag
|
|
- **Authentication**: Only the authenticated peer can produce valid ciphertext
|
|
for this link's session keys
|
|
|
|
### What Encryption Does Not Provide
|
|
|
|
- **End-to-end confidentiality**: Link encryption protects traffic on a single
|
|
hop. Each transit node decrypts, reads the routing envelope, and re-encrypts
|
|
for the next hop. Session-layer encryption (FSP) provides end-to-end
|
|
confidentiality.
|
|
- **Traffic analysis protection**: Packet timing, sizes, and volume are visible
|
|
to transport-layer observers.
|
|
|
|
## Index-Based Session Dispatch
|
|
|
|
Incoming packets are dispatched to the correct Noise session using a
|
|
receiver index — a random 32-bit value chosen by the receiver during the
|
|
handshake. This enables O(1) lookup without relying on source addresses.
|
|
|
|
Each party in a link has two indices:
|
|
- **our_index**: Chosen by us, included by the peer in packets sent to us
|
|
- **their_index**: Chosen by them, included by us in packets sent to them
|
|
|
|
The tuple `(transport_id, receiver_idx)` uniquely identifies a session.
|
|
|
|
### Index Properties
|
|
|
|
- **Random**: Cryptographically random to prevent guessing
|
|
- **Unique per transport**: No two active sessions on the same transport share
|
|
an index
|
|
- **Scoped to transport**: The same index value may appear on different
|
|
transports
|
|
- **Rotated on rekey**: New indices allocated on rekey to prevent cross-session
|
|
correlation by passive observers
|
|
|
|
### Dispatch Flow
|
|
|
|
1. Read discriminator byte to determine packet type
|
|
2. For encrypted frames (0x00): look up `(transport_id, receiver_idx)` in the
|
|
session table — O(1) hash lookup. Unknown indices are rejected before any
|
|
cryptographic operation.
|
|
3. For handshake msg2 (0x02): look up by our sender index to match to a
|
|
pending outbound handshake
|
|
4. For handshake msg1 (0x01): rate-limited processing, creates new state
|
|
|
|
This approach follows WireGuard's design: source address is informational,
|
|
not authoritative. Only successful cryptographic verification establishes
|
|
authenticity.
|
|
|
|
## Roaming
|
|
|
|
When an encrypted packet successfully decrypts, the sender is the
|
|
authenticated peer regardless of what transport address the packet arrived
|
|
from. FLP updates the peer's current address to the packet's source address,
|
|
and subsequent outbound packets use the updated address.
|
|
|
|
This allows peers to change transport addresses (IP:port for UDP, connection
|
|
handle for TCP) without session interruption. The mechanism is:
|
|
|
|
1. Packet arrives from a different address than expected
|
|
2. Receiver index lookup finds the session
|
|
3. AEAD decryption succeeds — the sender is cryptographically authenticated
|
|
4. Peer's address is updated to the new source address
|
|
|
|
Roaming is most useful for UDP, where source addresses can change due to NAT
|
|
rebinding or network changes. For connection-oriented transports, "roaming"
|
|
manifests as reconnection rather than mid-session address change.
|
|
|
|
## Replay Protection
|
|
|
|
Each link session maintains per-direction counters:
|
|
|
|
- **Send counter**: Monotonically increasing, used as the AEAD nonce for each
|
|
outbound packet
|
|
- **Receive window**: A sliding bitmap (2048 entries) tracking which counters
|
|
have been seen
|
|
|
|
The receive window handles the realities of unreliable transports: packets may
|
|
arrive out of order, be duplicated, or be lost. The window accepts any counter
|
|
not yet seen and within 2048 of the highest counter received. Counters older
|
|
than the window are rejected.
|
|
|
|
The replay check is performed before decryption to prevent CPU exhaustion from
|
|
replayed packets that would pass the index lookup but fail decryption.
|
|
|
|
## Rate Limiting
|
|
|
|
Handshake initiation (msg1) is the primary attack surface for unauthenticated
|
|
traffic. Each msg1 requires Noise DH operations (~200µs on modern CPUs),
|
|
state allocation, and response generation.
|
|
|
|
FLP uses a global token bucket rate limiter:
|
|
- **Burst capacity**: Handles legitimate connection storms (e.g., node restart
|
|
with many configured peers)
|
|
- **Sustained rate**: Limits steady-state new connections per second
|
|
- **Global scope**: Rate limiting is global, not per-source, because UDP
|
|
source addresses are trivially spoofable
|
|
|
|
Additional protections:
|
|
- **Connection limit**: Maximum number of pending inbound handshakes, capping
|
|
memory usage
|
|
- **Handshake timeout**: Stale pending handshakes are cleaned up after a
|
|
configurable timeout
|
|
- **Allowlist/blocklist**: Optional peer filtering before handshake processing
|
|
|
|
## Disconnect
|
|
|
|
FLP supports orderly link teardown via a Disconnect message carrying a reason
|
|
code (shutdown, restart, protocol error, transport failure, resource
|
|
exhaustion, security violation, configuration change, timeout).
|
|
|
|
On receiving Disconnect, FLP immediately cleans up state: removes the peer
|
|
from the peer table, frees the session index, removes the link, and cleans up
|
|
address mappings. If the departed peer was the tree parent, FLP triggers parent
|
|
reselection.
|
|
|
|
Disconnect is best-effort — if the transport is broken, the message won't
|
|
arrive. Timeout-based detection remains the fallback for detecting failed
|
|
links.
|
|
|
|
On node shutdown, Disconnect is sent to all active peers before transports are
|
|
stopped.
|
|
|
|
## Liveness Detection
|
|
|
|
FLP detects link liveness through timeout-based mechanisms. There are no
|
|
dedicated keepalive or ping/pong messages. Liveness is inferred from:
|
|
|
|
- **Data traffic**: Any successfully decrypted packet confirms the peer is
|
|
alive
|
|
- **Gossip messages**: TreeAnnounce and FilterAnnounce messages sent
|
|
periodically as part of normal mesh operation serve as implicit heartbeats
|
|
|
|
When no traffic is received for the configured timeout period, FLP marks the
|
|
link as stale. If the stale state persists, the link is torn down.
|
|
|
|
The two-state liveness model:
|
|
- **Connected → Stale**: No traffic received for threshold duration
|
|
- **Stale → Connected**: Valid traffic received (peer is alive again)
|
|
- **Stale → Disconnected**: Timeout exceeded, link torn down
|
|
|
|
## Link Message Types
|
|
|
|
FLP defines six message types carried inside encrypted frames:
|
|
|
|
| Type | Name | Purpose |
|
|
| ---- | ---- | ------- |
|
|
| 0x10 | TreeAnnounce | Spanning tree state announcements between peers |
|
|
| 0x20 | FilterAnnounce | Bloom filter reachability updates |
|
|
| 0x30 | LookupRequest | Coordinate discovery — flood toward destination |
|
|
| 0x31 | LookupResponse | Coordinate discovery — response with coordinates |
|
|
| 0x40 | SessionDatagram | Encapsulated session-layer payload for forwarding |
|
|
| 0x50 | Disconnect | Orderly link teardown with reason code |
|
|
|
|
Additionally, handshake messages (0x01 msg1, 0x02 msg2) are sent unencrypted
|
|
before the link session is established.
|
|
|
|
TreeAnnounce and FilterAnnounce are exchanged between direct peers only — they
|
|
are not forwarded. LookupRequest and LookupResponse are forwarded through the
|
|
mesh (flooded with deduplication). SessionDatagram is forwarded hop-by-hop
|
|
toward the destination. Disconnect is peer-to-peer.
|
|
|
|
See [fips-mesh-operation.md](fips-mesh-operation.md) for how these messages
|
|
work together to build and maintain the mesh, and
|
|
[fips-wire-formats.md](fips-wire-formats.md) for byte-level message layouts.
|
|
|
|
## Security Properties
|
|
|
|
### Threat Resistance
|
|
|
|
| Threat | Mitigation |
|
|
| ------ | ---------- |
|
|
| Connection exhaustion | Token bucket rate limit + connection count limit |
|
|
| CPU exhaustion (msg1 flood) | Rate limit before crypto operations |
|
|
| Replay attacks | Counter-based nonces with sliding window |
|
|
| State confusion | Strict handshake state machine validation |
|
|
| Spoofed encrypted packets | Index lookup + AEAD verification |
|
|
| Spoofed msg2 | Index lookup + Noise ephemeral key binding |
|
|
| Address spoofing | Cryptographic authority, not address-based |
|
|
| Session correlation | Index rotation on rekey |
|
|
|
|
### Unauthenticated Attack Surface
|
|
|
|
Only handshake msg1 can be sent by unauthenticated parties. Encrypted frames
|
|
require a known session index, and msg2 requires a response to a specific
|
|
ephemeral key. The msg1 attack surface is protected by rate limiting,
|
|
connection limits, and handshake timeouts.
|
|
|
|
### Authenticated Peer Misbehavior
|
|
|
|
Authentication establishes identity but does not grant trust. An authenticated
|
|
peer can send malformed packets (which fail AEAD and are dropped) or
|
|
high-frequency traffic (rate-limited by higher layers). False tree coordinate
|
|
claims are constrained by signature verification on TreeAnnounce messages.
|
|
|
|
### Silent Drop Policy
|
|
|
|
Invalid packets are silently dropped without error responses. This prevents
|
|
information leakage about internal state and avoids amplification attacks where
|
|
an attacker sends invalid packets to elicit responses.
|
|
|
|
## Implementation Status
|
|
|
|
| Feature | Status |
|
|
| ------- | ------ |
|
|
| Noise IK handshake | **Implemented** |
|
|
| Link encryption (ChaCha20-Poly1305) | **Implemented** |
|
|
| Index-based session dispatch | **Implemented** |
|
|
| Replay protection (sliding window) | **Implemented** |
|
|
| Roaming (address-follows-crypto) | **Implemented** |
|
|
| Rate limiting (token bucket) | **Implemented** |
|
|
| Disconnect with reason codes | **Implemented** |
|
|
| Liveness detection (timeout-based) | **Implemented** |
|
|
| Reconnection handling | **Implemented** |
|
|
| Rekey with index rotation | Planned |
|
|
| Allowlist/blocklist | Planned |
|
|
|
|
## References
|
|
|
|
- [fips-intro.md](fips-intro.md) — Protocol overview and architecture
|
|
- [fips-transport-layer.md](fips-transport-layer.md) — Transport layer (below FLP)
|
|
- [fips-session-layer.md](fips-session-layer.md) — FSP (above FLP)
|
|
- [fips-mesh-operation.md](fips-mesh-operation.md) — How FLP's routing and
|
|
self-organization work in practice
|
|
- [fips-wire-formats.md](fips-wire-formats.md) — Byte-level wire format reference
|