Skip to main content

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;