Update design docs and diagrams for Noise XX and forklift changes
Update CHANGELOG Breaking section with all v0.4.0 wire format changes. Comprehensive update to fips-wire-formats.md (handshake, MMP, FilterAnnounce, discovery, size tables). Update fips-mesh-layer.md, fips-session-layer.md, fips-intro.md for IK/XK→XX transition and spin bit removal. Document disable_routing and leaf_only profile config in fips-configuration.md. Update fips-transport-layer.md for 4-byte beacon and unified header. Update fips-bloom-filters.md for delta compression and variable sizing. Update fips-mesh-operation.md for node profiles and discovery min_mtu. Create/update 8 SVG diagrams (XX msg1/2/3, handshake flow, session setup/ack/msg3, filter-announce, sender/receiver report). Update README.md Noise pattern references.
@@ -7,7 +7,79 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
## Breaking
|
||||
|
||||
_Wire-format and API breaking changes for the next major release._
|
||||
Wire-format breaking changes for v0.4.0. All nodes in a mesh must
|
||||
run the same major version — these changes are not backward compatible
|
||||
with v0.2.x peers.
|
||||
|
||||
### Changed
|
||||
|
||||
#### Noise XX Handshake (FMP and FSP)
|
||||
|
||||
- FMP link handshake switched from Noise IK (2 messages) to Noise XX
|
||||
(3 messages). Neither side requires prior knowledge of the peer's
|
||||
static key. Responder identity revealed in msg2, initiator in msg3.
|
||||
FMP wire version incremented to 1.
|
||||
- FSP session handshake switched from Noise XK to Noise XX. Same
|
||||
3-message flow with post-handshake identity verification using
|
||||
x-only key comparison (parity-independent for npub compatibility).
|
||||
- Protocol negotiation payload added to XX msg2/msg3 for both layers:
|
||||
format byte, packed version min/max, 64-bit feature bitfield, and
|
||||
forward-compatible TLV extensions. Enables rolling protocol upgrades
|
||||
in future releases.
|
||||
- FMP msg1 reduced from 106 to 33 bytes (ephemeral key only, no
|
||||
encrypted static key or DH products).
|
||||
|
||||
#### FMP Node Profiles
|
||||
|
||||
- Node profile enum (Full, NonRouting, Leaf) advertised in FMP feature
|
||||
bitfield bits 0-2. At least one side of a link must be Full.
|
||||
- MMP report flow gated by wants/provides bits (bits 3-6): reports
|
||||
only sent when the sender can provide and the receiver wants them.
|
||||
- Non-routing nodes receive bloom filters (one-way) but do not send
|
||||
them; the full peer inserts their identity as a leaf dependent.
|
||||
- Leaf nodes enforce single-peer constraint with no tree, bloom, or
|
||||
transit participation.
|
||||
|
||||
#### MMP Report Format
|
||||
|
||||
- Spin bit removed. Reclaims FMP flags bit 2 and FSP inner flags
|
||||
bit 0. Superseded by MMP receiver report timestamp echo for RTT.
|
||||
- SenderReport reduced from 48 to 20 bytes (3 fields: interval
|
||||
packets/bytes sent, cumulative packets sent).
|
||||
- ReceiverReport reduced from 68 to 54 bytes (10 fields retained;
|
||||
removed max/mean burst loss and interval recv counters).
|
||||
- Both report types use extensibility header: `[format_version:1]
|
||||
[total_length:2 LE]` replacing reserved bytes. Decoders skip
|
||||
unknown trailing bytes for forward compatibility.
|
||||
|
||||
#### Discovery Wire Format
|
||||
|
||||
- Dropped `origin_coords` from LookupRequest (saves 2 + 16*depth
|
||||
bytes per request). Reverse-path routing via `recent_requests` is
|
||||
the primary response mechanism.
|
||||
- `min_mtu` field wired up in LookupRequest: transit nodes skip peers
|
||||
whose link MTU is below the request's minimum.
|
||||
- TLV extension section added to LookupRequest and LookupResponse
|
||||
after fixed fields. Transit nodes forward TLV bytes verbatim.
|
||||
|
||||
#### Shared-Media Beacons
|
||||
|
||||
- Ethernet frame header unified to 4 bytes `[type][flags][length:2
|
||||
LE]` for all frame types. Beacons reduced from 34 to 5 bytes
|
||||
(pubkey stripped — identity learned from XX handshake).
|
||||
- BLE pre-handshake pubkey exchange removed. Cross-probe tie-breaker
|
||||
eliminated (unnecessary with XX).
|
||||
|
||||
#### Bloom Filter Wire Format
|
||||
|
||||
- FilterAnnounce gains flags byte (delta bit), `base_seq` field, and
|
||||
RLE-compressed payload for XOR-diff delta compression.
|
||||
- New FilterNack message type (0x21) for out-of-sequence delta
|
||||
recovery (triggers full retransmit).
|
||||
- Filter size decoupled from FMP negotiation: announced dynamically in
|
||||
filter updates. Bit 7 and TLV field 1 removed from handshake.
|
||||
- Variable filter sizes (512 bytes to 32 KB) with adaptive sizing
|
||||
based on outgoing fill ratio (step-up at 20%, step-down at 5%).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
|
||||
@@ -40,8 +40,9 @@ endpoints.
|
||||
filter guided discovery, no global routing tables
|
||||
- **Multi-transport** — UDP, TCP, Ethernet, Tor, and Bluetooth (BLE L2CAP)
|
||||
today; designed for serial and radio
|
||||
- **Noise encryption** — hop-by-hop link encryption (IK) plus independent
|
||||
end-to-end session encryption (XK), with periodic rekey for forward secrecy
|
||||
- **Noise encryption** — hop-by-hop link encryption and independent
|
||||
end-to-end session encryption (both Noise XX), with periodic rekey for
|
||||
forward secrecy and protocol negotiation in the handshake
|
||||
- **Nostr-native identity** — secp256k1 keypairs as node addresses, no
|
||||
registration or central authority
|
||||
- **IPv6 adaptation** — TUN interface maps npubs to fd00::/8 addresses for
|
||||
@@ -322,7 +323,7 @@ Ethernet, Tor, and Bluetooth (BLE) with a small live mesh of deployed nodes.
|
||||
|
||||
- Spanning tree construction with greedy coordinate routing
|
||||
- Bloom filter guided discovery (no flooding, single-path with retry)
|
||||
- Noise IK (link layer) and Noise XK (session layer) encryption
|
||||
- Noise XX encryption at both layers with protocol negotiation
|
||||
- Periodic Noise rekey with hitless cutover for forward secrecy (FMP + FSP)
|
||||
- Persistent node identity with key file management
|
||||
- IPv6 TUN adapter with DNS resolution of `.fips` names
|
||||
|
||||
@@ -1,43 +1,52 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 270" font-family="monospace" font-size="13">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
|
||||
<!-- Background -->
|
||||
<rect width="580" height="270" fill="#1a1a2e" rx="4"/>
|
||||
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FilterAnnounce (0x20) — 11 + filter bytes</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FilterAnnounce (0x20) — 19-byte header + RLE payload</text>
|
||||
|
||||
<!-- Row 0 (0–3): msg_type(1) + sequence starts -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
<!-- Row 0 (0-1): msg_type + flags -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–1</text>
|
||||
|
||||
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
|
||||
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
|
||||
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
|
||||
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x20</text>
|
||||
|
||||
<rect x="185" y="36" width="390" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="380" y="66" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
|
||||
<rect x="185" y="36" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="380" y="56" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
|
||||
<text x="380" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte — bit 0: delta (XOR diff)</text>
|
||||
|
||||
<!-- Row 1 (4–8): sequence continued -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–8</text>
|
||||
<!-- Row 1 (2-9): sequence -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">2–9</text>
|
||||
|
||||
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="118" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — monotonic counter</text>
|
||||
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sequence</text>
|
||||
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE — per-peer monotonic counter</text>
|
||||
|
||||
<!-- Row 2 (9–10): hash_count + size_class -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">9–10</text>
|
||||
<!-- Row 2 (10-17): base_seq -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">10–17</text>
|
||||
|
||||
<rect x="55" y="140" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">hash_count</text>
|
||||
<text x="185" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
|
||||
<rect x="55" y="140" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">base_seq</text>
|
||||
<text x="315" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">8 bytes LE — reference sequence for delta (0 if full)</text>
|
||||
|
||||
<rect x="315" y="140" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">size_class</text>
|
||||
<text x="445" y="180" fill="#d8a0f8" text-anchor="middle" font-size="10">1 byte</text>
|
||||
<!-- Row 3 (18): size_class -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">18</text>
|
||||
|
||||
<!-- Row 3 (11–): filter_bits -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">11–</text>
|
||||
<rect x="55" y="192" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">size_class</text>
|
||||
<text x="315" y="232" fill="#8af8f8" text-anchor="middle" font-size="10">1 byte — filter size = 512 « size_class bytes</text>
|
||||
|
||||
<rect x="55" y="198" width="520" height="40" fill="#1f1f3a" stroke="#4ad9d9" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
|
||||
<text x="315" y="222" fill="#8af8f8" text-anchor="middle" font-size="11">filter_bits (variable, 512 << size_class bytes)</text>
|
||||
<!-- Row 4 (19-): compressed_payload -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">19–</text>
|
||||
|
||||
<rect x="55" y="250" width="520" height="40" fill="#1f1f3a" stroke="#4a9090" stroke-width="1" stroke-dasharray="4,3" rx="3"/>
|
||||
<text x="315" y="274" fill="#8ad8d8" text-anchor="middle" font-size="11">compressed_payload (RLE: [count:2 LE][word:8 LE] per run)</text>
|
||||
|
||||
<!-- Explanation -->
|
||||
<rect x="55" y="304" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="315" y="324" fill="#888" text-anchor="middle" font-size="10">delta: XOR diff of current vs last-sent filter — full: raw filter words</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="258" fill="#777" font-size="10" text-anchor="middle">v1 payload: 1,035 bytes (11 header + 1,024 filter)</text>
|
||||
<text x="310" y="362" fill="#777" font-size="10" text-anchor="middle">19-byte header + variable compressed payload</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 3.3 KiB |
@@ -1,26 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 500" font-family="monospace" font-size="13">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 620 620" font-family="monospace" font-size="13">
|
||||
<defs>
|
||||
<marker id="arrowR" markerWidth="10" markerHeight="8" refX="9" refY="4" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="0,0 10,4 0,8" fill="#e0e0e0"/>
|
||||
</marker>
|
||||
<marker id="arrowL" markerWidth="10" markerHeight="8" refX="1" refY="4" orient="auto" markerUnits="userSpaceOnUse">
|
||||
<polygon points="10,0 0,4 10,8" fill="#e0e0e0"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<!-- Background -->
|
||||
<rect width="620" height="500" fill="#1a1a2e" rx="4"/>
|
||||
<rect width="620" height="620" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP Handshake Flow (Noise IK)</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">FMP Handshake Flow (Noise XX)</text>
|
||||
|
||||
<!-- Column headers -->
|
||||
<text x="100" y="54" fill="#8ab4f8" text-anchor="middle" font-size="12" font-weight="bold">Initiator</text>
|
||||
<text x="520" y="54" fill="#8ab4f8" text-anchor="middle" font-size="12" font-weight="bold">Responder</text>
|
||||
|
||||
<!-- Lifelines -->
|
||||
<line x1="100" y1="62" x2="100" y2="480" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<line x1="520" y1="62" x2="520" y2="480" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<line x1="100" y1="62" x2="100" y2="600" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
<line x1="520" y1="62" x2="520" y2="600" stroke="#333" stroke-width="1" stroke-dasharray="4,4"/>
|
||||
|
||||
<!-- Step 1: Initiator prepares -->
|
||||
<text x="20" y="88" fill="#8af8c8" font-size="10">generates sender_idx</text>
|
||||
@@ -28,39 +25,51 @@
|
||||
|
||||
<!-- Arrow 1: msg1 (initiator -> responder) -->
|
||||
<line x1="100" y1="120" x2="520" y2="120" stroke="#4a90d9" stroke-width="1.5" marker-end="url(#arrowR)"/>
|
||||
<rect x="130" y="128" width="360" height="24" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="144" fill="#e0e0e0" text-anchor="middle" font-size="10">[0x01|flags=0|len] | sender_idx | noise_msg1</text>
|
||||
<text x="310" y="166" fill="#666" text-anchor="middle" font-size="9">phase 0x1 — 114 bytes</text>
|
||||
<rect x="145" y="128" width="330" height="24" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="144" fill="#e0e0e0" text-anchor="middle" font-size="10">[0x11|flags=0|len] | sender_idx | noise_msg1</text>
|
||||
<text x="310" y="166" fill="#666" text-anchor="middle" font-size="9">phase 0x1 — 41 bytes — pattern: → e</text>
|
||||
|
||||
<!-- Step 2: Responder processes (right-aligned to stay in bounds) -->
|
||||
<text x="600" y="192" fill="#8af8c8" font-size="10" text-anchor="end">validates msg1</text>
|
||||
<text x="600" y="206" fill="#8af8c8" font-size="10" text-anchor="end">learns initiator's static key</text>
|
||||
<text x="600" y="220" fill="#8af8c8" font-size="10" text-anchor="end">generates sender_idx</text>
|
||||
<text x="600" y="234" fill="#8af8c8" font-size="10" text-anchor="end">generates ephemeral keypair</text>
|
||||
<!-- Step 2: Responder processes msg1 -->
|
||||
<text x="600" y="192" fill="#8af8c8" font-size="10" text-anchor="end">validates msg1 (ephemeral only)</text>
|
||||
<text x="600" y="206" fill="#8af8c8" font-size="10" text-anchor="end">generates sender_idx</text>
|
||||
<text x="600" y="220" fill="#8af8c8" font-size="10" text-anchor="end">generates ephemeral keypair</text>
|
||||
|
||||
<!-- Arrow 2: msg2 (responder -> initiator) -->
|
||||
<!-- Draw line right-to-left so orient="auto" points the arrowhead left -->
|
||||
<line x1="520" y1="256" x2="100" y2="256" stroke="#4ad99a" stroke-width="1.5" marker-end="url(#arrowR)"/>
|
||||
<rect x="130" y="264" width="360" height="24" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="280" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x02|flags=0|len] | sender_idx | receiver_idx | noise_msg2</text>
|
||||
<text x="310" y="300" fill="#666" text-anchor="middle" font-size="9">phase 0x2 — 69 bytes</text>
|
||||
<line x1="520" y1="244" x2="100" y2="244" stroke="#4ad99a" stroke-width="1.5" marker-end="url(#arrowR)"/>
|
||||
<rect x="115" y="252" width="390" height="24" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="268" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x12|flags=0|len] | sender_idx | receiver_idx | noise_msg2 + negotiation</text>
|
||||
<text x="310" y="288" fill="#666" text-anchor="middle" font-size="9">phase 0x2 — 144 bytes — pattern: ← e, ee, s, es</text>
|
||||
<text x="600" y="300" fill="#d8a0f8" font-size="9" text-anchor="end">responder identity revealed</text>
|
||||
|
||||
<!-- Step 3: Initiator completes -->
|
||||
<text x="20" y="324" fill="#8af8c8" font-size="10">validates msg2</text>
|
||||
<text x="20" y="338" fill="#8af8c8" font-size="10">derives session keys</text>
|
||||
<!-- Step 3: Initiator processes msg2, learns responder identity -->
|
||||
<text x="20" y="322" fill="#8af8c8" font-size="10">validates msg2</text>
|
||||
<text x="20" y="336" fill="#8af8c8" font-size="10">learns responder identity</text>
|
||||
<text x="20" y="350" fill="#8af8c8" font-size="10">checks epoch (restart detection)</text>
|
||||
|
||||
<!-- Arrow 3: msg3 (initiator -> responder) -->
|
||||
<line x1="100" y1="368" x2="520" y2="368" stroke="#d94a6a" stroke-width="1.5" marker-end="url(#arrowR)"/>
|
||||
<rect x="115" y="376" width="390" height="24" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="392" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x13|flags=0|len] | sender_idx | receiver_idx | noise_msg3 + neg</text>
|
||||
<text x="310" y="412" fill="#666" text-anchor="middle" font-size="9">phase 0x3 — 111 bytes — pattern: → s, se</text>
|
||||
<text x="20" y="424" fill="#d8a0f8" font-size="9">initiator identity revealed</text>
|
||||
|
||||
<!-- Step 4: Responder processes msg3, learns initiator identity -->
|
||||
<text x="600" y="440" fill="#8af8c8" font-size="10" text-anchor="end">validates msg3</text>
|
||||
<text x="600" y="454" fill="#8af8c8" font-size="10" text-anchor="end">learns initiator identity</text>
|
||||
<text x="600" y="468" fill="#8af8c8" font-size="10" text-anchor="end">checks epoch, derives session keys</text>
|
||||
|
||||
<!-- Handshake complete separator -->
|
||||
<line x1="30" y1="360" x2="230" y2="360" stroke="#d9904a" stroke-width="1.5"/>
|
||||
<text x="310" y="364" fill="#d9904a" text-anchor="middle" font-size="11" font-weight="bold">HANDSHAKE COMPLETE</text>
|
||||
<line x1="390" y1="360" x2="590" y2="360" stroke="#d9904a" stroke-width="1.5"/>
|
||||
<line x1="30" y1="488" x2="230" y2="488" stroke="#d9904a" stroke-width="1.5"/>
|
||||
<text x="310" y="492" fill="#d9904a" text-anchor="middle" font-size="11" font-weight="bold">HANDSHAKE COMPLETE</text>
|
||||
<line x1="390" y1="488" x2="590" y2="488" stroke="#d9904a" stroke-width="1.5"/>
|
||||
|
||||
<!-- Arrow 3: first encrypted frame -->
|
||||
<text x="20" y="392" fill="#777" font-size="10">first encrypted frame:</text>
|
||||
<line x1="100" y1="406" x2="520" y2="406" stroke="#d94a6a" stroke-width="1.5" marker-end="url(#arrowR)"/>
|
||||
<rect x="115" y="414" width="390" height="24" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="430" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag</text>
|
||||
<text x="310" y="450" fill="#666" text-anchor="middle" font-size="9">phase 0x0 — established frame</text>
|
||||
<!-- Arrow 4: first encrypted frame -->
|
||||
<text x="20" y="516" fill="#777" font-size="10">first encrypted frame:</text>
|
||||
<line x1="100" y1="530" x2="520" y2="530" stroke="#d9b04a" stroke-width="1.5" marker-end="url(#arrowR)"/>
|
||||
<rect x="115" y="538" width="390" height="24" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1" rx="3"/>
|
||||
<text x="310" y="554" fill="#e0e0e0" text-anchor="middle" font-size="9">[0x00|flags|len] | receiver_idx | counter=0 | ciphertext+tag</text>
|
||||
<text x="310" y="574" fill="#666" text-anchor="middle" font-size="9">phase 0x0 — established frame</text>
|
||||
|
||||
<!-- Legend -->
|
||||
<text x="310" y="478" fill="#555" text-anchor="middle" font-size="9">Both parties hold identical symmetric keys. Epoch exchange enables restart detection.</text>
|
||||
<text x="310" y="600" fill="#555" text-anchor="middle" font-size="9">Both parties hold identical symmetric keys. Epoch + negotiation exchanged in msg2/msg3.</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 5.1 KiB |
@@ -1,67 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 426" font-family="monospace" font-size="13">
|
||||
<!-- Background -->
|
||||
<rect width="580" height="426" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise IK Message 1 — phase 0x1 (114 bytes)</text>
|
||||
|
||||
<!-- Row 0: Common prefix (bytes 0-3) -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
|
||||
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
|
||||
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
|
||||
|
||||
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
|
||||
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
|
||||
|
||||
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
|
||||
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
|
||||
|
||||
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
|
||||
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
|
||||
|
||||
<!-- Row 1: sender_idx (bytes 4-7) -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–7</text>
|
||||
|
||||
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
|
||||
<text x="315" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Noise IK msg1 label -->
|
||||
<text x="10" y="250" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 250)">Noise IK msg1 (106 bytes)</text>
|
||||
<line x1="18" y1="140" x2="18" y2="348" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
|
||||
|
||||
<!-- Rows 2-3: ephemeral_pubkey (bytes 8-40, 33 bytes) -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8–40</text>
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="170" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
|
||||
|
||||
<rect x="55" y="192" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="222" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
|
||||
|
||||
<!-- Rows 4-5: encrypted_static (bytes 41-89, 49 bytes) -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">41–89</text>
|
||||
|
||||
<rect x="55" y="244" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="274" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
|
||||
|
||||
<rect x="55" y="296" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="326" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
|
||||
|
||||
<!-- Row 6: encrypted_epoch (bytes 90-113, 24 bytes) -->
|
||||
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">90–113</text>
|
||||
|
||||
<rect x="55" y="348" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="372" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
|
||||
<text x="315" y="390" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="418" fill="#777" font-size="10" text-anchor="middle">total: 114 bytes · payload_len = 110 · pattern: e, es, s, ss</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.3 KiB |
@@ -1,12 +1,12 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 374" font-family="monospace" font-size="13">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 218" font-family="monospace" font-size="13">
|
||||
<!-- Background -->
|
||||
<rect width="580" height="374" fill="#1a1a2e" rx="4"/>
|
||||
<rect width="580" height="218" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise IK Message 2 — phase 0x2 (69 bytes)</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise XX Message 1 — phase 0x1 (41 bytes)</text>
|
||||
|
||||
<!-- Row 0: Common prefix (bytes 0-3) -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
|
||||
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
|
||||
@@ -25,41 +25,19 @@
|
||||
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
|
||||
|
||||
<!-- Row 1: sender_idx (bytes 4-7) -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–7</text>
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–7</text>
|
||||
|
||||
<rect x="55" y="88" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
|
||||
<text x="315" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 2: receiver_idx (bytes 8-11) -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8–11</text>
|
||||
<!-- Row 2: ephemeral_pubkey (bytes 8-40, 33 bytes) -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8–40</text>
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
|
||||
<text x="315" y="182" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Noise IK msg2 bracket -->
|
||||
<text x="10" y="270" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 270)">Noise IK msg2 (57 bytes)</text>
|
||||
<line x1="18" y1="192" x2="18" y2="348" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="192" x2="23" y2="192" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
|
||||
|
||||
<!-- Rows 3-4: ephemeral_pubkey (bytes 12-44, 33 bytes) -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">12–44</text>
|
||||
|
||||
<rect x="55" y="192" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="222" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
|
||||
|
||||
<rect x="55" y="244" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="274" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
|
||||
|
||||
<!-- Row 5: encrypted_epoch (bytes 45-68, 24 bytes) -->
|
||||
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">45–68</text>
|
||||
|
||||
<rect x="55" y="296" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
|
||||
<text x="315" y="338" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
|
||||
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
|
||||
<text x="315" y="182" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="366" fill="#777" font-size="10" text-anchor="middle">total: 69 bytes · payload_len = 65 · pattern: e, ee, se</text>
|
||||
<text x="310" y="210" fill="#777" font-size="10" text-anchor="middle">total: 41 bytes · payload_len = 37 · pattern: → e</text>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 2.7 KiB |
@@ -0,0 +1,82 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 478" font-family="monospace" font-size="13">
|
||||
<!-- Background -->
|
||||
<rect width="580" height="478" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise XX Message 2 — phase 0x2 (118–144 bytes)</text>
|
||||
|
||||
<!-- Row 0: Common prefix (bytes 0-3) -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
|
||||
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
|
||||
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
|
||||
|
||||
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
|
||||
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
|
||||
|
||||
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
|
||||
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
|
||||
|
||||
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
|
||||
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
|
||||
|
||||
<!-- Row 1: sender_idx (bytes 4-7) -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–7</text>
|
||||
|
||||
<rect x="55" y="88" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
|
||||
<text x="185" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- receiver_idx (bytes 8-11) -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end"/>
|
||||
|
||||
<rect x="315" y="88" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
|
||||
<text x="445" y="130" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Noise XX msg2 bracket -->
|
||||
<text x="10" y="290" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 290)">Noise XX msg2 (106 bytes base)</text>
|
||||
<line x1="18" y1="140" x2="18" y2="400" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="400" x2="23" y2="400" stroke="#555" stroke-width="1"/>
|
||||
|
||||
<!-- Row 2: ephemeral_pubkey (bytes 12-44, 33 bytes) -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12–44</text>
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ephemeral_pubkey</text>
|
||||
<text x="315" y="182" fill="#f8c88a" text-anchor="middle" font-size="10">33 bytes (compressed secp256k1)</text>
|
||||
|
||||
<!-- Row 3: encrypted_static (bytes 45-93, 49 bytes) -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">45–93</text>
|
||||
|
||||
<rect x="55" y="192" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="216" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
|
||||
<text x="315" y="234" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
|
||||
|
||||
<!-- Row 4: encrypted_epoch (bytes 94-117, 24 bytes) -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">94–117</text>
|
||||
|
||||
<rect x="55" y="244" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
|
||||
<text x="315" y="286" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
|
||||
|
||||
<!-- Row 5: negotiation payload (variable, optional) -->
|
||||
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">118+</text>
|
||||
|
||||
<rect x="55" y="296" width="520" height="52" fill="#2d4a4a" stroke="#4a9090" stroke-width="1.5" rx="3" stroke-dasharray="4,3"/>
|
||||
<text x="315" y="320" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted negotiation payload</text>
|
||||
<text x="315" y="338" fill="#8ad8d8" text-anchor="middle" font-size="10">26 bytes typical (10 payload + 16 AEAD tag)</text>
|
||||
|
||||
<!-- Row 6: AEAD explanation -->
|
||||
<rect x="55" y="362" width="520" height="38" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="315" y="386" fill="#888" text-anchor="middle" font-size="10">negotiation appended via encrypt_payload() — extends Noise hash chain</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="425" fill="#777" font-size="10" text-anchor="middle">base: 118 bytes · with FMP negotiation: 144 bytes</text>
|
||||
<text x="310" y="442" fill="#777" font-size="10" text-anchor="middle">pattern: ← e, ee, s, es</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 5.4 KiB |
@@ -0,0 +1,72 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 426" font-family="monospace" font-size="13">
|
||||
<!-- Background -->
|
||||
<rect width="580" height="426" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">Noise XX Message 3 — phase 0x3 (85–111 bytes)</text>
|
||||
|
||||
<!-- Row 0: Common prefix (bytes 0-3) -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
|
||||
<rect x="55" y="36" width="95" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="102" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ver</text>
|
||||
<text x="102" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">4 bits</text>
|
||||
|
||||
<rect x="150" y="36" width="95" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="197" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">phase</text>
|
||||
<text x="197" y="78" fill="#8af8c8" text-anchor="middle" font-size="10">4 bits</text>
|
||||
|
||||
<rect x="245" y="36" width="130" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="310" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">flags</text>
|
||||
<text x="310" y="78" fill="#f8c88a" text-anchor="middle" font-size="10">1 byte</text>
|
||||
|
||||
<rect x="375" y="36" width="200" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="475" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">payload_len</text>
|
||||
<text x="475" y="78" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE</text>
|
||||
|
||||
<!-- Row 1: sender_idx + receiver_idx (bytes 4-11) -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–11</text>
|
||||
|
||||
<rect x="55" y="88" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">sender_idx</text>
|
||||
<text x="185" y="130" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<rect x="315" y="88" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="112" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">receiver_idx</text>
|
||||
<text x="445" y="130" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Noise XX msg3 bracket -->
|
||||
<text x="10" y="240" fill="#777" font-size="10" text-anchor="middle" transform="rotate(-90, 10, 240)">Noise XX msg3 (73 bytes base)</text>
|
||||
<line x1="18" y1="140" x2="18" y2="348" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="140" x2="23" y2="140" stroke="#555" stroke-width="1"/>
|
||||
<line x1="18" y1="348" x2="23" y2="348" stroke="#555" stroke-width="1"/>
|
||||
|
||||
<!-- Row 2: encrypted_static (bytes 12-60, 49 bytes) -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12–60</text>
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="164" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_static</text>
|
||||
<text x="315" y="182" fill="#f88aaa" text-anchor="middle" font-size="10">49 bytes (static key 33 + AEAD tag 16)</text>
|
||||
|
||||
<!-- Row 3: encrypted_epoch (bytes 61-84, 24 bytes) -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">61–84</text>
|
||||
|
||||
<rect x="55" y="192" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="216" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted_epoch</text>
|
||||
<text x="315" y="234" fill="#d8a0f8" text-anchor="middle" font-size="10">24 bytes (epoch 8 + AEAD tag 16)</text>
|
||||
|
||||
<!-- Row 4: negotiation payload (variable, optional) -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">85+</text>
|
||||
|
||||
<rect x="55" y="244" width="520" height="52" fill="#2d4a4a" stroke="#4a9090" stroke-width="1.5" rx="3" stroke-dasharray="4,3"/>
|
||||
<text x="315" y="268" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">encrypted negotiation payload</text>
|
||||
<text x="315" y="286" fill="#8ad8d8" text-anchor="middle" font-size="10">26 bytes typical (10 payload + 16 AEAD tag)</text>
|
||||
|
||||
<!-- Row 5: explanation -->
|
||||
<rect x="55" y="310" width="520" height="38" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="315" y="334" fill="#888" text-anchor="middle" font-size="10">negotiation appended via encrypt_payload() — extends Noise hash chain</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="378" fill="#777" font-size="10" text-anchor="middle">base: 85 bytes · with FMP negotiation: 111 bytes</text>
|
||||
<text x="310" y="395" fill="#777" font-size="10" text-anchor="middle">pattern: → s, se</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -3,104 +3,95 @@
|
||||
<rect width="580" height="634" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">ReceiverReport (0x02) — 68 bytes</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">ReceiverReport (0x02) — 54 bytes</text>
|
||||
|
||||
<!-- Row 0 (0–3): msg_type + reserved -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
<!-- Row 0 (0-3): msg_type + format_version + total_length -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
|
||||
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
|
||||
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x02</text>
|
||||
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">msg_type</text>
|
||||
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x02</text>
|
||||
|
||||
<rect x="185" y="36" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="380" y="66" fill="#666" text-anchor="middle" font-size="12">reserved (3 bytes zero)</text>
|
||||
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="250" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">fmt_ver</text>
|
||||
<text x="250" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
|
||||
|
||||
<!-- Row 1 (4–11): highest_counter -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–11</text>
|
||||
<rect x="315" y="36" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">total_length</text>
|
||||
<text x="445" y="74" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE (= 50)</text>
|
||||
|
||||
<!-- Row 1 (4-7): timestamp_echo -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–7</text>
|
||||
|
||||
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">highest_counter</text>
|
||||
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp_echo</text>
|
||||
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">4 bytes LE (echoed sender timestamp for RTT)</text>
|
||||
|
||||
<!-- Row 2 (12–19): cumulative_packets_recv -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12–19</text>
|
||||
<!-- Row 2 (8-9): dwell_time -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8–9</text>
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_recv</text>
|
||||
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dwell_time</text>
|
||||
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">2 bytes LE (ms between receive and echo)</text>
|
||||
|
||||
<!-- Row 3 (20–27): cumulative_bytes_recv -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">20–27</text>
|
||||
<!-- Row 3 (10-17): highest_counter -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">10–17</text>
|
||||
|
||||
<rect x="55" y="192" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_recv</text>
|
||||
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">highest_counter</text>
|
||||
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
|
||||
<!-- Row 4 (28–33): timestamp_echo + dwell_time -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">28–33</text>
|
||||
<!-- Row 4 (18-25): cumulative_packets_recv -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">18–25</text>
|
||||
|
||||
<rect x="55" y="244" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">timestamp_echo</text>
|
||||
<text x="185" y="284" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
<rect x="55" y="244" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_recv</text>
|
||||
<text x="315" y="284" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
|
||||
<rect x="315" y="244" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">dwell_time</text>
|
||||
<text x="445" y="284" fill="#8af8f8" text-anchor="middle" font-size="10">2 bytes LE ms</text>
|
||||
<!-- Row 5 (26-33): cumulative_bytes_recv -->
|
||||
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">26–33</text>
|
||||
|
||||
<!-- Row 5 (34–39): max_burst_loss + mean_burst_loss + reserved -->
|
||||
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">34–39</text>
|
||||
<rect x="55" y="296" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_recv</text>
|
||||
<text x="315" y="336" fill="#8af8f8" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
|
||||
<rect x="55" y="296" width="173" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
|
||||
<text x="141" y="318" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">max_burst</text>
|
||||
<text x="141" y="336" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes LE</text>
|
||||
<!-- Row 6 (34-41): jitter + ecn_ce_count -->
|
||||
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">34–41</text>
|
||||
|
||||
<rect x="228" y="296" width="174" height="52" fill="#5a2d3d" stroke="#d94a6a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">mean_burst</text>
|
||||
<text x="315" y="336" fill="#f88aaa" text-anchor="middle" font-size="10">2 bytes u8.8</text>
|
||||
|
||||
<rect x="402" y="296" width="173" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="488" y="322" fill="#666" text-anchor="middle" font-size="10">reserved</text>
|
||||
|
||||
<!-- Row 6 (40–47): jitter + ecn_ce_count -->
|
||||
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">40–47</text>
|
||||
|
||||
<rect x="55" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
|
||||
<rect x="55" y="348" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">jitter</text>
|
||||
<text x="185" y="388" fill="#f8d88a" text-anchor="middle" font-size="10">4 bytes LE µs</text>
|
||||
<text x="185" y="388" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE (µs)</text>
|
||||
|
||||
<rect x="315" y="348" width="260" height="52" fill="#4a3d2d" stroke="#d9b04a" stroke-width="1.5" rx="3"/>
|
||||
<rect x="315" y="348" width="260" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">ecn_ce_count</text>
|
||||
<text x="445" y="388" fill="#f8d88a" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
<text x="445" y="388" fill="#8af8c8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 7 (48–55): owd_trend + burst_loss_count -->
|
||||
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">48–55</text>
|
||||
<!-- Row 7 (42-49): owd_trend + burst_loss_count -->
|
||||
<text x="50" y="426" fill="#666" font-size="10" text-anchor="end">42–49</text>
|
||||
|
||||
<rect x="55" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
|
||||
<rect x="55" y="400" width="260" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="422" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">owd_trend</text>
|
||||
<text x="185" y="440" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes i32 LE µs/s</text>
|
||||
<text x="185" y="440" fill="#f8c88a" text-anchor="middle" font-size="10">i32 LE (µs/s)</text>
|
||||
|
||||
<rect x="315" y="400" width="260" height="52" fill="#3d4a2d" stroke="#8ad94a" stroke-width="1.5" rx="3"/>
|
||||
<rect x="315" y="400" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="422" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">burst_loss_count</text>
|
||||
<text x="445" y="440" fill="#c8f88a" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
<text x="445" y="440" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 8 (56–63): cumulative_reorder_count + interval_packets_recv -->
|
||||
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">56–63</text>
|
||||
<!-- Row 8 (50-53): cumulative_reorder_count -->
|
||||
<text x="50" y="478" fill="#666" font-size="10" text-anchor="end">50–53</text>
|
||||
|
||||
<rect x="55" y="452" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="474" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">reorder_count</text>
|
||||
<text x="185" y="492" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE cumulative</text>
|
||||
<rect x="55" y="452" width="520" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="474" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_reorder_count</text>
|
||||
<text x="315" y="492" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<rect x="315" y="452" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="474" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">interval_pkts_recv</text>
|
||||
<text x="445" y="492" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
<!-- Forward compat note -->
|
||||
<rect x="55" y="518" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="315" y="538" fill="#888" text-anchor="middle" font-size="10">decoders skip trailing bytes beyond total_length (forward compatibility)</text>
|
||||
|
||||
<!-- Row 9 (64–67): interval_bytes_recv -->
|
||||
<text x="50" y="530" fill="#666" font-size="10" text-anchor="end">64–67</text>
|
||||
|
||||
<rect x="55" y="504" width="260" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="526" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">interval_bytes_recv</text>
|
||||
<text x="185" y="544" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
<!-- Removed fields note -->
|
||||
<rect x="55" y="558" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="315" y="578" fill="#666" text-anchor="middle" font-size="10">removed from v0.2: max_burst_loss, mean_burst_loss, interval_packets/bytes_recv</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="586" fill="#777" font-size="10" text-anchor="middle">total: 68 bytes</text>
|
||||
<text x="310" y="618" fill="#777" font-size="10" text-anchor="middle">total: 54 bytes · format_version = 0 · total_length = 50</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 6.8 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -1,66 +1,50 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 478" font-family="monospace" font-size="13">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 580 322" font-family="monospace" font-size="13">
|
||||
<!-- Background -->
|
||||
<rect width="580" height="478" fill="#1a1a2e" rx="4"/>
|
||||
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SenderReport (0x01) — 48 bytes</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SenderReport (0x01) — 20 bytes</text>
|
||||
|
||||
<!-- Row 0 (0–3): msg_type + reserved -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
<!-- Row 0 (0-3): msg_type + format_version + total_length -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
|
||||
<rect x="55" y="36" width="130" height="52" fill="#2d4a7a" stroke="#4a90d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="120" y="60" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">msg_type</text>
|
||||
<text x="120" y="78" fill="#8ab4f8" text-anchor="middle" font-size="10">0x01</text>
|
||||
<text x="120" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">msg_type</text>
|
||||
<text x="120" y="74" fill="#8ab4f8" text-anchor="middle" font-size="10">0x01</text>
|
||||
|
||||
<rect x="185" y="36" width="390" height="52" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="380" y="66" fill="#666" text-anchor="middle" font-size="12">reserved (3 bytes zero)</text>
|
||||
<rect x="185" y="36" width="130" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="250" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">fmt_ver</text>
|
||||
<text x="250" y="74" fill="#8af8c8" text-anchor="middle" font-size="10">1 byte</text>
|
||||
|
||||
<!-- Row 1 (4–11): interval_start_counter -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–11</text>
|
||||
<rect x="315" y="36" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="56" fill="#e0e0e0" text-anchor="middle" font-size="11" font-weight="bold">total_length</text>
|
||||
<text x="445" y="74" fill="#d8a0f8" text-anchor="middle" font-size="10">2 bytes LE (= 16)</text>
|
||||
|
||||
<!-- Row 1 (4-7): interval_packets_sent -->
|
||||
<text x="50" y="114" fill="#666" font-size="10" text-anchor="end">4–7</text>
|
||||
|
||||
<rect x="55" y="88" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_start_counter</text>
|
||||
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
<text x="315" y="110" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_packets_sent</text>
|
||||
<text x="315" y="128" fill="#f8c88a" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 2 (12–19): interval_end_counter -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">12–19</text>
|
||||
<!-- Row 2 (8-11): interval_bytes_sent -->
|
||||
<text x="50" y="166" fill="#666" font-size="10" text-anchor="end">8–11</text>
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#5a3d2d" stroke="#d9904a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_end_counter</text>
|
||||
<text x="315" y="180" fill="#f8c88a" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
<rect x="55" y="140" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_bytes_sent</text>
|
||||
<text x="315" y="180" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 3 (20–27): interval_start_timestamp + interval_end_timestamp -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">20–27</text>
|
||||
<!-- Row 3 (12-19): cumulative_packets_sent -->
|
||||
<text x="50" y="218" fill="#666" font-size="10" text-anchor="end">12–19</text>
|
||||
|
||||
<rect x="55" y="192" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="185" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_start_ts</text>
|
||||
<text x="185" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
<rect x="55" y="192" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_sent</text>
|
||||
<text x="315" y="232" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
|
||||
<rect x="315" y="192" width="260" height="52" fill="#4a2d5a" stroke="#b04ad9" stroke-width="1.5" rx="3"/>
|
||||
<text x="445" y="214" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_end_ts</text>
|
||||
<text x="445" y="232" fill="#d8a0f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 4 (28–31): interval_bytes_sent -->
|
||||
<text x="50" y="270" fill="#666" font-size="10" text-anchor="end">28–31</text>
|
||||
|
||||
<rect x="55" y="244" width="520" height="52" fill="#2d5a5a" stroke="#4ad9d9" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="266" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">interval_bytes_sent</text>
|
||||
<text x="315" y="284" fill="#8af8f8" text-anchor="middle" font-size="10">4 bytes LE</text>
|
||||
|
||||
<!-- Row 5 (32–39): cumulative_packets_sent -->
|
||||
<text x="50" y="322" fill="#666" font-size="10" text-anchor="end">32–39</text>
|
||||
|
||||
<rect x="55" y="296" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="318" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_packets_sent</text>
|
||||
<text x="315" y="336" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
|
||||
<!-- Row 6 (40–47): cumulative_bytes_sent -->
|
||||
<text x="50" y="374" fill="#666" font-size="10" text-anchor="end">40–47</text>
|
||||
|
||||
<rect x="55" y="348" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="370" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">cumulative_bytes_sent</text>
|
||||
<text x="315" y="388" fill="#8af8c8" text-anchor="middle" font-size="10">8 bytes LE</text>
|
||||
<!-- Forward compat note -->
|
||||
<rect x="55" y="258" width="520" height="32" fill="#1f1f3a" stroke="#444" stroke-width="1" rx="3"/>
|
||||
<text x="315" y="278" fill="#888" text-anchor="middle" font-size="10">decoders skip trailing bytes beyond total_length (forward compatibility)</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="422" fill="#777" font-size="10" text-anchor="middle">total: 48 bytes</text>
|
||||
<text x="310" y="314" fill="#777" font-size="10" text-anchor="middle">total: 20 bytes · format_version = 0 · total_length = 16</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 4.1 KiB After Width: | Height: | Size: 3.2 KiB |
@@ -3,7 +3,7 @@
|
||||
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionAck (phase 0x2) — Noise XK msg2</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionAck (phase 0x2) — Noise XX msg2</text>
|
||||
|
||||
<!-- Row 0 (0–3): FSP prefix -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
@@ -40,8 +40,8 @@
|
||||
|
||||
<rect x="185" y="232" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="380" y="254" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
|
||||
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg2 (57 bytes — ephemeral + epoch)</text>
|
||||
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XX msg2 (106+ bytes — ephemeral + static + epoch)</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~190 bytes (depth-dependent, carries both endpoints' coords)</text>
|
||||
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~240 bytes (depth-dependent, carries both endpoints' coords)</text>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -3,7 +3,7 @@
|
||||
<rect width="580" height="244" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionMsg3 (phase 0x3) — Noise XK msg3</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionMsg3 (phase 0x3) — Noise XX msg3</text>
|
||||
|
||||
<!-- Row 0 (0–3): FSP prefix -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
@@ -28,7 +28,7 @@
|
||||
|
||||
<rect x="55" y="140" width="520" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="315" y="162" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
|
||||
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg3 (73 bytes — encrypted static + encrypted epoch)</text>
|
||||
<text x="315" y="180" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XX msg3 (73+ bytes — encrypted static + epoch + optional negotiation)</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="218" fill="#777" font-size="10" text-anchor="middle">~80 bytes · no coordinates (both endpoints already have them)</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
@@ -3,7 +3,7 @@
|
||||
<rect width="580" height="322" fill="#1a1a2e" rx="4"/>
|
||||
|
||||
<!-- Title -->
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionSetup (phase 0x1) — Noise XK msg1</text>
|
||||
<text x="310" y="26" fill="#e0e0e0" text-anchor="middle" font-size="14" font-weight="bold">SessionSetup (phase 0x1) — Noise XX msg1</text>
|
||||
|
||||
<!-- Row 0 (0–3): FSP prefix -->
|
||||
<text x="50" y="62" fill="#666" font-size="10" text-anchor="end">0–3</text>
|
||||
@@ -40,7 +40,7 @@
|
||||
|
||||
<rect x="185" y="232" width="390" height="52" fill="#2d5a4a" stroke="#4ad99a" stroke-width="1.5" rx="3"/>
|
||||
<text x="380" y="254" fill="#e0e0e0" text-anchor="middle" font-size="12" font-weight="bold">handshake_payload</text>
|
||||
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XK msg1 (33 bytes — ephemeral key)</text>
|
||||
<text x="380" y="272" fill="#8af8c8" text-anchor="middle" font-size="10">Noise XX msg1 (33 bytes — ephemeral key)</text>
|
||||
|
||||
<!-- Total -->
|
||||
<text x="310" y="308" fill="#777" font-size="10" text-anchor="middle">typical ~170 bytes (depth-dependent)</text>
|
||||
|
||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 2.9 KiB |
@@ -245,7 +245,7 @@ for i in 0..hash_count:
|
||||
return true // Maybe present (possible false positive)
|
||||
```
|
||||
|
||||
Where `filter_bits = 8 × (512 << size_class)` — 8,192 for v1.
|
||||
Where `filter_bits = 8 × (512 << size_class)` — 8,192 for size_class 1 (default).
|
||||
|
||||
## Wire Format
|
||||
|
||||
@@ -254,27 +254,30 @@ FilterAnnounce messages are carried inside encrypted link-layer frames:
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | msg_type | 1 byte | 0x20 |
|
||||
| 1 | sequence | 8 bytes LE | Monotonic counter for freshness |
|
||||
| 9 | hash_count | 1 byte | Number of hash functions (5 in v1) |
|
||||
| 10 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
|
||||
| 11 | filter_bits | 1,024 bytes | Bloom filter bit array (v1) |
|
||||
| 1 | flags | 1 byte | Bit 0: delta (XOR diff), bits 1-7 reserved |
|
||||
| 2 | sequence | 8 bytes LE | Monotonic counter, per-peer |
|
||||
| 10 | base_seq | 8 bytes LE | Reference filter sequence for delta (0 if full) |
|
||||
| 18 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
|
||||
| 19 | compressed_payload | variable | RLE-encoded filter or XOR diff |
|
||||
|
||||
**v1 total**: 1,035 bytes payload, 1,064 bytes with link encryption
|
||||
overhead.
|
||||
Payloads are RLE-compressed: each run = `[count:2 LE][word:8 LE]`
|
||||
(10 bytes per run). XOR diffs between consecutive filters are mostly
|
||||
zero words, compressing to very few runs. A FilterNack (msg_type 0x21)
|
||||
requests full retransmission when a sequence gap is detected.
|
||||
|
||||
See [fips-wire-formats.md](fips-wire-formats.md) for the complete wire
|
||||
format reference.
|
||||
|
||||
## Scale and Size Classes
|
||||
|
||||
### v1 Scale Limits
|
||||
### Scale Limits
|
||||
|
||||
Coordinate-based tree distance checking ensures correct routing decisions
|
||||
at all network sizes — bloom filters are an optimization that narrows the
|
||||
set of peers considered, not a correctness requirement. As filters
|
||||
saturate, routing still works; it just evaluates more candidates per hop.
|
||||
|
||||
With the v1 mandatory 1 KB filter (size_class 1):
|
||||
With the default 1 KB filter (size_class 1):
|
||||
|
||||
- **Small networks (< 1,000 nodes)**: Both upward and downward filters
|
||||
are highly accurate (worst-case FPR < 1%). Filters effectively narrow
|
||||
@@ -295,34 +298,29 @@ With the v1 mandatory 1 KB filter (size_class 1):
|
||||
|
||||
### Size Class Table
|
||||
|
||||
| size_class | Bytes | Bits | Status |
|
||||
| ---------- | ----- | ---- | ------ |
|
||||
| 0 | 512 | 4,096 | Reserved |
|
||||
| 1 | 1,024 | 8,192 | **v1 (MUST use)** |
|
||||
| 2 | 2,048 | 16,384 | Reserved |
|
||||
| 3 | 4,096 | 32,768 | Reserved |
|
||||
| size_class | Bytes | Bits | Notes |
|
||||
| ---------- | ----- | ---- | ----- |
|
||||
| 0 | 512 | 4,096 | Minimum |
|
||||
| 1 | 1,024 | 8,192 | Default |
|
||||
| 2 | 2,048 | 16,384 | |
|
||||
| 3 | 4,096 | 32,768 | |
|
||||
| 4 | 8,192 | 65,536 | |
|
||||
| 5 | 16,384 | 131,072 | |
|
||||
| 6 | 32,768 | 262,144 | Maximum |
|
||||
|
||||
FMP v1 mandates size_class = 1. Nodes MUST use size_class = 1 and MUST
|
||||
reject FilterAnnounce messages with any other size_class. The size_class
|
||||
field is reserved in the wire format to support future protocol versions
|
||||
with larger default filter sizes.
|
||||
### Adaptive Sizing
|
||||
|
||||
### Scaling Strategy
|
||||
Filter size is a node property, not a link property. Each node selects
|
||||
its own size class based on outgoing filter fill ratio: step up above
|
||||
~20%, step down below ~5%, with hysteresis to prevent oscillation.
|
||||
Nodes near the root of the spanning tree — which carry larger combined
|
||||
filters — naturally upsize, while leaf and edge nodes stay small.
|
||||
|
||||
The 1 KB filter becomes a practical limitation beyond ~2,000 nodes. The
|
||||
size class mechanism provides the path forward: future FMP versions may
|
||||
use larger default filters (size_class 2 or 3) to support larger networks
|
||||
while remaining compatible with constrained nodes through folding.
|
||||
Size_class 2 (2 KB, 16,384 bits) would roughly double the practical
|
||||
network size limit.
|
||||
|
||||
The envisioned approach is that hub nodes near the root — which carry the
|
||||
largest downward filters — would use larger size classes, while leaf nodes
|
||||
and resource-constrained nodes continue with smaller filters. A node
|
||||
receiving a filter larger than its own size class folds it down locally.
|
||||
The mechanism by which heterogeneous filter sizes propagate through the
|
||||
tree is a future design direction not specified in v1. See
|
||||
[IDEA-0043](../../ideas/IDEA-0043-heterogeneous-filter-propagation.md).
|
||||
When a node receives a filter at a different size class than its own,
|
||||
it converts on receipt: larger filters are folded down, smaller filters
|
||||
are expanded via bit duplication. Routing queries use the peer's filter
|
||||
at its native (advertised) size for full resolution; conversion happens
|
||||
only when building the node's own outgoing filter.
|
||||
|
||||
### Folding
|
||||
|
||||
@@ -339,7 +337,7 @@ positions that folding produces.
|
||||
|
||||
| Feature | Status |
|
||||
| ------- | ------ |
|
||||
| 1 KB bloom filter (size_class 1) | **Implemented** |
|
||||
| Variable-size bloom filters (512 B – 32 KB) | **Implemented** |
|
||||
| 5 hash functions | **Implemented** |
|
||||
| Split-horizon filter computation | **Implemented** |
|
||||
| Tree-only merge propagation | **Implemented** |
|
||||
@@ -347,6 +345,10 @@ positions that folding produces.
|
||||
| Per-peer filter maintenance | **Implemented** |
|
||||
| Event-driven updates | **Implemented** |
|
||||
| 500ms rate limiting | **Implemented** |
|
||||
| Delta compression (XOR diff + RLE) | **Implemented** |
|
||||
| FilterNack sequence recovery | **Implemented** |
|
||||
| Adaptive sizing (fill-ratio heuristic) | **Implemented** |
|
||||
| Fold/duplicate size conversion | **Implemented** |
|
||||
| FilterAnnounce gossip (all peers) | **Implemented** |
|
||||
| Filter cardinality logging | **Implemented** |
|
||||
| Size class negotiation | Future direction |
|
||||
|
||||
@@ -90,7 +90,8 @@ to the highest-priority config file for operator visibility, even in ephemeral m
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `node.leaf_only` | bool | `false` | Leaf-only mode: node does not forward traffic or participate in routing |
|
||||
| `node.disable_routing` | bool | `false` | Non-routing mode: participates in spanning tree but does not forward transit traffic or send bloom filters |
|
||||
| `node.leaf_only` | bool | `false` | Leaf mode: single upstream peer, no tree/bloom/transit participation. Implies `disable_routing: true` |
|
||||
| `node.tick_interval_secs` | u64 | `1` | Periodic maintenance tick interval (retry checks, timeout cleanup, tree refresh) |
|
||||
| `node.base_rtt_ms` | u64 | `100` | Initial RTT estimate for new links before measurements converge |
|
||||
| `node.heartbeat_interval_secs` | u64 | `10` | Heartbeat send interval per peer for liveness detection |
|
||||
@@ -109,7 +110,7 @@ Controls capacity for connections, peers, and links.
|
||||
|
||||
### Rate Limiting (`node.rate_limit.*`)
|
||||
|
||||
Handshake rate limiting protects against DoS on the Noise IK handshake path.
|
||||
Handshake rate limiting protects against DoS on the Noise XX handshake path.
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
@@ -204,7 +205,7 @@ stays set for all subsequent hops to the destination.
|
||||
### Rekey (`node.rekey.*`)
|
||||
|
||||
Controls periodic Noise rekey for forward secrecy. When enabled, both FMP
|
||||
(link-layer IK) and FSP (session-layer XK) sessions perform fresh Diffie-Hellman
|
||||
(link-layer XX) and FSP (session-layer XX) sessions perform fresh Diffie-Hellman
|
||||
key exchanges after a time or message count threshold, whichever comes first.
|
||||
A 10-second drain window keeps the old session active for decryption during
|
||||
cutover.
|
||||
@@ -238,7 +239,7 @@ Metrics Measurement Protocol for per-peer link measurement. See
|
||||
|
||||
| Parameter | Type | Default | Description |
|
||||
|-----------|------|---------|-------------|
|
||||
| `node.mmp.mode` | string | `"full"` | Operating mode: `full` (sender + receiver reports), `lightweight` (receiver reports only), or `minimal` (spin bit + CE echo only, no reports) |
|
||||
| `node.mmp.mode` | string | `"full"` | Operating mode: `full` (sender + receiver reports), `lightweight` (receiver reports only), or `minimal` (CE echo only, no reports) |
|
||||
| `node.mmp.log_interval_secs` | u64 | `30` | Periodic operator log interval for link metrics |
|
||||
| `node.mmp.owd_window_size` | usize | `32` | One-way delay trend ring buffer size |
|
||||
|
||||
@@ -666,6 +667,7 @@ node:
|
||||
identity:
|
||||
nsec: null # secret key in nsec or hex (null = depends on persistent)
|
||||
persistent: false # true = load/save fips.key; false = ephemeral each start
|
||||
disable_routing: false
|
||||
leaf_only: false
|
||||
tick_interval_secs: 1
|
||||
base_rtt_ms: 100
|
||||
|
||||
@@ -134,7 +134,7 @@ See [fips-transport-layer.md](fips-transport-layer.md) for the transport layer
|
||||
specification.
|
||||
|
||||
**FIPS Mesh Protocol (FMP)**: Manages peer connections, authenticates peers
|
||||
via Noise IK handshakes, and encrypts all traffic on each link. FMP is where
|
||||
via Noise XX handshakes, and encrypts all traffic on each link. FMP is where
|
||||
the mesh organizes itself — nodes exchange spanning tree announcements and
|
||||
bloom filters with their direct peers, and FMP makes forwarding decisions
|
||||
for transit traffic. FMP provides authenticated, encrypted forwarding to FSP
|
||||
@@ -203,7 +203,7 @@ same keypair.
|
||||
|
||||

|
||||
|
||||
The pubkey is the node's cryptographic identity, used in Noise IK handshakes
|
||||
The pubkey is the node's cryptographic identity, used in Noise XX handshakes
|
||||
for both link and session encryption. It is never exposed beyond the
|
||||
endpoints of an encrypted channel. The node_addr, a one-way SHA-256 hash
|
||||
truncated to 16 bytes, serves as the routing identifier in packet headers
|
||||
@@ -243,31 +243,27 @@ FIPS uses independent encryption at two protocol layers:
|
||||
|
||||
| Layer | Scope | Pattern | Purpose |
|
||||
| ----- | ----- | ------- | ------- |
|
||||
| **FMP (Mesh)** | Hop-by-hop | Noise IK | Encrypt all traffic on each peer link |
|
||||
| **FSP (Session)** | End-to-end | Noise XK | Encrypt application payload between endpoints |
|
||||
| **FMP (Mesh)** | Hop-by-hop | Noise XX | Encrypt all traffic on each peer link |
|
||||
| **FSP (Session)** | End-to-end | Noise XX | Encrypt application payload between endpoints |
|
||||
|
||||
### Link Layer (Hop-by-Hop)
|
||||
|
||||
When two nodes establish a direct connection, they perform a [Noise
|
||||
IK](https://noiseprotocol.org/) handshake. This authenticates both parties
|
||||
XX](https://noiseprotocol.org/) handshake. This authenticates both parties
|
||||
and establishes symmetric keys for encrypting all traffic on that link.
|
||||
Every packet between direct peers is encrypted — gossip messages, routing
|
||||
queries, and forwarded session datagrams alike.
|
||||
|
||||
The IK pattern is used because outbound connections know the peer's npub
|
||||
from configuration, while inbound connections learn the initiator's identity
|
||||
from the first handshake message.
|
||||
queries, and forwarded session datagrams alike. Neither side requires prior
|
||||
knowledge of the other's static key — both identities are revealed during
|
||||
the three-message handshake, along with a protocol negotiation payload
|
||||
that enables rolling upgrades.
|
||||
|
||||
### Session Layer (End-to-End)
|
||||
|
||||
FIPS establishes end-to-end encrypted sessions between any two communicating
|
||||
nodes using Noise XK, regardless of how many hops separate them. The
|
||||
initiator knows the destination's npub (required for XK's pre-message);
|
||||
the responder learns the initiator's identity from the third handshake
|
||||
message. Unlike the link-layer IK pattern where the initiator's identity
|
||||
is revealed in msg1, XK delays identity disclosure until msg3, providing
|
||||
stronger initiator identity protection for traffic traversing untrusted
|
||||
intermediate nodes.
|
||||
nodes using Noise XX, regardless of how many hops separate them. The same
|
||||
three-message XX pattern is used at both layers — neither side reveals its
|
||||
identity until msg2 (responder) or msg3 (initiator), providing mutual
|
||||
identity protection for traffic traversing untrusted intermediate nodes.
|
||||
|
||||
A packet from A to D through intermediate nodes B and C:
|
||||
|
||||
@@ -455,7 +451,7 @@ MMP operates in three modes. **Full** mode exchanges both SenderReports and
|
||||
ReceiverReports to compute all metrics including RTT. **Lightweight** mode
|
||||
exchanges only ReceiverReports, providing loss and jitter but not RTT — useful
|
||||
for constrained links. **Minimal** mode disables reports entirely, relying
|
||||
only on spin bit and congestion echo flags in the frame header.
|
||||
only on congestion echo flags in the frame header.
|
||||
|
||||
Reports are sent at RTT-adaptive intervals (clamped to 100 ms–2 s), so
|
||||
high-latency links don't generate excessive measurement traffic while
|
||||
@@ -472,7 +468,7 @@ evaluates `effective_depth = depth + link_cost` using
|
||||
`find_next_hop()` candidate ranking for data forwarding.
|
||||
|
||||
See [fips-mesh-layer.md](fips-mesh-layer.md) for MMP operating modes, report
|
||||
scheduling, and the spin bit design.
|
||||
scheduling, and RTT measurement.
|
||||
|
||||
---
|
||||
|
||||
@@ -488,7 +484,7 @@ A **transport** is a driver for a particular medium. A **link** is a peer
|
||||
connection established over a transport. Transport addresses (IP:port, MAC
|
||||
address, .onion) are opaque to all layers above FMP — they exist only to
|
||||
deliver datagrams and are discarded once FMP has authenticated the peer via
|
||||
the Noise IK handshake. From that point on, the peer is identified solely by
|
||||
the Noise XX handshake. From that point on, the peer is identified solely by
|
||||
its cryptographic identity.
|
||||
|
||||
Transports fall into three categories based on their connectivity model:
|
||||
@@ -543,7 +539,7 @@ different layer of the protocol.
|
||||
|
||||
A passive observer on the underlying transport — someone monitoring a WiFi
|
||||
network, tapping an Ethernet segment, or inspecting UDP traffic — sees only
|
||||
encrypted packets. The FMP link-layer Noise IK session encrypts all traffic
|
||||
encrypted packets. The FMP link-layer Noise XX session encrypts all traffic
|
||||
between direct peers, including routing gossip and forwarded session
|
||||
datagrams. The observer can infer timing, packet sizes, and which transport
|
||||
endpoints are exchanging traffic, but cannot read content or determine
|
||||
@@ -555,7 +551,7 @@ infer communication relationships — is not defended against (see
|
||||
### Active Attackers on the Transport
|
||||
|
||||
An adversary who can inject, modify, drop, or replay packets on the
|
||||
transport is also defeated by the FMP link-layer Noise IK session. Mutual
|
||||
transport is also defeated by the FMP link-layer Noise XX session. Mutual
|
||||
authentication prevents impersonation, AEAD encryption detects tampering,
|
||||
and counter-based nonces with a sliding replay window reject replayed
|
||||
packets.
|
||||
@@ -565,7 +561,7 @@ packets.
|
||||
The most important adversary class is the operators of other nodes in the
|
||||
mesh — the peers that forward your traffic. FIPS treats every intermediate
|
||||
router as potentially adversarial. The FSP session layer establishes a
|
||||
completely independent Noise XK session between the communicating endpoints,
|
||||
completely independent Noise XX session between the communicating endpoints,
|
||||
so intermediate nodes cannot read application payloads even though they
|
||||
decrypt and re-encrypt the link-layer envelope at each hop.
|
||||
|
||||
@@ -678,20 +674,16 @@ confidentiality and integrity rather than hiding traffic patterns.
|
||||
### Noise Protocol Framework
|
||||
|
||||
FIPS uses the [Noise Protocol Framework](https://noiseprotocol.org/) at both
|
||||
protocol layers, with different handshake patterns chosen for each layer's
|
||||
threat model. FMP link encryption uses **Noise IK**, providing mutual
|
||||
authentication with a single round trip where the initiator knows the
|
||||
responder's static key in advance.
|
||||
[WireGuard](https://www.wireguard.com/) uses the same IK base pattern
|
||||
(extended with a pre-shared key as IKpsk2) for VPN tunnels. FSP session
|
||||
encryption uses **Noise XK**, the same pattern used by the
|
||||
[Lightning Network](https://github.com/lightning/bolts/blob/master/08-transport.md),
|
||||
where the initiator's static key is transmitted in a third message rather
|
||||
than the first. XK provides stronger initiator identity hiding at the cost
|
||||
of an additional round trip — a worthwhile tradeoff for session-layer traffic
|
||||
that traverses untrusted intermediate nodes. At the link layer, where both
|
||||
peers are configured and directly connected, IK's single round trip is
|
||||
preferred.
|
||||
protocol layers with the **Noise XX** handshake pattern. XX requires no
|
||||
prior knowledge of the peer's static key — both identities are revealed
|
||||
during a three-message handshake (responder in msg2, initiator in msg3).
|
||||
This enables anonymous peer discovery on shared-media transports and
|
||||
allows a protocol negotiation payload to be exchanged alongside the
|
||||
handshake, supporting rolling protocol upgrades without extra round trips.
|
||||
[WireGuard](https://www.wireguard.com/) uses the related IK pattern for
|
||||
VPN tunnels;
|
||||
[Lightning Network](https://github.com/lightning/bolts/blob/master/08-transport.md)
|
||||
uses XK for transport encryption.
|
||||
|
||||
### Index-Based Session Dispatch
|
||||
|
||||
@@ -728,13 +720,6 @@ computation used in TCP for retransmission timeout calculation since 1988.
|
||||
MMP derives RTT from timestamp-echo in ReceiverReports with dwell-time
|
||||
compensation, rather than from packet round-trips.
|
||||
|
||||
The spin bit in the FMP frame header follows the
|
||||
[QUIC](https://www.rfc-editor.org/rfc/rfc9000) spin bit
|
||||
([RFC 9312](https://www.rfc-editor.org/rfc/rfc9312)) — a single bit that
|
||||
alternates each round trip, enabling passive latency measurement. FIPS
|
||||
implements the spin bit state machine but relies on timestamp-echo for SRTT,
|
||||
as irregular mesh traffic makes spin bit RTT unreliable.
|
||||
|
||||
The Expected Transmission Count (ETX) metric, computed from bidirectional
|
||||
delivery ratios, was introduced by
|
||||
[De Couto et al. (2003)](https://pdos.csail.mit.edu/papers/grid:mobicom03/paper.pdf)
|
||||
@@ -809,7 +794,6 @@ of self-sovereign identity systems. No novel cryptography is introduced.
|
||||
- [WireGuard Whitepaper](https://www.wireguard.com/papers/wireguard.pdf)
|
||||
- [Lightning Network BOLT #8 — Transport](https://github.com/lightning/bolts/blob/master/08-transport.md)
|
||||
- [QUIC (RFC 9000)](https://www.rfc-editor.org/rfc/rfc9000)
|
||||
- [QUIC Spin Bit (RFC 9312)](https://www.rfc-editor.org/rfc/rfc9312)
|
||||
- [RTCP (RFC 3550)](https://www.rfc-editor.org/rfc/rfc3550)
|
||||
- [TCP SRTT / RTO (RFC 6298)](https://www.rfc-editor.org/rfc/rfc6298)
|
||||
- [ECN (RFC 3168)](https://www.rfc-editor.org/rfc/rfc3168)
|
||||
|
||||
@@ -9,7 +9,7 @@ the mesh self-organizes, and where forwarding decisions are made.
|
||||
|
||||
FMP manages direct peer connections over transports. When a transport delivers
|
||||
a datagram from an unknown address, FMP authenticates the sender through a
|
||||
Noise IK handshake, establishing a cryptographic link. Once authenticated, the
|
||||
Noise XX 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.
|
||||
@@ -82,7 +82,7 @@ for the encrypted frame wrapper: 16-byte outer header + 5-byte inner header +
|
||||
### Connection Lifecycle
|
||||
|
||||
For connection-oriented transports, the transport must establish the underlying
|
||||
connection before FMP can begin the Noise IK handshake. For connectionless
|
||||
connection before FMP can begin the Noise XX handshake. For connectionless
|
||||
transports, datagrams can flow immediately.
|
||||
|
||||
### Endpoint Discovery (Optional)
|
||||
@@ -94,46 +94,52 @@ through configuration.
|
||||
|
||||
## Peer Authentication
|
||||
|
||||
### Noise IK Handshake
|
||||
### Noise XX Handshake
|
||||
|
||||
Every peer connection begins with a Noise IK handshake that mutually
|
||||
Every peer connection begins with a Noise XX handshake that mutually
|
||||
authenticates both parties and establishes symmetric keys for link encryption.
|
||||
|
||||
The IK pattern is chosen because:
|
||||
The XX 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
|
||||
- **Neither side** requires prior knowledge of the other's static public key
|
||||
- The **responder** reveals its identity in msg2; the **initiator** reveals
|
||||
its identity in msg3
|
||||
- A protocol negotiation payload (version range, feature bitfield, TLV
|
||||
extensions) is appended to msg2 and msg3, enabling rolling protocol
|
||||
upgrades without additional round-trips
|
||||
|
||||
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.
|
||||
After the three-message handshake completes, both parties share symmetric
|
||||
session keys derived from three DH operations (ee, es, se). The handshake
|
||||
provides mutual authentication, forward secrecy, and identity hiding for
|
||||
both parties until they choose to reveal.
|
||||
|
||||
### Epoch Exchange and Peer Restart Detection
|
||||
|
||||
Both IK handshake messages carry an encrypted epoch payload — an 8-byte
|
||||
random value generated once at node startup:
|
||||
XX handshake messages msg2 and msg3 carry an encrypted epoch payload — an
|
||||
8-byte random value generated once at node startup:
|
||||
|
||||
- **msg1**: Ephemeral key (33 bytes) + encrypted static key (49 bytes) +
|
||||
encrypted epoch (24 bytes) = 106 bytes total
|
||||
- **msg2**: Ephemeral key (33 bytes) + encrypted epoch (24 bytes) = 57 bytes
|
||||
total
|
||||
- **msg1**: Ephemeral key only (33 bytes) — no identity or epoch
|
||||
- **msg2**: Ephemeral key (33 bytes) + encrypted static key (49 bytes) +
|
||||
encrypted epoch (24 bytes) = 106 bytes base, plus negotiation payload
|
||||
- **msg3**: Encrypted static key (49 bytes) + encrypted epoch (24 bytes)
|
||||
= 73 bytes base, plus negotiation payload
|
||||
|
||||
The encrypted epoch (EPOCH_ENCRYPTED_SIZE = 24 bytes) consists of the
|
||||
8-byte epoch value plus a 16-byte AEAD tag.
|
||||
|
||||
On reconnection, each peer compares the received epoch with the previously
|
||||
stored epoch for that peer. An epoch mismatch indicates the peer has
|
||||
restarted (generated a new epoch), triggering full link re-establishment
|
||||
rather than treating the handshake as a simple reconnection. This prevents
|
||||
stale session state from persisting across restarts.
|
||||
Because msg1 carries no identity, restart detection is deferred: the
|
||||
initiator checks the responder's epoch after msg2, and the responder
|
||||
checks the initiator's epoch after msg3. An epoch mismatch indicates the
|
||||
peer has restarted, triggering full link re-establishment rather than
|
||||
treating the handshake as a simple reconnection. This prevents stale
|
||||
session state from persisting across restarts.
|
||||
|
||||
### Identity Binding
|
||||
|
||||
The Noise handshake binds the link to the peer's cryptographic identity. After
|
||||
handshake completion:
|
||||
The Noise handshake binds the link to the peer's cryptographic identity.
|
||||
With XX, identity confirmation happens at different points: the initiator
|
||||
learns the responder's identity from msg2, and the responder learns the
|
||||
initiator's identity from msg3. 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)
|
||||
@@ -143,8 +149,12 @@ handshake completion:
|
||||
|
||||
### Reconnection
|
||||
|
||||
When a Noise IK msg1 arrives from a peer that already has an authenticated
|
||||
link, FMP accepts the new handshake alongside the existing session. If the new
|
||||
When a Noise XX msg1 arrives from an address that already has an
|
||||
authenticated link, FMP accepts the new handshake alongside the existing
|
||||
session. With XX, the peer's identity is not known at msg1 time — FMP can
|
||||
only detect the duplicate by transport address. Identity-based checks
|
||||
(restart detection, rekey recognition, cross-connection resolution) are
|
||||
deferred to msg3 when the initiator's identity is revealed. 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.
|
||||
@@ -163,7 +173,7 @@ The auto-reconnect path:
|
||||
3. If eligible, the peer is fed into the retry system with unlimited retries
|
||||
and exponential backoff (same base interval and max backoff as startup
|
||||
retries, configured via `node.retry.*`)
|
||||
4. On each retry tick, a fresh Noise IK handshake is initiated toward the
|
||||
4. On each retry tick, a fresh Noise XX handshake is initiated toward the
|
||||
peer's configured transport addresses
|
||||
|
||||
Auto-reconnect only applies to peers in the static peer list with
|
||||
@@ -173,8 +183,8 @@ config) is responsible for re-establishing the link.
|
||||
|
||||
### Handshake Message Retry
|
||||
|
||||
Both link-layer (Noise IK msg1/msg2) and session-layer (SessionSetup/
|
||||
SessionAck) handshakes use message-level retry with exponential backoff
|
||||
Both link-layer (Noise XX msg1/msg2/msg3) and session-layer (SessionSetup/
|
||||
SessionAck/SessionMsg3) handshakes use message-level retry with exponential backoff
|
||||
within the handshake timeout window. This handles packet loss on the
|
||||
underlying transport without waiting for the full handshake timeout to
|
||||
expire.
|
||||
@@ -446,7 +456,7 @@ MMP supports three modes, configured via `node.mmp.mode`:
|
||||
| ---- | ----------------- | ----------------- |
|
||||
| **Full** (default) | SenderReport + ReceiverReport | All metrics including RTT, loss, jitter, goodput, OWD trend |
|
||||
| **Lightweight** | ReceiverReport only | Loss (from counter gaps), jitter, OWD trend. No RTT. |
|
||||
| **Minimal** | None | Spin bit and CE echo flags only. No computed metrics. |
|
||||
| **Minimal** | None | CE echo flags only. No computed metrics. |
|
||||
|
||||
### Report Scheduling
|
||||
|
||||
@@ -454,16 +464,10 @@ Reports are sent at RTT-adaptive intervals, clamped to [100ms, 2s]. A
|
||||
cold-start interval of 500ms is used before SRTT converges. The interval
|
||||
formula is `clamp(2 × SRTT, 100ms, 2000ms)`.
|
||||
|
||||
### Spin Bit and RTT
|
||||
### RTT Measurement
|
||||
|
||||
The SP (spin bit) flag in the FMP inner header follows the QUIC spin bit
|
||||
pattern: reflected on receive, toggled on send when the reflected value
|
||||
matches the last sent value. The spin bit state machine runs for TX
|
||||
reflection, but **RTT samples from the spin bit are discarded**. In a mesh
|
||||
protocol where frames are sent irregularly (tree announces, bloom filters,
|
||||
MMP reports on different timers), inter-frame processing delays inflate spin
|
||||
bit RTT measurements unpredictably. Timestamp-echo from ReceiverReports
|
||||
(with dwell-time compensation) is the sole SRTT source.
|
||||
RTT is measured exclusively via timestamp-echo in ReceiverReports with
|
||||
dwell-time compensation.
|
||||
|
||||
### ECN Congestion Signaling
|
||||
|
||||
@@ -555,7 +559,9 @@ an attacker sends invalid packets to elicit responses.
|
||||
|
||||
| Feature | Status |
|
||||
| ------- | ------ |
|
||||
| Noise IK handshake (with epoch) | **Implemented** |
|
||||
| Noise XX handshake (with epoch and negotiation) | **Implemented** |
|
||||
| Protocol negotiation (version + features + TLV) | **Implemented** |
|
||||
| Node profiles (Full, NonRouting, Leaf) | **Implemented** |
|
||||
| Peer restart detection (epoch mismatch) | **Implemented** |
|
||||
| Link encryption (ChaCha20-Poly1305) | **Implemented** |
|
||||
| Index-based session dispatch | **Implemented** |
|
||||
|
||||
@@ -347,8 +347,8 @@ The source creates a LookupRequest containing:
|
||||
- **request_id**: Unique identifier for deduplication
|
||||
- **target**: The node_addr being sought
|
||||
- **origin**: The requester's node_addr
|
||||
- **origin_coords**: The requester's current tree coordinates (so the
|
||||
response can route back)
|
||||
- **min_mtu**: Minimum transport MTU the origin requires (transit nodes
|
||||
skip peers whose link MTU is below this)
|
||||
- **TTL**: Bounds the forwarding radius
|
||||
|
||||
### Bloom-Guided Tree Routing
|
||||
@@ -433,8 +433,8 @@ the primary mechanism: each transit node looks up the `request_id` in its
|
||||
`recent_requests` table to find the peer that forwarded the original request,
|
||||
and sends the response back through that peer. This ensures the response
|
||||
follows the same path as the request. Greedy tree routing toward the
|
||||
`origin_coords` is used only as a fallback if the reverse-path entry has
|
||||
expired.
|
||||
greedy tree routing toward the origin's coordinates is used only as a
|
||||
fallback if the reverse-path entry has expired.
|
||||
|
||||
**Response-forwarded flag**: Each `recent_requests` entry tracks whether a
|
||||
response has already been forwarded for that `request_id`. If a second
|
||||
@@ -663,16 +663,23 @@ When traffic resumes:
|
||||
3. Coordinates: discovery may be needed if cache has expired
|
||||
4. SessionSetup re-warms transit caches on the new path
|
||||
|
||||
## Leaf-Only Operation *(under development)*
|
||||
## Node Profiles
|
||||
|
||||
Leaf-only operation is an optimization for resource-constrained nodes
|
||||
(sensors, battery-powered devices). The core infrastructure exists (config
|
||||
flag, node constructor, bloom filter support) but is not yet enabled in
|
||||
normal operation.
|
||||
Nodes advertise a profile during FMP negotiation (bits 0-2 of the feature
|
||||
bitfield): **Full** (default), **NonRouting**, or **Leaf**. At least one
|
||||
side of a link must be Full. Config mapping: `disable_routing: true` →
|
||||
NonRouting, `leaf_only: true` → Leaf.
|
||||
|
||||
### Concept
|
||||
### Non-Routing Nodes
|
||||
|
||||
A leaf-only node connects to a single upstream peer that handles all routing
|
||||
A non-routing node participates in the spanning tree but does not forward
|
||||
transit traffic or send bloom filters. Its full peer inserts the
|
||||
non-routing node's identity as a leaf dependent. MMP report flow is gated
|
||||
by wants/provides bits negotiated during the handshake.
|
||||
|
||||
### Leaf Nodes
|
||||
|
||||
A leaf node connects to a single upstream peer that handles all routing
|
||||
on its behalf:
|
||||
|
||||
- **No bloom filter storage or processing**: The upstream peer includes the
|
||||
@@ -695,7 +702,7 @@ The upstream peer:
|
||||
|
||||
Even as a leaf-only node, it still:
|
||||
|
||||
- Maintains its own Noise IK link session with the upstream peer (FMP layer)
|
||||
- Maintains its own Noise XX link session with the upstream peer (FMP layer)
|
||||
- Can establish end-to-end FSP sessions with arbitrary destinations
|
||||
- Has its own identity (npub, node_addr)
|
||||
|
||||
@@ -707,8 +714,8 @@ routing decisions but retains its own end-to-end encryption and identity.
|
||||
| Message | Typical Size | When | Forwarded? |
|
||||
| ------- | ------------ | ---- | ---------- |
|
||||
| TreeAnnounce | Variable (depth-dependent) | Topology changes | No (peer-to-peer) |
|
||||
| FilterAnnounce | ~1 KB | Topology changes | No (peer-to-peer) |
|
||||
| LookupRequest | ~300 bytes | First contact, recovery | Yes (bloom-guided tree) |
|
||||
| FilterAnnounce | variable (RLE compressed) | Topology changes | No (peer-to-peer) |
|
||||
| LookupRequest | 44 bytes + TLV | First contact, recovery | Yes (bloom-guided tree) |
|
||||
| LookupResponse | ~400 bytes | Response to discovery | Yes (greedy routed) |
|
||||
| SessionDatagram + SessionSetup | ~232–402 bytes | Session establishment | Yes (routed) |
|
||||
| SessionDatagram + SessionAck | ~170 bytes | Session confirmation | Yes (routed) |
|
||||
@@ -766,7 +773,7 @@ recovery).
|
||||
| Discovery originator backoff | **Implemented** |
|
||||
| Discovery transit-side rate limiting | **Implemented** |
|
||||
| Discovery response-forwarded dedup | **Implemented** |
|
||||
| Leaf-only operation | Under development |
|
||||
| Node profiles (Full, NonRouting, Leaf) | **Implemented** |
|
||||
| Link cost in parent selection (ETX) | **Implemented** |
|
||||
| Link cost in candidate ranking | **Implemented** |
|
||||
|
||||
|
||||
@@ -120,24 +120,29 @@ node, FMP delivers it to FSP for session-layer processing.
|
||||
Sessions are established on demand when the first datagram needs to be sent to
|
||||
a destination with no existing session.
|
||||
|
||||
FSP uses Noise XK for session key agreement (Noise Protocol Framework;
|
||||
Perrin 2018). The initiator knows the destination's npub (required for
|
||||
XK's pre-message `s` token); the responder learns the initiator's
|
||||
identity from msg3 (not msg1, unlike IK at the link layer). This provides stronger initiator identity hiding
|
||||
— the initiator's static key is encrypted under the established shared
|
||||
secret rather than under only the responder's static key.
|
||||
FSP uses Noise XX for session key agreement (Noise Protocol Framework;
|
||||
Perrin 2018). Neither side requires prior knowledge of the other's
|
||||
static key — both identities are revealed during the handshake
|
||||
(responder in msg2, initiator in msg3). An optional protocol negotiation
|
||||
payload may be appended to msg2/msg3 (omitted for rekey handshakes).
|
||||
|
||||
The handshake is a three-message flow carried in SessionSetup, SessionAck,
|
||||
and SessionMsg3:
|
||||
|
||||
1. **Initiator** sends SessionSetup containing Noise XK msg1 (ephemeral key
|
||||
1. **Initiator** sends SessionSetup containing Noise XX msg1 (ephemeral key
|
||||
only) and both parties' tree coordinates
|
||||
2. **Responder** processes msg1, sends SessionAck containing Noise XK msg2
|
||||
(ephemeral key + encrypted epoch) and both parties' tree coordinates.
|
||||
The responder transitions to AwaitingMsg3 state.
|
||||
3. **Initiator** processes msg2, sends SessionMsg3 containing the encrypted
|
||||
static key and encrypted epoch. Both parties derive identical symmetric
|
||||
session keys and the session is established.
|
||||
2. **Responder** processes msg1, sends SessionAck containing Noise XX msg2
|
||||
(ephemeral key + encrypted static key + encrypted epoch) and both
|
||||
parties' tree coordinates. The responder transitions to AwaitingMsg3
|
||||
state.
|
||||
3. **Initiator** processes msg2 (learning the responder's identity), sends
|
||||
SessionMsg3 containing its encrypted static key and encrypted epoch.
|
||||
The responder learns the initiator's identity from msg3. Both parties
|
||||
derive identical symmetric session keys and the session is established.
|
||||
|
||||
Post-handshake identity verification uses x-only key comparison
|
||||
(parity-independent) to confirm the revealed identity matches the
|
||||
expected npub.
|
||||
|
||||
Each side's epoch (an 8-byte random value generated at startup) is
|
||||
exchanged encrypted in msg2 and msg3. On subsequent handshakes, an epoch
|
||||
@@ -218,29 +223,30 @@ than network addresses. A session survives:
|
||||
|
||||
## End-to-End Encryption
|
||||
|
||||
### Noise XK Pattern
|
||||
### Noise XX Pattern
|
||||
|
||||
FSP uses Noise XK for session encryption, distinct from the Noise IK
|
||||
pattern used at the link layer. The full Noise descriptor is
|
||||
`Noise_XK_secp256k1_ChaChaPoly_SHA256`.
|
||||
FSP uses the same Noise XX pattern as the link layer (FMP). The full
|
||||
Noise descriptor is `Noise_XX_secp256k1_ChaChaPoly_SHA256`.
|
||||
|
||||
The XK pattern (pre-message: `← s`):
|
||||
The XX pattern (no pre-message):
|
||||
|
||||
- **msg1** (`→ e, es`): Initiator sends ephemeral key only. The initiator's
|
||||
static identity is not revealed in this message.
|
||||
- **msg2** (`← e, ee`): Responder sends ephemeral key and encrypted epoch.
|
||||
- **msg1** (`→ e`): Initiator sends ephemeral key only. No identity
|
||||
disclosed, no DH with static keys.
|
||||
- **msg2** (`← e, ee, s, es`): Responder sends ephemeral key, encrypted
|
||||
static key, and encrypted epoch. The initiator learns the responder's
|
||||
identity.
|
||||
- **msg3** (`→ s, se`): Initiator sends encrypted static key and encrypted
|
||||
epoch. Both parties now share identical session keys.
|
||||
epoch. The responder learns the initiator's identity. 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.
|
||||
(`send_key`, `recv_key`) used with ChaCha20-Poly1305 for all subsequent
|
||||
data.
|
||||
|
||||
The XK pattern requires the initiator to know the responder's static key
|
||||
in advance (the `← s` pre-message), which is satisfied by the discovery
|
||||
or DNS lookup that precedes session establishment. In exchange, XK
|
||||
provides stronger initiator identity protection than IK — the initiator's
|
||||
static key is encrypted under the full shared secret (after three DH
|
||||
operations) rather than under only the responder's static key.
|
||||
XX requires no prior knowledge of the peer's static key. The initiator
|
||||
still needs the destination's npub to address the SessionSetup, but the
|
||||
Noise handshake itself does not depend on it — identity is verified
|
||||
post-handshake by comparing the revealed key against the expected npub.
|
||||
|
||||
### Cryptographic Primitives
|
||||
|
||||
@@ -258,26 +264,27 @@ messaging standard.
|
||||
|
||||
### secp256k1 Parity Normalization
|
||||
|
||||
Nostr npubs encode x-only public keys (32 bytes, no y-coordinate parity). The
|
||||
Noise XK 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.
|
||||
Nostr npubs encode x-only public keys (32 bytes, no y-coordinate parity).
|
||||
When the Noise XX handshake reveals a peer's static key via
|
||||
`public_key().serialize()`, the key has its actual parity (0x02 or 0x03
|
||||
prefix). The default secp256k1 ECDH hash also 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.
|
||||
Both operations are normalized to be parity-independent: ECDH hashes only
|
||||
the x-coordinate of the result point, and post-handshake identity
|
||||
verification uses `x_only_public_key()` to strip parity before comparing
|
||||
against the expected npub. This ensures handshakes and identity checks
|
||||
succeed regardless of key parity.
|
||||
|
||||
### Privacy Note
|
||||
|
||||
Noise XK provides stronger initiator identity protection than IK. In XK, the
|
||||
initiator's static key is encrypted in msg3 under the full shared secret
|
||||
(derived from three DH operations), so an attacker who compromises only the
|
||||
responder's nsec cannot decrypt the initiator's identity from captured
|
||||
handshake messages (they would also need the responder's ephemeral key).
|
||||
This is the primary reason FSP uses XK rather than IK — session-layer
|
||||
traffic traverses untrusted intermediate nodes, making initiator identity
|
||||
protection more valuable than at the link layer.
|
||||
Noise XX provides mutual identity protection — both the initiator's and
|
||||
responder's static keys are encrypted under the evolving shared secret
|
||||
(derived from DH operations completed in earlier messages). An attacker
|
||||
who compromises only one side's nsec cannot decrypt the other side's
|
||||
identity from captured handshake messages without also obtaining the
|
||||
corresponding ephemeral key. Since session-layer traffic traverses
|
||||
untrusted intermediate nodes, this mutual identity hiding is valuable.
|
||||
|
||||
### Data Packet Authentication
|
||||
|
||||
@@ -534,7 +541,7 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
|
||||
|
||||
| Feature | Status |
|
||||
| ------- | ------ |
|
||||
| Session establishment (Noise XK) | **Implemented** |
|
||||
| Session establishment (Noise XX) | **Implemented** |
|
||||
| Peer restart detection (epoch exchange) | **Implemented** |
|
||||
| MtuExceeded handling | **Implemented** |
|
||||
| End-to-end encryption (ChaCha20-Poly1305) | **Implemented** |
|
||||
@@ -571,7 +578,7 @@ MMP session metrics session=npub1tdwa...84le rtt=4.3ms loss=0.6% jitter=0.2ms go
|
||||
|
||||
- Perrin, T. ["The Noise Protocol Framework"](https://noiseprotocol.org/noise.html).
|
||||
Revision 34, 2018. *Framework for building crypto protocols using Diffie-Hellman
|
||||
key agreement and AEAD ciphers. FSP uses the XK handshake pattern.*
|
||||
key agreement and AEAD ciphers. FSP uses the XX handshake pattern.*
|
||||
|
||||
- Donenfeld, J.A. ["WireGuard: Next Generation Kernel Network Tunnel"](https://www.wireguard.com/papers/wireguard.pdf).
|
||||
NDSS 2017. *Transport-independent cryptographic sessions bound to identity keys
|
||||
|
||||
@@ -165,7 +165,7 @@ When a node receives a TreeAnnounce from peer P:
|
||||
|
||||
1. **Validate version**: Reject if version ≠ 0x01
|
||||
2. **Verify signature**: Check P's declaration signature using P's known
|
||||
public key (established during Noise IK handshake)
|
||||
public key (established during Noise XX handshake)
|
||||
3. **Verify identity**: Confirm the declaration's node_addr matches the
|
||||
sender's known identity
|
||||
4. **Check freshness**: If `sequence ≤ stored sequence for P`, discard
|
||||
|
||||
@@ -16,7 +16,7 @@ to the FIPS Mesh Protocol (FMP) above.
|
||||
The transport layer deals exclusively in **transport addresses** — IP:port
|
||||
or hostname:port addresses, MAC addresses, .onion identifiers, radio device addresses. These are
|
||||
opaque to every layer above FMP. The mapping from transport address to FIPS
|
||||
identity happens at the link layer after the Noise IK link handshake completes.
|
||||
identity happens at the link layer after the Noise XX link handshake completes.
|
||||
The word "peer" belongs to the link layer and above; the transport layer
|
||||
knows only about remote endpoints identified by transport addresses.
|
||||
|
||||
@@ -69,7 +69,7 @@ forwarding and LookupResponse transit annotation.
|
||||
|
||||
For connection-oriented transports, manage the underlying connection: TCP
|
||||
handshake, Tor circuit establishment, Bluetooth pairing. FMP cannot begin
|
||||
the Noise IK link handshake until the transport-layer connection is
|
||||
the Noise XX link handshake until the transport-layer connection is
|
||||
established.
|
||||
|
||||
Connection-oriented transports expose a non-blocking connect interface.
|
||||
@@ -153,7 +153,7 @@ and duplication at the routing layer.
|
||||
|
||||
**Connection model**: Connectionless transports (UDP, raw Ethernet) allow
|
||||
immediate datagram exchange. Connection-oriented transports (TCP, Tor, BLE)
|
||||
require connection setup before FMP can begin the Noise IK link handshake,
|
||||
require connection setup before FMP can begin the Noise XX link handshake,
|
||||
adding startup latency.
|
||||
|
||||
**Stream vs. datagram**: Datagram transports have natural packet boundaries.
|
||||
@@ -292,30 +292,28 @@ EtherType 0x2121. SOCK_DGRAM mode
|
||||
lets the kernel handle Ethernet header construction and parsing — the
|
||||
transport deals only with payloads and MAC addresses.
|
||||
|
||||
Data frames use a 3-byte header: a 1-byte frame type (`0x00`) followed by
|
||||
a 2-byte little-endian payload length. The length field allows the receiver
|
||||
to trim Ethernet minimum-frame padding that would otherwise corrupt AEAD
|
||||
verification. Beacon frames (`0x01`) use only the 1-byte type prefix
|
||||
(fixed 34-byte payload). Beacons and data share the same EtherType and
|
||||
socket.
|
||||
All frames use a unified 4-byte header: `[type:1][flags:1][length:2 LE]`.
|
||||
The length field allows the receiver to trim Ethernet minimum-frame padding
|
||||
that would otherwise corrupt AEAD verification. Frame types: `0x00` (data),
|
||||
`0x01` (beacon). Beacons and data share the same EtherType and socket.
|
||||
|
||||
| Property | Value |
|
||||
| -------- | ----- |
|
||||
| EtherType | 0x2121 |
|
||||
| Socket type | AF_PACKET SOCK_DGRAM |
|
||||
| Data frame header | `[type:1][length:2 LE][payload]` |
|
||||
| Beacon frame header | `[type:1][payload]` (fixed 34 bytes) |
|
||||
| Effective MTU | Interface MTU - 3 (typically 1497) |
|
||||
| Frame header | `[type:1][flags:1][length:2 LE][payload]` |
|
||||
| Effective MTU | Interface MTU - 4 (typically 1496) |
|
||||
| Addressing | 6-byte MAC address |
|
||||
| Platform | Linux only (`CAP_NET_RAW` required) |
|
||||
|
||||
### Beacon Discovery
|
||||
|
||||
Ethernet nodes discover peers via broadcast beacons sent to
|
||||
ff:ff:ff:ff:ff:ff. Each beacon is a 34-byte frame containing the sender's
|
||||
x-only public key. Receiving nodes extract the MAC source address from the
|
||||
frame and the public key from the payload, then report the discovered peer
|
||||
to FMP.
|
||||
ff:ff:ff:ff:ff:ff. Beacons are minimal 5-byte frames (4-byte header +
|
||||
1-byte beacon type) — no public key is included. The peer's identity is
|
||||
learned from the Noise XX handshake after the connection is established.
|
||||
Receiving nodes extract the MAC source address from the frame and report
|
||||
the discovered address to FMP.
|
||||
|
||||
Four configuration flags control discovery behavior:
|
||||
|
||||
@@ -400,7 +398,7 @@ every tick, `poll_pending_connects()` calls `connection_state(addr)` to
|
||||
check progress. When the transport reports `Connected`, the completed
|
||||
connection is promoted to the established pool (stream split into
|
||||
read/write halves, per-connection receive task spawned), and the node
|
||||
initiates the Noise IK link handshake. If the transport reports `Failed`,
|
||||
initiates the Noise XX link handshake. If the transport reports `Failed`,
|
||||
the node schedules a retry with exponential backoff.
|
||||
|
||||
As a fallback, `send(addr, data)` still performs synchronous
|
||||
@@ -447,7 +445,7 @@ socket is created).
|
||||
The Tor transport routes FIPS traffic through the Tor network, hiding
|
||||
a node's IP address from its peers. A node behind Tor connects outbound
|
||||
through a local Tor SOCKS5 proxy; the remote peer sees the Tor exit
|
||||
node's IP, not the initiator's. After the Noise IK handshake, the remote
|
||||
node's IP, not the initiator's. After the Noise XX handshake, the remote
|
||||
peer knows the initiator's FIPS identity (npub) but not its network
|
||||
location.
|
||||
|
||||
@@ -528,7 +526,7 @@ The inbound accept loop mirrors the TCP transport's pattern: accept
|
||||
connection, configure socket (TCP_NODELAY, keepalive), spawn a
|
||||
per-connection receive loop using the shared FMP stream reader. Inbound
|
||||
connections arrive from `127.0.0.1` (Tor daemon's local forwarding); peer
|
||||
identity is resolved during the Noise IK handshake, not from the transport
|
||||
identity is resolved during the Noise XX handshake, not from the transport
|
||||
address.
|
||||
|
||||
Configuration requires coordinating `torrc` and `fips.yaml`:
|
||||
|
||||
@@ -24,14 +24,14 @@ byte stream; the common prefix `payload_len` field provides this
|
||||
framing directly. TCP and Tor share a common stream reader
|
||||
(`tcp/stream.rs`) that implements this framing.
|
||||
|
||||
**Ethernet data frame header.** The Ethernet transport prepends a 3-byte
|
||||
header before the FMP payload on data frames: a 1-byte frame type
|
||||
(`0x00`) followed by a 2-byte little-endian payload length. The length
|
||||
field allows the receiver to trim Ethernet minimum-frame padding that
|
||||
would otherwise corrupt AEAD verification. Beacon frames (`0x01`) have
|
||||
no length field (fixed 34-byte payload). These bytes are consumed by the
|
||||
transport layer and are not visible to FMP. The effective MTU for FMP is
|
||||
the interface MTU minus three bytes (typically 1497).
|
||||
**Ethernet frame header.** The Ethernet transport prepends a 4-byte
|
||||
unified header before the payload: `[type:1][flags:1][length:2 LE]`.
|
||||
The length field allows the receiver to trim Ethernet minimum-frame
|
||||
padding that would otherwise corrupt AEAD verification. Frame types:
|
||||
`0x00` (data), `0x01` (beacon). Beacons are 5 bytes total (4-byte
|
||||
header + 1-byte beacon type). These bytes are consumed by the
|
||||
transport layer and are not visible to FMP. The effective MTU for FMP
|
||||
is the interface MTU minus four bytes (typically 1496).
|
||||
|
||||
## Link-Layer Formats
|
||||
|
||||
@@ -45,7 +45,7 @@ length.
|
||||
|
||||
| Field | Size | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| version | 4 bits (high) | Protocol version. Currently 0x0 |
|
||||
| version | 4 bits (high) | Protocol version. Currently 0x1 |
|
||||
| phase | 4 bits (low) | Session lifecycle phase (see table) |
|
||||
| flags | 1 byte | Per-packet signal flags (zero during handshake) |
|
||||
| payload_len | 2 bytes LE | Length of payload after phase-specific header, excluding AEAD tag |
|
||||
@@ -55,8 +55,9 @@ length.
|
||||
| Phase | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| 0x0 | Established frame | Post-handshake encrypted traffic |
|
||||
| 0x1 | Noise IK msg1 | Handshake initiation |
|
||||
| 0x2 | Noise IK msg2 | Handshake response |
|
||||
| 0x1 | Noise XX msg1 | Handshake initiation (ephemeral only) |
|
||||
| 0x2 | Noise XX msg2 | Handshake response (responder identity + negotiation) |
|
||||
| 0x3 | Noise XX msg3 | Handshake completion (initiator identity + negotiation) |
|
||||
|
||||
### Flags (Established Phase Only)
|
||||
|
||||
@@ -64,10 +65,9 @@ length.
|
||||
| --- | ---- | ----------- |
|
||||
| 0 | K (key epoch) | Selects active key during rekeying |
|
||||
| 1 | CE | Congestion Experienced echo |
|
||||
| 2 | SP (spin bit) | RTT measurement |
|
||||
| 3-7 | — | Reserved (must be zero) |
|
||||
| 2-7 | — | Reserved (must be zero) |
|
||||
|
||||
Flags must be zero in handshake packets (phase 0x1 and 0x2).
|
||||
Flags must be zero in handshake packets (phase 0x1, 0x2, and 0x3).
|
||||
|
||||
### Established Frame (phase 0x0)
|
||||
|
||||
@@ -80,7 +80,7 @@ encrypted link-layer message.
|
||||
|
||||
| Field | Size | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| common prefix | 4 bytes | ver=0, phase=0, flags, payload_len |
|
||||
| common prefix | 4 bytes | ver=1, phase=0, flags, payload_len |
|
||||
| receiver_idx | 4 bytes LE | Session index for O(1) lookup |
|
||||
| counter | 8 bytes LE | Monotonic nonce, used as AEAD nonce and for replay detection |
|
||||
|
||||
@@ -108,67 +108,141 @@ the 1-byte message type and message-specific fields.
|
||||
| Type | Message | Description |
|
||||
| ---- | ------- | ----------- |
|
||||
| 0x00 | SessionDatagram | Encapsulated session-layer payload for forwarding |
|
||||
| 0x01 | SenderReport | MMP sender-side metrics report (48 bytes) |
|
||||
| 0x02 | ReceiverReport | MMP receiver-side metrics report (68 bytes) |
|
||||
| 0x01 | SenderReport | MMP sender-side metrics report (20 bytes) |
|
||||
| 0x02 | ReceiverReport | MMP receiver-side metrics report (54 bytes) |
|
||||
| 0x10 | TreeAnnounce | Spanning tree state announcement |
|
||||
| 0x20 | FilterAnnounce | Bloom filter reachability update |
|
||||
| 0x21 | FilterNack | Bloom filter delta NACK (retransmit request) |
|
||||
| 0x30 | LookupRequest | Coordinate discovery request |
|
||||
| 0x31 | LookupResponse | Coordinate discovery response |
|
||||
| 0x50 | Disconnect | Orderly link teardown |
|
||||
| 0x51 | Heartbeat | Link liveness probe |
|
||||
|
||||
### Noise IK Message 1 (phase 0x1)
|
||||
### Noise XX Message 1 (phase 0x1)
|
||||
|
||||
Handshake initiation from connecting party.
|
||||
Handshake initiation from connecting party. The initiator sends only its
|
||||
ephemeral key — neither side's static identity is revealed in msg1.
|
||||
|
||||

|
||||

|
||||
|
||||
Common prefix: ver=0, phase=0x1, flags=0, payload_len=110 (4 + 106).
|
||||
Common prefix: ver=1, phase=0x1, flags=0, payload_len=37 (4 + 33).
|
||||
|
||||
| Field | Size | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| common prefix | 4 bytes | ver=0, phase=1, flags=0, payload_len |
|
||||
| common prefix | 4 bytes | ver=1, phase=1, flags=0, payload_len |
|
||||
| sender_idx | 4 bytes LE | Initiator's session index (becomes receiver's `receiver_idx`) |
|
||||
| noise_msg1 | 106 bytes | Noise IK first message |
|
||||
| noise_msg1 | 33 bytes | Noise XX first message |
|
||||
|
||||
**Noise msg1 breakdown** (106 bytes):
|
||||
**Noise msg1 breakdown** (33 bytes):
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | ephemeral_pubkey | 33 bytes | Initiator's ephemeral key (compressed secp256k1) |
|
||||
| 33 | encrypted_static | 49 bytes | Initiator's static key (33) + AEAD tag (16) |
|
||||
| 82 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
|
||||
|
||||
Noise pattern: `-> e, es, s, ss` with epoch payload
|
||||
Noise pattern: `-> e`
|
||||
|
||||
### Noise IK Message 2 (phase 0x2)
|
||||
**Total wire size**: 41 bytes (4 prefix + 4 sender_idx + 33 noise).
|
||||
|
||||
Handshake response from responder.
|
||||
### Noise XX Message 2 (phase 0x2)
|
||||
|
||||

|
||||
Handshake response from responder. The responder reveals its identity
|
||||
(ephemeral key, encrypted static key, and encrypted epoch) plus an
|
||||
optional protocol negotiation payload.
|
||||
|
||||
Common prefix: ver=0, phase=0x2, flags=0, payload_len=65 (4 + 4 + 57).
|
||||

|
||||
|
||||
Common prefix: ver=1, phase=0x2, flags=0, payload_len varies.
|
||||
|
||||
| Field | Size | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| common prefix | 4 bytes | ver=0, phase=2, flags=0, payload_len |
|
||||
| common prefix | 4 bytes | ver=1, phase=2, flags=0, payload_len |
|
||||
| sender_idx | 4 bytes LE | Responder's session index |
|
||||
| receiver_idx | 4 bytes LE | Echo of initiator's sender_idx from msg1 |
|
||||
| noise_msg2 | 57 bytes | Noise IK second message |
|
||||
| noise_msg2 | 106+ bytes | Noise XX second message (variable with negotiation) |
|
||||
|
||||
**Noise msg2 breakdown** (57 bytes):
|
||||
**Noise msg2 breakdown** (106 bytes base):
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | ephemeral_pubkey | 33 bytes | Responder's ephemeral key (compressed secp256k1) |
|
||||
| 33 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
|
||||
| 33 | encrypted_static | 49 bytes | Responder's static key (33) + AEAD tag (16) |
|
||||
| 82 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
|
||||
|
||||
Noise pattern: `<- e, ee, se` with epoch payload
|
||||
Noise pattern: `<- e, ee, s, es` with epoch and negotiation payload
|
||||
|
||||
After msg2, both parties derive identical symmetric session keys. The
|
||||
encrypted epoch in msg1 and msg2 enables peer restart detection — if a
|
||||
peer's epoch changes, the other side knows it restarted and must
|
||||
re-establish the link.
|
||||
**Negotiation payload** (variable, appended via `encrypt_payload()`):
|
||||
encrypted negotiation bytes + AEAD tag. Minimum 26 bytes (10 payload +
|
||||
16 tag) when present. See [Protocol Negotiation Payload](#protocol-negotiation-payload).
|
||||
|
||||
**Total wire size**: 118 bytes minimum (without negotiation), 144 bytes
|
||||
typical (with FMP negotiation: 118 + 26).
|
||||
|
||||
After msg2, the responder's identity is known to the initiator.
|
||||
|
||||
### Noise XX Message 3 (phase 0x3)
|
||||
|
||||
Handshake completion from initiator. The initiator reveals its encrypted
|
||||
static identity and epoch, plus an optional negotiation payload.
|
||||
|
||||

|
||||
|
||||
| Field | Size | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| common prefix | 4 bytes | ver=1, phase=3, flags=0, payload_len |
|
||||
| sender_idx | 4 bytes LE | Echo of initiator's sender_idx |
|
||||
| receiver_idx | 4 bytes LE | Echo of responder's sender_idx from msg2 |
|
||||
| noise_msg3 | 73+ bytes | Noise XX third message (variable with negotiation) |
|
||||
|
||||
**Noise msg3 breakdown** (73 bytes base):
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | encrypted_static | 49 bytes | Initiator's static key (33) + AEAD tag (16) |
|
||||
| 49 | encrypted_epoch | 24 bytes | Startup epoch (8) + AEAD tag (16) |
|
||||
|
||||
Noise pattern: `-> s, se` with epoch and negotiation payload
|
||||
|
||||
**Negotiation payload** (variable, appended via `encrypt_payload()`):
|
||||
same format as msg2. Minimum 26 bytes when present.
|
||||
|
||||
**Total wire size**: 85 bytes minimum (without negotiation), 111 bytes
|
||||
typical (with FMP negotiation: 85 + 26).
|
||||
|
||||
After msg3, both parties derive identical symmetric session keys and
|
||||
have exchanged negotiation payloads. The encrypted epoch in msg2 and
|
||||
msg3 enables peer restart detection — if a peer's epoch changes, the
|
||||
other side knows it restarted and must re-establish the link.
|
||||
|
||||
### Protocol Negotiation Payload
|
||||
|
||||
Appended to XX msg2 and msg3 via Noise `encrypt_payload()` — extends
|
||||
the Noise hash chain so negotiation data is authenticated alongside the
|
||||
handshake transcript.
|
||||
|
||||
**Wire format**: `[format:1][versions:1][features:8][TLV entries...]`
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | format | 1 byte | Must be 0 |
|
||||
| 1 | versions | 1 byte | High nibble = version_min, low nibble = version_max |
|
||||
| 2 | features | 8 bytes LE | 64-bit feature bitfield |
|
||||
| 10 | tlv_entries | variable | TLV extensions: `[field_num:2 LE][length:2 LE][value:N]` per entry |
|
||||
|
||||
**Minimum size**: 10 bytes (no TLV entries). With AEAD tag: 26 bytes on
|
||||
the wire.
|
||||
|
||||
**FMP feature bits**:
|
||||
|
||||
| Bits | Name | Description |
|
||||
| ---- | ---- | ----------- |
|
||||
| 0-2 | Node profile | Full (0), NonRouting (1), Leaf (2) |
|
||||
| 3-6 | MMP wants/provides | MMP capability negotiation |
|
||||
| 7 | Bloom | Bloom filter capability |
|
||||
| 8-63 | — | Reserved |
|
||||
|
||||
**Critical**: `decrypt_payload()` MUST be called even if the result is
|
||||
discarded, to maintain hash chain consistency between the two Noise
|
||||
transport channels.
|
||||
|
||||
### Index Semantics
|
||||
|
||||
@@ -230,28 +304,49 @@ includes self)
|
||||
### FilterAnnounce (0x20)
|
||||
|
||||
Bloom filter reachability update, exchanged between direct peers only.
|
||||
Supports both full sends and delta (XOR diff) updates with RLE
|
||||
compression.
|
||||
|
||||

|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | msg_type | 1 byte | 0x20 |
|
||||
| 1 | sequence | 8 bytes LE | Monotonic counter for freshness |
|
||||
| 9 | hash_count | 1 byte | Number of hash functions (5 in v1) |
|
||||
| 10 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
|
||||
| 11 | filter_bits | variable | Bloom filter bit array |
|
||||
| 1 | flags | 1 byte | Bit 0: delta (XOR diff), bits 1-7 reserved |
|
||||
| 2 | sequence | 8 bytes LE | Monotonic counter, per-peer (only increments on actual send) |
|
||||
| 10 | base_seq | 8 bytes LE | Sequence of reference filter for delta (0 if full send) |
|
||||
| 18 | size_class | 1 byte | Filter size: `512 << size_class` bytes |
|
||||
| 19 | compressed_payload | variable | RLE-encoded filter or XOR diff |
|
||||
|
||||
**RLE format**: each run = `[count:2 LE][word:8 LE]` (10 bytes per run).
|
||||
Sparse XOR diffs compress to very few runs.
|
||||
|
||||
**Size class table**:
|
||||
|
||||
| size_class | Bytes | Bits | Status |
|
||||
| ---------- | ----- | ---- | ------ |
|
||||
| 0 | 512 | 4,096 | Reserved |
|
||||
| 1 | 1,024 | 8,192 | **v1 (MUST use)** |
|
||||
| 2 | 2,048 | 16,384 | Reserved |
|
||||
| 3 | 4,096 | 32,768 | Reserved |
|
||||
| size_class | Bytes | Bits | Notes |
|
||||
| ---------- | ----- | ---- | ----- |
|
||||
| 0 | 512 | 4,096 | Minimum |
|
||||
| 1 | 1,024 | 8,192 | Default |
|
||||
| 2 | 2,048 | 16,384 | |
|
||||
| 3 | 4,096 | 32,768 | |
|
||||
| 4 | 8,192 | 65,536 | |
|
||||
| 5 | 16,384 | 131,072 | |
|
||||
| 6 | 32,768 | 262,144 | Maximum |
|
||||
|
||||
**v1 payload**: 1,035 bytes (11 header + 1,024 filter).
|
||||
With link overhead: 1,072 bytes.
|
||||
**Size**: variable (19-byte header + RLE-compressed payload).
|
||||
|
||||
### FilterNack (0x21)
|
||||
|
||||
Request full filter retransmission. Sent when a node receives an
|
||||
out-of-sequence delta update (sequence gap detected), triggering a
|
||||
full retransmit from the sender.
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | msg_type | 1 byte | 0x21 |
|
||||
| 1 | expected_seq | 8 bytes LE | Sequence number the receiver expected |
|
||||
|
||||
**Total**: 9 bytes.
|
||||
|
||||
### LookupRequest (0x30)
|
||||
|
||||
@@ -271,16 +366,10 @@ restructuring.
|
||||
| 25 | origin | 16 bytes | Requester's NodeAddr |
|
||||
| 41 | ttl | 1 byte | Remaining hops (default 64) |
|
||||
| 42 | min_mtu | 2 bytes LE | Minimum transport MTU the origin requires (0 = no requirement) |
|
||||
| 44 | origin_coords_cnt | 2 bytes LE | Number of coordinate entries |
|
||||
| 46 | origin_coords | 16 x n bytes | Requester's ancestry (NodeAddr only) |
|
||||
| 44 | tlv_entries | variable | TLV extensions, forwarded verbatim by transit |
|
||||
|
||||
**Size**: `46 + (n x 16)` bytes, where n = origin depth + 1
|
||||
|
||||
| Origin Depth | Payload |
|
||||
| ------------ | ------- |
|
||||
| 3 | 110 bytes |
|
||||
| 5 | 142 bytes |
|
||||
| 10 | 222 bytes |
|
||||
**Size**: 44 bytes fixed + TLV entries. TLV format:
|
||||
`[field_num:2 LE][length:2 LE][value:N]` per entry.
|
||||
|
||||
### LookupResponse (0x31)
|
||||
|
||||
@@ -298,11 +387,12 @@ requester via the transit nodes that forwarded the request.
|
||||
| 27 | target_coords_cnt | 2 bytes LE | Number of coordinate entries |
|
||||
| 29 | target_coords | 16 x n bytes | Target's ancestry (NodeAddr only) |
|
||||
| 29 + 16n | proof | 64 bytes | Schnorr signature over `(request_id \|\| target \|\| target_coords)` |
|
||||
| 93 + 16n | tlv_entries | variable | TLV extensions, forwarded verbatim by transit |
|
||||
|
||||
**Size**: `93 + (n x 16)` bytes
|
||||
**Size**: `93 + (n x 16)` bytes + TLV entries
|
||||
|
||||
| Target Depth | Payload |
|
||||
| ------------ | ------- |
|
||||
| Target Depth | Payload (no TLV) |
|
||||
| ------------ | ---------------- |
|
||||
| 3 | 141 bytes |
|
||||
| 5 | 173 bytes |
|
||||
| 10 | 253 bytes |
|
||||
@@ -375,16 +465,16 @@ Sent by the frame sender to provide interval-based transmission statistics.
|
||||
| Offset | Field | Size | Encoding |
|
||||
| ------ | ----- | ---- | -------- |
|
||||
| 0 | msg_type | 1 | `0x01` |
|
||||
| 1 | reserved | 3 | Zero |
|
||||
| 4 | interval_start_counter | 8 | u64 LE — first counter in this interval |
|
||||
| 12 | interval_end_counter | 8 | u64 LE — last counter in this interval |
|
||||
| 20 | interval_start_timestamp | 4 | u32 LE — timestamp at interval start |
|
||||
| 24 | interval_end_timestamp | 4 | u32 LE — timestamp at interval end |
|
||||
| 28 | interval_bytes_sent | 4 | u32 LE — payload bytes sent in interval |
|
||||
| 32 | cumulative_packets_sent | 8 | u64 LE — total packets sent on this link |
|
||||
| 40 | cumulative_bytes_sent | 8 | u64 LE — total bytes sent on this link |
|
||||
| 1 | format_version | 1 | 0 (current) |
|
||||
| 2 | total_length | 2 | u16 LE — 16 (v0 payload size) |
|
||||
| 4 | interval_packets_sent | 4 | u32 LE — packets sent in interval |
|
||||
| 8 | interval_bytes_sent | 4 | u32 LE — payload bytes sent in interval |
|
||||
| 12 | cumulative_packets_sent | 8 | u64 LE — total packets sent on this link |
|
||||
|
||||
**Total: 48 bytes.**
|
||||
**Total: 20 bytes.**
|
||||
|
||||
Decoders skip trailing bytes beyond `total_length` for forward
|
||||
compatibility with future format versions.
|
||||
|
||||
### ReceiverReport (0x02)
|
||||
|
||||
@@ -395,24 +485,26 @@ Sent by the frame receiver to provide loss, jitter, and timing feedback.
|
||||
| Offset | Field | Size | Encoding |
|
||||
| ------ | ----- | ---- | -------- |
|
||||
| 0 | msg_type | 1 | `0x02` |
|
||||
| 1 | reserved | 3 | Zero |
|
||||
| 4 | highest_counter | 8 | u64 LE — highest counter value received |
|
||||
| 12 | cumulative_packets_recv | 8 | u64 LE — total packets received |
|
||||
| 20 | cumulative_bytes_recv | 8 | u64 LE — total bytes received |
|
||||
| 28 | timestamp_echo | 4 | u32 LE — echoed sender timestamp for RTT |
|
||||
| 32 | dwell_time | 2 | u16 LE — time between receive and echo (ms) |
|
||||
| 34 | max_burst_loss | 2 | u16 LE — largest loss burst in interval |
|
||||
| 36 | mean_burst_loss | 2 | u16 LE — mean burst length (u8.8 fixed-point) |
|
||||
| 38 | reserved | 2 | Zero |
|
||||
| 40 | jitter | 4 | u32 LE — interarrival jitter (microseconds) |
|
||||
| 44 | ecn_ce_count | 4 | u32 LE — cumulative ECN-CE marked packets |
|
||||
| 48 | owd_trend | 4 | i32 LE — one-way delay trend (µs/s, signed) |
|
||||
| 52 | burst_loss_count | 4 | u32 LE — number of loss bursts in interval |
|
||||
| 56 | cumulative_reorder_count | 4 | u32 LE — total reordered packets |
|
||||
| 60 | interval_packets_recv | 4 | u32 LE — packets received in interval |
|
||||
| 64 | interval_bytes_recv | 4 | u32 LE — bytes received in interval |
|
||||
| 1 | format_version | 1 | 0 (current) |
|
||||
| 2 | total_length | 2 | u16 LE — 50 (v0 payload size) |
|
||||
| 4 | timestamp_echo | 4 | u32 LE — echoed sender timestamp for RTT |
|
||||
| 8 | dwell_time | 2 | u16 LE — time between receive and echo (ms) |
|
||||
| 10 | highest_counter | 8 | u64 LE — highest counter value received |
|
||||
| 18 | cumulative_packets_recv | 8 | u64 LE — total packets received |
|
||||
| 26 | cumulative_bytes_recv | 8 | u64 LE — total bytes received |
|
||||
| 34 | jitter | 4 | u32 LE — interarrival jitter (microseconds) |
|
||||
| 38 | ecn_ce_count | 4 | u32 LE — cumulative ECN-CE marked packets |
|
||||
| 42 | owd_trend | 4 | i32 LE — one-way delay trend (µs/s, signed) |
|
||||
| 46 | burst_loss_count | 4 | u32 LE — number of loss bursts in interval |
|
||||
| 50 | cumulative_reorder_count | 4 | u32 LE — total reordered packets |
|
||||
|
||||
**Total: 68 bytes.**
|
||||
**Total: 54 bytes.**
|
||||
|
||||
Fields removed from v0.2: `max_burst_loss`, `mean_burst_loss`,
|
||||
`interval_packets_recv`, `interval_bytes_recv`.
|
||||
|
||||
Decoders skip trailing bytes beyond `total_length` for forward
|
||||
compatibility with future format versions.
|
||||
|
||||
## Session-Layer Message Formats
|
||||
|
||||
@@ -434,9 +526,9 @@ protocol version, session lifecycle phase, per-packet flags, and payload length.
|
||||
| Phase | Type | Description |
|
||||
| ----- | ---- | ----------- |
|
||||
| 0x0 | Established | Post-handshake encrypted traffic or plaintext error signals |
|
||||
| 0x1 | Handshake msg1 | SessionSetup (Noise XK msg1) |
|
||||
| 0x2 | Handshake msg2 | SessionAck (Noise XK msg2) |
|
||||
| 0x3 | Handshake msg3 | SessionMsg3 (Noise XK msg3) |
|
||||
| 0x1 | Handshake msg1 | SessionSetup (Noise XX msg1) |
|
||||
| 0x2 | Handshake msg2 | SessionAck (Noise XX msg2) |
|
||||
| 0x3 | Handshake msg3 | SessionMsg3 (Noise XX msg3) |
|
||||
|
||||
### FSP Flags (Established Phase Only)
|
||||
|
||||
@@ -479,7 +571,7 @@ Transit nodes parse the CP flag and extract coordinates without decryption.
|
||||
| ----- | ---- | ----------- |
|
||||
| timestamp | 4 bytes LE | Session-relative milliseconds (u32) |
|
||||
| msg_type | 1 byte | Session-layer message type |
|
||||
| inner_flags | 1 byte | Bit 0: SP (spin bit for RTT measurement) |
|
||||
| inner_flags | 1 byte | Reserved (must be zero) |
|
||||
|
||||
After the inner header, the remaining plaintext is the message-type-specific
|
||||
body.
|
||||
@@ -513,8 +605,8 @@ happens at the session level based on the FSP message type.
|
||||
### SessionSetup (phase 0x1)
|
||||
|
||||
Establishes a session and warms transit coordinate caches. Contains the
|
||||
first message of the Noise XK handshake (ephemeral key only — the
|
||||
initiator's static identity is not revealed until msg3).
|
||||
first message of the Noise XX handshake (ephemeral key only — neither
|
||||
side's static identity is revealed until msg2/msg3).
|
||||
|
||||
SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
|
||||
field in the FSP common prefix (0x1, 0x2, 0x3), not by a message type
|
||||
@@ -535,12 +627,13 @@ Encoded with FSP prefix: ver=0, phase=0x1, flags=0, payload_len.
|
||||
| ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
|
||||
| ... | dest_coords | 16 x m bytes | Destination's ancestry |
|
||||
| ... | handshake_len | 2 bytes LE | Noise payload length |
|
||||
| ... | handshake_payload | variable | Noise XK msg1 (33 bytes — ephemeral key only) |
|
||||
| ... | handshake_payload | variable | Noise XX msg1 (33 bytes — ephemeral key only) |
|
||||
|
||||
### SessionAck (phase 0x2)
|
||||
|
||||
Second message of the Noise XK handshake. The responder sends its
|
||||
ephemeral key and encrypted epoch.
|
||||
Second message of the Noise XX handshake. The responder reveals its
|
||||
identity (ephemeral key, encrypted static key, and encrypted epoch).
|
||||
Optional FSP negotiation payload may be appended (omitted for rekey).
|
||||
Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
|
||||
|
||||

|
||||
@@ -555,12 +648,13 @@ Encoded with FSP prefix: ver=0, phase=0x2, flags=0, payload_len.
|
||||
| ... | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries |
|
||||
| ... | dest_coords | 16 x m bytes | Initiator's ancestry (for return-path cache warming) |
|
||||
| ... | handshake_len | 2 bytes LE | Noise payload length |
|
||||
| ... | handshake_payload | variable | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch) |
|
||||
| ... | handshake_payload | variable | Noise XX msg2 (106+ bytes — ephemeral + encrypted static + encrypted epoch + optional negotiation) |
|
||||
|
||||
### SessionMsg3 (phase 0x3)
|
||||
|
||||
Third and final message of the Noise XK handshake. The initiator reveals
|
||||
its encrypted static identity and epoch. After msg3, both parties derive
|
||||
Third and final message of the Noise XX handshake. The initiator reveals
|
||||
its encrypted static identity and epoch. Optional FSP negotiation payload
|
||||
may be appended (omitted for rekey). After msg3, both parties derive
|
||||
identical symmetric session keys and the session is established.
|
||||
Encoded with FSP prefix: ver=0, phase=0x3, flags=0, payload_len.
|
||||
|
||||
@@ -572,9 +666,9 @@ Encoded with FSP prefix: ver=0, phase=0x3, flags=0, payload_len.
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
| 0 | flags | 1 byte | Reserved |
|
||||
| 1 | handshake_len | 2 bytes LE | Noise payload length |
|
||||
| 3 | handshake_payload | variable | Noise XK msg3 (73 bytes — encrypted static + encrypted epoch) |
|
||||
| 3 | handshake_payload | variable | Noise XX msg3 (73+ bytes — encrypted static + encrypted epoch + optional negotiation) |
|
||||
|
||||
**Noise XK msg3 breakdown** (73 bytes):
|
||||
**Noise XX msg3 breakdown** (73 bytes base):
|
||||
|
||||
| Offset | Field | Size | Description |
|
||||
| ------ | ----- | ---- | ----------- |
|
||||
@@ -823,29 +917,33 @@ endpoint session keys).
|
||||
|
||||
## Size Summary
|
||||
|
||||
### FMP Handshake Messages (Noise IK)
|
||||
### FMP Handshake Messages (Noise XX)
|
||||
|
||||
| Message | Raw Noise | Wire Frame |
|
||||
| ------- | --------- | ---------- |
|
||||
| IK msg1 (ephemeral + encrypted static + encrypted epoch) | 106 bytes | 114 bytes |
|
||||
| IK msg2 (ephemeral + encrypted epoch) | 57 bytes | 69 bytes |
|
||||
| XX msg1 (ephemeral only) | 33 bytes | 41 bytes |
|
||||
| XX msg2 (ephemeral + encrypted static + epoch) | 106 bytes | 144 bytes (with negotiation) |
|
||||
| XX msg3 (encrypted static + epoch) | 73 bytes | 111 bytes (with negotiation) |
|
||||
|
||||
### FSP Handshake Messages (Noise XK)
|
||||
### FSP Handshake Messages (Noise XX)
|
||||
|
||||
| Message | Raw Noise | Notes |
|
||||
| ------- | --------- | ----- |
|
||||
| XK msg1 (ephemeral only) | 33 bytes | Carried in SessionSetup |
|
||||
| XK msg2 (ephemeral + encrypted epoch) | 57 bytes | Carried in SessionAck |
|
||||
| XK msg3 (encrypted static + encrypted epoch) | 73 bytes | Carried in SessionMsg3 |
|
||||
| XX msg1 (ephemeral only) | 33 bytes | Carried in SessionSetup |
|
||||
| XX msg2 (ephemeral + encrypted static + epoch) | 106 bytes | Carried in SessionAck |
|
||||
| XX msg3 (encrypted static + epoch) | 73 bytes | Carried in SessionMsg3 |
|
||||
|
||||
### Link-Layer Messages (inside encrypted frame)
|
||||
|
||||
| Message | Size | Notes |
|
||||
| ------- | ---- | ----- |
|
||||
| TreeAnnounce | 100 + 32n bytes | n = depth + 1 |
|
||||
| FilterAnnounce | 1,035 bytes | v1 (1KB filter) |
|
||||
| LookupRequest | 303 + 16n bytes | n = origin depth + 1 |
|
||||
| LookupResponse | 93 + 16n bytes | n = target depth + 1 |
|
||||
| FilterAnnounce | variable | 19-byte header + RLE-compressed payload |
|
||||
| FilterNack | 9 bytes | |
|
||||
| LookupRequest | 44 bytes + TLV | Fixed (no longer depth-dependent) |
|
||||
| LookupResponse | 93 + 16n bytes + TLV | n = target depth + 1 |
|
||||
| SenderReport | 20 bytes | Extensibility header |
|
||||
| ReceiverReport | 54 bytes | Extensibility header |
|
||||
| SessionDatagram | 36 + payload bytes | Fixed 36-byte header |
|
||||
| Disconnect | 2 bytes | |
|
||||
|
||||
@@ -853,13 +951,13 @@ endpoint session keys).
|
||||
|
||||
| Message | Typical Size | Notes |
|
||||
| ------- | ------------ | ----- |
|
||||
| SessionSetup | ~170 bytes | Depth-dependent (XK msg1 = 33 bytes) |
|
||||
| SessionAck | ~190 bytes | Depth-dependent, carries both endpoints' coords (XK msg2 = 57 bytes) |
|
||||
| SessionMsg3 | ~80 bytes | Fixed (XK msg3 = 73 bytes, no coords) |
|
||||
| SessionSetup | ~170 bytes | Depth-dependent (XX msg1 = 33 bytes) |
|
||||
| SessionAck | ~240 bytes | Depth-dependent, carries both endpoints' coords (XX msg2 = 106+ bytes) |
|
||||
| SessionMsg3 | ~80 bytes | Fixed (XX msg3 = 73+ bytes, no coords) |
|
||||
| Data (minimal) | 12 + 6 + 4 + payload + 16 bytes | Steady state (port header included) |
|
||||
| Data (with coords) | 12 + ~130 + 6 + 4 + payload + 16 bytes | Warmup/recovery (port header included) |
|
||||
| SenderReport | 12 + 6 + 46 + 16 bytes | MMP metrics |
|
||||
| ReceiverReport | 12 + 6 + 66 + 16 bytes | MMP metrics |
|
||||
| SenderReport | 12 + 6 + 20 + 16 bytes | MMP metrics |
|
||||
| ReceiverReport | 12 + 6 + 54 + 16 bytes | MMP metrics |
|
||||
| PathMtuNotification | 12 + 6 + 2 + 16 bytes | MTU signal |
|
||||
| CoordsWarmup | 12 + coords + 6 + 16 bytes | Standalone warmup (empty body) |
|
||||
| CoordsRequired | 38 bytes | Fixed (prefix + msg_type + body) |
|
||||
|
||||