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