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