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