Skip to main content

rustbgpd_wire/
pmsi.rs

1//! P-Multicast Service Interface (PMSI) Tunnel attribute — RFC 6514 §5.
2//!
3//! This is BGP path attribute type 22 (RFC 6514 §11.1, IANA-managed).
4//! It tells receivers how to forward BUM (broadcast / unknown unicast /
5//! multicast) traffic for the EVI a Type 3 IMET route advertises.
6//!
7//! # Wire format (RFC 6514 §5)
8//!
9//! ```text
10//! +---------------------------------+
11//! |  Flags (1 octet)                |
12//! +---------------------------------+
13//! |  Tunnel Type (1 octet)          |
14//! +---------------------------------+
15//! |  MPLS Label (3 octets)          |
16//! +---------------------------------+
17//! |  Tunnel Identifier (variable)   |
18//! +---------------------------------+
19//! ```
20//!
21//! - **Flags** — RFC 6514 §5 defines bit 0 ("Leaf Information Required")
22//!   only. EVPN ingress replication does not use it.
23//! - **Tunnel Type** — IANA registry, RFC 7385. Values 0–7 are well-
24//!   known; unknown values must round-trip without loss for forward
25//!   compatibility.
26//! - **MPLS Label** — 3 octets. For pure-MPLS deployments the
27//!   high-order 20 bits carry the MPLS label value (RFC 6514 §5).
28//!   For EVPN-VXLAN deployments **the full 24-bit field is the VNI**,
29//!   not `VNI << 4` — RFC 8365 §5.1.3 explicitly redefines the field
30//!   semantics to "the VNI" when EVPN routes ride VXLAN encap. This
31//!   matches `EvpnMacIp.label1` (also a raw 24-bit VNI per RFC 8365)
32//!   and what FRR/Cumulus emit on the wire. A label of 0 still means
33//!   "no label present" in either case.
34//! - **Tunnel Identifier** — variable-length, semantics depend on
35//!   Tunnel Type. For Ingress Replication (type 6) it is the unicast
36//!   tunnel endpoint IP — 4 octets for IPv4, 16 octets for IPv6
37//!   (RFC 6514 §5; ipv6 form per RFC 8365). Other tunnel types carry
38//!   opaque bytes that the codec preserves without interpretation.
39//!
40//! # Why a typed `PmsiTunnelType`
41//!
42//! Validating the tunnel type at decode time catches the most common
43//! interop bug (operator misconfigures FRR with the wrong tunnel type
44//! and the wire becomes nonsensical) without forcing the daemon to
45//! reject otherwise-legal future tunnel types — `PmsiTunnelType::Other`
46//! preserves any unknown value the IANA registry adds later.
47//!
48//! # Gate 7b+1 scope
49//!
50//! Only `PmsiTunnelType::IngressReplication` is exercised by rustbgpd
51//! origination today. Decode handles all variants; encode round-trips
52//! all variants. Phase F (Type 3 IMET) emits Ingress Replication with
53//! the raw 24-bit VNI in the label field (RFC 8365 §5.1.3) and the
54//! local VTEP IP as the tunnel identifier.
55
56use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
57
58use crate::error::DecodeError;
59
60/// PMSI tunnel type (RFC 6514 §11.1, IANA registry RFC 7385).
61#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
62pub enum PmsiTunnelType {
63    /// 0 — no tunnel info present (PE listens on a local set).
64    NoTunnelInfo,
65    /// 1 — RSVP-TE P2MP LSP.
66    RsvpTeP2mp,
67    /// 2 — mLDP P2MP LSP.
68    MldpP2mp,
69    /// 3 — PIM-SSM tree.
70    PimSsm,
71    /// 4 — PIM-SM tree.
72    PimSm,
73    /// 5 — BIDIR-PIM tree.
74    BidirPim,
75    /// 6 — Ingress Replication. Tunnel ID is the unicast endpoint IP.
76    /// EVPN BUM uses this exclusively over VXLAN.
77    IngressReplication,
78    /// 7 — mLDP MP2MP LSP.
79    MldpMp2mp,
80    /// Forward-compat: any value not yet known.
81    Other(u8),
82}
83
84impl PmsiTunnelType {
85    /// Wire encoding (1 octet).
86    #[must_use]
87    pub fn as_u8(self) -> u8 {
88        match self {
89            Self::NoTunnelInfo => 0,
90            Self::RsvpTeP2mp => 1,
91            Self::MldpP2mp => 2,
92            Self::PimSsm => 3,
93            Self::PimSm => 4,
94            Self::BidirPim => 5,
95            Self::IngressReplication => 6,
96            Self::MldpMp2mp => 7,
97            Self::Other(v) => v,
98        }
99    }
100
101    /// Decode from a wire octet.
102    #[must_use]
103    pub fn from_u8(v: u8) -> Self {
104        match v {
105            0 => Self::NoTunnelInfo,
106            1 => Self::RsvpTeP2mp,
107            2 => Self::MldpP2mp,
108            3 => Self::PimSsm,
109            4 => Self::PimSm,
110            5 => Self::BidirPim,
111            6 => Self::IngressReplication,
112            7 => Self::MldpMp2mp,
113            other => Self::Other(other),
114        }
115    }
116}
117
118/// Tunnel Identifier — variable-length, semantics keyed by tunnel type.
119///
120/// For Ingress Replication, this is the originator's unicast endpoint
121/// IP address. Other tunnel types carry opaque bytes.
122#[derive(Debug, Clone, PartialEq, Eq, Hash)]
123pub enum PmsiTunnelIdentifier {
124    /// No identifier (zero-length on the wire).
125    Empty,
126    /// 4-octet IPv4 unicast endpoint.
127    Ipv4(Ipv4Addr),
128    /// 16-octet IPv6 unicast endpoint.
129    Ipv6(Ipv6Addr),
130    /// Anything else — preserved for round-trip without interpretation.
131    Raw(Vec<u8>),
132}
133
134/// Decoded PMSI Tunnel attribute.
135#[derive(Debug, Clone, PartialEq, Eq, Hash)]
136pub struct PmsiTunnel {
137    /// Wire flags (RFC 6514 §5 bit 0 is Leaf Information Required).
138    pub flags: u8,
139    /// Tunnel type (RFC 7385 IANA registry).
140    pub tunnel_type: PmsiTunnelType,
141    /// 24-bit Label field.
142    ///
143    /// Stored in canonical wire form: low 24 bits hold the value.
144    /// **Field semantics depend on encap**:
145    /// - Pure MPLS (RFC 6514 §5): high-order 20 bits = label, low 4 = TC+S.
146    /// - EVPN-VXLAN (RFC 8365 §5.1.3): all 24 bits = VNI, no shift.
147    ///
148    /// For EVPN ingress replication, use
149    /// [`Self::for_evpn_ingress_replication`] which handles the VNI
150    /// width check and emits the field as a raw 24-bit VNI.
151    pub mpls_label: u32,
152    /// Tunnel Identifier — variable-length.
153    pub tunnel_identifier: PmsiTunnelIdentifier,
154}
155
156impl PmsiTunnel {
157    /// Build a PMSI Tunnel attribute for EVPN ingress replication over
158    /// VXLAN (RFC 6514 §5 + RFC 8365 §5.1.3).
159    ///
160    /// The label field carries the **full 24-bit VNI** unmodified —
161    /// RFC 8365 §5.1.3 redefines the field semantics for EVPN-VXLAN
162    /// (no MPLS-style high-20-bits shift). This matches
163    /// `EvpnMacIp.label1` for Type 2 routes and what FRR/Cumulus emit.
164    ///
165    /// `vni` is masked to 24 bits to defend against callers that pass
166    /// a value outside `EvpnInstanceId`'s valid range; in normal
167    /// operation `EvpnInstanceId::new` already rejects VNI > 0xFFFFFF
168    /// at config time, so the mask is purely belt-and-braces.
169    #[must_use]
170    pub fn for_evpn_ingress_replication(vni: u32, originator: IpAddr) -> Self {
171        let tunnel_identifier = match originator {
172            IpAddr::V4(v4) => PmsiTunnelIdentifier::Ipv4(v4),
173            IpAddr::V6(v6) => PmsiTunnelIdentifier::Ipv6(v6),
174        };
175        Self {
176            flags: 0,
177            tunnel_type: PmsiTunnelType::IngressReplication,
178            mpls_label: vni & 0x00FF_FFFF,
179            tunnel_identifier,
180        }
181    }
182
183    /// Encode into a wire-format byte buffer.
184    pub fn encode(&self, buf: &mut Vec<u8>) {
185        buf.push(self.flags);
186        buf.push(self.tunnel_type.as_u8());
187        // 3-octet MPLS Label, big-endian (use the low 24 bits).
188        let label = self.mpls_label & 0x00FF_FFFF;
189        buf.push(((label >> 16) & 0xff) as u8);
190        buf.push(((label >> 8) & 0xff) as u8);
191        buf.push((label & 0xff) as u8);
192        match &self.tunnel_identifier {
193            PmsiTunnelIdentifier::Empty => {}
194            PmsiTunnelIdentifier::Ipv4(v4) => buf.extend_from_slice(&v4.octets()),
195            PmsiTunnelIdentifier::Ipv6(v6) => buf.extend_from_slice(&v6.octets()),
196            PmsiTunnelIdentifier::Raw(bytes) => buf.extend_from_slice(bytes),
197        }
198    }
199
200    /// Decode from a wire-format byte slice.
201    ///
202    /// The slice is the attribute *value* — caller has already stripped
203    /// flags, type code, and length.
204    ///
205    /// Tunnel Identifier interpretation:
206    /// - Tunnel Type 6 (Ingress Replication) with 4-octet rest → IPv4.
207    /// - Tunnel Type 6 with 16-octet rest → IPv6.
208    /// - Anything else (including type 6 with non-4/16 rest) → `Raw`.
209    ///
210    /// # Errors
211    ///
212    /// Returns [`DecodeError::MalformedField`] when the value is shorter
213    /// than the 5-octet header (flags + type + 3-octet label).
214    pub fn decode(value: &[u8]) -> Result<Self, DecodeError> {
215        if value.len() < 5 {
216            return Err(DecodeError::MalformedField {
217                message_type: "UPDATE",
218                detail: format!(
219                    "PMSI Tunnel attribute truncated: need ≥5 bytes (flags+type+label), got {}",
220                    value.len()
221                ),
222            });
223        }
224        let flags = value[0];
225        let tunnel_type = PmsiTunnelType::from_u8(value[1]);
226        let label = (u32::from(value[2]) << 16) | (u32::from(value[3]) << 8) | u32::from(value[4]);
227        let rest = &value[5..];
228
229        let tunnel_identifier = match (tunnel_type, rest.len()) {
230            (_, 0) => PmsiTunnelIdentifier::Empty,
231            (PmsiTunnelType::IngressReplication, 4) => {
232                let mut o = [0u8; 4];
233                o.copy_from_slice(rest);
234                PmsiTunnelIdentifier::Ipv4(Ipv4Addr::from(o))
235            }
236            (PmsiTunnelType::IngressReplication, 16) => {
237                let mut o = [0u8; 16];
238                o.copy_from_slice(rest);
239                PmsiTunnelIdentifier::Ipv6(Ipv6Addr::from(o))
240            }
241            _ => PmsiTunnelIdentifier::Raw(rest.to_vec()),
242        };
243
244        Ok(Self {
245            flags,
246            tunnel_type,
247            mpls_label: label,
248            tunnel_identifier,
249        })
250    }
251}
252
253#[cfg(test)]
254mod tests {
255    use super::*;
256
257    fn roundtrip(t: &PmsiTunnel) {
258        let mut buf = Vec::new();
259        t.encode(&mut buf);
260        let decoded = PmsiTunnel::decode(&buf).expect("decode");
261        assert_eq!(&decoded, t);
262    }
263
264    #[test]
265    fn ingress_replication_ipv4_roundtrip() {
266        let t = PmsiTunnel::for_evpn_ingress_replication(100, "10.0.0.1".parse().unwrap());
267        roundtrip(&t);
268        // RFC 8365 §5.1.3: EVPN-VXLAN PMSI label is the raw 24-bit VNI,
269        // no MPLS-style high-20-bits shift.
270        assert_eq!(t.mpls_label, 100);
271        assert_eq!(t.tunnel_type, PmsiTunnelType::IngressReplication);
272        assert_eq!(
273            t.tunnel_identifier,
274            PmsiTunnelIdentifier::Ipv4(Ipv4Addr::new(10, 0, 0, 1))
275        );
276    }
277
278    #[test]
279    fn ingress_replication_ipv6_roundtrip() {
280        let t = PmsiTunnel::for_evpn_ingress_replication(50, "2001:db8::1".parse().unwrap());
281        roundtrip(&t);
282        assert_eq!(t.mpls_label, 50);
283    }
284
285    #[test]
286    fn ingress_replication_ipv4_wire_bytes_match_rfc_8365() {
287        // RFC 6514 §5 wire layout: flags(1) | type(1) | label(3) |
288        // tunnel id(variable). For EVPN ingress replication of
289        // vni=100, RFC 8365 §5.1.3 says the label field is the raw
290        // 24-bit VNI: 100 = 0x000064. This matches FRR/Cumulus on the
291        // wire and stays consistent with `EvpnMacIp.label1` (also a
292        // raw 24-bit VNI per RFC 8365).
293        let t = PmsiTunnel::for_evpn_ingress_replication(100, "10.0.0.1".parse().unwrap());
294        let mut buf = Vec::new();
295        t.encode(&mut buf);
296        assert_eq!(
297            buf,
298            vec![
299                0x00, // flags
300                0x06, // tunnel type = Ingress Replication
301                0x00, 0x00, 0x64, // label = vni 100 (raw, no shift)
302                10, 0, 0, 1, // IPv4 originator
303            ]
304        );
305    }
306
307    #[test]
308    fn no_tunnel_info_with_no_identifier_roundtrip() {
309        let t = PmsiTunnel {
310            flags: 0,
311            tunnel_type: PmsiTunnelType::NoTunnelInfo,
312            mpls_label: 0,
313            tunnel_identifier: PmsiTunnelIdentifier::Empty,
314        };
315        roundtrip(&t);
316    }
317
318    #[test]
319    fn rsvp_te_p2mp_with_opaque_id_roundtrip() {
320        let t = PmsiTunnel {
321            flags: 0,
322            tunnel_type: PmsiTunnelType::RsvpTeP2mp,
323            mpls_label: 0x1234 << 4,
324            tunnel_identifier: PmsiTunnelIdentifier::Raw(vec![1, 2, 3, 4, 5, 6, 7, 8]),
325        };
326        roundtrip(&t);
327    }
328
329    #[test]
330    fn mldp_p2mp_roundtrip() {
331        let t = PmsiTunnel {
332            flags: 0,
333            tunnel_type: PmsiTunnelType::MldpP2mp,
334            mpls_label: 42 << 4,
335            tunnel_identifier: PmsiTunnelIdentifier::Raw(vec![0xaa; 12]),
336        };
337        roundtrip(&t);
338    }
339
340    #[test]
341    fn pim_ssm_roundtrip() {
342        let t = PmsiTunnel {
343            flags: 0,
344            tunnel_type: PmsiTunnelType::PimSsm,
345            mpls_label: 0,
346            tunnel_identifier: PmsiTunnelIdentifier::Raw(vec![10, 0, 0, 1, 224, 0, 0, 1]),
347        };
348        roundtrip(&t);
349    }
350
351    #[test]
352    fn pim_sm_roundtrip() {
353        let t = PmsiTunnel {
354            flags: 0,
355            tunnel_type: PmsiTunnelType::PimSm,
356            mpls_label: 0,
357            tunnel_identifier: PmsiTunnelIdentifier::Raw(vec![0xff; 8]),
358        };
359        roundtrip(&t);
360    }
361
362    #[test]
363    fn bidir_pim_roundtrip() {
364        let t = PmsiTunnel {
365            flags: 0,
366            tunnel_type: PmsiTunnelType::BidirPim,
367            mpls_label: 0,
368            tunnel_identifier: PmsiTunnelIdentifier::Raw(vec![1, 2, 3]),
369        };
370        roundtrip(&t);
371    }
372
373    #[test]
374    fn mldp_mp2mp_roundtrip() {
375        // Note: zero-length Raw collapses to Empty on decode (the
376        // wire byte stream is identical, so we cannot distinguish).
377        // Use Empty here to make the round-trip equality precise.
378        let t = PmsiTunnel {
379            flags: 0,
380            tunnel_type: PmsiTunnelType::MldpMp2mp,
381            mpls_label: 0,
382            tunnel_identifier: PmsiTunnelIdentifier::Empty,
383        };
384        roundtrip(&t);
385    }
386
387    #[test]
388    fn unknown_tunnel_type_round_trips_without_loss() {
389        let t = PmsiTunnel {
390            flags: 0x01, // pretend Leaf Information Required is set
391            tunnel_type: PmsiTunnelType::Other(99),
392            mpls_label: 0x00ab_cdef,
393            tunnel_identifier: PmsiTunnelIdentifier::Raw(vec![0xde, 0xad, 0xbe, 0xef]),
394        };
395        roundtrip(&t);
396    }
397
398    #[test]
399    fn decode_rejects_truncated_value() {
400        let buf = [0u8, 6u8, 0u8, 0u8]; // 4 bytes — needs ≥5
401        let err = PmsiTunnel::decode(&buf).unwrap_err();
402        assert!(matches!(err, DecodeError::MalformedField { .. }));
403    }
404
405    #[test]
406    fn decode_zero_length_tunnel_id_after_label_yields_empty() {
407        let buf = [0u8, 1u8, 0u8, 0u8, 0x10]; // RSVP-TE P2MP, label=1, no ID
408        let t = PmsiTunnel::decode(&buf).unwrap();
409        assert_eq!(t.tunnel_identifier, PmsiTunnelIdentifier::Empty);
410    }
411
412    #[test]
413    fn ingress_replication_with_8_byte_id_treated_as_raw() {
414        // Tunnel type 6 but identifier neither 4 nor 16 octets: keep as Raw
415        // for forward-compat (an extension might define a longer form).
416        let buf = [
417            0u8, 6u8, // type = Ingress Replication
418            0u8, 0u8, 0u8, // label = 0
419            1, 2, 3, 4, 5, 6, 7, 8, // 8-byte tunnel identifier
420        ];
421        let t = PmsiTunnel::decode(&buf).unwrap();
422        assert!(matches!(t.tunnel_identifier, PmsiTunnelIdentifier::Raw(_)));
423    }
424
425    #[test]
426    fn for_evpn_ingress_replication_masks_vni_at_24_bits() {
427        // `EvpnInstanceId::new` rejects VNI > 0xFFFFFF at config time,
428        // so this defensive mask is purely belt-and-braces. RFC 8365
429        // §5.1.3 says the field IS the VNI directly (no shift), so a
430        // 24-bit-bounded raw value is the correct on-wire form.
431        let t = PmsiTunnel::for_evpn_ingress_replication(0xFF00_1234, "10.0.0.1".parse().unwrap());
432        assert_eq!(t.mpls_label, 0x0000_1234);
433    }
434}