Skip to main content

rustbgpd_wire/
attribute.rs

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