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/// A 3-byte MPLS label field (RFC 3032) as carried in EVPN NLRI.
203///
204/// For VXLAN-encapsulated EVPN (RFC 8365), the 24-bit label field carries
205/// a 24-bit VNI. The codec does not distinguish — consumers interpret the
206/// value based on the Encapsulation extended community.
207#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
208pub struct MplsLabel(pub u32);
209
210impl MplsLabel {
211    /// Construct from a raw 24-bit value (upper 8 bits ignored).
212    #[must_use]
213    pub const fn new(value: u32) -> Self {
214        Self(value & 0x00FF_FFFF)
215    }
216
217    /// Raw 24-bit value.
218    #[must_use]
219    pub const fn value(&self) -> u32 {
220        self.0
221    }
222
223    /// Interpret this field as a VXLAN VNI (RFC 8365 §5).
224    ///
225    /// The label field on the wire is `(label << 4) | (TC << 1) | S` when
226    /// used for MPLS, but for VXLAN the full 24 bits are the VNI. Both
227    /// uses call this accessor; it's up to the caller to know the encap.
228    #[must_use]
229    pub const fn as_vni(&self) -> u32 {
230        self.0
231    }
232
233    /// Extract the 20-bit MPLS label field (upper 20 bits of the 24-bit word).
234    #[must_use]
235    pub const fn as_mpls_label(&self) -> u32 {
236        self.0 >> 4
237    }
238}
239
240/// An IP prefix encoded inside an EVPN Type 5 NLRI — IPv4 or IPv6.
241#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
242pub enum EvpnIpPrefixValue {
243    /// IPv4 prefix.
244    V4(Ipv4Prefix),
245    /// IPv6 prefix.
246    V6(Ipv6Prefix),
247}
248
249impl fmt::Display for EvpnIpPrefixValue {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        match self {
252            Self::V4(p) => write!(f, "{}/{}", p.addr, p.len),
253            Self::V6(p) => write!(f, "{}/{}", p.addr, p.len),
254        }
255    }
256}
257
258// ---------------------------------------------------------------------------
259// Per-route-type payload structs
260// ---------------------------------------------------------------------------
261
262/// Type 1: Ethernet Auto-Discovery per-ES route (RFC 7432 §7.1).
263///
264/// Ethernet Tag field MUST be `MAX_ET`; discriminated from per-EVI by value.
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
266pub struct EvpnEadPerEs {
267    /// Route Distinguisher.
268    pub rd: RouteDistinguisher,
269    /// Ethernet Segment Identifier (must be non-zero).
270    pub esi: EthernetSegmentIdentifier,
271    /// Ethernet Tag (must be `MAX_ET` for per-ES).
272    pub ethernet_tag: EthernetTagId,
273    /// MPLS label — typically the ESI label for per-ES.
274    pub label: MplsLabel,
275}
276
277/// Type 1: Ethernet Auto-Discovery per-EVI route (RFC 7432 §7.1).
278///
279/// Same wire shape as per-ES but Ethernet Tag is a real EVI/bridge-domain
280/// identifier rather than `MAX_ET`.
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
282pub struct EvpnEadPerEvi {
283    /// Route Distinguisher.
284    pub rd: RouteDistinguisher,
285    /// Ethernet Segment Identifier (must be non-zero).
286    pub esi: EthernetSegmentIdentifier,
287    /// Ethernet Tag identifying the EVI / bridge domain.
288    pub ethernet_tag: EthernetTagId,
289    /// MPLS label / VNI for this EVI on this ES.
290    pub label: MplsLabel,
291}
292
293/// Type 2: MAC/IP Advertisement route (RFC 7432 §7.2).
294#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
295pub struct EvpnMacIp {
296    /// Route Distinguisher.
297    pub rd: RouteDistinguisher,
298    /// Ethernet Segment Identifier (may be ZERO for single-homed CE).
299    pub esi: EthernetSegmentIdentifier,
300    /// Ethernet Tag identifying the EVI / bridge domain.
301    pub ethernet_tag: EthernetTagId,
302    /// MAC address being advertised.
303    pub mac: MacAddress,
304    /// Host IP address, if any. Wire length is 0, 4, or 16 bytes.
305    pub ip: Option<IpAddr>,
306    /// Primary MPLS label / VNI (present on all Type 2 routes).
307    pub label1: MplsLabel,
308    /// Secondary label for symmetric IRB (RFC 9135), if present.
309    pub label2: Option<MplsLabel>,
310}
311
312/// Type 3: Inclusive Multicast Ethernet Tag route (RFC 7432 §7.3).
313#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
314pub struct EvpnImet {
315    /// Route Distinguisher.
316    pub rd: RouteDistinguisher,
317    /// Ethernet Tag identifying the EVI / bridge domain.
318    pub ethernet_tag: EthernetTagId,
319    /// Originator Router IP. Wire length is 4 or 16 bytes.
320    pub originator_ip: IpAddr,
321}
322
323/// Type 4: Ethernet Segment route (RFC 7432 §7.4).
324#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
325pub struct EvpnEs {
326    /// Route Distinguisher.
327    pub rd: RouteDistinguisher,
328    /// Ethernet Segment Identifier (must be non-zero).
329    pub esi: EthernetSegmentIdentifier,
330    /// Originator Router IP. Wire length is 4 or 16 bytes.
331    pub originator_ip: IpAddr,
332}
333
334/// Type 5: IP Prefix route (RFC 9136).
335#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
336pub struct EvpnIpPrefixRoute {
337    /// Route Distinguisher.
338    pub rd: RouteDistinguisher,
339    /// Ethernet Segment Identifier (may be ZERO).
340    pub esi: EthernetSegmentIdentifier,
341    /// Ethernet Tag (often 0 for Type 5).
342    pub ethernet_tag: EthernetTagId,
343    /// IP prefix being advertised (IPv4 or IPv6).
344    pub prefix: EvpnIpPrefixValue,
345    /// Gateway IP address. Same family as `prefix`. May be 0.0.0.0 / ::.
346    pub gateway: IpAddr,
347    /// MPLS label / L3 VNI.
348    pub label: MplsLabel,
349}
350
351// ---------------------------------------------------------------------------
352// EvpnRoute + EvpnRouteKey top-level enums
353// ---------------------------------------------------------------------------
354
355/// A single EVPN NLRI entry (RFC 7432 §7), one of five route types.
356///
357/// This carries the full wire payload — needed for round-trip encoding and
358/// for reflection through a route reflector. For a minimal hashable
359/// identifier suitable as a RIB key, see [`EvpnRouteKey`].
360#[derive(Debug, Clone, PartialEq, Eq, Hash)]
361pub enum EvpnRoute {
362    /// Type 1 — EAD per-ES.
363    EadPerEs(EvpnEadPerEs),
364    /// Type 1 — EAD per-EVI.
365    EadPerEvi(EvpnEadPerEvi),
366    /// Type 2 — MAC/IP Advertisement.
367    MacIp(EvpnMacIp),
368    /// Type 3 — Inclusive Multicast Ethernet Tag.
369    Imet(EvpnImet),
370    /// Type 4 — Ethernet Segment.
371    Es(EvpnEs),
372    /// Type 5 — IP Prefix (RFC 9136).
373    IpPrefix(EvpnIpPrefixRoute),
374}
375
376impl EvpnRoute {
377    /// Wire route-type byte (1..=5).
378    #[must_use]
379    pub const fn route_type(&self) -> u8 {
380        match self {
381            Self::EadPerEs(_) | Self::EadPerEvi(_) => 1,
382            Self::MacIp(_) => 2,
383            Self::Imet(_) => 3,
384            Self::Es(_) => 4,
385            Self::IpPrefix(_) => 5,
386        }
387    }
388
389    /// Identifying subset of the route, suitable as a RIB key.
390    #[must_use]
391    pub fn key(&self) -> EvpnRouteKey {
392        match self {
393            Self::EadPerEs(r) => EvpnRouteKey::EadPerEs {
394                rd: r.rd,
395                esi: r.esi,
396                ethernet_tag: r.ethernet_tag,
397            },
398            Self::EadPerEvi(r) => EvpnRouteKey::EadPerEvi {
399                rd: r.rd,
400                esi: r.esi,
401                ethernet_tag: r.ethernet_tag,
402            },
403            Self::MacIp(r) => EvpnRouteKey::MacIp {
404                rd: r.rd,
405                ethernet_tag: r.ethernet_tag,
406                mac: r.mac,
407                ip: r.ip,
408            },
409            Self::Imet(r) => EvpnRouteKey::Imet {
410                rd: r.rd,
411                ethernet_tag: r.ethernet_tag,
412                originator_ip: r.originator_ip,
413            },
414            Self::Es(r) => EvpnRouteKey::Es {
415                rd: r.rd,
416                esi: r.esi,
417                originator_ip: r.originator_ip,
418            },
419            Self::IpPrefix(r) => EvpnRouteKey::IpPrefix {
420                rd: r.rd,
421                ethernet_tag: r.ethernet_tag,
422                prefix: r.prefix,
423            },
424        }
425    }
426}
427
428/// Identifying subset of an EVPN route — the fields that make two routes
429/// distinct per RFC 7432. Suitable as a `HashMap` key in the RIB.
430///
431/// EAD per-ES and EAD per-EVI share a wire format but get distinct variants
432/// here so the RIB never accidentally collapses them.
433#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
434pub enum EvpnRouteKey {
435    /// Type 1 per-ES key.
436    EadPerEs {
437        /// Route Distinguisher.
438        rd: RouteDistinguisher,
439        /// Ethernet Segment Identifier.
440        esi: EthernetSegmentIdentifier,
441        /// Ethernet Tag (`MAX_ET` for per-ES).
442        ethernet_tag: EthernetTagId,
443    },
444    /// Type 1 per-EVI key.
445    EadPerEvi {
446        /// Route Distinguisher.
447        rd: RouteDistinguisher,
448        /// Ethernet Segment Identifier.
449        esi: EthernetSegmentIdentifier,
450        /// Ethernet Tag identifying the EVI.
451        ethernet_tag: EthernetTagId,
452    },
453    /// Type 2 key — RD, tag, MAC, optional IP.
454    MacIp {
455        /// Route Distinguisher.
456        rd: RouteDistinguisher,
457        /// Ethernet Tag identifying the EVI.
458        ethernet_tag: EthernetTagId,
459        /// MAC address.
460        mac: MacAddress,
461        /// Optional host IP (0, 32, or 128 bits on the wire).
462        ip: Option<IpAddr>,
463    },
464    /// Type 3 key.
465    Imet {
466        /// Route Distinguisher.
467        rd: RouteDistinguisher,
468        /// Ethernet Tag identifying the EVI.
469        ethernet_tag: EthernetTagId,
470        /// Originator Router IP.
471        originator_ip: IpAddr,
472    },
473    /// Type 4 key.
474    Es {
475        /// Route Distinguisher.
476        rd: RouteDistinguisher,
477        /// Ethernet Segment Identifier.
478        esi: EthernetSegmentIdentifier,
479        /// Originator Router IP.
480        originator_ip: IpAddr,
481    },
482    /// Type 5 key.
483    IpPrefix {
484        /// Route Distinguisher.
485        rd: RouteDistinguisher,
486        /// Ethernet Tag.
487        ethernet_tag: EthernetTagId,
488        /// IP prefix.
489        prefix: EvpnIpPrefixValue,
490    },
491}
492
493impl EvpnRouteKey {
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
507// ---------------------------------------------------------------------------
508// Decode helpers
509// ---------------------------------------------------------------------------
510
511fn decode_rd(buf: &[u8]) -> Result<RouteDistinguisher, DecodeError> {
512    if buf.len() < 8 {
513        return Err(DecodeError::MalformedField {
514            message_type: "UPDATE",
515            detail: "EVPN NLRI truncated: expected 8-byte Route Distinguisher".to_string(),
516        });
517    }
518    let mut bytes = [0u8; 8];
519    bytes.copy_from_slice(&buf[..8]);
520    Ok(RouteDistinguisher(bytes))
521}
522
523fn decode_esi(buf: &[u8]) -> Result<EthernetSegmentIdentifier, DecodeError> {
524    if buf.len() < 10 {
525        return Err(DecodeError::MalformedField {
526            message_type: "UPDATE",
527            detail: "EVPN NLRI truncated: expected 10-byte ESI".to_string(),
528        });
529    }
530    let mut bytes = [0u8; 10];
531    bytes.copy_from_slice(&buf[..10]);
532    Ok(EthernetSegmentIdentifier(bytes))
533}
534
535fn decode_ethernet_tag(buf: &[u8]) -> Result<EthernetTagId, DecodeError> {
536    if buf.len() < 4 {
537        return Err(DecodeError::MalformedField {
538            message_type: "UPDATE",
539            detail: "EVPN NLRI truncated: expected 4-byte Ethernet Tag".to_string(),
540        });
541    }
542    Ok(EthernetTagId(u32::from_be_bytes([
543        buf[0], buf[1], buf[2], buf[3],
544    ])))
545}
546
547fn decode_mpls_label(buf: &[u8]) -> Result<MplsLabel, DecodeError> {
548    if buf.len() < 3 {
549        return Err(DecodeError::MalformedField {
550            message_type: "UPDATE",
551            detail: "EVPN NLRI truncated: expected 3-byte MPLS label".to_string(),
552        });
553    }
554    // 24-bit value stored in 3 bytes, big-endian.
555    let value = (u32::from(buf[0]) << 16) | (u32::from(buf[1]) << 8) | u32::from(buf[2]);
556    Ok(MplsLabel(value))
557}
558
559fn decode_ip_addr(buf: &[u8], len: usize, field: &str) -> Result<IpAddr, DecodeError> {
560    match len {
561        4 => {
562            if buf.len() < 4 {
563                return Err(DecodeError::MalformedField {
564                    message_type: "UPDATE",
565                    detail: format!("EVPN NLRI truncated: expected 4 bytes for {field}"),
566                });
567            }
568            Ok(IpAddr::V4(Ipv4Addr::new(buf[0], buf[1], buf[2], buf[3])))
569        }
570        16 => {
571            if buf.len() < 16 {
572                return Err(DecodeError::MalformedField {
573                    message_type: "UPDATE",
574                    detail: format!("EVPN NLRI truncated: expected 16 bytes for {field}"),
575                });
576            }
577            let mut octets = [0u8; 16];
578            octets.copy_from_slice(&buf[..16]);
579            Ok(IpAddr::V6(Ipv6Addr::from(octets)))
580        }
581        other => Err(DecodeError::MalformedField {
582            message_type: "UPDATE",
583            detail: format!("EVPN NLRI {field} length {other} (expected 4 or 16)"),
584        }),
585    }
586}
587
588// ---------------------------------------------------------------------------
589// Per-route-type decode
590// ---------------------------------------------------------------------------
591
592fn decode_type1(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
593    // RD (8) | ESI (10) | Ethernet Tag (4) | MPLS Label (3) = 25 bytes
594    if payload.len() != 25 {
595        return Err(DecodeError::MalformedField {
596            message_type: "UPDATE",
597            detail: format!("EVPN Type 1 payload length {} (expected 25)", payload.len()),
598        });
599    }
600    let rd = decode_rd(&payload[0..8])?;
601    let esi = decode_esi(&payload[8..18])?;
602    // RFC 7432 §7.1: EAD routes (per-ES and per-EVI) carry a non-zero ESI
603    // identifying the Ethernet Segment. Reject ESI=0 at the wire boundary.
604    if esi.is_zero() {
605        return Err(DecodeError::MalformedField {
606            message_type: "UPDATE",
607            detail: "EVPN Type 1 EAD route with all-zero ESI (RFC 7432 §7.1)".into(),
608        });
609    }
610    let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
611    let label = decode_mpls_label(&payload[22..25])?;
612    if ethernet_tag.is_max_et() {
613        Ok(EvpnRoute::EadPerEs(EvpnEadPerEs {
614            rd,
615            esi,
616            ethernet_tag,
617            label,
618        }))
619    } else {
620        Ok(EvpnRoute::EadPerEvi(EvpnEadPerEvi {
621            rd,
622            esi,
623            ethernet_tag,
624            label,
625        }))
626    }
627}
628
629fn decode_type2(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
630    // RD (8) | ESI (10) | Ethernet Tag (4) | MAC Addr Len (1) | MAC (6) |
631    //   IP Addr Len (1, bits) | IP (0/4/16) | Label1 (3) | [Label2 (3)]
632    if payload.len() < 25 {
633        return Err(DecodeError::MalformedField {
634            message_type: "UPDATE",
635            detail: format!(
636                "EVPN Type 2 payload too short: {} bytes (need at least 25)",
637                payload.len()
638            ),
639        });
640    }
641    let rd = decode_rd(&payload[0..8])?;
642    let esi = decode_esi(&payload[8..18])?;
643    let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
644    let mac_addr_len = payload[22];
645    if mac_addr_len != 48 {
646        return Err(DecodeError::MalformedField {
647            message_type: "UPDATE",
648            detail: format!("EVPN Type 2 MAC Addr Length {mac_addr_len} (expected 48)"),
649        });
650    }
651    if payload.len() < 23 + 6 + 1 {
652        return Err(DecodeError::MalformedField {
653            message_type: "UPDATE",
654            detail: "EVPN Type 2 truncated before IP Addr Length byte".into(),
655        });
656    }
657    let mac = MacAddress([
658        payload[23],
659        payload[24],
660        payload[25],
661        payload[26],
662        payload[27],
663        payload[28],
664    ]);
665    let ip_addr_len_bits = payload[29];
666    let ip_bytes_expected = match ip_addr_len_bits {
667        0 => 0,
668        32 => 4,
669        128 => 16,
670        other => {
671            return Err(DecodeError::MalformedField {
672                message_type: "UPDATE",
673                detail: format!("EVPN Type 2 IP Addr Length {other} bits (expected 0, 32, 128)"),
674            });
675        }
676    };
677    let ip_start = 30;
678    if payload.len() < ip_start + ip_bytes_expected + 3 {
679        return Err(DecodeError::MalformedField {
680            message_type: "UPDATE",
681            detail: format!(
682                "EVPN Type 2 truncated: need {} bytes for IP + Label1, have {}",
683                ip_bytes_expected + 3,
684                payload.len() - ip_start
685            ),
686        });
687    }
688    let ip = if ip_bytes_expected == 0 {
689        None
690    } else {
691        Some(decode_ip_addr(
692            &payload[ip_start..ip_start + ip_bytes_expected],
693            ip_bytes_expected,
694            "Type 2 IP",
695        )?)
696    };
697    let label1_start = ip_start + ip_bytes_expected;
698    let label1 = decode_mpls_label(&payload[label1_start..label1_start + 3])?;
699    let label2_start = label1_start + 3;
700    let label2 = match payload.len() - label2_start {
701        0 => None,
702        3 => Some(decode_mpls_label(&payload[label2_start..label2_start + 3])?),
703        other => {
704            return Err(DecodeError::MalformedField {
705                message_type: "UPDATE",
706                detail: format!(
707                    "EVPN Type 2 trailing bytes {other} (expected 0 or 3 for optional Label2)"
708                ),
709            });
710        }
711    };
712    Ok(EvpnRoute::MacIp(EvpnMacIp {
713        rd,
714        esi,
715        ethernet_tag,
716        mac,
717        ip,
718        label1,
719        label2,
720    }))
721}
722
723fn decode_type3(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
724    // RD (8) | Ethernet Tag (4) | IP Addr Len (1, bits) | Originator IP (variable)
725    if payload.len() < 13 {
726        return Err(DecodeError::MalformedField {
727            message_type: "UPDATE",
728            detail: format!(
729                "EVPN Type 3 payload too short: {} bytes (need at least 13)",
730                payload.len()
731            ),
732        });
733    }
734    let rd = decode_rd(&payload[0..8])?;
735    let ethernet_tag = decode_ethernet_tag(&payload[8..12])?;
736    let ip_len_bits = payload[12];
737    let ip_bytes = match ip_len_bits {
738        32 => 4,
739        128 => 16,
740        other => {
741            return Err(DecodeError::MalformedField {
742                message_type: "UPDATE",
743                detail: format!("EVPN Type 3 IP Addr Length {other} bits (expected 32 or 128)"),
744            });
745        }
746    };
747    if payload.len() != 13 + ip_bytes {
748        return Err(DecodeError::MalformedField {
749            message_type: "UPDATE",
750            detail: format!(
751                "EVPN Type 3 payload length {} (expected {})",
752                payload.len(),
753                13 + ip_bytes
754            ),
755        });
756    }
757    let originator_ip = decode_ip_addr(&payload[13..], ip_bytes, "Type 3 originator IP")?;
758    Ok(EvpnRoute::Imet(EvpnImet {
759        rd,
760        ethernet_tag,
761        originator_ip,
762    }))
763}
764
765fn decode_type4(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
766    // RD (8) | ESI (10) | IP Addr Len (1, bits) | Originator IP (variable)
767    if payload.len() < 19 {
768        return Err(DecodeError::MalformedField {
769            message_type: "UPDATE",
770            detail: format!(
771                "EVPN Type 4 payload too short: {} bytes (need at least 19)",
772                payload.len()
773            ),
774        });
775    }
776    let rd = decode_rd(&payload[0..8])?;
777    let esi = decode_esi(&payload[8..18])?;
778    // RFC 7432 §7.4: ES routes carry a non-zero ESI identifying the segment.
779    if esi.is_zero() {
780        return Err(DecodeError::MalformedField {
781            message_type: "UPDATE",
782            detail: "EVPN Type 4 ES route with all-zero ESI (RFC 7432 §7.4)".into(),
783        });
784    }
785    let ip_len_bits = payload[18];
786    let ip_bytes = match ip_len_bits {
787        32 => 4,
788        128 => 16,
789        other => {
790            return Err(DecodeError::MalformedField {
791                message_type: "UPDATE",
792                detail: format!("EVPN Type 4 IP Addr Length {other} bits (expected 32 or 128)"),
793            });
794        }
795    };
796    if payload.len() != 19 + ip_bytes {
797        return Err(DecodeError::MalformedField {
798            message_type: "UPDATE",
799            detail: format!(
800                "EVPN Type 4 payload length {} (expected {})",
801                payload.len(),
802                19 + ip_bytes
803            ),
804        });
805    }
806    let originator_ip = decode_ip_addr(&payload[19..], ip_bytes, "Type 4 originator IP")?;
807    Ok(EvpnRoute::Es(EvpnEs {
808        rd,
809        esi,
810        originator_ip,
811    }))
812}
813
814fn decode_type5(payload: &[u8]) -> Result<EvpnRoute, DecodeError> {
815    // RFC 9136:
816    //   RD (8) | ESI (10) | Ethernet Tag (4) | IP Prefix Length (1) |
817    //   IP Prefix (4 or 16) | GW IP (4 or 16, same family) | MPLS Label (3)
818    // IPv4 total = 8+10+4+1+4+4+3 = 34
819    // IPv6 total = 8+10+4+1+16+16+3 = 58
820    //
821    // Family discrimination is by NLRI total length only — RFC 9136 does
822    // not carry an explicit AFI inside the Type 5 body. Receivers must
823    // therefore reject any other length as malformed. Non-canonical IP
824    // prefix bytes (host bits set beyond `prefix_len`) are silently
825    // canonicalized by `Ipv4Prefix::new` / `Ipv6Prefix::new`.
826    let total = payload.len();
827    if total != 34 && total != 58 {
828        return Err(DecodeError::MalformedField {
829            message_type: "UPDATE",
830            detail: format!("EVPN Type 5 payload length {total} (expected 34 or 58)"),
831        });
832    }
833    let rd = decode_rd(&payload[0..8])?;
834    let esi = decode_esi(&payload[8..18])?;
835    let ethernet_tag = decode_ethernet_tag(&payload[18..22])?;
836    let prefix_len = payload[22];
837    let is_v6 = total == 58;
838    let prefix = if is_v6 {
839        if prefix_len > 128 {
840            return Err(DecodeError::MalformedField {
841                message_type: "UPDATE",
842                detail: format!("EVPN Type 5 IPv6 prefix length {prefix_len} > 128"),
843            });
844        }
845        let mut octets = [0u8; 16];
846        octets.copy_from_slice(&payload[23..39]);
847        EvpnIpPrefixValue::V6(Ipv6Prefix::new(Ipv6Addr::from(octets), prefix_len))
848    } else {
849        if prefix_len > 32 {
850            return Err(DecodeError::MalformedField {
851                message_type: "UPDATE",
852                detail: format!("EVPN Type 5 IPv4 prefix length {prefix_len} > 32"),
853            });
854        }
855        let addr = Ipv4Addr::new(payload[23], payload[24], payload[25], payload[26]);
856        EvpnIpPrefixValue::V4(Ipv4Prefix::new(addr, prefix_len))
857    };
858    let (gateway, label_start) = if is_v6 {
859        let mut octets = [0u8; 16];
860        octets.copy_from_slice(&payload[39..55]);
861        (IpAddr::V6(Ipv6Addr::from(octets)), 55)
862    } else {
863        (
864            IpAddr::V4(Ipv4Addr::new(
865                payload[27],
866                payload[28],
867                payload[29],
868                payload[30],
869            )),
870            31,
871        )
872    };
873    let label = decode_mpls_label(&payload[label_start..label_start + 3])?;
874    Ok(EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
875        rd,
876        esi,
877        ethernet_tag,
878        prefix,
879        gateway,
880        label,
881    }))
882}
883
884// ---------------------------------------------------------------------------
885// Public NLRI encode / decode
886// ---------------------------------------------------------------------------
887
888/// Decode one or more EVPN NLRI entries from a contiguous buffer.
889///
890/// Each entry is framed as `route_type (1) | length (1) | payload`.
891///
892/// Unknown route types (anything outside 1..=5) are skipped per
893/// RFC 7432 §11.2 ("Receivers MUST ignore Route Types they do not
894/// understand"), so a future EVPN extension does not tear down the
895/// session. Truncation and per-type malformed payloads still error.
896///
897/// # Errors
898///
899/// Returns [`DecodeError`] if a length byte runs past the end of `buf`,
900/// or a recognized route type's payload is malformed.
901pub fn decode_evpn_nlri(mut buf: &[u8]) -> Result<Vec<EvpnRoute>, DecodeError> {
902    let mut routes = Vec::new();
903    while !buf.is_empty() {
904        if buf.len() < 2 {
905            return Err(DecodeError::MalformedField {
906                message_type: "UPDATE",
907                detail: "EVPN NLRI truncated: need route-type + length bytes".into(),
908            });
909        }
910        let route_type = buf[0];
911        let length = usize::from(buf[1]);
912        if buf.len() < 2 + length {
913            return Err(DecodeError::MalformedField {
914                message_type: "UPDATE",
915                detail: format!(
916                    "EVPN NLRI truncated: route type {route_type} claims length {length}, \
917                     but only {} bytes remain",
918                    buf.len() - 2
919                ),
920            });
921        }
922        let payload = &buf[2..2 + length];
923        match route_type {
924            1 => routes.push(decode_type1(payload)?),
925            2 => routes.push(decode_type2(payload)?),
926            3 => routes.push(decode_type3(payload)?),
927            4 => routes.push(decode_type4(payload)?),
928            5 => routes.push(decode_type5(payload)?),
929            // RFC 7432 §11.2: silently skip unknown types so the session
930            // survives a peer advertising a future EVPN extension.
931            _ => {}
932        }
933        buf = &buf[2 + length..];
934    }
935    Ok(routes)
936}
937
938// ---------------------------------------------------------------------------
939// Per-route-type encode
940// ---------------------------------------------------------------------------
941
942fn encode_mpls_label(label: MplsLabel, out: &mut Vec<u8>) {
943    let v = label.0 & 0x00FF_FFFF;
944    #[expect(clippy::cast_possible_truncation)]
945    {
946        out.push((v >> 16) as u8);
947        out.push((v >> 8) as u8);
948        out.push(v as u8);
949    }
950}
951
952fn encode_ip_addr(ip: IpAddr, out: &mut Vec<u8>) {
953    match ip {
954        IpAddr::V4(v4) => out.extend_from_slice(&v4.octets()),
955        IpAddr::V6(v6) => out.extend_from_slice(&v6.octets()),
956    }
957}
958
959fn encode_type1_body(
960    rd: RouteDistinguisher,
961    esi: EthernetSegmentIdentifier,
962    ethernet_tag: EthernetTagId,
963    label: MplsLabel,
964    out: &mut Vec<u8>,
965) {
966    out.extend_from_slice(&rd.0);
967    out.extend_from_slice(&esi.0);
968    out.extend_from_slice(&ethernet_tag.0.to_be_bytes());
969    encode_mpls_label(label, out);
970}
971
972fn encode_type2_body(r: &EvpnMacIp, out: &mut Vec<u8>) {
973    out.extend_from_slice(&r.rd.0);
974    out.extend_from_slice(&r.esi.0);
975    out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
976    out.push(48); // MAC Addr Length in bits
977    out.extend_from_slice(&r.mac.0);
978    match r.ip {
979        None => out.push(0),
980        Some(IpAddr::V4(v4)) => {
981            out.push(32);
982            out.extend_from_slice(&v4.octets());
983        }
984        Some(IpAddr::V6(v6)) => {
985            out.push(128);
986            out.extend_from_slice(&v6.octets());
987        }
988    }
989    encode_mpls_label(r.label1, out);
990    if let Some(label2) = r.label2 {
991        encode_mpls_label(label2, out);
992    }
993}
994
995fn encode_type3_body(r: &EvpnImet, out: &mut Vec<u8>) {
996    out.extend_from_slice(&r.rd.0);
997    out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
998    match r.originator_ip {
999        IpAddr::V4(_) => out.push(32),
1000        IpAddr::V6(_) => out.push(128),
1001    }
1002    encode_ip_addr(r.originator_ip, out);
1003}
1004
1005fn encode_type4_body(r: &EvpnEs, out: &mut Vec<u8>) {
1006    out.extend_from_slice(&r.rd.0);
1007    out.extend_from_slice(&r.esi.0);
1008    match r.originator_ip {
1009        IpAddr::V4(_) => out.push(32),
1010        IpAddr::V6(_) => out.push(128),
1011    }
1012    encode_ip_addr(r.originator_ip, out);
1013}
1014
1015fn encode_type5_body(r: &EvpnIpPrefixRoute, out: &mut Vec<u8>) {
1016    // RFC 9136 §3 requires the GW IP to be the same family as the IP
1017    // prefix. A debug assertion catches programmer bugs immediately;
1018    // release builds fall back to UNSPECIFIED in the prefix family
1019    // rather than silently truncating/scrambling the wrong-family
1020    // address bytes.
1021    debug_assert!(
1022        matches!(
1023            (&r.prefix, &r.gateway),
1024            (EvpnIpPrefixValue::V4(_), IpAddr::V4(_)) | (EvpnIpPrefixValue::V6(_), IpAddr::V6(_))
1025        ),
1026        "EVPN Type 5: gateway family must match prefix family"
1027    );
1028    out.extend_from_slice(&r.rd.0);
1029    out.extend_from_slice(&r.esi.0);
1030    out.extend_from_slice(&r.ethernet_tag.0.to_be_bytes());
1031    match r.prefix {
1032        EvpnIpPrefixValue::V4(p) => {
1033            out.push(p.len);
1034            out.extend_from_slice(&p.addr.octets());
1035            if let IpAddr::V4(gw) = r.gateway {
1036                out.extend_from_slice(&gw.octets());
1037            } else {
1038                out.extend_from_slice(&Ipv4Addr::UNSPECIFIED.octets());
1039            }
1040        }
1041        EvpnIpPrefixValue::V6(p) => {
1042            out.push(p.len);
1043            out.extend_from_slice(&p.addr.octets());
1044            if let IpAddr::V6(gw) = r.gateway {
1045                out.extend_from_slice(&gw.octets());
1046            } else {
1047                out.extend_from_slice(&Ipv6Addr::UNSPECIFIED.octets());
1048            }
1049        }
1050    }
1051    encode_mpls_label(r.label, out);
1052}
1053
1054/// Encode a list of EVPN NLRI entries to wire bytes.
1055pub fn encode_evpn_nlri(routes: &[EvpnRoute], buf: &mut Vec<u8>) {
1056    for route in routes {
1057        let route_type = route.route_type();
1058        let len_placeholder = buf.len();
1059        buf.push(route_type);
1060        buf.push(0); // length placeholder, backfilled below
1061        let body_start = buf.len();
1062        match route {
1063            EvpnRoute::EadPerEs(r) => {
1064                // RFC 7432 §7.1: EAD-per-ES carries MAX_ET in the Ethernet
1065                // Tag field; the decoder uses that to discriminate from
1066                // EAD-per-EVI. Force MAX_ET on the wire regardless of the
1067                // struct field so a buggy upstream cannot silently flip
1068                // the route's identity.
1069                debug_assert!(
1070                    r.ethernet_tag.is_max_et(),
1071                    "EVPN EAD-per-ES must carry MAX_ET ethernet tag"
1072                );
1073                encode_type1_body(r.rd, r.esi, EthernetTagId::MAX_ET, r.label, buf);
1074            }
1075            EvpnRoute::EadPerEvi(r) => {
1076                debug_assert!(
1077                    !r.ethernet_tag.is_max_et(),
1078                    "EVPN EAD-per-EVI must not carry MAX_ET ethernet tag"
1079                );
1080                encode_type1_body(r.rd, r.esi, r.ethernet_tag, r.label, buf);
1081            }
1082            EvpnRoute::MacIp(r) => encode_type2_body(r, buf),
1083            EvpnRoute::Imet(r) => encode_type3_body(r, buf),
1084            EvpnRoute::Es(r) => encode_type4_body(r, buf),
1085            EvpnRoute::IpPrefix(r) => encode_type5_body(r, buf),
1086        }
1087        let body_len = buf.len() - body_start;
1088        debug_assert!(
1089            u8::try_from(body_len).is_ok(),
1090            "EVPN NLRI body exceeds 255 bytes"
1091        );
1092        #[expect(clippy::cast_possible_truncation)]
1093        {
1094            buf[len_placeholder + 1] = body_len as u8;
1095        }
1096    }
1097}
1098
1099// ---------------------------------------------------------------------------
1100// Tests
1101// ---------------------------------------------------------------------------
1102
1103#[cfg(test)]
1104mod tests {
1105    use super::*;
1106
1107    fn sample_rd() -> RouteDistinguisher {
1108        // Type 0: 2-byte ASN 65000 + 4-byte assigned 100
1109        RouteDistinguisher([0x00, 0x00, 0xFD, 0xE8, 0x00, 0x00, 0x00, 0x64])
1110    }
1111
1112    fn sample_esi() -> EthernetSegmentIdentifier {
1113        EthernetSegmentIdentifier([0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A])
1114    }
1115
1116    fn roundtrip(routes: &[EvpnRoute]) {
1117        let mut buf = Vec::new();
1118        encode_evpn_nlri(routes, &mut buf);
1119        let decoded = decode_evpn_nlri(&buf).expect("decode should succeed");
1120        assert_eq!(routes, decoded.as_slice(), "round-trip mismatch");
1121    }
1122
1123    #[test]
1124    fn rd_display_type0() {
1125        assert_eq!(sample_rd().to_string(), "65000:100");
1126    }
1127
1128    #[test]
1129    fn rd_display_type1() {
1130        let rd = RouteDistinguisher([0x00, 0x01, 10, 0, 0, 1, 0x00, 0x42]);
1131        assert_eq!(rd.to_string(), "10.0.0.1:66");
1132    }
1133
1134    #[test]
1135    fn ethernet_tag_max_et() {
1136        assert!(EthernetTagId::MAX_ET.is_max_et());
1137        assert_eq!(EthernetTagId::MAX_ET.to_string(), "MAX_ET");
1138        assert!(!EthernetTagId(100).is_max_et());
1139    }
1140
1141    #[test]
1142    fn mac_display() {
1143        let mac = MacAddress([0x00, 0x11, 0x22, 0xaa, 0xbb, 0xcc]);
1144        assert_eq!(mac.to_string(), "00:11:22:aa:bb:cc");
1145    }
1146
1147    #[test]
1148    fn mpls_label_vxlan_vni() {
1149        let label = MplsLabel::new(10_000);
1150        assert_eq!(label.as_vni(), 10_000);
1151    }
1152
1153    #[test]
1154    fn roundtrip_type1_per_es() {
1155        roundtrip(&[EvpnRoute::EadPerEs(EvpnEadPerEs {
1156            rd: sample_rd(),
1157            esi: sample_esi(),
1158            ethernet_tag: EthernetTagId::MAX_ET,
1159            label: MplsLabel::new(500),
1160        })]);
1161    }
1162
1163    #[test]
1164    fn roundtrip_type1_per_evi() {
1165        roundtrip(&[EvpnRoute::EadPerEvi(EvpnEadPerEvi {
1166            rd: sample_rd(),
1167            esi: sample_esi(),
1168            ethernet_tag: EthernetTagId(200),
1169            label: MplsLabel::new(10_001),
1170        })]);
1171    }
1172
1173    #[test]
1174    fn roundtrip_type2_mac_only() {
1175        roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
1176            rd: sample_rd(),
1177            esi: EthernetSegmentIdentifier::ZERO,
1178            ethernet_tag: EthernetTagId(100),
1179            mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
1180            ip: None,
1181            label1: MplsLabel::new(10_000),
1182            label2: None,
1183        })]);
1184    }
1185
1186    #[test]
1187    fn roundtrip_type2_mac_ipv4_two_labels() {
1188        roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
1189            rd: sample_rd(),
1190            esi: sample_esi(),
1191            ethernet_tag: EthernetTagId(100),
1192            mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
1193            ip: Some(IpAddr::V4(Ipv4Addr::new(192, 0, 2, 10))),
1194            label1: MplsLabel::new(10_000),
1195            label2: Some(MplsLabel::new(20_000)),
1196        })]);
1197    }
1198
1199    #[test]
1200    fn roundtrip_type2_mac_ipv6() {
1201        roundtrip(&[EvpnRoute::MacIp(EvpnMacIp {
1202            rd: sample_rd(),
1203            esi: sample_esi(),
1204            ethernet_tag: EthernetTagId(100),
1205            mac: MacAddress([0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff]),
1206            ip: Some(IpAddr::V6("2001:db8::10".parse().unwrap())),
1207            label1: MplsLabel::new(10_000),
1208            label2: None,
1209        })]);
1210    }
1211
1212    #[test]
1213    fn roundtrip_type3_ipv4() {
1214        roundtrip(&[EvpnRoute::Imet(EvpnImet {
1215            rd: sample_rd(),
1216            ethernet_tag: EthernetTagId(100),
1217            originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1218        })]);
1219    }
1220
1221    #[test]
1222    fn roundtrip_type3_ipv6() {
1223        roundtrip(&[EvpnRoute::Imet(EvpnImet {
1224            rd: sample_rd(),
1225            ethernet_tag: EthernetTagId(100),
1226            originator_ip: IpAddr::V6("2001:db8::1".parse().unwrap()),
1227        })]);
1228    }
1229
1230    #[test]
1231    fn roundtrip_type4_ipv4() {
1232        roundtrip(&[EvpnRoute::Es(EvpnEs {
1233            rd: sample_rd(),
1234            esi: sample_esi(),
1235            originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1236        })]);
1237    }
1238
1239    #[test]
1240    fn roundtrip_type5_ipv4() {
1241        roundtrip(&[EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1242            rd: sample_rd(),
1243            esi: EthernetSegmentIdentifier::ZERO,
1244            ethernet_tag: EthernetTagId(0),
1245            prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(10, 100, 0, 0), 24)),
1246            gateway: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
1247            label: MplsLabel::new(20_001),
1248        })]);
1249    }
1250
1251    #[test]
1252    fn roundtrip_type5_ipv6() {
1253        roundtrip(&[EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1254            rd: sample_rd(),
1255            esi: EthernetSegmentIdentifier::ZERO,
1256            ethernet_tag: EthernetTagId(0),
1257            prefix: EvpnIpPrefixValue::V6(Ipv6Prefix::new("2001:db8:100::".parse().unwrap(), 48)),
1258            gateway: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
1259            label: MplsLabel::new(20_001),
1260        })]);
1261    }
1262
1263    #[test]
1264    fn roundtrip_all_types_one_nlri() {
1265        roundtrip(&[
1266            EvpnRoute::EadPerEs(EvpnEadPerEs {
1267                rd: sample_rd(),
1268                esi: sample_esi(),
1269                ethernet_tag: EthernetTagId::MAX_ET,
1270                label: MplsLabel::new(500),
1271            }),
1272            EvpnRoute::Imet(EvpnImet {
1273                rd: sample_rd(),
1274                ethernet_tag: EthernetTagId(100),
1275                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1276            }),
1277            EvpnRoute::MacIp(EvpnMacIp {
1278                rd: sample_rd(),
1279                esi: EthernetSegmentIdentifier::ZERO,
1280                ethernet_tag: EthernetTagId(100),
1281                mac: MacAddress([0xaa; 6]),
1282                ip: Some(IpAddr::V4(Ipv4Addr::new(10, 0, 0, 5))),
1283                label1: MplsLabel::new(10_000),
1284                label2: None,
1285            }),
1286            EvpnRoute::Es(EvpnEs {
1287                rd: sample_rd(),
1288                esi: sample_esi(),
1289                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1290            }),
1291            EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1292                rd: sample_rd(),
1293                esi: EthernetSegmentIdentifier::ZERO,
1294                ethernet_tag: EthernetTagId(0),
1295                prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(192, 168, 0, 0), 24)),
1296                gateway: IpAddr::V4(Ipv4Addr::UNSPECIFIED),
1297                label: MplsLabel::new(20_001),
1298            }),
1299        ]);
1300    }
1301
1302    #[test]
1303    fn decode_truncated_nlri_fails() {
1304        // Route type 2 with declared length 25 but only 20 bytes
1305        let bytes = [
1306            2u8, 25, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
1307        ];
1308        assert!(decode_evpn_nlri(&bytes).is_err());
1309    }
1310
1311    /// RFC 7432 §11.2: receivers MUST silently ignore unknown route
1312    /// types so a session survives a peer advertising a future EVPN
1313    /// extension. The decoder skips the unknown TLV and continues
1314    /// parsing — known route types after the unknown one still decode.
1315    #[test]
1316    fn decode_skips_unknown_route_type() {
1317        // Build: known Type 3 IMET | unknown Type 99 (length 4) | another known Type 3.
1318        let imet = EvpnRoute::Imet(EvpnImet {
1319            rd: sample_rd(),
1320            ethernet_tag: EthernetTagId(100),
1321            originator_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 1)),
1322        });
1323        let imet2 = EvpnRoute::Imet(EvpnImet {
1324            rd: sample_rd(),
1325            ethernet_tag: EthernetTagId(200),
1326            originator_ip: IpAddr::V4(Ipv4Addr::new(192, 0, 2, 2)),
1327        });
1328        let mut buf = Vec::new();
1329        encode_evpn_nlri(std::slice::from_ref(&imet), &mut buf);
1330        // Append an unknown route type (99) with a 4-byte payload.
1331        buf.extend_from_slice(&[99u8, 4, 0xAA, 0xBB, 0xCC, 0xDD]);
1332        encode_evpn_nlri(std::slice::from_ref(&imet2), &mut buf);
1333
1334        let decoded = decode_evpn_nlri(&buf).unwrap();
1335        assert_eq!(decoded.len(), 2, "unknown type should be skipped");
1336        assert!(matches!(decoded[0], EvpnRoute::Imet(_)));
1337        assert!(matches!(decoded[1], EvpnRoute::Imet(_)));
1338    }
1339
1340    /// Truncation still fails — the length byte must point inside the buffer.
1341    #[test]
1342    fn decode_unknown_route_type_truncated_still_fails() {
1343        // Type 99 claims length 10 but only 2 bytes follow.
1344        let bytes = [99u8, 10, 0, 0];
1345        assert!(decode_evpn_nlri(&bytes).is_err());
1346    }
1347
1348    /// Regression: RFC 7432 §7.1 — Type 1 EAD with all-zero ESI is malformed.
1349    #[test]
1350    fn decode_type1_rejects_zero_esi() {
1351        let mut bytes = vec![1u8, 25];
1352        bytes.extend_from_slice(&[0u8; 8]); // RD
1353        bytes.extend_from_slice(&[0u8; 10]); // ESI = ZERO
1354        bytes.extend_from_slice(&[0xFF; 4]); // ethernet_tag MAX_ET
1355        bytes.extend_from_slice(&[0, 0, 0]); // label
1356        let err = decode_evpn_nlri(&bytes).unwrap_err();
1357        let DecodeError::MalformedField { detail, .. } = err else {
1358            panic!("expected MalformedField");
1359        };
1360        assert!(detail.contains("Type 1"), "unexpected detail: {detail}");
1361    }
1362
1363    /// Regression: RFC 7432 §7.4 — Type 4 ES with all-zero ESI is malformed.
1364    #[test]
1365    fn decode_type4_rejects_zero_esi() {
1366        let mut bytes = vec![4u8, 23];
1367        bytes.extend_from_slice(&[0u8; 8]); // RD
1368        bytes.extend_from_slice(&[0u8; 10]); // ESI = ZERO
1369        bytes.push(32); // IP len bits = IPv4
1370        bytes.extend_from_slice(&[10, 0, 0, 1]); // originator IP
1371        let err = decode_evpn_nlri(&bytes).unwrap_err();
1372        let DecodeError::MalformedField { detail, .. } = err else {
1373            panic!("expected MalformedField");
1374        };
1375        assert!(detail.contains("Type 4"), "unexpected detail: {detail}");
1376    }
1377
1378    #[test]
1379    fn empty_buffer_decodes_to_empty() {
1380        assert_eq!(decode_evpn_nlri(&[]).unwrap(), Vec::<EvpnRoute>::new());
1381    }
1382
1383    #[test]
1384    fn route_key_discriminates_ead_per_es_vs_per_evi() {
1385        let per_es = EvpnRoute::EadPerEs(EvpnEadPerEs {
1386            rd: sample_rd(),
1387            esi: sample_esi(),
1388            ethernet_tag: EthernetTagId::MAX_ET,
1389            label: MplsLabel::new(500),
1390        });
1391        let per_evi = EvpnRoute::EadPerEvi(EvpnEadPerEvi {
1392            rd: sample_rd(),
1393            esi: sample_esi(),
1394            ethernet_tag: EthernetTagId(200),
1395            label: MplsLabel::new(500),
1396        });
1397        assert_ne!(per_es.key(), per_evi.key());
1398    }
1399
1400    /// Regression: an EAD-per-ES route round-trips encode → decode back
1401    /// to `EadPerEs`, never silently becoming `EadPerEvi`. The encoder
1402    /// pins the ethernet tag to `MAX_ET` (the per-ES discriminator
1403    /// per RFC 7432 §7.1) regardless of what the struct field holds.
1404    #[test]
1405    fn ead_per_es_encode_round_trips_to_per_es() {
1406        let r = EvpnRoute::EadPerEs(EvpnEadPerEs {
1407            rd: sample_rd(),
1408            esi: sample_esi(),
1409            ethernet_tag: EthernetTagId::MAX_ET,
1410            label: MplsLabel::new(7),
1411        });
1412        let mut buf = Vec::new();
1413        encode_evpn_nlri(std::slice::from_ref(&r), &mut buf);
1414        let decoded = decode_evpn_nlri(&buf).unwrap();
1415        assert_eq!(decoded.len(), 1);
1416        assert!(matches!(decoded[0], EvpnRoute::EadPerEs(_)));
1417    }
1418
1419    /// Regression: gateway-family mismatch on Type 5 trips the
1420    /// `debug_assert!` so encoder bugs surface in tests/CI rather than
1421    /// silently corrupting the wire payload. Cargo runs unit tests with
1422    /// debug assertions on, so this test is `#[should_panic]`.
1423    #[test]
1424    #[should_panic(expected = "gateway family must match prefix family")]
1425    fn type5_encode_panics_on_family_mismatch_in_debug() {
1426        let r = EvpnRoute::IpPrefix(EvpnIpPrefixRoute {
1427            rd: sample_rd(),
1428            esi: EthernetSegmentIdentifier::ZERO,
1429            ethernet_tag: EthernetTagId(0),
1430            prefix: EvpnIpPrefixValue::V4(Ipv4Prefix::new(Ipv4Addr::new(10, 0, 0, 0), 8)),
1431            gateway: IpAddr::V6(Ipv6Addr::LOCALHOST),
1432            label: MplsLabel::new(100),
1433        });
1434        let mut buf = Vec::new();
1435        encode_evpn_nlri(&[r], &mut buf);
1436    }
1437}