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