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