fips_core/proto/protocol/session.rs
1//! Session-layer message types: setup, ack, data, and error messages.
2
3use super::ProtocolError;
4use crate::NodeAddr;
5use crate::proto::stp::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 /// Both endpoints may carry established FSP records directly on an
215 /// authenticated physical transport instead of wrapping them in FMP.
216 pub direct_fsp_transport: bool,
217}
218
219impl SessionFlags {
220 /// Create default flags.
221 pub fn new() -> Self {
222 Self::default()
223 }
224
225 /// Set request_ack flag.
226 pub fn with_ack(mut self) -> Self {
227 self.request_ack = true;
228 self
229 }
230
231 /// Set bidirectional flag.
232 pub fn bidirectional(mut self) -> Self {
233 self.bidirectional = true;
234 self
235 }
236
237 /// Advertise support for direct-on-transport FSP records.
238 pub fn with_direct_fsp_transport(mut self) -> Self {
239 self.direct_fsp_transport = true;
240 self
241 }
242
243 /// Convert to a byte.
244 pub fn to_byte(&self) -> u8 {
245 let mut flags = 0u8;
246 if self.request_ack {
247 flags |= 0x01;
248 }
249 if self.bidirectional {
250 flags |= 0x02;
251 }
252 if self.direct_fsp_transport {
253 flags |= 0x04;
254 }
255 flags
256 }
257
258 /// Convert from a byte.
259 pub fn from_byte(byte: u8) -> Self {
260 Self {
261 request_ack: byte & 0x01 != 0,
262 bidirectional: byte & 0x02 != 0,
263 direct_fsp_transport: byte & 0x04 != 0,
264 }
265 }
266}
267
268// ============================================================================
269// FSP Packet Flags
270// ============================================================================
271
272/// FSP common prefix flags (cleartext, in outer header).
273///
274/// | Bit | Name | Description |
275/// |-----|------|------------------------------------------------|
276/// | 0 | CP | Coords present between header and ciphertext |
277/// | 1 | K | Key epoch (for rekeying) |
278/// | 2 | U | Unencrypted payload (error signals) |
279/// | 3-7 | | Reserved |
280#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
281pub struct FspFlags {
282 /// Coordinates present between header and ciphertext.
283 pub coords_present: bool,
284 /// Key epoch bit for rekeying.
285 pub key_epoch: bool,
286 /// Unencrypted payload (plaintext error signals from transit routers).
287 pub unencrypted: bool,
288}
289
290impl FspFlags {
291 /// Create default flags (all clear).
292 pub fn new() -> Self {
293 Self::default()
294 }
295
296 /// Convert to a byte.
297 pub fn to_byte(&self) -> u8 {
298 let mut flags = 0u8;
299 if self.coords_present {
300 flags |= 0x01;
301 }
302 if self.key_epoch {
303 flags |= 0x02;
304 }
305 if self.unencrypted {
306 flags |= 0x04;
307 }
308 flags
309 }
310
311 /// Convert from a byte.
312 pub fn from_byte(byte: u8) -> Self {
313 Self {
314 coords_present: byte & 0x01 != 0,
315 key_epoch: byte & 0x02 != 0,
316 unencrypted: byte & 0x04 != 0,
317 }
318 }
319}
320
321/// FSP inner header flags (encrypted, inside AEAD envelope).
322///
323/// | Bit | Name | Description |
324/// |-----|------|---------------------------------|
325/// | 0 | SP | Spin bit for RTT measurement |
326/// | 1-7 | | Reserved |
327#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
328pub struct FspInnerFlags {
329 /// Spin bit for passive RTT measurement.
330 pub spin_bit: bool,
331}
332
333impl FspInnerFlags {
334 /// Create default inner flags (all clear).
335 pub fn new() -> Self {
336 Self::default()
337 }
338
339 /// Convert to a byte.
340 pub fn to_byte(&self) -> u8 {
341 if self.spin_bit { 0x01 } else { 0x00 }
342 }
343
344 /// Convert from a byte.
345 pub fn from_byte(byte: u8) -> Self {
346 Self {
347 spin_bit: byte & 0x01 != 0,
348 }
349 }
350}
351
352// ============================================================================
353// Session Setup
354// ============================================================================
355
356/// Session setup to establish cached coordinate state.
357///
358/// Carried inside a SessionDatagram envelope which provides src_addr and
359/// dest_addr. The SessionSetup payload contains coordinates, session flags,
360/// and the Noise XK handshake message for session establishment.
361///
362/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
363/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte.
364/// The `msg_type` field in the encrypted inner header applies only to
365/// established-phase (0x0) messages.
366///
367/// ## Wire Format
368///
369/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`,
370/// where `ver_phase = 0x01` (version 0, phase MSG1) and `flags = 0` for handshake.
371///
372/// **Body** (after 4-byte FSP prefix):
373///
374/// | Offset | Field | Size | Description |
375/// |--------|-------------------|------------|-----------------------------------------------------|
376/// | 0 | flags | 1 byte | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL, Bit 2: DIRECT_FSP |
377/// | 1 | src_coords_count | 2 bytes LE | Number of source coordinate entries |
378/// | 3 | src_coords | 16 × n | Source's ancestry (NodeAddr, self → root) |
379/// | ... | dest_coords_count | 2 bytes LE | Number of dest coordinate entries |
380/// | ... | dest_coords | 16 × m | Destination's ancestry |
381/// | ... | handshake_len | 2 bytes LE | Noise payload length |
382/// | ... | handshake_payload | variable | Noise XK msg1 (33 bytes — ephemeral key only) |
383#[derive(Clone, Debug)]
384pub struct SessionSetup {
385 /// Source coordinates (for return path caching).
386 pub src_coords: TreeCoordinate,
387 /// Destination coordinates (for forward routing).
388 pub dest_coords: TreeCoordinate,
389 /// Session options.
390 pub flags: SessionFlags,
391 /// Noise IK handshake message 1.
392 pub handshake_payload: Vec<u8>,
393}
394
395impl SessionSetup {
396 /// Create a new session setup message.
397 pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
398 Self {
399 src_coords,
400 dest_coords,
401 flags: SessionFlags::new(),
402 handshake_payload: Vec::new(),
403 }
404 }
405
406 /// Set session flags.
407 pub fn with_flags(mut self, flags: SessionFlags) -> Self {
408 self.flags = flags;
409 self
410 }
411
412 /// Set the Noise handshake payload.
413 pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
414 self.handshake_payload = payload;
415 self
416 }
417
418 /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
419 ///
420 /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
421 /// where ver_phase = 0x01 (version 0, phase MSG1).
422 pub fn encode(&self) -> Vec<u8> {
423 // Build body first to compute payload_len
424 let mut body = Vec::new();
425 body.push(self.flags.to_byte());
426 encode_coords(&self.src_coords, &mut body);
427 encode_coords(&self.dest_coords, &mut body);
428 let hs_len = self.handshake_payload.len() as u16;
429 body.extend_from_slice(&hs_len.to_le_bytes());
430 body.extend_from_slice(&self.handshake_payload);
431
432 // Prepend 4-byte FSP common prefix
433 let payload_len = body.len() as u16;
434 let mut buf = Vec::with_capacity(4 + body.len());
435 buf.push(0x01); // version 0, phase 0x1 (MSG1)
436 buf.push(0x00); // flags (must be zero for handshake)
437 buf.extend_from_slice(&payload_len.to_le_bytes());
438 buf.extend_from_slice(&body);
439 buf
440 }
441
442 /// Decode from wire format (after 4-byte FSP prefix has been consumed).
443 pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
444 if payload.is_empty() {
445 return Err(ProtocolError::MessageTooShort {
446 expected: 1,
447 got: 0,
448 });
449 }
450 let flags = SessionFlags::from_byte(payload[0]);
451 let mut offset = 1;
452
453 let (src_coords, consumed) = decode_coords(&payload[offset..])?;
454 offset += consumed;
455
456 let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
457 offset += consumed;
458
459 if payload.len() < offset + 2 {
460 return Err(ProtocolError::MessageTooShort {
461 expected: offset + 2,
462 got: payload.len(),
463 });
464 }
465 let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
466 offset += 2;
467
468 if payload.len() < offset + hs_len {
469 return Err(ProtocolError::MessageTooShort {
470 expected: offset + hs_len,
471 got: payload.len(),
472 });
473 }
474 let handshake_payload = payload[offset..offset + hs_len].to_vec();
475
476 Ok(Self {
477 src_coords,
478 dest_coords,
479 flags,
480 handshake_payload,
481 })
482 }
483}
484
485// ============================================================================
486// Session Ack
487// ============================================================================
488
489/// Session acknowledgement.
490///
491/// Carried inside a SessionDatagram envelope which provides src_addr and
492/// dest_addr. The SessionAck payload contains both the acknowledger's and
493/// initiator's coordinates for route cache warming (ensuring return-path
494/// transit nodes can route independently of the forward path) and the Noise
495/// XK handshake response.
496///
497/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
498/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte.
499///
500/// ## Wire Format
501///
502/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`,
503/// where `ver_phase = 0x02` (version 0, phase MSG2) and `flags = 0` for handshake.
504///
505/// **Body** (after 4-byte FSP prefix):
506///
507/// | Offset | Field | Size | Description |
508/// |--------|-------------------|------------|--------------------------------------------------------------|
509/// | 0 | flags | 1 byte | Reserved |
510/// | 1 | src_coords_count | 2 bytes LE | Number of acknowledger coordinate entries |
511/// | 3 | src_coords | 16 × n | Acknowledger's ancestry (for cache warming) |
512/// | ... | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries |
513/// | ... | dest_coords | 16 × m | Initiator's ancestry (for return-path cache warming) |
514/// | ... | handshake_len | 2 bytes LE | Noise payload length |
515/// | ... | handshake_payload | variable | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch) |
516#[derive(Clone, Debug)]
517pub struct SessionAck {
518 /// Acknowledger's coordinates.
519 pub src_coords: TreeCoordinate,
520 /// Initiator's coordinates (for return-path cache warming).
521 pub dest_coords: TreeCoordinate,
522 /// Reserved flags byte (for forward compatibility).
523 pub flags: u8,
524 /// Noise IK handshake message 2.
525 pub handshake_payload: Vec<u8>,
526}
527
528impl SessionAck {
529 /// Create a new session acknowledgement.
530 pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
531 Self {
532 src_coords,
533 dest_coords,
534 flags: 0,
535 handshake_payload: Vec::new(),
536 }
537 }
538
539 /// Set the Noise handshake payload.
540 pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
541 self.handshake_payload = payload;
542 self
543 }
544
545 /// Advertise support for direct-on-transport FSP records.
546 pub fn with_direct_fsp_transport(mut self) -> Self {
547 self.flags |= 0x04;
548 self
549 }
550
551 /// Whether the responder advertised direct-on-transport FSP support.
552 pub fn supports_direct_fsp_transport(&self) -> bool {
553 self.flags & 0x04 != 0
554 }
555
556 /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
557 ///
558 /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
559 /// where ver_phase = 0x02 (version 0, phase MSG2).
560 pub fn encode(&self) -> Vec<u8> {
561 // Build body first to compute payload_len
562 let mut body = Vec::new();
563 body.push(self.flags);
564 encode_coords(&self.src_coords, &mut body);
565 encode_coords(&self.dest_coords, &mut body);
566 let hs_len = self.handshake_payload.len() as u16;
567 body.extend_from_slice(&hs_len.to_le_bytes());
568 body.extend_from_slice(&self.handshake_payload);
569
570 // Prepend 4-byte FSP common prefix
571 let payload_len = body.len() as u16;
572 let mut buf = Vec::with_capacity(4 + body.len());
573 buf.push(0x02); // version 0, phase 0x2 (MSG2)
574 buf.push(0x00); // flags (must be zero for handshake)
575 buf.extend_from_slice(&payload_len.to_le_bytes());
576 buf.extend_from_slice(&body);
577 buf
578 }
579
580 /// Decode from wire format (after 4-byte FSP prefix has been consumed).
581 pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
582 if payload.is_empty() {
583 return Err(ProtocolError::MessageTooShort {
584 expected: 1,
585 got: 0,
586 });
587 }
588 let flags = payload[0];
589 let mut offset = 1;
590
591 let (src_coords, consumed) = decode_coords(&payload[offset..])?;
592 offset += consumed;
593
594 let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
595 offset += consumed;
596
597 if payload.len() < offset + 2 {
598 return Err(ProtocolError::MessageTooShort {
599 expected: offset + 2,
600 got: payload.len(),
601 });
602 }
603 let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
604 offset += 2;
605
606 if payload.len() < offset + hs_len {
607 return Err(ProtocolError::MessageTooShort {
608 expected: offset + hs_len,
609 got: payload.len(),
610 });
611 }
612 let handshake_payload = payload[offset..offset + hs_len].to_vec();
613
614 Ok(Self {
615 src_coords,
616 dest_coords,
617 flags,
618 handshake_payload,
619 })
620 }
621}
622
623// ============================================================================
624// Session Msg3 (XK Handshake Message 3)
625// ============================================================================
626
627/// XK handshake message 3 (initiator -> responder).
628///
629/// Carries the initiator's encrypted static key and epoch. Sent by the
630/// initiator after receiving msg2. The responder learns the initiator's
631/// identity from this message.
632///
633/// ## Wire Format
634///
635/// | Offset | Field | Size | Description |
636/// |--------|------------------|---------|-------------------------------------|
637/// | 0 | flags | 1 byte | Reserved |
638/// | 1 | handshake_len | 2 bytes | u16 LE, Noise payload length |
639/// | 3 | handshake_payload| variable| Noise XK msg3 (73 bytes typical) |
640#[derive(Clone, Debug)]
641pub struct SessionMsg3 {
642 /// Reserved flags byte.
643 pub flags: u8,
644 /// Noise XK handshake message 3.
645 pub handshake_payload: Vec<u8>,
646}
647
648impl SessionMsg3 {
649 /// Create a new SessionMsg3 with the given handshake payload.
650 pub fn new(handshake_payload: Vec<u8>) -> Self {
651 Self {
652 flags: 0,
653 handshake_payload,
654 }
655 }
656
657 /// Encode as wire format (4-byte FSP prefix + flags + handshake).
658 ///
659 /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
660 /// where ver_phase = 0x03 (version 0, phase MSG3).
661 pub fn encode(&self) -> Vec<u8> {
662 // Build body first to compute payload_len
663 let mut body = Vec::new();
664 body.push(self.flags);
665 let hs_len = self.handshake_payload.len() as u16;
666 body.extend_from_slice(&hs_len.to_le_bytes());
667 body.extend_from_slice(&self.handshake_payload);
668
669 // Prepend 4-byte FSP common prefix
670 let payload_len = body.len() as u16;
671 let mut buf = Vec::with_capacity(4 + body.len());
672 buf.push(0x03); // version 0, phase 0x3 (MSG3)
673 buf.push(0x00); // flags (must be zero for handshake)
674 buf.extend_from_slice(&payload_len.to_le_bytes());
675 buf.extend_from_slice(&body);
676 buf
677 }
678
679 /// Decode from wire format (after 4-byte FSP prefix has been consumed).
680 pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
681 if payload.is_empty() {
682 return Err(ProtocolError::MessageTooShort {
683 expected: 1,
684 got: 0,
685 });
686 }
687 let flags = payload[0];
688 let mut offset = 1;
689
690 if payload.len() < offset + 2 {
691 return Err(ProtocolError::MessageTooShort {
692 expected: offset + 2,
693 got: payload.len(),
694 });
695 }
696 let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
697 offset += 2;
698
699 if payload.len() < offset + hs_len {
700 return Err(ProtocolError::MessageTooShort {
701 expected: offset + hs_len,
702 got: payload.len(),
703 });
704 }
705 let handshake_payload = payload[offset..offset + hs_len].to_vec();
706
707 Ok(Self {
708 flags,
709 handshake_payload,
710 })
711 }
712}
713
714// ============================================================================
715// Session-Layer MMP Reports
716// ============================================================================
717
718/// Session-layer sender report (msg_type 0x11).
719///
720/// Mirrors the FMP `SenderReport` fields but carried as an FSP session
721/// message inside the AEAD envelope. The msg_type is in the FSP inner
722/// header, so the body starts with reserved bytes.
723///
724/// ## Wire Format (46 bytes body, after inner header stripped)
725///
726/// ```text
727/// [0-1] reserved (zero)
728/// [2-9] interval_start_counter: u64 LE
729/// [10-17] interval_end_counter: u64 LE
730/// [18-21] interval_start_timestamp: u32 LE
731/// [22-25] interval_end_timestamp: u32 LE
732/// [26-29] interval_bytes_sent: u32 LE
733/// [30-37] cumulative_packets_sent: u64 LE
734/// [38-45] cumulative_bytes_sent: u64 LE
735/// ```
736#[derive(Debug, Clone, PartialEq, Eq)]
737pub struct SessionSenderReport {
738 pub interval_start_counter: u64,
739 pub interval_end_counter: u64,
740 pub interval_start_timestamp: u32,
741 pub interval_end_timestamp: u32,
742 pub interval_bytes_sent: u32,
743 pub cumulative_packets_sent: u64,
744 pub cumulative_bytes_sent: u64,
745}
746
747/// Body size for SessionSenderReport: 2 reserved + 44 fields.
748pub const SESSION_SENDER_REPORT_SIZE: usize = 46;
749
750impl SessionSenderReport {
751 /// Encode to wire format (46 bytes body).
752 pub fn encode(&self) -> Vec<u8> {
753 let mut buf = Vec::with_capacity(SESSION_SENDER_REPORT_SIZE);
754 buf.extend_from_slice(&[0u8; 2]); // reserved
755 buf.extend_from_slice(&self.interval_start_counter.to_le_bytes());
756 buf.extend_from_slice(&self.interval_end_counter.to_le_bytes());
757 buf.extend_from_slice(&self.interval_start_timestamp.to_le_bytes());
758 buf.extend_from_slice(&self.interval_end_timestamp.to_le_bytes());
759 buf.extend_from_slice(&self.interval_bytes_sent.to_le_bytes());
760 buf.extend_from_slice(&self.cumulative_packets_sent.to_le_bytes());
761 buf.extend_from_slice(&self.cumulative_bytes_sent.to_le_bytes());
762 buf
763 }
764
765 /// Decode from body (after FSP inner header has been stripped).
766 pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
767 if body.len() < SESSION_SENDER_REPORT_SIZE {
768 return Err(ProtocolError::MessageTooShort {
769 expected: SESSION_SENDER_REPORT_SIZE,
770 got: body.len(),
771 });
772 }
773 // Skip 2 reserved bytes
774 let p = &body[2..];
775 Ok(Self {
776 interval_start_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
777 interval_end_counter: u64::from_le_bytes(p[8..16].try_into().unwrap()),
778 interval_start_timestamp: u32::from_le_bytes(p[16..20].try_into().unwrap()),
779 interval_end_timestamp: u32::from_le_bytes(p[20..24].try_into().unwrap()),
780 interval_bytes_sent: u32::from_le_bytes(p[24..28].try_into().unwrap()),
781 cumulative_packets_sent: u64::from_le_bytes(p[28..36].try_into().unwrap()),
782 cumulative_bytes_sent: u64::from_le_bytes(p[36..44].try_into().unwrap()),
783 })
784 }
785}
786
787/// Session-layer receiver report (msg_type 0x12).
788///
789/// Mirrors the FMP `ReceiverReport` fields but carried as an FSP session
790/// message inside the AEAD envelope.
791///
792/// ## Wire Format (66 bytes body, after inner header stripped)
793///
794/// ```text
795/// [0-1] reserved (zero)
796/// [2-9] highest_counter: u64 LE
797/// [10-17] cumulative_packets_recv: u64 LE
798/// [18-25] cumulative_bytes_recv: u64 LE
799/// [26-29] timestamp_echo: u32 LE
800/// [30-31] dwell_time: u16 LE
801/// [32-33] max_burst_loss: u16 LE
802/// [34-35] mean_burst_loss: u16 LE (u8.8 fixed-point)
803/// [36-37] reserved: u16 LE
804/// [38-41] jitter: u32 LE (microseconds)
805/// [42-45] ecn_ce_count: u32 LE
806/// [46-49] owd_trend: i32 LE (µs/s)
807/// [50-53] burst_loss_count: u32 LE
808/// [54-57] cumulative_reorder_count: u32 LE
809/// [58-61] interval_packets_recv: u32 LE
810/// [62-65] interval_bytes_recv: u32 LE
811/// ```
812#[derive(Debug, Clone, PartialEq, Eq)]
813pub struct SessionReceiverReport {
814 pub highest_counter: u64,
815 pub cumulative_packets_recv: u64,
816 pub cumulative_bytes_recv: u64,
817 pub timestamp_echo: u32,
818 pub dwell_time: u16,
819 pub max_burst_loss: u16,
820 pub mean_burst_loss: u16,
821 pub jitter: u32,
822 pub ecn_ce_count: u32,
823 pub owd_trend: i32,
824 pub burst_loss_count: u32,
825 pub cumulative_reorder_count: u32,
826 pub interval_packets_recv: u32,
827 pub interval_bytes_recv: u32,
828}
829
830/// Body size for SessionReceiverReport: 2 reserved + 64 fields.
831pub const SESSION_RECEIVER_REPORT_SIZE: usize = 66;
832
833impl SessionReceiverReport {
834 /// Encode to wire format (66 bytes body).
835 pub fn encode(&self) -> Vec<u8> {
836 let mut buf = Vec::with_capacity(SESSION_RECEIVER_REPORT_SIZE);
837 buf.extend_from_slice(&[0u8; 2]); // reserved
838 buf.extend_from_slice(&self.highest_counter.to_le_bytes());
839 buf.extend_from_slice(&self.cumulative_packets_recv.to_le_bytes());
840 buf.extend_from_slice(&self.cumulative_bytes_recv.to_le_bytes());
841 buf.extend_from_slice(&self.timestamp_echo.to_le_bytes());
842 buf.extend_from_slice(&self.dwell_time.to_le_bytes());
843 buf.extend_from_slice(&self.max_burst_loss.to_le_bytes());
844 buf.extend_from_slice(&self.mean_burst_loss.to_le_bytes());
845 buf.extend_from_slice(&[0u8; 2]); // reserved
846 buf.extend_from_slice(&self.jitter.to_le_bytes());
847 buf.extend_from_slice(&self.ecn_ce_count.to_le_bytes());
848 buf.extend_from_slice(&self.owd_trend.to_le_bytes());
849 buf.extend_from_slice(&self.burst_loss_count.to_le_bytes());
850 buf.extend_from_slice(&self.cumulative_reorder_count.to_le_bytes());
851 buf.extend_from_slice(&self.interval_packets_recv.to_le_bytes());
852 buf.extend_from_slice(&self.interval_bytes_recv.to_le_bytes());
853 buf
854 }
855
856 /// Decode from body (after FSP inner header has been stripped).
857 pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
858 if body.len() < SESSION_RECEIVER_REPORT_SIZE {
859 return Err(ProtocolError::MessageTooShort {
860 expected: SESSION_RECEIVER_REPORT_SIZE,
861 got: body.len(),
862 });
863 }
864 // Skip 2 reserved bytes
865 let p = &body[2..];
866 Ok(Self {
867 highest_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
868 cumulative_packets_recv: u64::from_le_bytes(p[8..16].try_into().unwrap()),
869 cumulative_bytes_recv: u64::from_le_bytes(p[16..24].try_into().unwrap()),
870 timestamp_echo: u32::from_le_bytes(p[24..28].try_into().unwrap()),
871 dwell_time: u16::from_le_bytes(p[28..30].try_into().unwrap()),
872 max_burst_loss: u16::from_le_bytes(p[30..32].try_into().unwrap()),
873 mean_burst_loss: u16::from_le_bytes(p[32..34].try_into().unwrap()),
874 // skip 2 reserved bytes at p[34..36]
875 jitter: u32::from_le_bytes(p[36..40].try_into().unwrap()),
876 ecn_ce_count: u32::from_le_bytes(p[40..44].try_into().unwrap()),
877 owd_trend: i32::from_le_bytes(p[44..48].try_into().unwrap()),
878 burst_loss_count: u32::from_le_bytes(p[48..52].try_into().unwrap()),
879 cumulative_reorder_count: u32::from_le_bytes(p[52..56].try_into().unwrap()),
880 interval_packets_recv: u32::from_le_bytes(p[56..60].try_into().unwrap()),
881 interval_bytes_recv: u32::from_le_bytes(p[60..64].try_into().unwrap()),
882 })
883 }
884}
885
886#[cfg(test)]
887mod tests;