Skip to main content

rustbgpd_wire/
evpn.rs

1//! RFC 7432 EVPN NLRI codec and types.
2//!
3//! EVPN NLRI is a typed TLV format carried under AFI=25 / SAFI=70. Each NLRI
4//! entry has a 1-byte route type, 1-byte length, and a type-specific payload.
5//! Five route types are defined:
6//!
7//! - Type 1: Ethernet Auto-Discovery (EAD) — per-ES (ethernet_tag=MAX_ET) or per-EVI
8//! - Type 2: MAC/IP Advertisement
9//! - Type 3: Inclusive Multicast Ethernet Tag (IMET)
10//! - Type 4: Ethernet Segment (ES)
11//! - Type 5: IP Prefix Route (RFC 9136)
12//!
13//! This module is the structural codec — no semantic interpretation of RDs,
14//! ESIs, MACs, or labels. Upstream layers (RIB, best-path) apply meaning.
15//!
16//! The module intentionally splits route payload from route identity:
17//! [`EvpnRoute`] carries the full wire payload (needed for encoding and
18//! reflection), while [`EvpnRouteKey`] carries only the RFC 7432 identifying
19//! fields per route type (used as a HashMap key in the RIB).
20
21use std::fmt;
22use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
23
24use crate::error::DecodeError;
25use crate::nlri::{Ipv4Prefix, Ipv6Prefix};
26
27// ---------------------------------------------------------------------------
28// Core primitive types
29// ---------------------------------------------------------------------------
30
31/// Ethernet Tag ID — 32-bit namespace identifier within an EVI (RFC 7432 §7.1).
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct EthernetTagId(pub u32);
34
35impl EthernetTagId {
36    /// `MAX_ET` (RFC 7432 §7.1) — Ethernet Tag 0xFFFFFFFF marks EAD-per-ES routes.
37    pub const MAX_ET: Self = Self(0xFFFF_FFFF);
38
39    /// Returns `true` if this tag equals `MAX_ET` (EAD per-ES discriminator).
40    #[must_use]
41    pub fn is_max_et(&self) -> bool {
42        self.0 == Self::MAX_ET.0
43    }
44}
45
46impl fmt::Display for EthernetTagId {
47    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48        if self.is_max_et() {
49            write!(f, "MAX_ET")
50        } else {
51            write!(f, "{}", self.0)
52        }
53    }
54}
55
56/// 48-bit Ethernet MAC address.
57#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
58pub struct MacAddress(pub [u8; 6]);
59
60impl MacAddress {
61    /// Construct from a raw 6-byte array.
62    #[must_use]
63    pub const fn new(bytes: [u8; 6]) -> Self {
64        Self(bytes)
65    }
66
67    /// The underlying 6 bytes.
68    #[must_use]
69    pub const fn octets(&self) -> [u8; 6] {
70        self.0
71    }
72}
73
74impl fmt::Display for MacAddress {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        let o = &self.0;
77        write!(
78            f,
79            "{:02x}:{:02x}:{:02x}:{:02x}:{:02x}:{:02x}",
80            o[0], o[1], o[2], o[3], o[4], o[5]
81        )
82    }
83}
84
85/// 10-byte Ethernet Segment Identifier (RFC 7432 §5).
86///
87/// ESI type is encoded in the first byte; the remaining 9 bytes carry the
88/// type-specific value. The codec stores ESIs opaquely — consumers that
89/// need to interpret the ESI type (single-active, LACP-derived, etc.) must
90/// do so on the raw bytes.
91#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
92pub struct EthernetSegmentIdentifier(pub [u8; 10]);
93
94impl EthernetSegmentIdentifier {
95    /// ESI Type 0 (all zero) — used when a CE is single-homed.
96    pub const ZERO: Self = Self([0u8; 10]);
97
98    /// Construct from a raw 10-byte array.
99    #[must_use]
100    pub const fn new(bytes: [u8; 10]) -> Self {
101        Self(bytes)
102    }
103
104    /// The underlying 10 bytes.
105    #[must_use]
106    pub const fn octets(&self) -> [u8; 10] {
107        self.0
108    }
109
110    /// ESI Type (first byte per RFC 7432 §5).
111    #[must_use]
112    pub const fn esi_type(&self) -> u8 {
113        self.0[0]
114    }
115
116    /// Returns `true` if all 10 bytes are zero (ESI Type 0, single-homed).
117    #[must_use]
118    pub fn is_zero(&self) -> bool {
119        self.0.iter().all(|&b| b == 0)
120    }
121}
122
123impl fmt::Display for EthernetSegmentIdentifier {
124    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
125        for (i, byte) in self.0.iter().enumerate() {
126            if i > 0 {
127                write!(f, ":")?;
128            }
129            write!(f, "{byte:02x}")?;
130        }
131        Ok(())
132    }
133}
134
135/// Route Distinguisher (RFC 4364 §4.2) — 8-byte administratively-assigned
136/// identifier that makes VPN routes unique across EVIs.
137///
138/// RFC 4364 defines three encodings, distinguished by the first 2 bytes
139/// (Type field). The codec preserves the raw 8 bytes and exposes typed
140/// decode helpers; it never rejects unknown RD types.
141#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
142pub struct RouteDistinguisher(pub [u8; 8]);
143
144impl RouteDistinguisher {
145    /// All-zero RD, used where a valid RD is required but none is meaningful.
146    pub const ZERO: Self = Self([0u8; 8]);
147
148    /// Construct from a raw 8-byte array.
149    #[must_use]
150    pub const fn new(bytes: [u8; 8]) -> Self {
151        Self(bytes)
152    }
153
154    /// The underlying 8 bytes.
155    #[must_use]
156    pub const fn octets(&self) -> [u8; 8] {
157        self.0
158    }
159
160    /// RD type (first 2 bytes, big-endian).
161    #[must_use]
162    pub fn rd_type(&self) -> u16 {
163        u16::from_be_bytes([self.0[0], self.0[1]])
164    }
165}
166
167impl fmt::Display for RouteDistinguisher {
168    /// Format per RFC 4364 §4.2:
169    /// - Type 0: `<admin-asn-16>:<assigned-32>`
170    /// - Type 1: `<admin-ipv4>:<assigned-16>`
171    /// - Type 2: `<admin-asn-32>:<assigned-16>`
172    /// - Other: hex-encoded fallback.
173    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
174        let b = &self.0;
175        match self.rd_type() {
176            0 => {
177                let asn = u16::from_be_bytes([b[2], b[3]]);
178                let assigned = u32::from_be_bytes([b[4], b[5], b[6], b[7]]);
179                write!(f, "{asn}:{assigned}")
180            }
181            1 => {
182                let ip = Ipv4Addr::new(b[2], b[3], b[4], b[5]);
183                let assigned = u16::from_be_bytes([b[6], b[7]]);
184                write!(f, "{ip}:{assigned}")
185            }
186            2 => {
187                let asn = u32::from_be_bytes([b[2], b[3], b[4], b[5]]);
188                let assigned = u16::from_be_bytes([b[6], b[7]]);
189                write!(f, "{asn}:{assigned}")
190            }
191            _ => {
192                write!(f, "0x")?;
193                for byte in b {
194                    write!(f, "{byte:02x}")?;
195                }
196                Ok(())
197            }
198        }
199    }
200}
201
202/// Errors returned by `RouteDistinguisher::from_str` when a textual RD
203/// fails to parse against the RFC 4364 §4.2 encodings.
204#[derive(Debug, Clone, PartialEq, Eq)]
205pub enum RouteDistinguisherParseError {
206    /// Input did not contain exactly one `':'` separating administrator and
207    /// assigned-number fields.
208    MissingColon,
209    /// The administrator field could not be parsed as either an ASN
210    /// (`u32`) or an IPv4 address.
211    InvalidAdministrator(String),
212    /// The assigned-number field could not be parsed as a non-negative
213    /// integer in the range required for the inferred RD type.
214    InvalidAssignedNumber(String),
215    /// The assigned-number value exceeded the per-RD-type maximum
216    /// (`u32::MAX` for type 0, `u16::MAX` for types 1 and 2).
217    AssignedNumberOutOfRange { value: u64, max: u64 },
218}
219
220impl fmt::Display for RouteDistinguisherParseError {
221    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
222        match self {
223            Self::MissingColon => write!(
224                f,
225                "expected RD in form `<asn|ipv4>:<assigned>`, missing `:`"
226            ),
227            Self::InvalidAdministrator(s) => write!(
228                f,
229                "invalid RD administrator {s:?}: expected ASN (u32) or IPv4 address"
230            ),
231            Self::InvalidAssignedNumber(s) => {
232                write!(
233                    f,
234                    "invalid RD assigned number {s:?}: expected non-negative integer"
235                )
236            }
237            Self::AssignedNumberOutOfRange { value, max } => write!(
238                f,
239                "RD assigned number {value} exceeds maximum {max} for the inferred RD type"
240            ),
241        }
242    }
243}
244
245impl std::error::Error for RouteDistinguisherParseError {}
246
247impl std::str::FromStr for RouteDistinguisher {
248    type Err = RouteDistinguisherParseError;
249
250    /// Parse the textual RD encodings from RFC 4364 §4.2.
251    ///
252    /// - `<u16-asn>:<u32-assigned>`  → Type 0
253    /// - `<ipv4>:<u16-assigned>`     → Type 1
254    /// - `<u32-asn>:<u16-assigned>`  → Type 2 (when ASN > 65535)
255    ///
256    /// Disambiguation between Type 0 and Type 2 follows the convention
257    /// adopted by FRR / Cisco / Junos: an `asn:value` form with
258    /// `asn ≤ 65535` is Type 0; otherwise Type 2.
259    fn from_str(s: &str) -> Result<Self, Self::Err> {
260        let (admin, assigned) = s
261            .split_once(':')
262            .ok_or(RouteDistinguisherParseError::MissingColon)?;
263        let assigned_u64 = parse_unsigned(assigned)?;
264
265        if let Ok(ipv4) = admin.parse::<Ipv4Addr>() {
266            // Type 1: IPv4 admin + 2-byte assigned.
267            let assigned_u16 = u16::try_from(assigned_u64).map_err(|_| {
268                RouteDistinguisherParseError::AssignedNumberOutOfRange {
269                    value: assigned_u64,
270                    max: u64::from(u16::MAX),
271                }
272            })?;
273            let mut bytes = [0u8; 8];
274            bytes[0..2].copy_from_slice(&1u16.to_be_bytes());
275            bytes[2..6].copy_from_slice(&ipv4.octets());
276            bytes[6..8].copy_from_slice(&assigned_u16.to_be_bytes());
277            return Ok(Self(bytes));
278        }
279
280        let asn = admin
281            .parse::<u32>()
282            .map_err(|_| RouteDistinguisherParseError::InvalidAdministrator(admin.to_string()))?;
283
284        if let Ok(asn_u16) = u16::try_from(asn) {
285            // Type 0: 2-byte ASN + 4-byte assigned.
286            let assigned_u32 = u32::try_from(assigned_u64).map_err(|_| {
287                RouteDistinguisherParseError::AssignedNumberOutOfRange {
288                    value: assigned_u64,
289                    max: u64::from(u32::MAX),
290                }
291            })?;
292            let mut bytes = [0u8; 8];
293            bytes[0..2].copy_from_slice(&0u16.to_be_bytes());
294            bytes[2..4].copy_from_slice(&asn_u16.to_be_bytes());
295            bytes[4..8].copy_from_slice(&assigned_u32.to_be_bytes());
296            Ok(Self(bytes))
297        } else {
298            // Type 2: 4-byte ASN + 2-byte assigned.
299            let assigned_u16 = u16::try_from(assigned_u64).map_err(|_| {
300                RouteDistinguisherParseError::AssignedNumberOutOfRange {
301                    value: assigned_u64,
302                    max: u64::from(u16::MAX),
303                }
304            })?;
305            let mut bytes = [0u8; 8];
306            bytes[0..2].copy_from_slice(&2u16.to_be_bytes());
307            bytes[2..6].copy_from_slice(&asn.to_be_bytes());
308            bytes[6..8].copy_from_slice(&assigned_u16.to_be_bytes());
309            Ok(Self(bytes))
310        }
311    }
312}
313
314fn parse_unsigned(s: &str) -> Result<u64, RouteDistinguisherParseError> {
315    s.parse::<u64>()
316        .map_err(|_| RouteDistinguisherParseError::InvalidAssignedNumber(s.to_string()))
317}
318
319/// A 3-byte MPLS label field (RFC 3032) as carried in EVPN NLRI.
320///
321/// For VXLAN-encapsulated EVPN (RFC 8365), the 24-bit label field carries
322/// a 24-bit VNI. The codec does not distinguish — consumers interpret the
323/// value based on the Encapsulation extended community.
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
325pub struct MplsLabel(pub u32);
326
327impl MplsLabel {
328    /// Construct from a raw 24-bit value (upper 8 bits ignored).
329    #[must_use]
330    pub const fn new(value: u32) -> Self {
331        Self(value & 0x00FF_FFFF)
332    }
333
334    /// Raw 24-bit value.
335    #[must_use]
336    pub const fn value(&self) -> u32 {
337        self.0
338    }
339
340    /// Interpret this field as a VXLAN VNI (RFC 8365 §5).
341    ///
342    /// The label field on the wire is `(label << 4) | (TC << 1) | S` when
343    /// used for MPLS, but for VXLAN the full 24 bits are the VNI. Both
344    /// uses call this accessor; it's up to the caller to know the encap.
345    #[must_use]
346    pub const fn as_vni(&self) -> u32 {
347        self.0
348    }
349
350    /// Extract the 20-bit MPLS label field (upper 20 bits of the 24-bit word).
351    #[must_use]
352    pub const fn as_mpls_label(&self) -> u32 {
353        self.0 >> 4
354    }
355}
356
357/// An IP prefix encoded inside an EVPN Type 5 NLRI — IPv4 or IPv6.
358#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
359pub enum EvpnIpPrefixValue {
360    /// IPv4 prefix.
361    V4(Ipv4Prefix),
362    /// IPv6 prefix.
363    V6(Ipv6Prefix),
364}
365
366impl fmt::Display for EvpnIpPrefixValue {
367    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
368        match self {
369            Self::V4(p) => write!(f, "{}/{}", p.addr, p.len),
370            Self::V6(p) => write!(f, "{}/{}", p.addr, p.len),
371        }
372    }
373}
374
375// ---------------------------------------------------------------------------
376// Per-route-type payload structs
377// ---------------------------------------------------------------------------
378
379/// Type 1: Ethernet Auto-Discovery per-ES route (RFC 7432 §7.1).
380///
381/// Ethernet Tag field MUST be `MAX_ET`; discriminated from per-EVI by value.
382#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
383pub struct EvpnEadPerEs {
384    /// Route Distinguisher.
385    pub rd: RouteDistinguisher,
386    /// Ethernet Segment Identifier (must be non-zero).
387    pub esi: EthernetSegmentIdentifier,
388    /// Ethernet Tag (must be `MAX_ET` for per-ES).
389    pub ethernet_tag: EthernetTagId,
390    /// MPLS label — typically the ESI label for per-ES.
391    pub label: MplsLabel,
392}
393
394/// Type 1: Ethernet Auto-Discovery per-EVI route (RFC 7432 §7.1).
395///
396/// Same wire shape as per-ES but Ethernet Tag is a real EVI/bridge-domain
397/// identifier rather than `MAX_ET`.
398#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
399pub struct EvpnEadPerEvi {
400    /// Route Distinguisher.
401    pub rd: RouteDistinguisher,
402    /// Ethernet Segment Identifier (must be non-zero).
403    pub esi: EthernetSegmentIdentifier,
404    /// Ethernet Tag identifying the EVI / bridge domain.
405    pub ethernet_tag: EthernetTagId,
406    /// MPLS label / VNI for this EVI on this ES.
407    pub label: MplsLabel,
408}
409
410/// Type 2: MAC/IP Advertisement route (RFC 7432 §7.2).
411#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
412pub struct EvpnMacIp {
413    /// Route Distinguisher.
414    pub rd: RouteDistinguisher,
415    /// Ethernet Segment Identifier (may be ZERO for single-homed CE).
416    pub esi: EthernetSegmentIdentifier,
417    /// Ethernet Tag identifying the EVI / bridge domain.
418    pub ethernet_tag: EthernetTagId,
419    /// MAC address being advertised.
420    pub mac: MacAddress,
421    /// Host IP address, if any. Wire length is 0, 4, or 16 bytes.
422    pub ip: Option<IpAddr>,
423    /// Primary MPLS label / VNI (present on all Type 2 routes).
424    pub label1: MplsLabel,
425    /// Secondary label for symmetric IRB (RFC 9135), if present.
426    pub label2: Option<MplsLabel>,
427}
428
429/// Type 3: Inclusive Multicast Ethernet Tag route (RFC 7432 §7.3).
430#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
431pub struct EvpnImet {
432    /// Route Distinguisher.
433    pub rd: RouteDistinguisher,
434    /// Ethernet Tag identifying the EVI / bridge domain.
435    pub ethernet_tag: EthernetTagId,
436    /// Originator Router IP. Wire length is 4 or 16 bytes.
437    pub originator_ip: IpAddr,
438}
439
440/// Type 4: Ethernet Segment route (RFC 7432 §7.4).
441#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
442pub struct EvpnEs {
443    /// Route Distinguisher.
444    pub rd: RouteDistinguisher,
445    /// Ethernet Segment Identifier (must be non-zero).
446    pub esi: EthernetSegmentIdentifier,
447    /// Originator Router IP. Wire length is 4 or 16 bytes.
448    pub originator_ip: IpAddr,
449}
450
451/// Type 5: IP Prefix route (RFC 9136).
452#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
453pub struct EvpnIpPrefixRoute {
454    /// Route Distinguisher.
455    pub rd: RouteDistinguisher,
456    /// Ethernet Segment Identifier (may be ZERO).
457    pub esi: EthernetSegmentIdentifier,
458    /// Ethernet Tag (often 0 for Type 5).
459    pub ethernet_tag: EthernetTagId,
460    /// IP prefix being advertised (IPv4 or IPv6).
461    pub prefix: EvpnIpPrefixValue,
462    /// Gateway IP address. Same family as `prefix`. May be 0.0.0.0 / ::.
463    pub gateway: IpAddr,
464    /// MPLS label / L3 VNI.
465    pub label: MplsLabel,
466}
467
468// ---------------------------------------------------------------------------
469// EvpnRoute + EvpnRouteKey top-level enums
470// ---------------------------------------------------------------------------
471
472/// A single EVPN NLRI entry (RFC 7432 §7), one of five route types.
473///
474/// This carries the full wire payload — needed for round-trip encoding and
475/// for reflection through a route reflector. For a minimal hashable
476/// identifier suitable as a RIB key, see [`EvpnRouteKey`].
477#[derive(Debug, Clone, PartialEq, Eq, Hash)]
478pub enum EvpnRoute {
479    /// Type 1 — EAD per-ES.
480    EadPerEs(EvpnEadPerEs),
481    /// Type 1 — EAD per-EVI.
482    EadPerEvi(EvpnEadPerEvi),
483    /// Type 2 — MAC/IP Advertisement.
484    MacIp(EvpnMacIp),
485    /// Type 3 — Inclusive Multicast Ethernet Tag.
486    Imet(EvpnImet),
487    /// Type 4 — Ethernet Segment.
488    Es(EvpnEs),
489    /// Type 5 — IP Prefix (RFC 9136).
490    IpPrefix(EvpnIpPrefixRoute),
491}
492
493impl EvpnRoute {
494    /// Wire route-type byte (1..=5).
495    #[must_use]
496    pub const fn route_type(&self) -> u8 {
497        match self {
498            Self::EadPerEs(_) | Self::EadPerEvi(_) => 1,
499            Self::MacIp(_) => 2,
500            Self::Imet(_) => 3,
501            Self::Es(_) => 4,
502            Self::IpPrefix(_) => 5,
503        }
504    }
505
506    /// Identifying subset of the route, suitable as a RIB key.
507    #[must_use]
508    pub fn key(&self) -> EvpnRouteKey {
509        match self {
510            Self::EadPerEs(r) => EvpnRouteKey::EadPerEs {
511                rd: r.rd,
512                esi: r.esi,
513                ethernet_tag: r.ethernet_tag,
514            },
515            Self::EadPerEvi(r) => EvpnRouteKey::EadPerEvi {
516                rd: r.rd,
517                esi: r.esi,
518                ethernet_tag: r.ethernet_tag,
519            },
520            Self::MacIp(r) => EvpnRouteKey::MacIp {
521                rd: r.rd,
522                ethernet_tag: r.ethernet_tag,
523                mac: r.mac,
524                ip: r.ip,
525            },
526            Self::Imet(r) => EvpnRouteKey::Imet {
527                rd: r.rd,
528                ethernet_tag: r.ethernet_tag,
529                originator_ip: r.originator_ip,
530            },
531            Self::Es(r) => EvpnRouteKey::Es {
532                rd: r.rd,
533                esi: r.esi,
534                originator_ip: r.originator_ip,
535            },
536            Self::IpPrefix(r) => EvpnRouteKey::IpPrefix {
537                rd: r.rd,
538                ethernet_tag: r.ethernet_tag,
539                prefix: r.prefix,
540            },
541        }
542    }
543}
544
545/// Identifying subset of an EVPN route — the fields that make two routes
546/// distinct per RFC 7432. Suitable as a `HashMap` key in the RIB.
547///
548/// EAD per-ES and EAD per-EVI share a wire format but get distinct variants
549/// here so the RIB never accidentally collapses them.
550#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
551pub enum EvpnRouteKey {
552    /// Type 1 per-ES key.
553    EadPerEs {
554        /// Route Distinguisher.
555        rd: RouteDistinguisher,
556        /// Ethernet Segment Identifier.
557        esi: EthernetSegmentIdentifier,
558        /// Ethernet Tag (`MAX_ET` for per-ES).
559        ethernet_tag: EthernetTagId,
560    },
561    /// Type 1 per-EVI key.
562    EadPerEvi {
563        /// Route Distinguisher.
564        rd: RouteDistinguisher,
565        /// Ethernet Segment Identifier.
566        esi: EthernetSegmentIdentifier,
567        /// Ethernet Tag identifying the EVI.
568        ethernet_tag: EthernetTagId,
569    },
570    /// Type 2 key — RD, tag, MAC, optional IP.
571    MacIp {
572        /// Route Distinguisher.
573        rd: RouteDistinguisher,
574        /// Ethernet Tag identifying the EVI.
575        ethernet_tag: EthernetTagId,
576        /// MAC address.
577        mac: MacAddress,
578        /// Optional host IP (0, 32, or 128 bits on the wire).
579        ip: Option<IpAddr>,
580    },
581    /// Type 3 key.
582    Imet {
583        /// Route Distinguisher.
584        rd: RouteDistinguisher,
585        /// Ethernet Tag identifying the EVI.
586        ethernet_tag: EthernetTagId,
587        /// Originator Router IP.
588        originator_ip: IpAddr,
589    },
590    /// Type 4 key.
591    Es {
592        /// Route Distinguisher.
593        rd: RouteDistinguisher,
594        /// Ethernet Segment Identifier.
595        esi: EthernetSegmentIdentifier,
596        /// Originator Router IP.
597        originator_ip: IpAddr,
598    },
599    /// Type 5 key.
600    IpPrefix {
601        /// Route Distinguisher.
602        rd: RouteDistinguisher,
603        /// Ethernet Tag.
604        ethernet_tag: EthernetTagId,
605        /// IP prefix.
606        prefix: EvpnIpPrefixValue,
607    },
608}
609
610impl EvpnRouteKey {
611    /// Wire route-type byte (1..=5).
612    #[must_use]
613    pub const fn route_type(&self) -> u8 {
614        match self {
615            Self::EadPerEs { .. } | Self::EadPerEvi { .. } => 1,
616            Self::MacIp { .. } => 2,
617            Self::Imet { .. } => 3,
618            Self::Es { .. } => 4,
619            Self::IpPrefix { .. } => 5,
620        }
621    }
622}
623
624// ---------------------------------------------------------------------------
625// Decode helpers
626// ---------------------------------------------------------------------------
627
628fn decode_rd(buf: &[u8]) -> Result<RouteDistinguisher, DecodeError> {
629    if buf.len() < 8 {
630        return Err(DecodeError::MalformedField {
631            message_type: "UPDATE",
632            detail: "EVPN NLRI truncated: expected 8-byte Route Distinguisher".to_string(),
633        });
634    }
635    let mut bytes = [0u8; 8];
636    bytes.copy_from_slice(&buf[..8]);
637    Ok(RouteDistinguisher(bytes))
638}
639
640fn decode_esi(buf: &[u8]) -> Result<EthernetSegmentIdentifier, DecodeError> {
641    if buf.len() < 10 {
642        return Err(DecodeError::MalformedField {
643            message_type: "UPDATE",
644            detail: "EVPN NLRI truncated: expected 10-byte ESI".to_string(),
645        });
646    }
647    let mut bytes = [0u8; 10];
648    bytes.copy_from_slice(&buf[..10]);
649    Ok(EthernetSegmentIdentifier(bytes))
650}
651
652fn decode_ethernet_tag(buf: &[u8]) -> Result<EthernetTagId, DecodeError> {
653    if buf.len() < 4 {
654        return Err(DecodeError::MalformedField {
655            message_type: "UPDATE",
656            detail: "EVPN NLRI truncated: expected 4-byte Ethernet Tag".to_string(),
657        });
658    }
659    Ok(EthernetTagId(u32::from_be_bytes([
660        buf[0], buf[1], buf[2], buf[3],
661    ])))
662}
663
664fn decode_mpls_label(buf: &[u8]) -> Result<MplsLabel, DecodeError> {
665    if buf.len() < 3 {
666        return Err(DecodeError::MalformedField {
667            message_type: "UPDATE",
668            detail: "EVPN NLRI truncated: expected 3-byte MPLS label".to_string(),
669        });
670    }
671    // 24-bit value stored in 3 bytes, big-endian.
672    let value = (u32::from(buf[0]) << 16) | (u32::from(buf[1]) << 8) | u32::from(buf[2]);
673    Ok(MplsLabel(value))
674}
675
676fn decode_ip_addr(buf: &[u8], len: usize, field: &str) -> Result<IpAddr, DecodeError> {
677    match len {
678        4 => {
679            if buf.len() < 4 {
680                return Err(DecodeError::MalformedField {
681                    message_type: "UPDATE",
682                    detail: format!("EVPN NLRI truncated: expected 4 bytes for {field}"),
683                });
684            }
685            Ok(IpAddr::V4(Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3])))
686        }
687        16 => {
688            if buf.len() < 16 {
689                return Err(DecodeError::MalformedField {
690                    message_type: "UPDATE",
691                    detail: format!("EVPN NLRI truncated: expected 16 bytes for {field}"),
692                });
693            }
694            let mut octets = [0u8; 16];
695            octets.copy_from_slice(&buf[..16]);
696            Ok(IpAddr::V6(Ipv6Addr::from(octets)))
697        }
698        other => Err(DecodeError::MalformedField {
699            message_type: "UPDATE",
700            detail: format!("EVPN NLRI {field} length {other} (expected 4 or 16)"),
701        }),
702    }
703}
704
705// ---------------------------------------------------------------------------
706// Per-route-type decode
707// ---------------------------------------------------------------------------
708
709fn decode_type1(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
710    // RD (8) | ESI (10) | Ethernet Tag (4) | MPLS Label (3) = 25 bytes
711    if payload.len() != 25 {
712        return Err(DecodeError::MalformedField {
713            message_type: "UPDATE",
714            detail: format!("EVPN Type 1 payload length {} (expected 25)", payload.len()),
715        });
716    }
717    let rd = decode_rd(&payload[0..8])?;
718    let esi = decode_esi(&payload[8..18])?;
719    // RFC 7432 §7.1: EAD routes (per-ES and per-EVI) carry a non-zero ESI
720    // identifying the Ethernet Segment. Reject ESI=0 at the wire boundary.
721    if esi.is_zero() {
722        return Err(DecodeError::MalformedField {
723            message_type: "UPDATE",
724            detail: "EVPN Type 1 EAD route with all-zero ESI (RFC 7432 §7.1)".into(),
725        });
726    }
727    let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
728    let label = decode_mpls_label(&payload[22..25])?;
729    if ethernet_tag.is_max_et() {
730        Ok(EvpnRoute::EadPerEs(EvpnEadPerEs {
731            rd,
732            esi,
733            ethernet_tag,
734            label,
735        }))
736    } else {
737        Ok(EvpnRoute::EadPerEvi(EvpnEadPerEvi {
738            rd,
739            esi,
740            ethernet_tag,
741            label,
742        }))
743    }
744}
745
746fn decode_type2(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
747    // RD (8) | ESI (10) | Ethernet Tag (4) | MAC Addr Len (1) | MAC (6) |
748    //   IP Addr Len (1, bits) | IP (0/4/16) | Label1 (3) | [Label2 (3)]
749    if payload.len() < 25 {
750        return Err(DecodeError::MalformedField {
751            message_type: "UPDATE",
752            detail: format!(
753                "EVPN Type 2 payload too short: {} bytes (need at least 25)",
754                payload.len()
755            ),
756        });
757    }
758    let rd = decode_rd(&payload[0..8])?;
759    let esi = decode_esi(&payload[8..18])?;
760    let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
761    let mac_addr_len = payload[22];
762    if mac_addr_len != 48 {
763        return Err(DecodeError::MalformedField {
764            message_type: "UPDATE",
765            detail: format!("EVPN Type 2 MAC Addr Length {mac_addr_len} (expected 48)"),
766        });
767    }
768    if payload.len() < 23 + 6 + 1 {
769        return Err(DecodeError::MalformedField {
770            message_type: "UPDATE",
771            detail: "EVPN Type 2 truncated before IP Addr Length byte".into(),
772        });
773    }
774    let mac = MacAddress([
775        payload[23],
776        payload[24],
777        payload[25],
778        payload[26],
779        payload[27],
780        payload[28],
781    ]);
782    let ip_addr_len_bits = payload[29];
783    let ip_bytes_expected = match ip_addr_len_bits {
784        0 => 0,
785        32 => 4,
786        128 => 16,
787        other => {
788            return Err(DecodeError::MalformedField {
789                message_type: "UPDATE",
790                detail: format!("EVPN Type 2 IP Addr Length {other} bits (expected 0, 32, 128)"),
791            });
792        }
793    };
794    let ip_start = 30;
795    if payload.len() < ip_start + ip_bytes_expected + 3 {
796        return Err(DecodeError::MalformedField {
797            message_type: "UPDATE",
798            detail: format!(
799                "EVPN Type 2 truncated: need {} bytes for IP + Label1, have {}",
800                ip_bytes_expected + 3,
801                payload.len() - ip_start
802            ),
803        });
804    }
805    let ip = if ip_bytes_expected == 0 {
806        None
807    } else {
808        Some(decode_ip_addr(
809            &payload[ip_start..ip_start + ip_bytes_expected],
810            ip_bytes_expected,
811            "Type 2 IP",
812        )?)
813    };
814    let label1_start = ip_start + ip_bytes_expected;
815    let label1 = decode_mpls_label(&payload[label1_start..label1_start + 3])?;
816    let label2_start = label1_start + 3;
817    let label2 = match payload.len() - label2_start {
818        0 => None,
819        3 => Some(decode_mpls_label(&payload[label2_start..label2_start + 3])?),
820        other => {
821            return Err(DecodeError::MalformedField {
822                message_type: "UPDATE",
823                detail: format!(
824                    "EVPN Type 2 trailing bytes {other} (expected 0 or 3 for optional Label2)"
825                ),
826            });
827        }
828    };
829    Ok(EvpnRoute::MacIp(EvpnMacIp {
830        rd,
831        esi,
832        ethernet_tag,
833        mac,
834        ip,
835        label1,
836        label2,
837    }))
838}
839
840fn decode_type3(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
841    // RD (8) | Ethernet Tag (4) | IP Addr Len (1, bits) | Originator IP (variable)
842    if payload.len() < 13 {
843        return Err(DecodeError::MalformedField {
844            message_type: "UPDATE",
845            detail: format!(
846                "EVPN Type 3 payload too short: {} bytes (need at least 13)",
847                payload.len()
848            ),
849        });
850    }
851    let rd = decode_rd(&payload[0..8])?;
852    let ethernet_tag = decode_ethernet_tag(&payload[8..12])?;
853    let ip_len_bits = payload[12];
854    let ip_bytes = match ip_len_bits {
855        32 => 4,
856        128 => 16,
857        other => {
858            return Err(DecodeError::MalformedField {
859                message_type: "UPDATE",
860                detail: format!("EVPN Type 3 IP Addr Length {other} bits (expected 32 or 128)"),
861            });
862        }
863    };
864    if payload.len() != 13 + ip_bytes {
865        return Err(DecodeError::MalformedField {
866            message_type: "UPDATE",
867            detail: format!(
868                "EVPN Type 3 payload length {} (expected {})",
869                payload.len(),
870                13 + ip_bytes
871            ),
872        });
873    }
874    let originator_ip = decode_ip_addr(&payload[13..], ip_bytes, "Type 3 originator IP")?;
875    Ok(EvpnRoute::Imet(EvpnImet {
876        rd,
877        ethernet_tag,
878        originator_ip,
879    }))
880}
881
882fn decode_type4(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
883    // RD (8) | ESI (10) | IP Addr Len (1, bits) | Originator IP (variable)
884    if payload.len() < 19 {
885        return Err(DecodeError::MalformedField {
886            message_type: "UPDATE",
887            detail: format!(
888                "EVPN Type 4 payload too short: {} bytes (need at least 19)",
889                payload.len()
890            ),
891        });
892    }
893    let rd = decode_rd(&payload[0..8])?;
894    let esi = decode_esi(&payload[8..18])?;
895    // RFC 7432 §7.4: ES routes carry a non-zero ESI identifying the segment.
896    if esi.is_zero() {
897        return Err(DecodeError::MalformedField {
898            message_type: "UPDATE",
899            detail: "EVPN Type 4 ES route with all-zero ESI (RFC 7432 §7.4)".into(),
900        });
901    }
902    let ip_len_bits = payload[18];
903    let ip_bytes = match ip_len_bits {
904        32 => 4,
905        128 => 16,
906        other => {
907            return Err(DecodeError::MalformedField {
908                message_type: "UPDATE",
909                detail: format!("EVPN Type 4 IP Addr Length {other} bits (expected 32 or 128)"),
910            });
911        }
912    };
913    if payload.len() != 19 + ip_bytes {
914        return Err(DecodeError::MalformedField {
915            message_type: "UPDATE",
916            detail: format!(
917                "EVPN Type 4 payload length {} (expected {})",
918                payload.len(),
919                19 + ip_bytes
920            ),
921        });
922    }
923    let originator_ip = decode_ip_addr(&payload[19..], ip_bytes, "Type 4 originator IP")?;
924    Ok(EvpnRoute::Es(EvpnEs {
925        rd,
926        esi,
927        originator_ip,
928    }))
929}
930
931fn decode_type5(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
932    // RFC 9136:
933    //   RD (8) | ESI (10) | Ethernet Tag (4) | IP Prefix Length (1) |
934    //   IP Prefix (4 or 16) | GW IP (4 or 16, same family) | MPLS Label (3)
935    // IPv4 total = 8+10+4+1+4+4+3 = 34
936    // IPv6 total = 8+10+4+1+16+16+3 = 58
937    //
938    // Family discrimination is by NLRI total length only — RFC 9136 does
939    // not carry an explicit AFI inside the Type 5 body. Receivers must
940    // therefore reject any other length as malformed. Non-canonical IP
941    // prefix bytes (host bits set beyond `prefix_len`) are silently
942    // canonicalized by `Ipv4Prefix::new` / `Ipv6Prefix::new`.
943    let total = payload.len();
944    if total != 34 && total != 58 {
945        return Err(DecodeError::MalformedField {
946            message_type: "UPDATE",
947            detail: format!("EVPN Type 5 payload length {total} (expected 34 or 58)"),
948        });
949    }
950    let rd = decode_rd(&payload[0..8])?;
951    let esi = decode_esi(&payload[8..18])?;
952    let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
953    let prefix_len = payload[22];
954    let is_v6 = total == 58;
955    let prefix = if is_v6 {
956        if prefix_len > 128 {
957            return Err(DecodeError::MalformedField {
958                message_type: "UPDATE",
959                detail: format!("EVPN Type 5 IPv6 prefix length {prefix_len} > 128"),
960            });
961        }
962        let mut octets = [0u8; 16];
963        octets.copy_from_slice(&payload[23..39]);
964        EvpnIpPrefixValue::V6(Ipv6Prefix::new(Ipv6Addr::from(octets), prefix_len))
965    } else {
966        if prefix_len > 32 {
967            return Err(DecodeError::MalformedField {
968                message_type: "UPDATE",
969                detail: format!("EVPN Type 5 IPv4 prefix length {prefix_len} > 32"),
970            });
971        }
972        let addr = Ipv4Addr::new(payload[23], payload[24], payload[25], payload[26]);
973        EvpnIpPrefixValue::V4(Ipv4Prefix::new(addr, prefix_len))
974    };
975    let (gateway, label_start) = if is_v6 {
976        let mut octets = [0u8; 16];
977        octets.copy_from_slice(&payload[39..55]);
978        (IpAddr::V6(Ipv6Addr::from(octets)), 55)
979    } else {
980        (
981            IpAddr::V4(Ipv4Addr::new(
982                payload[27],
983                payload[28],
984                payload[29],
985                payload[30],
986            )),
987            31,
988        )
989    };
990    let label = decode_mpls_label(&payload[label_start..label_start + 3])?;
991    Ok(EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
992        rd,
993        esi,
994        ethernet_tag,
995        prefix,
996        gateway,
997        label,
998    }))
999}
1000
1001// ---------------------------------------------------------------------------
1002// Public NLRI encode / decode
1003// ---------------------------------------------------------------------------
1004
1005/// Decode one or more EVPN NLRI entries from a contiguous buffer.
1006///
1007/// Each entry is framed as `route_type (1) | length (1) | payload`.
1008///
1009/// Unknown route types (anything outside 1..=5) are skipped per
1010/// RFC 7432 §11.2 ("Receivers MUST ignore Route Types they do not
1011/// understand"), so a future EVPN extension does not tear down the
1012/// session. Truncation and per-type malformed payloads still error.
1013///
1014/// # Errors
1015///
1016/// Returns [`DecodeError`] if a length byte runs past the end of `buf`,
1017/// or a recognized route type's payload is malformed.
1018pub fn decode_evpn_nlri(mut buf: &[u8]) -> Result<Vec<EvpnRoute>, DecodeError> {
1019    let mut routes = Vec::new();
1020    while !buf.is_empty() {
1021        if buf.len() < 2 {
1022            return Err(DecodeError::MalformedField {
1023                message_type: "UPDATE",
1024                detail: "EVPN NLRI truncated: need route-type + length bytes".into(),
1025            });
1026        }
1027        let route_type = buf[0];
1028        let length = usize::from(buf[1]);
1029        if buf.len() < 2 + length {
1030            return Err(DecodeError::MalformedField {
1031                message_type: "UPDATE",
1032                detail: format!(
1033                    "EVPN NLRI truncated: route type {route_type} claims length {length}, \
1034                     but only {} bytes remain",
1035                    buf.len() - 2
1036                ),
1037            });
1038        }
1039        let payload = &buf[2..2 + length];
1040        match route_type {
1041            1 => routes.push(decode_type1(payload)?),
1042            2 => routes.push(decode_type2(payload)?),
1043            3 => routes.push(decode_type3(payload)?),
1044            4 => routes.push(decode_type4(payload)?),
1045            5 => routes.push(decode_type5(payload)?),
1046            // RFC 7432 §11.2: silently skip unknown types so the session
1047            // survives a peer advertising a future EVPN extension.
1048            _ => {}
1049        }
1050        buf = &buf[2 + length..];
1051    }
1052    Ok(routes)
1053}
1054
1055// ---------------------------------------------------------------------------
1056// Per-route-type encode
1057// ---------------------------------------------------------------------------
1058
1059fn encode_mpls_label(label: MplsLabel, out: &mut Vec<u8>) {
1060    let v = label.0 & 0x00FF_FFFF;
1061    #[expect(clippy::cast_possible_truncation)]
1062    {
1063        out.push((v >> 16) as u8);
1064        out.push((v >> 8) as u8);
1065        out.push(v as u8);
1066    }
1067}
1068
1069fn encode_ip_addr(ip: IpAddr, out: &mut Vec<u8>) {
1070    match ip {
1071        IpAddr::V4(v4) => out.extend_from_slice(&v4.octets()),
1072        IpAddr::V6(v6) => out.extend_from_slice(&v6.octets()),
1073    }
1074}
1075
1076fn encode_type1_body(
1077    rd: RouteDistinguisher,
1078    esi: EthernetSegmentIdentifier,
1079    ethernet_tag: EthernetTagId,
1080    label: MplsLabel,
1081    out: &mut Vec<u8>,
1082) {
1083    out.extend_from_slice(&rd.0);
1084    out.extend_from_slice(&esi.0);
1085    out.extend_from_slice(&ethernet_tag.0.to_be_bytes());
1086    encode_mpls_label(label, out);
1087}
1088
1089fn encode_type2_body(r: &EvpnMacIp, out: &mut Vec<u8>) {
1090    out.extend_from_slice(&r.rd.0);
1091    out.extend_from_slice(&r.esi.0);
1092    out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
1093    out.push(48); // MAC Addr Length in bits
1094    out.extend_from_slice(&r.mac.0);
1095    match r.ip {
1096        None => out.push(0),
1097        Some(IpAddr::V4(v4)) => {
1098            out.push(32);
1099            out.extend_from_slice(&v4.octets());
1100        }
1101        Some(IpAddr::V6(v6)) => {
1102            out.push(128);
1103            out.extend_from_slice(&v6.octets());
1104        }
1105    }
1106    encode_mpls_label(r.label1, out);
1107    if let Some(label2) = r.label2 {
1108        encode_mpls_label(label2, out);
1109    }
1110}
1111
1112fn encode_type3_body(r: &EvpnImet, out: &mut Vec<u8>) {
1113    out.extend_from_slice(&r.rd.0);
1114    out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
1115    match r.originator_ip {
1116        IpAddr::V4(_) => out.push(32),
1117        IpAddr::V6(_) => out.push(128),
1118    }
1119    encode_ip_addr(r.originator_ip, out);
1120}
1121
1122fn encode_type4_body(r: &EvpnEs, out: &mut Vec<u8>) {
1123    out.extend_from_slice(&r.rd.0);
1124    out.extend_from_slice(&r.esi.0);
1125    match r.originator_ip {
1126        IpAddr::V4(_) => out.push(32),
1127        IpAddr::V6(_) => out.push(128),
1128    }
1129    encode_ip_addr(r.originator_ip, out);
1130}
1131
1132fn encode_type5_body(r: &EvpnIpPrefixRoute, out: &mut Vec<u8>) {
1133    // RFC 9136 §3 requires the GW IP to be the same family as the IP
1134    // prefix. A debug assertion catches programmer bugs immediately;
1135    // release builds fall back to UNSPECIFIED in the prefix family
1136    // rather than silently truncating/scrambling the wrong-family
1137    // address bytes.
1138    debug_assert!(
1139        matches!(
1140            (&r.prefix, &r.gateway),
1141            (EvpnIpPrefixValue::V4(_), IpAddr::V4(_)) | (EvpnIpPrefixValue::V6(_), IpAddr::V6(_))
1142        ),
1143        "EVPN Type 5: gateway family must match prefix family"
1144    );
1145    out.extend_from_slice(&r.rd.0);
1146    out.extend_from_slice(&r.esi.0);
1147    out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
1148    match r.prefix {
1149        EvpnIpPrefixValue::V4(p) => {
1150            out.push(p.len);
1151            out.extend_from_slice(&p.addr.octets());
1152            if let IpAddr::V4(gw) = r.gateway {
1153                out.extend_from_slice(&gw.octets());
1154            } else {
1155                out.extend_from_slice(&Ipv4Addr::UNSPECIFIED.octets());
1156            }
1157        }
1158        EvpnIpPrefixValue::V6(p) => {
1159            out.push(p.len);
1160            out.extend_from_slice(&p.addr.octets());
1161            if let IpAddr::V6(gw) = r.gateway {
1162                out.extend_from_slice(&gw.octets());
1163            } else {
1164                out.extend_from_slice(&Ipv6Addr::UNSPECIFIED.octets());
1165            }
1166        }
1167    }
1168    encode_mpls_label(r.label, out);
1169}
1170
1171/// Encode a list of EVPN NLRI entries to wire bytes.
1172pub fn encode_evpn_nlri(routes: &[EvpnRoute], buf: &mut Vec<u8>) {
1173    for route in routes {
1174        let route_type = route.route_type();
1175        let len_placeholder = buf.len();
1176        buf.push(route_type);
1177        buf.push(0); // length placeholder, backfilled below
1178        let body_start = buf.len();
1179        match route {
1180            EvpnRoute::EadPerEs(r) => {
1181                // RFC 7432 §7.1: EAD-per-ES carries MAX_ET in the Ethernet
1182                // Tag field; the decoder uses that to discriminate from
1183                // EAD-per-EVI. Force MAX_ET on the wire regardless of the
1184                // struct field so a buggy upstream cannot silently flip
1185                // the route's identity.
1186                debug_assert!(
1187                    r.ethernet_tag.is_max_et(),
1188                    "EVPN EAD-per-ES must carry MAX_ET ethernet tag"
1189                );
1190                encode_type1_body(r.rd, r.esi, EthernetTagId::MAX_ET, r.label, buf);
1191            }
1192            EvpnRoute::EadPerEvi(r) => {
1193                debug_assert!(
1194                    !r.ethernet_tag.is_max_et(),
1195                    "EVPN EAD-per-EVI must not carry MAX_ET ethernet tag"
1196                );
1197                encode_type1_body(r.rd, r.esi, r.ethernet_tag, r.label, buf);
1198            }
1199            EvpnRoute::MacIp(r) => encode_type2_body(r, buf),
1200            EvpnRoute::Imet(r) => encode_type3_body(r, buf),
1201            EvpnRoute::Es(r) => encode_type4_body(r, buf),
1202            EvpnRoute::IpPrefix(r) => encode_type5_body(r, buf),
1203        }
1204        let body_len = buf.len() - body_start;
1205        debug_assert!(
1206            u8::try_from(body_len).is_ok(),
1207            "EVPN NLRI body exceeds 255 bytes"
1208        );
1209        #[expect(clippy::cast_possible_truncation)]
1210        {
1211            buf[len_placeholder + 1] = body_len as u8;
1212        }
1213    }
1214}
1215
1216// ---------------------------------------------------------------------------
1217// Tests
1218// ---------------------------------------------------------------------------
1219
1220#[cfg(test)]
1221mod tests {
1222    use super::*;
1223
1224    fn sample_rd() -> RouteDistinguisher {
1225        // Type 0: 2-byte ASN 65000 + 4-byte assigned 100
1226        RouteDistinguisher([0x00, 0x00, 0xFD, 0xE8, 0x00, 0x00, 0x00, 0x64])
1227    }
1228
1229    fn sample_esi() -> EthernetSegmentIdentifier {
1230        EthernetSegmentIdentifier([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A])
1231    }
1232
1233    fn roundtrip(routes: &[EvpnRoute]) {
1234        let mut buf = Vec::new();
1235        encode_evpn_nlri(routes, &mut buf);
1236        let decoded = decode_evpn_nlri(&buf).expect("decode should succeed");
1237        assert_eq!(routes, decoded.as_slice(), "round-trip mismatch");
1238    }
1239
1240    #[test]
1241    fn rd_display_type0() {
1242        assert_eq!(sample_rd().to_string(), "65000:100");
1243    }
1244
1245    #[test]
1246    fn rd_display_type1() {
1247        let rd = RouteDistinguisher([0x00, 0x01, 10, 0, 0, 1, 0x00, 0x42]);
1248        assert_eq!(rd.to_string(), "10.0.0.1:66");
1249    }
1250
1251    #[test]
1252    fn rd_parse_type0_roundtrip() {
1253        // ASN ≤ 65535 picks Type 0 with a 32-bit assigned number.
1254        let rd: RouteDistinguisher = "65000:100".parse().unwrap();
1255        assert_eq!(rd.rd_type(), 0);
1256        assert_eq!(rd.to_string(), "65000:100");
1257        // Max 32-bit assigned still parses.
1258        let rd_max: RouteDistinguisher = "65000:4294967295".parse().unwrap();
1259        assert_eq!(rd_max.rd_type(), 0);
1260    }
1261
1262    #[test]
1263    fn rd_parse_type1_roundtrip() {
1264        let rd: RouteDistinguisher = "10.0.0.1:66".parse().unwrap();
1265        assert_eq!(rd.rd_type(), 1);
1266        assert_eq!(rd.to_string(), "10.0.0.1:66");
1267    }
1268
1269    #[test]
1270    fn rd_parse_type2_roundtrip() {
1271        // ASN > 65535 picks Type 2 with a 16-bit assigned number.
1272        let rd: RouteDistinguisher = "4200000000:200".parse().unwrap();
1273        assert_eq!(rd.rd_type(), 2);
1274        assert_eq!(rd.to_string(), "4200000000:200");
1275    }
1276
1277    #[test]
1278    fn rd_parse_rejects_missing_colon() {
1279        let err = "65000".parse::<RouteDistinguisher>().unwrap_err();
1280        assert!(matches!(err, RouteDistinguisherParseError::MissingColon));
1281    }
1282
1283    #[test]
1284    fn rd_parse_rejects_invalid_admin() {
1285        let err = "not-an-asn:1".parse::<RouteDistinguisher>().unwrap_err();
1286        assert!(matches!(
1287            err,
1288            RouteDistinguisherParseError::InvalidAdministrator(_)
1289        ));
1290    }
1291
1292    #[test]
1293    fn rd_parse_rejects_invalid_assigned() {
1294        let err = "65000:abc".parse::<RouteDistinguisher>().unwrap_err();
1295        assert!(matches!(
1296            err,
1297            RouteDistinguisherParseError::InvalidAssignedNumber(_)
1298        ));
1299    }
1300
1301    #[test]
1302    fn rd_parse_rejects_assigned_overflow_type1() {
1303        // IPv4 admin → Type 1 → 16-bit assigned ceiling.
1304        let err = "10.0.0.1:65536".parse::<RouteDistinguisher>().unwrap_err();
1305        assert!(matches!(
1306            err,
1307            RouteDistinguisherParseError::AssignedNumberOutOfRange { max: 0xFFFF, .. }
1308        ));
1309    }
1310
1311    #[test]
1312    fn rd_parse_rejects_assigned_overflow_type2() {
1313        // ASN > 65535 → Type 2 → 16-bit assigned ceiling.
1314        let err = "4200000000:65536"
1315            .parse::<RouteDistinguisher>()
1316            .unwrap_err();
1317        assert!(matches!(
1318            err,
1319            RouteDistinguisherParseError::AssignedNumberOutOfRange { max: 0xFFFF, .. }
1320        ));
1321    }
1322
1323    #[test]
1324    fn ethernet_tag_max_et() {
1325        assert!(EthernetTagId::MAX_ET.is_max_et());
1326        assert_eq!(EthernetTagId::MAX_ET.to_string(), "MAX_ET");
1327        assert!(!EthernetTagId(100).is_max_et());
1328    }
1329
1330    #[test]
1331    fn mac_display() {
1332        let mac = MacAddress([0x00, 0x11, 0x22, 0xaa, 0xbb, 0xcc]);
1333        assert_eq!(mac.to_string(), "00:11:22:aa:bb:cc");
1334    }
1335
1336    #[test]
1337    fn mpls_label_vxlan_vni() {
1338        let label = MplsLabel::new(10_000);
1339        assert_eq!(label.as_vni(), 10_000);
1340    }
1341
1342    #[test]
1343    fn roundtrip_type1_per_es() {
1344        roundtrip(&[EvpnRoute::EadPerEs(EvpnEadPerEs {
1345            rd: sample_rd(),
1346            esi: sample_esi(),
1347            ethernet_tag: EthernetTagId::MAX_ET,
1348            label: MplsLabel::new(500),
1349        })]);
1350    }
1351
1352    #[test]
1353    fn roundtrip_type1_per_evi() {
1354        roundtrip(&[EvpnRoute::EadPerEvi(EvpnEadPerEvi {
1355            rd: sample_rd(),
1356            esi: sample_esi(),
1357            ethernet_tag: EthernetTagId(200),
1358            label: MplsLabel::new(10_001),
1359        })]);
1360    }
1361
1362    #[test]
1363    fn roundtrip_type2_mac_only() {
1364        roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
1365            rd: sample_rd(),
1366            esi: EthernetSegmentIdentifier::ZERO,
1367            ethernet_tag: EthernetTagId(100),
1368            mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
1369            ip: None,
1370            label1: MplsLabel::new(10_000),
1371            label2: None,
1372        })]);
1373    }
1374
1375    #[test]
1376    fn roundtrip_type2_mac_ipv4_two_labels() {
1377        roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
1378            rd: sample_rd(),
1379            esi: sample_esi(),
1380            ethernet_tag: EthernetTagId(100),
1381            mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
1382            ip: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10))),
1383            label1: MplsLabel::new(10_000),
1384            label2: Some(MplsLabel::new(20_000)),
1385        })]);
1386    }
1387
1388    #[test]
1389    fn roundtrip_type2_mac_ipv6() {
1390        roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
1391            rd: sample_rd(),
1392            esi: sample_esi(),
1393            ethernet_tag: EthernetTagId(100),
1394            mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
1395            ip: Some(IpAddr::V6("2001:db8::10".parse().unwrap())),
1396            label1: MplsLabel::new(10_000),
1397            label2: None,
1398        })]);
1399    }
1400
1401    #[test]
1402    fn roundtrip_type3_ipv4() {
1403        roundtrip(&[EvpnRoute::Imet(EvpnImet {
1404            rd: sample_rd(),
1405            ethernet_tag: EthernetTagId(100),
1406            originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1407        })]);
1408    }
1409
1410    #[test]
1411    fn roundtrip_type3_ipv6() {
1412        roundtrip(&[EvpnRoute::Imet(EvpnImet {
1413            rd: sample_rd(),
1414            ethernet_tag: EthernetTagId(100),
1415            originator_ip: IpAddr::V6("2001:db8::1".parse().unwrap()),
1416        })]);
1417    }
1418
1419    #[test]
1420    fn roundtrip_type4_ipv4() {
1421        roundtrip(&[EvpnRoute::Es(EvpnEs {
1422            rd: sample_rd(),
1423            esi: sample_esi(),
1424            originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1425        })]);
1426    }
1427
1428    #[test]
1429    fn roundtrip_type5_ipv4() {
1430        roundtrip(&[EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1431            rd: sample_rd(),
1432            esi: EthernetSegmentIdentifier::ZERO,
1433            ethernet_tag: EthernetTagId(0),
1434            prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(10, 100, 0, 0), 24)),
1435            gateway: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
1436            label: MplsLabel::new(20_001),
1437        })]);
1438    }
1439
1440    #[test]
1441    fn roundtrip_type5_ipv6() {
1442        roundtrip(&[EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1443            rd: sample_rd(),
1444            esi: EthernetSegmentIdentifier::ZERO,
1445            ethernet_tag: EthernetTagId(0),
1446            prefix: EvpnIpPrefixValue::V6(Ipv6Prefix::new("2001:db8:100::".parse().unwrap(), 48)),
1447            gateway: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1448            label: MplsLabel::new(20_001),
1449        })]);
1450    }
1451
1452    #[test]
1453    fn roundtrip_all_types_one_nlri() {
1454        roundtrip(&[
1455            EvpnRoute::EadPerEs(EvpnEadPerEs {
1456                rd: sample_rd(),
1457                esi: sample_esi(),
1458                ethernet_tag: EthernetTagId::MAX_ET,
1459                label: MplsLabel::new(500),
1460            }),
1461            EvpnRoute::Imet(EvpnImet {
1462                rd: sample_rd(),
1463                ethernet_tag: EthernetTagId(100),
1464                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1465            }),
1466            EvpnRoute::MacIp(EvpnMacIp {
1467                rd: sample_rd(),
1468                esi: EthernetSegmentIdentifier::ZERO,
1469                ethernet_tag: EthernetTagId(100),
1470                mac: MacAddress([0xaa; 6]),
1471                ip: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5))),
1472                label1: MplsLabel::new(10_000),
1473                label2: None,
1474            }),
1475            EvpnRoute::Es(EvpnEs {
1476                rd: sample_rd(),
1477                esi: sample_esi(),
1478                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1479            }),
1480            EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1481                rd: sample_rd(),
1482                esi: EthernetSegmentIdentifier::ZERO,
1483                ethernet_tag: EthernetTagId(0),
1484                prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(192, 168, 0, 0), 24)),
1485                gateway: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
1486                label: MplsLabel::new(20_001),
1487            }),
1488        ]);
1489    }
1490
1491    #[test]
1492    fn decode_truncated_nlri_fails() {
1493        // Route type 2 with declared length 25 but only 20 bytes
1494        let bytes = [
1495            2u8, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1496        ];
1497        assert!(decode_evpn_nlri(&bytes).is_err());
1498    }
1499
1500    /// RFC 7432 §11.2: receivers MUST silently ignore unknown route
1501    /// types so a session survives a peer advertising a future EVPN
1502    /// extension. The decoder skips the unknown TLV and continues
1503    /// parsing — known route types after the unknown one still decode.
1504    #[test]
1505    fn decode_skips_unknown_route_type() {
1506        // Build: known Type 3 IMET | unknown Type 99 (length 4) | another known Type 3.
1507        let imet = EvpnRoute::Imet(EvpnImet {
1508            rd: sample_rd(),
1509            ethernet_tag: EthernetTagId(100),
1510            originator_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
1511        });
1512        let imet2 = EvpnRoute::Imet(EvpnImet {
1513            rd: sample_rd(),
1514            ethernet_tag: EthernetTagId(200),
1515            originator_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 2)),
1516        });
1517        let mut buf = Vec::new();
1518        encode_evpn_nlri(std::slice::from_ref(&imet), &mut buf);
1519        // Append an unknown route type (99) with a 4-byte payload.
1520        buf.extend_from_slice(&[99u8, 4, 0xAA, 0xBB, 0xCC, 0xDD]);
1521        encode_evpn_nlri(std::slice::from_ref(&imet2), &mut buf);
1522
1523        let decoded = decode_evpn_nlri(&buf).unwrap();
1524        assert_eq!(decoded.len(), 2, "unknown type should be skipped");
1525        assert!(matches!(decoded[0], EvpnRoute::Imet(_)));
1526        assert!(matches!(decoded[1], EvpnRoute::Imet(_)));
1527    }
1528
1529    /// Truncation still fails — the length byte must point inside the buffer.
1530    #[test]
1531    fn decode_unknown_route_type_truncated_still_fails() {
1532        // Type 99 claims length 10 but only 2 bytes follow.
1533        let bytes = [99u8, 10, 0, 0];
1534        assert!(decode_evpn_nlri(&bytes).is_err());
1535    }
1536
1537    /// Regression: RFC 7432 §7.1 — Type 1 EAD with all-zero ESI is malformed.
1538    #[test]
1539    fn decode_type1_rejects_zero_esi() {
1540        let mut bytes = vec![1u8, 25];
1541        bytes.extend_from_slice(&[0u8; 8]); // RD
1542        bytes.extend_from_slice(&[0u8; 10]); // ESI = ZERO
1543        bytes.extend_from_slice(&[0xFF; 4]); // ethernet_tag MAX_ET
1544        bytes.extend_from_slice(&[0, 0, 0]); // label
1545        let err = decode_evpn_nlri(&bytes).unwrap_err();
1546        let DecodeError::MalformedField { detail, .. } = err else {
1547            panic!("expected MalformedField");
1548        };
1549        assert!(detail.contains("Type 1"), "unexpected detail: {detail}");
1550    }
1551
1552    /// Regression: RFC 7432 §7.4 — Type 4 ES with all-zero ESI is malformed.
1553    #[test]
1554    fn decode_type4_rejects_zero_esi() {
1555        let mut bytes = vec![4u8, 23];
1556        bytes.extend_from_slice(&[0u8; 8]); // RD
1557        bytes.extend_from_slice(&[0u8; 10]); // ESI = ZERO
1558        bytes.push(32); // IP len bits = IPv4
1559        bytes.extend_from_slice(&[10, 0, 0, 1]); // originator IP
1560        let err = decode_evpn_nlri(&bytes).unwrap_err();
1561        let DecodeError::MalformedField { detail, .. } = err else {
1562            panic!("expected MalformedField");
1563        };
1564        assert!(detail.contains("Type 4"), "unexpected detail: {detail}");
1565    }
1566
1567    #[test]
1568    fn empty_buffer_decodes_to_empty() {
1569        assert_eq!(decode_evpn_nlri(&[]).unwrap(), Vec::<EvpnRoute>::new());
1570    }
1571
1572    #[test]
1573    fn route_key_discriminates_ead_per_es_vs_per_evi() {
1574        let per_es = EvpnRoute::EadPerEs(EvpnEadPerEs {
1575            rd: sample_rd(),
1576            esi: sample_esi(),
1577            ethernet_tag: EthernetTagId::MAX_ET,
1578            label: MplsLabel::new(500),
1579        });
1580        let per_evi = EvpnRoute::EadPerEvi(EvpnEadPerEvi {
1581            rd: sample_rd(),
1582            esi: sample_esi(),
1583            ethernet_tag: EthernetTagId(200),
1584            label: MplsLabel::new(500),
1585        });
1586        assert_ne!(per_es.key(), per_evi.key());
1587    }
1588
1589    /// Regression: an EAD-per-ES route round-trips encode → decode back
1590    /// to `EadPerEs`, never silently becoming `EadPerEvi`. The encoder
1591    /// pins the ethernet tag to `MAX_ET` (the per-ES discriminator
1592    /// per RFC 7432 §7.1) regardless of what the struct field holds.
1593    #[test]
1594    fn ead_per_es_encode_round_trips_to_per_es() {
1595        let r = EvpnRoute::EadPerEs(EvpnEadPerEs {
1596            rd: sample_rd(),
1597            esi: sample_esi(),
1598            ethernet_tag: EthernetTagId::MAX_ET,
1599            label: MplsLabel::new(7),
1600        });
1601        let mut buf = Vec::new();
1602        encode_evpn_nlri(std::slice::from_ref(&r), &mut buf);
1603        let decoded = decode_evpn_nlri(&buf).unwrap();
1604        assert_eq!(decoded.len(), 1);
1605        assert!(matches!(decoded[0], EvpnRoute::EadPerEs(_)));
1606    }
1607
1608    /// Regression: gateway-family mismatch on Type 5 trips the
1609    /// `debug_assert!` so encoder bugs surface in tests/CI rather than
1610    /// silently corrupting the wire payload. Cargo runs unit tests with
1611    /// debug assertions on, so this test is `#[should_panic]`.
1612    #[test]
1613    #[should_panic(expected = "gateway family must match prefix family")]
1614    fn type5_encode_panics_on_family_mismatch_in_debug() {
1615        let r = EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1616            rd: sample_rd(),
1617            esi: EthernetSegmentIdentifier::ZERO,
1618            ethernet_tag: EthernetTagId(0),
1619            prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(10, 0, 0, 0), 8)),
1620            gateway: IpAddr::V6(Ipv6Addr::LOCALHOST),
1621            label: MplsLabel::new(100),
1622        });
1623        let mut buf = Vec::new();
1624        encode_evpn_nlri(&[r], &mut buf);
1625    }
1626}