Skip to main content

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;