Lesson 9
Reading the Wire
Every preceding lesson worked at the level of boxes and arrows: a packet enters a router, the router decrypts the link layer, reads the routing envelope, and forwards. This lesson zooms in on the bytes that make those boxes real. The goal is to make you comfortable walking through a capture or staring at a hex dump and recognizing what each byte is for.
Ground rules
- Multi-byte integers are little-endian.
-
A
NodeAddris 16 bytes (truncated SHA-256 of the x-only public key). - Signatures are 64 bytes (secp256k1 Schnorr).
- Variable-length arrays use a 2-byte little-endian count prefix, then that many fixed-size items.
-
Public keys on the wire are 33 bytes (compressed secp256k1, the
0x02/0x03prefix plus 32 bytes of X).
The 4-byte common prefix
Every FMP and FSP packet starts with the same 4-byte common prefix. It is the only piece of either protocol that a transit transport can depend on without knowing anything else about the link session.
Phase is how a receiver tells handshake packets apart from established traffic:
0x0 is post-handshake,
0x1 and 0x2 are Noise IK msg1 and msg2 at
the link layer,
0x1, 0x2, and
0x3 are SessionSetup / SessionAck / SessionMsg3 at the session layer
(FSP adds a third handshake message for XK).
Flags are per-packet signals. The interesting ones at the link layer are
K (key epoch during rekey), CE
(ECN echo), and SP (spin bit for RTT). At the FSP layer the CP bit signals that cleartext coordinates follow the header, and U marks
the packet as unencrypted (used by the three error signals).
Link frame, encrypted
The 16-byte FMP outer header is the AEAD associated data: common prefix,
receiver_idx (so the receiver can find the session in O(1)), and an
8-byte counter that doubles as the AEAD nonce and the replay-window sequence number. ChaCha20-Poly1305
covers everything after. The 16-byte Poly1305 tag sits at the end of the frame.
Inside the AEAD plaintext, the first five bytes are a link-layer inner header: a 4-byte
session-relative timestamp and the 1-byte
msg_type. Everything after that is whatever the message type says
it is: TreeAnnounce, FilterAnnounce, SessionDatagram, LookupRequest, and so on.
SessionDatagram, the routing envelope
Multi-hop traffic rides inside a SessionDatagram, which is what transit routers actually
look at. Its fixed fields are
ttl (1B), path_mtu (2B LE, min'd at every
hop), src_addr and
dest_addr (16B each). After the header comes an opaque FSP message that
the router will not, and cannot, decrypt.
FSP brings its own cleartext 12-byte header: the same common prefix plus an 8-byte AEAD
counter, matching the link layer's structure. When
CP is set, the next bytes are two variable-length coordinate arrays
before the ciphertext. Transit routers parse them and update their caches without touching the
ciphertext.
Where the overhead goes
Every IPv6 packet carried by FIPS has
77 bytes of protocol overhead at the link layer:
106 bytes of base FSP + FMP + SessionDatagram,
4 bytes for the
src_port / dst_port, minus 33 bytes saved by the IPv6 shim compressing the 40-byte IPv6 header down to 7 bytes of residual
fields.
Walk it yourself
Pick a message type below. Each block is a field, sized roughly by its byte count. Click a block to see what it means, and adjust the toggles to watch the totals change.
An IPv6 ping inside FSP inside SessionDatagram inside FMP. Toggle CP to watch the cleartext coordinates appear between the FSP header and the ciphertext.
Click any block above to see what the field is and what it means on the wire.
Message-type table, at a glance
Link-layer (FMP) message types
| 0x00 | SessionDatagram |
| 0x01 | SenderReport |
| 0x02 | ReceiverReport |
| 0x10 | TreeAnnounce |
| 0x20 | FilterAnnounce |
| 0x30 | LookupRequest |
| 0x31 | LookupResponse |
| 0x50 | Disconnect |
| 0x51 | Heartbeat |
Session-layer (FSP) message types
| 0x10 | Data (port-multiplexed) |
| 0x11 | SenderReport |
| 0x12 | ReceiverReport |
| 0x13 | PathMtuNotification |
| 0x14 | CoordsWarmup |
| 0x20 | CoordsRequired (plaintext) |
| 0x21 | PathBroken (plaintext) |
| 0x22 | MtuExceeded (plaintext) |
A couple of details worth remembering
- TreeAnnounce carries a Schnorr signature over the whole message. Any peer can verify it before replaying the state, so tree updates do not depend on trusting the relay.
-
LookupResponse signs
(request_id || target || target_coords). Thepath_mtufield is excluded from the signature because every transit hop rewrites it. -
Error signals (CoordsRequired, PathBroken, MtuExceeded) set the
Uflag in the FSP common prefix and carry a plaintext body. No session keys are needed to read them. - The Ethernet transport adds a 3-byte header (frame type + 2-byte length) on data frames so the receiver can trim Ethernet minimum-frame padding that would otherwise corrupt AEAD verification.
Lesson quiz
1. How many bytes is the FMP common prefix, and what does it contain?
2. What is the purpose of the receiver_idx field in an established FMP frame?
3. Which field is excluded from the LookupResponse's Schnorr signature, and why?
4. A transit router sees a SessionDatagram go by. What can it read, and what is it blind to?
5. IPv6 traffic through FIPS has 77 bytes of net overhead. Where does the number come from?
6. How does a receiver know whether an FSP packet carries cleartext coordinates?