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