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