Skip to main content

rustbgpd_wire/
attribute.rs

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