Skip to main content

fips_core/protocol/
link.rs

1//! Link-layer message types: handshake, link control, disconnect, session datagram.
2
3use super::ProtocolError;
4use crate::NodeAddr;
5use std::fmt;
6
7// ============================================================================
8// Handshake Message Types
9// ============================================================================
10
11/// Handshake message type identifiers.
12///
13/// These messages are exchanged during Noise IK handshake before link
14/// encryption is established. They use the same TLV framing as link
15/// messages but payloads are not encrypted (except Noise-internal encryption).
16#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17#[repr(u8)]
18pub enum HandshakeMessageType {
19    /// Noise IK message 1: initiator sends ephemeral + encrypted static.
20    /// Payload: 82 bytes (33 ephemeral + 33 static + 16 tag).
21    NoiseIKMsg1 = 0x01,
22
23    /// Noise IK message 2: responder sends ephemeral.
24    /// Payload: 33 bytes (ephemeral pubkey only).
25    NoiseIKMsg2 = 0x02,
26}
27
28impl HandshakeMessageType {
29    /// Try to convert from a byte.
30    pub fn from_byte(b: u8) -> Option<Self> {
31        match b {
32            0x01 => Some(HandshakeMessageType::NoiseIKMsg1),
33            0x02 => Some(HandshakeMessageType::NoiseIKMsg2),
34            _ => None,
35        }
36    }
37
38    /// Convert to a byte.
39    pub fn to_byte(self) -> u8 {
40        self as u8
41    }
42
43    /// Check if a byte represents a handshake message type.
44    pub fn is_handshake(b: u8) -> bool {
45        matches!(b, 0x01 | 0x02)
46    }
47}
48
49impl fmt::Display for HandshakeMessageType {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        let name = match self {
52            HandshakeMessageType::NoiseIKMsg1 => "NoiseIKMsg1",
53            HandshakeMessageType::NoiseIKMsg2 => "NoiseIKMsg2",
54        };
55        write!(f, "{}", name)
56    }
57}
58
59// ============================================================================
60// Link-Layer Message Types
61// ============================================================================
62
63/// Link-layer message type identifiers.
64///
65/// These messages are exchanged between directly connected peers over
66/// Noise-encrypted links. All payloads are encrypted with session keys
67/// established during the Noise IK handshake.
68#[derive(Clone, Copy, Debug, PartialEq, Eq)]
69#[repr(u8)]
70pub enum LinkMessageType {
71    // Forwarding (0x00-0x0F)
72    /// Encapsulated session-layer datagram for forwarding.
73    /// Payload is opaque to intermediate nodes (end-to-end encrypted).
74    SessionDatagram = 0x00,
75
76    // MMP reports (0x01-0x02) — content defined in TASK-2026-0006
77    /// Sender-side MMP report (stub).
78    SenderReport = 0x01,
79    /// Receiver-side MMP report (stub).
80    ReceiverReport = 0x02,
81    // 0x03 is intentionally reserved. Endpoint data stays inside
82    // `SessionDatagram` + FSP even for direct neighbors.
83
84    // Tree protocol (0x10-0x1F)
85    /// Spanning tree state announcement.
86    TreeAnnounce = 0x10,
87
88    // Bloom filter (0x20-0x2F)
89    /// Bloom filter reachability update.
90    FilterAnnounce = 0x20,
91
92    // Discovery (0x30-0x3F)
93    /// Request to discover a node's coordinates.
94    LookupRequest = 0x30,
95    /// Response with target's coordinates.
96    LookupResponse = 0x31,
97
98    // Link Control (0x50-0x5F)
99    /// Orderly disconnect notification before link closure.
100    Disconnect = 0x50,
101    /// Periodic heartbeat for link liveness detection.
102    /// No payload — the msg_type byte alone is sufficient.
103    Heartbeat = 0x51,
104}
105
106impl LinkMessageType {
107    /// Try to convert from a byte.
108    pub fn from_byte(b: u8) -> Option<Self> {
109        match b {
110            0x00 => Some(LinkMessageType::SessionDatagram),
111            0x01 => Some(LinkMessageType::SenderReport),
112            0x02 => Some(LinkMessageType::ReceiverReport),
113            0x10 => Some(LinkMessageType::TreeAnnounce),
114            0x20 => Some(LinkMessageType::FilterAnnounce),
115            0x30 => Some(LinkMessageType::LookupRequest),
116            0x31 => Some(LinkMessageType::LookupResponse),
117            0x50 => Some(LinkMessageType::Disconnect),
118            0x51 => Some(LinkMessageType::Heartbeat),
119            _ => None,
120        }
121    }
122
123    /// Convert to a byte.
124    pub fn to_byte(self) -> u8 {
125        self as u8
126    }
127}
128
129impl fmt::Display for LinkMessageType {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        let name = match self {
132            LinkMessageType::SessionDatagram => "SessionDatagram",
133            LinkMessageType::SenderReport => "SenderReport",
134            LinkMessageType::ReceiverReport => "ReceiverReport",
135            LinkMessageType::TreeAnnounce => "TreeAnnounce",
136            LinkMessageType::FilterAnnounce => "FilterAnnounce",
137            LinkMessageType::LookupRequest => "LookupRequest",
138            LinkMessageType::LookupResponse => "LookupResponse",
139            LinkMessageType::Disconnect => "Disconnect",
140            LinkMessageType::Heartbeat => "Heartbeat",
141        };
142        write!(f, "{}", name)
143    }
144}
145
146// ============================================================================
147// Disconnect Reason Codes
148// ============================================================================
149
150/// Reason for an orderly disconnect notification.
151#[derive(Clone, Copy, Debug, PartialEq, Eq)]
152#[repr(u8)]
153pub enum DisconnectReason {
154    /// Normal shutdown (operator requested).
155    Shutdown = 0x00,
156    /// Restarting (may reconnect soon).
157    Restart = 0x01,
158    /// Protocol error encountered.
159    ProtocolError = 0x02,
160    /// Transport failure.
161    TransportFailure = 0x03,
162    /// Resource exhaustion (memory, connections).
163    ResourceExhaustion = 0x04,
164    /// Authentication or security policy violation.
165    SecurityViolation = 0x05,
166    /// Configuration change (peer removed from config).
167    ConfigurationChange = 0x06,
168    /// Timeout or keepalive failure.
169    Timeout = 0x07,
170    /// Unspecified reason.
171    Other = 0xFF,
172}
173
174impl DisconnectReason {
175    /// Try to convert from a byte.
176    pub fn from_byte(b: u8) -> Option<Self> {
177        match b {
178            0x00 => Some(DisconnectReason::Shutdown),
179            0x01 => Some(DisconnectReason::Restart),
180            0x02 => Some(DisconnectReason::ProtocolError),
181            0x03 => Some(DisconnectReason::TransportFailure),
182            0x04 => Some(DisconnectReason::ResourceExhaustion),
183            0x05 => Some(DisconnectReason::SecurityViolation),
184            0x06 => Some(DisconnectReason::ConfigurationChange),
185            0x07 => Some(DisconnectReason::Timeout),
186            0xFF => Some(DisconnectReason::Other),
187            _ => None,
188        }
189    }
190
191    /// Convert to a byte.
192    pub fn to_byte(self) -> u8 {
193        self as u8
194    }
195}
196
197impl fmt::Display for DisconnectReason {
198    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
199        let name = match self {
200            DisconnectReason::Shutdown => "Shutdown",
201            DisconnectReason::Restart => "Restart",
202            DisconnectReason::ProtocolError => "ProtocolError",
203            DisconnectReason::TransportFailure => "TransportFailure",
204            DisconnectReason::ResourceExhaustion => "ResourceExhaustion",
205            DisconnectReason::SecurityViolation => "SecurityViolation",
206            DisconnectReason::ConfigurationChange => "ConfigurationChange",
207            DisconnectReason::Timeout => "Timeout",
208            DisconnectReason::Other => "Other",
209        };
210        write!(f, "{}", name)
211    }
212}
213
214// ============================================================================
215// Disconnect Message
216// ============================================================================
217
218/// Orderly disconnect notification sent before closing a peer link.
219///
220/// Sent as a link-layer message (type 0x50) inside an encrypted frame.
221/// Allows the receiving peer to immediately clean up state rather than
222/// waiting for timeout-based detection.
223///
224/// ## Wire Format
225///
226/// | Offset | Field    | Size   | Notes                  |
227/// |--------|----------|--------|------------------------|
228/// | 0      | msg_type | 1 byte | 0x50                   |
229/// | 1      | reason   | 1 byte | DisconnectReason value |
230#[derive(Clone, Debug)]
231pub struct Disconnect {
232    /// Reason for disconnection.
233    pub reason: DisconnectReason,
234}
235
236impl Disconnect {
237    /// Create a new Disconnect message.
238    pub fn new(reason: DisconnectReason) -> Self {
239        Self { reason }
240    }
241
242    /// Encode as link-layer plaintext (msg_type + reason).
243    pub fn encode(&self) -> [u8; 2] {
244        [LinkMessageType::Disconnect.to_byte(), self.reason.to_byte()]
245    }
246
247    /// Decode from link-layer payload (after msg_type byte has been consumed).
248    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
249        if payload.is_empty() {
250            return Err(ProtocolError::MessageTooShort {
251                expected: 1,
252                got: 0,
253            });
254        }
255        let reason = DisconnectReason::from_byte(payload[0]).unwrap_or(DisconnectReason::Other);
256        Ok(Self { reason })
257    }
258}
259
260// ============================================================================
261// Session Datagram (Link-Layer Encapsulation)
262// ============================================================================
263
264/// Encapsulated session-layer datagram for multi-hop forwarding.
265///
266/// This is a link-layer message (type 0x00) that carries session-layer
267/// payloads through the mesh. The envelope provides source and destination
268/// addressing that transit routers use for forwarding decisions and error
269/// routing.
270///
271/// ## Wire Format (36-byte fixed header)
272///
273/// | Offset | Field     | Size     | Description                         |
274/// |--------|-----------|----------|-------------------------------------|
275/// | 0      | msg_type  | 1 byte   | 0x00                                |
276/// | 1      | ttl       | 1 byte   | Decremented each hop                |
277/// | 2      | path_mtu  | 2 bytes  | Path MTU (LE), min'd at each hop    |
278/// | 4      | src_addr  | 16 bytes | Source node_addr                    |
279/// | 20     | dest_addr | 16 bytes | Destination node_addr               |
280/// | 36     | payload   | variable | Session-layer message               |
281///
282/// The payload is either end-to-end encrypted (handshake messages, data,
283/// reports) or plaintext error signals (CoordsRequired, PathBroken)
284/// generated by transit routers.
285#[derive(Clone, Debug)]
286pub struct SessionDatagram {
287    /// Source node address (originator of this datagram).
288    /// For data traffic: the source endpoint.
289    /// For error signals: the transit router that generated the error.
290    pub src_addr: NodeAddr,
291    /// Destination node address (for routing decisions).
292    pub dest_addr: NodeAddr,
293    /// Time-to-live (decremented at each hop, dropped at zero).
294    pub ttl: u8,
295    /// Path MTU: minimum link MTU along the path so far.
296    /// Each forwarding hop applies min(path_mtu, outgoing_link_mtu).
297    pub path_mtu: u16,
298    /// Session-layer payload (e2e encrypted or plaintext error signal).
299    pub payload: Vec<u8>,
300}
301
302/// SessionDatagram fixed header size: msg_type(1) + ttl(1) + path_mtu(2) + src_addr(16) + dest_addr(16).
303pub const SESSION_DATAGRAM_HEADER_SIZE: usize = 36;
304
305impl SessionDatagram {
306    /// Create a new session datagram.
307    pub fn new(src_addr: NodeAddr, dest_addr: NodeAddr, payload: Vec<u8>) -> Self {
308        Self {
309            src_addr,
310            dest_addr,
311            ttl: 64,
312            path_mtu: u16::MAX,
313            payload,
314        }
315    }
316
317    /// Set the TTL.
318    pub fn with_ttl(mut self, ttl: u8) -> Self {
319        self.ttl = ttl;
320        self
321    }
322
323    /// Set the path MTU.
324    pub fn with_path_mtu(mut self, path_mtu: u16) -> Self {
325        self.path_mtu = path_mtu;
326        self
327    }
328
329    /// Decrement TTL, returning false if exhausted.
330    pub fn decrement_ttl(&mut self) -> bool {
331        if self.ttl > 0 {
332            self.ttl -= 1;
333            true
334        } else {
335            false
336        }
337    }
338
339    /// Check if the datagram can be forwarded.
340    pub fn can_forward(&self) -> bool {
341        self.ttl > 0
342    }
343
344    /// Encode as link-layer message (msg_type + ttl + path_mtu + src_addr + dest_addr + payload).
345    pub fn encode(&self) -> Vec<u8> {
346        let mut buf = Vec::with_capacity(SESSION_DATAGRAM_HEADER_SIZE + self.payload.len());
347        buf.push(LinkMessageType::SessionDatagram.to_byte());
348        buf.push(self.ttl);
349        buf.extend_from_slice(&self.path_mtu.to_le_bytes());
350        buf.extend_from_slice(self.src_addr.as_bytes());
351        buf.extend_from_slice(self.dest_addr.as_bytes());
352        buf.extend_from_slice(&self.payload);
353        buf
354    }
355
356    /// Decode from link-layer payload (after msg_type byte has been consumed).
357    ///
358    /// **Owning** variant — allocates a `Vec<u8>` for the inner
359    /// payload. The data-plane hot path should prefer
360    /// [`SessionDatagramRef::decode`] which is zero-copy.
361    pub fn decode(payload: &[u8]) -> Result<Self, ProtocolError> {
362        let r = SessionDatagramRef::decode(payload)?;
363        Ok(Self {
364            src_addr: r.src_addr,
365            dest_addr: r.dest_addr,
366            ttl: r.ttl,
367            path_mtu: r.path_mtu,
368            payload: r.payload.to_vec(),
369        })
370    }
371}
372
373/// Zero-copy view over a `SessionDatagram`. The inner `payload` is a
374/// borrowed slice into the caller's buffer — no allocation, no memcpy.
375///
376/// Use this on the bulk data-plane RX path where every saved alloc /
377/// copy is ~150 MB/sec of memory bandwidth at line rate.
378#[derive(Debug, Clone, Copy)]
379pub struct SessionDatagramRef<'a> {
380    pub src_addr: NodeAddr,
381    pub dest_addr: NodeAddr,
382    pub ttl: u8,
383    pub path_mtu: u16,
384    pub payload: &'a [u8],
385}
386
387impl<'a> SessionDatagramRef<'a> {
388    /// Decode from a link-layer payload (after msg_type byte has been
389    /// consumed). The returned `payload` borrows `buf[35..]`.
390    pub fn decode(buf: &'a [u8]) -> Result<Self, ProtocolError> {
391        // ttl(1) + path_mtu(2) + src_addr(16) + dest_addr(16) = 35
392        if buf.len() < 35 {
393            return Err(ProtocolError::MessageTooShort {
394                expected: 35,
395                got: buf.len(),
396            });
397        }
398        let ttl = buf[0];
399        let path_mtu = u16::from_le_bytes([buf[1], buf[2]]);
400        let mut src_bytes = [0u8; 16];
401        src_bytes.copy_from_slice(&buf[3..19]);
402        let mut dest_bytes = [0u8; 16];
403        dest_bytes.copy_from_slice(&buf[19..35]);
404        Ok(Self {
405            src_addr: NodeAddr::from_bytes(src_bytes),
406            dest_addr: NodeAddr::from_bytes(dest_bytes),
407            ttl,
408            path_mtu,
409            payload: &buf[35..],
410        })
411    }
412
413    /// Fixed-header size — the byte offset at which `payload` starts
414    /// inside the decode buffer.
415    pub const HEADER_LEN: usize = 35;
416}
417
418// Legacy type alias for compatibility during transition
419#[deprecated(note = "Use LinkMessageType or SessionMessageType instead")]
420pub type MessageType = LinkMessageType;
421
422#[cfg(test)]
423mod tests {
424    use super::*;
425
426    // ===== HandshakeMessageType Tests =====
427
428    #[test]
429    fn test_handshake_message_type_roundtrip() {
430        let types = [
431            HandshakeMessageType::NoiseIKMsg1,
432            HandshakeMessageType::NoiseIKMsg2,
433        ];
434
435        for ty in types {
436            let byte = ty.to_byte();
437            let restored = HandshakeMessageType::from_byte(byte);
438            assert_eq!(restored, Some(ty));
439        }
440    }
441
442    #[test]
443    fn test_handshake_message_type_invalid() {
444        assert!(HandshakeMessageType::from_byte(0x00).is_none());
445        assert!(HandshakeMessageType::from_byte(0x03).is_none());
446        assert!(HandshakeMessageType::from_byte(0x10).is_none());
447    }
448
449    #[test]
450    fn test_handshake_message_type_is_handshake() {
451        assert!(HandshakeMessageType::is_handshake(0x01));
452        assert!(HandshakeMessageType::is_handshake(0x02));
453        assert!(!HandshakeMessageType::is_handshake(0x00));
454        assert!(!HandshakeMessageType::is_handshake(0x10));
455    }
456
457    // ===== LinkMessageType Tests =====
458
459    #[test]
460    fn test_link_message_type_roundtrip() {
461        let types = [
462            LinkMessageType::TreeAnnounce,
463            LinkMessageType::FilterAnnounce,
464            LinkMessageType::LookupRequest,
465            LinkMessageType::LookupResponse,
466            LinkMessageType::SessionDatagram,
467            LinkMessageType::Disconnect,
468            LinkMessageType::Heartbeat,
469        ];
470
471        for ty in types {
472            let byte = ty.to_byte();
473            let restored = LinkMessageType::from_byte(byte);
474            assert_eq!(restored, Some(ty));
475        }
476    }
477
478    #[test]
479    fn test_link_message_type_invalid() {
480        assert!(LinkMessageType::from_byte(0xFF).is_none());
481        assert!(LinkMessageType::from_byte(0x03).is_none());
482        assert!(LinkMessageType::from_byte(0x04).is_none());
483        assert!(LinkMessageType::from_byte(0x40).is_none());
484    }
485
486    // ===== DisconnectReason Tests =====
487
488    #[test]
489    fn test_disconnect_reason_roundtrip() {
490        let reasons = [
491            DisconnectReason::Shutdown,
492            DisconnectReason::Restart,
493            DisconnectReason::ProtocolError,
494            DisconnectReason::TransportFailure,
495            DisconnectReason::ResourceExhaustion,
496            DisconnectReason::SecurityViolation,
497            DisconnectReason::ConfigurationChange,
498            DisconnectReason::Timeout,
499            DisconnectReason::Other,
500        ];
501
502        for reason in reasons {
503            let byte = reason.to_byte();
504            let restored = DisconnectReason::from_byte(byte);
505            assert_eq!(restored, Some(reason));
506        }
507    }
508
509    #[test]
510    fn test_disconnect_reason_unknown_byte() {
511        assert!(DisconnectReason::from_byte(0x08).is_none());
512        assert!(DisconnectReason::from_byte(0x80).is_none());
513        assert!(DisconnectReason::from_byte(0xFE).is_none());
514    }
515
516    // ===== Disconnect Message Tests =====
517
518    #[test]
519    fn test_disconnect_encode_decode() {
520        let msg = Disconnect::new(DisconnectReason::Shutdown);
521        let encoded = msg.encode();
522
523        assert_eq!(encoded.len(), 2);
524        assert_eq!(encoded[0], 0x50); // LinkMessageType::Disconnect
525        assert_eq!(encoded[1], 0x00); // DisconnectReason::Shutdown
526
527        // Decode from payload (after msg_type byte)
528        let decoded = Disconnect::decode(&encoded[1..]).unwrap();
529        assert_eq!(decoded.reason, DisconnectReason::Shutdown);
530    }
531
532    #[test]
533    fn test_disconnect_all_reasons() {
534        let reasons = [
535            DisconnectReason::Shutdown,
536            DisconnectReason::Restart,
537            DisconnectReason::ProtocolError,
538            DisconnectReason::Other,
539        ];
540
541        for reason in reasons {
542            let msg = Disconnect::new(reason);
543            let encoded = msg.encode();
544            let decoded = Disconnect::decode(&encoded[1..]).unwrap();
545            assert_eq!(decoded.reason, reason);
546        }
547    }
548
549    #[test]
550    fn test_disconnect_decode_empty_payload() {
551        let result = Disconnect::decode(&[]);
552        assert!(result.is_err());
553    }
554
555    #[test]
556    fn test_disconnect_decode_unknown_reason() {
557        let decoded = Disconnect::decode(&[0x80]).unwrap();
558        assert_eq!(decoded.reason, DisconnectReason::Other);
559    }
560
561    // ===== SessionDatagram Tests =====
562
563    fn make_node_addr(val: u8) -> NodeAddr {
564        let mut bytes = [0u8; 16];
565        bytes[0] = val;
566        NodeAddr::from_bytes(bytes)
567    }
568
569    #[test]
570    fn test_session_datagram_encode_decode() {
571        let src = make_node_addr(0xAA);
572        let dest = make_node_addr(0xBB);
573        let payload = vec![0x10, 0x00, 0x05, 0x00, 1, 2, 3, 4, 5]; // session payload
574        let dg = SessionDatagram::new(src, dest, payload.clone()).with_ttl(32);
575
576        let encoded = dg.encode();
577        assert_eq!(encoded[0], 0x00); // msg_type (SessionDatagram)
578        assert_eq!(encoded.len(), SESSION_DATAGRAM_HEADER_SIZE + payload.len());
579
580        // Decode (after msg_type)
581        let decoded = SessionDatagram::decode(&encoded[1..]).unwrap();
582        assert_eq!(decoded.src_addr, src);
583        assert_eq!(decoded.dest_addr, dest);
584        assert_eq!(decoded.ttl, 32);
585        assert_eq!(decoded.payload, payload);
586    }
587
588    #[test]
589    fn test_session_datagram_empty_payload() {
590        let dg = SessionDatagram::new(make_node_addr(1), make_node_addr(2), Vec::new());
591
592        let encoded = dg.encode();
593        assert_eq!(encoded.len(), SESSION_DATAGRAM_HEADER_SIZE);
594
595        let decoded = SessionDatagram::decode(&encoded[1..]).unwrap();
596        assert!(decoded.payload.is_empty());
597    }
598
599    #[test]
600    fn test_session_datagram_decode_too_short() {
601        assert!(SessionDatagram::decode(&[]).is_err());
602        assert!(SessionDatagram::decode(&[0x00; 20]).is_err());
603    }
604
605    #[test]
606    fn test_session_datagram_ttl_roundtrip() {
607        for hop in [0u8, 1, 64, 128, 255] {
608            let dg = SessionDatagram::new(make_node_addr(1), make_node_addr(2), vec![0x42])
609                .with_ttl(hop);
610
611            let encoded = dg.encode();
612            let decoded = SessionDatagram::decode(&encoded[1..]).unwrap();
613            assert_eq!(decoded.ttl, hop);
614        }
615    }
616}