bgpkit_parser/models/bgp/
community.rs

1use crate::models::Asn;
2use num_enum::{FromPrimitive, IntoPrimitive};
3use std::fmt::{Display, Formatter};
4use std::net::{Ipv4Addr, Ipv6Addr};
5
6#[derive(Debug, PartialEq, Copy, Clone, Eq)]
7#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
8#[cfg_attr(feature = "serde", serde(untagged))]
9pub enum MetaCommunity {
10    Plain(Community),
11    Extended(ExtendedCommunity),
12    Ipv6Extended(Ipv6AddrExtCommunity),
13    Large(LargeCommunity),
14}
15
16#[derive(Debug, PartialEq, Copy, Clone, Eq)]
17#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
18pub enum Community {
19    NoExport,
20    NoAdvertise,
21    NoExportSubConfed,
22    Custom(Asn, u16),
23}
24
25/// Large community structure as defined in [RFC8092](https://datatracker.ietf.org/doc/html/rfc8092)
26///
27/// ## Display
28///
29/// Large community is displayed as `GLOBAL_ADMINISTRATOR:LOCAL_DATA_1:LOCAL_DATA_2`
30#[derive(Debug, PartialEq, Clone, Copy, Eq)]
31#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
32pub struct LargeCommunity {
33    pub global_admin: u32,
34    pub local_data: [u32; 2],
35}
36
37impl LargeCommunity {
38    pub fn new(global_admin: u32, local_data: [u32; 2]) -> LargeCommunity {
39        LargeCommunity {
40            global_admin,
41            local_data,
42        }
43    }
44}
45
46/// Type definitions of extended communities
47#[derive(Debug, FromPrimitive, IntoPrimitive, PartialEq, Eq, Hash, Copy, Clone)]
48#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
49#[repr(u8)]
50pub enum ExtendedCommunityType {
51    // transitive types
52    TransitiveTwoOctetAs = 0x00,
53    TransitiveIpv4Addr = 0x01,
54    TransitiveFourOctetAs = 0x02,
55    TransitiveOpaque = 0x03,
56
57    // non-transitive types
58    NonTransitiveTwoOctetAs = 0x40,
59    NonTransitiveIpv4Addr = 0x41,
60    NonTransitiveFourOctetAs = 0x42,
61    NonTransitiveOpaque = 0x43,
62    // the rest are either draft or experimental
63    #[num_enum(catch_all)]
64    Unknown(u8),
65}
66
67/// Extended Communities.
68///
69/// ## Overview  
70///
71/// It is a 8-octet data that has flexible definition based on the types:
72/// <https://datatracker.ietf.org/doc/html/rfc4360>
73///
74/// For more up-to-date definitions, see [IANA' website](https://www.iana.org/assignments/bgp-extended-communities/bgp-extended-communities.xhtml).
75///
76/// ```text
77///    Each Extended Community is encoded as an 8-octet quantity, as
78///    follows:
79///
80///       - Type Field  : 1 or 2 octets
81///       - Value Field : Remaining octets
82///
83///        0                   1                   2                   3
84///        0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
85///       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
86///       |  Type high    |  Type low(*)  |                               |
87///       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+          Value                |
88///       |                                                               |
89///       +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
90///
91///       (*) Present for Extended types only, used for the Value field
92///           otherwise.
93/// ```
94#[derive(Debug, PartialEq, Clone, Copy, Eq)]
95#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
96pub enum ExtendedCommunity {
97    TransitiveTwoOctetAs(TwoOctetAsExtCommunity),
98    TransitiveIpv4Addr(Ipv4AddrExtCommunity),
99    TransitiveFourOctetAs(FourOctetAsExtCommunity),
100    TransitiveOpaque(OpaqueExtCommunity),
101    NonTransitiveTwoOctetAs(TwoOctetAsExtCommunity),
102    NonTransitiveIpv4Addr(Ipv4AddrExtCommunity),
103    NonTransitiveFourOctetAs(FourOctetAsExtCommunity),
104    NonTransitiveOpaque(OpaqueExtCommunity),
105    /// Flow-Spec Traffic Rate - RFC 8955
106    FlowSpecTrafficRate(FlowSpecTrafficRate),
107    /// Flow-Spec Traffic Action - RFC 8955  
108    FlowSpecTrafficAction(FlowSpecTrafficAction),
109    /// Flow-Spec Redirect - RFC 8955
110    FlowSpecRedirect(TwoOctetAsExtCommunity),
111    /// Flow-Spec Traffic Marking - RFC 8955
112    FlowSpecTrafficMarking(FlowSpecTrafficMarking),
113    Raw([u8; 8]),
114}
115
116impl ExtendedCommunity {
117    pub const fn community_type(&self) -> ExtendedCommunityType {
118        use ExtendedCommunityType::*;
119        match self {
120            ExtendedCommunity::TransitiveTwoOctetAs(_) => TransitiveTwoOctetAs,
121            ExtendedCommunity::TransitiveIpv4Addr(_) => TransitiveIpv4Addr,
122            ExtendedCommunity::TransitiveFourOctetAs(_) => TransitiveFourOctetAs,
123            ExtendedCommunity::TransitiveOpaque(_) => TransitiveOpaque,
124            ExtendedCommunity::NonTransitiveTwoOctetAs(_) => NonTransitiveTwoOctetAs,
125            ExtendedCommunity::NonTransitiveIpv4Addr(_) => NonTransitiveIpv4Addr,
126            ExtendedCommunity::NonTransitiveFourOctetAs(_) => NonTransitiveFourOctetAs,
127            ExtendedCommunity::NonTransitiveOpaque(_) => NonTransitiveOpaque,
128            ExtendedCommunity::FlowSpecTrafficRate(_) => NonTransitiveTwoOctetAs,
129            ExtendedCommunity::FlowSpecTrafficAction(_) => NonTransitiveTwoOctetAs,
130            ExtendedCommunity::FlowSpecRedirect(_) => NonTransitiveTwoOctetAs,
131            ExtendedCommunity::FlowSpecTrafficMarking(_) => NonTransitiveTwoOctetAs,
132            ExtendedCommunity::Raw(buffer) => Unknown(buffer[0]),
133        }
134    }
135}
136
137#[derive(Debug, PartialEq, Clone, Copy, Eq)]
138#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
139pub struct Ipv6AddrExtCommunity {
140    pub community_type: ExtendedCommunityType,
141    pub subtype: u8,
142    // 16 octets
143    pub global_admin: Ipv6Addr,
144    // 2 octets
145    pub local_admin: [u8; 2],
146}
147
148/// Two-Octet AS Specific Extended Community
149///
150/// <https://datatracker.ietf.org/doc/html/rfc4360#section-3.1>
151#[derive(Debug, PartialEq, Clone, Copy, Eq)]
152#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
153pub struct TwoOctetAsExtCommunity {
154    pub subtype: u8,
155    // 2 octet
156    pub global_admin: Asn,
157    // 4 octet
158    pub local_admin: [u8; 4],
159}
160
161/// Four-Octet AS Specific Extended Community
162///
163/// <https://datatracker.ietf.org/doc/html/rfc5668#section-2>
164#[derive(Debug, PartialEq, Clone, Copy, Eq)]
165#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
166pub struct FourOctetAsExtCommunity {
167    pub subtype: u8,
168    // 4 octet
169    pub global_admin: Asn,
170    // 2 octet
171    pub local_admin: [u8; 2],
172}
173
174/// IPv4 Address Specific Extended Community
175///
176/// <https://datatracker.ietf.org/doc/html/rfc4360#section-3.2>
177#[derive(Debug, PartialEq, Clone, Copy, Eq)]
178#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
179pub struct Ipv4AddrExtCommunity {
180    pub subtype: u8,
181    // 4 octet
182    pub global_admin: Ipv4Addr,
183    // 2 octet
184    pub local_admin: [u8; 2],
185}
186
187/// Opaque Extended Community
188///
189/// <https://datatracker.ietf.org/doc/html/rfc4360#section-3.3>
190#[derive(Debug, PartialEq, Clone, Copy, Eq)]
191#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
192pub struct OpaqueExtCommunity {
193    pub subtype: u8,
194    // 6 octet
195    pub value: [u8; 6],
196}
197
198/// Flow-Spec Traffic Rate Extended Community
199///
200/// RFC 8955 - subtype 0x06
201#[derive(Debug, Clone, Copy)]
202#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
203pub struct FlowSpecTrafficRate {
204    /// AS Number (2 octets)
205    pub as_number: u16,
206    /// Rate in bytes per second (IEEE 754 single precision float)
207    pub rate_bytes_per_sec: f32,
208}
209
210impl PartialEq for FlowSpecTrafficRate {
211    fn eq(&self, other: &Self) -> bool {
212        self.as_number == other.as_number
213            && self.rate_bytes_per_sec.to_bits() == other.rate_bytes_per_sec.to_bits()
214    }
215}
216
217impl Eq for FlowSpecTrafficRate {}
218
219/// Flow-Spec Traffic Action Extended Community
220///
221/// RFC 8955 - subtype 0x07  
222#[derive(Debug, PartialEq, Clone, Copy, Eq)]
223#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
224pub struct FlowSpecTrafficAction {
225    /// AS Number (2 octets)
226    pub as_number: u16,
227    /// Terminal action - stop processing additional flow-specs
228    pub terminal: bool,
229    /// Sample action - enable traffic sampling
230    pub sample: bool,
231}
232
233/// Flow-Spec Traffic Marking Extended Community
234///
235/// RFC 8955 - subtype 0x09
236#[derive(Debug, PartialEq, Clone, Copy, Eq)]
237#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
238pub struct FlowSpecTrafficMarking {
239    /// AS Number (2 octets)
240    pub as_number: u16,
241    /// DSCP value (6 bits)
242    pub dscp: u8,
243}
244
245impl FlowSpecTrafficRate {
246    /// Create a new traffic rate community
247    pub fn new(as_number: u16, rate_bytes_per_sec: f32) -> Self {
248        Self {
249            as_number,
250            rate_bytes_per_sec,
251        }
252    }
253
254    /// Create a "discard all traffic" rate (rate = 0.0)
255    pub fn discard(as_number: u16) -> Self {
256        Self {
257            as_number,
258            rate_bytes_per_sec: 0.0,
259        }
260    }
261}
262
263impl FlowSpecTrafficAction {
264    /// Create a new traffic action community
265    pub fn new(as_number: u16, terminal: bool, sample: bool) -> Self {
266        Self {
267            as_number,
268            terminal,
269            sample,
270        }
271    }
272}
273
274impl FlowSpecTrafficMarking {
275    /// Create a new traffic marking community
276    pub fn new(as_number: u16, dscp: u8) -> Self {
277        Self {
278            as_number,
279            dscp: dscp & 0x3F,
280        } // Mask to 6 bits
281    }
282}
283
284/////////////
285// DISPLAY //
286/////////////
287
288struct ToHexString<'a>(&'a [u8]);
289
290impl Display for ToHexString<'_> {
291    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
292        for byte in self.0 {
293            write!(f, "{byte:02X}")?;
294        }
295        Ok(())
296    }
297}
298
299impl Display for Community {
300    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
301        match self {
302            Community::NoExport => write!(f, "no-export"),
303            Community::NoAdvertise => write!(f, "no-advertise"),
304            Community::NoExportSubConfed => write!(f, "no-export-sub-confed"),
305            Community::Custom(asn, value) => write!(f, "{asn}:{value}"),
306        }
307    }
308}
309
310impl Display for LargeCommunity {
311    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
312        write!(
313            f,
314            "{}:{}:{}",
315            self.global_admin, self.local_data[0], self.local_data[1]
316        )
317    }
318}
319
320impl Display for ExtendedCommunity {
321    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
322        let ec_type = u8::from(self.community_type());
323        match self {
324            ExtendedCommunity::TransitiveTwoOctetAs(ec)
325            | ExtendedCommunity::NonTransitiveTwoOctetAs(ec) => {
326                write!(
327                    f,
328                    "{}:{}:{}:{}",
329                    ec_type,
330                    ec.subtype,
331                    ec.global_admin,
332                    ToHexString(&ec.local_admin)
333                )
334            }
335            ExtendedCommunity::TransitiveIpv4Addr(ec)
336            | ExtendedCommunity::NonTransitiveIpv4Addr(ec) => {
337                write!(
338                    f,
339                    "{}:{}:{}:{}",
340                    ec_type,
341                    ec.subtype,
342                    ec.global_admin,
343                    ToHexString(&ec.local_admin)
344                )
345            }
346            ExtendedCommunity::TransitiveFourOctetAs(ec)
347            | ExtendedCommunity::NonTransitiveFourOctetAs(ec) => {
348                write!(
349                    f,
350                    "{}:{}:{}:{}",
351                    ec_type,
352                    ec.subtype,
353                    ec.global_admin,
354                    ToHexString(&ec.local_admin)
355                )
356            }
357            ExtendedCommunity::TransitiveOpaque(ec)
358            | ExtendedCommunity::NonTransitiveOpaque(ec) => {
359                write!(f, "{}:{}:{}", ec_type, ec.subtype, ToHexString(&ec.value))
360            }
361            ExtendedCommunity::FlowSpecTrafficRate(rate) => {
362                write!(
363                    f,
364                    "rate:{} bytes/sec (AS {})",
365                    rate.rate_bytes_per_sec, rate.as_number
366                )
367            }
368            ExtendedCommunity::FlowSpecTrafficAction(action) => {
369                let mut flags = Vec::new();
370                if action.terminal {
371                    flags.push("terminal");
372                }
373                if action.sample {
374                    flags.push("sample");
375                }
376                write!(f, "action:{} (AS {})", flags.join(","), action.as_number)
377            }
378            ExtendedCommunity::FlowSpecRedirect(redirect) => {
379                write!(
380                    f,
381                    "redirect:AS{}:{}",
382                    redirect.global_admin,
383                    ToHexString(&redirect.local_admin)
384                )
385            }
386            ExtendedCommunity::FlowSpecTrafficMarking(marking) => {
387                write!(f, "mark:DSCP{} (AS {})", marking.dscp, marking.as_number)
388            }
389            ExtendedCommunity::Raw(ec) => {
390                write!(f, "{}", ToHexString(ec))
391            }
392        }
393    }
394}
395
396impl Display for Ipv6AddrExtCommunity {
397    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
398        write!(
399            f,
400            "{}:{}:{}:{}",
401            u8::from(self.community_type),
402            self.subtype,
403            self.global_admin,
404            ToHexString(&self.local_admin)
405        )
406    }
407}
408
409impl Display for MetaCommunity {
410    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
411        match self {
412            MetaCommunity::Plain(c) => write!(f, "{c}"),
413            MetaCommunity::Extended(c) => write!(f, "{c}"),
414            MetaCommunity::Large(c) => write!(f, "{c}"),
415            MetaCommunity::Ipv6Extended(c) => write!(f, "{c}"),
416        }
417    }
418}
419
420#[cfg(test)]
421mod tests {
422    use super::*;
423
424    #[test]
425    fn test_large_community_new() {
426        let global_admin = 56;
427        let local_data = [3, 4];
428        let large_comm = LargeCommunity::new(global_admin, local_data);
429        assert_eq!(large_comm.global_admin, global_admin);
430        assert_eq!(large_comm.local_data, local_data);
431    }
432
433    #[test]
434    fn test_extended_community_community_type() {
435        let two_octet_as_ext_comm = TwoOctetAsExtCommunity {
436            subtype: 0,
437            global_admin: Asn::new_32bit(0),
438            local_admin: [0; 4],
439        };
440        let extended_community = ExtendedCommunity::TransitiveTwoOctetAs(two_octet_as_ext_comm);
441        assert_eq!(
442            extended_community.community_type(),
443            ExtendedCommunityType::TransitiveTwoOctetAs
444        );
445    }
446
447    #[test]
448    fn test_display_community() {
449        assert_eq!(format!("{}", Community::NoExport), "no-export");
450        assert_eq!(format!("{}", Community::NoAdvertise), "no-advertise");
451        assert_eq!(
452            format!("{}", Community::NoExportSubConfed),
453            "no-export-sub-confed"
454        );
455        assert_eq!(
456            format!("{}", Community::Custom(Asn::new_32bit(64512), 100)),
457            "64512:100"
458        );
459    }
460
461    #[test]
462    fn test_display_large_community() {
463        let large_community = LargeCommunity::new(1, [2, 3]);
464        assert_eq!(format!("{large_community}"), "1:2:3");
465    }
466
467    #[test]
468    fn test_display_extended_community() {
469        let two_octet_as_ext_comm = TwoOctetAsExtCommunity {
470            subtype: 0,
471            global_admin: Asn::new_32bit(0),
472            local_admin: [0; 4],
473        };
474        let extended_community = ExtendedCommunity::TransitiveTwoOctetAs(two_octet_as_ext_comm);
475        assert_eq!(format!("{extended_community}"), "0:0:0:00000000");
476
477        let two_octet_as_ext_comm = TwoOctetAsExtCommunity {
478            subtype: 0,
479            global_admin: Asn::new_32bit(0),
480            local_admin: [0; 4],
481        };
482        let extended_community = ExtendedCommunity::NonTransitiveTwoOctetAs(two_octet_as_ext_comm);
483        assert_eq!(format!("{extended_community}"), "64:0:0:00000000");
484
485        let ipv4_ext_comm = Ipv4AddrExtCommunity {
486            subtype: 1,
487            global_admin: "192.168.1.1".parse().unwrap(),
488            local_admin: [5, 6],
489        };
490        let extended_community = ExtendedCommunity::TransitiveIpv4Addr(ipv4_ext_comm);
491        assert_eq!(format!("{extended_community}"), "1:1:192.168.1.1:0506");
492
493        let ipv4_ext_comm = Ipv4AddrExtCommunity {
494            subtype: 1,
495            global_admin: "192.168.1.1".parse().unwrap(),
496            local_admin: [5, 6],
497        };
498        let extended_community = ExtendedCommunity::NonTransitiveIpv4Addr(ipv4_ext_comm);
499        assert_eq!(format!("{extended_community}"), "65:1:192.168.1.1:0506");
500
501        let four_octet_as_ext_comm = FourOctetAsExtCommunity {
502            subtype: 2,
503            global_admin: Asn::new_32bit(64512),
504            local_admin: [7, 8],
505        };
506        let extended_community = ExtendedCommunity::TransitiveFourOctetAs(four_octet_as_ext_comm);
507        assert_eq!(format!("{extended_community}"), "2:2:64512:0708");
508
509        let four_octet_as_ext_comm = FourOctetAsExtCommunity {
510            subtype: 2,
511            global_admin: Asn::new_32bit(64512),
512            local_admin: [7, 8],
513        };
514        let extended_community =
515            ExtendedCommunity::NonTransitiveFourOctetAs(four_octet_as_ext_comm);
516        assert_eq!(format!("{extended_community}"), "66:2:64512:0708");
517
518        let opaque_ext_comm = OpaqueExtCommunity {
519            subtype: 3,
520            value: [9, 10, 11, 12, 13, 14],
521        };
522        let extended_community = ExtendedCommunity::TransitiveOpaque(opaque_ext_comm);
523        assert_eq!(format!("{extended_community}"), "3:3:090A0B0C0D0E");
524
525        let opaque_ext_comm = OpaqueExtCommunity {
526            subtype: 3,
527            value: [9, 10, 11, 12, 13, 14],
528        };
529        let extended_community = ExtendedCommunity::NonTransitiveOpaque(opaque_ext_comm);
530        assert_eq!(format!("{extended_community}"), "67:3:090A0B0C0D0E");
531
532        let raw_ext_comm = [0, 1, 2, 3, 4, 5, 6, 7];
533        let extended_community = ExtendedCommunity::Raw(raw_ext_comm);
534        assert_eq!(format!("{extended_community}"), "0001020304050607");
535    }
536
537    #[test]
538    fn test_display_ipv6_addr_ext_community() {
539        let ipv6_addr_ext_comm = Ipv6AddrExtCommunity {
540            community_type: ExtendedCommunityType::TransitiveTwoOctetAs,
541            subtype: 0,
542            global_admin: "2001:db8::8a2e:370:7334".parse().unwrap(),
543            local_admin: [0, 1],
544        };
545        assert_eq!(
546            format!("{ipv6_addr_ext_comm}"),
547            "0:0:2001:db8::8a2e:370:7334:0001"
548        );
549    }
550
551    #[test]
552    fn test_display_meta_community() {
553        let large_community = LargeCommunity::new(1, [2, 3]);
554        let meta_community = MetaCommunity::Large(large_community);
555        assert_eq!(format!("{meta_community}"), "1:2:3");
556    }
557
558    #[test]
559    fn test_to_hex_string() {
560        // Test empty array
561        assert_eq!(format!("{}", ToHexString(&[])), "");
562
563        // Test single byte
564        assert_eq!(format!("{}", ToHexString(&[0x0A])), "0A");
565
566        // Test multiple bytes
567        assert_eq!(format!("{}", ToHexString(&[0x0A, 0x0B, 0x0C])), "0A0B0C");
568
569        // Test zero byte
570        assert_eq!(format!("{}", ToHexString(&[0x00])), "00");
571
572        // Test byte with value > 0x0F (needs two hex digits)
573        assert_eq!(format!("{}", ToHexString(&[0x10])), "10");
574
575        // Test mixed bytes
576        assert_eq!(
577            format!("{}", ToHexString(&[0x00, 0x0F, 0x10, 0xFF])),
578            "000F10FF"
579        );
580    }
581
582    #[test]
583    #[cfg(feature = "serde")]
584    fn test_serde() {
585        let meta_community = MetaCommunity::Large(LargeCommunity::new(1, [2, 3]));
586        let serialized = serde_json::to_string(&meta_community).unwrap();
587        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
588        assert_eq!(meta_community, deserialized);
589
590        let meta_community = MetaCommunity::Extended(ExtendedCommunity::TransitiveTwoOctetAs(
591            TwoOctetAsExtCommunity {
592                subtype: 0,
593                global_admin: Asn::new_32bit(0),
594                local_admin: [0; 4],
595            },
596        ));
597        let serialized = serde_json::to_string(&meta_community).unwrap();
598        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
599        assert_eq!(meta_community, deserialized);
600
601        let meta_community = MetaCommunity::Plain(Community::NoExport);
602        let serialized = serde_json::to_string(&meta_community).unwrap();
603        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
604        assert_eq!(meta_community, deserialized);
605
606        let meta_community = MetaCommunity::Ipv6Extended(Ipv6AddrExtCommunity {
607            community_type: ExtendedCommunityType::TransitiveTwoOctetAs,
608            subtype: 0,
609            global_admin: "2001:db8::8a2e:370:7334".parse().unwrap(),
610            local_admin: [0, 1],
611        });
612        let serialized = serde_json::to_string(&meta_community).unwrap();
613        let deserialized: MetaCommunity = serde_json::from_str(&serialized).unwrap();
614        assert_eq!(meta_community, deserialized);
615    }
616
617    #[test]
618    fn test_flowspec_traffic_rate() {
619        let rate = FlowSpecTrafficRate::new(64512, 1000.0);
620        assert_eq!(rate.as_number, 64512);
621        assert_eq!(rate.rate_bytes_per_sec, 1000.0);
622
623        let discard = FlowSpecTrafficRate::discard(64512);
624        assert_eq!(discard.rate_bytes_per_sec, 0.0);
625    }
626
627    #[test]
628    fn test_flowspec_traffic_action() {
629        let action = FlowSpecTrafficAction::new(64512, true, false);
630        assert_eq!(action.as_number, 64512);
631        assert!(action.terminal);
632        assert!(!action.sample);
633    }
634
635    #[test]
636    fn test_flowspec_traffic_marking() {
637        let marking = FlowSpecTrafficMarking::new(64512, 46); // EF DSCP
638        assert_eq!(marking.as_number, 64512);
639        assert_eq!(marking.dscp, 46);
640
641        // Test DSCP masking
642        let masked = FlowSpecTrafficMarking::new(64512, 255);
643        assert_eq!(masked.dscp, 63); // Should be masked to 6 bits
644    }
645
646    #[test]
647    fn test_flowspec_community_display() {
648        let rate = ExtendedCommunity::FlowSpecTrafficRate(FlowSpecTrafficRate::new(64512, 1000.0));
649        assert_eq!(format!("{}", rate), "rate:1000 bytes/sec (AS 64512)");
650
651        let action =
652            ExtendedCommunity::FlowSpecTrafficAction(FlowSpecTrafficAction::new(64512, true, true));
653        assert_eq!(format!("{}", action), "action:terminal,sample (AS 64512)");
654
655        let marking =
656            ExtendedCommunity::FlowSpecTrafficMarking(FlowSpecTrafficMarking::new(64512, 46));
657        assert_eq!(format!("{}", marking), "mark:DSCP46 (AS 64512)");
658    }
659}