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}