Skip to main content

bgpkit_parser/models/bgp/attributes/
mod.rs

1//! BGP attribute structs
2mod aspath;
3mod nlri;
4mod origin;
5
6use crate::models::network::*;
7use bitflags::bitflags;
8use bytes::Bytes;
9use num_enum::{FromPrimitive, IntoPrimitive};
10use std::cmp::Ordering;
11use std::iter::{FromIterator, Map};
12use std::net::IpAddr;
13use std::slice::Iter;
14use std::vec::IntoIter;
15
16use crate::error::BgpValidationWarning;
17use crate::models::*;
18
19pub use aspath::*;
20pub use nlri::*;
21pub use origin::*;
22
23bitflags! {
24    /// The high-order bit (bit 0) of the Attribute Flags octet is the
25    /// Optional bit.  It defines whether the attribute is optional (if
26    /// set to 1) or well-known (if set to 0).
27    ///
28    /// The second high-order bit (bit 1) of the Attribute Flags octet
29    /// is the Transitive bit.  It defines whether an optional
30    /// attribute is transitive (if set to 1) or non-transitive (if set
31    /// to 0).
32    ///
33    /// For well-known attributes, the Transitive bit MUST be set to 1.
34    /// (See Section 5 for a discussion of transitive attributes.)
35    ///
36    /// The third high-order bit (bit 2) of the Attribute Flags octet
37    /// is the Partial bit.  It defines whether the information
38    /// contained in the optional transitive attribute is partial (if
39    /// set to 1) or complete (if set to 0).  For well-known attributes
40    /// and for optional non-transitive attributes, the Partial bit
41    /// MUST be set to 0.
42    ///
43    /// The fourth high-order bit (bit 3) of the Attribute Flags octet
44    /// is the Extended Length bit.  It defines whether the Attribute
45    /// Length is one octet (if set to 0) or two octets (if set to 1).
46    #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
47    #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
48    pub struct AttrFlags: u8 {
49        const OPTIONAL   = 0b10000000;
50        const TRANSITIVE = 0b01000000;
51        const PARTIAL    = 0b00100000;
52        const EXTENDED   = 0b00010000;
53    }
54}
55
56/// Attribute types.
57///
58/// All attributes currently defined and not Unassigned or Deprecated are included here.
59/// To see the full list, check out IANA at:
60/// <https://www.iana.org/assignments/bgp-parameters/bgp-parameters.xhtml#bgp-parameters-2>
61#[allow(non_camel_case_types)]
62#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone, FromPrimitive, IntoPrimitive)]
63#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
64#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
65#[repr(u8)]
66pub enum AttrType {
67    RESERVED = 0,
68    ORIGIN = 1,
69    AS_PATH = 2,
70    NEXT_HOP = 3,
71    MULTI_EXIT_DISCRIMINATOR = 4,
72    LOCAL_PREFERENCE = 5,
73    ATOMIC_AGGREGATE = 6,
74    AGGREGATOR = 7,
75    COMMUNITIES = 8,
76    /// <https://tools.ietf.org/html/rfc4456>
77    ORIGINATOR_ID = 9,
78    CLUSTER_LIST = 10,
79    /// <https://tools.ietf.org/html/rfc4760>
80    MP_REACHABLE_NLRI = 14,
81    MP_UNREACHABLE_NLRI = 15,
82    /// <https://datatracker.ietf.org/doc/html/rfc4360>
83    EXTENDED_COMMUNITIES = 16,
84    AS4_PATH = 17,
85    AS4_AGGREGATOR = 18,
86    PMSI_TUNNEL = 22,
87    TUNNEL_ENCAPSULATION = 23,
88    TRAFFIC_ENGINEERING = 24,
89    IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES = 25,
90    AIGP = 26,
91    PE_DISTINGUISHER_LABELS = 27,
92    BGP_LS_ATTRIBUTE = 29,
93    LARGE_COMMUNITIES = 32,
94    BGPSEC_PATH = 33,
95    ONLY_TO_CUSTOMER = 35,
96    BGP_DOMAIN_PATH = 36,
97    SFP_ATTRIBUTE = 37,
98    BFD_DISCRIMINATOR = 38,
99    BGP_PREFIX_SID = 40,
100    BIER = 41,
101    ATTR_SET = 128,
102    /// <https://datatracker.ietf.org/doc/html/rfc2042>
103    DEVELOPMENT = 255,
104
105    /// Catch all for any unknown attribute types
106    #[num_enum(catch_all)]
107    // We have to explicitly assign this variant a number, otherwise the compiler will attempt to
108    // assign it to 256 (previous + 1) and overflow the type.
109    Unknown(u8) = 254,
110}
111
112impl PartialOrd for AttrType {
113    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
114        Some(self.cmp(other))
115    }
116}
117
118impl Ord for AttrType {
119    fn cmp(&self, other: &Self) -> Ordering {
120        u8::from(*self).cmp(&u8::from(*other))
121    }
122}
123
124pub fn get_deprecated_attr_type(attr_type: u8) -> Option<&'static str> {
125    match attr_type {
126        11 => Some("DPA"),
127        12 => Some("ADVERTISER"),
128        13 => Some("RCID_PATH"),
129        19 => Some("SAFI Specific Attribute"),
130        20 => Some("Connector Attribute"),
131        21 => Some("AS_PATHLIMIT"),
132        28 => Some("BGP Entropy Label Capability"),
133        30 | 31 | 129 | 241 | 242 | 243 => Some("RFC8093"),
134
135        _ => None,
136    }
137}
138
139/// Convenience wrapper for a list of attributes
140#[derive(PartialEq, Clone, Default, Eq)]
141pub struct Attributes {
142    // Black box type to allow for later changes/optimizations. The most common attributes could be
143    // added as fields to allow for easier lookup.
144    pub(crate) inner: Vec<Attribute>,
145    /// RFC 7606 validation warnings collected during parsing
146    pub(crate) validation_warnings: Vec<BgpValidationWarning>,
147    /// Bitmask of seen attributes to allow O(1) checks. Fits in 4 u64s.
148    pub(crate) attr_mask: [u64; 4],
149}
150
151impl std::fmt::Debug for Attributes {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.debug_struct("Attributes")
154            .field("inner", &self.inner)
155            .field("validation_warnings", &self.validation_warnings)
156            .finish()
157    }
158}
159
160impl Attributes {
161    pub fn has_attr(&self, ty: AttrType) -> bool {
162        let attr = u8::from(ty);
163        (self.attr_mask[(attr / 64) as usize] & (1u64 << (attr % 64))) != 0
164    }
165
166    pub fn get_attr(&self, ty: AttrType) -> Option<Attribute> {
167        self.inner
168            .iter()
169            .find(|x| x.value.attr_type() == ty)
170            .cloned()
171    }
172
173    pub fn add_attr(&mut self, attr: Attribute) {
174        let ty = attr.value.attr_code();
175        self.attr_mask[(ty / 64) as usize] |= 1u64 << (ty % 64);
176        self.inner.push(attr);
177    }
178
179    /// Check for missing well-known mandatory attributes.
180    ///
181    /// RFC 4271 (BGP-4) and RFC 4760 (MP-BGP) define which attributes are mandatory.
182    /// - Pure Withdrawals: NO path attributes are required.
183    /// - Announcements: ORIGIN and AS_PATH are strictly required.
184    /// - NEXT_HOP is required if standard IPv4 NLRI is present or if no MP_REACH_NLRI is present.
185    pub fn check_mandatory_attributes(&mut self, is_announcement: bool, has_standard_nlri: bool) {
186        if !is_announcement {
187            return;
188        }
189
190        // ORIGIN and AS_PATH are universally mandatory for all announcements.
191        if !self.has_attr(AttrType::ORIGIN) {
192            self.validation_warnings
193                .push(BgpValidationWarning::MissingWellKnownAttribute {
194                    attr_type: AttrType::ORIGIN,
195                });
196        }
197        if !self.has_attr(AttrType::AS_PATH) {
198            self.validation_warnings
199                .push(BgpValidationWarning::MissingWellKnownAttribute {
200                    attr_type: AttrType::AS_PATH,
201                });
202        }
203
204        // NEXT_HOP is required if this is an IPv4 announcement (has standard NLRI)
205        // or if we haven't seen MP_REACH_NLRI,
206        // which implies standard NLRI is expected, since is_announcement.
207        let has_mp_reach = self.has_attr(AttrType::MP_REACHABLE_NLRI);
208        if (has_standard_nlri || !has_mp_reach) && !self.has_attr(AttrType::NEXT_HOP) {
209            self.validation_warnings
210                .push(BgpValidationWarning::MissingWellKnownAttribute {
211                    attr_type: AttrType::NEXT_HOP,
212                });
213        }
214    }
215
216    /// RFC 10039 §4 does not allow D-PATH on classic IPv4 unicast NLRI, which an UPDATE can
217    /// carry next to an IPVPN/EVPN MP_REACH_NLRI announcement. The UPDATE parser calls this
218    /// once the NLRI fields are parsed, because the attribute-level check cannot see them.
219    pub(crate) fn check_domain_path_with_classic_nlri(&mut self, has_classic_nlri: bool) {
220        if !has_classic_nlri {
221            return;
222        }
223
224        let has_domain_path = self
225            .inner
226            .iter()
227            .any(|attribute| matches!(attribute.value, AttributeValue::DomainPath(_)));
228        let already_reported = self.validation_warnings.iter().any(|warning| {
229            matches!(
230                warning,
231                BgpValidationWarning::OptionalAttributeError { attr_type, .. }
232                    if *attr_type == AttrType::BGP_DOMAIN_PATH
233            )
234        });
235        if !has_domain_path || already_reported {
236            return;
237        }
238
239        self.add_validation_warning(BgpValidationWarning::OptionalAttributeError {
240            attr_type: AttrType::BGP_DOMAIN_PATH,
241            reason: "D-PATH is only valid on IPVPN or EVPN routes, this UPDATE also carries classic IPv4 unicast NLRI (AFI 1, SAFI 1): RFC 10039 §4 requires treat-as-withdraw".to_string(),
242        });
243    }
244
245    /// Add a validation warning to the attributes
246    pub fn add_validation_warning(&mut self, warning: BgpValidationWarning) {
247        self.validation_warnings.push(warning);
248    }
249
250    /// Get all validation warnings for these attributes
251    pub fn validation_warnings(&self) -> &[BgpValidationWarning] {
252        &self.validation_warnings
253    }
254
255    /// Check if there are any validation warnings
256    pub fn has_validation_warnings(&self) -> bool {
257        !self.validation_warnings.is_empty()
258    }
259
260    /// Get the `ORIGIN` attribute. In the event that this attribute is not present,
261    /// [Origin::INCOMPLETE] will be returned instead.
262    pub fn origin(&self) -> Origin {
263        self.inner
264            .iter()
265            .find_map(|x| match &x.value {
266                AttributeValue::Origin(x) => Some(*x),
267                _ => None,
268            })
269            .unwrap_or(Origin::INCOMPLETE)
270    }
271
272    /// Get the `ORIGINATOR_ID` attribute if present.
273    pub fn origin_id(&self) -> Option<BgpIdentifier> {
274        self.inner.iter().find_map(|x| match &x.value {
275            AttributeValue::OriginatorId(x) => Some(*x),
276            _ => None,
277        })
278    }
279
280    /// Get the `NEXT_HOP` attribute if present.
281    ///
282    /// **Note**: Even when this attribute is not present, the next hop address may still be
283    /// attainable from the `MP_REACH_NLRI` attribute.
284    pub fn next_hop(&self) -> Option<IpAddr> {
285        self.inner.iter().find_map(|x| match &x.value {
286            AttributeValue::NextHop(x) => Some(*x),
287            _ => None,
288        })
289    }
290
291    pub fn multi_exit_discriminator(&self) -> Option<u32> {
292        self.inner.iter().find_map(|x| match &x.value {
293            AttributeValue::MultiExitDiscriminator(x) => Some(*x),
294            _ => None,
295        })
296    }
297
298    pub fn local_preference(&self) -> Option<u32> {
299        self.inner.iter().find_map(|x| match &x.value {
300            AttributeValue::LocalPreference(x) => Some(*x),
301            _ => None,
302        })
303    }
304
305    pub fn only_to_customer(&self) -> Option<Asn> {
306        self.inner.iter().find_map(|x| match &x.value {
307            AttributeValue::OnlyToCustomer(x) => Some(*x),
308            _ => None,
309        })
310    }
311
312    pub fn atomic_aggregate(&self) -> bool {
313        self.inner
314            .iter()
315            .any(|x| matches!(&x.value, AttributeValue::AtomicAggregate))
316    }
317
318    pub fn aggregator(&self) -> Option<(Asn, BgpIdentifier)> {
319        // Begin searching at the end of the attributes to increase the odds of finding an AS4
320        // attribute first.
321        self.inner.iter().rev().find_map(|x| match &x.value {
322            AttributeValue::Aggregator { asn, id } | AttributeValue::As4Aggregator { asn, id } => {
323                Some((*asn, *id))
324            }
325            _ => None,
326        })
327    }
328
329    pub fn clusters(&self) -> Option<&[u32]> {
330        self.inner.iter().find_map(|x| match &x.value {
331            AttributeValue::Clusters(x) => Some(x.as_ref()),
332            _ => None,
333        })
334    }
335
336    // These implementations are horribly inefficient, but they were super easy to write and use
337
338    /// Get the AS_PATH (type 2) attribute value, without merging a
339    /// coexisting AS4_PATH. See [`Attributes::effective_as_path`] for the
340    /// RFC 6793 merged path.
341    pub fn as_path(&self) -> Option<&AsPath> {
342        self.inner.iter().find_map(|x| match &x.value {
343            AttributeValue::AsPath(path) => Some(path),
344            _ => None,
345        })
346    }
347
348    /// Get the AS4_PATH (type 17) attribute value if present.
349    pub fn as4_path(&self) -> Option<&AsPath> {
350        self.inner.iter().find_map(|x| match &x.value {
351            AttributeValue::As4Path(path) => Some(path),
352            _ => None,
353        })
354    }
355
356    /// Get the effective AS path per RFC 6793 §4.2.3.
357    ///
358    /// When both AS_PATH and AS4_PATH are present (a 2-octet session carrying
359    /// 4-octet AS numbers), the two are merged: the leading ASes come from the
360    /// 2-octet AS_PATH and the trailing ASes from the 4-octet AS4_PATH.
361    /// Otherwise whichever attribute is present is returned as-is.
362    pub fn effective_as_path(&self) -> Option<AsPath> {
363        match (self.as_path(), self.as4_path()) {
364            (None, None) => None,
365            (Some(path), None) | (None, Some(path)) => Some(path.clone()),
366            (Some(path), Some(as4_path)) => Some(AsPath::merge_aspath_as4path(path, as4_path)),
367        }
368    }
369
370    pub fn get_reachable_nlri(&self) -> Option<&Nlri> {
371        self.inner.iter().find_map(|x| match &x.value {
372            AttributeValue::MpReachNlri(x) => Some(x),
373            _ => None,
374        })
375    }
376
377    pub fn get_unreachable_nlri(&self) -> Option<&Nlri> {
378        self.inner.iter().find_map(|x| match &x.value {
379            AttributeValue::MpUnreachNlri(x) => Some(x),
380            _ => None,
381        })
382    }
383
384    pub fn iter_communities(&self) -> MetaCommunitiesIter<'_> {
385        MetaCommunitiesIter {
386            attributes: &self.inner,
387            index: 0,
388        }
389    }
390
391    /// Get an iterator over the held [AttributeValue]s. If you also need attribute flags, consider
392    /// using [Attributes::into_attributes_iter] instead.
393    pub fn iter(&self) -> <&'_ Self as IntoIterator>::IntoIter {
394        self.into_iter()
395    }
396
397    /// Get an iterator over the held [Attribute]s. If you do no not need attribute flags, consider
398    /// using [Attributes::iter] instead.
399    pub fn into_attributes_iter(self) -> impl Iterator<Item = Attribute> {
400        self.inner.into_iter()
401    }
402}
403
404pub struct MetaCommunitiesIter<'a> {
405    attributes: &'a [Attribute],
406    index: usize,
407}
408
409impl Iterator for MetaCommunitiesIter<'_> {
410    type Item = MetaCommunity;
411
412    fn next(&mut self) -> Option<Self::Item> {
413        loop {
414            match &self.attributes.first()?.value {
415                AttributeValue::Communities(x) if self.index < x.len() => {
416                    self.index += 1;
417                    return Some(MetaCommunity::Plain(x[self.index - 1]));
418                }
419                AttributeValue::ExtendedCommunities(x) if self.index < x.len() => {
420                    self.index += 1;
421                    return Some(MetaCommunity::Extended(x[self.index - 1]));
422                }
423                AttributeValue::LargeCommunities(x) if self.index < x.len() => {
424                    self.index += 1;
425                    return Some(MetaCommunity::Large(x[self.index - 1]));
426                }
427                _ => {
428                    self.attributes = &self.attributes[1..];
429                    self.index = 0;
430                }
431            }
432        }
433    }
434}
435
436fn compute_mask(inner: &[Attribute]) -> [u64; 4] {
437    let mut attr_mask = [0; 4];
438    for attr in inner {
439        let ty = attr.value.attr_code();
440        attr_mask[(ty / 64) as usize] |= 1u64 << (ty % 64);
441    }
442    attr_mask
443}
444
445impl FromIterator<Attribute> for Attributes {
446    fn from_iter<T: IntoIterator<Item = Attribute>>(iter: T) -> Self {
447        let inner: Vec<Attribute> = iter.into_iter().collect();
448        let attr_mask = compute_mask(&inner);
449        Attributes {
450            inner,
451            validation_warnings: Vec::new(),
452            attr_mask,
453        }
454    }
455}
456
457impl From<Vec<Attribute>> for Attributes {
458    fn from(value: Vec<Attribute>) -> Self {
459        let attr_mask = compute_mask(&value);
460        Attributes {
461            inner: value,
462            validation_warnings: Vec::new(),
463            attr_mask,
464        }
465    }
466}
467
468impl Extend<Attribute> for Attributes {
469    fn extend<T: IntoIterator<Item = Attribute>>(&mut self, iter: T) {
470        for attr in iter {
471            self.add_attr(attr);
472        }
473    }
474}
475
476impl Extend<AttributeValue> for Attributes {
477    fn extend<T: IntoIterator<Item = AttributeValue>>(&mut self, iter: T) {
478        self.extend(iter.into_iter().map(Attribute::from))
479    }
480}
481
482impl FromIterator<AttributeValue> for Attributes {
483    fn from_iter<T: IntoIterator<Item = AttributeValue>>(iter: T) -> Self {
484        let inner: Vec<Attribute> = iter.into_iter().map(Attribute::from).collect();
485        let attr_mask = compute_mask(&inner);
486        Attributes {
487            inner,
488            validation_warnings: Vec::new(),
489            attr_mask,
490        }
491    }
492}
493
494impl IntoIterator for Attributes {
495    type Item = AttributeValue;
496    type IntoIter = Map<IntoIter<Attribute>, fn(Attribute) -> AttributeValue>;
497
498    fn into_iter(self) -> Self::IntoIter {
499        self.inner.into_iter().map(|x| x.value)
500    }
501}
502
503impl<'a> IntoIterator for &'a Attributes {
504    type Item = &'a AttributeValue;
505    type IntoIter = Map<Iter<'a, Attribute>, fn(&Attribute) -> &AttributeValue>;
506
507    fn into_iter(self) -> Self::IntoIter {
508        self.inner.iter().map(|x| &x.value)
509    }
510}
511
512#[cfg(feature = "serde")]
513mod serde_impl {
514    use super::*;
515    use serde::{Deserialize, Deserializer, Serialize, Serializer};
516
517    impl Serialize for Attributes {
518        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
519        where
520            S: Serializer,
521        {
522            self.inner.serialize(serializer)
523        }
524    }
525
526    impl<'de> Deserialize<'de> for Attributes {
527        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
528        where
529            D: Deserializer<'de>,
530        {
531            let inner = <Vec<Attribute>>::deserialize(deserializer)?;
532            let attr_mask = compute_mask(&inner);
533            Ok(Attributes {
534                inner,
535                validation_warnings: Vec::new(),
536                attr_mask,
537            })
538        }
539    }
540}
541
542/// BGP Attribute struct with attribute value and flag
543#[derive(Debug, PartialEq, Clone, Eq)]
544#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
545#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
546pub struct Attribute {
547    pub value: AttributeValue,
548    /// Bitflags serialize as a string like `"OPTIONAL | TRANSITIVE"`.
549    #[cfg_attr(feature = "ts-rs", ts(type = "string"))]
550    pub flag: AttrFlags,
551}
552
553impl Attribute {
554    pub const fn is_optional(&self) -> bool {
555        self.flag.contains(AttrFlags::OPTIONAL)
556    }
557
558    pub const fn is_transitive(&self) -> bool {
559        self.flag.contains(AttrFlags::TRANSITIVE)
560    }
561
562    pub const fn is_partial(&self) -> bool {
563        self.flag.contains(AttrFlags::PARTIAL)
564    }
565
566    pub const fn is_extended(&self) -> bool {
567        self.flag.contains(AttrFlags::EXTENDED)
568    }
569}
570
571impl From<AttributeValue> for Attribute {
572    fn from(value: AttributeValue) -> Self {
573        Attribute {
574            flag: value.default_flags(),
575            value,
576        }
577    }
578}
579
580/// AIGP TLV (Type-Length-Value) entry - RFC 7311
581#[derive(Debug, PartialEq, Clone, Eq)]
582#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
583pub struct AigpTlv {
584    pub tlv_type: u8,
585    pub length: u16,
586    pub value: Bytes,
587}
588
589/// AIGP (Accumulated IGP Metric) Attribute - RFC 7311
590///
591/// Type 26, optional non-transitive attribute containing TLVs.
592/// The AIGP TLV (Type=1) contains an 8-octet accumulated metric value.
593#[derive(Debug, PartialEq, Clone, Eq)]
594#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
595pub struct Aigp {
596    pub tlvs: Vec<AigpTlv>,
597}
598
599impl Aigp {
600    /// Get the accumulated metric from the first AIGP TLV (Type=1)
601    pub fn accumulated_metric(&self) -> Option<u64> {
602        self.tlvs
603            .iter()
604            .find(|tlv| tlv.tlv_type == 1)
605            .and_then(|tlv| {
606                if tlv.value.len() >= 8 {
607                    Some(u64::from_be_bytes([
608                        tlv.value[0],
609                        tlv.value[1],
610                        tlv.value[2],
611                        tlv.value[3],
612                        tlv.value[4],
613                        tlv.value[5],
614                        tlv.value[6],
615                        tlv.value[7],
616                    ]))
617                } else {
618                    None
619                }
620            })
621    }
622}
623
624/// Raw TLV with 1-octet type and 1-octet value length.
625#[derive(Debug, PartialEq, Clone, Eq)]
626#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
627pub struct RawTlv8 {
628    pub tlv_type: u8,
629    pub value: Bytes,
630}
631
632/// Raw TLV with 1-octet type and 2-octet value length.
633#[derive(Debug, PartialEq, Clone, Eq)]
634#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
635pub struct RawTlv8Ext {
636    pub tlv_type: u8,
637    pub value: Bytes,
638}
639
640/// Raw TLV with 2-octet type and 2-octet value length.
641#[derive(Debug, PartialEq, Clone, Eq)]
642#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
643pub struct RawTlv16 {
644    pub tlv_type: u16,
645    pub value: Bytes,
646}
647
648/// BFD Discriminator Attribute - RFC 9026
649#[derive(Debug, PartialEq, Clone, Eq)]
650#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
651pub struct BfdDiscriminatorAttribute {
652    pub mode: u8,
653    pub discriminator: u32,
654    pub tlvs: Vec<RawTlv8>,
655}
656
657/// BGP Prefix-SID Attribute - RFC 8669
658#[derive(Debug, PartialEq, Clone, Eq)]
659#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
660pub struct BgpPrefixSidAttribute {
661    pub tlvs: Vec<RawTlv8Ext>,
662}
663
664/// BIER Attribute - RFC 9793
665#[derive(Debug, PartialEq, Clone, Eq)]
666#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
667pub struct BierAttribute {
668    pub tlvs: Vec<RawTlv16>,
669}
670
671/// SFP Attribute - RFC 9015
672#[derive(Debug, PartialEq, Clone, Eq)]
673#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
674pub struct SfpAttribute {
675    pub tlvs: Vec<RawTlv8Ext>,
676}
677
678/// BGP Domain Path (D-PATH) Attribute - RFC 10039
679///
680/// Optional transitive attribute listing the sequence of domains through
681/// which an EVPN/IPVPN inter-subnet forwarding route has passed.
682#[derive(Debug, PartialEq, Clone, Eq)]
683#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
684pub struct DomainPathAttribute {
685    pub segments: Vec<DomainPathSegment>,
686}
687
688/// One domain segment of a BGP Domain Path (D-PATH) attribute - RFC 10039
689///
690/// RFC 10039 §4 requires every segment to contain at least one domain.
691#[derive(Debug, PartialEq, Clone, Eq)]
692#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
693pub struct DomainPathSegment {
694    pub domains: Vec<DomainPathDomain>,
695}
696
697/// One domain of a BGP Domain Path (D-PATH) attribute - RFC 10039
698///
699/// The Global/Local Administrator pair forms the 6-octet DOMAIN-ID; RFC 10039
700/// §4 recommends reporting both parts as opaque unsigned integers.
701#[derive(Debug, PartialEq, Clone, Eq)]
702#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
703pub struct DomainPathDomain {
704    /// 4-octet Global Administrator (opaque; MAY carry an ASN or IPv4 address).
705    pub global_admin: u32,
706    /// 2-octet Local Administrator (opaque).
707    pub local_admin: u16,
708    /// 1-octet ISF_SAFI_TYPE (0 = gateway PE local ISF route, 70 = EVPN, 128 = IPVPN).
709    pub isf_safi_type: u8,
710}
711
712/// BGP Traffic Engineering Attribute - RFC 5543
713#[derive(Debug, Clone)]
714#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
715pub struct TrafficEngineering {
716    /// Switching Capability code defined by GMPLS.
717    pub switching_capability: u8,
718
719    /// Interface encoding code defined by GMPLS.
720    pub encoding: u8,
721
722    /// Reserved wire value.
723    ///
724    /// RFC 5543 says originators should set this to zero and receivers
725    /// must ignore it.
726    pub reserved: u16,
727
728    /// Maximum LSP bandwidth for priorities 0 through 7, in bytes per second.
729    pub max_lsp_bandwidth: [f32; 8],
730
731    /// Switching-capability-specific information retained as raw bytes.
732    pub switching_capability_specific: Bytes,
733}
734
735impl PartialEq for TrafficEngineering {
736    fn eq(&self, other: &Self) -> bool {
737        self.switching_capability == other.switching_capability
738            && self.encoding == other.encoding
739            && self.reserved == other.reserved
740            && self
741                .max_lsp_bandwidth
742                .iter()
743                .zip(other.max_lsp_bandwidth.iter())
744                .all(|(left, right)| left.to_bits() == right.to_bits())
745            && self.switching_capability_specific == other.switching_capability_specific
746    }
747}
748
749impl Eq for TrafficEngineering {}
750
751/// ATTR_SET Attribute - RFC 6368
752///
753/// Used in BGP/MPLS IP VPNs to transparently carry customer BGP path attributes
754/// through the VPN core. Acts as a stack where attributes are "pushed" at the
755/// PE ingress and "popped" at the PE egress.
756///
757/// Type 128, optional transitive attribute.
758#[derive(Debug, PartialEq, Clone, Eq)]
759#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
760pub struct AttrSet {
761    /// Origin AS number (customer network AS)
762    pub origin_as: Asn,
763    /// Nested path attributes
764    pub attributes: Attributes,
765}
766
767/// The `AttributeValue` enum represents different kinds of Attribute values.
768///
769/// Serde (and therefore the WASM JSON output) uses default external tagging:
770/// data-carrying variants serialize as `{ "<VariantName>": payload }`, while
771/// unit variants (e.g. `AtomicAggregate`) serialize as the bare string
772/// `"<VariantName>"`.
773///
774/// Long-tail variants (`LinkState`, `TunnelEncapsulation`, ...) are typed as
775/// opaque `Record<string, unknown>` in the generated TypeScript; the common
776/// variants are fully typed. `AsPath`/`As4Path` payloads have a custom serde
777/// encoding (flat array of ASNs, nested arrays for AS_SETs), inlined here.
778#[derive(Debug, PartialEq, Clone, Eq)]
779#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
780#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
781pub enum AttributeValue {
782    Origin(Origin),
783    /// AS_PATH (type 2), RFC 4271 §4.3.
784    ///
785    /// On encode, the segment width follows the session's `asn_len` passed to
786    /// [`Attribute::encode_to`]: a 4-octet session encodes 4-octet AS
787    /// numbers directly. This is the variant to use when building announcements.
788    #[cfg_attr(feature = "ts-rs", ts(as = "AsPathWire"))]
789    AsPath(AsPath),
790    /// AS4_PATH (type 17), RFC 6793 §4.2 — the migration fallback that carries
791    /// the full path with 4-octet AS numbers alongside a 2-octet AS_PATH.
792    ///
793    /// Segments always encode as 4-octet regardless of the session's `asn_len`.
794    /// Only speakers sending 4-octet AS numbers over a 2-octet session should
795    /// emit this attribute; RFC 6793 §4.1 forbids it on 4-octet sessions.
796    #[cfg_attr(feature = "ts-rs", ts(as = "AsPathWire"))]
797    As4Path(AsPath),
798    NextHop(IpAddr),
799    MultiExitDiscriminator(u32),
800    LocalPreference(u32),
801    OnlyToCustomer(Asn),
802    AtomicAggregate,
803    /// AGGREGATOR (type 7), RFC 4271 §4.3.8.
804    ///
805    /// On encode, the AS number width follows the session's `asn_len`.
806    Aggregator {
807        asn: Asn,
808        id: BgpIdentifier,
809    },
810    /// AS4_AGGREGATOR (type 18), RFC 6793 — carries the aggregator AS number
811    /// as 4 octets alongside a 2-octet AGGREGATOR. Always encodes the AS
812    /// number as 4 octets.
813    As4Aggregator {
814        asn: Asn,
815        id: BgpIdentifier,
816    },
817    Communities(Vec<Community>),
818    ExtendedCommunities(Vec<ExtendedCommunity>),
819    Ipv6AddressSpecificExtendedCommunities(Vec<Ipv6AddrExtCommunity>),
820    LargeCommunities(Vec<LargeCommunity>),
821    OriginatorId(BgpIdentifier),
822    Clusters(Vec<u32>),
823    MpReachNlri(Nlri),
824    MpUnreachNlri(Nlri),
825    /// BGP Link-State attribute - RFC 7752
826    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
827    LinkState(crate::models::bgp::linkstate::LinkStateAttribute),
828    /// BGP Tunnel Encapsulation attribute - RFC 9012
829    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
830    TunnelEncapsulation(crate::models::bgp::tunnel_encap::TunnelEncapAttribute),
831    /// BGP Traffic Engineering attribute - RFC 5543
832    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
833    TrafficEngineering(TrafficEngineering),
834    /// BFD Discriminator attribute - RFC 9026
835    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
836    BfdDiscriminator(BfdDiscriminatorAttribute),
837    /// BGP Prefix-SID attribute - RFC 8669
838    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
839    BgpPrefixSid(BgpPrefixSidAttribute),
840    /// BIER attribute - RFC 9793
841    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
842    Bier(BierAttribute),
843    /// SFP attribute - RFC 9015
844    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
845    Sfp(SfpAttribute),
846    /// BGP Domain Path (D-PATH) attribute - RFC 10039
847    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
848    DomainPath(DomainPathAttribute),
849    Development(Vec<u8>),
850    Raw(AttrRaw),
851    Deprecated(AttrRaw),
852    Unknown(AttrRaw),
853    /// AIGP (Accumulated IGP Metric) attribute - RFC 7311
854    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
855    Aigp(Aigp),
856    /// ATTR_SET attribute - RFC 6368
857    #[cfg_attr(feature = "ts-rs", ts(type = "Record<string, unknown>"))]
858    AttrSet(AttrSet),
859}
860
861impl From<Origin> for AttributeValue {
862    fn from(value: Origin) -> Self {
863        AttributeValue::Origin(value)
864    }
865}
866
867/// Converts to the AS_PATH (type 2) attribute value. Use [`AttributeValue::As4Path`]
868/// explicitly to build the RFC 6793 migration-fallback attribute.
869impl From<AsPath> for AttributeValue {
870    fn from(path: AsPath) -> Self {
871        AttributeValue::AsPath(path)
872    }
873}
874
875/// Category of an attribute.
876///
877/// <https://datatracker.ietf.org/doc/html/rfc4271#section-5>
878#[derive(Debug, PartialEq, Eq, Hash, Copy, Clone)]
879#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
880pub enum AttributeCategory {
881    WellKnownMandatory,
882    WellKnownDiscretionary,
883    OptionalTransitive,
884    OptionalNonTransitive,
885}
886
887impl AttributeValue {
888    pub fn attr_type(&self) -> AttrType {
889        match self {
890            AttributeValue::Origin(_) => AttrType::ORIGIN,
891            AttributeValue::AsPath(_) => AttrType::AS_PATH,
892            AttributeValue::As4Path(_) => AttrType::AS4_PATH,
893            AttributeValue::NextHop(_) => AttrType::NEXT_HOP,
894            AttributeValue::MultiExitDiscriminator(_) => AttrType::MULTI_EXIT_DISCRIMINATOR,
895            AttributeValue::LocalPreference(_) => AttrType::LOCAL_PREFERENCE,
896            AttributeValue::OnlyToCustomer(_) => AttrType::ONLY_TO_CUSTOMER,
897            AttributeValue::AtomicAggregate => AttrType::ATOMIC_AGGREGATE,
898            AttributeValue::Aggregator { .. } => AttrType::AGGREGATOR,
899            AttributeValue::As4Aggregator { .. } => AttrType::AS4_AGGREGATOR,
900            AttributeValue::Communities(_) => AttrType::COMMUNITIES,
901            AttributeValue::ExtendedCommunities(_) => AttrType::EXTENDED_COMMUNITIES,
902            AttributeValue::Ipv6AddressSpecificExtendedCommunities(_) => {
903                AttrType::IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES
904            }
905            AttributeValue::LargeCommunities(_) => AttrType::LARGE_COMMUNITIES,
906            AttributeValue::OriginatorId(_) => AttrType::ORIGINATOR_ID,
907            AttributeValue::Clusters(_) => AttrType::CLUSTER_LIST,
908            AttributeValue::MpReachNlri(_) => AttrType::MP_REACHABLE_NLRI,
909            AttributeValue::MpUnreachNlri(_) => AttrType::MP_UNREACHABLE_NLRI,
910            AttributeValue::LinkState(_) => AttrType::BGP_LS_ATTRIBUTE,
911            AttributeValue::TunnelEncapsulation(_) => AttrType::TUNNEL_ENCAPSULATION,
912            AttributeValue::TrafficEngineering(_) => AttrType::TRAFFIC_ENGINEERING,
913            AttributeValue::BfdDiscriminator(_) => AttrType::BFD_DISCRIMINATOR,
914            AttributeValue::BgpPrefixSid(_) => AttrType::BGP_PREFIX_SID,
915            AttributeValue::Bier(_) => AttrType::BIER,
916            AttributeValue::Sfp(_) => AttrType::SFP_ATTRIBUTE,
917            AttributeValue::DomainPath(_) => AttrType::BGP_DOMAIN_PATH,
918            AttributeValue::Development(_) => AttrType::DEVELOPMENT,
919            AttributeValue::Raw(x) | AttributeValue::Deprecated(x) | AttributeValue::Unknown(x) => {
920                x.attr_type()
921            }
922            AttributeValue::Aigp(_) => AttrType::AIGP,
923            AttributeValue::AttrSet(_) => AttrType::ATTR_SET,
924        }
925    }
926
927    pub fn attr_code(&self) -> u8 {
928        match self {
929            AttributeValue::Raw(x) | AttributeValue::Deprecated(x) | AttributeValue::Unknown(x) => {
930                x.code
931            }
932            _ => self.attr_type().into(),
933        }
934    }
935
936    pub fn attr_category(&self) -> Option<AttributeCategory> {
937        use AttributeCategory::*;
938
939        match self {
940            AttributeValue::Origin(_) => Some(WellKnownMandatory),
941            AttributeValue::AsPath(_) => Some(WellKnownMandatory),
942            AttributeValue::As4Path(_) => Some(OptionalTransitive),
943            AttributeValue::NextHop(_) => Some(WellKnownMandatory),
944            AttributeValue::MultiExitDiscriminator(_) => Some(OptionalNonTransitive),
945            // If we receive this attribute we must be in IBGP so it is required
946            AttributeValue::LocalPreference(_) => Some(WellKnownMandatory),
947            AttributeValue::OnlyToCustomer(_) => Some(OptionalTransitive),
948            AttributeValue::AtomicAggregate => Some(WellKnownDiscretionary),
949            AttributeValue::Aggregator { .. } => Some(OptionalTransitive),
950            AttributeValue::As4Aggregator { .. } => Some(OptionalTransitive),
951            AttributeValue::Communities(_) => Some(OptionalTransitive),
952            AttributeValue::ExtendedCommunities(_) => Some(OptionalTransitive),
953            AttributeValue::LargeCommunities(_) => Some(OptionalTransitive),
954            AttributeValue::OriginatorId(_) => Some(OptionalNonTransitive),
955            AttributeValue::Clusters(_) => Some(OptionalNonTransitive),
956            AttributeValue::MpReachNlri(_) => Some(OptionalNonTransitive),
957            AttributeValue::MpUnreachNlri(_) => Some(OptionalNonTransitive),
958            AttributeValue::LinkState(_) => Some(OptionalNonTransitive),
959            AttributeValue::TrafficEngineering(_) => Some(OptionalNonTransitive),
960            AttributeValue::Aigp(_) => Some(OptionalNonTransitive),
961            AttributeValue::BfdDiscriminator(_) => Some(OptionalTransitive),
962            AttributeValue::BgpPrefixSid(_) => Some(OptionalTransitive),
963            AttributeValue::Bier(_) => Some(OptionalTransitive),
964            AttributeValue::Sfp(_) => Some(OptionalTransitive),
965            AttributeValue::DomainPath(_) => Some(OptionalTransitive),
966            AttributeValue::AttrSet(_) => Some(OptionalTransitive),
967            _ => None,
968        }
969    }
970
971    /// Get flags based on the attribute type. The [AttrFlags::EXTENDED] is not taken into account
972    /// when determining the correct flags.
973    pub fn default_flags(&self) -> AttrFlags {
974        match self.attr_category() {
975            None => AttrFlags::OPTIONAL | AttrFlags::PARTIAL | AttrFlags::TRANSITIVE,
976            Some(AttributeCategory::WellKnownMandatory) => AttrFlags::TRANSITIVE,
977            Some(AttributeCategory::WellKnownDiscretionary) => AttrFlags::TRANSITIVE,
978            Some(AttributeCategory::OptionalTransitive) => {
979                AttrFlags::OPTIONAL | AttrFlags::TRANSITIVE
980            }
981            Some(AttributeCategory::OptionalNonTransitive) => AttrFlags::OPTIONAL,
982        }
983    }
984}
985
986#[derive(Debug, PartialEq, Clone, Eq)]
987#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
988#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS), ts(export))]
989pub struct AttrRaw {
990    pub code: u8,
991    /// `bytes::Bytes` has no ts-rs impl; it serializes as a JSON byte array.
992    #[cfg_attr(feature = "ts-rs", ts(type = "number[]"))]
993    pub bytes: Bytes,
994}
995
996impl AttrRaw {
997    /// Map the raw wire code back to an `AttrType`.
998    ///
999    /// For `Raw` variants (known-but-unparsed codes like `PMSI_TUNNEL`),
1000    /// this returns the concrete `AttrType` variant (e.g. `AttrType::PMSI_TUNNEL`).
1001    /// For `Deprecated` and `Unknown` variants, this returns `AttrType::Unknown(code)`.
1002    pub fn attr_type(&self) -> AttrType {
1003        AttrType::from(self.code)
1004    }
1005}
1006
1007#[cfg(test)]
1008mod tests {
1009    use super::*;
1010    use std::net::Ipv4Addr;
1011    use std::str::FromStr;
1012
1013    #[test]
1014    fn test_attr_type() {
1015        let attr_value = AttributeValue::Origin(Origin::IGP);
1016        assert_eq!(attr_value.attr_type(), AttrType::ORIGIN);
1017    }
1018
1019    #[test]
1020    fn test_attr_category() {
1021        let attr_value = AttributeValue::Origin(Origin::IGP);
1022        let category = attr_value.attr_category().unwrap();
1023        assert_eq!(category, AttributeCategory::WellKnownMandatory);
1024    }
1025
1026    #[test]
1027    fn test_default_flags() {
1028        let attr_value = AttributeValue::Origin(Origin::IGP);
1029        let flags = attr_value.default_flags();
1030        assert_eq!(flags, AttrFlags::TRANSITIVE);
1031    }
1032
1033    #[test]
1034    fn test_from_iter_attribute_value_uses_default_flags() {
1035        let attributes = Attributes::from_iter(vec![
1036            AttributeValue::Origin(Origin::IGP),
1037            AttributeValue::AsPath(AsPath::new()),
1038        ]);
1039
1040        assert_eq!(
1041            attributes.get_attr(AttrType::ORIGIN).unwrap().flag,
1042            AttrFlags::TRANSITIVE
1043        );
1044        assert_eq!(
1045            attributes.get_attr(AttrType::AS_PATH).unwrap().flag,
1046            AttrFlags::TRANSITIVE
1047        );
1048    }
1049
1050    #[test]
1051    fn test_get_attr() {
1052        let attribute = Attribute {
1053            value: AttributeValue::Origin(Origin::IGP),
1054            flag: AttrFlags::TRANSITIVE,
1055        };
1056
1057        let mut attributes = Attributes::default();
1058        attributes.add_attr(attribute.clone());
1059
1060        assert_eq!(attributes.get_attr(AttrType::ORIGIN), Some(attribute));
1061    }
1062
1063    #[test]
1064    fn test_has_attr() {
1065        let attribute = Attribute {
1066            value: AttributeValue::Origin(Origin::IGP),
1067            flag: AttrFlags::TRANSITIVE,
1068        };
1069
1070        let mut attributes = Attributes::default();
1071        attributes.add_attr(attribute);
1072
1073        assert!(attributes.has_attr(AttrType::ORIGIN));
1074    }
1075
1076    #[test]
1077    fn test_getting_all_attributes() {
1078        let mut attributes = Attributes::default();
1079        attributes.add_attr(Attribute {
1080            value: AttributeValue::Origin(Origin::IGP),
1081            flag: AttrFlags::TRANSITIVE,
1082        });
1083        attributes.add_attr(Attribute {
1084            value: AttributeValue::AsPath(AsPath::new()),
1085            flag: AttrFlags::TRANSITIVE,
1086        });
1087        attributes.add_attr(Attribute {
1088            value: AttributeValue::NextHop(IpAddr::from_str("10.0.0.0").unwrap()),
1089            flag: AttrFlags::TRANSITIVE,
1090        });
1091        attributes.add_attr(Attribute {
1092            value: AttributeValue::MultiExitDiscriminator(1),
1093            flag: AttrFlags::TRANSITIVE,
1094        });
1095
1096        attributes.add_attr(Attribute {
1097            value: AttributeValue::LocalPreference(1),
1098            flag: AttrFlags::TRANSITIVE,
1099        });
1100        attributes.add_attr(Attribute {
1101            value: AttributeValue::OnlyToCustomer(Asn::new_32bit(1)),
1102            flag: AttrFlags::TRANSITIVE,
1103        });
1104        attributes.add_attr(Attribute {
1105            value: AttributeValue::AtomicAggregate,
1106            flag: AttrFlags::TRANSITIVE,
1107        });
1108        attributes.add_attr(Attribute {
1109            value: AttributeValue::Clusters(vec![1, 2, 3]),
1110            flag: AttrFlags::TRANSITIVE,
1111        });
1112        attributes.add_attr(Attribute {
1113            value: AttributeValue::Aggregator {
1114                asn: Asn::new_32bit(1),
1115                id: Ipv4Addr::from_str("0.0.0.0").unwrap(),
1116            },
1117            flag: AttrFlags::TRANSITIVE,
1118        });
1119        attributes.add_attr(Attribute {
1120            value: AttributeValue::OriginatorId(Ipv4Addr::from_str("0.0.0.0").unwrap()),
1121            flag: AttrFlags::TRANSITIVE,
1122        });
1123
1124        assert_eq!(attributes.origin(), Origin::IGP);
1125        assert_eq!(attributes.as_path(), Some(&AsPath::new()));
1126        assert_eq!(
1127            attributes.next_hop(),
1128            Some(IpAddr::from_str("10.0.0.0").unwrap())
1129        );
1130        assert_eq!(attributes.multi_exit_discriminator(), Some(1));
1131        assert_eq!(attributes.local_preference(), Some(1));
1132        assert_eq!(attributes.only_to_customer(), Some(Asn::new_32bit(1)));
1133        assert!(attributes.atomic_aggregate());
1134        assert_eq!(attributes.clusters(), Some(vec![1_u32, 2, 3].as_slice()));
1135        assert_eq!(
1136            attributes.aggregator(),
1137            Some((Asn::new_32bit(1), Ipv4Addr::from_str("0.0.0.0").unwrap()))
1138        );
1139        assert_eq!(
1140            attributes.origin_id(),
1141            Some(Ipv4Addr::from_str("0.0.0.0").unwrap())
1142        );
1143
1144        let aspath_attr = attributes.get_attr(AttrType::AS_PATH).unwrap();
1145        assert!(aspath_attr.is_transitive());
1146        assert!(!aspath_attr.is_extended());
1147        assert!(!aspath_attr.is_partial());
1148        assert!(!aspath_attr.is_optional());
1149
1150        for attr in attributes.iter() {
1151            println!("{attr:?}");
1152        }
1153    }
1154
1155    #[test]
1156    fn test_from() {
1157        let origin = Origin::IGP;
1158        let attr_value = AttributeValue::from(origin);
1159        assert_eq!(attr_value, AttributeValue::Origin(Origin::IGP));
1160
1161        let aspath = AsPath::new();
1162        let attr_value = AttributeValue::from(aspath);
1163        assert_eq!(attr_value, AttributeValue::AsPath(AsPath::new()));
1164    }
1165
1166    #[test]
1167    fn test_well_known_mandatory_attrs() {
1168        let origin_attr = AttributeValue::Origin(Origin::IGP);
1169        assert_eq!(
1170            origin_attr.attr_category(),
1171            Some(AttributeCategory::WellKnownMandatory)
1172        );
1173        let as_path_attr = AttributeValue::AsPath(AsPath::new());
1174        assert_eq!(
1175            as_path_attr.attr_category(),
1176            Some(AttributeCategory::WellKnownMandatory)
1177        );
1178        let next_hop_attr = AttributeValue::NextHop(IpAddr::from_str("10.0.0.0").unwrap());
1179        assert_eq!(
1180            next_hop_attr.attr_category(),
1181            Some(AttributeCategory::WellKnownMandatory)
1182        );
1183        let local_preference_attr = AttributeValue::LocalPreference(1);
1184        assert_eq!(
1185            local_preference_attr.attr_category(),
1186            Some(AttributeCategory::WellKnownMandatory)
1187        );
1188    }
1189
1190    #[test]
1191    fn test_well_known_discretionary_attrs() {
1192        let atomic_aggregate_attr = AttributeValue::AtomicAggregate;
1193        assert_eq!(
1194            atomic_aggregate_attr.attr_category(),
1195            Some(AttributeCategory::WellKnownDiscretionary)
1196        );
1197    }
1198
1199    #[test]
1200    fn test_optional_transitive_attrs() {
1201        let as4_path_attr = AttributeValue::As4Path(AsPath::new());
1202        assert_eq!(
1203            as4_path_attr.attr_type(),
1204            AttrType::AS4_PATH,
1205            "AS4_PATH must map to wire type 17"
1206        );
1207        assert_eq!(
1208            as4_path_attr.attr_category(),
1209            Some(AttributeCategory::OptionalTransitive)
1210        );
1211        let aggregator_attr = AttributeValue::Aggregator {
1212            asn: Asn::new_32bit(1),
1213            id: Ipv4Addr::from_str("0.0.0.0").unwrap(),
1214        };
1215        assert_eq!(
1216            aggregator_attr.attr_category(),
1217            Some(AttributeCategory::OptionalTransitive)
1218        );
1219        let only_to_customer_attr = AttributeValue::OnlyToCustomer(Asn::new_32bit(1));
1220        assert_eq!(
1221            only_to_customer_attr.attr_category(),
1222            Some(AttributeCategory::OptionalTransitive)
1223        );
1224        let communities_attr =
1225            AttributeValue::Communities(vec![Community::Custom(Asn::new_32bit(1), 1)]);
1226        assert_eq!(
1227            communities_attr.attr_category(),
1228            Some(AttributeCategory::OptionalTransitive)
1229        );
1230        let extended_communities_attr =
1231            AttributeValue::ExtendedCommunities(vec![ExtendedCommunity::Raw([0; 8])]);
1232        assert_eq!(
1233            extended_communities_attr.attr_category(),
1234            Some(AttributeCategory::OptionalTransitive)
1235        );
1236        let large_communities_attr =
1237            AttributeValue::LargeCommunities(vec![LargeCommunity::new(1, [1, 1])]);
1238        assert_eq!(
1239            large_communities_attr.attr_category(),
1240            Some(AttributeCategory::OptionalTransitive)
1241        );
1242        let as4_aggregator_attr = AttributeValue::As4Aggregator {
1243            asn: Asn::new_32bit(1),
1244            id: Ipv4Addr::from_str("0.0.0.0").unwrap(),
1245        };
1246        assert_eq!(
1247            as4_aggregator_attr.attr_type(),
1248            AttrType::AS4_AGGREGATOR,
1249            "AS4_AGGREGATOR must map to wire type 18"
1250        );
1251        assert_eq!(
1252            as4_aggregator_attr.attr_category(),
1253            Some(AttributeCategory::OptionalTransitive)
1254        );
1255    }
1256
1257    #[test]
1258    fn test_new_attribute_attr_categories() {
1259        // BFD Discriminator (RFC 9026): Optional Transitive
1260        assert_eq!(
1261            AttributeValue::BfdDiscriminator(BfdDiscriminatorAttribute {
1262                mode: 1,
1263                discriminator: 0,
1264                tlvs: vec![],
1265            })
1266            .attr_category(),
1267            Some(AttributeCategory::OptionalTransitive)
1268        );
1269        // BGP Prefix-SID (RFC 8669): Optional Transitive
1270        assert_eq!(
1271            AttributeValue::BgpPrefixSid(BgpPrefixSidAttribute { tlvs: vec![] }).attr_category(),
1272            Some(AttributeCategory::OptionalTransitive)
1273        );
1274        // BIER (RFC 9793): Optional Transitive
1275        assert_eq!(
1276            AttributeValue::Bier(BierAttribute { tlvs: vec![] }).attr_category(),
1277            Some(AttributeCategory::OptionalTransitive)
1278        );
1279        // SFP (RFC 9015): Optional Transitive
1280        assert_eq!(
1281            AttributeValue::Sfp(SfpAttribute { tlvs: vec![] }).attr_category(),
1282            Some(AttributeCategory::OptionalTransitive)
1283        );
1284        // BGP Domain Path (RFC 10039): Optional Transitive
1285        assert_eq!(
1286            AttributeValue::DomainPath(DomainPathAttribute { segments: vec![] }).attr_category(),
1287            Some(AttributeCategory::OptionalTransitive)
1288        );
1289    }
1290
1291    #[test]
1292    fn test_optional_non_transitive_attrs() {
1293        let multi_exit_discriminator_attr = AttributeValue::MultiExitDiscriminator(1);
1294        assert_eq!(
1295            multi_exit_discriminator_attr.attr_category(),
1296            Some(AttributeCategory::OptionalNonTransitive)
1297        );
1298        let originator_id_attr =
1299            AttributeValue::OriginatorId(Ipv4Addr::from_str("0.0.0.0").unwrap());
1300        assert_eq!(
1301            originator_id_attr.attr_category(),
1302            Some(AttributeCategory::OptionalNonTransitive)
1303        );
1304        let clusters_attr = AttributeValue::Clusters(vec![1, 2, 3]);
1305        assert_eq!(
1306            clusters_attr.attr_category(),
1307            Some(AttributeCategory::OptionalNonTransitive)
1308        );
1309        let mp_unreach_nlri_attr = AttributeValue::MpReachNlri(Nlri::new_unreachable(
1310            NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
1311        ));
1312        assert_eq!(
1313            mp_unreach_nlri_attr.attr_category(),
1314            Some(AttributeCategory::OptionalNonTransitive)
1315        );
1316
1317        let mp_reach_nlri_attr = AttributeValue::MpUnreachNlri(Nlri::new_unreachable(
1318            NetworkPrefix::from_str("10.0.0.0/24").unwrap(),
1319        ));
1320        assert_eq!(
1321            mp_reach_nlri_attr.attr_category(),
1322            Some(AttributeCategory::OptionalNonTransitive)
1323        );
1324
1325        let traffic_engineering_attr = AttributeValue::TrafficEngineering(TrafficEngineering {
1326            switching_capability: 1,
1327            encoding: 1,
1328            reserved: 0,
1329            max_lsp_bandwidth: [0.0; 8],
1330            switching_capability_specific: Bytes::new(),
1331        });
1332
1333        assert_eq!(
1334            traffic_engineering_attr.attr_category(),
1335            Some(AttributeCategory::OptionalNonTransitive)
1336        );
1337
1338        assert_eq!(
1339            traffic_engineering_attr.default_flags(),
1340            AttrFlags::OPTIONAL
1341        );
1342    }
1343
1344    #[test]
1345    #[cfg(feature = "serde")]
1346    fn test_serde() {
1347        let attributes = Attributes::from_iter(vec![
1348            Attribute {
1349                value: AttributeValue::Origin(Origin::IGP),
1350                flag: AttrFlags::TRANSITIVE,
1351            },
1352            Attribute {
1353                value: AttributeValue::AsPath(AsPath::new()),
1354                flag: AttrFlags::TRANSITIVE,
1355            },
1356        ]);
1357
1358        let serialized = serde_json::to_string(&attributes).unwrap();
1359        let deserialized: Attributes = serde_json::from_str(&serialized).unwrap();
1360
1361        assert_eq!(attributes, deserialized);
1362    }
1363
1364    #[test]
1365    fn test_as_path_accessors() {
1366        let mut attributes = Attributes::default();
1367        assert_eq!(attributes.as_path(), None);
1368        assert_eq!(attributes.as4_path(), None);
1369        assert_eq!(attributes.effective_as_path(), None);
1370
1371        attributes.add_attr(AttributeValue::AsPath(AsPath::from_sequence([23456, 64497])).into());
1372        assert_eq!(
1373            attributes
1374                .as_path()
1375                .map(|p| p.to_u32_vec_opt(false).unwrap()),
1376            Some(vec![23456, 64497])
1377        );
1378        assert_eq!(attributes.as4_path(), None);
1379        assert_eq!(
1380            attributes
1381                .effective_as_path()
1382                .unwrap()
1383                .to_u32_vec_opt(false)
1384                .unwrap(),
1385            vec![23456, 64497]
1386        );
1387
1388        attributes.add_attr(AttributeValue::As4Path(AsPath::from_sequence([65536, 64497])).into());
1389        assert_eq!(
1390            attributes
1391                .as4_path()
1392                .map(|p| p.to_u32_vec_opt(false).unwrap()),
1393            Some(vec![65536, 64497])
1394        );
1395        // RFC 6793 §4.2.3: leading ASes from AS_PATH, trailing ASes from AS4_PATH.
1396        assert_eq!(
1397            attributes
1398                .effective_as_path()
1399                .unwrap()
1400                .to_u32_vec_opt(false)
1401                .unwrap(),
1402            vec![65536, 64497]
1403        );
1404
1405        // An AS4_PATH without an AS_PATH is returned as-is.
1406        let mut attributes = Attributes::default();
1407        attributes.add_attr(AttributeValue::As4Path(AsPath::from_sequence([65536, 64497])).into());
1408        assert_eq!(attributes.as_path(), None);
1409        assert_eq!(
1410            attributes
1411                .effective_as_path()
1412                .unwrap()
1413                .to_u32_vec_opt(false)
1414                .unwrap(),
1415            vec![65536, 64497]
1416        );
1417    }
1418}