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_29_linkstate;
14mod attr_32_large_communities;
15mod attr_35_otc;
16
17use bytes::{Buf, BufMut, Bytes, BytesMut};
18use log::{debug, warn};
19use std::net::IpAddr;
20
21use crate::models::*;
22
23use crate::error::{BgpValidationWarning, ParserError};
24use crate::parser::bgp::attributes::attr_01_origin::{encode_origin, parse_origin};
25use crate::parser::bgp::attributes::attr_02_17_as_path::encode_as_path;
26pub(crate) use crate::parser::bgp::attributes::attr_02_17_as_path::parse_as_path;
27use crate::parser::bgp::attributes::attr_03_next_hop::{encode_next_hop, parse_next_hop};
28use crate::parser::bgp::attributes::attr_04_med::{encode_med, parse_med};
29use crate::parser::bgp::attributes::attr_05_local_pref::{encode_local_pref, parse_local_pref};
30use crate::parser::bgp::attributes::attr_07_18_aggregator::{encode_aggregator, parse_aggregator};
31use crate::parser::bgp::attributes::attr_08_communities::{
32    encode_regular_communities, parse_regular_communities,
33};
34use crate::parser::bgp::attributes::attr_09_originator::{
35    encode_originator_id, parse_originator_id,
36};
37use crate::parser::bgp::attributes::attr_10_13_cluster::{encode_clusters, parse_clusters};
38use crate::parser::bgp::attributes::attr_14_15_nlri::encode_nlri;
39pub(crate) use crate::parser::bgp::attributes::attr_14_15_nlri::parse_nlri;
40use crate::parser::bgp::attributes::attr_16_25_extended_communities::{
41    encode_extended_communities, encode_ipv6_extended_communities, parse_extended_community,
42    parse_ipv6_extended_community,
43};
44use crate::parser::bgp::attributes::attr_23_tunnel_encap::{
45    encode_tunnel_encapsulation_attribute, parse_tunnel_encapsulation_attribute,
46};
47use crate::parser::bgp::attributes::attr_29_linkstate::{
48    encode_link_state_attribute, parse_link_state_attribute,
49};
50use crate::parser::bgp::attributes::attr_32_large_communities::{
51    encode_large_communities, parse_large_communities,
52};
53use crate::parser::bgp::attributes::attr_35_otc::{
54    encode_only_to_customer, parse_only_to_customer,
55};
56use crate::parser::ReadUtils;
57
58/// Validate attribute flags according to RFC 4271 and RFC 7606
59fn validate_attribute_flags(
60    attr_type: AttrType,
61    flags: AttrFlags,
62    warnings: &mut Vec<BgpValidationWarning>,
63) {
64    let expected_flags = match attr_type {
65        // Well-known mandatory attributes
66        AttrType::ORIGIN | AttrType::AS_PATH | AttrType::NEXT_HOP => AttrFlags::TRANSITIVE,
67        // Well-known discretionary attributes
68        AttrType::ATOMIC_AGGREGATE => AttrFlags::TRANSITIVE,
69        // Optional non-transitive attributes
70        AttrType::MULTI_EXIT_DISCRIMINATOR
71        | AttrType::ORIGINATOR_ID
72        | AttrType::CLUSTER_LIST
73        | AttrType::MP_REACHABLE_NLRI
74        | AttrType::MP_UNREACHABLE_NLRI => AttrFlags::OPTIONAL,
75        // Optional transitive attributes
76        AttrType::AGGREGATOR
77        | AttrType::AS4_AGGREGATOR
78        | AttrType::AS4_PATH
79        | AttrType::COMMUNITIES
80        | AttrType::EXTENDED_COMMUNITIES
81        | AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES
82        | AttrType::LARGE_COMMUNITIES
83        | AttrType::ONLY_TO_CUSTOMER => AttrFlags::OPTIONAL | AttrFlags::TRANSITIVE,
84        // LOCAL_PREFERENCE is well-known mandatory for IBGP
85        AttrType::LOCAL_PREFERENCE => AttrFlags::TRANSITIVE,
86        // Unknown or development attributes
87        _ => return, // Don't validate unknown attributes
88    };
89
90    // Check if flags match expected (ignoring EXTENDED and PARTIAL flags for this check)
91    let relevant_flags = flags & (AttrFlags::OPTIONAL | AttrFlags::TRANSITIVE);
92    if relevant_flags != expected_flags {
93        warnings.push(BgpValidationWarning::AttributeFlagsError {
94            attr_type,
95            expected_flags: expected_flags.bits(),
96            actual_flags: relevant_flags.bits(),
97        });
98    }
99
100    // Check partial flag constraint
101    if flags.contains(AttrFlags::PARTIAL) {
102        match attr_type {
103            // Partial bit MUST be 0 for well-known attributes and optional non-transitive
104            AttrType::ORIGIN
105            | AttrType::AS_PATH
106            | AttrType::NEXT_HOP
107            | AttrType::LOCAL_PREFERENCE
108            | AttrType::ATOMIC_AGGREGATE
109            | AttrType::MULTI_EXIT_DISCRIMINATOR
110            | AttrType::ORIGINATOR_ID
111            | AttrType::CLUSTER_LIST
112            | AttrType::MP_REACHABLE_NLRI
113            | AttrType::MP_UNREACHABLE_NLRI => {
114                warnings.push(BgpValidationWarning::AttributeFlagsError {
115                    attr_type,
116                    expected_flags: expected_flags.bits(),
117                    actual_flags: flags.bits(),
118                });
119            }
120            _ => {} // Partial is OK for optional transitive attributes
121        }
122    }
123}
124
125/// Check if an attribute type is well-known mandatory
126fn is_well_known_mandatory(attr_type: AttrType) -> bool {
127    matches!(
128        attr_type,
129        AttrType::ORIGIN | AttrType::AS_PATH | AttrType::NEXT_HOP | AttrType::LOCAL_PREFERENCE
130    )
131}
132
133pub(crate) struct AttributeValidationState {
134    warnings: Vec<BgpValidationWarning>,
135    attr_mask: [u64; 4],
136}
137
138impl AttributeValidationState {
139    pub(crate) fn new() -> Self {
140        Self {
141            warnings: Vec::new(),
142            attr_mask: [0; 4],
143        }
144    }
145
146    fn has_raw_attr(&self, attr: u8) -> bool {
147        (self.attr_mask[(attr / 64) as usize] & (1u64 << (attr % 64))) != 0
148    }
149
150    pub(crate) fn has_attr(&self, attr_type: AttrType) -> bool {
151        self.has_raw_attr(u8::from(attr_type))
152    }
153
154    fn set_attr(&mut self, attr: u8) {
155        self.attr_mask[(attr / 64) as usize] |= 1u64 << (attr % 64);
156    }
157
158    pub(crate) fn observe_header(
159        &mut self,
160        raw_attr_type: u8,
161        attr_type: AttrType,
162        flags: AttrFlags,
163        length: usize,
164    ) -> bool {
165        if self.has_raw_attr(raw_attr_type) {
166            self.warnings
167                .push(BgpValidationWarning::DuplicateAttribute { attr_type });
168        }
169        self.set_attr(raw_attr_type);
170
171        validate_attribute_flags(attr_type, flags, &mut self.warnings);
172        validate_attribute_length(attr_type, length, &mut self.warnings);
173
174        flags.contains(AttrFlags::PARTIAL)
175    }
176
177    pub(crate) fn observe_parse_error(
178        &mut self,
179        attr_type: AttrType,
180        partial: bool,
181        error: &ParserError,
182    ) {
183        if partial {
184            self.warnings
185                .push(BgpValidationWarning::PartialAttributeError {
186                    attr_type,
187                    reason: error.to_string(),
188                });
189            debug!("PARTIAL attribute error: {}", error);
190        } else if is_well_known_mandatory(attr_type) {
191            self.warnings
192                .push(BgpValidationWarning::MalformedAttributeList {
193                    reason: format!(
194                        "Well-known mandatory attribute {} parsing failed: {}",
195                        u8::from(attr_type),
196                        error
197                    ),
198                });
199            debug!(
200                "Well-known mandatory attribute parsing failed, treating as withdraw: {}",
201                error
202            );
203        } else {
204            self.warnings
205                .push(BgpValidationWarning::OptionalAttributeError {
206                    attr_type,
207                    reason: error.to_string(),
208                });
209            debug!("Optional attribute error, discarding: {}", error);
210        }
211    }
212
213    pub(crate) fn check_mandatory_attributes(
214        &mut self,
215        is_announcement: bool,
216        has_standard_nlri: bool,
217    ) {
218        if !is_announcement {
219            return;
220        }
221
222        if !self.has_attr(AttrType::ORIGIN) {
223            self.warnings
224                .push(BgpValidationWarning::MissingWellKnownAttribute {
225                    attr_type: AttrType::ORIGIN,
226                });
227        }
228
229        if !self.has_attr(AttrType::AS_PATH) {
230            self.warnings
231                .push(BgpValidationWarning::MissingWellKnownAttribute {
232                    attr_type: AttrType::AS_PATH,
233                });
234        }
235
236        let has_mp_reach = self.has_attr(AttrType::MP_REACHABLE_NLRI);
237        if (has_standard_nlri || !has_mp_reach) && !self.has_attr(AttrType::NEXT_HOP) {
238            self.warnings
239                .push(BgpValidationWarning::MissingWellKnownAttribute {
240                    attr_type: AttrType::NEXT_HOP,
241                });
242        }
243    }
244
245    pub(crate) fn finish(self) -> (Vec<BgpValidationWarning>, [u64; 4]) {
246        (self.warnings, self.attr_mask)
247    }
248}
249
250/// Validate attribute length constraints
251fn validate_attribute_length(
252    attr_type: AttrType,
253    length: usize,
254    warnings: &mut Vec<BgpValidationWarning>,
255) {
256    let expected_length = match attr_type {
257        AttrType::ORIGIN => Some(1),
258        AttrType::NEXT_HOP => Some(4), // IPv4 next hop
259        AttrType::MULTI_EXIT_DISCRIMINATOR => Some(4),
260        AttrType::LOCAL_PREFERENCE => Some(4),
261        AttrType::ATOMIC_AGGREGATE => Some(0),
262        AttrType::ORIGINATOR_ID => Some(4),
263        AttrType::ONLY_TO_CUSTOMER => Some(4),
264        // Variable length attributes - no fixed constraint
265        AttrType::AS_PATH
266        | AttrType::AS4_PATH
267        | AttrType::AGGREGATOR
268        | AttrType::AS4_AGGREGATOR
269        | AttrType::COMMUNITIES
270        | AttrType::EXTENDED_COMMUNITIES
271        | AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES
272        | AttrType::LARGE_COMMUNITIES
273        | AttrType::CLUSTER_LIST
274        | AttrType::MP_REACHABLE_NLRI
275        | AttrType::MP_UNREACHABLE_NLRI => None,
276        _ => None, // Unknown attributes
277    };
278
279    if let Some(expected) = expected_length {
280        if length != expected {
281            warnings.push(BgpValidationWarning::AttributeLengthError {
282                attr_type,
283                expected_length: Some(expected),
284                actual_length: length,
285            });
286        }
287    }
288}
289
290/// Parse BGP attributes given a slice of u8 and some options.
291///
292/// The `data: &[u8]` contains the entirety of the attributes bytes, therefore the size of
293/// the slice is the total byte length of the attributes section of the message.
294pub fn parse_attributes(
295    mut data: Bytes,
296    asn_len: &AsnLength,
297    add_path: bool,
298    afi: Option<Afi>,
299    safi: Option<Safi>,
300    prefixes: Option<&[NetworkPrefix]>,
301) -> Result<Attributes, ParserError> {
302    // Estimate capacity from data size: each attribute is at least 3 bytes
303    // (flag + type + length). Cap at 256 to avoid over-allocation for corrupted data.
304    let estimated_attrs = (data.remaining() / 3).min(256);
305    let mut attributes: Vec<Attribute> = Vec::with_capacity(estimated_attrs.max(8));
306    let mut validation = AttributeValidationState::new();
307
308    while data.remaining() >= 3 {
309        // each attribute is at least 3 bytes: flag(1) + type(1) + length(1)
310        // thus the while loop condition is set to be at least 3 bytes to read.
311
312        // has content to read
313        let flag = AttrFlags::from_bits_retain(data.read_u8()?);
314        let attr_type = data.read_u8()?;
315        let attr_length = match flag.contains(AttrFlags::EXTENDED) {
316            false => data.read_u8()? as usize,
317            true => data.read_u16()? as usize,
318        };
319
320        debug!(
321            "reading attribute: type -- {:?}, length -- {}",
322            &attr_type, attr_length
323        );
324
325        let parsed_attr_type = AttrType::from(attr_type);
326
327        let partial = validation.observe_header(attr_type, parsed_attr_type, flag, attr_length);
328
329        let attr_type = match parsed_attr_type {
330            attr_type @ AttrType::Unknown(unknown_type) => {
331                // skip pass the remaining bytes of this attribute
332                let bytes = data.read_n_bytes(attr_length)?;
333                let attr_value = match get_deprecated_attr_type(unknown_type) {
334                    Some(t) => {
335                        debug!("deprecated attribute type: {} - {}", unknown_type, t);
336                        AttributeValue::Deprecated(AttrRaw { attr_type, bytes })
337                    }
338                    None => {
339                        debug!("unknown attribute type: {}", unknown_type);
340                        AttributeValue::Unknown(AttrRaw { attr_type, bytes })
341                    }
342                };
343
344                assert_eq!(attr_type, attr_value.attr_type());
345                attributes.push(Attribute {
346                    value: attr_value,
347                    flag,
348                });
349                continue;
350            }
351            t => t,
352        };
353
354        let bytes_left = data.remaining();
355
356        if data.remaining() < attr_length {
357            warn!(
358                "{:?} attribute encodes a length ({}) that is longer than the remaining attribute data ({}). Skipping remaining attribute data for BGP message",
359                attr_type, attr_length, bytes_left
360            );
361            // break and return already parsed attributes
362            break;
363        }
364
365        // we know data has enough bytes to read, so we can split the bytes into a new Bytes object
366        data.has_n_remaining(attr_length)?;
367        let mut attr_data = data.split_to(attr_length);
368
369        let attr = match attr_type {
370            AttrType::ORIGIN => parse_origin(attr_data),
371            AttrType::AS_PATH => {
372                parse_as_path(attr_data, asn_len).map(|path| AttributeValue::AsPath {
373                    path,
374                    is_as4: false,
375                })
376            }
377            AttrType::NEXT_HOP => parse_next_hop(attr_data, &afi),
378            AttrType::MULTI_EXIT_DISCRIMINATOR => parse_med(attr_data),
379            AttrType::LOCAL_PREFERENCE => parse_local_pref(attr_data),
380            AttrType::ATOMIC_AGGREGATE => Ok(AttributeValue::AtomicAggregate),
381            AttrType::AGGREGATOR => {
382                parse_aggregator(attr_data, asn_len).map(|(asn, id)| AttributeValue::Aggregator {
383                    asn,
384                    id,
385                    is_as4: false,
386                })
387            }
388            AttrType::ORIGINATOR_ID => parse_originator_id(attr_data),
389            AttrType::CLUSTER_LIST => parse_clusters(attr_data),
390            AttrType::MP_REACHABLE_NLRI => {
391                parse_nlri(attr_data, &afi, &safi, &prefixes, true, add_path)
392            }
393            AttrType::MP_UNREACHABLE_NLRI => {
394                parse_nlri(attr_data, &afi, &safi, &prefixes, false, add_path)
395            }
396            AttrType::AS4_PATH => parse_as_path(attr_data, &AsnLength::Bits32)
397                .map(|path| AttributeValue::AsPath { path, is_as4: true }),
398            AttrType::AS4_AGGREGATOR => {
399                parse_aggregator(attr_data, &AsnLength::Bits32).map(|(asn, id)| {
400                    AttributeValue::Aggregator {
401                        asn,
402                        id,
403                        is_as4: true,
404                    }
405                })
406            }
407
408            // communities
409            AttrType::COMMUNITIES => parse_regular_communities(attr_data),
410            AttrType::LARGE_COMMUNITIES => parse_large_communities(attr_data),
411            AttrType::EXTENDED_COMMUNITIES => parse_extended_community(attr_data),
412            AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES => {
413                parse_ipv6_extended_community(attr_data)
414            }
415            AttrType::DEVELOPMENT => {
416                let mut value = vec![];
417                for _i in 0..attr_length {
418                    value.push(attr_data.read_u8()?);
419                }
420                Ok(AttributeValue::Development(value))
421            }
422            AttrType::ONLY_TO_CUSTOMER => parse_only_to_customer(attr_data),
423            AttrType::TUNNEL_ENCAPSULATION => parse_tunnel_encapsulation_attribute(attr_data),
424            AttrType::BGP_LS_ATTRIBUTE => parse_link_state_attribute(attr_data),
425            _ => Err(ParserError::Unsupported(format!(
426                "unsupported attribute type: {attr_type:?}"
427            ))),
428        };
429
430        match attr {
431            Ok(value) => {
432                assert_eq!(attr_type, value.attr_type());
433                attributes.push(Attribute { value, flag });
434            }
435            Err(e) => {
436                validation.observe_parse_error(attr_type, partial, &e);
437                continue;
438            }
439        };
440    }
441
442    let (validation_warnings, attr_mask) = validation.finish();
443    Ok(Attributes {
444        inner: attributes,
445        validation_warnings,
446        attr_mask,
447    })
448}
449
450impl Attribute {
451    pub fn encode(&self, asn_len: AsnLength) -> Bytes {
452        let mut bytes = BytesMut::new();
453
454        let flag = self.flag.bits();
455        let type_code = self.value.attr_type().into();
456
457        bytes.put_u8(flag);
458        bytes.put_u8(type_code);
459
460        let value_bytes = match &self.value {
461            AttributeValue::Origin(v) => encode_origin(v),
462            AttributeValue::AsPath { path, is_as4 } => {
463                let four_byte = match is_as4 {
464                    true => AsnLength::Bits32,
465                    false => match asn_len.is_four_byte() {
466                        true => AsnLength::Bits32,
467                        false => AsnLength::Bits16,
468                    },
469                };
470                encode_as_path(path, four_byte)
471            }
472            AttributeValue::NextHop(v) => encode_next_hop(v),
473            AttributeValue::MultiExitDiscriminator(v) => encode_med(*v),
474            AttributeValue::LocalPreference(v) => encode_local_pref(*v),
475            AttributeValue::OnlyToCustomer(v) => encode_only_to_customer(v.into()),
476            AttributeValue::AtomicAggregate => Bytes::default(),
477            AttributeValue::Aggregator { asn, id, is_as4: _ } => {
478                encode_aggregator(asn, &IpAddr::from(*id))
479            }
480            AttributeValue::Communities(v) => encode_regular_communities(v),
481            AttributeValue::ExtendedCommunities(v) => encode_extended_communities(v),
482            AttributeValue::LargeCommunities(v) => encode_large_communities(v),
483            AttributeValue::Ipv6AddressSpecificExtendedCommunities(v) => {
484                encode_ipv6_extended_communities(v)
485            }
486            AttributeValue::OriginatorId(v) => encode_originator_id(&IpAddr::from(*v)),
487            AttributeValue::Clusters(v) => encode_clusters(v),
488            AttributeValue::MpReachNlri(v) => {
489                // Infer ADD-PATH from presence of path_id in any labeled prefix
490                let add_path = v
491                    .labeled_prefixes
492                    .as_ref()
493                    .is_some_and(|prefixes| prefixes.iter().any(|p| p.path_id.is_some()));
494                encode_nlri(v, true, add_path).unwrap_or_else(|e| {
495                    log::warn!("Failed to encode MP_REACH_NLRI: {}", e);
496                    Bytes::new()
497                })
498            }
499            AttributeValue::MpUnreachNlri(v) => {
500                // Withdrawals don't use ADD-PATH encoding per RFC 8277
501                encode_nlri(v, false, false).unwrap_or_else(|e| {
502                    log::warn!("Failed to encode MP_UNREACH_NLRI: {}", e);
503                    Bytes::new()
504                })
505            }
506            AttributeValue::LinkState(v) => encode_link_state_attribute(v),
507            AttributeValue::TunnelEncapsulation(v) => encode_tunnel_encapsulation_attribute(v),
508            AttributeValue::Development(v) => Bytes::from(v.to_owned()),
509            AttributeValue::Deprecated(v) => Bytes::from(v.bytes.to_owned()),
510            AttributeValue::Unknown(v) => Bytes::from(v.bytes.to_owned()),
511            AttributeValue::Aigp(_v) => {
512                // AIGP encoding not yet implemented - return empty bytes
513                Bytes::new()
514            }
515            AttributeValue::AttrSet(_v) => {
516                // ATTR_SET encoding not yet implemented - return empty bytes
517                Bytes::new()
518            }
519        };
520
521        match self.is_extended() {
522            false => {
523                bytes.put_u8(value_bytes.len() as u8);
524            }
525            true => {
526                bytes.put_u16(value_bytes.len() as u16);
527            }
528        }
529        bytes.extend(value_bytes);
530        bytes.freeze()
531    }
532}
533
534impl Attributes {
535    pub fn encode(&self, asn_len: AsnLength) -> Bytes {
536        let mut bytes = BytesMut::new();
537        for attr in &self.inner {
538            bytes.extend(attr.encode(asn_len));
539        }
540        bytes.freeze()
541    }
542}
543
544#[cfg(test)]
545mod tests {
546    use super::*;
547
548    #[test]
549    fn test_unknwon_attribute_type() {
550        let data = Bytes::from(vec![0x40, 0xFE, 0x00]);
551        let asn_len = AsnLength::Bits16;
552        let add_path = false;
553        let afi = None;
554        let safi = None;
555        let prefixes = None;
556        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes);
557        assert!(attributes.is_ok());
558        let attributes = attributes.unwrap();
559        assert_eq!(attributes.inner.len(), 1);
560        assert_eq!(
561            attributes.inner[0].value.attr_type(),
562            AttrType::Unknown(254)
563        );
564    }
565
566    #[test]
567    fn test_rfc7606_attribute_flags_error() {
568        // Create an ORIGIN attribute with wrong flags (should be transitive, not optional)
569        let data = Bytes::from(vec![0x80, 0x01, 0x01, 0x00]); // Optional flag set incorrectly
570        let asn_len = AsnLength::Bits16;
571        let add_path = false;
572        let afi = None;
573        let safi = None;
574        let prefixes = None;
575
576        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
577
578        // Should have validation warning for incorrect flags
579        assert!(attributes.has_validation_warnings());
580        let warnings = attributes.validation_warnings();
581        // Will have attribute flags error + missing mandatory attributes
582        assert!(!warnings.is_empty());
583
584        match &warnings[0] {
585            BgpValidationWarning::AttributeFlagsError { attr_type, .. } => {
586                assert_eq!(*attr_type, AttrType::ORIGIN);
587            }
588            _ => panic!("Expected AttributeFlagsError warning"),
589        }
590    }
591
592    #[test]
593    fn test_rfc7606_missing_mandatory_attribute() {
594        // Attributes with only LOCAL_PREF (missing ORIGIN, AS_PATH, NEXT_HOP)
595        let data = Bytes::from(vec![
596            0x40, 0x05, 0x04, 0x00, 0x00, 0x00, 0x64, // LOCAL_PREF = 100
597        ]);
598        let asn_len = AsnLength::Bits16;
599        let add_path = false;
600        let afi = None;
601        let safi = None;
602        let prefixes = None;
603
604        let mut attributes =
605            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
606        // Manually trigger mandatory check as an announcement with standard NLRI
607        attributes.check_mandatory_attributes(true, true);
608
609        // Should have warnings for missing mandatory attributes
610        assert!(attributes.has_validation_warnings());
611        let warnings = attributes.validation_warnings();
612        // LOCAL_PREF is not a withdrawal, so ORIGIN, AS_PATH, NEXT_HOP are required
613        assert_eq!(warnings.len(), 3); // ORIGIN, AS_PATH, NEXT_HOP
614
615        for warning in warnings {
616            match warning {
617                BgpValidationWarning::MissingWellKnownAttribute { attr_type } => {
618                    assert!(matches!(
619                        attr_type,
620                        AttrType::ORIGIN | AttrType::AS_PATH | AttrType::NEXT_HOP
621                    ));
622                }
623                _ => panic!("Expected MissingWellKnownAttribute warning"),
624            }
625        }
626    }
627
628    #[test]
629    fn test_mp_reach_no_next_hop() {
630        // Attributes with MP_REACH_NLRI (missing ORIGIN, AS_PATH)
631        // MP_REACH_NLRI is type 14 (0x0E).
632        // We just need a dummy MP_REACH_NLRI.
633        let data = Bytes::from(vec![
634            0x80, 0x0E, 0x06, 0x00, 0x01, 0x01, // AFI=1, SAFI=1
635            0x00, // Next Hop Len = 0 (invalid for parsing, but enough to trigger logic)
636            0x00, // Reserved
637            0x00, // NLRI
638        ]);
639        let asn_len = AsnLength::Bits16;
640        let add_path = false;
641        let afi = None;
642        let safi = None;
643        let prefixes = None;
644
645        let mut attributes =
646            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
647        // Manually trigger mandatory check as an announcement but NO standard NLRI (MP only)
648        attributes.check_mandatory_attributes(true, false);
649
650        // Should NOT have NEXT_HOP warning because MP_REACH_NLRI is present and has_standard_nlri is false
651        let warnings = attributes.validation_warnings();
652        let has_next_hop_warning = warnings.iter().any(|w| {
653            matches!(
654                w,
655                BgpValidationWarning::MissingWellKnownAttribute {
656                    attr_type: AttrType::NEXT_HOP
657                }
658            )
659        });
660        assert!(!has_next_hop_warning);
661    }
662
663    #[test]
664    fn test_pure_withdrawal_no_warnings() {
665        // Empty attributes - pure withdrawal
666        let data = Bytes::from(vec![]);
667        let asn_len = AsnLength::Bits16;
668        let add_path = false;
669        let afi = None;
670        let safi = None;
671        let prefixes = None;
672
673        let mut attributes =
674            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
675        // Manually trigger mandatory check as a withdrawal
676        attributes.check_mandatory_attributes(false, false);
677
678        // Should have NO warnings
679        assert!(!attributes.has_validation_warnings());
680
681        // Attributes with only MP_UNREACH_NLRI - pure withdrawal
682        let data = Bytes::from(vec![
683            0x80, 0x0F, 0x03, 0x00, 0x01, 0x01, // AFI=1, SAFI=1
684        ]);
685        let mut attributes =
686            parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
687        attributes.check_mandatory_attributes(false, false);
688        assert!(!attributes.has_validation_warnings());
689    }
690
691    #[test]
692    fn test_rfc7606_duplicate_attribute() {
693        // Create two ORIGIN attributes
694        let data = Bytes::from(vec![
695            0x40, 0x01, 0x01, 0x00, // First ORIGIN attribute
696            0x40, 0x01, 0x01, 0x01, // Second ORIGIN attribute (duplicate)
697        ]);
698        let asn_len = AsnLength::Bits16;
699        let add_path = false;
700        let afi = None;
701        let safi = None;
702        let prefixes = None;
703
704        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
705
706        // Should have warning for duplicate attribute
707        assert!(attributes.has_validation_warnings());
708        let warnings = attributes.validation_warnings();
709
710        // Should have at least one duplicate attribute warning
711        let has_duplicate_warning = warnings
712            .iter()
713            .any(|w| matches!(w, BgpValidationWarning::DuplicateAttribute { .. }));
714        assert!(has_duplicate_warning);
715    }
716
717    #[test]
718    fn test_attribute_type_boundaries() {
719        let asn_len = AsnLength::Bits16;
720        let add_path = false;
721        let afi = None;
722        let safi = None;
723        let prefixes = None;
724
725        // Required attributes for valid BGP message
726        const REQUIRED_ATTRS: &[u8] = &[
727            0x40, 0x01, 0x01, 0x00, // origin
728            0x40, 0x02, 0x00, // as_path
729            0x40, 0x03, 0x04, 0x01, 0x02, 0x03, 0x04, // next_hop
730        ];
731
732        // Test highest (development) attribute type
733        let mut data = REQUIRED_ATTRS.to_vec();
734        data.extend_from_slice(&[0x40, 0xFF, 0x01, 0x00]); // development
735        let data = Bytes::from(data);
736
737        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
738
739        assert!(attributes.has_attr(AttrType::DEVELOPMENT));
740        assert!(!attributes.has_validation_warnings());
741
742        // Test lowest (reserved) attribute type
743        let mut data = REQUIRED_ATTRS.to_vec();
744        data.extend_from_slice(&[0x40, 0x00, 0x01, 0x01]); // reserved
745        let data = Bytes::from(data);
746
747        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
748
749        // There is a validation warning about the reserved attribute
750        assert!(attributes.validation_warnings.iter().any(|vw| {
751            matches!(vw, BgpValidationWarning::OptionalAttributeError { attr_type, reason:_ } if *attr_type == AttrType::RESERVED)
752        }));
753    }
754
755    #[test]
756    fn test_rfc7606_attribute_length_error() {
757        // Create an ORIGIN attribute with wrong length (should be 1 byte, not 2)
758        let data = Bytes::from(vec![0x40, 0x01, 0x02, 0x00, 0x01]);
759        let asn_len = AsnLength::Bits16;
760        let add_path = false;
761        let afi = None;
762        let safi = None;
763        let prefixes = None;
764
765        let attributes = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes).unwrap();
766
767        // Should have warning for incorrect attribute length
768        assert!(attributes.has_validation_warnings());
769        let warnings = attributes.validation_warnings();
770
771        let has_length_warning = warnings
772            .iter()
773            .any(|w| matches!(w, BgpValidationWarning::AttributeLengthError { .. }));
774        assert!(has_length_warning);
775    }
776
777    #[test]
778    fn test_rfc7606_no_session_reset() {
779        // Test that parsing continues even with multiple errors
780        let data = Bytes::from(vec![
781            0x80, 0x01, 0x02, 0x00, 0x01, // Wrong flags and length for ORIGIN
782            0x40, 0x01, 0x01, 0x00, // Duplicate ORIGIN
783            0x40, 0xFF, 0x01, 0x00, // Unknown attribute
784        ]);
785        let asn_len = AsnLength::Bits16;
786        let add_path = false;
787        let afi = None;
788        let safi = None;
789        let prefixes = None;
790
791        // Should not panic or return error - RFC 7606 requires continued parsing
792        let result = parse_attributes(data, &asn_len, add_path, afi, safi, prefixes);
793        assert!(result.is_ok());
794
795        let attributes = result.unwrap();
796        assert!(attributes.has_validation_warnings());
797
798        // Should have multiple warnings but parsing should continue
799        let warnings = attributes.validation_warnings();
800        assert!(!warnings.is_empty());
801    }
802}