Skip to main content

bgpkit_parser/parser/bgp/attributes/
mod.rs

1mod attr_01_origin;
2mod attr_02_17_as_path;
3mod attr_03_next_hop;
4mod attr_04_med;
5mod attr_05_local_pref;
6mod attr_07_18_aggregator;
7mod attr_08_communities;
8mod attr_09_originator;
9mod attr_10_13_cluster;
10mod attr_14_15_nlri;
11mod attr_16_25_extended_communities;
12mod attr_23_tunnel_encap;
13mod attr_24_traffic_engineering;
14mod attr_26_aigp;
15mod attr_29_linkstate;
16mod attr_32_large_communities;
17mod attr_35_otc;
18mod attr_36_domain_path;
19mod attr_37_sfp;
20mod attr_38_bfd_discriminator;
21mod attr_40_bgp_prefix_sid;
22mod attr_41_bier;
23
24use bytes::{Buf, BufMut, Bytes, BytesMut};
25use log::{debug, warn};
26use std::net::IpAddr;
27
28use crate::models::*;
29
30use crate::encoder::sink::{with_u16_len, with_u8_len};
31use crate::error::{BgpValidationWarning, EncodingError, ParserError};
32use crate::parser::bgp::attributes::attr_01_origin::{encode_origin, parse_origin};
33use crate::parser::bgp::attributes::attr_02_17_as_path::encode_as_path;
34pub(crate) use crate::parser::bgp::attributes::attr_02_17_as_path::parse_as_path;
35use crate::parser::bgp::attributes::attr_03_next_hop::{encode_next_hop, parse_next_hop};
36use crate::parser::bgp::attributes::attr_04_med::{encode_med, parse_med};
37use crate::parser::bgp::attributes::attr_05_local_pref::{encode_local_pref, parse_local_pref};
38use crate::parser::bgp::attributes::attr_07_18_aggregator::{encode_aggregator, parse_aggregator};
39use crate::parser::bgp::attributes::attr_08_communities::{
40    encode_regular_communities, parse_regular_communities,
41};
42use crate::parser::bgp::attributes::attr_09_originator::{
43    encode_originator_id, parse_originator_id,
44};
45use crate::parser::bgp::attributes::attr_10_13_cluster::{encode_clusters, parse_clusters};
46use crate::parser::bgp::attributes::attr_14_15_nlri::encode_nlri;
47pub(crate) use crate::parser::bgp::attributes::attr_14_15_nlri::parse_nlri;
48use crate::parser::bgp::attributes::attr_16_25_extended_communities::{
49    encode_extended_communities, encode_ipv6_extended_communities, parse_extended_community,
50    parse_ipv6_extended_community,
51};
52use crate::parser::bgp::attributes::attr_23_tunnel_encap::{
53    encode_tunnel_encapsulation_attribute, parse_tunnel_encapsulation_attribute,
54};
55use crate::parser::bgp::attributes::attr_24_traffic_engineering::{
56    encode_traffic_engineering, parse_traffic_engineering,
57};
58use crate::parser::bgp::attributes::attr_26_aigp::{encode_aigp, parse_aigp};
59use crate::parser::bgp::attributes::attr_29_linkstate::{
60    encode_link_state_attribute, parse_link_state_attribute,
61};
62use crate::parser::bgp::attributes::attr_32_large_communities::{
63    encode_large_communities, parse_large_communities,
64};
65use crate::parser::bgp::attributes::attr_35_otc::{
66    encode_only_to_customer, parse_only_to_customer,
67};
68use crate::parser::bgp::attributes::attr_36_domain_path::{encode_domain_path, parse_domain_path};
69use crate::parser::bgp::attributes::attr_37_sfp::{encode_sfp, parse_sfp};
70use crate::parser::bgp::attributes::attr_38_bfd_discriminator::{
71    encode_bfd_discriminator, parse_bfd_discriminator,
72};
73use crate::parser::bgp::attributes::attr_40_bgp_prefix_sid::{
74    encode_bgp_prefix_sid, parse_bgp_prefix_sid,
75};
76use crate::parser::bgp::attributes::attr_41_bier::{encode_bier, parse_bier};
77use crate::parser::ReadUtils;
78
79/// Validate attribute flags according to RFC 4271 and RFC 7606
80fn validate_attribute_flags(
81    attr_type: AttrType,
82    flags: AttrFlags,
83    warnings: &mut Vec<BgpValidationWarning>,
84) {
85    let expected_flags = match attr_type {
86        // Well-known mandatory attributes
87        AttrType::ORIGIN | AttrType::AS_PATH | AttrType::NEXT_HOP => AttrFlags::TRANSITIVE,
88        // Well-known discretionary attributes
89        AttrType::ATOMIC_AGGREGATE => AttrFlags::TRANSITIVE,
90        // Optional non-transitive attributes
91        AttrType::MULTI_EXIT_DISCRIMINATOR
92        | AttrType::ORIGINATOR_ID
93        | AttrType::CLUSTER_LIST
94        | AttrType::MP_REACHABLE_NLRI
95        | AttrType::MP_UNREACHABLE_NLRI
96        | AttrType::TRAFFIC_ENGINEERING => AttrFlags::OPTIONAL,
97        // Optional transitive attributes
98        AttrType::AGGREGATOR
99        | AttrType::AS4_AGGREGATOR
100        | AttrType::AS4_PATH
101        | AttrType::COMMUNITIES
102        | AttrType::EXTENDED_COMMUNITIES
103        | AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES
104        | AttrType::LARGE_COMMUNITIES
105        | AttrType::ONLY_TO_CUSTOMER
106        | AttrType::BGP_DOMAIN_PATH => AttrFlags::OPTIONAL | AttrFlags::TRANSITIVE,
107        // LOCAL_PREFERENCE is well-known mandatory for IBGP
108        AttrType::LOCAL_PREFERENCE => AttrFlags::TRANSITIVE,
109        // Unknown or development attributes
110        _ => return, // Don't validate unknown attributes
111    };
112
113    // Check if flags match expected (ignoring EXTENDED and PARTIAL flags for this check)
114    let relevant_flags = flags & (AttrFlags::OPTIONAL | AttrFlags::TRANSITIVE);
115    if relevant_flags != expected_flags {
116        warnings.push(BgpValidationWarning::AttributeFlagsError {
117            attr_type,
118            expected_flags: expected_flags.bits(),
119            actual_flags: relevant_flags.bits(),
120        });
121    }
122
123    // Check partial flag constraint
124    if flags.contains(AttrFlags::PARTIAL) {
125        match attr_type {
126            // Partial bit MUST be 0 for well-known attributes and optional non-transitive
127            AttrType::ORIGIN
128            | AttrType::AS_PATH
129            | AttrType::NEXT_HOP
130            | AttrType::LOCAL_PREFERENCE
131            | AttrType::ATOMIC_AGGREGATE
132            | AttrType::MULTI_EXIT_DISCRIMINATOR
133            | AttrType::ORIGINATOR_ID
134            | AttrType::CLUSTER_LIST
135            | AttrType::MP_REACHABLE_NLRI
136            | AttrType::MP_UNREACHABLE_NLRI
137            | AttrType::TRAFFIC_ENGINEERING => {
138                warnings.push(BgpValidationWarning::AttributeFlagsError {
139                    attr_type,
140                    expected_flags: expected_flags.bits(),
141                    actual_flags: flags.bits(),
142                });
143            }
144            _ => {} // Partial is OK for optional transitive attributes
145        }
146    }
147}
148
149fn is_raw_retained_attr(attr_type: AttrType) -> bool {
150    matches!(
151        attr_type,
152        AttrType::RESERVED
153            | AttrType::PMSI_TUNNEL
154            | AttrType::PE_DISTINGUISHER_LABELS
155            | AttrType::BGPSEC_PATH
156            | AttrType::ATTR_SET
157    )
158}
159
160/// Check if an attribute type is well-known mandatory
161fn is_well_known_mandatory(attr_type: AttrType) -> bool {
162    matches!(
163        attr_type,
164        AttrType::ORIGIN | AttrType::AS_PATH | AttrType::NEXT_HOP | AttrType::LOCAL_PREFERENCE
165    )
166}
167
168pub(crate) struct AttributeValidationState {
169    warnings: Vec<BgpValidationWarning>,
170    attr_mask: [u64; 4],
171}
172
173impl AttributeValidationState {
174    pub(crate) fn new() -> Self {
175        Self {
176            warnings: Vec::new(),
177            attr_mask: [0; 4],
178        }
179    }
180
181    fn has_raw_attr(&self, attr: u8) -> bool {
182        (self.attr_mask[(attr / 64) as usize] & (1u64 << (attr % 64))) != 0
183    }
184
185    pub(crate) fn has_attr(&self, attr_type: AttrType) -> bool {
186        self.has_raw_attr(u8::from(attr_type))
187    }
188
189    fn set_attr(&mut self, attr: u8) {
190        self.attr_mask[(attr / 64) as usize] |= 1u64 << (attr % 64);
191    }
192
193    pub(crate) fn observe_header(
194        &mut self,
195        raw_attr_type: u8,
196        attr_type: AttrType,
197        flags: AttrFlags,
198        length: usize,
199    ) -> bool {
200        if self.has_raw_attr(raw_attr_type) {
201            self.warnings
202                .push(BgpValidationWarning::DuplicateAttribute { attr_type });
203        }
204        self.set_attr(raw_attr_type);
205
206        validate_attribute_flags(attr_type, flags, &mut self.warnings);
207        validate_attribute_length(attr_type, length, &mut self.warnings);
208
209        flags.contains(AttrFlags::PARTIAL)
210    }
211
212    pub(crate) fn observe_parse_error(
213        &mut self,
214        attr_type: AttrType,
215        partial: bool,
216        error: &ParserError,
217    ) {
218        if partial {
219            self.warnings
220                .push(BgpValidationWarning::PartialAttributeError {
221                    attr_type,
222                    reason: error.to_string(),
223                });
224            debug!("PARTIAL attribute error: {}", error);
225        } else if is_well_known_mandatory(attr_type) {
226            self.warnings
227                .push(BgpValidationWarning::MalformedAttributeList {
228                    reason: format!(
229                        "Well-known mandatory attribute {} parsing failed: {}",
230                        u8::from(attr_type),
231                        error
232                    ),
233                });
234            debug!(
235                "Well-known mandatory attribute parsing failed, treating as withdraw: {}",
236                error
237            );
238        } else {
239            self.warnings
240                .push(BgpValidationWarning::OptionalAttributeError {
241                    attr_type,
242                    reason: error.to_string(),
243                });
244            debug!("Optional attribute error, discarding: {}", error);
245        }
246    }
247
248    pub(crate) fn check_mandatory_attributes(
249        &mut self,
250        is_announcement: bool,
251        has_standard_nlri: bool,
252    ) {
253        if !is_announcement {
254            return;
255        }
256
257        if !self.has_attr(AttrType::ORIGIN) {
258            self.warnings
259                .push(BgpValidationWarning::MissingWellKnownAttribute {
260                    attr_type: AttrType::ORIGIN,
261                });
262        }
263
264        if !self.has_attr(AttrType::AS_PATH) {
265            self.warnings
266                .push(BgpValidationWarning::MissingWellKnownAttribute {
267                    attr_type: AttrType::AS_PATH,
268                });
269        }
270
271        let has_mp_reach = self.has_attr(AttrType::MP_REACHABLE_NLRI);
272        if (has_standard_nlri || !has_mp_reach) && !self.has_attr(AttrType::NEXT_HOP) {
273            self.warnings
274                .push(BgpValidationWarning::MissingWellKnownAttribute {
275                    attr_type: AttrType::NEXT_HOP,
276                });
277        }
278    }
279
280    pub(crate) fn finish(self) -> (Vec<BgpValidationWarning>, [u64; 4]) {
281        (self.warnings, self.attr_mask)
282    }
283}
284
285/// Validate attribute length constraints
286fn validate_attribute_length(
287    attr_type: AttrType,
288    length: usize,
289    warnings: &mut Vec<BgpValidationWarning>,
290) {
291    let expected_length = match attr_type {
292        AttrType::ORIGIN => Some(1),
293        AttrType::NEXT_HOP => Some(4), // IPv4 next hop
294        AttrType::MULTI_EXIT_DISCRIMINATOR => Some(4),
295        AttrType::LOCAL_PREFERENCE => Some(4),
296        AttrType::ATOMIC_AGGREGATE => Some(0),
297        AttrType::ORIGINATOR_ID => Some(4),
298        AttrType::ONLY_TO_CUSTOMER => Some(4),
299        // Variable length attributes - no fixed constraint
300        AttrType::AS_PATH
301        | AttrType::AS4_PATH
302        | AttrType::AGGREGATOR
303        | AttrType::AS4_AGGREGATOR
304        | AttrType::COMMUNITIES
305        | AttrType::EXTENDED_COMMUNITIES
306        | AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES
307        | AttrType::LARGE_COMMUNITIES
308        | AttrType::CLUSTER_LIST
309        | AttrType::MP_REACHABLE_NLRI
310        | AttrType::MP_UNREACHABLE_NLRI => None,
311        _ => None, // Unknown attributes
312    };
313
314    if let Some(expected) = expected_length {
315        if length != expected {
316            warnings.push(BgpValidationWarning::AttributeLengthError {
317                attr_type,
318                expected_length: Some(expected),
319                actual_length: length,
320            });
321        }
322    }
323}
324
325/// Whether RFC 10039 §4 allows the D-PATH attribute on routes of this family: IPVPN, which is
326/// IPv4 or IPv6 with SAFI 128, and EVPN, which is AFI 25 with SAFI 70.
327fn is_domain_path_family(afi: u16, safi: u8) -> bool {
328    matches!((afi, safi), (1 | 2, 128) | (25, 70))
329}
330
331/// The AFI/SAFI header an MP_REACH_NLRI attribute value starts with, when it is long enough to
332/// carry one. The header identifies the family even when the rest did not decode.
333fn mp_reach_family(bytes: &[u8]) -> Option<(u16, u8)> {
334    match bytes.len() >= 3 {
335        true => Some((u16::from_be_bytes([bytes[0], bytes[1]]), bytes[2])),
336        false => None,
337    }
338}
339
340/// The route families a D-PATH-carrying message carries, as far as the caller can tell.
341#[derive(Debug, Clone, Copy, Default)]
342struct DomainPathFamilies {
343    /// Family the caller already knows, e.g. a RIB entry from its record header.
344    declared: Option<(u16, u8)>,
345    /// Family of the routes an MP_REACH_NLRI announces, decoded or read off its header.
346    announced: Option<(u16, u8)>,
347    /// Classic IPv4 unicast NLRI, which the UPDATE parser adds once it has parsed them.
348    classic_ipv4: bool,
349}
350
351impl DomainPathFamilies {
352    /// The families visible in an attribute set on its own, before any NLRI is parsed. An
353    /// MP_UNREACH_NLRI announces nothing, so it says nothing about the family here. An
354    /// MP_REACH_NLRI whose NLRI did not decode still identifies its family through the AFI/SAFI
355    /// header it starts with, which is enough to judge it.
356    fn from_attributes(attributes: &[Attribute], declared: Option<(u16, u8)>) -> Self {
357        let announced = attributes
358            .iter()
359            .find_map(|attribute| match &attribute.value {
360                AttributeValue::MpReachNlri(nlri) => Some((nlri.afi as u16, nlri.safi as u8)),
361                _ => None,
362            })
363            .or_else(|| {
364                attributes
365                    .iter()
366                    .find_map(|attribute| match &attribute.value {
367                        AttributeValue::Raw(raw) | AttributeValue::Unknown(raw)
368                            if raw.code == u8::from(AttrType::MP_REACHABLE_NLRI) =>
369                        {
370                            mp_reach_family(&raw.bytes)
371                        }
372                        _ => None,
373                    })
374            });
375
376        DomainPathFamilies {
377            declared,
378            announced,
379            classic_ipv4: false,
380        }
381    }
382
383    /// Describe a carried family that RFC 10039 §4 does not allow D-PATH on, if there is one.
384    ///
385    /// The rule is about the routes a message carries, so a decoded announcement decides the
386    /// answer and a withdrawal never introduces one. A family this attribute set does not
387    /// reveal stays unknown: the UPDATE parser reports classic IPv4 unicast NLRI once it has
388    /// parsed them, and a caller that knows the family up front passes it in.
389    fn disallowed(&self) -> Option<String> {
390        let declared = self
391            .declared
392            .filter(|(afi, safi)| !is_domain_path_family(*afi, *safi));
393        if let Some((afi, safi)) = declared {
394            return Some(describe_family(afi, safi, "declared"));
395        }
396
397        if let Some((afi, safi)) = self
398            .announced
399            .filter(|(afi, safi)| !is_domain_path_family(*afi, *safi))
400        {
401            return Some(describe_family(afi, safi, "announced"));
402        }
403
404        if self.classic_ipv4 {
405            return Some("classic IPv4 unicast NLRI (AFI 1, SAFI 1)".to_string());
406        }
407
408        None
409    }
410}
411
412fn describe_family(afi: u16, safi: u8, source: &str) -> String {
413    match (afi, safi) {
414        // a mismatched EVPN SAFI is worth naming, since EVPN is AFI 25 with SAFI 70
415        (_, 70) if afi != 25 => {
416            format!("{source} AFI {afi} routes with SAFI 70 (EVPN requires AFI 25)")
417        }
418        _ => format!("{source} AFI {afi} routes with SAFI {safi}"),
419    }
420}
421
422/// RFC 10039 §4 limits D-PATH to UPDATEs that carry IPVPN or EVPN routes and requires the
423/// treat-as-withdraw action for any other family, so report the finding when a carried family
424/// is known to be something else.
425fn domain_path_family_warning(families: DomainPathFamilies) -> Option<BgpValidationWarning> {
426    families.disallowed().map(|family| {
427        BgpValidationWarning::OptionalAttributeError {
428            attr_type: AttrType::BGP_DOMAIN_PATH,
429            reason: format!(
430                "D-PATH is only valid on IPVPN (AFI 1/2, SAFI 128) or EVPN (AFI 25, SAFI 70) routes, this UPDATE carries {family}: RFC 10039 §4 requires treat-as-withdraw"
431            ),
432        }
433    })
434}
435
436/// Parse BGP attributes given a slice of u8 and some options.
437///
438/// The `data: &[u8]` contains the entirety of the attributes bytes, therefore the size of
439/// the slice is the total byte length of the attributes section of the message.
440pub fn parse_attributes(
441    mut data: Bytes,
442    asn_len: &AsnLength,
443    add_path: bool,
444    afi: Option<Afi>,
445    safi: Option<Safi>,
446    prefixes: Option<&[NetworkPrefix]>,
447) -> Result<Attributes, ParserError> {
448    // Estimate capacity from data size: each attribute is at least 3 bytes
449    // (flag + type + length). Cap at 256 to avoid over-allocation for corrupted data.
450    let estimated_attrs = (data.remaining() / 3).min(256);
451    let mut attributes: Vec<Attribute> = Vec::with_capacity(estimated_attrs.max(8));
452    let mut validation = AttributeValidationState::new();
453    // A handle on the whole attribute section, so an attribute that fails to parse can be kept
454    // raw by re-slicing it, without cloning every attribute's bytes up front.
455    let section = data.clone();
456
457    while data.remaining() >= 3 {
458        // each attribute is at least 3 bytes: flag(1) + type(1) + length(1)
459        // thus the while loop condition is set to be at least 3 bytes to read.
460
461        // has content to read
462        let flag = AttrFlags::from_bits_retain(data.read_u8()?);
463        let attr_type = data.read_u8()?;
464        let attr_length = match flag.contains(AttrFlags::EXTENDED) {
465            false => data.read_u8()? as usize,
466            true => data.read_u16()? as usize,
467        };
468
469        debug!(
470            "reading attribute: type -- {:?}, length -- {}",
471            attr_type, attr_length
472        );
473
474        let attr_type = AttrType::from(attr_type);
475
476        let partial = validation.observe_header(attr_type.into(), attr_type, flag, attr_length);
477
478        let bytes_left = data.remaining();
479
480        if data.remaining() < attr_length {
481            warn!(
482                "{:?} attribute encodes a length ({}) that is longer than the remaining attribute data ({}). Skipping remaining attribute data for BGP message",
483                attr_type, attr_length, bytes_left
484            );
485            // break and return already parsed attributes
486            break;
487        }
488
489        // we know data has enough bytes to read, so we can split the bytes into a new Bytes object
490        data.has_n_remaining(attr_length)?;
491        let value_start = section.len() - data.len();
492        let mut attr_data = data.split_to(attr_length);
493        let raw_code = u8::from(attr_type);
494
495        if let Some(t) = get_deprecated_attr_type(raw_code) {
496            debug!("deprecated attribute type: {} - {}", raw_code, t);
497            attributes.push(Attribute {
498                value: AttributeValue::Deprecated(AttrRaw {
499                    code: raw_code,
500                    bytes: attr_data,
501                }),
502                flag,
503            });
504            continue;
505        }
506
507        if matches!(attr_type, AttrType::Unknown(_)) {
508            debug!("unknown attribute type: {}", raw_code);
509            attributes.push(Attribute {
510                value: AttributeValue::Unknown(AttrRaw {
511                    code: raw_code,
512                    bytes: attr_data,
513                }),
514                flag,
515            });
516            continue;
517        }
518
519        if is_raw_retained_attr(attr_type) {
520            debug!("raw-retained attribute type: {}", raw_code);
521            attributes.push(Attribute {
522                value: AttributeValue::Raw(AttrRaw {
523                    code: raw_code,
524                    bytes: attr_data,
525                }),
526                flag,
527            });
528            continue;
529        }
530
531        let attr = match attr_type {
532            AttrType::ORIGIN => parse_origin(attr_data),
533            AttrType::AS_PATH => parse_as_path(attr_data, asn_len).map(AttributeValue::AsPath),
534            AttrType::NEXT_HOP => parse_next_hop(attr_data, &afi),
535            AttrType::MULTI_EXIT_DISCRIMINATOR => parse_med(attr_data),
536            AttrType::LOCAL_PREFERENCE => parse_local_pref(attr_data),
537            AttrType::ATOMIC_AGGREGATE => Ok(AttributeValue::AtomicAggregate),
538            AttrType::AGGREGATOR => parse_aggregator(attr_data, asn_len)
539                .map(|(asn, id)| AttributeValue::Aggregator { asn, id }),
540            AttrType::ORIGINATOR_ID => parse_originator_id(attr_data),
541            AttrType::CLUSTER_LIST => parse_clusters(attr_data),
542            AttrType::MP_REACHABLE_NLRI => {
543                parse_nlri(attr_data, &afi, &safi, &prefixes, true, add_path)
544            }
545            AttrType::MP_UNREACHABLE_NLRI => {
546                parse_nlri(attr_data, &afi, &safi, &prefixes, false, add_path)
547            }
548            AttrType::AS4_PATH => {
549                parse_as_path(attr_data, &AsnLength::Bits32).map(AttributeValue::As4Path)
550            }
551            AttrType::AS4_AGGREGATOR => parse_aggregator(attr_data, &AsnLength::Bits32)
552                .map(|(asn, id)| AttributeValue::As4Aggregator { asn, id }),
553
554            // communities
555            AttrType::COMMUNITIES => parse_regular_communities(attr_data),
556            AttrType::LARGE_COMMUNITIES => parse_large_communities(attr_data),
557            AttrType::EXTENDED_COMMUNITIES => parse_extended_community(attr_data),
558            AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES => {
559                parse_ipv6_extended_community(attr_data)
560            }
561            AttrType::DEVELOPMENT => {
562                let mut value = vec![];
563                for _i in 0..attr_length {
564                    value.push(attr_data.read_u8()?);
565                }
566                Ok(AttributeValue::Development(value))
567            }
568            AttrType::ONLY_TO_CUSTOMER => parse_only_to_customer(attr_data),
569            AttrType::AIGP => parse_aigp(attr_data),
570            AttrType::TUNNEL_ENCAPSULATION => parse_tunnel_encapsulation_attribute(attr_data),
571            AttrType::TRAFFIC_ENGINEERING => parse_traffic_engineering(attr_data),
572            AttrType::BGP_LS_ATTRIBUTE => parse_link_state_attribute(attr_data),
573            AttrType::BGP_DOMAIN_PATH => parse_domain_path(attr_data),
574            AttrType::SFP_ATTRIBUTE => parse_sfp(attr_data),
575            AttrType::BFD_DISCRIMINATOR => parse_bfd_discriminator(attr_data),
576            AttrType::BGP_PREFIX_SID => parse_bgp_prefix_sid(attr_data),
577            AttrType::BIER => parse_bier(attr_data),
578            _ => Err(ParserError::Unsupported(format!(
579                "unsupported attribute type: {attr_type:?}"
580            ))),
581        };
582
583        match attr {
584            Ok(value) => {
585                assert_eq!(attr_type, value.attr_type());
586                attributes.push(Attribute { value, flag });
587            }
588            Err(e) => {
589                validation.observe_parse_error(attr_type, partial, &e);
590                attributes.push(Attribute {
591                    value: AttributeValue::Raw(AttrRaw {
592                        code: raw_code,
593                        bytes: section.slice(value_start..value_start + attr_length),
594                    }),
595                    flag,
596                });
597                continue;
598            }
599        };
600    }
601
602    let (mut validation_warnings, attr_mask) = validation.finish();
603    if attributes
604        .iter()
605        .any(|attribute| matches!(attribute.value, AttributeValue::DomainPath(_)))
606    {
607        let declared = afi.zip(safi).map(|(afi, safi)| (afi as u16, safi as u8));
608        let families = DomainPathFamilies::from_attributes(&attributes, declared);
609        if let Some(warning) = domain_path_family_warning(families) {
610            validation_warnings.push(warning);
611        }
612    }
613
614    Ok(Attributes {
615        inner: attributes,
616        validation_warnings,
617        attr_mask,
618    })
619}
620
621impl Attribute {
622    /// Append the wire representation of this attribute to `buf`.
623    ///
624    /// The length-field width follows the attribute's EXTENDED_LENGTH flag; a
625    /// value that does not fit the resulting field yields
626    /// [`EncodingError::ValueTooLarge`] instead of being truncated.
627    pub fn encode_to(&self, asn_len: AsnLength, buf: &mut BytesMut) -> Result<(), EncodingError> {
628        buf.put_u8(self.flag.bits());
629        buf.put_u8(self.value.attr_code());
630
631        let write_value = |b: &mut BytesMut| -> Result<(), EncodingError> {
632            match &self.value {
633                AttributeValue::Origin(v) => b.put_slice(&encode_origin(v)),
634                // AS_PATH segment width follows the session's AS number length.
635                AttributeValue::AsPath(path) => {
636                    encode_as_path(path, asn_len, b)?;
637                }
638                // AS4_PATH segments are 4-octet by definition (RFC 6793 §4.2),
639                // independent of the session's AS number length.
640                AttributeValue::As4Path(path) => {
641                    encode_as_path(path, AsnLength::Bits32, b)?;
642                }
643                AttributeValue::NextHop(v) => b.put_slice(&encode_next_hop(v)),
644                AttributeValue::MultiExitDiscriminator(v) => b.put_slice(&encode_med(*v)),
645                AttributeValue::LocalPreference(v) => b.put_slice(&encode_local_pref(*v)),
646                AttributeValue::OnlyToCustomer(v) => {
647                    b.put_slice(&encode_only_to_customer(v.into()))
648                }
649                AttributeValue::AtomicAggregate => {}
650                AttributeValue::Aggregator { asn, id } => {
651                    b.put_slice(&encode_aggregator(asn, &IpAddr::from(*id), asn_len)?)
652                }
653                AttributeValue::As4Aggregator { asn, id } => b.put_slice(&encode_aggregator(
654                    asn,
655                    &IpAddr::from(*id),
656                    AsnLength::Bits32,
657                )?),
658                AttributeValue::Communities(v) => b.put_slice(&encode_regular_communities(v)),
659                AttributeValue::ExtendedCommunities(v) => {
660                    b.put_slice(&encode_extended_communities(v))
661                }
662                AttributeValue::LargeCommunities(v) => b.put_slice(&encode_large_communities(v)),
663                AttributeValue::Ipv6AddressSpecificExtendedCommunities(v) => {
664                    b.put_slice(&encode_ipv6_extended_communities(v))
665                }
666                AttributeValue::OriginatorId(v) => {
667                    b.put_slice(&encode_originator_id(&IpAddr::from(*v)))
668                }
669                AttributeValue::Clusters(v) => b.put_slice(&encode_clusters(v)),
670                AttributeValue::MpReachNlri(v) => {
671                    // Infer ADD-PATH from presence of path_id in any labeled prefix
672                    let add_path = v
673                        .labeled_prefixes
674                        .as_ref()
675                        .is_some_and(|prefixes| prefixes.iter().any(|p| p.path_id.is_some()));
676                    encode_nlri(v, true, add_path, b)?;
677                }
678                AttributeValue::MpUnreachNlri(v) => {
679                    // Withdrawals don't use ADD-PATH encoding per RFC 8277
680                    encode_nlri(v, false, false, b)?;
681                }
682                AttributeValue::LinkState(v) => encode_link_state_attribute(v, b)?,
683                AttributeValue::TunnelEncapsulation(v) => {
684                    encode_tunnel_encapsulation_attribute(v, b)?
685                }
686                AttributeValue::TrafficEngineering(v) => encode_traffic_engineering(v, b)?,
687                AttributeValue::DomainPath(v) => encode_domain_path(v, b)?,
688                AttributeValue::BfdDiscriminator(v) => encode_bfd_discriminator(v, b)?,
689                AttributeValue::BgpPrefixSid(v) => encode_bgp_prefix_sid(v, b)?,
690                AttributeValue::Bier(v) => encode_bier(v, b)?,
691                AttributeValue::Sfp(v) => encode_sfp(v, b)?,
692                AttributeValue::Development(v) => b.put_slice(v),
693                AttributeValue::Raw(v) => b.put_slice(&v.bytes),
694                AttributeValue::Deprecated(v) => b.put_slice(&v.bytes),
695                AttributeValue::Unknown(v) => b.put_slice(&v.bytes),
696                AttributeValue::Aigp(v) => b.put_slice(&encode_aigp(v)),
697                AttributeValue::AttrSet(_v) => {
698                    return Err(EncodingError::unencodable(
699                        "ATTR_SET attribute",
700                        "encoding not implemented",
701                    ));
702                }
703            }
704            Ok(())
705        };
706
707        match self.is_extended() {
708            false => with_u8_len(buf, "BGP attribute value length", write_value),
709            true => with_u16_len(buf, "BGP attribute value length (extended)", write_value),
710        }
711    }
712
713    /// Encode this attribute into a fresh buffer.
714    pub fn encode(&self, asn_len: AsnLength) -> Result<Bytes, EncodingError> {
715        let mut buf = BytesMut::new();
716        self.encode_to(asn_len, &mut buf)?;
717        Ok(buf.freeze())
718    }
719}
720
721impl Attributes {
722    /// Append the wire representation of all attributes to `buf`.
723    pub fn encode_to(&self, asn_len: AsnLength, buf: &mut BytesMut) -> Result<(), EncodingError> {
724        for attr in &self.inner {
725            attr.encode_to(asn_len, buf)?;
726        }
727        Ok(())
728    }
729
730    /// Encode all attributes into a fresh buffer.
731    pub fn encode(&self, asn_len: AsnLength) -> Result<Bytes, EncodingError> {
732        let mut buf = BytesMut::new();
733        self.encode_to(asn_len, &mut buf)?;
734        Ok(buf.freeze())
735    }
736}
737
738#[cfg(test)]
739mod tests {
740    use super::*;
741
742    #[test]
743    fn test_unknwon_attribute_type() {
744        let data = Bytes::from(vec![0x40, 0xFE, 0x00]);
745        let asn_len = AsnLength::Bits16;
746        let add_path = false;
747        let afi = None;
748        let safi = None;
749        let prefixes = None;
750        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes);
751        assert!(attributes.is_ok());
752        let attributes = attributes.unwrap();
753        assert_eq!(attributes.inner.len(), 1);
754        assert_eq!(
755            attributes.inner[0].value.attr_type(),
756            AttrType::Unknown(254)
757        );
758    }
759
760    #[test]
761    fn test_rfc7606_attribute_flags_error() {
762        // Create an ORIGIN attribute with wrong flags (should be transitive, not optional)
763        let data = Bytes::from(vec![0x80, 0x01, 0x01, 0x00]); // Optional flag set incorrectly
764        let asn_len = AsnLength::Bits16;
765        let add_path = false;
766        let afi = None;
767        let safi = None;
768        let prefixes = None;
769
770        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
771
772        // Should have validation warning for incorrect flags
773        assert!(attributes.has_validation_warnings());
774        let warnings = attributes.validation_warnings();
775        // Will have attribute flags error + missing mandatory attributes
776        assert!(!warnings.is_empty());
777
778        match &warnings[0] {
779            BgpValidationWarning::AttributeFlagsError { attr_type, .. } => {
780                assert_eq!(*attr_type, AttrType::ORIGIN);
781            }
782            _ => panic!("Expected AttributeFlagsError warning"),
783        }
784    }
785
786    #[test]
787    fn test_rfc7606_missing_mandatory_attribute() {
788        // Attributes with only LOCAL_PREF (missing ORIGIN, AS_PATH, NEXT_HOP)
789        let data = Bytes::from(vec![
790            0x40, 0x05, 0x04, 0x00, 0x00, 0x00, 0x64, // LOCAL_PREF = 100
791        ]);
792        let asn_len = AsnLength::Bits16;
793        let add_path = false;
794        let afi = None;
795        let safi = None;
796        let prefixes = None;
797
798        let mut attributes =
799            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
800        // Manually trigger mandatory check as an announcement with standard NLRI
801        attributes.check_mandatory_attributes(true, true);
802
803        // Should have warnings for missing mandatory attributes
804        assert!(attributes.has_validation_warnings());
805        let warnings = attributes.validation_warnings();
806        // LOCAL_PREF is not a withdrawal, so ORIGIN, AS_PATH, NEXT_HOP are required
807        assert_eq!(warnings.len(), 3); // ORIGIN, AS_PATH, NEXT_HOP
808
809        for warning in warnings {
810            match warning {
811                BgpValidationWarning::MissingWellKnownAttribute { attr_type } => {
812                    assert!(matches!(
813                        attr_type,
814                        AttrType::ORIGIN | AttrType::AS_PATH | AttrType::NEXT_HOP
815                    ));
816                }
817                _ => panic!("Expected MissingWellKnownAttribute warning"),
818            }
819        }
820    }
821
822    #[test]
823    fn test_mp_reach_no_next_hop() {
824        // Attributes with MP_REACH_NLRI (missing ORIGIN, AS_PATH)
825        // MP_REACH_NLRI is type 14 (0x0E).
826        // We just need a dummy MP_REACH_NLRI.
827        let data = Bytes::from(vec![
828            0x80, 0x0E, 0x06, 0x00, 0x01, 0x01, // AFI=1, SAFI=1
829            0x00, // Next Hop Len = 0 (invalid for parsing, but enough to trigger logic)
830            0x00, // Reserved
831            0x00, // NLRI
832        ]);
833        let asn_len = AsnLength::Bits16;
834        let add_path = false;
835        let afi = None;
836        let safi = None;
837        let prefixes = None;
838
839        let mut attributes =
840            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
841        // Manually trigger mandatory check as an announcement but NO standard NLRI (MP only)
842        attributes.check_mandatory_attributes(true, false);
843
844        // Should NOT have NEXT_HOP warning because MP_REACH_NLRI is present and has_standard_nlri is false
845        let warnings = attributes.validation_warnings();
846        let has_next_hop_warning = warnings.iter().any(|w| {
847            matches!(
848                w,
849                BgpValidationWarning::MissingWellKnownAttribute {
850                    attr_type: AttrType::NEXT_HOP
851                }
852            )
853        });
854        assert!(!has_next_hop_warning);
855    }
856
857    #[test]
858    fn test_pure_withdrawal_no_warnings() {
859        // Empty attributes - pure withdrawal
860        let data = Bytes::from(vec![]);
861        let asn_len = AsnLength::Bits16;
862        let add_path = false;
863        let afi = None;
864        let safi = None;
865        let prefixes = None;
866
867        let mut attributes =
868            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
869        // Manually trigger mandatory check as a withdrawal
870        attributes.check_mandatory_attributes(false, false);
871
872        // Should have NO warnings
873        assert!(!attributes.has_validation_warnings());
874
875        // Attributes with only MP_UNREACH_NLRI - pure withdrawal
876        let data = Bytes::from(vec![
877            0x80, 0x0F, 0x03, 0x00, 0x01, 0x01, // AFI=1, SAFI=1
878        ]);
879        let mut attributes =
880            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
881        attributes.check_mandatory_attributes(false, false);
882        assert!(!attributes.has_validation_warnings());
883    }
884
885    #[test]
886    fn test_rfc7606_duplicate_attribute() {
887        // Create two ORIGIN attributes
888        let data = Bytes::from(vec![
889            0x40, 0x01, 0x01, 0x00, // First ORIGIN attribute
890            0x40, 0x01, 0x01, 0x01, // Second ORIGIN attribute (duplicate)
891        ]);
892        let asn_len = AsnLength::Bits16;
893        let add_path = false;
894        let afi = None;
895        let safi = None;
896        let prefixes = None;
897
898        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
899
900        // Should have warning for duplicate attribute
901        assert!(attributes.has_validation_warnings());
902        let warnings = attributes.validation_warnings();
903
904        // Should have at least one duplicate attribute warning
905        let has_duplicate_warning = warnings
906            .iter()
907            .any(|w| matches!(w, BgpValidationWarning::DuplicateAttribute { .. }));
908        assert!(has_duplicate_warning);
909    }
910
911    #[test]
912    fn test_attribute_type_boundaries() {
913        let asn_len = AsnLength::Bits16;
914        let add_path = false;
915        let afi = None;
916        let safi = None;
917        let prefixes = None;
918
919        // Required attributes for valid BGP message
920        const REQUIRED_ATTRS: &[u8] = &[
921            0x40, 0x01, 0x01, 0x00, // origin
922            0x40, 0x02, 0x00, // as_path
923            0x40, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, // next_hop
924        ];
925
926        // Test highest (development) attribute type
927        let mut data = REQUIRED_ATTRS.to_vec();
928        data.extend_from_slice(&[0x40, 0xFF, 0x01, 0x00]); // development
929        let data = Bytes::from(data);
930
931        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
932
933        assert!(attributes.has_attr(AttrType::DEVELOPMENT));
934        assert!(!attributes.has_validation_warnings());
935
936        // Test lowest (reserved) attribute type
937        let mut data = REQUIRED_ATTRS.to_vec();
938        data.extend_from_slice(&[0x40, 0x00, 0x01, 0x01]); // reserved
939        let data = Bytes::from(data);
940
941        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
942
943        assert!(attributes.inner.iter().any(|attr| {
944            matches!(
945                &attr.value,
946                AttributeValue::Raw(raw)
947                    if raw.code == u8::from(AttrType::RESERVED)
948                        && raw.bytes == Bytes::from_static(&[0x01])
949            )
950        }));
951        assert!(!attributes.validation_warnings.iter().any(|vw| {
952            matches!(vw, BgpValidationWarning::OptionalAttributeError { attr_type, reason:_ } if *attr_type == AttrType::RESERVED)
953        }));
954    }
955
956    #[test]
957    fn test_raw_retention_for_known_unsupported_attribute() {
958        let data = Bytes::from(vec![0x80, 0x16, 0x03, 0xaa, 0xbb, 0xcc]); // PMSI_TUNNEL
959        let attributes =
960            parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap();
961
962        assert_eq!(attributes.inner.len(), 1);
963        match &attributes.inner[0].value {
964            AttributeValue::Raw(raw) => {
965                assert_eq!(raw.code, 22);
966                assert_eq!(raw.attr_type(), AttrType::PMSI_TUNNEL);
967                assert_eq!(raw.bytes, Bytes::from_static(&[0xaa, 0xbb, 0xcc]));
968            }
969            value => panic!("expected Raw, got {value:?}"),
970        }
971        assert_eq!(
972            attributes.encode(AsnLength::Bits16).unwrap(),
973            Bytes::from_static(&[0x80, 0x16, 0x03, 0xaa, 0xbb, 0xcc])
974        );
975    }
976
977    #[test]
978    fn test_deprecated_code_13_retained_as_deprecated() {
979        let data = Bytes::from(vec![0x80, 0x0d, 0x04, 0x01, 0x02, 0x03, 0x04]);
980        let attributes =
981            parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap();
982
983        assert_eq!(attributes.inner.len(), 1);
984        match &attributes.inner[0].value {
985            AttributeValue::Deprecated(raw) => {
986                assert_eq!(raw.code, 13);
987                assert_eq!(raw.attr_type(), AttrType::Unknown(13));
988                assert_eq!(raw.bytes, Bytes::from_static(&[0x01, 0x02, 0x03, 0x04]));
989            }
990            value => panic!("expected Deprecated, got {value:?}"),
991        }
992        assert_eq!(
993            attributes.encode(AsnLength::Bits16).unwrap(),
994            Bytes::from_static(&[0x80, 0x0d, 0x04, 0x01, 0x02, 0x03, 0x04])
995        );
996    }
997
998    #[test]
999    fn test_malformed_typed_attribute_falls_back_to_raw() {
1000        let data = Bytes::from(vec![0x40, 0x03, 0x03, 0x01, 0x02, 0x03]); // NEXT_HOP length must be 4
1001        let attributes =
1002            parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap();
1003
1004        assert!(attributes.has_validation_warnings());
1005        match &attributes.inner[0].value {
1006            AttributeValue::Raw(raw) => {
1007                assert_eq!(raw.code, 3);
1008                assert_eq!(raw.attr_type(), AttrType::NEXT_HOP);
1009                assert_eq!(raw.bytes, Bytes::from_static(&[0x01, 0x02, 0x03]));
1010            }
1011            value => panic!("expected Raw fallback, got {value:?}"),
1012        }
1013        assert_eq!(
1014            attributes.encode(AsnLength::Bits16).unwrap(),
1015            Bytes::from_static(&[0x40, 0x03, 0x03, 0x01, 0x02, 0x03])
1016        );
1017    }
1018
1019    #[test]
1020    fn test_malformed_attribute_after_others_keeps_its_own_bytes() {
1021        let data = Bytes::from(
1022            [
1023                // ORIGIN IGP
1024                &[0x40, 0x01, 0x01, 0x00][..],
1025                // NEXT_HOP with an extended length of 3 (must be 4)
1026                &[0x50, 0x03, 0x00, 0x03, 0x0a, 0x0b, 0x0c],
1027                // MED 7
1028                &[0x80, 0x04, 0x04, 0x00, 0x00, 0x00, 0x07],
1029            ]
1030            .concat(),
1031        );
1032        let attributes =
1033            parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap();
1034
1035        assert_eq!(attributes.inner.len(), 3);
1036        assert_eq!(
1037            attributes.inner[0].value,
1038            AttributeValue::Origin(Origin::IGP)
1039        );
1040        match &attributes.inner[1].value {
1041            AttributeValue::Raw(raw) => {
1042                assert_eq!(raw.code, 3);
1043                assert_eq!(raw.bytes, Bytes::from_static(&[0x0a, 0x0b, 0x0c]));
1044            }
1045            value => panic!("expected Raw fallback, got {value:?}"),
1046        }
1047        assert_eq!(
1048            attributes.inner[2].value,
1049            AttributeValue::MultiExitDiscriminator(7)
1050        );
1051    }
1052
1053    #[test]
1054    fn test_all_raw_retained_attribute_codes_parse_and_round_trip() {
1055        let raw_codes = [0, 22, 24, 27, 33, 128];
1056
1057        for code in raw_codes {
1058            let wire = vec![0xc0, code, 0x02, 0xaa, 0xbb];
1059            let attributes = parse_attributes(
1060                Bytes::from(wire.clone()),
1061                &AsnLength::Bits16,
1062                false,
1063                None,
1064                None,
1065                None,
1066            )
1067            .unwrap();
1068            assert_eq!(attributes.inner.len(), 1, "code {code}");
1069            match &attributes.inner[0].value {
1070                AttributeValue::Raw(raw) => {
1071                    assert_eq!(raw.code, code);
1072                    assert_eq!(raw.bytes, Bytes::from_static(&[0xaa, 0xbb]));
1073                }
1074                value => panic!("expected Raw for code {code}, got {value:?}"),
1075            }
1076            assert!(attributes.has_attr(AttrType::from(code)), "code {code}");
1077            assert_eq!(
1078                attributes.encode(AsnLength::Bits16).unwrap(),
1079                Bytes::from(wire)
1080            );
1081        }
1082    }
1083
1084    #[test]
1085    fn test_unassigned_attribute_code_retained_as_unknown() {
1086        let wire = vec![0xc0, 0x7f, 0x02, 0xaa, 0xbb];
1087        let attributes = parse_attributes(
1088            Bytes::from(wire.clone()),
1089            &AsnLength::Bits16,
1090            false,
1091            None,
1092            None,
1093            None,
1094        )
1095        .unwrap();
1096
1097        assert_eq!(attributes.inner.len(), 1);
1098        match &attributes.inner[0].value {
1099            AttributeValue::Unknown(raw) => {
1100                assert_eq!(raw.code, 0x7f);
1101                assert_eq!(raw.attr_type(), AttrType::Unknown(0x7f));
1102                assert_eq!(raw.bytes, Bytes::from_static(&[0xaa, 0xbb]));
1103            }
1104            value => panic!("expected Unknown, got {value:?}"),
1105        }
1106        assert!(attributes.has_attr(AttrType::Unknown(0x7f)));
1107        assert_eq!(
1108            attributes.encode(AsnLength::Bits16).unwrap(),
1109            Bytes::from(wire)
1110        );
1111    }
1112
1113    #[test]
1114    fn test_structured_tlv_attributes_parse_and_round_trip() {
1115        let cases = [
1116            (
1117                vec![0xc0, 0x26, 0x05, 0x01, 0x01, 0x02, 0x03, 0x04],
1118                "BFD Discriminator",
1119            ),
1120            (
1121                vec![0xc0, 0x28, 0x05, 0x7f, 0x00, 0x02, 0xaa, 0xbb],
1122                "BGP Prefix-SID",
1123            ),
1124            (
1125                vec![0xc0, 0x29, 0x06, 0x12, 0x34, 0x00, 0x02, 0xde, 0xad],
1126                "BIER",
1127            ),
1128            (vec![0xc0, 0x25, 0x05, 0x7f, 0x00, 0x02, 0xde, 0xad], "SFP"),
1129            (
1130                vec![
1131                    0xc0, 0x24, 0x0f, // BGP Domain Path, 15-octet value
1132                    0x02, // segment: 2 domains
1133                    0x00, 0x00, 0x00, 0x0A, 0x00, 0x02, 0x80, // ASN 10 / 2 / 128
1134                    0x00, 0x00, 0x00, 0x0B, 0x00, 0x03, 0x46, // ASN 11 / 3 / 70
1135                ],
1136                "BGP Domain Path",
1137            ),
1138        ];
1139
1140        for (wire, name) in cases {
1141            let data = Bytes::from(wire.clone());
1142            let attributes =
1143                parse_attributes(data, &AsnLength::Bits16, false, None, None, None).unwrap();
1144            assert_eq!(attributes.inner.len(), 1, "{name}");
1145            match (name, &attributes.inner[0].value) {
1146                ("BFD Discriminator", AttributeValue::BfdDiscriminator(_))
1147                | ("BGP Prefix-SID", AttributeValue::BgpPrefixSid(_))
1148                | ("BIER", AttributeValue::Bier(_))
1149                | ("SFP", AttributeValue::Sfp(_))
1150                | ("BGP Domain Path", AttributeValue::DomainPath(_)) => {}
1151                (_, value) => panic!("unexpected value for {name}: {value:?}"),
1152            }
1153            assert_eq!(
1154                attributes.encode(AsnLength::Bits16).unwrap(),
1155                Bytes::from(wire),
1156                "{name}"
1157            );
1158        }
1159    }
1160
1161    #[test]
1162    fn test_rfc7606_attribute_length_error() {
1163        // Create an ORIGIN attribute with wrong length (should be 1 byte, not 2)
1164        let data = Bytes::from(vec![0x40, 0x01, 0x02, 0x00, 0x01]);
1165        let asn_len = AsnLength::Bits16;
1166        let add_path = false;
1167        let afi = None;
1168        let safi = None;
1169        let prefixes = None;
1170
1171        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
1172
1173        // Should have warning for incorrect attribute length
1174        assert!(attributes.has_validation_warnings());
1175        let warnings = attributes.validation_warnings();
1176
1177        let has_length_warning = warnings
1178            .iter()
1179            .any(|w| matches!(w, BgpValidationWarning::AttributeLengthError { .. }));
1180        assert!(has_length_warning);
1181    }
1182
1183    #[test]
1184    fn test_rfc7606_no_session_reset() {
1185        // Test that parsing continues even with multiple errors
1186        let data = Bytes::from(vec![
1187            0x80, 0x01, 0x02, 0x00, 0x01, // Wrong flags and length for ORIGIN
1188            0x40, 0x01, 0x01, 0x00, // Duplicate ORIGIN
1189            0x40, 0xFF, 0x01, 0x00, // Unknown attribute
1190        ]);
1191        let asn_len = AsnLength::Bits16;
1192        let add_path = false;
1193        let afi = None;
1194        let safi = None;
1195        let prefixes = None;
1196
1197        // Should not panic or return error - RFC 7606 requires continued parsing
1198        let result = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes);
1199        assert!(result.is_ok());
1200
1201        let attributes = result.unwrap();
1202        assert!(attributes.has_validation_warnings());
1203
1204        // Should have multiple warnings but parsing should continue
1205        let warnings = attributes.validation_warnings();
1206        assert!(!warnings.is_empty());
1207    }
1208
1209    #[test]
1210    fn test_encode_as_path_attribute_type_selection() {
1211        // Regression test for issue #329: an AS_PATH built for a 4-octet
1212        // session must encode as attribute type 2 with 4-octet segments, not
1213        // as AS4_PATH (type 17).
1214        let attrs = Attributes::from_iter(vec![AttributeValue::AsPath(AsPath::from_sequence([
1215            400644,
1216        ]))]);
1217        let buf = attrs.encode(AsnLength::Bits32).unwrap();
1218        assert_eq!(buf[1], 2, "attribute type must be AS_PATH (2)");
1219        // segment header (type 2, count 1) + one 4-octet ASN
1220        assert_eq!(&buf[3..], &[0x02, 0x01, 0x00, 0x06, 0x1D, 0x04]);
1221
1222        // AS4_PATH always encodes as type 17 with 4-octet segments, even on a
1223        // 2-octet session (RFC 6793 §4.2).
1224        let attrs = Attributes::from_iter(vec![AttributeValue::As4Path(AsPath::from_sequence([
1225            400644,
1226        ]))]);
1227        let buf = attrs.encode(AsnLength::Bits16).unwrap();
1228        assert_eq!(buf[1], 17, "attribute type must be AS4_PATH (17)");
1229        assert_eq!(&buf[3..], &[0x02, 0x01, 0x00, 0x06, 0x1D, 0x04]);
1230    }
1231
1232    #[test]
1233    fn test_encode_as_path_on_2octet_session() {
1234        let attrs = Attributes::from_iter(vec![AttributeValue::AsPath(AsPath::from_sequence([
1235            64496, 64497,
1236        ]))]);
1237        let buf = attrs.encode(AsnLength::Bits16).unwrap();
1238        assert_eq!(buf[1], 2);
1239        // segment header (type 2, count 2) + two 2-octet ASNs
1240        assert_eq!(&buf[3..], &[0x02, 0x02, 0xFB, 0xF0, 0xFB, 0xF1]);
1241    }
1242
1243    #[test]
1244    fn test_encode_as_path_rejects_4octet_asn_on_2octet_session() {
1245        // A 4-octet AS number that does not fit a 2-octet segment must be an
1246        // encoding error, not a silent truncation to its low 16 bits.
1247        let attrs = Attributes::from_iter(vec![AttributeValue::AsPath(AsPath::from_sequence([
1248            400644,
1249        ]))]);
1250        let err = attrs.encode(AsnLength::Bits16).unwrap_err();
1251        assert_eq!(
1252            err,
1253            EncodingError::ValueTooLarge {
1254                field: "2-octet AS number in AS_PATH segment",
1255                actual: 400644,
1256                max: 65535,
1257            }
1258        );
1259    }
1260
1261    #[test]
1262    fn test_encode_aggregator_attribute_type_selection() {
1263        let attrs = Attributes::from_iter(vec![AttributeValue::As4Aggregator {
1264            asn: Asn::new_32bit(400644),
1265            id: std::net::Ipv4Addr::new(192, 0, 2, 1),
1266        }]);
1267        let buf = attrs.encode(AsnLength::Bits16).unwrap();
1268        assert_eq!(buf[1], 18, "attribute type must be AS4_AGGREGATOR (18)");
1269        assert_eq!(
1270            buf.len(),
1271            3 + 8,
1272            "value must be a 4-octet ASN plus an IPv4 id"
1273        );
1274    }
1275
1276    #[test]
1277    fn test_encode_aggregator_width_follows_session_not_asn_flag() {
1278        // A 4-octet-flagged Asn holding a value that fits 16 bits must encode
1279        // as 2 octets on a 2-octet session: the session width decides, not the
1280        // Asn's internal flag.
1281        let attrs = Attributes::from_iter(vec![AttributeValue::Aggregator {
1282            asn: Asn::new_32bit(258),
1283            id: std::net::Ipv4Addr::new(10, 0, 0, 1),
1284        }]);
1285        let buf = attrs.encode(AsnLength::Bits16).unwrap();
1286        assert_eq!(buf[1], 7, "attribute type must be AGGREGATOR (7)");
1287        assert_eq!(buf.len(), 3 + 6);
1288        assert_eq!(&buf[3..], &[0x01, 0x02, 10, 0, 0, 1]);
1289    }
1290
1291    #[test]
1292    fn test_parse_attributes_maps_as4_wire_types() {
1293        // AS4_PATH (type 17): optional transitive flag 0xC0, one AS_SEQUENCE
1294        // segment with the 4-octet ASN 400644.
1295        let data = Bytes::from(vec![0xC0, 17, 6, 0x02, 0x01, 0x00, 0x06, 0x1D, 0x04]);
1296        let attributes = parse_attributes(data, &AsnLength::Bits16, false, None, None, None)
1297            .expect("AS4_PATH must parse");
1298        match attributes.get_attr(AttrType::AS4_PATH).unwrap().value {
1299            AttributeValue::As4Path(path) => assert_eq!(
1300                path.to_u32_vec_opt(false).unwrap(),
1301                vec![400644],
1302                "AS4_PATH segments must parse as 4-octet regardless of session length"
1303            ),
1304            other => panic!("expected As4Path variant, got {other:?}"),
1305        }
1306
1307        // AS4_AGGREGATOR (type 18): 4-octet ASN plus an IPv4 id.
1308        let data = Bytes::from(vec![0xC0, 18, 8, 0x00, 0x06, 0x1D, 0x04, 192, 0, 2, 1]);
1309        let attributes = parse_attributes(data, &AsnLength::Bits16, false, None, None, None)
1310            .expect("AS4_AGGREGATOR must parse");
1311        match attributes.get_attr(AttrType::AS4_AGGREGATOR).unwrap().value {
1312            AttributeValue::As4Aggregator { asn, .. } => {
1313                assert_eq!(asn, Asn::new_32bit(400644))
1314            }
1315            other => panic!("expected As4Aggregator variant, got {other:?}"),
1316        }
1317    }
1318}