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
8// ============================================================================
9// Session Layer Message Types
10// ============================================================================
11
12/// SessionDatagram payload message type identifiers.
13///
14/// These messages are carried as payloads inside `SessionDatagram` (link
15/// message type 0x00). Post-handshake messages (data, reports) are end-to-end
16/// encrypted with session keys via the FSP pipeline. Error signals
17/// (CoordsRequired, PathBroken) are plaintext messages generated by transit
18/// routers that cannot establish e2e sessions with the source.
19///
20/// Handshake messages (SessionSetup, SessionAck, SessionMsg3) are **not**
21/// identified by a message-type byte; they are dispatched by the FSP phase
22/// nibble in the common prefix (0x1, 0x2, 0x3 respectively). The 0x00-0x0F
23/// range is therefore unallocated in this enum.
24#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25#[repr(u8)]
26pub enum SessionMessageType {
27    // Data and metrics (0x10-0x1F) — encrypted, inner header msg_type
28    /// Port-multiplexed service payload: `[src_port:2 LE][dst_port:2 LE][service data...]`.
29    /// Port 256 = IPv6 shim (compressed header). Receiver dispatches by dst_port.
30    DataPacket = 0x10,
31    /// MMP sender report (metrics from sender to receiver).
32    SenderReport = 0x11,
33    /// MMP receiver report (metrics from receiver to sender).
34    ReceiverReport = 0x12,
35    /// Path MTU notification (discovered path MTU).
36    PathMtuNotification = 0x13,
37    /// Standalone coordinate cache warming (empty body, coords in CP flag).
38    CoordsWarmup = 0x14,
39    /// App-facing endpoint payload, without DataPacket port dispatch.
40    EndpointData = 0x15,
41    /// NAT traversal offer carried over an established end-to-end FIPS session.
42    TraversalOffer = 0x16,
43    /// NAT traversal answer carried over an established end-to-end FIPS session.
44    TraversalAnswer = 0x17,
45
46    // Link-layer error signals (0x20-0x2F) — plaintext, from transit routers
47    /// Router cache miss — needs coordinates (link-layer error signal).
48    CoordsRequired = 0x20,
49    /// Routing failure — local minimum or unreachable (link-layer error signal).
50    PathBroken = 0x21,
51    /// MTU exceeded — forwarded packet too large for next-hop transport (link-layer error signal).
52    MtuExceeded = 0x22,
53}
54
55impl SessionMessageType {
56    /// Try to convert from a byte.
57    pub fn from_byte(b: u8) -> Option<Self> {
58        match b {
59            0x10 => Some(SessionMessageType::DataPacket),
60            0x11 => Some(SessionMessageType::SenderReport),
61            0x12 => Some(SessionMessageType::ReceiverReport),
62            0x13 => Some(SessionMessageType::PathMtuNotification),
63            0x14 => Some(SessionMessageType::CoordsWarmup),
64            0x15 => Some(SessionMessageType::EndpointData),
65            0x16 => Some(SessionMessageType::TraversalOffer),
66            0x17 => Some(SessionMessageType::TraversalAnswer),
67            0x20 => Some(SessionMessageType::CoordsRequired),
68            0x21 => Some(SessionMessageType::PathBroken),
69            0x22 => Some(SessionMessageType::MtuExceeded),
70            _ => None,
71        }
72    }
73
74    /// Convert to a byte.
75    pub fn to_byte(self) -> u8 {
76        self as u8
77    }
78}
79
80impl fmt::Display for SessionMessageType {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        let name = match self {
83            SessionMessageType::DataPacket => "DataPacket",
84            SessionMessageType::SenderReport => "SenderReport",
85            SessionMessageType::ReceiverReport => "ReceiverReport",
86            SessionMessageType::PathMtuNotification => "PathMtuNotification",
87            SessionMessageType::CoordsWarmup => "CoordsWarmup",
88            SessionMessageType::EndpointData => "EndpointData",
89            SessionMessageType::TraversalOffer => "TraversalOffer",
90            SessionMessageType::TraversalAnswer => "TraversalAnswer",
91            SessionMessageType::CoordsRequired => "CoordsRequired",
92            SessionMessageType::PathBroken => "PathBroken",
93            SessionMessageType::MtuExceeded => "MtuExceeded",
94        };
95        write!(f, "{}", name)
96    }
97}
98
99// ============================================================================
100// Coordinate Wire Format Helpers
101// ============================================================================
102
103/// Wire size of a TreeCoordinate in address-only format: 2 + entries × 16.
104pub(crate) fn coords_wire_size(coords: &TreeCoordinate) -> usize {
105    2 + coords.entries().len() * 16
106}
107
108/// Encode a TreeCoordinate as address-only wire format: count(u16 LE) + addrs(16 × n).
109///
110/// Session-layer messages serialize coordinates as NodeAddr arrays (16 bytes each),
111/// without the sequence/timestamp metadata used by the tree gossip protocol.
112pub(crate) fn encode_coords(coords: &TreeCoordinate, buf: &mut Vec<u8>) {
113    let addrs: Vec<&NodeAddr> = coords.node_addrs().collect();
114    let count = addrs.len() as u16;
115    buf.extend_from_slice(&count.to_le_bytes());
116    for addr in addrs {
117        buf.extend_from_slice(addr.as_bytes());
118    }
119}
120
121/// Decode a TreeCoordinate from address-only wire format.
122///
123/// Returns the decoded coordinate and the number of bytes consumed.
124pub(crate) fn decode_coords(data: &[u8]) -> Result<(TreeCoordinate, usize), ProtocolError> {
125    if data.len() < 2 {
126        return Err(ProtocolError::MessageTooShort {
127            expected: 2,
128            got: data.len(),
129        });
130    }
131    let count = u16::from_le_bytes([data[0], data[1]]) as usize;
132    let needed = 2 + count * 16;
133    if data.len() < needed {
134        return Err(ProtocolError::MessageTooShort {
135            expected: needed,
136            got: data.len(),
137        });
138    }
139    if count == 0 {
140        return Err(ProtocolError::Malformed(
141            "coordinate with zero entries".into(),
142        ));
143    }
144    let mut addrs = Vec::with_capacity(count);
145    for i in 0..count {
146        let offset = 2 + i * 16;
147        let mut bytes = [0u8; 16];
148        bytes.copy_from_slice(&data[offset..offset + 16]);
149        addrs.push(NodeAddr::from_bytes(bytes));
150    }
151    let coord =
152        TreeCoordinate::from_addrs(addrs).map_err(|e| ProtocolError::Malformed(e.to_string()))?;
153    Ok((coord, needed))
154}
155
156/// Decode an optional coordinate field (count may be 0).
157///
158/// Returns None if count is 0, Some(coord) otherwise, plus bytes consumed.
159pub(crate) fn decode_optional_coords(
160    data: &[u8],
161) -> Result<(Option<TreeCoordinate>, usize), ProtocolError> {
162    if data.len() < 2 {
163        return Err(ProtocolError::MessageTooShort {
164            expected: 2,
165            got: data.len(),
166        });
167    }
168    let count = u16::from_le_bytes([data[0], data[1]]) as usize;
169    let needed = 2 + count * 16;
170    if data.len() < needed {
171        return Err(ProtocolError::MessageTooShort {
172            expected: needed,
173            got: data.len(),
174        });
175    }
176    if count == 0 {
177        return Ok((None, 2));
178    }
179    let mut addrs = Vec::with_capacity(count);
180    for i in 0..count {
181        let offset = 2 + i * 16;
182        let mut bytes = [0u8; 16];
183        bytes.copy_from_slice(&data[offset..offset + 16]);
184        addrs.push(NodeAddr::from_bytes(bytes));
185    }
186    let coord =
187        TreeCoordinate::from_addrs(addrs).map_err(|e| ProtocolError::Malformed(e.to_string()))?;
188    Ok((Some(coord), needed))
189}
190
191/// Encode a count of zero (for empty/absent coordinate fields).
192fn encode_empty_coords(buf: &mut Vec<u8>) {
193    buf.extend_from_slice(&0u16.to_le_bytes());
194}
195
196// ============================================================================
197// Session Flags
198// ============================================================================
199
200/// Session flags for setup options.
201#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
202pub struct SessionFlags {
203    /// Request acknowledgement from destination.
204    pub request_ack: bool,
205    /// Set up bidirectional session.
206    pub bidirectional: bool,
207}
208
209impl SessionFlags {
210    /// Create default flags.
211    pub fn new() -> Self {
212        Self::default()
213    }
214
215    /// Set request_ack flag.
216    pub fn with_ack(mut self) -> Self {
217        self.request_ack = true;
218        self
219    }
220
221    /// Set bidirectional flag.
222    pub fn bidirectional(mut self) -> Self {
223        self.bidirectional = true;
224        self
225    }
226
227    /// Convert to a byte.
228    pub fn to_byte(&self) -> u8 {
229        let mut flags = 0u8;
230        if self.request_ack {
231            flags |= 0x01;
232        }
233        if self.bidirectional {
234            flags |= 0x02;
235        }
236        flags
237    }
238
239    /// Convert from a byte.
240    pub fn from_byte(byte: u8) -> Self {
241        Self {
242            request_ack: byte & 0x01 != 0,
243            bidirectional: byte & 0x02 != 0,
244        }
245    }
246}
247
248// ============================================================================
249// FSP Packet Flags
250// ============================================================================
251
252/// FSP common prefix flags (cleartext, in outer header).
253///
254/// | Bit | Name | Description                                    |
255/// |-----|------|------------------------------------------------|
256/// | 0   | CP   | Coords present between header and ciphertext   |
257/// | 1   | K    | Key epoch (for rekeying)                       |
258/// | 2   | U    | Unencrypted payload (error signals)            |
259/// | 3-7 |      | Reserved                                       |
260#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
261pub struct FspFlags {
262    /// Coordinates present between header and ciphertext.
263    pub coords_present: bool,
264    /// Key epoch bit for rekeying.
265    pub key_epoch: bool,
266    /// Unencrypted payload (plaintext error signals from transit routers).
267    pub unencrypted: bool,
268}
269
270impl FspFlags {
271    /// Create default flags (all clear).
272    pub fn new() -> Self {
273        Self::default()
274    }
275
276    /// Convert to a byte.
277    pub fn to_byte(&self) -> u8 {
278        let mut flags = 0u8;
279        if self.coords_present {
280            flags |= 0x01;
281        }
282        if self.key_epoch {
283            flags |= 0x02;
284        }
285        if self.unencrypted {
286            flags |= 0x04;
287        }
288        flags
289    }
290
291    /// Convert from a byte.
292    pub fn from_byte(byte: u8) -> Self {
293        Self {
294            coords_present: byte & 0x01 != 0,
295            key_epoch: byte & 0x02 != 0,
296            unencrypted: byte & 0x04 != 0,
297        }
298    }
299}
300
301/// FSP inner header flags (encrypted, inside AEAD envelope).
302///
303/// | Bit | Name | Description                     |
304/// |-----|------|---------------------------------|
305/// | 0   | SP   | Spin bit for RTT measurement    |
306/// | 1-7 |      | Reserved                        |
307#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
308pub struct FspInnerFlags {
309    /// Spin bit for passive RTT measurement.
310    pub spin_bit: bool,
311}
312
313impl FspInnerFlags {
314    /// Create default inner flags (all clear).
315    pub fn new() -> Self {
316        Self::default()
317    }
318
319    /// Convert to a byte.
320    pub fn to_byte(&self) -> u8 {
321        if self.spin_bit { 0x01 } else { 0x00 }
322    }
323
324    /// Convert from a byte.
325    pub fn from_byte(byte: u8) -> Self {
326        Self {
327            spin_bit: byte & 0x01 != 0,
328        }
329    }
330}
331
332// ============================================================================
333// Session Setup
334// ============================================================================
335
336/// Session setup to establish cached coordinate state.
337///
338/// Carried inside a SessionDatagram envelope which provides src_addr and
339/// dest_addr. The SessionSetup payload contains coordinates, session flags,
340/// and the Noise XK handshake message for session establishment.
341///
342/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
343/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte.
344/// The `msg_type` field in the encrypted inner header applies only to
345/// established-phase (0x0) messages.
346///
347/// ## Wire Format
348///
349/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`,
350/// where `ver_phase = 0x01` (version 0, phase MSG1) and `flags = 0` for handshake.
351///
352/// **Body** (after 4-byte FSP prefix):
353///
354/// | Offset | Field             | Size       | Description                                         |
355/// |--------|-------------------|------------|-----------------------------------------------------|
356/// | 0      | flags             | 1 byte     | Bit 0: REQUEST_ACK, Bit 1: BIDIRECTIONAL            |
357/// | 1      | src_coords_count  | 2 bytes LE | Number of source coordinate entries                 |
358/// | 3      | src_coords        | 16 × n     | Source's ancestry (NodeAddr, self → root)           |
359/// | ...    | dest_coords_count | 2 bytes LE | Number of dest coordinate entries                   |
360/// | ...    | dest_coords       | 16 × m     | Destination's ancestry                              |
361/// | ...    | handshake_len     | 2 bytes LE | Noise payload length                                |
362/// | ...    | handshake_payload | variable   | Noise XK msg1 (33 bytes — ephemeral key only)       |
363#[derive(Clone, Debug)]
364pub struct SessionSetup {
365    /// Source coordinates (for return path caching).
366    pub src_coords: TreeCoordinate,
367    /// Destination coordinates (for forward routing).
368    pub dest_coords: TreeCoordinate,
369    /// Session options.
370    pub flags: SessionFlags,
371    /// Noise IK handshake message 1.
372    pub handshake_payload: Vec<u8>,
373}
374
375impl SessionSetup {
376    /// Create a new session setup message.
377    pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
378        Self {
379            src_coords,
380            dest_coords,
381            flags: SessionFlags::new(),
382            handshake_payload: Vec::new(),
383        }
384    }
385
386    /// Set session flags.
387    pub fn with_flags(mut self, flags: SessionFlags) -> Self {
388        self.flags = flags;
389        self
390    }
391
392    /// Set the Noise handshake payload.
393    pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
394        self.handshake_payload = payload;
395        self
396    }
397
398    /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
399    ///
400    /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
401    /// where ver_phase = 0x01 (version 0, phase MSG1).
402    pub fn encode(&self) -> Vec<u8> {
403        // Build body first to compute payload_len
404        let mut body = Vec::new();
405        body.push(self.flags.to_byte());
406        encode_coords(&self.src_coords, &mut body);
407        encode_coords(&self.dest_coords, &mut body);
408        let hs_len = self.handshake_payload.len() as u16;
409        body.extend_from_slice(&hs_len.to_le_bytes());
410        body.extend_from_slice(&self.handshake_payload);
411
412        // Prepend 4-byte FSP common prefix
413        let payload_len = body.len() as u16;
414        let mut buf = Vec::with_capacity(4 + body.len());
415        buf.push(0x01); // version 0, phase 0x1 (MSG1)
416        buf.push(0x00); // flags (must be zero for handshake)
417        buf.extend_from_slice(&payload_len.to_le_bytes());
418        buf.extend_from_slice(&body);
419        buf
420    }
421
422    /// Decode from wire format (after 4-byte FSP prefix has been consumed).
423    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
424        if payload.is_empty() {
425            return Err(ProtocolError::MessageTooShort {
426                expected: 1,
427                got: 0,
428            });
429        }
430        let flags = SessionFlags::from_byte(payload[0]);
431        let mut offset = 1;
432
433        let (src_coords, consumed) = decode_coords(&payload[offset..])?;
434        offset += consumed;
435
436        let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
437        offset += consumed;
438
439        if payload.len() < offset + 2 {
440            return Err(ProtocolError::MessageTooShort {
441                expected: offset + 2,
442                got: payload.len(),
443            });
444        }
445        let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
446        offset += 2;
447
448        if payload.len() < offset + hs_len {
449            return Err(ProtocolError::MessageTooShort {
450                expected: offset + hs_len,
451                got: payload.len(),
452            });
453        }
454        let handshake_payload = payload[offset..offset + hs_len].to_vec();
455
456        Ok(Self {
457            src_coords,
458            dest_coords,
459            flags,
460            handshake_payload,
461        })
462    }
463}
464
465// ============================================================================
466// Session Ack
467// ============================================================================
468
469/// Session acknowledgement.
470///
471/// Carried inside a SessionDatagram envelope which provides src_addr and
472/// dest_addr. The SessionAck payload contains both the acknowledger's and
473/// initiator's coordinates for route cache warming (ensuring return-path
474/// transit nodes can route independently of the forward path) and the Noise
475/// XK handshake response.
476///
477/// SessionSetup, SessionAck, and SessionMsg3 are identified by the **phase**
478/// field in the FSP common prefix (0x1, 0x2, 0x3), not by a message-type byte.
479///
480/// ## Wire Format
481///
482/// Encoded with FSP common prefix: `[ver_phase:1][flags:1][payload_len:2 LE][body]`,
483/// where `ver_phase = 0x02` (version 0, phase MSG2) and `flags = 0` for handshake.
484///
485/// **Body** (after 4-byte FSP prefix):
486///
487/// | Offset | Field             | Size       | Description                                                  |
488/// |--------|-------------------|------------|--------------------------------------------------------------|
489/// | 0      | flags             | 1 byte     | Reserved                                                     |
490/// | 1      | src_coords_count  | 2 bytes LE | Number of acknowledger coordinate entries                    |
491/// | 3      | src_coords        | 16 × n     | Acknowledger's ancestry (for cache warming)                  |
492/// | ...    | dest_coords_count | 2 bytes LE | Number of initiator coordinate entries                       |
493/// | ...    | dest_coords       | 16 × m     | Initiator's ancestry (for return-path cache warming)         |
494/// | ...    | handshake_len     | 2 bytes LE | Noise payload length                                         |
495/// | ...    | handshake_payload | variable   | Noise XK msg2 (57 bytes — ephemeral key + encrypted epoch)   |
496#[derive(Clone, Debug)]
497pub struct SessionAck {
498    /// Acknowledger's coordinates.
499    pub src_coords: TreeCoordinate,
500    /// Initiator's coordinates (for return-path cache warming).
501    pub dest_coords: TreeCoordinate,
502    /// Reserved flags byte (for forward compatibility).
503    pub flags: u8,
504    /// Noise IK handshake message 2.
505    pub handshake_payload: Vec<u8>,
506}
507
508impl SessionAck {
509    /// Create a new session acknowledgement.
510    pub fn new(src_coords: TreeCoordinate, dest_coords: TreeCoordinate) -> Self {
511        Self {
512            src_coords,
513            dest_coords,
514            flags: 0,
515            handshake_payload: Vec::new(),
516        }
517    }
518
519    /// Set the Noise handshake payload.
520    pub fn with_handshake(mut self, payload: Vec<u8>) -> Self {
521        self.handshake_payload = payload;
522        self
523    }
524
525    /// Encode as wire format (4-byte FSP prefix + flags + coords + handshake).
526    ///
527    /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
528    /// where ver_phase = 0x02 (version 0, phase MSG2).
529    pub fn encode(&self) -> Vec<u8> {
530        // Build body first to compute payload_len
531        let mut body = Vec::new();
532        body.push(self.flags);
533        encode_coords(&self.src_coords, &mut body);
534        encode_coords(&self.dest_coords, &mut body);
535        let hs_len = self.handshake_payload.len() as u16;
536        body.extend_from_slice(&hs_len.to_le_bytes());
537        body.extend_from_slice(&self.handshake_payload);
538
539        // Prepend 4-byte FSP common prefix
540        let payload_len = body.len() as u16;
541        let mut buf = Vec::with_capacity(4 + body.len());
542        buf.push(0x02); // version 0, phase 0x2 (MSG2)
543        buf.push(0x00); // flags (must be zero for handshake)
544        buf.extend_from_slice(&payload_len.to_le_bytes());
545        buf.extend_from_slice(&body);
546        buf
547    }
548
549    /// Decode from wire format (after 4-byte FSP prefix has been consumed).
550    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
551        if payload.is_empty() {
552            return Err(ProtocolError::MessageTooShort {
553                expected: 1,
554                got: 0,
555            });
556        }
557        let flags = payload[0];
558        let mut offset = 1;
559
560        let (src_coords, consumed) = decode_coords(&payload[offset..])?;
561        offset += consumed;
562
563        let (dest_coords, consumed) = decode_coords(&payload[offset..])?;
564        offset += consumed;
565
566        if payload.len() < offset + 2 {
567            return Err(ProtocolError::MessageTooShort {
568                expected: offset + 2,
569                got: payload.len(),
570            });
571        }
572        let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
573        offset += 2;
574
575        if payload.len() < offset + hs_len {
576            return Err(ProtocolError::MessageTooShort {
577                expected: offset + hs_len,
578                got: payload.len(),
579            });
580        }
581        let handshake_payload = payload[offset..offset + hs_len].to_vec();
582
583        Ok(Self {
584            src_coords,
585            dest_coords,
586            flags,
587            handshake_payload,
588        })
589    }
590}
591
592// ============================================================================
593// Session Msg3 (XK Handshake Message 3)
594// ============================================================================
595
596/// XK handshake message 3 (initiator -> responder).
597///
598/// Carries the initiator's encrypted static key and epoch. Sent by the
599/// initiator after receiving msg2. The responder learns the initiator's
600/// identity from this message.
601///
602/// ## Wire Format
603///
604/// | Offset | Field            | Size    | Description                         |
605/// |--------|------------------|---------|-------------------------------------|
606/// | 0      | flags            | 1 byte  | Reserved                            |
607/// | 1      | handshake_len    | 2 bytes | u16 LE, Noise payload length        |
608/// | 3      | handshake_payload| variable| Noise XK msg3 (73 bytes typical)    |
609#[derive(Clone, Debug)]
610pub struct SessionMsg3 {
611    /// Reserved flags byte.
612    pub flags: u8,
613    /// Noise XK handshake message 3.
614    pub handshake_payload: Vec<u8>,
615}
616
617impl SessionMsg3 {
618    /// Create a new SessionMsg3 with the given handshake payload.
619    pub fn new(handshake_payload: Vec<u8>) -> Self {
620        Self {
621            flags: 0,
622            handshake_payload,
623        }
624    }
625
626    /// Encode as wire format (4-byte FSP prefix + flags + handshake).
627    ///
628    /// The 4-byte prefix: `[ver_phase:1][flags:1][payload_len:2 LE]`
629    /// where ver_phase = 0x03 (version 0, phase MSG3).
630    pub fn encode(&self) -> Vec<u8> {
631        // Build body first to compute payload_len
632        let mut body = Vec::new();
633        body.push(self.flags);
634        let hs_len = self.handshake_payload.len() as u16;
635        body.extend_from_slice(&hs_len.to_le_bytes());
636        body.extend_from_slice(&self.handshake_payload);
637
638        // Prepend 4-byte FSP common prefix
639        let payload_len = body.len() as u16;
640        let mut buf = Vec::with_capacity(4 + body.len());
641        buf.push(0x03); // version 0, phase 0x3 (MSG3)
642        buf.push(0x00); // flags (must be zero for handshake)
643        buf.extend_from_slice(&payload_len.to_le_bytes());
644        buf.extend_from_slice(&body);
645        buf
646    }
647
648    /// Decode from wire format (after 4-byte FSP prefix has been consumed).
649    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
650        if payload.is_empty() {
651            return Err(ProtocolError::MessageTooShort {
652                expected: 1,
653                got: 0,
654            });
655        }
656        let flags = payload[0];
657        let mut offset = 1;
658
659        if payload.len() < offset + 2 {
660            return Err(ProtocolError::MessageTooShort {
661                expected: offset + 2,
662                got: payload.len(),
663            });
664        }
665        let hs_len = u16::from_le_bytes([payload[offset], payload[offset + 1]]) as usize;
666        offset += 2;
667
668        if payload.len() < offset + hs_len {
669            return Err(ProtocolError::MessageTooShort {
670                expected: offset + hs_len,
671                got: payload.len(),
672            });
673        }
674        let handshake_payload = payload[offset..offset + hs_len].to_vec();
675
676        Ok(Self {
677            flags,
678            handshake_payload,
679        })
680    }
681}
682
683// ============================================================================
684// Session-Layer MMP Reports
685// ============================================================================
686
687/// Session-layer sender report (msg_type 0x11).
688///
689/// Mirrors the FMP `SenderReport` fields but carried as an FSP session
690/// message inside the AEAD envelope. The msg_type is in the FSP inner
691/// header, so the body starts with reserved bytes.
692///
693/// ## Wire Format (46 bytes body, after inner header stripped)
694///
695/// ```text
696/// [0-1]   reserved (zero)
697/// [2-9]   interval_start_counter: u64 LE
698/// [10-17] interval_end_counter: u64 LE
699/// [18-21] interval_start_timestamp: u32 LE
700/// [22-25] interval_end_timestamp: u32 LE
701/// [26-29] interval_bytes_sent: u32 LE
702/// [30-37] cumulative_packets_sent: u64 LE
703/// [38-45] cumulative_bytes_sent: u64 LE
704/// ```
705#[derive(Debug, Clone, PartialEq, Eq)]
706pub struct SessionSenderReport {
707    pub interval_start_counter: u64,
708    pub interval_end_counter: u64,
709    pub interval_start_timestamp: u32,
710    pub interval_end_timestamp: u32,
711    pub interval_bytes_sent: u32,
712    pub cumulative_packets_sent: u64,
713    pub cumulative_bytes_sent: u64,
714}
715
716/// Body size for SessionSenderReport: 2 reserved + 44 fields.
717pub const SESSION_SENDER_REPORT_SIZE: usize = 46;
718
719impl SessionSenderReport {
720    /// Encode to wire format (46 bytes body).
721    pub fn encode(&self) -> Vec<u8> {
722        let mut buf = Vec::with_capacity(SESSION_SENDER_REPORT_SIZE);
723        buf.extend_from_slice(&[0u8; 2]); // reserved
724        buf.extend_from_slice(&self.interval_start_counter.to_le_bytes());
725        buf.extend_from_slice(&self.interval_end_counter.to_le_bytes());
726        buf.extend_from_slice(&self.interval_start_timestamp.to_le_bytes());
727        buf.extend_from_slice(&self.interval_end_timestamp.to_le_bytes());
728        buf.extend_from_slice(&self.interval_bytes_sent.to_le_bytes());
729        buf.extend_from_slice(&self.cumulative_packets_sent.to_le_bytes());
730        buf.extend_from_slice(&self.cumulative_bytes_sent.to_le_bytes());
731        buf
732    }
733
734    /// Decode from body (after FSP inner header has been stripped).
735    pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
736        if body.len() < SESSION_SENDER_REPORT_SIZE {
737            return Err(ProtocolError::MessageTooShort {
738                expected: SESSION_SENDER_REPORT_SIZE,
739                got: body.len(),
740            });
741        }
742        // Skip 2 reserved bytes
743        let p = &body[2..];
744        Ok(Self {
745            interval_start_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
746            interval_end_counter: u64::from_le_bytes(p[8..16].try_into().unwrap()),
747            interval_start_timestamp: u32::from_le_bytes(p[16..20].try_into().unwrap()),
748            interval_end_timestamp: u32::from_le_bytes(p[20..24].try_into().unwrap()),
749            interval_bytes_sent: u32::from_le_bytes(p[24..28].try_into().unwrap()),
750            cumulative_packets_sent: u64::from_le_bytes(p[28..36].try_into().unwrap()),
751            cumulative_bytes_sent: u64::from_le_bytes(p[36..44].try_into().unwrap()),
752        })
753    }
754}
755
756/// Session-layer receiver report (msg_type 0x12).
757///
758/// Mirrors the FMP `ReceiverReport` fields but carried as an FSP session
759/// message inside the AEAD envelope.
760///
761/// ## Wire Format (66 bytes body, after inner header stripped)
762///
763/// ```text
764/// [0-1]   reserved (zero)
765/// [2-9]   highest_counter: u64 LE
766/// [10-17] cumulative_packets_recv: u64 LE
767/// [18-25] cumulative_bytes_recv: u64 LE
768/// [26-29] timestamp_echo: u32 LE
769/// [30-31] dwell_time: u16 LE
770/// [32-33] max_burst_loss: u16 LE
771/// [34-35] mean_burst_loss: u16 LE (u8.8 fixed-point)
772/// [36-37] reserved: u16 LE
773/// [38-41] jitter: u32 LE (microseconds)
774/// [42-45] ecn_ce_count: u32 LE
775/// [46-49] owd_trend: i32 LE (µs/s)
776/// [50-53] burst_loss_count: u32 LE
777/// [54-57] cumulative_reorder_count: u32 LE
778/// [58-61] interval_packets_recv: u32 LE
779/// [62-65] interval_bytes_recv: u32 LE
780/// ```
781#[derive(Debug, Clone, PartialEq, Eq)]
782pub struct SessionReceiverReport {
783    pub highest_counter: u64,
784    pub cumulative_packets_recv: u64,
785    pub cumulative_bytes_recv: u64,
786    pub timestamp_echo: u32,
787    pub dwell_time: u16,
788    pub max_burst_loss: u16,
789    pub mean_burst_loss: u16,
790    pub jitter: u32,
791    pub ecn_ce_count: u32,
792    pub owd_trend: i32,
793    pub burst_loss_count: u32,
794    pub cumulative_reorder_count: u32,
795    pub interval_packets_recv: u32,
796    pub interval_bytes_recv: u32,
797}
798
799/// Body size for SessionReceiverReport: 2 reserved + 64 fields.
800pub const SESSION_RECEIVER_REPORT_SIZE: usize = 66;
801
802impl SessionReceiverReport {
803    /// Encode to wire format (66 bytes body).
804    pub fn encode(&self) -> Vec<u8> {
805        let mut buf = Vec::with_capacity(SESSION_RECEIVER_REPORT_SIZE);
806        buf.extend_from_slice(&[0u8; 2]); // reserved
807        buf.extend_from_slice(&self.highest_counter.to_le_bytes());
808        buf.extend_from_slice(&self.cumulative_packets_recv.to_le_bytes());
809        buf.extend_from_slice(&self.cumulative_bytes_recv.to_le_bytes());
810        buf.extend_from_slice(&self.timestamp_echo.to_le_bytes());
811        buf.extend_from_slice(&self.dwell_time.to_le_bytes());
812        buf.extend_from_slice(&self.max_burst_loss.to_le_bytes());
813        buf.extend_from_slice(&self.mean_burst_loss.to_le_bytes());
814        buf.extend_from_slice(&[0u8; 2]); // reserved
815        buf.extend_from_slice(&self.jitter.to_le_bytes());
816        buf.extend_from_slice(&self.ecn_ce_count.to_le_bytes());
817        buf.extend_from_slice(&self.owd_trend.to_le_bytes());
818        buf.extend_from_slice(&self.burst_loss_count.to_le_bytes());
819        buf.extend_from_slice(&self.cumulative_reorder_count.to_le_bytes());
820        buf.extend_from_slice(&self.interval_packets_recv.to_le_bytes());
821        buf.extend_from_slice(&self.interval_bytes_recv.to_le_bytes());
822        buf
823    }
824
825    /// Decode from body (after FSP inner header has been stripped).
826    pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
827        if body.len() < SESSION_RECEIVER_REPORT_SIZE {
828            return Err(ProtocolError::MessageTooShort {
829                expected: SESSION_RECEIVER_REPORT_SIZE,
830                got: body.len(),
831            });
832        }
833        // Skip 2 reserved bytes
834        let p = &body[2..];
835        Ok(Self {
836            highest_counter: u64::from_le_bytes(p[0..8].try_into().unwrap()),
837            cumulative_packets_recv: u64::from_le_bytes(p[8..16].try_into().unwrap()),
838            cumulative_bytes_recv: u64::from_le_bytes(p[16..24].try_into().unwrap()),
839            timestamp_echo: u32::from_le_bytes(p[24..28].try_into().unwrap()),
840            dwell_time: u16::from_le_bytes(p[28..30].try_into().unwrap()),
841            max_burst_loss: u16::from_le_bytes(p[30..32].try_into().unwrap()),
842            mean_burst_loss: u16::from_le_bytes(p[32..34].try_into().unwrap()),
843            // skip 2 reserved bytes at p[34..36]
844            jitter: u32::from_le_bytes(p[36..40].try_into().unwrap()),
845            ecn_ce_count: u32::from_le_bytes(p[40..44].try_into().unwrap()),
846            owd_trend: i32::from_le_bytes(p[44..48].try_into().unwrap()),
847            burst_loss_count: u32::from_le_bytes(p[48..52].try_into().unwrap()),
848            cumulative_reorder_count: u32::from_le_bytes(p[52..56].try_into().unwrap()),
849            interval_packets_recv: u32::from_le_bytes(p[56..60].try_into().unwrap()),
850            interval_bytes_recv: u32::from_le_bytes(p[60..64].try_into().unwrap()),
851        })
852    }
853}
854
855/// Path MTU notification (msg_type 0x13).
856///
857/// Sent by a node that discovers a path MTU value (from transit router
858/// feedback or ICMP Packet Too Big). Allows the remote endpoint to
859/// adjust its sending MTU.
860///
861/// ## Wire Format (2 bytes body, after inner header stripped)
862///
863/// ```text
864/// [0-1]   path_mtu: u16 LE
865/// ```
866#[derive(Debug, Clone, PartialEq, Eq)]
867pub struct PathMtuNotification {
868    /// Discovered path MTU in bytes.
869    pub path_mtu: u16,
870}
871
872/// Body size for PathMtuNotification.
873pub const PATH_MTU_NOTIFICATION_SIZE: usize = 2;
874
875impl PathMtuNotification {
876    /// Create a new path MTU notification.
877    pub fn new(path_mtu: u16) -> Self {
878        Self { path_mtu }
879    }
880
881    /// Encode to wire format (2 bytes body).
882    pub fn encode(&self) -> Vec<u8> {
883        self.path_mtu.to_le_bytes().to_vec()
884    }
885
886    /// Decode from body (after FSP inner header has been stripped).
887    pub fn decode(body: &[u8]) -> Result<Self, ProtocolError> {
888        if body.len() < PATH_MTU_NOTIFICATION_SIZE {
889            return Err(ProtocolError::MessageTooShort {
890                expected: PATH_MTU_NOTIFICATION_SIZE,
891                got: body.len(),
892            });
893        }
894        Ok(Self {
895            path_mtu: u16::from_le_bytes([body[0], body[1]]),
896        })
897    }
898}
899
900// ============================================================================
901// Error Messages
902// ============================================================================
903
904/// Link-layer error signal indicating router cache miss.
905///
906/// Generated by a transit router when it cannot forward a SessionDatagram
907/// due to missing cached coordinates for the destination. Carried inside
908/// a new SessionDatagram addressed back to the original source
909/// (src_addr=reporter, dest_addr=original_source). Plaintext — not
910/// end-to-end encrypted, since the transit router has no session with
911/// the source.
912///
913/// ## Wire Format
914///
915/// | Offset | Field    | Size     | Description                        |
916/// |--------|----------|---------|------------------------------------|
917/// | 0      | msg_type | 1 byte  | 0x20                               |
918/// | 1      | flags    | 1 byte  | Reserved                           |
919/// | 2      | dest_addr| 16 bytes| The node_addr we couldn't route to |
920/// | 18     | reporter | 16 bytes| NodeAddr of reporting router       |
921///
922/// Payload: 34 bytes
923#[derive(Clone, Debug)]
924pub struct CoordsRequired {
925    /// Destination that couldn't be routed.
926    pub dest_addr: NodeAddr,
927    /// Router reporting the miss.
928    pub reporter: NodeAddr,
929}
930
931/// Wire size of CoordsRequired payload: msg_type(1) + flags(1) + dest_addr(16) + reporter(16).
932pub const COORDS_REQUIRED_SIZE: usize = 34;
933
934impl CoordsRequired {
935    /// Create a new CoordsRequired error.
936    pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
937        Self {
938            dest_addr,
939            reporter,
940        }
941    }
942
943    /// Encode as wire format (4-byte FSP prefix + msg_type + body).
944    ///
945    /// Error signals use phase=0x0 with U flag set.
946    pub fn encode(&self) -> Vec<u8> {
947        // Body: msg_type + flags(reserved) + dest_addr + reporter
948        let body_len = 1 + 1 + 16 + 16; // 34 bytes
949        let mut buf = Vec::with_capacity(4 + body_len);
950        // FSP prefix: version 0, phase 0x0, U flag set
951        buf.push(0x00); // version 0, phase 0x0
952        buf.push(0x04); // U flag
953        let payload_len = body_len as u16;
954        buf.extend_from_slice(&payload_len.to_le_bytes());
955        // msg_type byte (after prefix, before body)
956        buf.push(SessionMessageType::CoordsRequired.to_byte());
957        buf.push(0x00); // reserved flags
958        buf.extend_from_slice(self.dest_addr.as_bytes());
959        buf.extend_from_slice(self.reporter.as_bytes());
960        buf
961    }
962
963    /// Decode from wire format (after FSP prefix and msg_type byte consumed).
964    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
965        // flags(1) + dest_addr(16) + reporter(16) = 33
966        if payload.len() < 33 {
967            return Err(ProtocolError::MessageTooShort {
968                expected: 33,
969                got: payload.len(),
970            });
971        }
972        // payload[0] is flags (reserved, ignored)
973        let mut dest_bytes = [0u8; 16];
974        dest_bytes.copy_from_slice(&payload[1..17]);
975        let mut reporter_bytes = [0u8; 16];
976        reporter_bytes.copy_from_slice(&payload[17..33]);
977
978        Ok(Self {
979            dest_addr: NodeAddr::from_bytes(dest_bytes),
980            reporter: NodeAddr::from_bytes(reporter_bytes),
981        })
982    }
983}
984
985/// Error indicating routing failure (local minimum or unreachable).
986///
987/// Carried inside a SessionDatagram addressed back to the original source.
988/// The reporting router creates a new SessionDatagram with src_addr=reporter
989/// and dest_addr=original_source, so the `original_src` field from the old
990/// design is no longer needed — it's the SessionDatagram's dest_addr.
991///
992/// ## Wire Format
993///
994/// | Offset | Field             | Size     | Description                   |
995/// |--------|-------------------|----------|-------------------------------|
996/// | 0      | msg_type          | 1 byte   | 0x21                          |
997/// | 1      | flags             | 1 byte   | Reserved                      |
998/// | 2      | dest_addr         | 16 bytes | The unreachable node_addr     |
999/// | 18     | reporter          | 16 bytes | NodeAddr of reporting router   |
1000/// | 34     | last_coords_count | 2 bytes  | u16 LE                        |
1001/// | 36     | last_known_coords | 16 × n   | Stale coords that failed      |
1002#[derive(Clone, Debug)]
1003pub struct PathBroken {
1004    /// Destination that couldn't be reached.
1005    pub dest_addr: NodeAddr,
1006    /// Node that detected the failure.
1007    pub reporter: NodeAddr,
1008    /// Optional: last known coordinates of destination.
1009    pub last_known_coords: Option<TreeCoordinate>,
1010}
1011
1012impl PathBroken {
1013    /// Create a new PathBroken error.
1014    pub fn new(dest_addr: NodeAddr, reporter: NodeAddr) -> Self {
1015        Self {
1016            dest_addr,
1017            reporter,
1018            last_known_coords: None,
1019        }
1020    }
1021
1022    /// Add last known coordinates.
1023    pub fn with_last_coords(mut self, coords: TreeCoordinate) -> Self {
1024        self.last_known_coords = Some(coords);
1025        self
1026    }
1027
1028    /// Encode as wire format (4-byte FSP prefix + msg_type + body).
1029    ///
1030    /// Error signals use phase=0x0 with U flag set.
1031    pub fn encode(&self) -> Vec<u8> {
1032        // Build body first to compute length
1033        let mut body = Vec::new();
1034        body.push(SessionMessageType::PathBroken.to_byte());
1035        body.push(0x00); // reserved flags
1036        body.extend_from_slice(self.dest_addr.as_bytes());
1037        body.extend_from_slice(self.reporter.as_bytes());
1038        if let Some(ref coords) = self.last_known_coords {
1039            encode_coords(coords, &mut body);
1040        } else {
1041            encode_empty_coords(&mut body);
1042        }
1043
1044        // Prepend FSP prefix: version 0, phase 0x0, U flag set
1045        let payload_len = body.len() as u16;
1046        let mut buf = Vec::with_capacity(4 + body.len());
1047        buf.push(0x00); // version 0, phase 0x0
1048        buf.push(0x04); // U flag
1049        buf.extend_from_slice(&payload_len.to_le_bytes());
1050        buf.extend_from_slice(&body);
1051        buf
1052    }
1053
1054    /// Decode from wire format (after FSP prefix and msg_type byte consumed).
1055    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
1056        // flags(1) + dest_addr(16) + reporter(16) + coords_count(2) = 35 minimum
1057        if payload.len() < 35 {
1058            return Err(ProtocolError::MessageTooShort {
1059                expected: 35,
1060                got: payload.len(),
1061            });
1062        }
1063        // payload[0] is flags (reserved, ignored)
1064        let mut dest_bytes = [0u8; 16];
1065        dest_bytes.copy_from_slice(&payload[1..17]);
1066        let mut reporter_bytes = [0u8; 16];
1067        reporter_bytes.copy_from_slice(&payload[17..33]);
1068
1069        let (last_known_coords, _consumed) = decode_optional_coords(&payload[33..])?;
1070
1071        Ok(Self {
1072            dest_addr: NodeAddr::from_bytes(dest_bytes),
1073            reporter: NodeAddr::from_bytes(reporter_bytes),
1074            last_known_coords,
1075        })
1076    }
1077}
1078
1079/// Error indicating a forwarded packet exceeded the next-hop transport MTU.
1080///
1081/// Generated by a transit router when `send_encrypted_link_message()`
1082/// fails with `TransportError::MtuExceeded`. The reporter includes the
1083/// bottleneck MTU so the source can immediately reduce its sending MTU.
1084///
1085/// ## Wire Format
1086///
1087/// | Offset | Field     | Size     | Description                        |
1088/// |--------|-----------|----------|------------------------------------|
1089/// | 0      | msg_type  | 1 byte   | 0x22                               |
1090/// | 1      | flags     | 1 byte   | Reserved                           |
1091/// | 2      | dest_addr | 16 bytes | The destination we were forwarding |
1092/// | 18     | reporter  | 16 bytes | NodeAddr of reporting router       |
1093/// | 34     | mtu       | 2 bytes  | Bottleneck MTU (u16 LE)            |
1094///
1095/// Payload: 36 bytes
1096#[derive(Clone, Debug)]
1097pub struct MtuExceeded {
1098    /// Destination that the oversized packet was heading to.
1099    pub dest_addr: NodeAddr,
1100    /// Router that detected the MTU violation.
1101    pub reporter: NodeAddr,
1102    /// Transport MTU at the bottleneck hop.
1103    pub mtu: u16,
1104}
1105
1106/// Wire size of MtuExceeded payload: msg_type(1) + flags(1) + dest_addr(16) + reporter(16) + mtu(2).
1107pub const MTU_EXCEEDED_SIZE: usize = 36;
1108
1109impl MtuExceeded {
1110    /// Create a new MtuExceeded error.
1111    pub fn new(dest_addr: NodeAddr, reporter: NodeAddr, mtu: u16) -> Self {
1112        Self {
1113            dest_addr,
1114            reporter,
1115            mtu,
1116        }
1117    }
1118
1119    /// Encode as wire format (4-byte FSP prefix + msg_type + body).
1120    ///
1121    /// Error signals use phase=0x0 with U flag set.
1122    pub fn encode(&self) -> Vec<u8> {
1123        let body_len = MTU_EXCEEDED_SIZE; // 36 bytes
1124        let mut buf = Vec::with_capacity(4 + body_len);
1125        // FSP prefix: version 0, phase 0x0, U flag set
1126        buf.push(0x00); // version 0, phase 0x0
1127        buf.push(0x04); // U flag
1128        let payload_len = body_len as u16;
1129        buf.extend_from_slice(&payload_len.to_le_bytes());
1130        // msg_type byte
1131        buf.push(SessionMessageType::MtuExceeded.to_byte());
1132        buf.push(0x00); // reserved flags
1133        buf.extend_from_slice(self.dest_addr.as_bytes());
1134        buf.extend_from_slice(self.reporter.as_bytes());
1135        buf.extend_from_slice(&self.mtu.to_le_bytes());
1136        buf
1137    }
1138
1139    /// Decode from wire format (after FSP prefix and msg_type byte consumed).
1140    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
1141        // flags(1) + dest_addr(16) + reporter(16) + mtu(2) = 35
1142        if payload.len() < 35 {
1143            return Err(ProtocolError::MessageTooShort {
1144                expected: 35,
1145                got: payload.len(),
1146            });
1147        }
1148        // payload[0] is flags (reserved, ignored)
1149        let mut dest_bytes = [0u8; 16];
1150        dest_bytes.copy_from_slice(&payload[1..17]);
1151        let mut reporter_bytes = [0u8; 16];
1152        reporter_bytes.copy_from_slice(&payload[17..33]);
1153        let mtu = u16::from_le_bytes([payload[33], payload[34]]);
1154
1155        Ok(Self {
1156            dest_addr: NodeAddr::from_bytes(dest_bytes),
1157            reporter: NodeAddr::from_bytes(reporter_bytes),
1158            mtu,
1159        })
1160    }
1161}
1162
1163#[cfg(test)]
1164mod tests {
1165    use super::*;
1166
1167    fn make_node_addr(val: u8) -> NodeAddr {
1168        let mut bytes = [0u8; 16];
1169        bytes[0] = val;
1170        NodeAddr::from_bytes(bytes)
1171    }
1172
1173    fn make_coords(ids: &[u8]) -> TreeCoordinate {
1174        TreeCoordinate::from_addrs(ids.iter().map(|&v| make_node_addr(v)).collect()).unwrap()
1175    }
1176
1177    // ===== SessionMessageType Tests =====
1178
1179    #[test]
1180    fn test_session_message_type_roundtrip() {
1181        let types = [
1182            SessionMessageType::DataPacket,
1183            SessionMessageType::SenderReport,
1184            SessionMessageType::ReceiverReport,
1185            SessionMessageType::PathMtuNotification,
1186            SessionMessageType::CoordsWarmup,
1187            SessionMessageType::EndpointData,
1188            SessionMessageType::CoordsRequired,
1189            SessionMessageType::PathBroken,
1190            SessionMessageType::MtuExceeded,
1191        ];
1192
1193        for ty in types {
1194            let byte = ty.to_byte();
1195            let restored = SessionMessageType::from_byte(byte);
1196            assert_eq!(restored, Some(ty));
1197        }
1198    }
1199
1200    #[test]
1201    fn test_session_message_type_invalid() {
1202        assert!(SessionMessageType::from_byte(0xFF).is_none());
1203        assert!(SessionMessageType::from_byte(0x99).is_none());
1204    }
1205
1206    // ===== SessionFlags Tests =====
1207
1208    #[test]
1209    fn test_session_flags() {
1210        let flags = SessionFlags::new().with_ack().bidirectional();
1211
1212        assert!(flags.request_ack);
1213        assert!(flags.bidirectional);
1214
1215        let byte = flags.to_byte();
1216        let restored = SessionFlags::from_byte(byte);
1217
1218        assert_eq!(flags, restored);
1219    }
1220
1221    #[test]
1222    fn test_session_flags_default() {
1223        let flags = SessionFlags::new();
1224        assert!(!flags.request_ack);
1225        assert!(!flags.bidirectional);
1226        assert_eq!(flags.to_byte(), 0);
1227    }
1228
1229    // ===== SessionSetup Tests =====
1230
1231    #[test]
1232    fn test_session_setup() {
1233        let setup = SessionSetup::new(make_coords(&[1, 0]), make_coords(&[2, 0]))
1234            .with_flags(SessionFlags::new().with_ack());
1235
1236        assert!(setup.flags.request_ack);
1237        assert!(!setup.flags.bidirectional);
1238    }
1239
1240    // ===== CoordsRequired Tests =====
1241
1242    #[test]
1243    fn test_coords_required() {
1244        let err = CoordsRequired::new(make_node_addr(1), make_node_addr(2));
1245
1246        assert_eq!(err.dest_addr, make_node_addr(1));
1247        assert_eq!(err.reporter, make_node_addr(2));
1248    }
1249
1250    // ===== PathBroken Tests =====
1251
1252    #[test]
1253    fn test_path_broken() {
1254        let err = PathBroken::new(make_node_addr(2), make_node_addr(3))
1255            .with_last_coords(make_coords(&[2, 0]));
1256
1257        assert_eq!(err.dest_addr, make_node_addr(2));
1258        assert_eq!(err.reporter, make_node_addr(3));
1259        assert!(err.last_known_coords.is_some());
1260    }
1261
1262    // ===== Encode/Decode Roundtrip Tests =====
1263
1264    #[test]
1265    fn test_session_setup_encode_decode() {
1266        let handshake = vec![0xAA; 82]; // typical Noise IK msg1
1267        let setup = SessionSetup::new(make_coords(&[1, 2, 0]), make_coords(&[3, 4, 0]))
1268            .with_flags(SessionFlags::new().with_ack().bidirectional())
1269            .with_handshake(handshake.clone());
1270
1271        let encoded = setup.encode();
1272
1273        // Verify FSP prefix: ver_phase=0x01 (version 0, phase MSG1)
1274        assert_eq!(encoded[0], 0x01);
1275        assert_eq!(encoded[1], 0x00); // flags = 0 for handshake
1276        let payload_len = u16::from_le_bytes([encoded[2], encoded[3]]);
1277        assert_eq!(payload_len as usize, encoded.len() - 4);
1278
1279        // Decode (skip 4-byte FSP prefix)
1280        let decoded = SessionSetup::decode(&encoded[4..]).unwrap();
1281
1282        assert_eq!(decoded.flags, setup.flags);
1283        assert_eq!(decoded.src_coords, setup.src_coords);
1284        assert_eq!(decoded.dest_coords, setup.dest_coords);
1285        assert_eq!(decoded.handshake_payload, handshake);
1286    }
1287
1288    #[test]
1289    fn test_session_setup_no_handshake() {
1290        let setup = SessionSetup::new(make_coords(&[5, 0]), make_coords(&[6, 0]));
1291
1292        let encoded = setup.encode();
1293        let decoded = SessionSetup::decode(&encoded[4..]).unwrap();
1294
1295        assert!(decoded.handshake_payload.is_empty());
1296        assert_eq!(decoded.src_coords, setup.src_coords);
1297        assert_eq!(decoded.dest_coords, setup.dest_coords);
1298    }
1299
1300    #[test]
1301    fn test_session_ack_encode_decode() {
1302        let handshake = vec![0xBB; 33]; // typical Noise IK msg2
1303        let ack = SessionAck::new(make_coords(&[7, 8, 0]), make_coords(&[3, 4, 0]))
1304            .with_handshake(handshake.clone());
1305
1306        let encoded = ack.encode();
1307        // Verify FSP prefix: ver_phase=0x02 (version 0, phase MSG2)
1308        assert_eq!(encoded[0], 0x02);
1309        assert_eq!(encoded[1], 0x00); // flags = 0 for handshake
1310
1311        let decoded = SessionAck::decode(&encoded[4..]).unwrap();
1312        assert_eq!(decoded.src_coords, ack.src_coords);
1313        assert_eq!(decoded.dest_coords, ack.dest_coords);
1314        assert_eq!(decoded.handshake_payload, handshake);
1315    }
1316
1317    #[test]
1318    fn test_coords_required_encode_decode() {
1319        let err = CoordsRequired::new(make_node_addr(0xAA), make_node_addr(0xBB));
1320
1321        let encoded = err.encode();
1322        // 4 prefix + 1 msg_type + 1 flags + 16 dest + 16 reporter = 38
1323        assert_eq!(encoded.len(), 4 + COORDS_REQUIRED_SIZE);
1324        // Check FSP prefix: phase 0x0, U flag
1325        assert_eq!(encoded[0], 0x00);
1326        assert_eq!(encoded[1], 0x04); // U flag
1327        // msg_type after prefix
1328        assert_eq!(encoded[4], 0x20);
1329
1330        // decode after prefix + msg_type consumed
1331        let decoded = CoordsRequired::decode(&encoded[5..]).unwrap();
1332        assert_eq!(decoded.dest_addr, err.dest_addr);
1333        assert_eq!(decoded.reporter, err.reporter);
1334    }
1335
1336    #[test]
1337    fn test_path_broken_encode_decode_no_coords() {
1338        let err = PathBroken::new(make_node_addr(0xCC), make_node_addr(0xDD));
1339
1340        let encoded = err.encode();
1341        // Check FSP prefix
1342        assert_eq!(encoded[0], 0x00);
1343        assert_eq!(encoded[1], 0x04); // U flag
1344        assert_eq!(encoded[4], 0x21); // msg_type
1345
1346        let decoded = PathBroken::decode(&encoded[5..]).unwrap();
1347        assert_eq!(decoded.dest_addr, err.dest_addr);
1348        assert_eq!(decoded.reporter, err.reporter);
1349        assert!(decoded.last_known_coords.is_none());
1350    }
1351
1352    #[test]
1353    fn test_path_broken_encode_decode_with_coords() {
1354        let coords = make_coords(&[0xCC, 0xDD, 0xEE]);
1355        let err = PathBroken::new(make_node_addr(0x11), make_node_addr(0x22))
1356            .with_last_coords(coords.clone());
1357
1358        let encoded = err.encode();
1359        let decoded = PathBroken::decode(&encoded[5..]).unwrap();
1360
1361        assert_eq!(decoded.dest_addr, err.dest_addr);
1362        assert_eq!(decoded.reporter, err.reporter);
1363        assert_eq!(decoded.last_known_coords.unwrap(), coords);
1364    }
1365
1366    #[test]
1367    fn test_session_setup_decode_too_short() {
1368        assert!(SessionSetup::decode(&[]).is_err());
1369    }
1370
1371    #[test]
1372    fn test_session_ack_decode_too_short() {
1373        assert!(SessionAck::decode(&[]).is_err());
1374    }
1375
1376    #[test]
1377    fn test_coords_required_decode_too_short() {
1378        assert!(CoordsRequired::decode(&[]).is_err());
1379        assert!(CoordsRequired::decode(&[0x00; 10]).is_err());
1380    }
1381
1382    #[test]
1383    fn test_path_broken_decode_too_short() {
1384        assert!(PathBroken::decode(&[]).is_err());
1385        assert!(PathBroken::decode(&[0x00; 20]).is_err());
1386    }
1387
1388    #[test]
1389    fn test_session_setup_deep_coords() {
1390        // Depth-10 coordinate (11 entries: self + 10 ancestors)
1391        let addrs: Vec<u8> = (0..11).collect();
1392        let src = make_coords(&addrs);
1393        let dest = make_coords(&[20, 21, 22, 23, 24]);
1394        let setup = SessionSetup::new(src.clone(), dest.clone()).with_handshake(vec![0x55; 82]);
1395
1396        let encoded = setup.encode();
1397        let decoded = SessionSetup::decode(&encoded[4..]).unwrap();
1398
1399        assert_eq!(decoded.src_coords, src);
1400        assert_eq!(decoded.dest_coords, dest);
1401    }
1402
1403    // ===== FspFlags Tests =====
1404
1405    #[test]
1406    fn test_fsp_flags_default() {
1407        let flags = FspFlags::new();
1408        assert!(!flags.coords_present);
1409        assert!(!flags.key_epoch);
1410        assert!(!flags.unencrypted);
1411        assert_eq!(flags.to_byte(), 0x00);
1412    }
1413
1414    #[test]
1415    fn test_fsp_flags_roundtrip() {
1416        // All combinations of 3 bits
1417        for byte in 0u8..=0x07 {
1418            let flags = FspFlags::from_byte(byte);
1419            assert_eq!(flags.to_byte(), byte);
1420        }
1421    }
1422
1423    #[test]
1424    fn test_fsp_flags_individual_bits() {
1425        let cp = FspFlags::from_byte(0x01);
1426        assert!(cp.coords_present);
1427        assert!(!cp.key_epoch);
1428        assert!(!cp.unencrypted);
1429
1430        let k = FspFlags::from_byte(0x02);
1431        assert!(!k.coords_present);
1432        assert!(k.key_epoch);
1433        assert!(!k.unencrypted);
1434
1435        let u = FspFlags::from_byte(0x04);
1436        assert!(!u.coords_present);
1437        assert!(!u.key_epoch);
1438        assert!(u.unencrypted);
1439    }
1440
1441    #[test]
1442    fn test_fsp_flags_ignores_reserved_bits() {
1443        // Reserved bits in upper 5 bits are not preserved
1444        let flags = FspFlags::from_byte(0xFF);
1445        assert!(flags.coords_present);
1446        assert!(flags.key_epoch);
1447        assert!(flags.unencrypted);
1448        assert_eq!(flags.to_byte(), 0x07); // only lower 3 bits
1449    }
1450
1451    // ===== FspInnerFlags Tests =====
1452
1453    #[test]
1454    fn test_fsp_inner_flags_default() {
1455        let flags = FspInnerFlags::new();
1456        assert!(!flags.spin_bit);
1457        assert_eq!(flags.to_byte(), 0x00);
1458    }
1459
1460    #[test]
1461    fn test_fsp_inner_flags_roundtrip() {
1462        let flags = FspInnerFlags::from_byte(0x01);
1463        assert!(flags.spin_bit);
1464        assert_eq!(flags.to_byte(), 0x01);
1465
1466        let flags = FspInnerFlags::from_byte(0x00);
1467        assert!(!flags.spin_bit);
1468        assert_eq!(flags.to_byte(), 0x00);
1469    }
1470
1471    #[test]
1472    fn test_fsp_inner_flags_ignores_reserved() {
1473        let flags = FspInnerFlags::from_byte(0xFE);
1474        assert!(!flags.spin_bit);
1475        assert_eq!(flags.to_byte(), 0x00);
1476
1477        let flags = FspInnerFlags::from_byte(0xFF);
1478        assert!(flags.spin_bit);
1479        assert_eq!(flags.to_byte(), 0x01);
1480    }
1481
1482    // ===== New SessionMessageType Values =====
1483
1484    #[test]
1485    fn test_session_message_type_new_values() {
1486        assert_eq!(SessionMessageType::SenderReport.to_byte(), 0x11);
1487        assert_eq!(SessionMessageType::ReceiverReport.to_byte(), 0x12);
1488        assert_eq!(SessionMessageType::PathMtuNotification.to_byte(), 0x13);
1489        assert_eq!(SessionMessageType::CoordsWarmup.to_byte(), 0x14);
1490        assert_eq!(SessionMessageType::EndpointData.to_byte(), 0x15);
1491    }
1492
1493    #[test]
1494    fn test_session_message_type_display() {
1495        assert_eq!(
1496            format!("{}", SessionMessageType::SenderReport),
1497            "SenderReport"
1498        );
1499        assert_eq!(
1500            format!("{}", SessionMessageType::ReceiverReport),
1501            "ReceiverReport"
1502        );
1503        assert_eq!(
1504            format!("{}", SessionMessageType::PathMtuNotification),
1505            "PathMtuNotification"
1506        );
1507        assert_eq!(
1508            format!("{}", SessionMessageType::CoordsWarmup),
1509            "CoordsWarmup"
1510        );
1511        assert_eq!(
1512            format!("{}", SessionMessageType::EndpointData),
1513            "EndpointData"
1514        );
1515    }
1516
1517    // ===== SessionSenderReport Tests =====
1518
1519    fn sample_session_sender_report() -> SessionSenderReport {
1520        SessionSenderReport {
1521            interval_start_counter: 100,
1522            interval_end_counter: 200,
1523            interval_start_timestamp: 5000,
1524            interval_end_timestamp: 6000,
1525            interval_bytes_sent: 50_000,
1526            cumulative_packets_sent: 10_000,
1527            cumulative_bytes_sent: 5_000_000,
1528        }
1529    }
1530
1531    #[test]
1532    fn test_session_sender_report_encode_size() {
1533        let sr = sample_session_sender_report();
1534        let encoded = sr.encode();
1535        assert_eq!(encoded.len(), SESSION_SENDER_REPORT_SIZE);
1536    }
1537
1538    #[test]
1539    fn test_session_sender_report_roundtrip() {
1540        let sr = sample_session_sender_report();
1541        let encoded = sr.encode();
1542        let decoded = SessionSenderReport::decode(&encoded).unwrap();
1543        assert_eq!(sr, decoded);
1544    }
1545
1546    #[test]
1547    fn test_session_sender_report_too_short() {
1548        assert!(SessionSenderReport::decode(&[0u8; 10]).is_err());
1549    }
1550
1551    // ===== SessionReceiverReport Tests =====
1552
1553    fn sample_session_receiver_report() -> SessionReceiverReport {
1554        SessionReceiverReport {
1555            highest_counter: 195,
1556            cumulative_packets_recv: 9_500,
1557            cumulative_bytes_recv: 4_750_000,
1558            timestamp_echo: 5900,
1559            dwell_time: 5,
1560            max_burst_loss: 3,
1561            mean_burst_loss: 384,
1562            jitter: 1200,
1563            ecn_ce_count: 0,
1564            owd_trend: -50,
1565            burst_loss_count: 2,
1566            cumulative_reorder_count: 10,
1567            interval_packets_recv: 95,
1568            interval_bytes_recv: 47_500,
1569        }
1570    }
1571
1572    #[test]
1573    fn test_session_receiver_report_encode_size() {
1574        let rr = sample_session_receiver_report();
1575        let encoded = rr.encode();
1576        assert_eq!(encoded.len(), SESSION_RECEIVER_REPORT_SIZE);
1577    }
1578
1579    #[test]
1580    fn test_session_receiver_report_roundtrip() {
1581        let rr = sample_session_receiver_report();
1582        let encoded = rr.encode();
1583        let decoded = SessionReceiverReport::decode(&encoded).unwrap();
1584        assert_eq!(rr, decoded);
1585    }
1586
1587    #[test]
1588    fn test_session_receiver_report_too_short() {
1589        assert!(SessionReceiverReport::decode(&[0u8; 10]).is_err());
1590    }
1591
1592    #[test]
1593    fn test_session_receiver_report_negative_owd_trend() {
1594        let rr = SessionReceiverReport {
1595            owd_trend: -12345,
1596            ..sample_session_receiver_report()
1597        };
1598        let encoded = rr.encode();
1599        let decoded = SessionReceiverReport::decode(&encoded).unwrap();
1600        assert_eq!(decoded.owd_trend, -12345);
1601    }
1602
1603    // ===== PathMtuNotification Tests =====
1604
1605    #[test]
1606    fn test_path_mtu_notification_encode_size() {
1607        let n = PathMtuNotification::new(1400);
1608        let encoded = n.encode();
1609        assert_eq!(encoded.len(), PATH_MTU_NOTIFICATION_SIZE);
1610    }
1611
1612    #[test]
1613    fn test_path_mtu_notification_roundtrip() {
1614        let n = PathMtuNotification::new(1400);
1615        let encoded = n.encode();
1616        let decoded = PathMtuNotification::decode(&encoded).unwrap();
1617        assert_eq!(decoded.path_mtu, 1400);
1618    }
1619
1620    #[test]
1621    fn test_path_mtu_notification_too_short() {
1622        assert!(PathMtuNotification::decode(&[]).is_err());
1623        assert!(PathMtuNotification::decode(&[0x00]).is_err());
1624    }
1625
1626    #[test]
1627    fn test_path_mtu_notification_boundary_values() {
1628        for mtu in [0u16, 1280, 1500, u16::MAX] {
1629            let n = PathMtuNotification::new(mtu);
1630            let encoded = n.encode();
1631            let decoded = PathMtuNotification::decode(&encoded).unwrap();
1632            assert_eq!(decoded.path_mtu, mtu);
1633        }
1634    }
1635
1636    // ===== MtuExceeded Tests =====
1637
1638    #[test]
1639    fn test_mtu_exceeded_encode_size() {
1640        let err = MtuExceeded::new(make_node_addr(0xAA), make_node_addr(0xBB), 1400);
1641        let encoded = err.encode();
1642        // 4 prefix + 36 body = 40
1643        assert_eq!(encoded.len(), 4 + MTU_EXCEEDED_SIZE);
1644    }
1645
1646    #[test]
1647    fn test_mtu_exceeded_encode_decode() {
1648        let err = MtuExceeded::new(make_node_addr(0xAA), make_node_addr(0xBB), 1400);
1649
1650        let encoded = err.encode();
1651        // Check FSP prefix: phase 0x0, U flag
1652        assert_eq!(encoded[0], 0x00);
1653        assert_eq!(encoded[1], 0x04); // U flag
1654        // msg_type after prefix
1655        assert_eq!(encoded[4], 0x22);
1656
1657        // decode after prefix + msg_type consumed
1658        let decoded = MtuExceeded::decode(&encoded[5..]).unwrap();
1659        assert_eq!(decoded.dest_addr, err.dest_addr);
1660        assert_eq!(decoded.reporter, err.reporter);
1661        assert_eq!(decoded.mtu, 1400);
1662    }
1663
1664    #[test]
1665    fn test_mtu_exceeded_decode_too_short() {
1666        assert!(MtuExceeded::decode(&[]).is_err());
1667        assert!(MtuExceeded::decode(&[0x00; 20]).is_err());
1668        assert!(MtuExceeded::decode(&[0x00; 34]).is_err()); // exactly 1 byte short
1669    }
1670
1671    #[test]
1672    fn test_mtu_exceeded_boundary_mtu_values() {
1673        for mtu in [0u16, 1280, 1500, u16::MAX] {
1674            let err = MtuExceeded::new(make_node_addr(1), make_node_addr(2), mtu);
1675            let encoded = err.encode();
1676            let decoded = MtuExceeded::decode(&encoded[5..]).unwrap();
1677            assert_eq!(decoded.mtu, mtu);
1678        }
1679    }
1680
1681    #[test]
1682    fn test_mtu_exceeded_message_type_value() {
1683        assert_eq!(SessionMessageType::MtuExceeded.to_byte(), 0x22);
1684        assert_eq!(
1685            SessionMessageType::from_byte(0x22),
1686            Some(SessionMessageType::MtuExceeded)
1687        );
1688    }
1689
1690    #[test]
1691    fn test_mtu_exceeded_display() {
1692        assert_eq!(
1693            format!("{}", SessionMessageType::MtuExceeded),
1694            "MtuExceeded"
1695        );
1696    }
1697
1698    // ===== SessionMsg3 Tests =====
1699
1700    #[test]
1701    fn test_session_msg3_encode_decode() {
1702        let handshake = vec![0xCC; 73]; // typical XK msg3
1703        let msg3 = SessionMsg3::new(handshake.clone());
1704
1705        let encoded = msg3.encode();
1706        // Verify FSP prefix: ver_phase=0x03 (version 0, phase MSG3)
1707        assert_eq!(encoded[0], 0x03);
1708        assert_eq!(encoded[1], 0x00); // flags = 0 for handshake
1709        let payload_len = u16::from_le_bytes([encoded[2], encoded[3]]);
1710        assert_eq!(payload_len as usize, encoded.len() - 4);
1711
1712        // Decode (skip 4-byte FSP prefix)
1713        let decoded = SessionMsg3::decode(&encoded[4..]).unwrap();
1714        assert_eq!(decoded.flags, 0);
1715        assert_eq!(decoded.handshake_payload, handshake);
1716    }
1717
1718    #[test]
1719    fn test_session_msg3_decode_too_short() {
1720        assert!(SessionMsg3::decode(&[]).is_err());
1721        assert!(SessionMsg3::decode(&[0x00]).is_err()); // flags only, no hs_len
1722    }
1723
1724    #[test]
1725    fn test_session_msg3_empty_handshake() {
1726        let msg3 = SessionMsg3::new(vec![]);
1727        let encoded = msg3.encode();
1728        let decoded = SessionMsg3::decode(&encoded[4..]).unwrap();
1729        assert!(decoded.handshake_payload.is_empty());
1730    }
1731}