fips_core/protocol/session.rs
1//! Session-layer message types: setup, ack, data, and error messages.
2
3use super::ProtocolError;
4use crate::NodeAddr;
5use crate::tree::TreeCoordinate;
6use std::fmt;
7
8mod control_messages;
9
10pub use control_messages::{
11 COORDS_REQUIRED_SIZE, CoordsRequired, MTU_EXCEEDED_SIZE, MtuExceeded,
12 PATH_MTU_NOTIFICATION_SIZE, PathBroken, PathMtuNotification,
13};
14
15// ============================================================================
16// Session Layer Message Types
17// ============================================================================
18
19/// SessionDatagram payload message type identifiers.
20///
21/// These messages are carried as payloads inside `SessionDatagram` (link
22/// message type 0x00). Post-handshake messages (data, reports) are end-to-end
23/// encrypted with session keys via the FSP pipeline. Error signals
24/// (CoordsRequired, PathBroken) are plaintext messages generated by transit
25/// routers that cannot establish e2e sessions with the source.
26///
27/// Handshake messages (SessionSetup, SessionAck, SessionMsg3) are **not**
28/// identified by a message-type byte; they are dispatched by the FSP phase
29/// nibble in the common prefix (0x1, 0x2, 0x3 respectively). The 0x00-0x0F
30/// range is therefore unallocated in this enum.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32#[repr(u8)]
33pub enum SessionMessageType {
34 // Data and metrics (0x10-0x1F) — encrypted, inner header msg_type
35 /// Port-multiplexed service payload: `[src_port:2 LE][dst_port:2 LE][service data...]`.
36 /// Port 256 = IPv6 shim (compressed header). Receiver dispatches by dst_port.
37 DataPacket = 0x10,
38 /// MMP sender report (metrics from sender to receiver).
39 SenderReport = 0x11,
40 /// MMP receiver report (metrics from receiver to sender).
41 ReceiverReport = 0x12,
42 /// Path MTU notification (discovered path MTU).
43 PathMtuNotification = 0x13,
44 /// Standalone coordinate cache warming (empty body, coords in CP flag).
45 CoordsWarmup = 0x14,
46 /// App-facing endpoint payload, without DataPacket port dispatch.
47 EndpointData = 0x15,
48 /// NAT traversal offer carried over an established end-to-end FIPS session.
49 TraversalOffer = 0x16,
50 /// NAT traversal answer carried over an established end-to-end FIPS session.
51 TraversalAnswer = 0x17,
52
53 // Link-layer error signals (0x20-0x2F) — plaintext, from transit routers
54 /// Router cache miss — needs coordinates (link-layer error signal).
55 CoordsRequired = 0x20,
56 /// Routing failure — local minimum or unreachable (link-layer error signal).
57 PathBroken = 0x21,
58 /// MTU exceeded — forwarded packet too large for next-hop transport (link-layer error signal).
59 MtuExceeded = 0x22,
60}
61
62impl SessionMessageType {
63 /// Try to convert from a byte.
64 pub fn from_byte(b: u8) -> Option<Self> {
65 match b {
66 0x10 => Some(SessionMessageType::DataPacket),
67 0x11 => Some(SessionMessageType::SenderReport),
68 0x12 => Some(SessionMessageType::ReceiverReport),
69 0x13 => Some(SessionMessageType::PathMtuNotification),
70 0x14 => Some(SessionMessageType::CoordsWarmup),
71 0x15 => Some(SessionMessageType::EndpointData),
72 0x16 => Some(SessionMessageType::TraversalOffer),
73 0x17 => Some(SessionMessageType::TraversalAnswer),
74 0x20 => Some(SessionMessageType::CoordsRequired),
75 0x21 => Some(SessionMessageType::PathBroken),
76 0x22 => Some(SessionMessageType::MtuExceeded),
77 _ => None,
78 }
79 }
80
81 /// Convert to a byte.
82 pub fn to_byte(self) -> u8 {
83 self as u8
84 }
85}
86
87impl fmt::Display for SessionMessageType {
88 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89 let name = match self {
90 SessionMessageType::DataPacket => "DataPacket",
91 SessionMessageType::SenderReport => "SenderReport",
92 SessionMessageType::ReceiverReport => "ReceiverReport",
93 SessionMessageType::PathMtuNotification => "PathMtuNotification",
94 SessionMessageType::CoordsWarmup => "CoordsWarmup",
95 SessionMessageType::EndpointData => "EndpointData",
96 SessionMessageType::TraversalOffer => "TraversalOffer",
97 SessionMessageType::TraversalAnswer => "TraversalAnswer",
98 SessionMessageType::CoordsRequired => "CoordsRequired",
99 SessionMessageType::PathBroken => "PathBroken",
100 SessionMessageType::MtuExceeded => "MtuExceeded",
101 };
102 write!(f, "{}", name)
103 }
104}
105
106// ============================================================================
107// Coordinate Wire Format Helpers
108// ============================================================================
109
110/// Wire size of a TreeCoordinate in address-only format: 2 + entries × 16.
111pub(crate) fn coords_wire_size(coords: &TreeCoordinate) -> usize {
112 2 + coords.entries().len() * 16
113}
114
115/// Encode a TreeCoordinate as address-only wire format: count(u16 LE) + addrs(16 × n).
116///
117/// Session-layer messages serialize coordinates as NodeAddr arrays (16 bytes each),
118/// without the sequence/timestamp metadata used by the tree gossip protocol.
119pub(crate) fn encode_coords(coords: &TreeCoordinate, buf: &mut Vec<u8>) {
120 let addrs: Vec<&NodeAddr> = coords.node_addrs().collect();
121 let count = addrs.len() as u16;
122 buf.extend_from_slice(&count.to_le_bytes());
123 for addr in addrs {
124 buf.extend_from_slice(addr.as_bytes());
125 }
126}
127
128/// Decode a TreeCoordinate from address-only wire format.
129///
130/// Returns the decoded coordinate and the number of bytes consumed.
131pub(crate) fn decode_coords(data: &[u8]) -> Result<(TreeCoordinate, usize), ProtocolError> {
132 if data.len() < 2 {
133 return Err(ProtocolError::MessageTooShort {
134 expected: 2,
135 got: data.len(),
136 });
137 }
138 let count = u16::from_le_bytes([data[0], data[1]]) as usize;
139 let needed = 2 + count * 16;
140 if data.len() < needed {
141 return Err(ProtocolError::MessageTooShort {
142 expected: needed,
143 got: data.len(),
144 });
145 }
146 if count == 0 {
147 return Err(ProtocolError::Malformed(
148 "coordinate with zero entries".into(),
149 ));
150 }
151 let mut addrs = Vec::with_capacity(count);
152 for i in 0..count {
153 let offset = 2 + i * 16;
154 let mut bytes = [0u8; 16];
155 bytes.copy_from_slice(&data[offset..offset + 16]);
156 addrs.push(NodeAddr::from_bytes(bytes));
157 }
158 let coord =
159 TreeCoordinate::from_addrs(addrs).map_err(|e| ProtocolError::Malformed(e.to_string()))?;
160 Ok((coord, needed))
161}
162
163/// Decode an optional coordinate field (count may be 0).
164///
165/// Returns None if count is 0, Some(coord) otherwise, plus bytes consumed.
166pub(crate) fn decode_optional_coords(
167 data: &[u8],
168) -> Result<(Option<TreeCoordinate>, usize), ProtocolError> {
169 if data.len() < 2 {
170 return Err(ProtocolError::MessageTooShort {
171 expected: 2,
172 got: data.len(),
173 });
174 }
175 let count = u16::from_le_bytes([data[0], data[1]]) as usize;
176 let needed = 2 + count * 16;
177 if data.len() < needed {
178 return Err(ProtocolError::MessageTooShort {
179 expected: needed,
180 got: data.len(),
181 });
182 }
183 if count == 0 {
184 return Ok((None, 2));
185 }
186 let mut addrs = Vec::with_capacity(count);
187 for i in 0..count {
188 let offset = 2 + i * 16;
189 let mut bytes = [0u8; 16];
190 bytes.copy_from_slice(&data[offset..offset + 16]);
191 addrs.push(NodeAddr::from_bytes(bytes));
192 }
193 let coord =
194 TreeCoordinate::from_addrs(addrs).map_err(|e| ProtocolError::Malformed(e.to_string()))?;
195 Ok((Some(coord), needed))
196}
197
198/// Encode a count of zero (for empty/absent coordinate fields).
199fn encode_empty_coords(buf: &mut Vec<u8>) {
200 buf.extend_from_slice(&0u16.to_le_bytes());
201}
202
203// ============================================================================
204// Session Flags
205// ============================================================================
206
207/// Session flags for setup options.
208#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
209pub struct SessionFlags {
210 /// Request acknowledgement from destination.
211 pub request_ack: bool,
212 /// Set up bidirectional session.
213 pub bidirectional: bool,
214}
215
216impl SessionFlags {
217 /// Create default flags.
218 pub fn new() -> Self {
219 Self::default()
220 }
221
222 /// Set request_ack flag.
223 pub fn with_ack(mut self) -> Self {
224 self.request_ack = true;
225 self
226 }
227
228 /// Set bidirectional flag.
229 pub fn bidirectional(mut self) -> Self {
230 self.bidirectional = true;
231 self
232 }
233
234 /// Convert to a byte.
235 pub fn to_byte(&self) -> u8 {
236 let mut flags = 0u8;
237 if self.request_ack {
238 flags |= 0x01;
239 }
240 if self.bidirectional {
241 flags |= 0x02;
242 }
243 flags
244 }
245
246 /// Convert from a byte.
247 pub fn from_byte(byte: u8) -> Self {
248 Self {
249 request_ack: byte & 0x01 != 0,
250 bidirectional: byte & 0x02 != 0,
251 }
252 }
253}
254
255// ============================================================================
256// FSP Packet Flags
257// ============================================================================
258
259/// FSP common prefix flags (cleartext, in outer header).
260///
261/// | Bit | Name | Description |
262/// |-----|------|------------------------------------------------|
263/// | 0 | CP | Coords present between header and ciphertext |
264/// | 1 | K | Key epoch (for rekeying) |
265/// | 2 | U | Unencrypted payload (error signals) |
266/// | 3-7 | | Reserved |
267#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
268pub struct FspFlags {
269 /// Coordinates present between header and ciphertext.
270 pub coords_present: bool,
271 /// Key epoch bit for rekeying.
272 pub key_epoch: bool,
273 /// Unencrypted payload (plaintext error signals from transit routers).
274 pub unencrypted: bool,
275}
276
277impl FspFlags {
278 /// Create default flags (all clear).
279 pub fn new() -> Self {
280 Self::default()
281 }
282
283 /// Convert to a byte.
284 pub fn to_byte(&self) -> u8 {
285 let mut flags = 0u8;
286 if self.coords_present {
287 flags |= 0x01;
288 }
289 if self.key_epoch {
290 flags |= 0x02;
291 }
292 if self.unencrypted {
293 flags |= 0x04;
294 }
295 flags
296 }
297
298 /// Convert from a byte.
299 pub fn from_byte(byte: u8) -> Self {
300 Self {
301 coords_present: byte & 0x01 != 0,
302 key_epoch: byte & 0x02 != 0,
303 unencrypted: byte & 0x04 != 0,
304 }
305 }
306}
307
308/// FSP inner header flags (encrypted, inside AEAD envelope).
309///
310/// | Bit | Name | Description |
311/// |-----|------|---------------------------------|
312/// | 0 | SP | Spin bit for RTT measurement |
313/// | 1-7 | | Reserved |
314#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
315pub struct FspInnerFlags {
316 /// Spin bit for passive RTT measurement.
317 pub spin_bit: bool,
318}
319
320impl FspInnerFlags {
321 /// Create default inner flags (all clear).
322 pub fn new() -> Self {
323 Self::default()
324 }
325
326 /// Convert to a byte.
327 pub fn to_byte(&self) -> u8 {
328 if self.spin_bit { 0x01 } else { 0x00 }
329 }
330
331 /// Convert from a byte.
332 pub fn from_byte(byte: u8) -> Self {
333 Self {
334 spin_bit: byte & 0x01 != 0,
335 }
336 }
337}
338
339// ============================================================================
340// Session Setup
341// ============================================================================
342
343/// Session setup to establish cached coordinate state.
344///
345/// Carried inside a SessionDatagram envelope which provides src_addr and
346/// dest_addr. The SessionSetup payload contains coordinates, session flags,
347/// and the Noise XK handshake message for session establishment.
348///
349/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
350/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte.
351/// The `msg_type` field in the encrypted inner header applies only to
352/// established-phase (0x0) messages.
353///
354/// ## Wire Format
355///
356/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`,
357/// where `ver_phase = 0x01` (version 0, phase MSG1) and `flags = 0` for handshake.
358///
359/// **Body** (after 4-byte FSP prefix):
360///
361/// | Offset | Field | Size | Description |
362/// |--------|-------------------|------------|-----------------------------------------------------|
363/// | 0 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL |
364/// | 1 | src_coords_count | 2 bytes LE | Number of source coordinate entries |
365/// | 3 | src_coords | 16 × n | Source's ancestry (NodeAddr, self → root) |
366/// | ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
367/// | ... | dest_coords | 16 × m | Destination's ancestry |
368/// | ... | handshake_len | 2 bytes LE | Noise payload length |
369/// | ... | handshake_payload | variable | Noise XK msg1 (33 bytes — ephemeral key only) |
370#[derive(Clone, Debug)]
371pub struct SessionSetup {
372 /// Source coordinates (for return path caching).
373 pub src_coords: TreeCoordinate,
374 /// Destination coordinates (for forward routing).
375 pub dest_coords: TreeCoordinate,
376 /// Session options.
377 pub flags: SessionFlags,
378 /// Noise IK handshake message 1.
379 pub handshake_payload: Vec<u8>,
380}
381
382impl SessionSetup {
383 /// Create a new session setup message.
384 pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
385 Self {
386 src_coords,
387 dest_coords,
388 flags: SessionFlags::new(),
389 handshake_payload: Vec::new(),
390 }
391 }
392
393 /// Set session flags.
394 pub fn with_flags(mut self, flags: SessionFlags) -> Self {
395 self.flags = flags;
396 self
397 }
398
399 /// Set the Noise handshake payload.
400 pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
401 self.handshake_payload = payload;
402 self
403 }
404
405 /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
406 ///
407 /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
408 /// where ver_phase = 0x01 (version 0, phase MSG1).
409 pub fn encode(&self) -> Vec<u8> {
410 // Build body first to compute payload_len
411 let mut body = Vec::new();
412 body.push(self.flags.to_byte());
413 encode_coords(&self.src_coords, &mut body);
414 encode_coords(&self.dest_coords, &mut body);
415 let hs_len = self.handshake_payload.len() as u16;
416 body.extend_from_slice(&hs_len.to_le_bytes());
417 body.extend_from_slice(&self.handshake_payload);
418
419 // Prepend 4-byte FSP common prefix
420 let payload_len = body.len() as u16;
421 let mut buf = Vec::with_capacity(4 + body.len());
422 buf.push(0x01); // version 0, phase 0x1 (MSG1)
423 buf.push(0x00); // flags (must be zero for handshake)
424 buf.extend_from_slice(&payload_len.to_le_bytes());
425 buf.extend_from_slice(&body);
426 buf
427 }
428
429 /// Decode from wire format (after 4-byte FSP prefix has been consumed).
430 pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
431 if payload.is_empty() {
432 return Err(ProtocolError::MessageTooShort {
433 expected: 1,
434 got: 0,
435 });
436 }
437 let flags = SessionFlags::from_byte(payload[0]);
438 let mut offset = 1;
439
440 let (src_coords, consumed) = decode_coords(&payload[offset..])?;
441 offset += consumed;
442
443 let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
444 offset += consumed;
445
446 if payload.len() < offset + 2 {
447 return Err(ProtocolError::MessageTooShort {
448 expected: offset + 2,
449 got: payload.len(),
450 });
451 }
452 let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
453 offset += 2;
454
455 if payload.len() < offset + hs_len {
456 return Err(ProtocolError::MessageTooShort {
457 expected: offset + hs_len,
458 got: payload.len(),
459 });
460 }
461 let handshake_payload = payload[offset..offset + hs_len].to_vec();
462
463 Ok(Self {
464 src_coords,
465 dest_coords,
466 flags,
467 handshake_payload,
468 })
469 }
470}
471
472// ============================================================================
473// Session Ack
474// ============================================================================
475
476/// Session acknowledgement.
477///
478/// Carried inside a SessionDatagram envelope which provides src_addr and
479/// dest_addr. The SessionAck payload contains both the acknowledger's and
480/// initiator's coordinates for route cache warming (ensuring return-path
481/// transit nodes can route independently of the forward path) and the Noise
482/// XK handshake response.
483///
484/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
485/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte.
486///
487/// ## Wire Format
488///
489/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`,
490/// where `ver_phase = 0x02` (version 0, phase MSG2) and `flags = 0` for handshake.
491///
492/// **Body** (after 4-byte FSP prefix):
493///
494/// | Offset | Field | Size | Description |
495/// |--------|-------------------|------------|--------------------------------------------------------------|
496/// | 0 | flags | 1 byte | Reserved |
497/// | 1 | src_coords_count | 2 bytes LE | Number of acknowledger coordinate entries |
498/// | 3 | src_coords | 16 × n | Acknowledger's ancestry (for cache warming) |
499/// | ... | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries |
500/// | ... | dest_coords | 16 × m | Initiator's ancestry (for return-path cache warming) |
501/// | ... | handshake_len | 2 bytes LE | Noise payload length |
502/// | ... | handshake_payload | variable | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch) |
503#[derive(Clone, Debug)]
504pub struct SessionAck {
505 /// Acknowledger's coordinates.
506 pub src_coords: TreeCoordinate,
507 /// Initiator's coordinates (for return-path cache warming).
508 pub dest_coords: TreeCoordinate,
509 /// Reserved flags byte (for forward compatibility).
510 pub flags: u8,
511 /// Noise IK handshake message 2.
512 pub handshake_payload: Vec<u8>,
513}
514
515impl SessionAck {
516 /// Create a new session acknowledgement.
517 pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
518 Self {
519 src_coords,
520 dest_coords,
521 flags: 0,
522 handshake_payload: Vec::new(),
523 }
524 }
525
526 /// Set the Noise handshake payload.
527 pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
528 self.handshake_payload = payload;
529 self
530 }
531
532 /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
533 ///
534 /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
535 /// where ver_phase = 0x02 (version 0, phase MSG2).
536 pub fn encode(&self) -> Vec<u8> {
537 // Build body first to compute payload_len
538 let mut body = Vec::new();
539 body.push(self.flags);
540 encode_coords(&self.src_coords, &mut body);
541 encode_coords(&self.dest_coords, &mut body);
542 let hs_len = self.handshake_payload.len() as u16;
543 body.extend_from_slice(&hs_len.to_le_bytes());
544 body.extend_from_slice(&self.handshake_payload);
545
546 // Prepend 4-byte FSP common prefix
547 let payload_len = body.len() as u16;
548 let mut buf = Vec::with_capacity(4 + body.len());
549 buf.push(0x02); // version 0, phase 0x2 (MSG2)
550 buf.push(0x00); // flags (must be zero for handshake)
551 buf.extend_from_slice(&payload_len.to_le_bytes());
552 buf.extend_from_slice(&body);
553 buf
554 }
555
556 /// Decode from wire format (after 4-byte FSP prefix has been consumed).
557 pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
558 if payload.is_empty() {
559 return Err(ProtocolError::MessageTooShort {
560 expected: 1,
561 got: 0,
562 });
563 }
564 let flags = payload[0];
565 let mut offset = 1;
566
567 let (src_coords, consumed) = decode_coords(&payload[offset..])?;
568 offset += consumed;
569
570 let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
571 offset += consumed;
572
573 if payload.len() < offset + 2 {
574 return Err(ProtocolError::MessageTooShort {
575 expected: offset + 2,
576 got: payload.len(),
577 });
578 }
579 let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
580 offset += 2;
581
582 if payload.len() < offset + hs_len {
583 return Err(ProtocolError::MessageTooShort {
584 expected: offset + hs_len,
585 got: payload.len(),
586 });
587 }
588 let handshake_payload = payload[offset..offset + hs_len].to_vec();
589
590 Ok(Self {
591 src_coords,
592 dest_coords,
593 flags,
594 handshake_payload,
595 })
596 }
597}
598
599// ============================================================================
600// Session Msg3 (XK Handshake Message 3)
601// ============================================================================
602
603/// XK handshake message 3 (initiator -> responder).
604///
605/// Carries the initiator's encrypted static key and epoch. Sent by the
606/// initiator after receiving msg2. The responder learns the initiator's
607/// identity from this message.
608///
609/// ## Wire Format
610///
611/// | Offset | Field | Size | Description |
612/// |--------|------------------|---------|-------------------------------------|
613/// | 0 | flags | 1 byte | Reserved |
614/// | 1 | handshake_len | 2 bytes | u16 LE, Noise payload length |
615/// | 3 | handshake_payload| variable| Noise XK msg3 (73 bytes typical) |
616#[derive(Clone, Debug)]
617pub struct SessionMsg3 {
618 /// Reserved flags byte.
619 pub flags: u8,
620 /// Noise XK handshake message 3.
621 pub handshake_payload: Vec<u8>,
622}
623
624impl SessionMsg3 {
625 /// Create a new SessionMsg3 with the given handshake payload.
626 pub fn new(handshake_payload: Vec<u8>) -> Self {
627 Self {
628 flags: 0,
629 handshake_payload,
630 }
631 }
632
633 /// Encode as wire format (4-byte FSP prefix + flags + handshake).
634 ///
635 /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
636 /// where ver_phase = 0x03 (version 0, phase MSG3).
637 pub fn encode(&self) -> Vec<u8> {
638 // Build body first to compute payload_len
639 let mut body = Vec::new();
640 body.push(self.flags);
641 let hs_len = self.handshake_payload.len() as u16;
642 body.extend_from_slice(&hs_len.to_le_bytes());
643 body.extend_from_slice(&self.handshake_payload);
644
645 // Prepend 4-byte FSP common prefix
646 let payload_len = body.len() as u16;
647 let mut buf = Vec::with_capacity(4 + body.len());
648 buf.push(0x03); // version 0, phase 0x3 (MSG3)
649 buf.push(0x00); // flags (must be zero for handshake)
650 buf.extend_from_slice(&payload_len.to_le_bytes());
651 buf.extend_from_slice(&body);
652 buf
653 }
654
655 /// Decode from wire format (after 4-byte FSP prefix has been consumed).
656 pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
657 if payload.is_empty() {
658 return Err(ProtocolError::MessageTooShort {
659 expected: 1,
660 got: 0,
661 });
662 }
663 let flags = payload[0];
664 let mut offset = 1;
665
666 if payload.len() < offset + 2 {
667 return Err(ProtocolError::MessageTooShort {
668 expected: offset + 2,
669 got: payload.len(),
670 });
671 }
672 let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
673 offset += 2;
674
675 if payload.len() < offset + hs_len {
676 return Err(ProtocolError::MessageTooShort {
677 expected: offset + hs_len,
678 got: payload.len(),
679 });
680 }
681 let handshake_payload = payload[offset..offset + hs_len].to_vec();
682
683 Ok(Self {
684 flags,
685 handshake_payload,
686 })
687 }
688}
689
690// ============================================================================
691// Session-Layer MMP Reports
692// ============================================================================
693
694/// Session-layer sender report (msg_type 0x11).
695///
696/// Mirrors the FMP `SenderReport` fields but carried as an FSP session
697/// message inside the AEAD envelope. The msg_type is in the FSP inner
698/// header, so the body starts with reserved bytes.
699///
700/// ## Wire Format (46 bytes body, after inner header stripped)
701///
702/// ```text
703/// [0-1] reserved (zero)
704/// [2-9] interval_start_counter: u64 LE
705/// [10-17] interval_end_counter: u64 LE
706/// [18-21] interval_start_timestamp: u32 LE
707/// [22-25] interval_end_timestamp: u32 LE
708/// [26-29] interval_bytes_sent: u32 LE
709/// [30-37] cumulative_packets_sent: u64 LE
710/// [38-45] cumulative_bytes_sent: u64 LE
711/// ```
712#[derive(Debug, Clone, PartialEq, Eq)]
713pub struct SessionSenderReport {
714 pub interval_start_counter: u64,
715 pub interval_end_counter: u64,
716 pub interval_start_timestamp: u32,
717 pub interval_end_timestamp: u32,
718 pub interval_bytes_sent: u32,
719 pub cumulative_packets_sent: u64,
720 pub cumulative_bytes_sent: u64,
721}
722
723/// Body size for SessionSenderReport: 2 reserved + 44 fields.
724pub const SESSION_SENDER_REPORT_SIZE: usize = 46;
725
726impl SessionSenderReport {
727 /// Encode to wire format (46 bytes body).
728 pub fn encode(&self) -> Vec<u8> {
729 let mut buf = Vec::with_capacity(SESSION_SENDER_REPORT_SIZE);
730 buf.extend_from_slice(&[0u8; 2]); // reserved
731 buf.extend_from_slice(&self.interval_start_counter.to_le_bytes());
732 buf.extend_from_slice(&self.interval_end_counter.to_le_bytes());
733 buf.extend_from_slice(&self.interval_start_timestamp.to_le_bytes());
734 buf.extend_from_slice(&self.interval_end_timestamp.to_le_bytes());
735 buf.extend_from_slice(&self.interval_bytes_sent.to_le_bytes());
736 buf.extend_from_slice(&self.cumulative_packets_sent.to_le_bytes());
737 buf.extend_from_slice(&self.cumulative_bytes_sent.to_le_bytes());
738 buf
739 }
740
741 /// Decode from body (after FSP inner header has been stripped).
742 pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
743 if body.len() < SESSION_SENDER_REPORT_SIZE {
744 return Err(ProtocolError::MessageTooShort {
745 expected: SESSION_SENDER_REPORT_SIZE,
746 got: body.len(),
747 });
748 }
749 // Skip 2 reserved bytes
750 let p = &body[2..];
751 Ok(Self {
752 interval_start_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
753 interval_end_counter: u64::from_le_bytes(p[8..16].try_into().unwrap()),
754 interval_start_timestamp: u32::from_le_bytes(p[16..20].try_into().unwrap()),
755 interval_end_timestamp: u32::from_le_bytes(p[20..24].try_into().unwrap()),
756 interval_bytes_sent: u32::from_le_bytes(p[24..28].try_into().unwrap()),
757 cumulative_packets_sent: u64::from_le_bytes(p[28..36].try_into().unwrap()),
758 cumulative_bytes_sent: u64::from_le_bytes(p[36..44].try_into().unwrap()),
759 })
760 }
761}
762
763/// Session-layer receiver report (msg_type 0x12).
764///
765/// Mirrors the FMP `ReceiverReport` fields but carried as an FSP session
766/// message inside the AEAD envelope.
767///
768/// ## Wire Format (66 bytes body, after inner header stripped)
769///
770/// ```text
771/// [0-1] reserved (zero)
772/// [2-9] highest_counter: u64 LE
773/// [10-17] cumulative_packets_recv: u64 LE
774/// [18-25] cumulative_bytes_recv: u64 LE
775/// [26-29] timestamp_echo: u32 LE
776/// [30-31] dwell_time: u16 LE
777/// [32-33] max_burst_loss: u16 LE
778/// [34-35] mean_burst_loss: u16 LE (u8.8 fixed-point)
779/// [36-37] reserved: u16 LE
780/// [38-41] jitter: u32 LE (microseconds)
781/// [42-45] ecn_ce_count: u32 LE
782/// [46-49] owd_trend: i32 LE (µs/s)
783/// [50-53] burst_loss_count: u32 LE
784/// [54-57] cumulative_reorder_count: u32 LE
785/// [58-61] interval_packets_recv: u32 LE
786/// [62-65] interval_bytes_recv: u32 LE
787/// ```
788#[derive(Debug, Clone, PartialEq, Eq)]
789pub struct SessionReceiverReport {
790 pub highest_counter: u64,
791 pub cumulative_packets_recv: u64,
792 pub cumulative_bytes_recv: u64,
793 pub timestamp_echo: u32,
794 pub dwell_time: u16,
795 pub max_burst_loss: u16,
796 pub mean_burst_loss: u16,
797 pub jitter: u32,
798 pub ecn_ce_count: u32,
799 pub owd_trend: i32,
800 pub burst_loss_count: u32,
801 pub cumulative_reorder_count: u32,
802 pub interval_packets_recv: u32,
803 pub interval_bytes_recv: u32,
804}
805
806/// Body size for SessionReceiverReport: 2 reserved + 64 fields.
807pub const SESSION_RECEIVER_REPORT_SIZE: usize = 66;
808
809impl SessionReceiverReport {
810 /// Encode to wire format (66 bytes body).
811 pub fn encode(&self) -> Vec<u8> {
812 let mut buf = Vec::with_capacity(SESSION_RECEIVER_REPORT_SIZE);
813 buf.extend_from_slice(&[0u8; 2]); // reserved
814 buf.extend_from_slice(&self.highest_counter.to_le_bytes());
815 buf.extend_from_slice(&self.cumulative_packets_recv.to_le_bytes());
816 buf.extend_from_slice(&self.cumulative_bytes_recv.to_le_bytes());
817 buf.extend_from_slice(&self.timestamp_echo.to_le_bytes());
818 buf.extend_from_slice(&self.dwell_time.to_le_bytes());
819 buf.extend_from_slice(&self.max_burst_loss.to_le_bytes());
820 buf.extend_from_slice(&self.mean_burst_loss.to_le_bytes());
821 buf.extend_from_slice(&[0u8; 2]); // reserved
822 buf.extend_from_slice(&self.jitter.to_le_bytes());
823 buf.extend_from_slice(&self.ecn_ce_count.to_le_bytes());
824 buf.extend_from_slice(&self.owd_trend.to_le_bytes());
825 buf.extend_from_slice(&self.burst_loss_count.to_le_bytes());
826 buf.extend_from_slice(&self.cumulative_reorder_count.to_le_bytes());
827 buf.extend_from_slice(&self.interval_packets_recv.to_le_bytes());
828 buf.extend_from_slice(&self.interval_bytes_recv.to_le_bytes());
829 buf
830 }
831
832 /// Decode from body (after FSP inner header has been stripped).
833 pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
834 if body.len() < SESSION_RECEIVER_REPORT_SIZE {
835 return Err(ProtocolError::MessageTooShort {
836 expected: SESSION_RECEIVER_REPORT_SIZE,
837 got: body.len(),
838 });
839 }
840 // Skip 2 reserved bytes
841 let p = &body[2..];
842 Ok(Self {
843 highest_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
844 cumulative_packets_recv: u64::from_le_bytes(p[8..16].try_into().unwrap()),
845 cumulative_bytes_recv: u64::from_le_bytes(p[16..24].try_into().unwrap()),
846 timestamp_echo: u32::from_le_bytes(p[24..28].try_into().unwrap()),
847 dwell_time: u16::from_le_bytes(p[28..30].try_into().unwrap()),
848 max_burst_loss: u16::from_le_bytes(p[30..32].try_into().unwrap()),
849 mean_burst_loss: u16::from_le_bytes(p[32..34].try_into().unwrap()),
850 // skip 2 reserved bytes at p[34..36]
851 jitter: u32::from_le_bytes(p[36..40].try_into().unwrap()),
852 ecn_ce_count: u32::from_le_bytes(p[40..44].try_into().unwrap()),
853 owd_trend: i32::from_le_bytes(p[44..48].try_into().unwrap()),
854 burst_loss_count: u32::from_le_bytes(p[48..52].try_into().unwrap()),
855 cumulative_reorder_count: u32::from_le_bytes(p[52..56].try_into().unwrap()),
856 interval_packets_recv: u32::from_le_bytes(p[56..60].try_into().unwrap()),
857 interval_bytes_recv: u32::from_le_bytes(p[60..64].try_into().unwrap()),
858 })
859 }
860}
861
862#[cfg(test)]
863mod tests;