Skip to main content

rustbgpd_wire/
attribute.rs

1use std::fmt;
2use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
3
4use bytes::Bytes;
5
6use crate::capability::{Afi, Safi};
7use crate::constants::{as_path_segment, attr_flags, attr_type};
8use crate::error::DecodeError;
9use crate::nlri::{NlriEntry, Prefix};
10use crate::notification::update_subcode;
11
12/// Origin attribute values per RFC 4271 §5.1.1.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[repr(u8)]
15pub enum Origin {
16    /// Learned via IGP.
17    Igp = 0,
18    /// Learned via EGP.
19    Egp = 1,
20    /// Origin undetermined.
21    Incomplete = 2,
22}
23
24impl Origin {
25    /// Create from a raw byte value.
26    #[must_use]
27    pub fn from_u8(value: u8) -> Option<Self> {
28        match value {
29            0 => Some(Self::Igp),
30            1 => Some(Self::Egp),
31            2 => Some(Self::Incomplete),
32            _ => None,
33        }
34    }
35}
36
37impl std::fmt::Display for Origin {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        match self {
40            Self::Igp => write!(f, "IGP"),
41            Self::Egp => write!(f, "EGP"),
42            Self::Incomplete => write!(f, "INCOMPLETE"),
43        }
44    }
45}
46
47/// `AS_PATH` segment types per RFC 4271 §4.3.
48#[derive(Debug, Clone, PartialEq, Eq, Hash)]
49pub enum AsPathSegment {
50    /// `AS_SET` — unordered set of ASNs.
51    AsSet(Vec<u32>),
52    /// `AS_SEQUENCE` — ordered sequence of ASNs.
53    AsSequence(Vec<u32>),
54}
55
56/// `AS_PATH` attribute.
57#[derive(Debug, Clone, PartialEq, Eq, Hash)]
58pub struct AsPath {
59    /// Ordered list of path segments.
60    pub segments: Vec<AsPathSegment>,
61}
62
63impl AsPath {
64    /// Count the total number of ASNs in the path for best-path comparison.
65    /// `AS_SET` counts as 1 regardless of size (RFC 4271 §9.1.2.2).
66    #[must_use]
67    pub fn len(&self) -> usize {
68        self.segments
69            .iter()
70            .map(|seg| match seg {
71                AsPathSegment::AsSequence(asns) => asns.len(),
72                AsPathSegment::AsSet(_) => 1,
73            })
74            .sum()
75    }
76
77    /// Returns `true` if the path has no segments.
78    #[must_use]
79    pub fn is_empty(&self) -> bool {
80        self.segments.is_empty()
81    }
82
83    /// Returns true if `asn` appears in any segment (`AS_SEQUENCE` or `AS_SET`).
84    /// Used for loop detection per RFC 4271 §9.1.2.
85    #[must_use]
86    pub fn contains_asn(&self, asn: u32) -> bool {
87        self.segments.iter().any(|seg| match seg {
88            AsPathSegment::AsSequence(asns) | AsPathSegment::AsSet(asns) => asns.contains(&asn),
89        })
90    }
91
92    /// Extract the origin ASN from the `AS_PATH`.
93    ///
94    /// The origin AS is the last ASN in the rightmost `AS_SEQUENCE` segment.
95    /// Returns `None` if the path has no `AS_SEQUENCE` segments or all
96    /// `AS_SEQUENCE` segments are empty.
97    #[must_use]
98    pub fn origin_asn(&self) -> Option<u32> {
99        self.segments.iter().rev().find_map(|seg| match seg {
100            AsPathSegment::AsSequence(asns) => asns.last().copied(),
101            AsPathSegment::AsSet(_) => None,
102        })
103    }
104
105    /// Returns `true` if every ASN in the path is a private ASN.
106    ///
107    /// Returns `false` for empty paths (no ASNs to check).
108    #[must_use]
109    pub fn all_private(&self) -> bool {
110        let mut count = 0;
111        for seg in &self.segments {
112            match seg {
113                AsPathSegment::AsSequence(asns) | AsPathSegment::AsSet(asns) => {
114                    for asn in asns {
115                        count += 1;
116                        if !is_private_asn(*asn) {
117                            return false;
118                        }
119                    }
120                }
121            }
122        }
123        count > 0
124    }
125
126    /// Convert to a string representation for regex matching.
127    ///
128    /// `AS_SEQUENCE` segments produce space-separated ASNs.
129    /// `AS_SET` segments produce `{ASN1 ASN2}` (curly braces, space-separated).
130    /// Multiple segments are space-separated.
131    ///
132    /// Examples: `"65001 65002"`, `"65001 {65003 65004}"`, `""` (empty path).
133    #[must_use]
134    pub fn to_aspath_string(&self) -> String {
135        let mut parts = Vec::new();
136        for seg in &self.segments {
137            match seg {
138                AsPathSegment::AsSequence(asns) => {
139                    for asn in asns {
140                        parts.push(asn.to_string());
141                    }
142                }
143                AsPathSegment::AsSet(asns) => {
144                    let inner: Vec<String> = asns.iter().map(ToString::to_string).collect();
145                    parts.push(format!("{{{}}}", inner.join(" ")));
146                }
147            }
148        }
149        parts.join(" ")
150    }
151}
152
153/// Returns `true` if the given ASN falls in a private-use range.
154///
155/// Private ranges (RFC 5398 + RFC 6996):
156/// - 16-bit: 64512–65534
157/// - 32-bit: 4200000000–4294967294
158#[must_use]
159pub fn is_private_asn(asn: u32) -> bool {
160    (64512..=65534).contains(&asn) || (4_200_000_000..=4_294_967_294).contains(&asn)
161}
162
163/// RFC 4760 `MP_REACH_NLRI` attribute (type code 14).
164///
165/// Uses [`NlriEntry`] to carry Add-Path path IDs alongside each prefix.
166/// For non-Add-Path peers, `path_id` is always 0.
167#[derive(Debug, Clone, PartialEq, Eq, Hash)]
168pub struct MpReachNlri {
169    /// Address family.
170    pub afi: Afi,
171    /// Sub-address family.
172    pub safi: Safi,
173    /// Global next-hop address for the announced prefixes.
174    ///
175    /// RFC 8950 allows IPv4 unicast NLRI to use an IPv6 next hop in
176    /// `MP_REACH_NLRI`, so this field may be IPv6 even when `afi == Ipv4`.
177    ///
178    /// For `FlowSpec` (SAFI 133), next-hop length is 0 and this field is
179    /// unused (defaults to `0.0.0.0`).
180    pub next_hop: IpAddr,
181    /// Optional IPv6 link-local next-hop carried alongside the global
182    /// address per RFC 4760 §3 / RFC 2545 §3. Populated only when the
183    /// wire NH-Len is 32 bytes (global + link-local). The decoder
184    /// preserves the second 16 bytes here so re-encode round-trips.
185    pub link_local_next_hop: Option<Ipv6Addr>,
186    /// Announced NLRI entries.
187    pub announced: Vec<NlriEntry>,
188    /// `FlowSpec` NLRI rules (RFC 8955). Populated only when `safi == FlowSpec`.
189    pub flowspec_announced: Vec<crate::flowspec::FlowSpecRule>,
190    /// EVPN NLRI routes (RFC 7432). Populated only when `safi == Evpn`.
191    pub evpn_announced: Vec<crate::evpn::EvpnRoute>,
192}
193
194/// RFC 4760 `MP_UNREACH_NLRI` attribute (type 15).
195///
196/// Uses [`NlriEntry`] to carry Add-Path path IDs alongside each prefix.
197/// For non-Add-Path peers, `path_id` is always 0.
198#[derive(Debug, Clone, PartialEq, Eq, Hash)]
199pub struct MpUnreachNlri {
200    /// Address family.
201    pub afi: Afi,
202    /// Sub-address family.
203    pub safi: Safi,
204    /// Withdrawn NLRI entries.
205    pub withdrawn: Vec<NlriEntry>,
206    /// `FlowSpec` NLRI rules withdrawn (RFC 8955). Populated only when `safi == FlowSpec`.
207    pub flowspec_withdrawn: Vec<crate::flowspec::FlowSpecRule>,
208    /// EVPN NLRI routes withdrawn (RFC 7432). Populated only when `safi == Evpn`.
209    pub evpn_withdrawn: Vec<crate::evpn::EvpnRoute>,
210}
211
212/// RFC 4360 Extended Community — 8-byte value stored as `u64`.
213///
214/// Wire layout: type (1) + sub-type (1) + value (6).
215/// Bit 6 of the type byte: 0 = transitive, 1 = non-transitive.
216#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
217pub struct ExtendedCommunity(u64);
218
219/// Decoded DF Election Extended Community (RFC 8584 / RFC 9785).
220#[derive(Debug, Clone, Copy, PartialEq, Eq)]
221pub struct DfElectionExtendedCommunity {
222    /// Five-bit DF algorithm ID.
223    pub algorithm_id: u8,
224    /// Two-octet capability bitmap.
225    pub capabilities: u16,
226    /// DF Preference. Defined only for RFC 9785 preference algorithms;
227    /// `None` for DefaultModulo/HRW where the trailing bytes are reserved.
228    pub preference: Option<u16>,
229}
230
231impl ExtendedCommunity {
232    /// Create from a raw 8-byte value.
233    #[must_use]
234    pub fn new(raw: u64) -> Self {
235        Self(raw)
236    }
237
238    /// Return the raw 8-byte value.
239    #[must_use]
240    pub fn as_u64(self) -> u64 {
241        self.0
242    }
243
244    /// High byte — IANA-assigned type.
245    #[must_use]
246    pub fn type_byte(self) -> u8 {
247        (self.0 >> 56) as u8
248    }
249
250    /// Second byte — sub-type within the type.
251    #[must_use]
252    pub fn subtype(self) -> u8 {
253        self.0.to_be_bytes()[1]
254    }
255
256    /// Transitive if bit 6 of the type byte is 0.
257    #[must_use]
258    pub fn is_transitive(self) -> bool {
259        self.type_byte() & 0x40 == 0
260    }
261
262    /// Bytes 2-7 of the community value.
263    #[must_use]
264    pub fn value_bytes(self) -> [u8; 6] {
265        let b = self.0.to_be_bytes();
266        [b[2], b[3], b[4], b[5], b[6], b[7]]
267    }
268
269    /// Decode as Route Target (sub-type 0x02).
270    ///
271    /// Returns `(global_admin, local_admin)` as raw u32 values. The
272    /// interpretation of `global_admin` depends on the type byte:
273    /// - Type 0x00 (2-octet AS specific): global = ASN (fits u16), local = u32
274    /// - Type 0x01 (IPv4 address specific): global = IPv4 addr as u32, local = u16
275    /// - Type 0x02 (4-octet AS specific): global = ASN (u32), local = u16
276    ///
277    /// Callers that need to distinguish these encodings (e.g. for display as
278    /// `RT:192.0.2.1:100` vs `RT:65001:100`) must also check [`type_byte()`](Self::type_byte).
279    #[must_use]
280    pub fn route_target(self) -> Option<(u32, u32)> {
281        if self.subtype() != 0x02 {
282            return None;
283        }
284        self.decode_two_part()
285    }
286
287    /// Decode as Route Origin (sub-type 0x03).
288    ///
289    /// Same layout as [`route_target()`](Self::route_target) — returns raw
290    /// `(global_admin, local_admin)` with the same type-byte-dependent
291    /// interpretation. Check [`type_byte()`](Self::type_byte) to distinguish
292    /// 2-octet AS, IPv4-address, and 4-octet AS encodings.
293    #[must_use]
294    pub fn route_origin(self) -> Option<(u32, u32)> {
295        if self.subtype() != 0x03 {
296            return None;
297        }
298        self.decode_two_part()
299    }
300
301    // -------------------------------------------------------------------
302    // EVPN-specific typed accessors (RFC 7432 / RFC 8365 / RFC 9135)
303    // -------------------------------------------------------------------
304
305    /// Decode as BGP Encapsulation Extended Community (RFC 9012 §4.1, encoded
306    /// per the widely-deployed RFC 5512 layout: 4-byte reserved + 2-byte
307    /// Tunnel Type). Type 0x03, subtype 0x0C.
308    ///
309    /// Returns the Tunnel Type code. For VXLAN-EVPN (RFC 8365), the value is
310    /// 8. Other common values: 7 = NVGRE, 11 = MPLS-over-GRE.
311    ///
312    /// The reserved bytes are intentionally not validated here: RFC 5512
313    /// specifies MUST-zero on send, ignored on receive. FRR, `GoBGP`, Cisco,
314    /// and Juniper all emit zeros in practice; rejecting non-zero reserves
315    /// would break interop in the rare case an unknown implementation
316    /// re-purposes those bytes. Consumers should treat the returned
317    /// `tunnel_type` as the semantic signal.
318    #[must_use]
319    pub fn as_bgp_encapsulation(self) -> Option<u16> {
320        if self.type_byte() & 0x3F != 0x03 || self.subtype() != 0x0C {
321            return None;
322        }
323        let v = self.value_bytes();
324        Some(u16::from_be_bytes([v[4], v[5]]))
325    }
326
327    /// Construct a BGP Encapsulation Extended Community (RFC 9012 §4.1).
328    ///
329    /// Writes 4 bytes of reserved zero followed by the 16-bit tunnel type.
330    #[must_use]
331    pub fn bgp_encapsulation(tunnel_type: u16) -> Self {
332        let tt = tunnel_type.to_be_bytes();
333        let raw = u64::from_be_bytes([0x03, 0x0C, 0, 0, 0, 0, tt[0], tt[1]]);
334        Self(raw)
335    }
336
337    /// Decode as MAC Mobility Extended Community (RFC 7432 §7.7).
338    /// Type 0x06, subtype 0x00.
339    ///
340    /// Returns `(sticky, sequence_number)`. The sticky bit (bit 0 of the
341    /// flags byte) marks the MAC as non-movable; receivers must not displace
342    /// a sticky MAC with a higher-sequence non-sticky advertisement.
343    #[must_use]
344    pub fn as_mac_mobility(self) -> Option<(bool, u32)> {
345        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x00 {
346            return None;
347        }
348        let v = self.value_bytes();
349        let sticky = (v[0] & 0x01) != 0;
350        let seq = u32::from_be_bytes([v[2], v[3], v[4], v[5]]);
351        Some((sticky, seq))
352    }
353
354    /// Construct a MAC Mobility Extended Community (RFC 7432 §7.7).
355    #[must_use]
356    pub fn mac_mobility(sticky: bool, sequence: u32) -> Self {
357        let flags = u8::from(sticky);
358        let s = sequence.to_be_bytes();
359        let raw = u64::from_be_bytes([0x06, 0x00, flags, 0, s[0], s[1], s[2], s[3]]);
360        Self(raw)
361    }
362
363    /// Decode as ESI Label Extended Community (RFC 7432 §7.5).
364    /// Type 0x06, subtype 0x01.
365    ///
366    /// Returns `(single_active, label)`. The single-active flag (bit 0 of
367    /// the flags byte) signals single-active multi-homing mode.
368    #[must_use]
369    pub fn as_esi_label(self) -> Option<(bool, u32)> {
370        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x01 {
371            return None;
372        }
373        let v = self.value_bytes();
374        let single_active = (v[0] & 0x01) != 0;
375        let label = (u32::from(v[3]) << 16) | (u32::from(v[4]) << 8) | u32::from(v[5]);
376        Some((single_active, label))
377    }
378
379    /// Construct an ESI Label Extended Community (RFC 7432 §7.5).
380    ///
381    /// `label` is a 24-bit MPLS label or VXLAN VNI; high 8 bits are masked.
382    #[must_use]
383    pub fn esi_label(single_active: bool, label: u32) -> Self {
384        let flags = u8::from(single_active);
385        let l = label & 0x00FF_FFFF;
386        #[expect(clippy::cast_possible_truncation)]
387        let raw = u64::from_be_bytes([
388            0x06,
389            0x01,
390            flags,
391            0,
392            0,
393            (l >> 16) as u8,
394            (l >> 8) as u8,
395            l as u8,
396        ]);
397        Self(raw)
398    }
399
400    /// Decode as ES-Import Route Target Extended Community (RFC 7432 §7.6).
401    /// Type 0x06, subtype 0x02.
402    ///
403    /// Returns the 6-byte MAC address that serves as the import target for
404    /// Type 4 ES routes.
405    #[must_use]
406    pub fn as_es_import_rt(self) -> Option<[u8; 6]> {
407        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x02 {
408            return None;
409        }
410        Some(self.value_bytes())
411    }
412
413    /// Construct an ES-Import Route Target Extended Community.
414    #[must_use]
415    pub fn es_import_rt(mac: [u8; 6]) -> Self {
416        let raw = u64::from_be_bytes([0x06, 0x02, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]]);
417        Self(raw)
418    }
419
420    /// Decode as DF Election Extended Community (RFC 8584 §2.2,
421    /// updated by RFC 9785 §3). Type 0x06, subtype 0x06.
422    #[must_use]
423    pub fn as_df_election(self) -> Option<DfElectionExtendedCommunity> {
424        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x06 {
425            return None;
426        }
427        let v = self.value_bytes();
428        let algorithm_id = v[0] & 0x1f;
429        let capabilities = u16::from_be_bytes([v[1], v[2]]);
430        let preference = match algorithm_id {
431            2 | 3 => Some(u16::from_be_bytes([v[4], v[5]])),
432            _ => None,
433        };
434        Some(DfElectionExtendedCommunity {
435            algorithm_id,
436            capabilities,
437            preference,
438        })
439    }
440
441    /// Construct a DF Election Extended Community (RFC 8584 §2.2,
442    /// RFC 9785 §3).
443    ///
444    /// `algorithm_id` is masked to the five-bit DF Alg field. For
445    /// `DefaultModulo` and HRW, pass `None` for `preference` so the
446    /// reserved trailing bytes are emitted as zero.
447    #[must_use]
448    pub fn df_election(algorithm_id: u8, capabilities: u16, preference: Option<u16>) -> Self {
449        let alg = algorithm_id & 0x1f;
450        let cap = capabilities.to_be_bytes();
451        let pref = preference.unwrap_or(0).to_be_bytes();
452        let raw = u64::from_be_bytes([0x06, 0x06, alg, cap[0], cap[1], 0, pref[0], pref[1]]);
453        Self(raw)
454    }
455
456    /// Decode as Router MAC Extended Community (RFC 9135 §4.1).
457    /// Type 0x06, subtype 0x03.
458    ///
459    /// Returns the 6-byte router MAC used for symmetric IRB.
460    #[must_use]
461    pub fn as_router_mac(self) -> Option<[u8; 6]> {
462        if self.type_byte() & 0x3F != 0x06 || self.subtype() != 0x03 {
463            return None;
464        }
465        Some(self.value_bytes())
466    }
467
468    /// Construct a Router MAC Extended Community (RFC 9135 §4.1).
469    #[must_use]
470    pub fn router_mac(mac: [u8; 6]) -> Self {
471        let raw = u64::from_be_bytes([0x06, 0x03, mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]]);
472        Self(raw)
473    }
474
475    /// Decode as Default Gateway Extended Community (RFC 4761 §3.2.5 /
476    /// RFC 7432). Type 0x03, subtype 0x0D. This is a flag-only community:
477    /// presence is the signal and the 6-byte value field must be all zeros.
478    /// Malformed advertisements with non-zero value bytes are treated as
479    /// non-matches rather than silently accepted — downstream policy and
480    /// validation consumers treat this accessor as semantic truth.
481    #[must_use]
482    pub fn as_default_gateway(self) -> bool {
483        self.type_byte() & 0x3F == 0x03 && self.subtype() == 0x0D && self.value_bytes() == [0u8; 6]
484    }
485
486    /// Construct a Default Gateway Extended Community.
487    #[must_use]
488    pub fn default_gateway() -> Self {
489        let raw = u64::from_be_bytes([0x03, 0x0D, 0, 0, 0, 0, 0, 0]);
490        Self(raw)
491    }
492
493    /// Decode the 6-byte value field as `(global_admin, local_admin)`.
494    ///
495    /// Handles all three RFC 4360 two-part layouts (2-octet AS, IPv4, 4-octet
496    /// AS). Returns raw u32 values — the caller decides how to interpret
497    /// `global_admin` (ASN vs IPv4 address) based on `type_byte()`.
498    fn decode_two_part(self) -> Option<(u32, u32)> {
499        let v = self.value_bytes();
500        let t = self.type_byte() & 0x3F; // mask off high two bits
501        match t {
502            // 2-octet AS specific: AS(2) + value(4)
503            0x00 => {
504                let global = u32::from(u16::from_be_bytes([v[0], v[1]]));
505                let local = u32::from_be_bytes([v[2], v[3], v[4], v[5]]);
506                Some((global, local))
507            }
508            // IPv4 Address specific (0x01) or 4-octet AS specific (0x02): 4 + 2
509            0x01 | 0x02 => {
510                let global = u32::from_be_bytes([v[0], v[1], v[2], v[3]]);
511                let local = u32::from(u16::from_be_bytes([v[4], v[5]]));
512                Some((global, local))
513            }
514            _ => None,
515        }
516    }
517}
518
519impl fmt::Display for ExtendedCommunity {
520    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
521        let is_ipv4 = self.type_byte() & 0x3F == 0x01;
522        if let Some((g, l)) = self.route_target() {
523            if is_ipv4 {
524                write!(f, "RT:{}:{l}", Ipv4Addr::from(g))
525            } else {
526                write!(f, "RT:{g}:{l}")
527            }
528        } else if let Some((g, l)) = self.route_origin() {
529            if is_ipv4 {
530                write!(f, "RO:{}:{l}", Ipv4Addr::from(g))
531            } else {
532                write!(f, "RO:{g}:{l}")
533            }
534        } else {
535            write!(f, "0x{:016x}", self.0)
536        }
537    }
538}
539
540/// RFC 8092 Large Community — 12-byte value: `(global_admin, local_data1, local_data2)`.
541///
542/// Each field is a 32-bit unsigned integer. Display format: `"65001:100:200"`.
543#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
544pub struct LargeCommunity {
545    /// Global administrator (typically ASN).
546    pub global_admin: u32,
547    /// First local data part.
548    pub local_data1: u32,
549    /// Second local data part.
550    pub local_data2: u32,
551}
552
553impl LargeCommunity {
554    /// Create a new large community value.
555    #[must_use]
556    pub fn new(global_admin: u32, local_data1: u32, local_data2: u32) -> Self {
557        Self {
558            global_admin,
559            local_data1,
560            local_data2,
561        }
562    }
563}
564
565impl fmt::Display for LargeCommunity {
566    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
567        write!(
568            f,
569            "{}:{}:{}",
570            self.global_admin, self.local_data1, self.local_data2
571        )
572    }
573}
574
575/// A known path attribute or raw preserved bytes.
576///
577/// Known attributes are decoded into typed variants. Unknown attributes
578/// are preserved as `RawAttribute` for pass-through with the Partial bit.
579#[derive(Debug, Clone, PartialEq, Eq, Hash)]
580pub enum PathAttribute {
581    /// `ORIGIN` attribute (type 1).
582    Origin(Origin),
583    /// `AS_PATH` attribute (type 2).
584    AsPath(AsPath),
585    /// `NEXT_HOP` attribute (type 3).
586    NextHop(Ipv4Addr),
587    /// `LOCAL_PREF` attribute (type 5).
588    LocalPref(u32),
589    /// `MULTI_EXIT_DISC` attribute (type 4).
590    Med(u32),
591    /// RFC 1997 COMMUNITIES — each u32 is high16=ASN, low16=value.
592    Communities(Vec<u32>),
593    /// RFC 4360 EXTENDED COMMUNITIES.
594    ExtendedCommunities(Vec<ExtendedCommunity>),
595    /// RFC 8092 LARGE COMMUNITIES.
596    LargeCommunities(Vec<LargeCommunity>),
597    /// RFC 4456 `ORIGINATOR_ID` — original router-id of the route.
598    OriginatorId(Ipv4Addr),
599    /// RFC 4456 `CLUSTER_LIST` — list of cluster-ids traversed.
600    ClusterList(Vec<Ipv4Addr>),
601    /// RFC 4760 `MP_REACH_NLRI`.
602    MpReachNlri(MpReachNlri),
603    /// RFC 4760 `MP_UNREACH_NLRI`.
604    MpUnreachNlri(MpUnreachNlri),
605    /// RFC 6514 §5 `PMSI Tunnel` — used by EVPN Type 3 IMET for
606    /// ingress-replication BUM forwarding.
607    PmsiTunnel(crate::pmsi::PmsiTunnel),
608    /// Unknown or unrecognized attribute, preserved for re-advertisement.
609    Unknown(RawAttribute),
610}
611
612impl PathAttribute {
613    /// Return the type code of this attribute.
614    #[must_use]
615    pub fn type_code(&self) -> u8 {
616        match self {
617            Self::Origin(_) => attr_type::ORIGIN,
618            Self::AsPath(_) => attr_type::AS_PATH,
619            Self::NextHop(_) => attr_type::NEXT_HOP,
620            Self::LocalPref(_) => attr_type::LOCAL_PREF,
621            Self::Med(_) => attr_type::MULTI_EXIT_DISC,
622            Self::Communities(_) => attr_type::COMMUNITIES,
623            Self::OriginatorId(_) => attr_type::ORIGINATOR_ID,
624            Self::ClusterList(_) => attr_type::CLUSTER_LIST,
625            Self::ExtendedCommunities(_) => attr_type::EXTENDED_COMMUNITIES,
626            Self::LargeCommunities(_) => attr_type::LARGE_COMMUNITIES,
627            Self::MpReachNlri(_) => attr_type::MP_REACH_NLRI,
628            Self::MpUnreachNlri(_) => attr_type::MP_UNREACH_NLRI,
629            Self::PmsiTunnel(_) => attr_type::PMSI_TUNNEL,
630            Self::Unknown(raw) => raw.type_code,
631        }
632    }
633
634    /// Return the wire flags for this attribute.
635    #[must_use]
636    pub fn flags(&self) -> u8 {
637        match self {
638            Self::Origin(_) | Self::AsPath(_) | Self::NextHop(_) | Self::LocalPref(_) => {
639                attr_flags::TRANSITIVE
640            }
641            Self::Med(_)
642            | Self::OriginatorId(_)
643            | Self::ClusterList(_)
644            | Self::MpReachNlri(_)
645            | Self::MpUnreachNlri(_) => attr_flags::OPTIONAL,
646            Self::Communities(_)
647            | Self::ExtendedCommunities(_)
648            | Self::LargeCommunities(_)
649            | Self::PmsiTunnel(_) => attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
650            Self::Unknown(raw) => raw.flags,
651        }
652    }
653}
654
655/// Raw attribute preserved for pass-through (RFC 4271 §5).
656///
657/// On re-advertisement, the Partial bit (0x20) is OR'd into `flags`.
658/// All other flags and bytes are preserved unchanged.
659#[derive(Debug, Clone, PartialEq, Eq, Hash)]
660pub struct RawAttribute {
661    /// Attribute flags byte (optional, transitive, partial, extended-length).
662    pub flags: u8,
663    /// Attribute type code.
664    pub type_code: u8,
665    /// Raw attribute value bytes.
666    pub data: Bytes,
667}
668
669/// Decode path attributes from wire bytes (RFC 4271 §4.3).
670///
671/// Each attribute is: flags(1) + type(1) + length(1 or 2) + value.
672/// The Extended Length flag determines 1-byte vs 2-byte length.
673///
674/// `four_octet_as` controls whether AS numbers in `AS_PATH` are 2 or 4 bytes.
675///
676/// # Errors
677///
678/// Returns `DecodeError` on truncated data or malformed attribute values.
679pub fn decode_path_attributes(
680    mut buf: &[u8],
681    four_octet_as: bool,
682    add_path_families: &[(Afi, Safi)],
683) -> Result<Vec<PathAttribute>, DecodeError> {
684    let mut attrs = Vec::new();
685
686    while !buf.is_empty() {
687        // Need at least flags(1) + type(1) = 2
688        if buf.len() < 2 {
689            return Err(DecodeError::MalformedField {
690                message_type: "UPDATE",
691                detail: "truncated attribute header".to_string(),
692            });
693        }
694
695        let flags = buf[0];
696        let type_code = buf[1];
697        buf = &buf[2..];
698
699        let extended = (flags & attr_flags::EXTENDED_LENGTH) != 0;
700        let value_len = if extended {
701            if buf.len() < 2 {
702                return Err(DecodeError::MalformedField {
703                    message_type: "UPDATE",
704                    detail: "truncated extended-length attribute".to_string(),
705                });
706            }
707            let len = u16::from_be_bytes([buf[0], buf[1]]) as usize;
708            buf = &buf[2..];
709            len
710        } else {
711            if buf.is_empty() {
712                return Err(DecodeError::MalformedField {
713                    message_type: "UPDATE",
714                    detail: "truncated attribute length".to_string(),
715                });
716            }
717            let len = buf[0] as usize;
718            buf = &buf[1..];
719            len
720        };
721
722        if buf.len() < value_len {
723            return Err(DecodeError::MalformedField {
724                message_type: "UPDATE",
725                detail: format!(
726                    "attribute type {type_code} value truncated: need {value_len}, have {}",
727                    buf.len()
728                ),
729            });
730        }
731
732        let value = &buf[..value_len];
733        buf = &buf[value_len..];
734
735        let attr =
736            decode_attribute_value(flags, type_code, value, four_octet_as, add_path_families)?;
737        attrs.push(attr);
738    }
739
740    Ok(attrs)
741}
742
743/// Decode a single attribute value given its flags, type code, and raw bytes.
744#[expect(clippy::too_many_lines)]
745fn decode_attribute_value(
746    flags: u8,
747    type_code: u8,
748    value: &[u8],
749    four_octet_as: bool,
750    add_path_families: &[(Afi, Safi)],
751) -> Result<PathAttribute, DecodeError> {
752    // Validate Optional + Transitive flags for known attribute types (RFC 4271 §6.3).
753    let flags_mask = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
754    if let Some(expected) = expected_flags(type_code)
755        && (flags & flags_mask) != expected
756    {
757        return Err(DecodeError::UpdateAttributeError {
758            subcode: update_subcode::ATTRIBUTE_FLAGS_ERROR,
759            data: attr_error_data(flags, type_code, value),
760            detail: format!(
761                "type {} flags {:#04x} (expected {:#04x})",
762                type_code,
763                flags & flags_mask,
764                expected
765            ),
766        });
767    }
768
769    match type_code {
770        attr_type::ORIGIN => {
771            if value.len() != 1 {
772                return Err(DecodeError::UpdateAttributeError {
773                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
774                    data: attr_error_data(flags, type_code, value),
775                    detail: format!("ORIGIN length {} (expected 1)", value.len()),
776                });
777            }
778            match Origin::from_u8(value[0]) {
779                Some(origin) => Ok(PathAttribute::Origin(origin)),
780                None => Err(DecodeError::UpdateAttributeError {
781                    subcode: update_subcode::INVALID_ORIGIN,
782                    data: attr_error_data(flags, type_code, value),
783                    detail: format!("invalid ORIGIN value {}", value[0]),
784                }),
785            }
786        }
787
788        attr_type::AS_PATH => {
789            let segments = decode_as_path(value, four_octet_as).map_err(|e| {
790                DecodeError::UpdateAttributeError {
791                    subcode: update_subcode::MALFORMED_AS_PATH,
792                    data: attr_error_data(flags, type_code, value),
793                    detail: e.to_string(),
794                }
795            })?;
796            Ok(PathAttribute::AsPath(AsPath { segments }))
797        }
798
799        attr_type::NEXT_HOP => {
800            if value.len() != 4 {
801                return Err(DecodeError::UpdateAttributeError {
802                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
803                    data: attr_error_data(flags, type_code, value),
804                    detail: format!("NEXT_HOP length {} (expected 4)", value.len()),
805                });
806            }
807            let addr = Ipv4Addr::new(value[0], value[1], value[2], value[3]);
808            Ok(PathAttribute::NextHop(addr))
809        }
810
811        attr_type::MULTI_EXIT_DISC => {
812            if value.len() != 4 {
813                return Err(DecodeError::UpdateAttributeError {
814                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
815                    data: attr_error_data(flags, type_code, value),
816                    detail: format!("MED length {} (expected 4)", value.len()),
817                });
818            }
819            let med = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
820            Ok(PathAttribute::Med(med))
821        }
822
823        attr_type::LOCAL_PREF => {
824            if value.len() != 4 {
825                return Err(DecodeError::UpdateAttributeError {
826                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
827                    data: attr_error_data(flags, type_code, value),
828                    detail: format!("LOCAL_PREF length {} (expected 4)", value.len()),
829                });
830            }
831            let lp = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
832            Ok(PathAttribute::LocalPref(lp))
833        }
834
835        attr_type::COMMUNITIES => {
836            if !value.len().is_multiple_of(4) {
837                return Err(DecodeError::UpdateAttributeError {
838                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
839                    data: attr_error_data(flags, type_code, value),
840                    detail: format!("COMMUNITIES length {} not a multiple of 4", value.len()),
841                });
842            }
843            let communities = value
844                .chunks_exact(4)
845                .map(|c| u32::from_be_bytes([c[0], c[1], c[2], c[3]]))
846                .collect();
847            Ok(PathAttribute::Communities(communities))
848        }
849
850        attr_type::EXTENDED_COMMUNITIES => {
851            if !value.len().is_multiple_of(8) {
852                return Err(DecodeError::UpdateAttributeError {
853                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
854                    data: attr_error_data(flags, type_code, value),
855                    detail: format!(
856                        "EXTENDED_COMMUNITIES length {} not a multiple of 8",
857                        value.len()
858                    ),
859                });
860            }
861            let communities = value
862                .chunks_exact(8)
863                .map(|c| {
864                    ExtendedCommunity::new(u64::from_be_bytes([
865                        c[0], c[1], c[2], c[3], c[4], c[5], c[6], c[7],
866                    ]))
867                })
868                .collect();
869            Ok(PathAttribute::ExtendedCommunities(communities))
870        }
871
872        attr_type::ORIGINATOR_ID => {
873            if value.len() != 4 {
874                return Err(DecodeError::UpdateAttributeError {
875                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
876                    data: attr_error_data(flags, type_code, value),
877                    detail: format!("ORIGINATOR_ID length {} (expected 4)", value.len()),
878                });
879            }
880            let addr = Ipv4Addr::new(value[0], value[1], value[2], value[3]);
881            Ok(PathAttribute::OriginatorId(addr))
882        }
883
884        attr_type::CLUSTER_LIST => {
885            if !value.len().is_multiple_of(4) {
886                return Err(DecodeError::UpdateAttributeError {
887                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
888                    data: attr_error_data(flags, type_code, value),
889                    detail: format!("CLUSTER_LIST length {} not a multiple of 4", value.len()),
890                });
891            }
892            let ids = value
893                .chunks_exact(4)
894                .map(|c| Ipv4Addr::new(c[0], c[1], c[2], c[3]))
895                .collect();
896            Ok(PathAttribute::ClusterList(ids))
897        }
898
899        attr_type::LARGE_COMMUNITIES => {
900            if value.is_empty() || !value.len().is_multiple_of(12) {
901                return Err(DecodeError::UpdateAttributeError {
902                    subcode: update_subcode::ATTRIBUTE_LENGTH_ERROR,
903                    data: attr_error_data(flags, type_code, value),
904                    detail: format!(
905                        "LARGE_COMMUNITIES length {} invalid (must be non-zero multiple of 12)",
906                        value.len()
907                    ),
908                });
909            }
910            let communities = value
911                .chunks_exact(12)
912                .map(|c| {
913                    LargeCommunity::new(
914                        u32::from_be_bytes([c[0], c[1], c[2], c[3]]),
915                        u32::from_be_bytes([c[4], c[5], c[6], c[7]]),
916                        u32::from_be_bytes([c[8], c[9], c[10], c[11]]),
917                    )
918                })
919                .collect();
920            Ok(PathAttribute::LargeCommunities(communities))
921        }
922
923        attr_type::MP_REACH_NLRI => decode_mp_reach_nlri(value, add_path_families),
924        attr_type::MP_UNREACH_NLRI => decode_mp_unreach_nlri(value, add_path_families),
925
926        attr_type::PMSI_TUNNEL => {
927            let pmsi = crate::pmsi::PmsiTunnel::decode(value)?;
928            Ok(PathAttribute::PmsiTunnel(pmsi))
929        }
930
931        // ATOMIC_AGGREGATE, AGGREGATOR, and any unknown type → RawAttribute
932        _ => Ok(PathAttribute::Unknown(RawAttribute {
933            flags,
934            type_code,
935            data: Bytes::copy_from_slice(value),
936        })),
937    }
938}
939
940/// Decode `MP_REACH_NLRI` (type 14) attribute value.
941///
942/// Wire layout (RFC 4760 §3):
943///   AFI (2) | SAFI (1) | NH-Len (1) | Next Hop (variable) | Reserved (1) | NLRI (variable)
944#[expect(clippy::too_many_lines)]
945fn decode_mp_reach_nlri(
946    value: &[u8],
947    add_path_families: &[(Afi, Safi)],
948) -> Result<PathAttribute, DecodeError> {
949    if value.len() < 5 {
950        return Err(DecodeError::MalformedField {
951            message_type: "UPDATE",
952            detail: format!("MP_REACH_NLRI too short: {} bytes", value.len()),
953        });
954    }
955
956    let afi_raw = u16::from_be_bytes([value[0], value[1]]);
957    let safi_raw = value[2];
958    let nh_len = value[3] as usize;
959
960    let afi = Afi::from_u16(afi_raw).ok_or_else(|| DecodeError::MalformedField {
961        message_type: "UPDATE",
962        detail: format!("MP_REACH_NLRI unsupported AFI {afi_raw}"),
963    })?;
964    let safi = Safi::from_u8(safi_raw).ok_or_else(|| DecodeError::MalformedField {
965        message_type: "UPDATE",
966        detail: format!("MP_REACH_NLRI unsupported SAFI {safi_raw}"),
967    })?;
968
969    // 4 bytes for AFI+SAFI+NH-Len, then nh_len bytes, then 1 reserved byte
970    if value.len() < 4 + nh_len + 1 {
971        return Err(DecodeError::MalformedField {
972            message_type: "UPDATE",
973            detail: format!(
974                "MP_REACH_NLRI truncated: NH-Len={nh_len}, have {} bytes total",
975                value.len()
976            ),
977        });
978    }
979
980    let nh_bytes = &value[4..4 + nh_len];
981    // FlowSpec (SAFI 133): NH length is 0 — no next-hop for filter rules
982    let mut link_local_next_hop: Option<Ipv6Addr> = None;
983    let next_hop = if safi == Safi::FlowSpec {
984        if nh_len != 0 {
985            return Err(DecodeError::MalformedField {
986                message_type: "UPDATE",
987                detail: format!("MP_REACH_NLRI FlowSpec next-hop length {nh_len} (expected 0)"),
988            });
989        }
990        IpAddr::V4(Ipv4Addr::UNSPECIFIED)
991    } else {
992        match afi {
993            Afi::Ipv4 => match nh_len {
994                4 => IpAddr::V4(Ipv4Addr::new(
995                    nh_bytes[0],
996                    nh_bytes[1],
997                    nh_bytes[2],
998                    nh_bytes[3],
999                )),
1000                16 | 32 => {
1001                    let mut octets = [0u8; 16];
1002                    octets.copy_from_slice(&nh_bytes[..16]);
1003                    if nh_len == 32 {
1004                        let mut ll = [0u8; 16];
1005                        ll.copy_from_slice(&nh_bytes[16..32]);
1006                        link_local_next_hop = Some(Ipv6Addr::from(ll));
1007                    }
1008                    IpAddr::V6(Ipv6Addr::from(octets))
1009                }
1010                _ => {
1011                    return Err(DecodeError::MalformedField {
1012                        message_type: "UPDATE",
1013                        detail: format!(
1014                            "MP_REACH_NLRI IPv4 next-hop length {nh_len} (expected 4, 16, or 32)"
1015                        ),
1016                    });
1017                }
1018            },
1019            Afi::Ipv6 => {
1020                if nh_len != 16 && nh_len != 32 {
1021                    return Err(DecodeError::MalformedField {
1022                        message_type: "UPDATE",
1023                        detail: format!(
1024                            "MP_REACH_NLRI IPv6 next-hop length {nh_len} (expected 16 or 32)"
1025                        ),
1026                    });
1027                }
1028                let mut octets = [0u8; 16];
1029                octets.copy_from_slice(&nh_bytes[..16]);
1030                if nh_len == 32 {
1031                    let mut ll = [0u8; 16];
1032                    ll.copy_from_slice(&nh_bytes[16..32]);
1033                    link_local_next_hop = Some(Ipv6Addr::from(ll));
1034                }
1035                IpAddr::V6(Ipv6Addr::from(octets))
1036            }
1037            Afi::L2Vpn => match nh_len {
1038                4 => IpAddr::V4(Ipv4Addr::new(
1039                    nh_bytes[0],
1040                    nh_bytes[1],
1041                    nh_bytes[2],
1042                    nh_bytes[3],
1043                )),
1044                16 => {
1045                    let mut octets = [0u8; 16];
1046                    octets.copy_from_slice(&nh_bytes[..16]);
1047                    IpAddr::V6(Ipv6Addr::from(octets))
1048                }
1049                _ => {
1050                    return Err(DecodeError::MalformedField {
1051                        message_type: "UPDATE",
1052                        detail: format!(
1053                            "MP_REACH_NLRI L2VPN next-hop length {nh_len} (expected 4 or 16)"
1054                        ),
1055                    });
1056                }
1057            },
1058        }
1059    };
1060
1061    // Skip reserved byte
1062    let nlri_start = 4 + nh_len + 1;
1063    let nlri_bytes = &value[nlri_start..];
1064
1065    // FlowSpec (SAFI 133): NLRI is FlowSpec rules, not prefixes
1066    if safi == Safi::FlowSpec {
1067        let flowspec_rules = crate::flowspec::decode_flowspec_nlri(nlri_bytes, afi)?;
1068        return Ok(PathAttribute::MpReachNlri(MpReachNlri {
1069            afi,
1070            safi,
1071            next_hop,
1072            link_local_next_hop,
1073            announced: vec![],
1074            flowspec_announced: flowspec_rules,
1075            evpn_announced: vec![],
1076        }));
1077    }
1078
1079    // EVPN (AFI 25 / SAFI 70): NLRI is typed EVPN routes, not prefixes
1080    if afi == Afi::L2Vpn && safi == Safi::Evpn {
1081        let routes = crate::evpn::decode_evpn_nlri(nlri_bytes)?;
1082        return Ok(PathAttribute::MpReachNlri(MpReachNlri {
1083            afi,
1084            safi,
1085            next_hop,
1086            link_local_next_hop,
1087            announced: vec![],
1088            flowspec_announced: vec![],
1089            evpn_announced: routes,
1090        }));
1091    }
1092
1093    // SAFI 70 (EVPN) is only defined for AFI 25 (L2VPN). Reject any other
1094    // AFI explicitly so the unicast NLRI fallthrough below cannot
1095    // misinterpret the typed EVPN payload as a prefix list.
1096    if safi == Safi::Evpn {
1097        return Err(DecodeError::MalformedField {
1098            message_type: "UPDATE",
1099            detail: format!(
1100                "MP_REACH_NLRI SAFI EVPN with non-L2VPN AFI {} (only AFI L2VPN supported)",
1101                afi as u16
1102            ),
1103        });
1104    }
1105
1106    let add_path = add_path_families.contains(&(afi, safi));
1107    let announced = match (afi, add_path) {
1108        (Afi::Ipv4, false) => crate::nlri::decode_nlri(nlri_bytes)?
1109            .into_iter()
1110            .map(|p| NlriEntry {
1111                path_id: 0,
1112                prefix: Prefix::V4(p),
1113            })
1114            .collect(),
1115        (Afi::Ipv4, true) => crate::nlri::decode_nlri_addpath(nlri_bytes)?
1116            .into_iter()
1117            .map(|e| NlriEntry {
1118                path_id: e.path_id,
1119                prefix: Prefix::V4(e.prefix),
1120            })
1121            .collect(),
1122        (Afi::Ipv6, false) => crate::nlri::decode_ipv6_nlri(nlri_bytes)?
1123            .into_iter()
1124            .map(|p| NlriEntry {
1125                path_id: 0,
1126                prefix: Prefix::V6(p),
1127            })
1128            .collect(),
1129        (Afi::Ipv6, true) => crate::nlri::decode_ipv6_nlri_addpath(nlri_bytes)?,
1130        (Afi::L2Vpn, _) => {
1131            return Err(DecodeError::MalformedField {
1132                message_type: "UPDATE",
1133                detail: format!(
1134                    "MP_REACH_NLRI L2VPN with unsupported SAFI {} (only EVPN supported)",
1135                    safi as u8
1136                ),
1137            });
1138        }
1139    };
1140
1141    Ok(PathAttribute::MpReachNlri(MpReachNlri {
1142        afi,
1143        safi,
1144        next_hop,
1145        link_local_next_hop,
1146        announced,
1147        flowspec_announced: vec![],
1148        evpn_announced: vec![],
1149    }))
1150}
1151
1152/// Decode `MP_UNREACH_NLRI` (type 15) attribute value.
1153///
1154/// Wire layout (RFC 4760 §4):
1155///   AFI (2) | SAFI (1) | Withdrawn Routes (variable)
1156fn decode_mp_unreach_nlri(
1157    value: &[u8],
1158    add_path_families: &[(Afi, Safi)],
1159) -> Result<PathAttribute, DecodeError> {
1160    if value.len() < 3 {
1161        return Err(DecodeError::MalformedField {
1162            message_type: "UPDATE",
1163            detail: format!("MP_UNREACH_NLRI too short: {} bytes", value.len()),
1164        });
1165    }
1166
1167    let afi_raw = u16::from_be_bytes([value[0], value[1]]);
1168    let safi_raw = value[2];
1169
1170    let afi = Afi::from_u16(afi_raw).ok_or_else(|| DecodeError::MalformedField {
1171        message_type: "UPDATE",
1172        detail: format!("MP_UNREACH_NLRI unsupported AFI {afi_raw}"),
1173    })?;
1174    let safi = Safi::from_u8(safi_raw).ok_or_else(|| DecodeError::MalformedField {
1175        message_type: "UPDATE",
1176        detail: format!("MP_UNREACH_NLRI unsupported SAFI {safi_raw}"),
1177    })?;
1178
1179    let withdrawn_bytes = &value[3..];
1180
1181    // FlowSpec (SAFI 133): withdrawn is FlowSpec rules
1182    if safi == Safi::FlowSpec {
1183        let flowspec_rules = crate::flowspec::decode_flowspec_nlri(withdrawn_bytes, afi)?;
1184        return Ok(PathAttribute::MpUnreachNlri(MpUnreachNlri {
1185            afi,
1186            safi,
1187            withdrawn: vec![],
1188            flowspec_withdrawn: flowspec_rules,
1189            evpn_withdrawn: vec![],
1190        }));
1191    }
1192
1193    // EVPN (AFI 25 / SAFI 70): withdrawn is typed EVPN routes, not prefixes
1194    if afi == Afi::L2Vpn && safi == Safi::Evpn {
1195        let routes = crate::evpn::decode_evpn_nlri(withdrawn_bytes)?;
1196        return Ok(PathAttribute::MpUnreachNlri(MpUnreachNlri {
1197            afi,
1198            safi,
1199            withdrawn: vec![],
1200            flowspec_withdrawn: vec![],
1201            evpn_withdrawn: routes,
1202        }));
1203    }
1204
1205    // SAFI 70 (EVPN) is only defined for AFI 25 (L2VPN). Reject any other
1206    // AFI explicitly so the unicast NLRI fallthrough below cannot
1207    // misinterpret the typed EVPN payload as a prefix list.
1208    if safi == Safi::Evpn {
1209        return Err(DecodeError::MalformedField {
1210            message_type: "UPDATE",
1211            detail: format!(
1212                "MP_UNREACH_NLRI SAFI EVPN with non-L2VPN AFI {} (only AFI L2VPN supported)",
1213                afi as u16
1214            ),
1215        });
1216    }
1217
1218    let add_path = add_path_families.contains(&(afi, safi));
1219    let withdrawn = match (afi, add_path) {
1220        (Afi::Ipv4, false) => crate::nlri::decode_nlri(withdrawn_bytes)?
1221            .into_iter()
1222            .map(|p| NlriEntry {
1223                path_id: 0,
1224                prefix: Prefix::V4(p),
1225            })
1226            .collect(),
1227        (Afi::Ipv4, true) => crate::nlri::decode_nlri_addpath(withdrawn_bytes)?
1228            .into_iter()
1229            .map(|e| NlriEntry {
1230                path_id: e.path_id,
1231                prefix: Prefix::V4(e.prefix),
1232            })
1233            .collect(),
1234        (Afi::Ipv6, false) => crate::nlri::decode_ipv6_nlri(withdrawn_bytes)?
1235            .into_iter()
1236            .map(|p| NlriEntry {
1237                path_id: 0,
1238                prefix: Prefix::V6(p),
1239            })
1240            .collect(),
1241        (Afi::Ipv6, true) => crate::nlri::decode_ipv6_nlri_addpath(withdrawn_bytes)?,
1242        (Afi::L2Vpn, _) => {
1243            return Err(DecodeError::MalformedField {
1244                message_type: "UPDATE",
1245                detail: format!(
1246                    "MP_UNREACH_NLRI L2VPN with unsupported SAFI {} (only EVPN supported)",
1247                    safi as u8
1248                ),
1249            });
1250        }
1251    };
1252
1253    Ok(PathAttribute::MpUnreachNlri(MpUnreachNlri {
1254        afi,
1255        safi,
1256        withdrawn,
1257        flowspec_withdrawn: vec![],
1258        evpn_withdrawn: vec![],
1259    }))
1260}
1261
1262/// Decode `AS_PATH` segments from the attribute value bytes.
1263fn decode_as_path(mut buf: &[u8], four_octet_as: bool) -> Result<Vec<AsPathSegment>, DecodeError> {
1264    let as_size: usize = if four_octet_as { 4 } else { 2 };
1265    let mut segments = Vec::new();
1266
1267    while !buf.is_empty() {
1268        if buf.len() < 2 {
1269            return Err(DecodeError::MalformedField {
1270                message_type: "UPDATE",
1271                detail: "truncated AS_PATH segment header".to_string(),
1272            });
1273        }
1274
1275        let seg_type = buf[0];
1276        let seg_count = buf[1] as usize;
1277        buf = &buf[2..];
1278
1279        let needed = seg_count * as_size;
1280        if buf.len() < needed {
1281            return Err(DecodeError::MalformedField {
1282                message_type: "UPDATE",
1283                detail: format!(
1284                    "AS_PATH segment truncated: need {needed} bytes for {seg_count} ASNs, have {}",
1285                    buf.len()
1286                ),
1287            });
1288        }
1289
1290        let mut asns = Vec::with_capacity(seg_count);
1291        for _ in 0..seg_count {
1292            let asn = if four_octet_as {
1293                let v = u32::from_be_bytes([buf[0], buf[1], buf[2], buf[3]]);
1294                buf = &buf[4..];
1295                v
1296            } else {
1297                let v = u32::from(u16::from_be_bytes([buf[0], buf[1]]));
1298                buf = &buf[2..];
1299                v
1300            };
1301            asns.push(asn);
1302        }
1303
1304        match seg_type {
1305            as_path_segment::AS_SET => segments.push(AsPathSegment::AsSet(asns)),
1306            as_path_segment::AS_SEQUENCE => segments.push(AsPathSegment::AsSequence(asns)),
1307            _ => {
1308                return Err(DecodeError::MalformedField {
1309                    message_type: "UPDATE",
1310                    detail: format!("unknown AS_PATH segment type {seg_type}"),
1311                });
1312            }
1313        }
1314    }
1315
1316    Ok(segments)
1317}
1318
1319/// Build the attribute-triplet (flags + type + length + value) used as
1320/// NOTIFICATION data in UPDATE error subcodes per RFC 4271 §6.3.
1321pub(crate) fn attr_error_data(flags: u8, type_code: u8, value: &[u8]) -> Vec<u8> {
1322    let mut buf = Vec::with_capacity(3 + value.len());
1323    if value.len() > 255 {
1324        buf.push(flags | attr_flags::EXTENDED_LENGTH);
1325        buf.push(type_code);
1326        #[expect(clippy::cast_possible_truncation)]
1327        let len = value.len() as u16;
1328        buf.extend_from_slice(&len.to_be_bytes());
1329    } else {
1330        buf.push(flags);
1331        buf.push(type_code);
1332        #[expect(clippy::cast_possible_truncation)]
1333        buf.push(value.len() as u8);
1334    }
1335    buf.extend_from_slice(value);
1336    buf
1337}
1338
1339/// Return the expected Optional + Transitive flags for known attribute types.
1340/// Returns `None` for unrecognized types (no validation performed).
1341fn expected_flags(type_code: u8) -> Option<u8> {
1342    match type_code {
1343        // Well-known mandatory/discretionary: Optional=0, Transitive=1
1344        attr_type::ORIGIN
1345        | attr_type::AS_PATH
1346        | attr_type::NEXT_HOP
1347        | attr_type::LOCAL_PREF
1348        | attr_type::ATOMIC_AGGREGATE => Some(attr_flags::TRANSITIVE),
1349        // Optional non-transitive (RFC 4760 §3/§4: MP_REACH/UNREACH are non-transitive;
1350        // RFC 4456: ORIGINATOR_ID and CLUSTER_LIST are optional non-transitive)
1351        attr_type::MULTI_EXIT_DISC
1352        | attr_type::ORIGINATOR_ID
1353        | attr_type::CLUSTER_LIST
1354        | attr_type::MP_REACH_NLRI
1355        | attr_type::MP_UNREACH_NLRI => Some(attr_flags::OPTIONAL),
1356        // Optional transitive
1357        attr_type::AGGREGATOR
1358        | attr_type::COMMUNITIES
1359        | attr_type::EXTENDED_COMMUNITIES
1360        | attr_type::LARGE_COMMUNITIES
1361        | attr_type::PMSI_TUNNEL => Some(attr_flags::OPTIONAL | attr_flags::TRANSITIVE),
1362        _ => None,
1363    }
1364}
1365
1366/// Encode path attributes to wire bytes.
1367///
1368/// `four_octet_as` controls whether AS numbers in `AS_PATH` are 2 or 4 bytes.
1369/// Encode a list of path attributes into wire format.
1370///
1371/// When `add_path_mp` is true, `MP_REACH_NLRI` and `MP_UNREACH_NLRI` NLRI
1372/// entries include 4-byte path IDs per RFC 7911.
1373#[expect(
1374    clippy::too_many_lines,
1375    reason = "dispatch arms are inherently O(variants); each new path attribute adds a small block"
1376)]
1377pub fn encode_path_attributes(
1378    attrs: &[PathAttribute],
1379    buf: &mut Vec<u8>,
1380    four_octet_as: bool,
1381    add_path_mp: bool,
1382) {
1383    for attr in attrs {
1384        let mut value = Vec::new();
1385        let flags;
1386        let type_code;
1387
1388        match attr {
1389            PathAttribute::Origin(origin) => {
1390                flags = attr_flags::TRANSITIVE;
1391                type_code = attr_type::ORIGIN;
1392                value.push(*origin as u8);
1393            }
1394            PathAttribute::AsPath(as_path) => {
1395                flags = attr_flags::TRANSITIVE;
1396                type_code = attr_type::AS_PATH;
1397                encode_as_path(as_path, &mut value, four_octet_as);
1398            }
1399            PathAttribute::NextHop(addr) => {
1400                flags = attr_flags::TRANSITIVE;
1401                type_code = attr_type::NEXT_HOP;
1402                value.extend_from_slice(&addr.octets());
1403            }
1404            PathAttribute::Med(med) => {
1405                flags = attr_flags::OPTIONAL;
1406                type_code = attr_type::MULTI_EXIT_DISC;
1407                value.extend_from_slice(&med.to_be_bytes());
1408            }
1409            PathAttribute::LocalPref(lp) => {
1410                flags = attr_flags::TRANSITIVE;
1411                type_code = attr_type::LOCAL_PREF;
1412                value.extend_from_slice(&lp.to_be_bytes());
1413            }
1414            PathAttribute::Communities(communities) => {
1415                flags = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1416                type_code = attr_type::COMMUNITIES;
1417                for &c in communities {
1418                    value.extend_from_slice(&c.to_be_bytes());
1419                }
1420            }
1421            PathAttribute::ExtendedCommunities(communities) => {
1422                flags = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1423                type_code = attr_type::EXTENDED_COMMUNITIES;
1424                for &c in communities {
1425                    value.extend_from_slice(&c.as_u64().to_be_bytes());
1426                }
1427            }
1428            PathAttribute::LargeCommunities(communities) => {
1429                flags = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1430                type_code = attr_type::LARGE_COMMUNITIES;
1431                for &c in communities {
1432                    value.extend_from_slice(&c.global_admin.to_be_bytes());
1433                    value.extend_from_slice(&c.local_data1.to_be_bytes());
1434                    value.extend_from_slice(&c.local_data2.to_be_bytes());
1435                }
1436            }
1437            PathAttribute::OriginatorId(addr) => {
1438                flags = attr_flags::OPTIONAL;
1439                type_code = attr_type::ORIGINATOR_ID;
1440                value.extend_from_slice(&addr.octets());
1441            }
1442            PathAttribute::ClusterList(ids) => {
1443                flags = attr_flags::OPTIONAL;
1444                type_code = attr_type::CLUSTER_LIST;
1445                for id in ids {
1446                    value.extend_from_slice(&id.octets());
1447                }
1448            }
1449            PathAttribute::MpReachNlri(mp) => {
1450                flags = attr_flags::OPTIONAL;
1451                type_code = attr_type::MP_REACH_NLRI;
1452                encode_mp_reach_nlri(mp, &mut value, add_path_mp);
1453            }
1454            PathAttribute::MpUnreachNlri(mp) => {
1455                flags = attr_flags::OPTIONAL;
1456                type_code = attr_type::MP_UNREACH_NLRI;
1457                encode_mp_unreach_nlri(mp, &mut value, add_path_mp);
1458            }
1459            PathAttribute::PmsiTunnel(pmsi) => {
1460                // RFC 6514 §5: Optional + Transitive.
1461                (flags, type_code) = (
1462                    attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
1463                    attr_type::PMSI_TUNNEL,
1464                );
1465                pmsi.encode(&mut value);
1466            }
1467            PathAttribute::Unknown(raw) => {
1468                // RFC 4271 §5: unrecognized *optional* transitive attributes
1469                // must be propagated with the Partial bit set. Well-known
1470                // transitive attributes (OPTIONAL=0) must NOT get PARTIAL.
1471                let optional_transitive = attr_flags::OPTIONAL | attr_flags::TRANSITIVE;
1472                flags = if (raw.flags & optional_transitive) == optional_transitive {
1473                    raw.flags | attr_flags::PARTIAL
1474                } else {
1475                    raw.flags
1476                };
1477                type_code = raw.type_code;
1478                value.extend_from_slice(&raw.data);
1479            }
1480        }
1481
1482        // Use extended length if value > 255 bytes
1483        if value.len() > 255 {
1484            buf.push(flags | attr_flags::EXTENDED_LENGTH);
1485            buf.push(type_code);
1486            #[expect(clippy::cast_possible_truncation)]
1487            let len = value.len() as u16;
1488            buf.extend_from_slice(&len.to_be_bytes());
1489        } else {
1490            buf.push(flags);
1491            buf.push(type_code);
1492            #[expect(clippy::cast_possible_truncation)]
1493            buf.push(value.len() as u8);
1494        }
1495        buf.extend_from_slice(&value);
1496    }
1497}
1498
1499/// Encode `MP_REACH_NLRI` value bytes.
1500///
1501/// When `add_path` is true, each NLRI entry includes a 4-byte path ID
1502/// prefix per RFC 7911.
1503fn encode_mp_reach_nlri(mp: &MpReachNlri, buf: &mut Vec<u8>, add_path: bool) {
1504    buf.extend_from_slice(&(mp.afi as u16).to_be_bytes());
1505    buf.push(mp.safi as u8);
1506
1507    // FlowSpec: NH length = 0, reserved = 0, then FlowSpec NLRI
1508    if mp.safi == Safi::FlowSpec {
1509        buf.push(0); // NH-Len = 0
1510        buf.push(0); // Reserved
1511        crate::flowspec::encode_flowspec_nlri(&mp.flowspec_announced, buf, mp.afi);
1512        return;
1513    }
1514
1515    // EVPN: next-hop is the VTEP loopback IP (4 or 16 bytes), then EVPN NLRI
1516    if mp.afi == Afi::L2Vpn && mp.safi == Safi::Evpn {
1517        match mp.next_hop {
1518            IpAddr::V4(addr) => {
1519                buf.push(4);
1520                buf.extend_from_slice(&addr.octets());
1521            }
1522            IpAddr::V6(addr) => {
1523                buf.push(16);
1524                buf.extend_from_slice(&addr.octets());
1525            }
1526        }
1527        buf.push(0); // Reserved
1528        crate::evpn::encode_evpn_nlri(&mp.evpn_announced, buf);
1529        return;
1530    }
1531
1532    match (mp.next_hop, mp.link_local_next_hop) {
1533        (IpAddr::V4(addr), _) => {
1534            buf.push(4); // NH-Len
1535            buf.extend_from_slice(&addr.octets());
1536        }
1537        (IpAddr::V6(addr), Some(ll)) => {
1538            // Symmetric to inbound validation: a NH-Len=32 form
1539            // requires the second 16 bytes to be in fe80::/10. No
1540            // live outbound construction site sets a non-LL value
1541            // (every `MpReachNlri { link_local_next_hop: ..., .. }`
1542            // in the daemon either passes `None` or a peer-validated
1543            // LL), so this is a defense-in-depth catch for future
1544            // code paths (MRT replay, RR reflection of corrupt
1545            // upstream input, etc.). Emitting a malformed 32-byte
1546            // form would tear sessions against any RFC-compliant
1547            // peer's validator (FRR, GoBGP) — exactly the inverse
1548            // of the v0.12.1 inbound bug.
1549            debug_assert!(
1550                (ll.segments()[0] & 0xffc0) == 0xfe80,
1551                "MP_REACH NH-Len=32 second segment must be link-local (fe80::/10), got {ll}"
1552            );
1553            buf.push(32); // NH-Len: global + link-local
1554            buf.extend_from_slice(&addr.octets());
1555            buf.extend_from_slice(&ll.octets());
1556        }
1557        (IpAddr::V6(addr), None) => {
1558            buf.push(16); // NH-Len
1559            buf.extend_from_slice(&addr.octets());
1560        }
1561    }
1562
1563    buf.push(0); // Reserved
1564
1565    if add_path {
1566        crate::nlri::encode_ipv6_nlri_addpath(&mp.announced, buf);
1567    } else {
1568        for entry in &mp.announced {
1569            match entry.prefix {
1570                Prefix::V4(p) => crate::nlri::encode_nlri(&[p], buf),
1571                Prefix::V6(p) => crate::nlri::encode_ipv6_nlri(&[p], buf),
1572            }
1573        }
1574    }
1575}
1576
1577/// Encode `MP_UNREACH_NLRI` value bytes.
1578///
1579/// When `add_path` is true, each withdrawn entry includes a 4-byte path ID.
1580fn encode_mp_unreach_nlri(mp: &MpUnreachNlri, buf: &mut Vec<u8>, add_path: bool) {
1581    buf.extend_from_slice(&(mp.afi as u16).to_be_bytes());
1582    buf.push(mp.safi as u8);
1583
1584    // FlowSpec: encode FlowSpec NLRI rules
1585    if mp.safi == Safi::FlowSpec {
1586        crate::flowspec::encode_flowspec_nlri(&mp.flowspec_withdrawn, buf, mp.afi);
1587        return;
1588    }
1589
1590    // EVPN: encode EVPN NLRI routes
1591    if mp.afi == Afi::L2Vpn && mp.safi == Safi::Evpn {
1592        crate::evpn::encode_evpn_nlri(&mp.evpn_withdrawn, buf);
1593        return;
1594    }
1595
1596    if add_path {
1597        crate::nlri::encode_ipv6_nlri_addpath(&mp.withdrawn, buf);
1598    } else {
1599        for entry in &mp.withdrawn {
1600            match entry.prefix {
1601                Prefix::V4(p) => crate::nlri::encode_nlri(&[p], buf),
1602                Prefix::V6(p) => crate::nlri::encode_ipv6_nlri(&[p], buf),
1603            }
1604        }
1605    }
1606}
1607
1608/// Encode `AS_PATH` segments into value bytes.
1609fn encode_as_path(as_path: &AsPath, buf: &mut Vec<u8>, four_octet_as: bool) {
1610    for segment in &as_path.segments {
1611        let (seg_type, asns) = match segment {
1612            AsPathSegment::AsSet(asns) => (as_path_segment::AS_SET, asns),
1613            AsPathSegment::AsSequence(asns) => (as_path_segment::AS_SEQUENCE, asns),
1614        };
1615        for chunk in asns.chunks(u8::MAX as usize) {
1616            buf.push(seg_type);
1617            #[expect(clippy::cast_possible_truncation)]
1618            buf.push(chunk.len() as u8);
1619            for &asn in chunk {
1620                if four_octet_as {
1621                    buf.extend_from_slice(&asn.to_be_bytes());
1622                } else {
1623                    // RFC 6793: ASNs > 65535 are mapped to AS_TRANS (23456)
1624                    // in 2-octet AS_PATH encoding.
1625                    let as2 = u16::try_from(asn).unwrap_or(crate::constants::AS_TRANS);
1626                    buf.extend_from_slice(&as2.to_be_bytes());
1627                }
1628            }
1629        }
1630    }
1631}
1632
1633#[cfg(test)]
1634mod tests {
1635    use super::*;
1636
1637    #[test]
1638    fn mp_reach_evpn_attribute_roundtrip() {
1639        use crate::evpn::{EthernetTagId, EvpnImet, EvpnRoute, RouteDistinguisher};
1640
1641        let mp = MpReachNlri {
1642            afi: Afi::L2Vpn,
1643            safi: Safi::Evpn,
1644            next_hop: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 100)),
1645            link_local_next_hop: None,
1646            announced: vec![],
1647            flowspec_announced: vec![],
1648            evpn_announced: vec![EvpnRoute::Imet(EvpnImet {
1649                rd: RouteDistinguisher([0, 0, 0xFD, 0xE8, 0, 0, 0, 0x64]),
1650                ethernet_tag: EthernetTagId(100),
1651                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 100)),
1652            })],
1653        };
1654        let attr = PathAttribute::MpReachNlri(mp);
1655
1656        let mut buf = Vec::new();
1657        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
1658        let decoded = decode_path_attributes(&buf, true, &[]).expect("decode");
1659        assert_eq!(decoded.len(), 1);
1660        assert_eq!(attr, decoded[0]);
1661
1662        let PathAttribute::MpReachNlri(dec) = &decoded[0] else {
1663            panic!("not MP_REACH after decode");
1664        };
1665        assert_eq!(dec.afi, Afi::L2Vpn);
1666        assert_eq!(dec.safi, Safi::Evpn);
1667        assert_eq!(dec.evpn_announced.len(), 1);
1668        assert!(matches!(dec.evpn_announced[0], EvpnRoute::Imet(_)));
1669    }
1670
1671    /// EVPN `MP_REACH` with an IPv6 VTEP next-hop. RFC 7432 §7.5
1672    /// allows the egress PE address to be IPv4 *or* IPv6; the
1673    /// IPv4 path was covered by `mp_reach_evpn_attribute_roundtrip`,
1674    /// the IPv6 path was the one validate-side audit gap. EVPN
1675    /// (AFI 25 / SAFI 70) uses a 16-byte single-address next-hop
1676    /// for IPv6 — there is no global+link-local 32-byte form here
1677    /// (that's RFC 2545 / unicast territory). Pinning it as a
1678    /// roundtrip catches any future regression in the EVPN-specific
1679    /// branch of `encode_mp_reach_nlri`, which is otherwise only
1680    /// exercised on the IPv4 path.
1681    #[test]
1682    fn mp_reach_evpn_ipv6_next_hop_roundtrip() {
1683        use crate::evpn::{EthernetTagId, EvpnImet, EvpnRoute, RouteDistinguisher};
1684
1685        let vtep_v6: Ipv6Addr = "2001:db8:dead::1".parse().unwrap();
1686        let mp = MpReachNlri {
1687            afi: Afi::L2Vpn,
1688            safi: Safi::Evpn,
1689            next_hop: IpAddr::V6(vtep_v6),
1690            link_local_next_hop: None,
1691            announced: vec![],
1692            flowspec_announced: vec![],
1693            evpn_announced: vec![EvpnRoute::Imet(EvpnImet {
1694                rd: RouteDistinguisher([0, 0, 0xFD, 0xE8, 0, 0, 0, 0x64]),
1695                ethernet_tag: EthernetTagId(100),
1696                originator_ip: IpAddr::V6(vtep_v6),
1697            })],
1698        };
1699        let attr = PathAttribute::MpReachNlri(mp.clone());
1700
1701        let mut buf = Vec::new();
1702        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
1703
1704        // Wire-level shape check: NH-Len byte is 16 (16-byte single
1705        // IPv6 address; EVPN does NOT use the 32-byte global+LL form),
1706        // followed by the 16 octets of vtep_v6, then Reserved=0,
1707        // then EVPN NLRI.
1708        // Value layout from `encode_mp_reach_nlri`: AFI(2) + SAFI(1)
1709        // + NH-Len(1) + NH bytes + Reserved(1) + NLRI.
1710        // Walk past the attribute header (flags(1) + type(1) + len
1711        // octet(s)) to land on the value. With a single IMET route
1712        // the value comfortably fits a single-byte length so the
1713        // header is 3 bytes total.
1714        let extended = (buf[0] & 0x10) != 0;
1715        let value_off = if extended { 4 } else { 3 };
1716        assert_eq!(
1717            buf[value_off + 3],
1718            16,
1719            "EVPN IPv6 NH-Len must be 16, not 32"
1720        );
1721        assert_eq!(
1722            &buf[value_off + 4..value_off + 20],
1723            &vtep_v6.octets(),
1724            "encoded VTEP next-hop bytes must match the input"
1725        );
1726
1727        let decoded = decode_path_attributes(&buf, true, &[]).expect("decode");
1728        assert_eq!(decoded.len(), 1);
1729        assert_eq!(PathAttribute::MpReachNlri(mp), decoded[0]);
1730
1731        let PathAttribute::MpReachNlri(dec) = &decoded[0] else {
1732            panic!("not MP_REACH after decode");
1733        };
1734        assert_eq!(dec.afi, Afi::L2Vpn);
1735        assert_eq!(dec.safi, Safi::Evpn);
1736        assert_eq!(dec.next_hop, IpAddr::V6(vtep_v6));
1737        assert!(
1738            dec.link_local_next_hop.is_none(),
1739            "EVPN's 16-byte form must not synthesize a link-local next-hop"
1740        );
1741        assert_eq!(dec.evpn_announced.len(), 1);
1742        match &dec.evpn_announced[0] {
1743            EvpnRoute::Imet(imet) => {
1744                assert_eq!(imet.originator_ip, IpAddr::V6(vtep_v6));
1745                assert_eq!(imet.ethernet_tag, EthernetTagId(100));
1746            }
1747            other => panic!("expected IMET, got {other:?}"),
1748        }
1749    }
1750
1751    /// EVPN (AFI 25 / SAFI 70) must reject the 32-byte global+
1752    /// link-local next-hop form. RFC 7432 §7.5 only permits a
1753    /// single IPv4 (4 bytes) or IPv6 (16 bytes) next-hop; the
1754    /// 32-byte form is RFC 2545 unicast-only territory. Pinning
1755    /// the rejection invariant catches a future regression that
1756    /// might broaden the L2VPN decoder by mistake.
1757    #[test]
1758    fn mp_reach_evpn_rejects_32byte_next_hop() {
1759        // Hand-crafted MP_REACH attribute: AFI=25 (L2VPN), SAFI=70
1760        // (EVPN), NH-Len=32 (illegal for EVPN), 32 bytes of
1761        // next-hop, Reserved=0, then zero bytes of NLRI.
1762        // Attribute header: flags=0x80 (optional non-transitive),
1763        // type=14 (MP_REACH), length=u8 = 4 + 32 + 1 = 37.
1764        let mut attr = vec![0x80u8, 14, 37];
1765        attr.extend_from_slice(&[
1766            0x00, 0x19, // AFI = 25 (L2VPN)
1767            0x46, // SAFI = 70 (EVPN)
1768            0x20, // NH-Len = 32 (illegal for L2VPN)
1769        ]);
1770        attr.extend(std::iter::repeat_n(0u8, 32)); // 32 NH bytes
1771        attr.push(0); // Reserved
1772
1773        let err = decode_path_attributes(&attr, true, &[]).unwrap_err();
1774        match err {
1775            DecodeError::MalformedField { detail, .. } => {
1776                assert!(
1777                    detail.contains("L2VPN next-hop length 32"),
1778                    "expected L2VPN NH-Len rejection, got: {detail}"
1779                );
1780            }
1781            other => panic!("expected MalformedField, got: {other:?}"),
1782        }
1783    }
1784
1785    #[test]
1786    fn mp_unreach_evpn_attribute_roundtrip() {
1787        use crate::evpn::{EthernetSegmentIdentifier, EvpnEs, EvpnRoute, RouteDistinguisher};
1788
1789        let mp = MpUnreachNlri {
1790            afi: Afi::L2Vpn,
1791            safi: Safi::Evpn,
1792            withdrawn: vec![],
1793            flowspec_withdrawn: vec![],
1794            evpn_withdrawn: vec![EvpnRoute::Es(EvpnEs {
1795                rd: RouteDistinguisher([0, 0, 0xFD, 0xE8, 0, 0, 0, 0x64]),
1796                esi: EthernetSegmentIdentifier([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]),
1797                originator_ip: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
1798            })],
1799        };
1800        let attr = PathAttribute::MpUnreachNlri(mp);
1801        let mut buf = Vec::new();
1802        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
1803        let decoded = decode_path_attributes(&buf, true, &[]).expect("decode");
1804        assert_eq!(decoded.len(), 1);
1805        assert_eq!(attr, decoded[0]);
1806    }
1807
1808    // ---- EVPN extended community typed accessors (RFC 7432 / 8365 / 9135) ---
1809
1810    #[test]
1811    fn ext_comm_bgp_encapsulation_vxlan() {
1812        let c = ExtendedCommunity::bgp_encapsulation(8); // VXLAN
1813        assert_eq!(c.type_byte(), 0x03);
1814        assert_eq!(c.subtype(), 0x0C);
1815        assert_eq!(c.as_bgp_encapsulation(), Some(8));
1816        // Wire layout: 4 bytes reserved + 2-byte tunnel type
1817        let b = c.as_u64().to_be_bytes();
1818        assert_eq!(b[2..6], [0, 0, 0, 0]);
1819        assert_eq!(&b[6..8], &[0, 8]);
1820        // Negative: other subtypes return None
1821        assert_eq!(ExtendedCommunity::new(0).as_bgp_encapsulation(), None);
1822    }
1823
1824    #[test]
1825    fn ext_comm_mac_mobility_sticky_and_sequence() {
1826        let m1 = ExtendedCommunity::mac_mobility(false, 42);
1827        assert_eq!(m1.as_mac_mobility(), Some((false, 42)));
1828        let m2 = ExtendedCommunity::mac_mobility(true, 12345);
1829        assert_eq!(m2.as_mac_mobility(), Some((true, 12345)));
1830        // Round-trip max sequence
1831        let m3 = ExtendedCommunity::mac_mobility(true, u32::MAX);
1832        assert_eq!(m3.as_mac_mobility(), Some((true, u32::MAX)));
1833        assert_eq!(ExtendedCommunity::new(0).as_mac_mobility(), None);
1834    }
1835
1836    #[test]
1837    fn ext_comm_esi_label_flags_and_label() {
1838        let e1 = ExtendedCommunity::esi_label(false, 10_000);
1839        assert_eq!(e1.as_esi_label(), Some((false, 10_000)));
1840        let e2 = ExtendedCommunity::esi_label(true, 0x00FF_FFFF);
1841        assert_eq!(e2.as_esi_label(), Some((true, 0x00FF_FFFF)));
1842    }
1843
1844    #[test]
1845    fn ext_comm_es_import_rt_mac() {
1846        let mac = [0x00, 0x11, 0x22, 0x33, 0x44, 0x55];
1847        let e = ExtendedCommunity::es_import_rt(mac);
1848        assert_eq!(e.as_es_import_rt(), Some(mac));
1849        assert_eq!(e.type_byte(), 0x06);
1850        assert_eq!(e.subtype(), 0x02);
1851    }
1852
1853    #[test]
1854    fn ext_comm_df_election_hrw_roundtrips_reserved_bytes_zero() {
1855        let ec = ExtendedCommunity::df_election(1, 0, None);
1856        assert_eq!(ec.type_byte(), 0x06);
1857        assert_eq!(ec.subtype(), 0x06);
1858        assert_eq!(
1859            ec.as_df_election(),
1860            Some(DfElectionExtendedCommunity {
1861                algorithm_id: 1,
1862                capabilities: 0,
1863                preference: None,
1864            })
1865        );
1866        assert_eq!(ec.as_u64().to_be_bytes(), [0x06, 0x06, 0x01, 0, 0, 0, 0, 0]);
1867    }
1868
1869    #[test]
1870    fn ext_comm_df_election_preference_bytes_decode_for_rfc9785_algorithms() {
1871        let ec = ExtendedCommunity::df_election(3, 0x8000, Some(42));
1872        assert_eq!(
1873            ec.as_df_election(),
1874            Some(DfElectionExtendedCommunity {
1875                algorithm_id: 3,
1876                capabilities: 0x8000,
1877                preference: Some(42),
1878            })
1879        );
1880    }
1881
1882    #[test]
1883    fn ext_comm_router_mac() {
1884        let mac = [0xaa, 0xbb, 0xcc, 0xdd, 0xee, 0xff];
1885        let e = ExtendedCommunity::router_mac(mac);
1886        assert_eq!(e.as_router_mac(), Some(mac));
1887    }
1888
1889    #[test]
1890    fn ext_comm_default_gateway_flag_only() {
1891        let d = ExtendedCommunity::default_gateway();
1892        assert!(d.as_default_gateway());
1893        // Not a default gateway
1894        assert!(!ExtendedCommunity::bgp_encapsulation(8).as_default_gateway());
1895    }
1896
1897    /// Regression: Default Gateway is a flag-only community (RFC 7432).
1898    /// Malformed advertisements that set non-zero bytes in the value
1899    /// field must NOT be treated as default-gateway matches.
1900    #[test]
1901    fn ext_comm_default_gateway_rejects_nonzero_value() {
1902        // Correct type/subtype (0x03/0x0D) but bogus value.
1903        let malformed =
1904            ExtendedCommunity::new(u64::from_be_bytes([0x03, 0x0D, 0, 0, 0, 0, 0, 0x01]));
1905        assert!(
1906            !malformed.as_default_gateway(),
1907            "default-gateway accessor must require all-zero value bytes"
1908        );
1909        // Sanity: the clean form still passes.
1910        assert!(ExtendedCommunity::default_gateway().as_default_gateway());
1911    }
1912
1913    #[test]
1914    fn ext_comm_accessors_return_none_on_unrelated_communities() {
1915        let rt = ExtendedCommunity::new(u64::from_be_bytes([0x00, 0x02, 0xFD, 0xE8, 0, 0, 0, 100])); // RT:65000:100
1916        assert_eq!(rt.as_bgp_encapsulation(), None);
1917        assert_eq!(rt.as_mac_mobility(), None);
1918        assert_eq!(rt.as_esi_label(), None);
1919        assert_eq!(rt.as_es_import_rt(), None);
1920        assert_eq!(rt.as_router_mac(), None);
1921        assert!(!rt.as_default_gateway());
1922    }
1923
1924    #[test]
1925    fn origin_from_u8_roundtrip() {
1926        assert_eq!(Origin::from_u8(0), Some(Origin::Igp));
1927        assert_eq!(Origin::from_u8(1), Some(Origin::Egp));
1928        assert_eq!(Origin::from_u8(2), Some(Origin::Incomplete));
1929        assert_eq!(Origin::from_u8(3), None);
1930    }
1931
1932    #[test]
1933    fn origin_ordering() {
1934        assert!(Origin::Igp < Origin::Egp);
1935        assert!(Origin::Egp < Origin::Incomplete);
1936    }
1937
1938    #[test]
1939    fn as_path_length_calculation() {
1940        let path = AsPath {
1941            segments: vec![
1942                AsPathSegment::AsSequence(vec![65001, 65002, 65003]),
1943                AsPathSegment::AsSet(vec![65004, 65005]),
1944            ],
1945        };
1946        // Sequence: 3 ASNs, Set: counts as 1 → total 4
1947        assert_eq!(path.len(), 4);
1948    }
1949
1950    #[test]
1951    fn as_path_empty() {
1952        let path = AsPath { segments: vec![] };
1953        assert!(path.is_empty());
1954        assert_eq!(path.len(), 0);
1955    }
1956
1957    #[test]
1958    fn contains_asn_in_sequence() {
1959        let path = AsPath {
1960            segments: vec![AsPathSegment::AsSequence(vec![65001, 65002, 65003])],
1961        };
1962        assert!(path.contains_asn(65002));
1963        assert!(!path.contains_asn(65004));
1964    }
1965
1966    #[test]
1967    fn contains_asn_in_set() {
1968        let path = AsPath {
1969            segments: vec![AsPathSegment::AsSet(vec![65004, 65005])],
1970        };
1971        assert!(path.contains_asn(65005));
1972        assert!(!path.contains_asn(65001));
1973    }
1974
1975    #[test]
1976    fn contains_asn_multiple_segments() {
1977        let path = AsPath {
1978            segments: vec![
1979                AsPathSegment::AsSequence(vec![65001, 65002]),
1980                AsPathSegment::AsSet(vec![65003]),
1981            ],
1982        };
1983        assert!(path.contains_asn(65001));
1984        assert!(path.contains_asn(65003));
1985        assert!(!path.contains_asn(65004));
1986    }
1987
1988    #[test]
1989    fn contains_asn_empty_path() {
1990        let path = AsPath { segments: vec![] };
1991        assert!(!path.contains_asn(65001));
1992    }
1993
1994    #[test]
1995    fn is_private_asn_boundaries() {
1996        // 16-bit private range boundaries
1997        assert!(!is_private_asn(64_511));
1998        assert!(is_private_asn(64_512));
1999        assert!(is_private_asn(65_534));
2000        assert!(!is_private_asn(65_535));
2001
2002        // 32-bit private range boundaries
2003        assert!(!is_private_asn(4_199_999_999));
2004        assert!(is_private_asn(4_200_000_000));
2005        assert!(is_private_asn(4_294_967_294));
2006        assert!(!is_private_asn(4_294_967_295));
2007    }
2008
2009    #[test]
2010    fn all_private_empty_path_is_false() {
2011        let path = AsPath { segments: vec![] };
2012        assert!(!path.all_private());
2013    }
2014
2015    #[test]
2016    fn all_private_mixed_segments() {
2017        let path = AsPath {
2018            segments: vec![
2019                AsPathSegment::AsSet(vec![64_512, 65_000]),
2020                AsPathSegment::AsSequence(vec![4_200_000_000, 65_534]),
2021            ],
2022        };
2023        assert!(path.all_private());
2024
2025        let non_private = AsPath {
2026            segments: vec![
2027                AsPathSegment::AsSet(vec![64_512, 65_000]),
2028                AsPathSegment::AsSequence(vec![65_535]),
2029            ],
2030        };
2031        assert!(!non_private.all_private());
2032    }
2033
2034    #[test]
2035    fn decode_origin_igp() {
2036        // flags=0x40 (transitive), type=1, len=1, value=0 (IGP)
2037        let buf = [0x40, 0x01, 0x01, 0x00];
2038        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2039        assert_eq!(attrs.len(), 1);
2040        assert_eq!(attrs[0], PathAttribute::Origin(Origin::Igp));
2041    }
2042
2043    #[test]
2044    fn decode_origin_egp() {
2045        let buf = [0x40, 0x01, 0x01, 0x01];
2046        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2047        assert_eq!(attrs[0], PathAttribute::Origin(Origin::Egp));
2048    }
2049
2050    #[test]
2051    fn decode_origin_invalid_value() {
2052        // ORIGIN with value 5 — not a valid Origin (only 0-2 are defined)
2053        let buf = [0x40, 0x01, 0x01, 0x05];
2054        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2055        match &err {
2056            DecodeError::UpdateAttributeError { subcode, .. } => {
2057                assert_eq!(*subcode, update_subcode::INVALID_ORIGIN);
2058            }
2059            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2060        }
2061    }
2062
2063    #[test]
2064    fn decode_next_hop() {
2065        // flags=0x40, type=3, len=4, value=10.0.0.1
2066        let buf = [0x40, 0x03, 0x04, 10, 0, 0, 1];
2067        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2068        assert_eq!(attrs[0], PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)));
2069    }
2070
2071    #[test]
2072    fn decode_med() {
2073        // flags=0x80 (optional), type=4, len=4, value=100
2074        let buf = [0x80, 0x04, 0x04, 0, 0, 0, 100];
2075        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2076        assert_eq!(attrs[0], PathAttribute::Med(100));
2077    }
2078
2079    #[test]
2080    fn decode_local_pref() {
2081        // flags=0x40, type=5, len=4, value=200
2082        let buf = [0x40, 0x05, 0x04, 0, 0, 0, 200];
2083        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2084        assert_eq!(attrs[0], PathAttribute::LocalPref(200));
2085    }
2086
2087    #[test]
2088    fn decode_as_path_4byte() {
2089        // flags=0x40, type=2, len=10
2090        // segment: type=2 (AS_SEQUENCE), count=2, ASNs: 65001, 65002 (4 bytes each)
2091        let buf = [
2092            0x40, 0x02, 0x0A, // header
2093            0x02, 0x02, // AS_SEQUENCE, 2 ASNs
2094            0x00, 0x00, 0xFD, 0xE9, // 65001
2095            0x00, 0x00, 0xFD, 0xEA, // 65002
2096        ];
2097        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2098        assert_eq!(
2099            attrs[0],
2100            PathAttribute::AsPath(AsPath {
2101                segments: vec![AsPathSegment::AsSequence(vec![65001, 65002])]
2102            })
2103        );
2104    }
2105
2106    #[test]
2107    fn decode_as_path_2byte() {
2108        // flags=0x40, type=2, len=6
2109        // segment: type=2 (AS_SEQUENCE), count=2, ASNs: 100, 200 (2 bytes each)
2110        let buf = [
2111            0x40, 0x02, 0x06, // header
2112            0x02, 0x02, // AS_SEQUENCE, 2 ASNs
2113            0x00, 0x64, // 100
2114            0x00, 0xC8, // 200
2115        ];
2116        let attrs = decode_path_attributes(&buf, false, &[]).unwrap();
2117        assert_eq!(
2118            attrs[0],
2119            PathAttribute::AsPath(AsPath {
2120                segments: vec![AsPathSegment::AsSequence(vec![100, 200])]
2121            })
2122        );
2123    }
2124
2125    #[test]
2126    fn decode_unknown_attribute_preserved() {
2127        // flags=0xC0 (optional+transitive), type=99, len=3, data=[1,2,3]
2128        let buf = [0xC0, 99, 0x03, 1, 2, 3];
2129        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2130        assert_eq!(
2131            attrs[0],
2132            PathAttribute::Unknown(RawAttribute {
2133                flags: 0xC0,
2134                type_code: 99,
2135                data: Bytes::from_static(&[1, 2, 3]),
2136            })
2137        );
2138    }
2139
2140    #[test]
2141    fn decode_atomic_aggregate_as_unknown() {
2142        // ATOMIC_AGGREGATE: flags=0x40, type=6, len=0
2143        let buf = [0x40, 0x06, 0x00];
2144        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2145        assert!(matches!(attrs[0], PathAttribute::Unknown(_)));
2146    }
2147
2148    #[test]
2149    fn decode_extended_length() {
2150        // flags=0x50 (transitive+extended), type=2, len=0x000A (10)
2151        // Same AS_PATH as the 4-byte test
2152        let buf = [
2153            0x50, 0x02, 0x00, 0x0A, // header with extended length
2154            0x02, 0x02, // AS_SEQUENCE, 2 ASNs
2155            0x00, 0x00, 0xFD, 0xE9, // 65001
2156            0x00, 0x00, 0xFD, 0xEA, // 65002
2157        ];
2158        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2159        assert_eq!(
2160            attrs[0],
2161            PathAttribute::AsPath(AsPath {
2162                segments: vec![AsPathSegment::AsSequence(vec![65001, 65002])]
2163            })
2164        );
2165    }
2166
2167    #[test]
2168    fn decode_multiple_attributes() {
2169        let mut buf = Vec::new();
2170        // ORIGIN IGP
2171        buf.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]);
2172        // NEXT_HOP 10.0.0.1
2173        buf.extend_from_slice(&[0x40, 0x03, 0x04, 10, 0, 0, 1]);
2174        // AS_PATH empty
2175        buf.extend_from_slice(&[0x40, 0x02, 0x00]);
2176
2177        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2178        assert_eq!(attrs.len(), 3);
2179        assert_eq!(attrs[0], PathAttribute::Origin(Origin::Igp));
2180        assert_eq!(attrs[1], PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)));
2181        assert_eq!(attrs[2], PathAttribute::AsPath(AsPath { segments: vec![] }));
2182    }
2183
2184    #[test]
2185    fn roundtrip_attributes_4byte() {
2186        let attrs = vec![
2187            PathAttribute::Origin(Origin::Igp),
2188            PathAttribute::AsPath(AsPath {
2189                segments: vec![AsPathSegment::AsSequence(vec![65001, 65002])],
2190            }),
2191            PathAttribute::NextHop(Ipv4Addr::new(10, 0, 0, 1)),
2192            PathAttribute::Med(100),
2193            PathAttribute::LocalPref(200),
2194        ];
2195
2196        let mut buf = Vec::new();
2197        encode_path_attributes(&attrs, &mut buf, true, false);
2198        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2199        assert_eq!(decoded, attrs);
2200    }
2201
2202    #[test]
2203    fn roundtrip_attributes_2byte() {
2204        let attrs = vec![
2205            PathAttribute::Origin(Origin::Egp),
2206            PathAttribute::AsPath(AsPath {
2207                segments: vec![AsPathSegment::AsSequence(vec![100, 200])],
2208            }),
2209            PathAttribute::NextHop(Ipv4Addr::new(172, 16, 0, 1)),
2210        ];
2211
2212        let mut buf = Vec::new();
2213        encode_path_attributes(&attrs, &mut buf, false, false);
2214        let decoded = decode_path_attributes(&buf, false, &[]).unwrap();
2215        assert_eq!(decoded, attrs);
2216    }
2217
2218    #[test]
2219    fn reject_truncated_attribute_header() {
2220        let buf = [0x40]; // only 1 byte
2221        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2222    }
2223
2224    #[test]
2225    fn reject_truncated_attribute_value() {
2226        // ORIGIN claims 1 byte value but nothing follows
2227        let buf = [0x40, 0x01, 0x01];
2228        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2229    }
2230
2231    #[test]
2232    fn reject_bad_origin_length() {
2233        // ORIGIN with 2-byte value
2234        let buf = [0x40, 0x01, 0x02, 0x00, 0x00];
2235        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2236    }
2237
2238    #[test]
2239    fn as_path_with_set_and_sequence() {
2240        // AS_SEQUENCE [65001], AS_SET [65002, 65003]
2241        let attrs = vec![PathAttribute::AsPath(AsPath {
2242            segments: vec![
2243                AsPathSegment::AsSequence(vec![65001]),
2244                AsPathSegment::AsSet(vec![65002, 65003]),
2245            ],
2246        })];
2247
2248        let mut buf = Vec::new();
2249        encode_path_attributes(&attrs, &mut buf, true, false);
2250        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2251        assert_eq!(decoded, attrs);
2252    }
2253
2254    #[test]
2255    fn decode_communities_single() {
2256        // flags=0xC0 (optional+transitive), type=8, len=4, community=65001:100
2257        // 65001 = 0xFDE9, 100 = 0x0064 → u32 = 0xFDE90064
2258        let community: u32 = (65001 << 16) | 0x0064;
2259        let bytes = community.to_be_bytes();
2260        let buf = [0xC0, 0x08, 0x04, bytes[0], bytes[1], bytes[2], bytes[3]];
2261        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2262        assert_eq!(attrs.len(), 1);
2263        assert_eq!(attrs[0], PathAttribute::Communities(vec![community]));
2264    }
2265
2266    #[test]
2267    fn decode_communities_multiple() {
2268        let c1: u32 = (65001 << 16) | 0x0064;
2269        let c2: u32 = (65002 << 16) | 0x00C8;
2270        let b1 = c1.to_be_bytes();
2271        let b2 = c2.to_be_bytes();
2272        let buf = [
2273            0xC0, 0x08, 0x08, b1[0], b1[1], b1[2], b1[3], b2[0], b2[1], b2[2], b2[3],
2274        ];
2275        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2276        assert_eq!(attrs[0], PathAttribute::Communities(vec![c1, c2]));
2277    }
2278
2279    #[test]
2280    fn decode_communities_empty() {
2281        // flags=0xC0, type=8, len=0
2282        let buf = [0xC0, 0x08, 0x00];
2283        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2284        assert_eq!(attrs[0], PathAttribute::Communities(vec![]));
2285    }
2286
2287    #[test]
2288    fn decode_communities_odd_length_rejected() {
2289        // flags=0xC0, type=8, len=3, only 3 bytes (not multiple of 4)
2290        let buf = [0xC0, 0x08, 0x03, 0x01, 0x02, 0x03];
2291        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2292    }
2293
2294    #[test]
2295    fn communities_roundtrip() {
2296        let c1: u32 = (65001 << 16) | 0x0064;
2297        let c2: u32 = (65002 << 16) | 0x00C8;
2298        let attrs = vec![PathAttribute::Communities(vec![c1, c2])];
2299
2300        let mut buf = Vec::new();
2301        encode_path_attributes(&attrs, &mut buf, true, false);
2302        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2303        assert_eq!(decoded, attrs);
2304    }
2305
2306    #[test]
2307    fn communities_type_code_and_flags() {
2308        let attr = PathAttribute::Communities(vec![]);
2309        assert_eq!(attr.type_code(), 8);
2310        assert_eq!(attr.flags(), attr_flags::OPTIONAL | attr_flags::TRANSITIVE);
2311    }
2312
2313    // --- Extended Communities (RFC 4360) tests ---
2314
2315    #[test]
2316    fn decode_extended_communities_single() {
2317        // Route Target 65001:100 — type 0x00, subtype 0x02, AS 65001 (2-octet), value 100
2318        let ec = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2319        let bytes = ec.as_u64().to_be_bytes();
2320        let buf = [
2321            0xC0, 0x10, 0x08, bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6],
2322            bytes[7],
2323        ];
2324        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2325        assert_eq!(attrs.len(), 1);
2326        assert_eq!(attrs[0], PathAttribute::ExtendedCommunities(vec![ec]));
2327    }
2328
2329    #[test]
2330    fn decode_extended_communities_multiple() {
2331        let ec1 = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2332        let ec2 = ExtendedCommunity::new(0x0003_FDEA_0000_00C8);
2333        let b1 = ec1.as_u64().to_be_bytes();
2334        let b2 = ec2.as_u64().to_be_bytes();
2335        let mut buf = vec![0xC0, 0x10, 16]; // flags, type=16, len=16
2336        buf.extend_from_slice(&b1);
2337        buf.extend_from_slice(&b2);
2338        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2339        assert_eq!(attrs[0], PathAttribute::ExtendedCommunities(vec![ec1, ec2]));
2340    }
2341
2342    #[test]
2343    fn decode_extended_communities_empty() {
2344        let buf = [0xC0, 0x10, 0x00];
2345        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2346        assert_eq!(attrs[0], PathAttribute::ExtendedCommunities(vec![]));
2347    }
2348
2349    #[test]
2350    fn decode_extended_communities_bad_length() {
2351        // length 5 is not a multiple of 8
2352        let buf = [0xC0, 0x10, 0x05, 0x01, 0x02, 0x03, 0x04, 0x05];
2353        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2354    }
2355
2356    #[test]
2357    fn extended_communities_roundtrip() {
2358        let ec1 = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2359        let ec2 = ExtendedCommunity::new(0x0003_FDEA_0000_00C8);
2360        let attrs = vec![PathAttribute::ExtendedCommunities(vec![ec1, ec2])];
2361
2362        let mut buf = Vec::new();
2363        encode_path_attributes(&attrs, &mut buf, true, false);
2364        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2365        assert_eq!(decoded, attrs);
2366    }
2367
2368    #[test]
2369    fn extended_communities_type_code_and_flags() {
2370        let attr = PathAttribute::ExtendedCommunities(vec![]);
2371        assert_eq!(attr.type_code(), 16);
2372        assert_eq!(attr.flags(), attr_flags::OPTIONAL | attr_flags::TRANSITIVE);
2373    }
2374
2375    #[test]
2376    fn extended_community_type_subtype() {
2377        // Type 0x00, Sub-type 0x02 (Route Target, 2-octet AS)
2378        let ec = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2379        assert_eq!(ec.type_byte(), 0x00);
2380        assert_eq!(ec.subtype(), 0x02);
2381        assert!(ec.is_transitive());
2382    }
2383
2384    #[test]
2385    fn extended_community_route_target() {
2386        // 2-octet AS RT: type=0x00, subtype=0x02, AS=65001, value=100
2387        let ec = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2388        assert_eq!(ec.route_target(), Some((65001, 100)));
2389        assert_eq!(ec.route_origin(), None);
2390
2391        // 4-octet AS RT: type=0x02, subtype=0x02, AS=65537, value=200
2392        let ec4 = ExtendedCommunity::new(0x0202_0001_0001_00C8);
2393        assert_eq!(ec4.route_target(), Some((65537, 200)));
2394
2395        // IPv4-specific RT: type=0x01, subtype=0x02, IP=192.0.2.1, value=100
2396        // 192.0.2.1 = 0xC0000201
2397        let ec_ipv4 = ExtendedCommunity::new(0x0102_C000_0201_0064);
2398        let (g, l) = ec_ipv4.route_target().unwrap();
2399        assert_eq!(g, 0xC000_0201); // 192.0.2.1 as u32
2400        assert_eq!(l, 100);
2401        // Callers distinguish via type_byte()
2402        assert_eq!(ec_ipv4.type_byte() & 0x3F, 0x01);
2403    }
2404
2405    #[test]
2406    fn extended_community_is_transitive() {
2407        // Type 0x00 → transitive (bit 6 = 0)
2408        let t = ExtendedCommunity::new(0x0002_0000_0000_0000);
2409        assert!(t.is_transitive());
2410
2411        // Type 0x40 → non-transitive (bit 6 = 1)
2412        let nt = ExtendedCommunity::new(0x4002_0000_0000_0000);
2413        assert!(!nt.is_transitive());
2414    }
2415
2416    #[test]
2417    fn extended_community_display() {
2418        let rt = ExtendedCommunity::new(0x0002_FDE9_0000_0064);
2419        assert_eq!(rt.to_string(), "RT:65001:100");
2420
2421        let ro = ExtendedCommunity::new(0x0003_FDE9_0000_0064);
2422        assert_eq!(ro.to_string(), "RO:65001:100");
2423
2424        // IPv4-specific RT: type=0x01, subtype=0x02, IP=192.0.2.1, value=100
2425        let target_v4 = ExtendedCommunity::new(0x0102_C000_0201_0064);
2426        assert_eq!(target_v4.to_string(), "RT:192.0.2.1:100");
2427
2428        // IPv4-specific RO
2429        let origin_v4 = ExtendedCommunity::new(0x0103_C000_0201_0064);
2430        assert_eq!(origin_v4.to_string(), "RO:192.0.2.1:100");
2431
2432        // 4-octet AS RT
2433        let rt_as4 = ExtendedCommunity::new(0x0202_0001_0001_00C8);
2434        assert_eq!(rt_as4.to_string(), "RT:65537:200");
2435
2436        // Non-transitive opaque → hex fallback
2437        let opaque = ExtendedCommunity::new(0x4300_1234_5678_9ABC);
2438        assert_eq!(opaque.to_string(), "0x4300123456789abc");
2439    }
2440
2441    #[test]
2442    fn unknown_attribute_roundtrip() {
2443        // Input has flags 0xC0 (optional+transitive). After encoding, the
2444        // Partial bit is OR'd in for transitive unknowns → 0xE0.
2445        let attrs = vec![PathAttribute::Unknown(RawAttribute {
2446            flags: 0xC0,
2447            type_code: 99,
2448            data: Bytes::from_static(&[1, 2, 3, 4, 5]),
2449        })];
2450
2451        let mut buf = Vec::new();
2452        encode_path_attributes(&attrs, &mut buf, true, false);
2453        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2454        assert_eq!(
2455            decoded,
2456            vec![PathAttribute::Unknown(RawAttribute {
2457                flags: 0xE0, // Partial bit set on re-advertisement
2458                type_code: 99,
2459                data: Bytes::from_static(&[1, 2, 3, 4, 5]),
2460            })]
2461        );
2462    }
2463
2464    #[test]
2465    fn origin_with_optional_flag_rejected() {
2466        // ORIGIN with flags 0xC0 (Optional+Transitive) — should be 0x40 (Transitive only)
2467        let buf = [0xC0, 0x01, 0x01, 0x00];
2468        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2469        match &err {
2470            DecodeError::UpdateAttributeError { subcode, .. } => {
2471                assert_eq!(*subcode, update_subcode::ATTRIBUTE_FLAGS_ERROR);
2472            }
2473            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2474        }
2475    }
2476
2477    #[test]
2478    fn med_with_transitive_flag_rejected() {
2479        // MED with flags 0xC0 (Optional+Transitive) — should be 0x80 (Optional only)
2480        let buf = [0xC0, 0x04, 0x04, 0, 0, 0, 100];
2481        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2482        match &err {
2483            DecodeError::UpdateAttributeError { subcode, .. } => {
2484                assert_eq!(*subcode, update_subcode::ATTRIBUTE_FLAGS_ERROR);
2485            }
2486            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2487        }
2488    }
2489
2490    #[test]
2491    fn communities_without_optional_rejected() {
2492        // COMMUNITIES with flags 0x40 (Transitive only) — should be 0xC0 (Optional+Transitive)
2493        let buf = [0x40, 0x08, 0x04, 0, 0, 0, 100];
2494        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2495        match &err {
2496            DecodeError::UpdateAttributeError { subcode, .. } => {
2497                assert_eq!(*subcode, update_subcode::ATTRIBUTE_FLAGS_ERROR);
2498            }
2499            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2500        }
2501    }
2502
2503    #[test]
2504    fn next_hop_length_error_subcode() {
2505        // NEXT_HOP with 3 bytes instead of 4
2506        let buf = [0x40, 0x03, 0x03, 10, 0, 0];
2507        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2508        match &err {
2509            DecodeError::UpdateAttributeError { subcode, .. } => {
2510                assert_eq!(*subcode, update_subcode::ATTRIBUTE_LENGTH_ERROR);
2511            }
2512            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2513        }
2514    }
2515
2516    #[test]
2517    fn invalid_origin_value_subcode() {
2518        // ORIGIN with value 5 → subcode 6 (INVALID_ORIGIN)
2519        let buf = [0x40, 0x01, 0x01, 0x05];
2520        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2521        match &err {
2522            DecodeError::UpdateAttributeError { subcode, .. } => {
2523                assert_eq!(*subcode, update_subcode::INVALID_ORIGIN);
2524            }
2525            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2526        }
2527    }
2528
2529    #[test]
2530    fn as_path_bad_segment_subcode() {
2531        // AS_PATH with unknown segment type 5
2532        let buf = [
2533            0x40, 0x02, 0x06, // AS_PATH header, length 6
2534            0x05, 0x01, // unknown segment type 5, count 1
2535            0x00, 0x00, 0xFD, 0xE9, // ASN 65001
2536        ];
2537        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2538        match &err {
2539            DecodeError::UpdateAttributeError { subcode, .. } => {
2540                assert_eq!(*subcode, update_subcode::MALFORMED_AS_PATH);
2541            }
2542            other => panic!("expected UpdateAttributeError, got: {other:?}"),
2543        }
2544    }
2545
2546    #[test]
2547    fn encode_unknown_transitive_sets_partial() {
2548        let attr = PathAttribute::Unknown(RawAttribute {
2549            flags: attr_flags::OPTIONAL | attr_flags::TRANSITIVE, // 0xC0
2550            type_code: 99,
2551            data: Bytes::from_static(&[1, 2]),
2552        });
2553        let mut buf = Vec::new();
2554        encode_path_attributes(&[attr], &mut buf, true, false);
2555        // First byte is flags — should have PARTIAL bit set
2556        assert_eq!(
2557            buf[0],
2558            attr_flags::OPTIONAL | attr_flags::TRANSITIVE | attr_flags::PARTIAL
2559        );
2560    }
2561
2562    #[test]
2563    fn encode_unknown_wellknown_transitive_no_partial() {
2564        // Well-known transitive (OPTIONAL=0, TRANSITIVE=1) should NOT get PARTIAL
2565        let attr = PathAttribute::Unknown(RawAttribute {
2566            flags: attr_flags::TRANSITIVE, // 0x40, well-known transitive
2567            type_code: 99,
2568            data: Bytes::from_static(&[1, 2]),
2569        });
2570        let mut buf = Vec::new();
2571        encode_path_attributes(&[attr], &mut buf, true, false);
2572        assert_eq!(buf[0], attr_flags::TRANSITIVE);
2573    }
2574
2575    #[test]
2576    fn encode_unknown_nontransitive_no_partial() {
2577        let attr = PathAttribute::Unknown(RawAttribute {
2578            flags: attr_flags::OPTIONAL, // 0x80, no Transitive
2579            type_code: 99,
2580            data: Bytes::from_static(&[1, 2]),
2581        });
2582        let mut buf = Vec::new();
2583        encode_path_attributes(&[attr], &mut buf, true, false);
2584        // First byte is flags — should NOT have PARTIAL bit
2585        assert_eq!(buf[0], attr_flags::OPTIONAL);
2586    }
2587
2588    // --- MP_REACH_NLRI / MP_UNREACH_NLRI tests ---
2589
2590    /// Helper to create a `NlriEntry` with `path_id=0`.
2591    fn nlri(prefix: Prefix) -> NlriEntry {
2592        NlriEntry { path_id: 0, prefix }
2593    }
2594
2595    #[test]
2596    fn mp_reach_nlri_ipv6_roundtrip() {
2597        use crate::capability::{Afi, Safi};
2598        use crate::nlri::{Ipv6Prefix, Prefix};
2599
2600        let mp = MpReachNlri {
2601            afi: Afi::Ipv6,
2602            safi: Safi::Unicast,
2603            next_hop: IpAddr::V6("2001:db8::1".parse().unwrap()),
2604            link_local_next_hop: None,
2605            announced: vec![
2606                nlri(Prefix::V6(Ipv6Prefix::new(
2607                    "2001:db8:1::".parse().unwrap(),
2608                    48,
2609                ))),
2610                nlri(Prefix::V6(Ipv6Prefix::new(
2611                    "2001:db8:2::".parse().unwrap(),
2612                    48,
2613                ))),
2614            ],
2615            flowspec_announced: vec![],
2616            evpn_announced: vec![],
2617        };
2618        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2619
2620        let mut buf = Vec::new();
2621        encode_path_attributes(&attrs, &mut buf, true, false);
2622        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2623        assert_eq!(decoded.len(), 1);
2624        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2625    }
2626
2627    #[test]
2628    fn mp_unreach_nlri_ipv6_roundtrip() {
2629        use crate::capability::{Afi, Safi};
2630        use crate::nlri::{Ipv6Prefix, Prefix};
2631
2632        let mp = MpUnreachNlri {
2633            afi: Afi::Ipv6,
2634            safi: Safi::Unicast,
2635            withdrawn: vec![nlri(Prefix::V6(Ipv6Prefix::new(
2636                "2001:db8:1::".parse().unwrap(),
2637                48,
2638            )))],
2639            flowspec_withdrawn: vec![],
2640            evpn_withdrawn: vec![],
2641        };
2642        let attrs = vec![PathAttribute::MpUnreachNlri(mp.clone())];
2643
2644        let mut buf = Vec::new();
2645        encode_path_attributes(&attrs, &mut buf, true, false);
2646        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2647        assert_eq!(decoded.len(), 1);
2648        assert_eq!(decoded[0], PathAttribute::MpUnreachNlri(mp));
2649    }
2650
2651    #[test]
2652    fn mp_reach_nlri_ipv4_roundtrip() {
2653        use crate::capability::{Afi, Safi};
2654        use crate::nlri::Prefix;
2655
2656        let mp = MpReachNlri {
2657            afi: Afi::Ipv4,
2658            safi: Safi::Unicast,
2659            next_hop: IpAddr::V4(Ipv4Addr::new(10, 0, 0, 1)),
2660            link_local_next_hop: None,
2661            announced: vec![nlri(Prefix::V4(crate::nlri::Ipv4Prefix::new(
2662                Ipv4Addr::new(10, 1, 0, 0),
2663                16,
2664            )))],
2665            flowspec_announced: vec![],
2666            evpn_announced: vec![],
2667        };
2668        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2669
2670        let mut buf = Vec::new();
2671        encode_path_attributes(&attrs, &mut buf, true, false);
2672        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2673        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2674    }
2675
2676    #[test]
2677    fn mp_reach_nlri_ipv4_with_ipv6_nexthop_roundtrip() {
2678        use crate::capability::{Afi, Safi};
2679        use crate::nlri::Prefix;
2680
2681        let mp = MpReachNlri {
2682            afi: Afi::Ipv4,
2683            safi: Safi::Unicast,
2684            next_hop: IpAddr::V6("2001:db8::1".parse().unwrap()),
2685            link_local_next_hop: None,
2686            announced: vec![nlri(Prefix::V4(crate::nlri::Ipv4Prefix::new(
2687                Ipv4Addr::new(10, 1, 0, 0),
2688                16,
2689            )))],
2690            flowspec_announced: vec![],
2691            evpn_announced: vec![],
2692        };
2693        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2694
2695        let mut buf = Vec::new();
2696        encode_path_attributes(&attrs, &mut buf, true, false);
2697        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2698        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2699    }
2700
2701    #[test]
2702    fn mp_reach_nlri_type_code_and_flags() {
2703        use crate::capability::{Afi, Safi};
2704
2705        let attr = PathAttribute::MpReachNlri(MpReachNlri {
2706            afi: Afi::Ipv6,
2707            safi: Safi::Unicast,
2708            next_hop: IpAddr::V6(Ipv6Addr::UNSPECIFIED),
2709            link_local_next_hop: None,
2710            announced: vec![],
2711            flowspec_announced: vec![],
2712            evpn_announced: vec![],
2713        });
2714        assert_eq!(attr.type_code(), 14);
2715        // RFC 4760 §3: MP_REACH_NLRI is optional non-transitive
2716        assert_eq!(attr.flags(), attr_flags::OPTIONAL);
2717    }
2718
2719    #[test]
2720    fn mp_unreach_nlri_type_code_and_flags() {
2721        use crate::capability::{Afi, Safi};
2722
2723        let attr = PathAttribute::MpUnreachNlri(MpUnreachNlri {
2724            afi: Afi::Ipv6,
2725            safi: Safi::Unicast,
2726            withdrawn: vec![],
2727            flowspec_withdrawn: vec![],
2728            evpn_withdrawn: vec![],
2729        });
2730        assert_eq!(attr.type_code(), 15);
2731        assert_eq!(attr.flags(), attr_flags::OPTIONAL);
2732    }
2733
2734    #[test]
2735    fn mp_reach_nlri_empty_nlri() {
2736        use crate::capability::{Afi, Safi};
2737
2738        let mp = MpReachNlri {
2739            afi: Afi::Ipv6,
2740            safi: Safi::Unicast,
2741            next_hop: IpAddr::V6("fe80::1".parse().unwrap()),
2742            link_local_next_hop: None,
2743            announced: vec![],
2744            flowspec_announced: vec![],
2745            evpn_announced: vec![],
2746        };
2747        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2748
2749        let mut buf = Vec::new();
2750        encode_path_attributes(&attrs, &mut buf, true, false);
2751        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2752        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2753    }
2754
2755    #[test]
2756    fn mp_reach_nlri_bad_flags_rejected() {
2757        // MP_REACH_NLRI (type 14) with flags 0x40 (Transitive only)
2758        // — should be 0xC0 (Optional+Transitive)
2759        // Build minimal valid value: AFI=2, SAFI=1, NH-Len=16, NH=::1, Reserved=0
2760        let mut value = Vec::new();
2761        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
2762        value.push(1); // SAFI Unicast
2763        value.push(16); // NH-Len
2764        value.extend_from_slice(&"::1".parse::<Ipv6Addr>().unwrap().octets()); // NH
2765        value.push(0); // Reserved
2766
2767        let mut buf = Vec::new();
2768        buf.push(0x40); // flags: Transitive only (wrong)
2769        buf.push(14); // type: MP_REACH_NLRI
2770        #[expect(clippy::cast_possible_truncation)]
2771        buf.push(value.len() as u8);
2772        buf.extend_from_slice(&value);
2773
2774        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
2775        assert!(matches!(
2776            err,
2777            DecodeError::UpdateAttributeError {
2778                subcode: 4, // ATTRIBUTE_FLAGS_ERROR
2779                ..
2780            }
2781        ));
2782    }
2783
2784    // --- MP Add-Path decode tests ---
2785
2786    #[test]
2787    #[expect(clippy::cast_possible_truncation)]
2788    fn mp_reach_nlri_ipv4_addpath_decode() {
2789        use crate::capability::{Afi, Safi};
2790        use crate::nlri::Prefix;
2791
2792        // Build MP_REACH_NLRI with Add-Path-encoded IPv4 NLRI:
2793        // path_id(4) + prefix_len(1) + prefix_bytes
2794        let mut value = Vec::new();
2795        value.extend_from_slice(&1u16.to_be_bytes()); // AFI IPv4
2796        value.push(1); // SAFI Unicast
2797        value.push(4); // NH-Len
2798        value.extend_from_slice(&[10, 0, 0, 1]); // Next Hop
2799        value.push(0); // Reserved
2800        // Add-Path NLRI: path_id=42, 10.1.0.0/16
2801        value.extend_from_slice(&42u32.to_be_bytes());
2802        value.push(16);
2803        value.extend_from_slice(&[10, 1]);
2804
2805        let mut buf = Vec::new();
2806        buf.push(0x90); // flags: Optional + Extended Length
2807        buf.push(14); // type: MP_REACH_NLRI
2808        buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
2809        buf.extend_from_slice(&value);
2810
2811        // With Add-Path for IPv4 unicast → decode path_id
2812        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv4, Safi::Unicast)]).unwrap();
2813        let PathAttribute::MpReachNlri(mp) = &decoded[0] else {
2814            panic!("expected MpReachNlri");
2815        };
2816        assert_eq!(mp.announced.len(), 1);
2817        assert_eq!(mp.announced[0].path_id, 42);
2818        assert!(matches!(mp.announced[0].prefix, Prefix::V4(p) if p.len == 16));
2819
2820        // Without Add-Path → plain decoder misinterprets the path_id bytes
2821        // as prefix encoding and rejects the garbled data.
2822        assert!(decode_path_attributes(&buf, true, &[]).is_err());
2823    }
2824
2825    #[test]
2826    #[expect(clippy::cast_possible_truncation)]
2827    fn mp_reach_nlri_ipv6_addpath_decode() {
2828        use crate::capability::{Afi, Safi};
2829        use crate::nlri::{Ipv6Prefix, Prefix};
2830
2831        // Build MP_REACH_NLRI with Add-Path-encoded IPv6 NLRI
2832        let mut value = Vec::new();
2833        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
2834        value.push(1); // SAFI Unicast
2835        value.push(16); // NH-Len
2836        value.extend_from_slice(&"2001:db8::1".parse::<Ipv6Addr>().unwrap().octets());
2837        value.push(0); // Reserved
2838        // Add-Path NLRI: path_id=99, 2001:db8:1::/48
2839        value.extend_from_slice(&99u32.to_be_bytes());
2840        value.push(48);
2841        value.extend_from_slice(&[0x20, 0x01, 0x0d, 0xb8, 0x00, 0x01]);
2842
2843        let mut buf = Vec::new();
2844        buf.push(0x90); // flags: Optional + Extended Length
2845        buf.push(14); // type: MP_REACH_NLRI
2846        buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
2847        buf.extend_from_slice(&value);
2848
2849        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv6, Safi::Unicast)]).unwrap();
2850        let PathAttribute::MpReachNlri(mp) = &decoded[0] else {
2851            panic!("expected MpReachNlri");
2852        };
2853        assert_eq!(mp.announced.len(), 1);
2854        assert_eq!(mp.announced[0].path_id, 99);
2855        assert_eq!(
2856            mp.announced[0].prefix,
2857            Prefix::V6(Ipv6Prefix::new("2001:db8:1::".parse().unwrap(), 48))
2858        );
2859    }
2860
2861    #[test]
2862    #[expect(clippy::cast_possible_truncation)]
2863    fn mp_unreach_nlri_ipv6_addpath_decode() {
2864        use crate::capability::{Afi, Safi};
2865        use crate::nlri::{Ipv6Prefix, Prefix};
2866
2867        // Build MP_UNREACH_NLRI with Add-Path-encoded IPv6 NLRI
2868        let mut value = Vec::new();
2869        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
2870        value.push(1); // SAFI Unicast
2871        // Add-Path NLRI: path_id=7, 2001:db8:2::/48
2872        value.extend_from_slice(&7u32.to_be_bytes());
2873        value.push(48);
2874        value.extend_from_slice(&[0x20, 0x01, 0x0d, 0xb8, 0x00, 0x02]);
2875
2876        let mut buf = Vec::new();
2877        buf.push(0x90); // flags: Optional + Extended Length
2878        buf.push(15); // type: MP_UNREACH_NLRI
2879        buf.extend_from_slice(&(value.len() as u16).to_be_bytes());
2880        buf.extend_from_slice(&value);
2881
2882        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv6, Safi::Unicast)]).unwrap();
2883        let PathAttribute::MpUnreachNlri(mp) = &decoded[0] else {
2884            panic!("expected MpUnreachNlri");
2885        };
2886        assert_eq!(mp.withdrawn.len(), 1);
2887        assert_eq!(mp.withdrawn[0].path_id, 7);
2888        assert_eq!(
2889            mp.withdrawn[0].prefix,
2890            Prefix::V6(Ipv6Prefix::new("2001:db8:2::".parse().unwrap(), 48))
2891        );
2892    }
2893
2894    #[test]
2895    fn mp_reach_addpath_only_applies_to_matching_family() {
2896        use crate::capability::{Afi, Safi};
2897        use crate::nlri::{Ipv6Prefix, Prefix};
2898
2899        // Build plain (non-Add-Path) MP_REACH_NLRI for IPv6
2900        let mp = MpReachNlri {
2901            afi: Afi::Ipv6,
2902            safi: Safi::Unicast,
2903            next_hop: IpAddr::V6("2001:db8::1".parse().unwrap()),
2904            link_local_next_hop: None,
2905            announced: vec![NlriEntry {
2906                path_id: 0,
2907                prefix: Prefix::V6(Ipv6Prefix::new("2001:db8:1::".parse().unwrap(), 48)),
2908            }],
2909            flowspec_announced: vec![],
2910            evpn_announced: vec![],
2911        };
2912        let attrs = vec![PathAttribute::MpReachNlri(mp.clone())];
2913
2914        let mut buf = Vec::new();
2915        encode_path_attributes(&attrs, &mut buf, true, false);
2916
2917        // Add-Path enabled for IPv4 only — IPv6 should still decode as plain
2918        let decoded = decode_path_attributes(&buf, true, &[(Afi::Ipv4, Safi::Unicast)]).unwrap();
2919        assert_eq!(decoded[0], PathAttribute::MpReachNlri(mp));
2920    }
2921
2922    // --- ORIGINATOR_ID tests ---
2923
2924    #[test]
2925    fn decode_originator_id() {
2926        // flags=0x80 (optional), type=9, len=4, value=1.2.3.4
2927        let buf = [0x80, 0x09, 0x04, 1, 2, 3, 4];
2928        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
2929        assert_eq!(
2930            attrs[0],
2931            PathAttribute::OriginatorId(Ipv4Addr::new(1, 2, 3, 4))
2932        );
2933    }
2934
2935    /// 32-byte IPv6 next-hop (global + link-local) round-trips through
2936    /// decode/encode without dropping the link-local. Regression for the
2937    /// pre-existing limitation where the decoder kept only the first
2938    /// 16 bytes and the encoder only emitted 16 bytes.
2939    #[test]
2940    fn mp_reach_ipv6_32byte_next_hop_roundtrip() {
2941        use crate::capability::{Afi, Safi};
2942        use crate::nlri::{Ipv6Prefix, Prefix};
2943        let global: Ipv6Addr = "2001:db8::1".parse().unwrap();
2944        let link_local: Ipv6Addr = "fe80::1".parse().unwrap();
2945        let mp = MpReachNlri {
2946            afi: Afi::Ipv6,
2947            safi: Safi::Unicast,
2948            next_hop: IpAddr::V6(global),
2949            link_local_next_hop: Some(link_local),
2950            announced: vec![NlriEntry {
2951                path_id: 0,
2952                prefix: Prefix::V6(Ipv6Prefix::new("2001:db8:1::".parse().unwrap(), 48)),
2953            }],
2954            flowspec_announced: vec![],
2955            evpn_announced: vec![],
2956        };
2957        let attr = PathAttribute::MpReachNlri(mp.clone());
2958        let mut buf = Vec::new();
2959        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
2960
2961        // The attribute value should start with NH-Len=32, then the
2962        // 16-byte global, then the 16-byte link-local.
2963        // Walk header: flags(1) + type(1) + len(1 or 3) + value.
2964        let extended = (buf[0] & 0x10) != 0;
2965        let value_off = if extended { 4 } else { 3 };
2966        // value layout: AFI(2) + SAFI(1) + NH-Len(1) + NH bytes + Reserved(1) + NLRI
2967        assert_eq!(buf[value_off + 3], 32, "NH-Len must be 32 for global+LL");
2968        assert_eq!(&buf[value_off + 4..value_off + 20], &global.octets());
2969        assert_eq!(
2970            &buf[value_off + 20..value_off + 36],
2971            &link_local.octets(),
2972            "encoded link-local bytes must match the input"
2973        );
2974
2975        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
2976        let PathAttribute::MpReachNlri(dec) = &decoded[0] else {
2977            panic!("expected MpReachNlri");
2978        };
2979        assert_eq!(dec.next_hop, IpAddr::V6(global));
2980        assert_eq!(dec.link_local_next_hop, Some(link_local));
2981    }
2982
2983    /// Audit follow-up: a peer sending an `MP_REACH` for `FlowSpec`
2984    /// (SAFI 133) with a non-zero `NH-Len` is malformed per RFC
2985    /// 8955 §6.1 — the decoder must reject so the rest of the
2986    /// pipeline never sees a misshapen `FlowSpec` advertisement.
2987    /// Logic exists at `decode_mp_reach_nlri` but had no direct
2988    /// regression test; adding one cheaply pins the wire-level
2989    /// guarantee that complements the validate-time skip.
2990    #[test]
2991    fn mp_reach_flowspec_rejects_nonzero_nh_len() {
2992        // AFI=1 (IPv4), SAFI=133 (FlowSpec), NH-Len=4, NH=10.0.0.1,
2993        // Reserved=0, then a single component-1 prefix (192.168.1.0/24).
2994        let value: &[u8] = &[
2995            0x00, 0x01, // AFI = IPv4
2996            0x85, // SAFI = 133 (FlowSpec)
2997            0x04, // NH-Len = 4 (illegal for FlowSpec — must be 0)
2998            10, 0, 0, 1,    // NH bytes
2999            0x00, // Reserved
3000            // FlowSpec NLRI: length(1) + component type 1 + prefix
3001            0x07, 0x01, 0x18, 192, 168, 1,
3002        ];
3003        // attribute header: flags(0x80 = optional) + type(14 =
3004        // MP_REACH) + len(value.len() as u8) + value
3005        let mut attr = vec![0x80, 14, u8::try_from(value.len()).unwrap()];
3006        attr.extend_from_slice(value);
3007        let err = decode_path_attributes(&attr, true, &[]).unwrap_err();
3008        match err {
3009            DecodeError::MalformedField { detail, .. } => {
3010                assert!(
3011                    detail.contains("FlowSpec next-hop length"),
3012                    "expected FlowSpec NH-Len rejection, got: {detail}"
3013                );
3014            }
3015            other => panic!("expected MalformedField, got {other:?}"),
3016        }
3017    }
3018
3019    #[test]
3020    fn originator_id_roundtrip() {
3021        let attr = PathAttribute::OriginatorId(Ipv4Addr::new(10, 0, 0, 1));
3022        let mut buf = Vec::new();
3023        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
3024        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
3025        assert_eq!(decoded, vec![attr]);
3026    }
3027
3028    #[test]
3029    fn originator_id_wrong_length() {
3030        // 3 bytes instead of 4
3031        let buf = [0x80, 0x09, 0x03, 1, 2, 3];
3032        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3033        assert!(matches!(
3034            err,
3035            DecodeError::UpdateAttributeError {
3036                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
3037                ..
3038            }
3039        ));
3040    }
3041
3042    #[test]
3043    fn originator_id_wrong_flags() {
3044        // flags=0x40 (transitive) — should be 0x80 (optional)
3045        let buf = [0x40, 0x09, 0x04, 1, 2, 3, 4];
3046        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3047        assert!(matches!(
3048            err,
3049            DecodeError::UpdateAttributeError {
3050                subcode: 4, // ATTRIBUTE_FLAGS_ERROR
3051                ..
3052            }
3053        ));
3054    }
3055
3056    // --- CLUSTER_LIST tests ---
3057
3058    #[test]
3059    fn decode_cluster_list() {
3060        // flags=0x80 (optional), type=10, len=8, two cluster IDs
3061        let buf = [0x80, 0x0A, 0x08, 1, 2, 3, 4, 5, 6, 7, 8];
3062        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
3063        assert_eq!(
3064            attrs[0],
3065            PathAttribute::ClusterList(vec![Ipv4Addr::new(1, 2, 3, 4), Ipv4Addr::new(5, 6, 7, 8),])
3066        );
3067    }
3068
3069    #[test]
3070    fn cluster_list_roundtrip() {
3071        let attr = PathAttribute::ClusterList(vec![
3072            Ipv4Addr::new(10, 0, 0, 1),
3073            Ipv4Addr::new(10, 0, 0, 2),
3074        ]);
3075        let mut buf = Vec::new();
3076        encode_path_attributes(std::slice::from_ref(&attr), &mut buf, true, false);
3077        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
3078        assert_eq!(decoded, vec![attr]);
3079    }
3080
3081    #[test]
3082    fn cluster_list_wrong_length() {
3083        // 5 bytes — not a multiple of 4
3084        let buf = [0x80, 0x0A, 0x05, 1, 2, 3, 4, 5];
3085        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3086        assert!(matches!(
3087            err,
3088            DecodeError::UpdateAttributeError {
3089                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
3090                ..
3091            }
3092        ));
3093    }
3094
3095    // -----------------------------------------------------------------------
3096    // Large Communities (RFC 8092)
3097    // -----------------------------------------------------------------------
3098
3099    #[test]
3100    fn large_community_display() {
3101        let lc = LargeCommunity::new(65001, 100, 200);
3102        assert_eq!(lc.to_string(), "65001:100:200");
3103    }
3104
3105    #[test]
3106    fn large_community_type_code_and_flags() {
3107        let attr = PathAttribute::LargeCommunities(vec![LargeCommunity::new(1, 2, 3)]);
3108        assert_eq!(attr.type_code(), attr_type::LARGE_COMMUNITIES);
3109        assert_eq!(attr.flags(), attr_flags::OPTIONAL | attr_flags::TRANSITIVE);
3110    }
3111
3112    #[test]
3113    fn decode_large_community_single() {
3114        // flags=0xC0 (Optional|Transitive), type=32, length=12
3115        let mut buf = vec![0xC0, 32, 12];
3116        buf.extend_from_slice(&65001u32.to_be_bytes());
3117        buf.extend_from_slice(&100u32.to_be_bytes());
3118        buf.extend_from_slice(&200u32.to_be_bytes());
3119        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
3120        assert_eq!(attrs.len(), 1);
3121        assert_eq!(
3122            attrs[0],
3123            PathAttribute::LargeCommunities(vec![LargeCommunity::new(65001, 100, 200)])
3124        );
3125    }
3126
3127    #[test]
3128    fn decode_large_community_multiple() {
3129        // Two LCs: 24 bytes total
3130        let mut buf = vec![0xC0, 32, 24];
3131        for (g, l1, l2) in [(65001u32, 100u32, 200u32), (65002, 300, 400)] {
3132            buf.extend_from_slice(&g.to_be_bytes());
3133            buf.extend_from_slice(&l1.to_be_bytes());
3134            buf.extend_from_slice(&l2.to_be_bytes());
3135        }
3136        let attrs = decode_path_attributes(&buf, true, &[]).unwrap();
3137        assert_eq!(
3138            attrs[0],
3139            PathAttribute::LargeCommunities(vec![
3140                LargeCommunity::new(65001, 100, 200),
3141                LargeCommunity::new(65002, 300, 400),
3142            ])
3143        );
3144    }
3145
3146    #[test]
3147    fn decode_large_community_bad_length() {
3148        // 10 bytes — not a multiple of 12
3149        let buf = [0xC0, 32, 10, 0, 0, 0, 1, 0, 0, 0, 2, 0, 0];
3150        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3151        assert!(matches!(
3152            err,
3153            DecodeError::UpdateAttributeError {
3154                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
3155                ..
3156            }
3157        ));
3158    }
3159
3160    #[test]
3161    fn decode_large_community_empty_rejected() {
3162        // Zero-length LARGE_COMMUNITIES is rejected (must carry at least one community).
3163        let buf = [0xC0, 32, 0];
3164        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3165        assert!(matches!(
3166            err,
3167            DecodeError::UpdateAttributeError {
3168                subcode: 5, // ATTRIBUTE_LENGTH_ERROR
3169                ..
3170            }
3171        ));
3172    }
3173
3174    #[test]
3175    fn large_community_roundtrip() {
3176        let lcs = vec![
3177            LargeCommunity::new(65001, 100, 200),
3178            LargeCommunity::new(0, u32::MAX, 42),
3179        ];
3180        let attr = PathAttribute::LargeCommunities(lcs.clone());
3181        let mut buf = Vec::new();
3182        encode_path_attributes(&[attr], &mut buf, true, false);
3183        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
3184        assert_eq!(decoded.len(), 1);
3185        assert_eq!(decoded[0], PathAttribute::LargeCommunities(lcs));
3186    }
3187
3188    #[test]
3189    fn large_community_expected_flags_validated() {
3190        // Wrong flags: TRANSITIVE only (0x40) instead of OPTIONAL|TRANSITIVE (0xC0)
3191        let mut buf = vec![0x40, 32, 12];
3192        buf.extend_from_slice(&1u32.to_be_bytes());
3193        buf.extend_from_slice(&2u32.to_be_bytes());
3194        buf.extend_from_slice(&3u32.to_be_bytes());
3195        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3196        assert!(matches!(
3197            err,
3198            DecodeError::UpdateAttributeError {
3199                subcode: 4, // ATTRIBUTE_FLAGS_ERROR
3200                ..
3201            }
3202        ));
3203    }
3204
3205    // -----------------------------------------------------------------------
3206    // AsPath::to_aspath_string()
3207    // -----------------------------------------------------------------------
3208
3209    #[test]
3210    fn aspath_string_sequence() {
3211        let p = AsPath {
3212            segments: vec![AsPathSegment::AsSequence(vec![65001, 65002, 65003])],
3213        };
3214        assert_eq!(p.to_aspath_string(), "65001 65002 65003");
3215    }
3216
3217    #[test]
3218    fn aspath_string_set() {
3219        let p = AsPath {
3220            segments: vec![AsPathSegment::AsSet(vec![65003, 65004])],
3221        };
3222        assert_eq!(p.to_aspath_string(), "{65003 65004}");
3223    }
3224
3225    #[test]
3226    fn aspath_string_mixed() {
3227        let p = AsPath {
3228            segments: vec![
3229                AsPathSegment::AsSequence(vec![65001, 65002]),
3230                AsPathSegment::AsSet(vec![65003, 65004]),
3231            ],
3232        };
3233        assert_eq!(p.to_aspath_string(), "65001 65002 {65003 65004}");
3234    }
3235
3236    #[test]
3237    fn aspath_string_empty() {
3238        let p = AsPath { segments: vec![] };
3239        assert_eq!(p.to_aspath_string(), "");
3240    }
3241
3242    /// Regression: SAFI 70 (EVPN) is only valid under AFI 25 (L2VPN).
3243    /// Other AFIs with SAFI=Evpn must be rejected explicitly so the
3244    /// unicast NLRI fallthrough never tries to parse the typed EVPN
3245    /// payload as a prefix list.
3246    #[test]
3247    fn mp_reach_nlri_rejects_evpn_safi_with_non_l2vpn_afi() {
3248        // AFI=Ipv4 (1), SAFI=Evpn (70), NH-len=4, NH=192.0.2.1, reserved=0,
3249        // followed by an arbitrary EVPN-shaped byte (route type 3, len 0).
3250        let bytes = vec![
3251            0x00, 0x01, // AFI = Ipv4
3252            70,   // SAFI = Evpn
3253            4, 192, 0, 2, 1, // NH len + NH
3254            0, // reserved
3255            3, 0, // EVPN-style NLRI (route type 3, length 0)
3256        ];
3257        let err = decode_mp_reach_nlri(&bytes, &[]).unwrap_err();
3258        match err {
3259            DecodeError::MalformedField { detail, .. } => {
3260                assert!(detail.contains("SAFI EVPN"), "unexpected detail: {detail}");
3261            }
3262            other => panic!("expected MalformedField, got {other:?}"),
3263        }
3264    }
3265
3266    #[test]
3267    fn mp_unreach_nlri_rejects_evpn_safi_with_non_l2vpn_afi() {
3268        let bytes = vec![
3269            0x00, 0x02, // AFI = Ipv6
3270            70,   // SAFI = Evpn
3271            3, 0, // EVPN-style withdrawal (route type 3, length 0)
3272        ];
3273        let err = decode_mp_unreach_nlri(&bytes, &[]).unwrap_err();
3274        match err {
3275            DecodeError::MalformedField { detail, .. } => {
3276                assert!(detail.contains("SAFI EVPN"), "unexpected detail: {detail}");
3277            }
3278            other => panic!("expected MalformedField, got {other:?}"),
3279        }
3280    }
3281
3282    #[test]
3283    fn pmsi_tunnel_path_attribute_round_trips_through_dispatch() {
3284        // Encode a multi-attribute payload that includes a PMSI Tunnel
3285        // alongside the typical path attribute set so the dispatcher
3286        // (and extended-length / flags / type-code paths) is exercised
3287        // end-to-end.
3288        let pmsi =
3289            crate::pmsi::PmsiTunnel::for_evpn_ingress_replication(100, "10.0.0.1".parse().unwrap());
3290        let attrs = vec![
3291            PathAttribute::Origin(Origin::Igp),
3292            PathAttribute::AsPath(AsPath { segments: vec![] }),
3293            PathAttribute::LocalPref(100),
3294            PathAttribute::PmsiTunnel(pmsi.clone()),
3295        ];
3296
3297        let mut buf = Vec::new();
3298        encode_path_attributes(&attrs, &mut buf, true, false);
3299        let decoded = decode_path_attributes(&buf, true, &[]).unwrap();
3300
3301        assert_eq!(decoded, attrs);
3302
3303        // Verify the encoded PMSI uses Optional+Transitive flags
3304        // (RFC 6514 §5) and type code 22.
3305        let pmsi_decoded = decoded
3306            .iter()
3307            .find_map(|a| match a {
3308                PathAttribute::PmsiTunnel(p) => Some(p),
3309                _ => None,
3310            })
3311            .expect("PMSI present");
3312        assert_eq!(pmsi_decoded, &pmsi);
3313        assert_eq!(
3314            PathAttribute::PmsiTunnel(pmsi).flags(),
3315            attr_flags::OPTIONAL | attr_flags::TRANSITIVE,
3316        );
3317    }
3318
3319    #[test]
3320    fn pmsi_tunnel_decode_attribute_with_truncated_value_is_malformed() {
3321        // 4 bytes of value (need ≥5: flags+type+3-octet label).
3322        let buf = [
3323            0xC0, // optional + transitive
3324            22,   // PMSI Tunnel type code
3325            0x04, // length = 4
3326            0x00, 0x06, 0x00, 0x00,
3327        ];
3328        let err = decode_path_attributes(&buf, true, &[]).unwrap_err();
3329        assert!(matches!(err, DecodeError::MalformedField { .. }));
3330    }
3331}