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