pcapsql_core/protocol/
bgp.rs

1//! BGP (Border Gateway Protocol) parser.
2//!
3//! BGP is the protocol backing the core routing decisions on the Internet.
4//! It maintains a table of IP networks or 'prefixes' which designate
5//! network reachability among autonomous systems (AS).
6//!
7//! RFC 4271: A Border Gateway Protocol 4 (BGP-4)
8//! RFC 6793: BGP Support for Four-Octet Autonomous System (AS) Number Space
9//! RFC 2918: Route Refresh Capability for BGP-4
10
11use compact_str::CompactString;
12use smallvec::SmallVec;
13
14use super::{FieldValue, ParseContext, ParseResult, Protocol};
15use crate::schema::{DataKind, FieldDescriptor};
16
17/// BGP TCP port.
18pub const BGP_PORT: u16 = 179;
19
20/// BGP message types.
21pub mod message_type {
22    pub const OPEN: u8 = 1;
23    pub const UPDATE: u8 = 2;
24    pub const NOTIFICATION: u8 = 3;
25    pub const KEEPALIVE: u8 = 4;
26    pub const ROUTE_REFRESH: u8 = 5;
27}
28
29/// BGP path attribute type codes.
30pub mod path_attr_type {
31    pub const ORIGIN: u8 = 1;
32    pub const AS_PATH: u8 = 2;
33    pub const NEXT_HOP: u8 = 3;
34    pub const MULTI_EXIT_DISC: u8 = 4;
35    pub const LOCAL_PREF: u8 = 5;
36    pub const ATOMIC_AGGREGATE: u8 = 6;
37    pub const AGGREGATOR: u8 = 7;
38    pub const COMMUNITIES: u8 = 8;
39    pub const MP_REACH_NLRI: u8 = 14;
40    pub const MP_UNREACH_NLRI: u8 = 15;
41}
42
43/// BGP ORIGIN attribute values.
44pub mod origin_type {
45    pub const IGP: u8 = 0;
46    pub const EGP: u8 = 1;
47    pub const INCOMPLETE: u8 = 2;
48}
49
50/// BGP AS_PATH segment types.
51pub mod as_path_segment_type {
52    pub const AS_SET: u8 = 1;
53    pub const AS_SEQUENCE: u8 = 2;
54}
55
56/// BGP error codes for NOTIFICATION messages.
57pub mod error_code {
58    pub const MESSAGE_HEADER_ERROR: u8 = 1;
59    pub const OPEN_MESSAGE_ERROR: u8 = 2;
60    pub const UPDATE_MESSAGE_ERROR: u8 = 3;
61    pub const HOLD_TIMER_EXPIRED: u8 = 4;
62    pub const FSM_ERROR: u8 = 5;
63    pub const CEASE: u8 = 6;
64    pub const ROUTE_REFRESH_ERROR: u8 = 7;
65}
66
67/// BGP capability codes in OPEN optional parameters.
68pub mod capability_code {
69    pub const MULTIPROTOCOL: u8 = 1;
70    pub const ROUTE_REFRESH: u8 = 2;
71    pub const FOUR_OCTET_AS: u8 = 65;
72    pub const ADD_PATH: u8 = 69;
73    pub const ENHANCED_ROUTE_REFRESH: u8 = 70;
74}
75
76/// AS_TRANS value used when 4-byte ASN is negotiated (RFC 6793).
77pub const AS_TRANS: u16 = 23456;
78
79/// BGP Marker: 16 bytes of 0xFF.
80const BGP_MARKER: [u8; 16] = [0xFF; 16];
81
82/// Get the name of a BGP message type.
83fn message_type_name(msg_type: u8) -> &'static str {
84    match msg_type {
85        message_type::OPEN => "OPEN",
86        message_type::UPDATE => "UPDATE",
87        message_type::NOTIFICATION => "NOTIFICATION",
88        message_type::KEEPALIVE => "KEEPALIVE",
89        message_type::ROUTE_REFRESH => "ROUTE-REFRESH",
90        _ => "UNKNOWN",
91    }
92}
93
94/// Get the name of a BGP ORIGIN value.
95fn origin_name(origin: u8) -> &'static str {
96    match origin {
97        origin_type::IGP => "IGP",
98        origin_type::EGP => "EGP",
99        origin_type::INCOMPLETE => "INCOMPLETE",
100        _ => "UNKNOWN",
101    }
102}
103
104/// Get the name of a BGP error code.
105fn error_code_name(code: u8) -> &'static str {
106    match code {
107        error_code::MESSAGE_HEADER_ERROR => "Message Header Error",
108        error_code::OPEN_MESSAGE_ERROR => "OPEN Message Error",
109        error_code::UPDATE_MESSAGE_ERROR => "UPDATE Message Error",
110        error_code::HOLD_TIMER_EXPIRED => "Hold Timer Expired",
111        error_code::FSM_ERROR => "Finite State Machine Error",
112        error_code::CEASE => "Cease",
113        error_code::ROUTE_REFRESH_ERROR => "ROUTE-REFRESH Message Error",
114        _ => "Unknown",
115    }
116}
117
118/// Get the name of a path attribute type.
119#[allow(dead_code)]
120fn path_attr_type_name(type_code: u8) -> &'static str {
121    match type_code {
122        path_attr_type::ORIGIN => "ORIGIN",
123        path_attr_type::AS_PATH => "AS_PATH",
124        path_attr_type::NEXT_HOP => "NEXT_HOP",
125        path_attr_type::MULTI_EXIT_DISC => "MULTI_EXIT_DISC",
126        path_attr_type::LOCAL_PREF => "LOCAL_PREF",
127        path_attr_type::ATOMIC_AGGREGATE => "ATOMIC_AGGREGATE",
128        path_attr_type::AGGREGATOR => "AGGREGATOR",
129        path_attr_type::COMMUNITIES => "COMMUNITIES",
130        path_attr_type::MP_REACH_NLRI => "MP_REACH_NLRI",
131        path_attr_type::MP_UNREACH_NLRI => "MP_UNREACH_NLRI",
132        _ => "UNKNOWN",
133    }
134}
135
136/// BGP protocol parser.
137#[derive(Debug, Clone, Copy)]
138pub struct BgpProtocol;
139
140impl Protocol for BgpProtocol {
141    fn name(&self) -> &'static str {
142        "bgp"
143    }
144
145    fn display_name(&self) -> &'static str {
146        "BGP"
147    }
148
149    fn can_parse(&self, context: &ParseContext) -> Option<u32> {
150        // Match when TCP dst_port or src_port hint equals 179
151        if let Some(dst_port) = context.hint("dst_port") {
152            if dst_port == BGP_PORT as u64 {
153                return Some(100);
154            }
155        }
156        if let Some(src_port) = context.hint("src_port") {
157            if src_port == BGP_PORT as u64 {
158                return Some(100);
159            }
160        }
161        None
162    }
163
164    fn parse<'a>(&self, data: &'a [u8], _context: &ParseContext) -> ParseResult<'a> {
165        // BGP header minimum is 19 bytes (16 marker + 2 length + 1 type)
166        if data.len() < 19 {
167            return ParseResult::error("BGP header too short".to_string(), data);
168        }
169
170        let mut fields = SmallVec::new();
171
172        // Bytes 0-15: Marker (should be all 0xFF)
173        if data[0..16] != BGP_MARKER {
174            return ParseResult::error("BGP: invalid marker".to_string(), data);
175        }
176
177        // Bytes 16-17: Length (total message length including header)
178        let length = u16::from_be_bytes([data[16], data[17]]);
179        fields.push(("length", FieldValue::UInt16(length)));
180
181        if !(19..=4096).contains(&length) {
182            return ParseResult::error(format!("BGP: invalid length {length}"), data);
183        }
184
185        // Byte 18: Type
186        let msg_type = data[18];
187        fields.push(("message_type", FieldValue::UInt8(msg_type)));
188        fields.push((
189            "message_type_name",
190            FieldValue::Str(message_type_name(msg_type)),
191        ));
192
193        // Parse message-specific fields
194        let message_data = if data.len() >= length as usize {
195            &data[19..length as usize]
196        } else {
197            &data[19..]
198        };
199
200        match msg_type {
201            message_type::OPEN => {
202                self.parse_open_message(message_data, &mut fields);
203            }
204            message_type::UPDATE => {
205                self.parse_update_message(message_data, &mut fields);
206            }
207            message_type::NOTIFICATION => {
208                self.parse_notification_message(message_data, &mut fields);
209            }
210            message_type::KEEPALIVE => {
211                // KEEPALIVE has no additional data (header only)
212            }
213            message_type::ROUTE_REFRESH => {
214                self.parse_route_refresh_message(message_data, &mut fields);
215            }
216            _ => {
217                // Unknown message type
218            }
219        }
220
221        // Calculate remaining data
222        let consumed = std::cmp::min(length as usize, data.len());
223        ParseResult::success(fields, &data[consumed..], SmallVec::new())
224    }
225
226    fn schema_fields(&self) -> Vec<FieldDescriptor> {
227        vec![
228            // Common header fields
229            FieldDescriptor::new("bgp.message_type", DataKind::UInt8).set_nullable(true),
230            FieldDescriptor::new("bgp.message_type_name", DataKind::String).set_nullable(true),
231            FieldDescriptor::new("bgp.length", DataKind::UInt16).set_nullable(true),
232            // OPEN message fields
233            FieldDescriptor::new("bgp.version", DataKind::UInt8).set_nullable(true),
234            FieldDescriptor::new("bgp.my_as", DataKind::UInt16).set_nullable(true),
235            FieldDescriptor::new("bgp.my_as_4byte", DataKind::UInt32).set_nullable(true),
236            FieldDescriptor::new("bgp.hold_time", DataKind::UInt16).set_nullable(true),
237            FieldDescriptor::new("bgp.bgp_id", DataKind::String).set_nullable(true),
238            FieldDescriptor::new("bgp.capabilities", DataKind::String).set_nullable(true),
239            // UPDATE message fields
240            FieldDescriptor::new("bgp.withdrawn_routes_len", DataKind::UInt16).set_nullable(true),
241            FieldDescriptor::new("bgp.withdrawn_routes", DataKind::String).set_nullable(true),
242            FieldDescriptor::new("bgp.withdrawn_count", DataKind::UInt16).set_nullable(true),
243            FieldDescriptor::new("bgp.path_attr_len", DataKind::UInt16).set_nullable(true),
244            FieldDescriptor::new("bgp.origin", DataKind::UInt8).set_nullable(true),
245            FieldDescriptor::new("bgp.origin_name", DataKind::String).set_nullable(true),
246            FieldDescriptor::new("bgp.as_path", DataKind::String).set_nullable(true),
247            FieldDescriptor::new("bgp.as_path_length", DataKind::UInt16).set_nullable(true),
248            FieldDescriptor::new("bgp.next_hop", DataKind::String).set_nullable(true),
249            FieldDescriptor::new("bgp.med", DataKind::UInt32).set_nullable(true),
250            FieldDescriptor::new("bgp.local_pref", DataKind::UInt32).set_nullable(true),
251            FieldDescriptor::new("bgp.atomic_aggregate", DataKind::Bool).set_nullable(true),
252            FieldDescriptor::new("bgp.aggregator_as", DataKind::UInt32).set_nullable(true),
253            FieldDescriptor::new("bgp.aggregator_ip", DataKind::String).set_nullable(true),
254            FieldDescriptor::new("bgp.nlri", DataKind::String).set_nullable(true),
255            FieldDescriptor::new("bgp.nlri_count", DataKind::UInt16).set_nullable(true),
256            // NOTIFICATION message fields
257            FieldDescriptor::new("bgp.error_code", DataKind::UInt8).set_nullable(true),
258            FieldDescriptor::new("bgp.error_code_name", DataKind::String).set_nullable(true),
259            FieldDescriptor::new("bgp.error_subcode", DataKind::UInt8).set_nullable(true),
260            // ROUTE-REFRESH message fields
261            FieldDescriptor::new("bgp.afi", DataKind::UInt16).set_nullable(true),
262            FieldDescriptor::new("bgp.safi", DataKind::UInt8).set_nullable(true),
263        ]
264    }
265
266    fn child_protocols(&self) -> &[&'static str] {
267        &[]
268    }
269
270    fn dependencies(&self) -> &'static [&'static str] {
271        &["tcp"]
272    }
273}
274
275impl BgpProtocol {
276    /// Parse BGP OPEN message.
277    ///
278    /// OPEN Message Format:
279    /// ```text
280    ///  0                   1                   2                   3
281    ///  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
282    /// +-+-+-+-+-+-+-+-+
283    /// |    Version    |
284    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
285    /// |     My Autonomous System      |
286    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
287    /// |           Hold Time           |
288    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
289    /// |                         BGP Identifier                        |
290    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
291    /// | Opt Parm Len  |
292    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
293    /// |             Optional Parameters (variable)                    |
294    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
295    /// ```
296    fn parse_open_message(
297        &self,
298        data: &[u8],
299        fields: &mut SmallVec<[(&'static str, FieldValue); 16]>,
300    ) {
301        if data.len() < 10 {
302            return;
303        }
304
305        // Byte 0: Version
306        let version = data[0];
307        fields.push(("version", FieldValue::UInt8(version)));
308
309        // Bytes 1-2: My AS (may be AS_TRANS=23456 if 4-byte ASN is used)
310        let my_as = u16::from_be_bytes([data[1], data[2]]);
311        fields.push(("my_as", FieldValue::UInt16(my_as)));
312
313        // Bytes 3-4: Hold Time
314        let hold_time = u16::from_be_bytes([data[3], data[4]]);
315        fields.push(("hold_time", FieldValue::UInt16(hold_time)));
316
317        // Bytes 5-8: BGP Identifier (IPv4 address)
318        let bgp_id = format!("{}.{}.{}.{}", data[5], data[6], data[7], data[8]);
319        fields.push((
320            "bgp_id",
321            FieldValue::OwnedString(CompactString::new(bgp_id)),
322        ));
323
324        // Byte 9: Optional Parameters Length
325        let opt_params_len = data[9] as usize;
326        if data.len() < 10 + opt_params_len {
327            return;
328        }
329
330        // Parse optional parameters to extract capabilities
331        let opt_params = &data[10..10 + opt_params_len];
332        self.parse_open_optional_params(opt_params, fields, my_as);
333    }
334
335    /// Parse OPEN message optional parameters to extract capabilities.
336    fn parse_open_optional_params(
337        &self,
338        data: &[u8],
339        fields: &mut SmallVec<[(&'static str, FieldValue); 16]>,
340        my_as_2byte: u16,
341    ) {
342        let mut offset = 0;
343        // Typical OPEN has 2-5 capabilities
344        let mut capabilities = Vec::with_capacity(4);
345        let mut four_byte_asn: Option<u32> = None;
346
347        while offset + 2 <= data.len() {
348            let param_type = data[offset];
349            let param_len = data[offset + 1] as usize;
350            offset += 2;
351
352            if offset + param_len > data.len() {
353                break;
354            }
355
356            // Parameter type 2 = Capabilities
357            if param_type == 2 {
358                // Parse capabilities within this parameter
359                let cap_data = &data[offset..offset + param_len];
360                let mut cap_offset = 0;
361
362                while cap_offset + 2 <= cap_data.len() {
363                    let cap_code = cap_data[cap_offset];
364                    let cap_len = cap_data[cap_offset + 1] as usize;
365                    cap_offset += 2;
366
367                    if cap_offset + cap_len > cap_data.len() {
368                        break;
369                    }
370
371                    // Extract capability name
372                    let cap_name = match cap_code {
373                        capability_code::MULTIPROTOCOL => "MULTIPROTOCOL",
374                        capability_code::ROUTE_REFRESH => "ROUTE_REFRESH",
375                        capability_code::FOUR_OCTET_AS => "4-BYTE-AS",
376                        capability_code::ADD_PATH => "ADD_PATH",
377                        capability_code::ENHANCED_ROUTE_REFRESH => "ENHANCED_ROUTE_REFRESH",
378                        _ => "UNKNOWN",
379                    };
380                    capabilities.push(cap_name);
381
382                    // Extract 4-byte ASN if present (capability 65)
383                    if cap_code == capability_code::FOUR_OCTET_AS && cap_len == 4 {
384                        let asn = u32::from_be_bytes([
385                            cap_data[cap_offset],
386                            cap_data[cap_offset + 1],
387                            cap_data[cap_offset + 2],
388                            cap_data[cap_offset + 3],
389                        ]);
390                        four_byte_asn = Some(asn);
391                    }
392
393                    cap_offset += cap_len;
394                }
395            }
396
397            offset += param_len;
398        }
399
400        if !capabilities.is_empty() {
401            fields.push((
402                "capabilities",
403                FieldValue::OwnedString(CompactString::new(capabilities.join(","))),
404            ));
405        }
406
407        // If 4-byte ASN capability was found, store it
408        // Also check if my_as is AS_TRANS (23456), indicating 4-byte ASN is in use
409        if let Some(asn) = four_byte_asn {
410            fields.push(("my_as_4byte", FieldValue::UInt32(asn)));
411        } else if my_as_2byte != AS_TRANS {
412            // No 4-byte capability, so the 2-byte AS is the real AS
413            fields.push(("my_as_4byte", FieldValue::UInt32(my_as_2byte as u32)));
414        }
415    }
416
417    /// Parse BGP UPDATE message with full path attribute decoding.
418    ///
419    /// UPDATE Message Format:
420    /// ```text
421    ///  0                   1                   2                   3
422    ///  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
423    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
424    /// |   Withdrawn Routes Length     |
425    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
426    /// |       Withdrawn Routes (variable)                            |
427    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
428    /// |      Total Path Attribute Length                              |
429    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
430    /// |       Path Attributes (variable)                             |
431    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
432    /// |       Network Layer Reachability Information (variable)      |
433    /// +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
434    /// ```
435    fn parse_update_message(
436        &self,
437        data: &[u8],
438        fields: &mut SmallVec<[(&'static str, FieldValue); 16]>,
439    ) {
440        if data.len() < 4 {
441            return;
442        }
443
444        let mut offset = 0;
445
446        // Bytes 0-1: Withdrawn Routes Length
447        let withdrawn_routes_len = u16::from_be_bytes([data[0], data[1]]) as usize;
448        fields.push((
449            "withdrawn_routes_len",
450            FieldValue::UInt16(withdrawn_routes_len as u16),
451        ));
452        offset += 2;
453
454        // Parse withdrawn routes (NLRI prefixes)
455        if data.len() < offset + withdrawn_routes_len {
456            return;
457        }
458        let withdrawn_data = &data[offset..offset + withdrawn_routes_len];
459        let withdrawn_prefixes = self.parse_nlri_prefixes(withdrawn_data);
460        if !withdrawn_prefixes.is_empty() {
461            fields.push((
462                "withdrawn_routes",
463                FieldValue::OwnedString(CompactString::new(withdrawn_prefixes.join(","))),
464            ));
465            fields.push((
466                "withdrawn_count",
467                FieldValue::UInt16(withdrawn_prefixes.len() as u16),
468            ));
469        }
470        offset += withdrawn_routes_len;
471
472        // Check for path attributes length field
473        if data.len() < offset + 2 {
474            return;
475        }
476
477        // Total Path Attribute Length
478        let path_attr_len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
479        fields.push(("path_attr_len", FieldValue::UInt16(path_attr_len as u16)));
480        offset += 2;
481
482        // Parse path attributes
483        if data.len() < offset + path_attr_len {
484            return;
485        }
486        let path_attr_data = &data[offset..offset + path_attr_len];
487        self.parse_path_attributes(path_attr_data, fields);
488        offset += path_attr_len;
489
490        // Parse NLRI (remaining bytes are advertised prefixes)
491        if offset < data.len() {
492            let nlri_data = &data[offset..];
493            let nlri_prefixes = self.parse_nlri_prefixes(nlri_data);
494            if !nlri_prefixes.is_empty() {
495                fields.push((
496                    "nlri",
497                    FieldValue::OwnedString(CompactString::new(nlri_prefixes.join(","))),
498                ));
499                fields.push(("nlri_count", FieldValue::UInt16(nlri_prefixes.len() as u16)));
500            }
501        }
502    }
503
504    /// Parse NLRI prefixes (used for both withdrawn routes and announced routes).
505    /// Each prefix is: length (1 byte) + prefix bytes (ceil(length/8) bytes)
506    fn parse_nlri_prefixes(&self, data: &[u8]) -> Vec<String> {
507        // UPDATE messages typically have 1-8 prefixes
508        let mut prefixes = Vec::with_capacity(8);
509        let mut offset = 0;
510
511        while offset < data.len() {
512            let prefix_len_bits = data[offset] as usize;
513            offset += 1;
514
515            // Calculate how many bytes the prefix occupies
516            let prefix_bytes = prefix_len_bits.div_ceil(8);
517            if offset + prefix_bytes > data.len() {
518                break;
519            }
520
521            // Build the prefix (pad with zeros to make 4 bytes for IPv4)
522            let mut prefix = [0u8; 4];
523            let copy_len = prefix_bytes.min(4);
524            prefix[..copy_len].copy_from_slice(&data[offset..offset + copy_len]);
525
526            let prefix_str = format!(
527                "{}.{}.{}.{}/{}",
528                prefix[0], prefix[1], prefix[2], prefix[3], prefix_len_bits
529            );
530            prefixes.push(prefix_str);
531
532            offset += prefix_bytes;
533        }
534
535        prefixes
536    }
537
538    /// Parse path attributes from UPDATE message.
539    fn parse_path_attributes(
540        &self,
541        data: &[u8],
542        fields: &mut SmallVec<[(&'static str, FieldValue); 16]>,
543    ) {
544        let mut offset = 0;
545
546        while offset + 3 <= data.len() {
547            // Attribute flags (1 byte)
548            let flags = data[offset];
549            let optional = (flags & 0x80) != 0;
550            let transitive = (flags & 0x40) != 0;
551            let partial = (flags & 0x20) != 0;
552            let extended_length = (flags & 0x10) != 0;
553            let _ = (optional, transitive, partial); // Silence unused warnings
554
555            // Attribute type code (1 byte)
556            let type_code = data[offset + 1];
557            offset += 2;
558
559            // Attribute length (1 or 2 bytes depending on extended_length flag)
560            let attr_len = if extended_length {
561                if offset + 2 > data.len() {
562                    break;
563                }
564                let len = u16::from_be_bytes([data[offset], data[offset + 1]]) as usize;
565                offset += 2;
566                len
567            } else {
568                if offset >= data.len() {
569                    break;
570                }
571                let len = data[offset] as usize;
572                offset += 1;
573                len
574            };
575
576            if offset + attr_len > data.len() {
577                break;
578            }
579
580            let attr_data = &data[offset..offset + attr_len];
581
582            // Parse specific attributes
583            match type_code {
584                path_attr_type::ORIGIN => {
585                    if !attr_data.is_empty() {
586                        let origin = attr_data[0];
587                        fields.push(("origin", FieldValue::UInt8(origin)));
588                        fields.push(("origin_name", FieldValue::Str(origin_name(origin))));
589                    }
590                }
591                path_attr_type::AS_PATH => {
592                    let (as_path_str, path_length) = self.parse_as_path(attr_data);
593                    fields.push((
594                        "as_path",
595                        FieldValue::OwnedString(CompactString::new(as_path_str)),
596                    ));
597                    fields.push(("as_path_length", FieldValue::UInt16(path_length)));
598                }
599                path_attr_type::NEXT_HOP => {
600                    if attr_data.len() >= 4 {
601                        let next_hop = format!(
602                            "{}.{}.{}.{}",
603                            attr_data[0], attr_data[1], attr_data[2], attr_data[3]
604                        );
605                        fields.push((
606                            "next_hop",
607                            FieldValue::OwnedString(CompactString::new(next_hop)),
608                        ));
609                    }
610                }
611                path_attr_type::MULTI_EXIT_DISC => {
612                    if attr_data.len() >= 4 {
613                        let med = u32::from_be_bytes([
614                            attr_data[0],
615                            attr_data[1],
616                            attr_data[2],
617                            attr_data[3],
618                        ]);
619                        fields.push(("med", FieldValue::UInt32(med)));
620                    }
621                }
622                path_attr_type::LOCAL_PREF => {
623                    if attr_data.len() >= 4 {
624                        let local_pref = u32::from_be_bytes([
625                            attr_data[0],
626                            attr_data[1],
627                            attr_data[2],
628                            attr_data[3],
629                        ]);
630                        fields.push(("local_pref", FieldValue::UInt32(local_pref)));
631                    }
632                }
633                path_attr_type::ATOMIC_AGGREGATE => {
634                    // Presence indicates atomic aggregate (no value)
635                    fields.push(("atomic_aggregate", FieldValue::Bool(true)));
636                }
637                path_attr_type::AGGREGATOR => {
638                    // Can be 6 bytes (2-byte AS + 4-byte IP) or 8 bytes (4-byte AS + 4-byte IP)
639                    if attr_data.len() >= 6 {
640                        let (asn, ip_offset) = if attr_data.len() >= 8 {
641                            // 4-byte AS
642                            (
643                                u32::from_be_bytes([
644                                    attr_data[0],
645                                    attr_data[1],
646                                    attr_data[2],
647                                    attr_data[3],
648                                ]),
649                                4,
650                            )
651                        } else {
652                            // 2-byte AS
653                            (u16::from_be_bytes([attr_data[0], attr_data[1]]) as u32, 2)
654                        };
655                        fields.push(("aggregator_as", FieldValue::UInt32(asn)));
656
657                        if attr_data.len() >= ip_offset + 4 {
658                            let ip = format!(
659                                "{}.{}.{}.{}",
660                                attr_data[ip_offset],
661                                attr_data[ip_offset + 1],
662                                attr_data[ip_offset + 2],
663                                attr_data[ip_offset + 3]
664                            );
665                            fields.push((
666                                "aggregator_ip",
667                                FieldValue::OwnedString(CompactString::new(ip)),
668                            ));
669                        }
670                    }
671                }
672                _ => {
673                    // Unknown or unhandled attribute type
674                }
675            }
676
677            offset += attr_len;
678        }
679    }
680
681    /// Parse AS_PATH attribute and return (path_string, path_length).
682    /// Supports both 2-byte and 4-byte AS numbers (auto-detected based on segment length).
683    fn parse_as_path(&self, data: &[u8]) -> (String, u16) {
684        // Typical AS path has 2-6 segments
685        let mut segments = Vec::with_capacity(4);
686        let mut total_length = 0u16;
687        let mut offset = 0;
688
689        while offset + 2 <= data.len() {
690            let segment_type = data[offset];
691            let segment_len = data[offset + 1] as usize;
692            offset += 2;
693
694            // Determine if this is 2-byte or 4-byte AS based on remaining data
695            // Heuristic: if remaining bytes / segment_len == 4, use 4-byte ASNs
696            let remaining = data.len() - offset;
697            let as_size = if segment_len > 0 && remaining >= segment_len * 4 {
698                4 // 4-byte ASNs
699            } else {
700                2 // 2-byte ASNs
701            };
702
703            let needed_bytes = segment_len * as_size;
704            if offset + needed_bytes > data.len() {
705                break;
706            }
707
708            // Pre-allocate based on segment_len
709            let mut asns = Vec::with_capacity(segment_len);
710            for i in 0..segment_len {
711                let asn = if as_size == 4 {
712                    u32::from_be_bytes([
713                        data[offset + i * 4],
714                        data[offset + i * 4 + 1],
715                        data[offset + i * 4 + 2],
716                        data[offset + i * 4 + 3],
717                    ])
718                } else {
719                    u16::from_be_bytes([data[offset + i * 2], data[offset + i * 2 + 1]]) as u32
720                };
721                asns.push(asn.to_string());
722            }
723
724            let segment_str = match segment_type {
725                as_path_segment_type::AS_SET => format!("{{{}}}", asns.join(",")),
726                as_path_segment_type::AS_SEQUENCE => asns.join(" "),
727                _ => asns.join(" "),
728            };
729            segments.push(segment_str);
730
731            // AS_SEQUENCE contributes to path length, AS_SET counts as 1
732            if segment_type == as_path_segment_type::AS_SEQUENCE {
733                total_length += segment_len as u16;
734            } else if segment_type == as_path_segment_type::AS_SET {
735                total_length += 1;
736            }
737
738            offset += needed_bytes;
739        }
740
741        (segments.join(" "), total_length)
742    }
743
744    /// Parse NOTIFICATION message.
745    fn parse_notification_message(
746        &self,
747        data: &[u8],
748        fields: &mut SmallVec<[(&'static str, FieldValue); 16]>,
749    ) {
750        if data.len() < 2 {
751            return;
752        }
753
754        // Byte 0: Error Code
755        let error_code = data[0];
756        fields.push(("error_code", FieldValue::UInt8(error_code)));
757        fields.push((
758            "error_code_name",
759            FieldValue::Str(error_code_name(error_code)),
760        ));
761
762        // Byte 1: Error Subcode
763        let error_subcode = data[1];
764        fields.push(("error_subcode", FieldValue::UInt8(error_subcode)));
765
766        // Remaining bytes are error-specific data (not parsed)
767    }
768
769    /// Parse ROUTE-REFRESH message (RFC 2918).
770    fn parse_route_refresh_message(
771        &self,
772        data: &[u8],
773        fields: &mut SmallVec<[(&'static str, FieldValue); 16]>,
774    ) {
775        if data.len() < 4 {
776            return;
777        }
778
779        // Bytes 0-1: AFI (Address Family Identifier)
780        let afi = u16::from_be_bytes([data[0], data[1]]);
781        fields.push(("afi", FieldValue::UInt16(afi)));
782
783        // Byte 2: Reserved (must be 0)
784        // Byte 3: SAFI (Subsequent Address Family Identifier)
785        let safi = data[3];
786        fields.push(("safi", FieldValue::UInt8(safi)));
787    }
788}
789
790#[cfg(test)]
791mod tests {
792    use super::*;
793
794    /// Create a BGP message header.
795    fn create_bgp_header(msg_type: u8, length: u16) -> Vec<u8> {
796        let mut header = Vec::new();
797
798        // Marker (16 bytes of 0xFF)
799        header.extend_from_slice(&BGP_MARKER);
800
801        // Length
802        header.extend_from_slice(&length.to_be_bytes());
803
804        // Type
805        header.push(msg_type);
806
807        header
808    }
809
810    /// Create a BGP OPEN message.
811    fn create_bgp_open(version: u8, my_as: u16, hold_time: u16, bgp_id: [u8; 4]) -> Vec<u8> {
812        let mut msg = create_bgp_header(message_type::OPEN, 29); // 19 + 10
813
814        // Version
815        msg.push(version);
816
817        // My AS
818        msg.extend_from_slice(&my_as.to_be_bytes());
819
820        // Hold Time
821        msg.extend_from_slice(&hold_time.to_be_bytes());
822
823        // BGP Identifier
824        msg.extend_from_slice(&bgp_id);
825
826        // Optional Parameters Length
827        msg.push(0);
828
829        msg
830    }
831
832    /// Create a BGP UPDATE message (minimal).
833    fn create_bgp_update(withdrawn_len: u16, path_attr_len: u16) -> Vec<u8> {
834        let total_len = 19 + 2 + withdrawn_len + 2 + path_attr_len;
835        let mut msg = create_bgp_header(message_type::UPDATE, total_len);
836
837        // Withdrawn Routes Length
838        msg.extend_from_slice(&withdrawn_len.to_be_bytes());
839
840        // Withdrawn Routes (empty)
841        msg.extend(vec![0u8; withdrawn_len as usize]);
842
843        // Path Attribute Length
844        msg.extend_from_slice(&path_attr_len.to_be_bytes());
845
846        // Path Attributes (empty)
847        msg.extend(vec![0u8; path_attr_len as usize]);
848
849        msg
850    }
851
852    // Test 1: can_parse with TCP port 179
853    #[test]
854    fn test_can_parse_with_tcp_port_179() {
855        let parser = BgpProtocol;
856
857        // Without hint
858        let ctx1 = ParseContext::new(1);
859        assert!(parser.can_parse(&ctx1).is_none());
860
861        // With wrong port
862        let mut ctx2 = ParseContext::new(1);
863        ctx2.insert_hint("dst_port", 80);
864        assert!(parser.can_parse(&ctx2).is_none());
865
866        // With BGP dst_port
867        let mut ctx3 = ParseContext::new(1);
868        ctx3.insert_hint("dst_port", 179);
869        assert!(parser.can_parse(&ctx3).is_some());
870
871        // With BGP src_port
872        let mut ctx4 = ParseContext::new(1);
873        ctx4.insert_hint("src_port", 179);
874        assert!(parser.can_parse(&ctx4).is_some());
875    }
876
877    // Test 2: Marker validation (16 bytes of 0xFF)
878    #[test]
879    fn test_marker_validation() {
880        let parser = BgpProtocol;
881        let mut context = ParseContext::new(1);
882        context.insert_hint("dst_port", 179);
883
884        // Valid marker
885        let valid_msg = create_bgp_header(message_type::KEEPALIVE, 19);
886        let result = parser.parse(&valid_msg, &context);
887        assert!(result.is_ok());
888
889        // Invalid marker
890        let mut invalid_msg = create_bgp_header(message_type::KEEPALIVE, 19);
891        invalid_msg[0] = 0xFE; // Corrupt marker
892        let result = parser.parse(&invalid_msg, &context);
893        assert!(!result.is_ok());
894        assert!(result.error.unwrap().contains("invalid marker"));
895    }
896
897    // Test 3: Message type parsing
898    #[test]
899    fn test_message_type_parsing() {
900        let parser = BgpProtocol;
901        let mut context = ParseContext::new(1);
902        context.insert_hint("dst_port", 179);
903
904        let test_types = [
905            (message_type::OPEN, "OPEN"),
906            (message_type::UPDATE, "UPDATE"),
907            (message_type::NOTIFICATION, "NOTIFICATION"),
908            (message_type::KEEPALIVE, "KEEPALIVE"),
909            (message_type::ROUTE_REFRESH, "ROUTE-REFRESH"),
910        ];
911
912        for (msg_type, name) in test_types {
913            // Use appropriate length for message type
914            let length = if msg_type == message_type::OPEN {
915                29
916            } else {
917                19
918            };
919            let mut msg = create_bgp_header(msg_type, length);
920            // Add padding for OPEN message
921            if msg_type == message_type::OPEN {
922                msg.extend(vec![0u8; 10]);
923            }
924
925            let result = parser.parse(&msg, &context);
926            assert!(result.is_ok());
927            assert_eq!(
928                result.get("message_type"),
929                Some(&FieldValue::UInt8(msg_type))
930            );
931            assert_eq!(
932                result.get("message_type_name"),
933                Some(&FieldValue::Str(name))
934            );
935        }
936    }
937
938    // Test 4: OPEN message parsing
939    #[test]
940    fn test_open_message_parsing() {
941        let parser = BgpProtocol;
942        let mut context = ParseContext::new(1);
943        context.insert_hint("dst_port", 179);
944
945        let msg = create_bgp_open(4, 65001, 180, [192, 168, 1, 1]);
946        let result = parser.parse(&msg, &context);
947
948        assert!(result.is_ok());
949        assert_eq!(result.get("version"), Some(&FieldValue::UInt8(4)));
950        assert_eq!(result.get("my_as"), Some(&FieldValue::UInt16(65001)));
951        assert_eq!(result.get("hold_time"), Some(&FieldValue::UInt16(180)));
952        assert_eq!(
953            result.get("bgp_id"),
954            Some(&FieldValue::OwnedString(CompactString::new("192.168.1.1")))
955        );
956    }
957
958    // Test 5: KEEPALIVE message (no payload)
959    #[test]
960    fn test_keepalive_message() {
961        let parser = BgpProtocol;
962        let mut context = ParseContext::new(1);
963        context.insert_hint("dst_port", 179);
964
965        let msg = create_bgp_header(message_type::KEEPALIVE, 19);
966        let result = parser.parse(&msg, &context);
967
968        assert!(result.is_ok());
969        assert_eq!(
970            result.get("message_type"),
971            Some(&FieldValue::UInt8(message_type::KEEPALIVE))
972        );
973        assert_eq!(result.get("length"), Some(&FieldValue::UInt16(19)));
974    }
975
976    // Test 6: UPDATE message basic parsing
977    #[test]
978    fn test_update_message_basic_parsing() {
979        let parser = BgpProtocol;
980        let mut context = ParseContext::new(1);
981        context.insert_hint("dst_port", 179);
982
983        let msg = create_bgp_update(5, 10);
984        let result = parser.parse(&msg, &context);
985
986        assert!(result.is_ok());
987        assert_eq!(
988            result.get("message_type"),
989            Some(&FieldValue::UInt8(message_type::UPDATE))
990        );
991        assert_eq!(
992            result.get("withdrawn_routes_len"),
993            Some(&FieldValue::UInt16(5))
994        );
995        assert_eq!(result.get("path_attr_len"), Some(&FieldValue::UInt16(10)));
996    }
997
998    // Test 7: NOTIFICATION message
999    #[test]
1000    fn test_notification_message() {
1001        let parser = BgpProtocol;
1002        let mut context = ParseContext::new(1);
1003        context.insert_hint("dst_port", 179);
1004
1005        let mut msg = create_bgp_header(message_type::NOTIFICATION, 21);
1006        msg.push(6); // Error code: Cease
1007        msg.push(4); // Error subcode: Administrative Reset
1008
1009        let result = parser.parse(&msg, &context);
1010
1011        assert!(result.is_ok());
1012        assert_eq!(
1013            result.get("message_type"),
1014            Some(&FieldValue::UInt8(message_type::NOTIFICATION))
1015        );
1016        assert_eq!(
1017            result.get("message_type_name"),
1018            Some(&FieldValue::Str("NOTIFICATION"))
1019        );
1020    }
1021
1022    // Test 8: Invalid marker rejection
1023    #[test]
1024    fn test_invalid_marker_rejection() {
1025        let parser = BgpProtocol;
1026        let mut context = ParseContext::new(1);
1027        context.insert_hint("dst_port", 179);
1028
1029        // All zeros marker (invalid)
1030        let mut msg = vec![0u8; 16];
1031        msg.extend_from_slice(&19u16.to_be_bytes());
1032        msg.push(message_type::KEEPALIVE);
1033
1034        let result = parser.parse(&msg, &context);
1035        assert!(!result.is_ok());
1036        assert!(result.error.unwrap().contains("invalid marker"));
1037    }
1038
1039    // Test 9: Too short message
1040    #[test]
1041    fn test_bgp_too_short() {
1042        let parser = BgpProtocol;
1043        let mut context = ParseContext::new(1);
1044        context.insert_hint("dst_port", 179);
1045
1046        let short_msg = [0xFF; 18]; // Only 18 bytes
1047        let result = parser.parse(&short_msg, &context);
1048
1049        assert!(!result.is_ok());
1050        assert!(result.error.is_some());
1051    }
1052
1053    // Test 10: Schema fields
1054    #[test]
1055    fn test_bgp_schema_fields() {
1056        let parser = BgpProtocol;
1057        let fields = parser.schema_fields();
1058
1059        assert!(!fields.is_empty());
1060        let field_names: Vec<&str> = fields.iter().map(|f| f.name).collect();
1061        assert!(field_names.contains(&"bgp.message_type"));
1062        assert!(field_names.contains(&"bgp.message_type_name"));
1063        assert!(field_names.contains(&"bgp.length"));
1064        assert!(field_names.contains(&"bgp.version"));
1065        assert!(field_names.contains(&"bgp.my_as"));
1066        assert!(field_names.contains(&"bgp.hold_time"));
1067        assert!(field_names.contains(&"bgp.bgp_id"));
1068        assert!(field_names.contains(&"bgp.withdrawn_routes_len"));
1069        assert!(field_names.contains(&"bgp.path_attr_len"));
1070    }
1071
1072    // Test 11: Multiple BGP messages in stream
1073    #[test]
1074    fn test_multiple_messages() {
1075        let parser = BgpProtocol;
1076        let mut context = ParseContext::new(1);
1077        context.insert_hint("dst_port", 179);
1078
1079        // Two KEEPALIVE messages back-to-back
1080        let mut data = create_bgp_header(message_type::KEEPALIVE, 19);
1081        data.extend(create_bgp_header(message_type::KEEPALIVE, 19));
1082
1083        let result = parser.parse(&data, &context);
1084
1085        assert!(result.is_ok());
1086        assert_eq!(result.remaining.len(), 19); // Second message remains
1087    }
1088
1089    // Test 12: OPEN message with 4-byte ASN capability (RFC 6793)
1090    #[test]
1091    fn test_open_with_4byte_asn_capability() {
1092        let parser = BgpProtocol;
1093        let mut context = ParseContext::new(1);
1094        context.insert_hint("dst_port", 179);
1095
1096        // Build OPEN with optional parameters containing 4-byte AS capability
1097        let mut msg = Vec::new();
1098        msg.extend_from_slice(&BGP_MARKER);
1099
1100        // Build optional parameters: capability 65 (4-byte AS) with value 4200000001
1101        let four_byte_asn: u32 = 4200000001;
1102        let cap_data = [
1103            2,
1104            6, // Parameter type 2 (Capabilities), length 6
1105            capability_code::FOUR_OCTET_AS,
1106            4, // Capability 65, length 4
1107            ((four_byte_asn >> 24) & 0xFF) as u8,
1108            ((four_byte_asn >> 16) & 0xFF) as u8,
1109            ((four_byte_asn >> 8) & 0xFF) as u8,
1110            (four_byte_asn & 0xFF) as u8,
1111        ];
1112
1113        let total_len = 19 + 10 + cap_data.len();
1114        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1115        msg.push(message_type::OPEN);
1116
1117        // OPEN fields
1118        msg.push(4); // Version
1119        msg.extend_from_slice(&AS_TRANS.to_be_bytes()); // my_as = AS_TRANS (23456)
1120        msg.extend_from_slice(&180u16.to_be_bytes()); // Hold time
1121        msg.extend_from_slice(&[10, 0, 0, 1]); // BGP ID
1122        msg.push(cap_data.len() as u8); // Opt params length
1123        msg.extend_from_slice(&cap_data);
1124
1125        let result = parser.parse(&msg, &context);
1126
1127        assert!(result.is_ok());
1128        assert_eq!(result.get("my_as"), Some(&FieldValue::UInt16(AS_TRANS)));
1129        assert_eq!(
1130            result.get("my_as_4byte"),
1131            Some(&FieldValue::UInt32(four_byte_asn))
1132        );
1133
1134        // Check capabilities string contains 4-BYTE-AS
1135        if let Some(FieldValue::OwnedString(caps)) = result.get("capabilities") {
1136            assert!(caps.contains("4-BYTE-AS"));
1137        } else {
1138            panic!("Expected capabilities field");
1139        }
1140    }
1141
1142    // Test 13: UPDATE message with withdrawn routes
1143    #[test]
1144    fn test_update_with_withdrawn_routes() {
1145        let parser = BgpProtocol;
1146        let mut context = ParseContext::new(1);
1147        context.insert_hint("dst_port", 179);
1148
1149        // Build UPDATE with withdrawn routes: 10.0.0.0/8, 192.168.0.0/16
1150        let mut msg = Vec::new();
1151        msg.extend_from_slice(&BGP_MARKER);
1152
1153        // Withdrawn routes: 10.0.0.0/8 (2 bytes: len=8, prefix=10)
1154        //                   192.168.0.0/16 (3 bytes: len=16, prefix=192,168)
1155        let withdrawn = [
1156            8, 10, // 10.0.0.0/8
1157            16, 192, 168, // 192.168.0.0/16
1158        ];
1159
1160        let total_len = 19 + 2 + withdrawn.len() + 2; // header + withdrawn_len + withdrawn + path_attr_len
1161        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1162        msg.push(message_type::UPDATE);
1163
1164        // Withdrawn routes length
1165        msg.extend_from_slice(&(withdrawn.len() as u16).to_be_bytes());
1166        msg.extend_from_slice(&withdrawn);
1167
1168        // Path attribute length (0 - no attributes)
1169        msg.extend_from_slice(&0u16.to_be_bytes());
1170
1171        let result = parser.parse(&msg, &context);
1172
1173        assert!(result.is_ok());
1174        assert_eq!(
1175            result.get("withdrawn_routes_len"),
1176            Some(&FieldValue::UInt16(5))
1177        );
1178        assert_eq!(result.get("withdrawn_count"), Some(&FieldValue::UInt16(2)));
1179
1180        if let Some(FieldValue::OwnedString(routes)) = result.get("withdrawn_routes") {
1181            assert!(routes.contains("10.0.0.0/8"));
1182            assert!(routes.contains("192.168.0.0/16"));
1183        } else {
1184            panic!("Expected withdrawn_routes field");
1185        }
1186    }
1187
1188    // Test 14: UPDATE message with path attributes
1189    #[test]
1190    fn test_update_with_path_attributes() {
1191        let parser = BgpProtocol;
1192        let mut context = ParseContext::new(1);
1193        context.insert_hint("dst_port", 179);
1194
1195        // Build path attributes
1196        let mut path_attrs = Vec::new();
1197
1198        // ORIGIN (type 1): IGP (0) - flags: well-known, transitive
1199        path_attrs.extend_from_slice(&[0x40, path_attr_type::ORIGIN, 1, origin_type::IGP]);
1200
1201        // AS_PATH (type 2): AS_SEQUENCE with 2-byte ASNs [65001, 65002]
1202        path_attrs.extend_from_slice(&[
1203            0x40,
1204            path_attr_type::AS_PATH,
1205            6,
1206            as_path_segment_type::AS_SEQUENCE,
1207            2, // AS_SEQUENCE with 2 ASNs
1208            0xFD,
1209            0xE9, // 65001
1210            0xFD,
1211            0xEA, // 65002
1212        ]);
1213
1214        // NEXT_HOP (type 3): 192.168.1.1
1215        path_attrs.extend_from_slice(&[0x40, path_attr_type::NEXT_HOP, 4, 192, 168, 1, 1]);
1216
1217        // MED (type 4): 100
1218        path_attrs.extend_from_slice(&[0x80, path_attr_type::MULTI_EXIT_DISC, 4, 0, 0, 0, 100]);
1219
1220        // LOCAL_PREF (type 5): 200
1221        path_attrs.extend_from_slice(&[0x40, path_attr_type::LOCAL_PREF, 4, 0, 0, 0, 200]);
1222
1223        // Build message
1224        let mut msg = Vec::new();
1225        msg.extend_from_slice(&BGP_MARKER);
1226
1227        let total_len = 19 + 2 + 2 + path_attrs.len();
1228        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1229        msg.push(message_type::UPDATE);
1230
1231        // Withdrawn routes length (0)
1232        msg.extend_from_slice(&0u16.to_be_bytes());
1233
1234        // Path attribute length
1235        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1236        msg.extend_from_slice(&path_attrs);
1237
1238        let result = parser.parse(&msg, &context);
1239
1240        assert!(result.is_ok());
1241        assert_eq!(
1242            result.get("origin"),
1243            Some(&FieldValue::UInt8(origin_type::IGP))
1244        );
1245        assert_eq!(result.get("origin_name"), Some(&FieldValue::Str("IGP")));
1246        assert_eq!(
1247            result.get("next_hop"),
1248            Some(&FieldValue::OwnedString(CompactString::new("192.168.1.1")))
1249        );
1250        assert_eq!(result.get("med"), Some(&FieldValue::UInt32(100)));
1251        assert_eq!(result.get("local_pref"), Some(&FieldValue::UInt32(200)));
1252        assert_eq!(result.get("as_path_length"), Some(&FieldValue::UInt16(2)));
1253
1254        if let Some(FieldValue::OwnedString(as_path)) = result.get("as_path") {
1255            assert!(as_path.contains("65001"));
1256            assert!(as_path.contains("65002"));
1257        } else {
1258            panic!("Expected as_path field");
1259        }
1260    }
1261
1262    // Test 15: UPDATE message with NLRI (advertised prefixes)
1263    #[test]
1264    fn test_update_with_nlri() {
1265        let parser = BgpProtocol;
1266        let mut context = ParseContext::new(1);
1267        context.insert_hint("dst_port", 179);
1268
1269        // Minimal path attributes (ORIGIN + AS_PATH + NEXT_HOP required)
1270        let path_attrs = [
1271            0x40,
1272            path_attr_type::ORIGIN,
1273            1,
1274            origin_type::IGP,
1275            0x40,
1276            path_attr_type::AS_PATH,
1277            0, // Empty AS_PATH
1278            0x40,
1279            path_attr_type::NEXT_HOP,
1280            4,
1281            10,
1282            0,
1283            0,
1284            1,
1285        ];
1286
1287        // NLRI: 172.16.0.0/12
1288        let nlri = [12, 172, 16]; // /12 prefix needs 2 bytes
1289
1290        let mut msg = Vec::new();
1291        msg.extend_from_slice(&BGP_MARKER);
1292
1293        let total_len = 19 + 2 + 2 + path_attrs.len() + nlri.len();
1294        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1295        msg.push(message_type::UPDATE);
1296
1297        msg.extend_from_slice(&0u16.to_be_bytes()); // No withdrawn
1298        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1299        msg.extend_from_slice(&path_attrs);
1300        msg.extend_from_slice(&nlri);
1301
1302        let result = parser.parse(&msg, &context);
1303
1304        assert!(result.is_ok());
1305        assert_eq!(result.get("nlri_count"), Some(&FieldValue::UInt16(1)));
1306
1307        if let Some(FieldValue::OwnedString(nlri_str)) = result.get("nlri") {
1308            assert!(nlri_str.contains("172.16.0.0/12"));
1309        } else {
1310            panic!("Expected nlri field");
1311        }
1312    }
1313
1314    // Test 16: ROUTE-REFRESH message (RFC 2918)
1315    #[test]
1316    fn test_route_refresh_message() {
1317        let parser = BgpProtocol;
1318        let mut context = ParseContext::new(1);
1319        context.insert_hint("dst_port", 179);
1320
1321        let mut msg = create_bgp_header(message_type::ROUTE_REFRESH, 23); // 19 + 4
1322
1323        // AFI = 1 (IPv4), Reserved = 0, SAFI = 1 (Unicast)
1324        msg.extend_from_slice(&1u16.to_be_bytes()); // AFI
1325        msg.push(0); // Reserved
1326        msg.push(1); // SAFI
1327
1328        let result = parser.parse(&msg, &context);
1329
1330        assert!(result.is_ok());
1331        assert_eq!(
1332            result.get("message_type"),
1333            Some(&FieldValue::UInt8(message_type::ROUTE_REFRESH))
1334        );
1335        assert_eq!(
1336            result.get("message_type_name"),
1337            Some(&FieldValue::Str("ROUTE-REFRESH"))
1338        );
1339        assert_eq!(result.get("afi"), Some(&FieldValue::UInt16(1)));
1340        assert_eq!(result.get("safi"), Some(&FieldValue::UInt8(1)));
1341    }
1342
1343    // Test 17: ROUTE-REFRESH for IPv6 multicast
1344    #[test]
1345    fn test_route_refresh_ipv6_multicast() {
1346        let parser = BgpProtocol;
1347        let mut context = ParseContext::new(1);
1348        context.insert_hint("dst_port", 179);
1349
1350        let mut msg = create_bgp_header(message_type::ROUTE_REFRESH, 23);
1351
1352        // AFI = 2 (IPv6), SAFI = 2 (Multicast)
1353        msg.extend_from_slice(&2u16.to_be_bytes()); // AFI
1354        msg.push(0); // Reserved
1355        msg.push(2); // SAFI
1356
1357        let result = parser.parse(&msg, &context);
1358
1359        assert!(result.is_ok());
1360        assert_eq!(result.get("afi"), Some(&FieldValue::UInt16(2)));
1361        assert_eq!(result.get("safi"), Some(&FieldValue::UInt8(2)));
1362    }
1363
1364    // Test 18: AS_PATH with AS_SET segment
1365    #[test]
1366    fn test_as_path_with_as_set() {
1367        let parser = BgpProtocol;
1368        let mut context = ParseContext::new(1);
1369        context.insert_hint("dst_port", 179);
1370
1371        // AS_PATH with AS_SET {65001, 65002, 65003}
1372        let path_attrs = [
1373            0x40,
1374            path_attr_type::ORIGIN,
1375            1,
1376            origin_type::IGP,
1377            0x40,
1378            path_attr_type::AS_PATH,
1379            8,
1380            as_path_segment_type::AS_SET,
1381            3, // AS_SET with 3 ASNs
1382            0xFD,
1383            0xE9, // 65001
1384            0xFD,
1385            0xEA, // 65002
1386            0xFD,
1387            0xEB, // 65003
1388            0x40,
1389            path_attr_type::NEXT_HOP,
1390            4,
1391            10,
1392            0,
1393            0,
1394            1,
1395        ];
1396
1397        let mut msg = Vec::new();
1398        msg.extend_from_slice(&BGP_MARKER);
1399
1400        let total_len = 19 + 2 + 2 + path_attrs.len();
1401        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1402        msg.push(message_type::UPDATE);
1403
1404        msg.extend_from_slice(&0u16.to_be_bytes());
1405        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1406        msg.extend_from_slice(&path_attrs);
1407
1408        let result = parser.parse(&msg, &context);
1409
1410        assert!(result.is_ok());
1411        // AS_SET counts as 1 for path length
1412        assert_eq!(result.get("as_path_length"), Some(&FieldValue::UInt16(1)));
1413
1414        if let Some(FieldValue::OwnedString(as_path)) = result.get("as_path") {
1415            // AS_SET should be formatted with braces
1416            assert!(as_path.contains("{"));
1417            assert!(as_path.contains("}"));
1418        } else {
1419            panic!("Expected as_path field");
1420        }
1421    }
1422
1423    // Test 19: NOTIFICATION with error code and subcode
1424    #[test]
1425    fn test_notification_error_codes() {
1426        let parser = BgpProtocol;
1427        let mut context = ParseContext::new(1);
1428        context.insert_hint("dst_port", 179);
1429
1430        let test_cases = [
1431            (error_code::MESSAGE_HEADER_ERROR, 1, "Message Header Error"),
1432            (error_code::OPEN_MESSAGE_ERROR, 2, "OPEN Message Error"),
1433            (error_code::UPDATE_MESSAGE_ERROR, 1, "UPDATE Message Error"),
1434            (error_code::HOLD_TIMER_EXPIRED, 0, "Hold Timer Expired"),
1435            (error_code::FSM_ERROR, 0, "Finite State Machine Error"),
1436            (error_code::CEASE, 4, "Cease"), // Subcode 4 = Administrative Reset
1437        ];
1438
1439        for (err_code, err_subcode, expected_name) in test_cases {
1440            let mut msg = create_bgp_header(message_type::NOTIFICATION, 21);
1441            msg.push(err_code);
1442            msg.push(err_subcode);
1443
1444            let result = parser.parse(&msg, &context);
1445
1446            assert!(result.is_ok());
1447            assert_eq!(result.get("error_code"), Some(&FieldValue::UInt8(err_code)));
1448            assert_eq!(
1449                result.get("error_subcode"),
1450                Some(&FieldValue::UInt8(err_subcode))
1451            );
1452            assert_eq!(
1453                result.get("error_code_name"),
1454                Some(&FieldValue::Str(expected_name))
1455            );
1456        }
1457    }
1458
1459    // Test 20: ATOMIC_AGGREGATE attribute
1460    #[test]
1461    fn test_atomic_aggregate_attribute() {
1462        let parser = BgpProtocol;
1463        let mut context = ParseContext::new(1);
1464        context.insert_hint("dst_port", 179);
1465
1466        let path_attrs = [
1467            0x40,
1468            path_attr_type::ORIGIN,
1469            1,
1470            origin_type::IGP,
1471            0x40,
1472            path_attr_type::AS_PATH,
1473            0,
1474            0x40,
1475            path_attr_type::NEXT_HOP,
1476            4,
1477            10,
1478            0,
1479            0,
1480            1,
1481            0x40,
1482            path_attr_type::ATOMIC_AGGREGATE,
1483            0, // Zero length
1484        ];
1485
1486        let mut msg = Vec::new();
1487        msg.extend_from_slice(&BGP_MARKER);
1488
1489        let total_len = 19 + 2 + 2 + path_attrs.len();
1490        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1491        msg.push(message_type::UPDATE);
1492
1493        msg.extend_from_slice(&0u16.to_be_bytes());
1494        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1495        msg.extend_from_slice(&path_attrs);
1496
1497        let result = parser.parse(&msg, &context);
1498
1499        assert!(result.is_ok());
1500        assert_eq!(
1501            result.get("atomic_aggregate"),
1502            Some(&FieldValue::Bool(true))
1503        );
1504    }
1505
1506    // Test 21: AGGREGATOR attribute
1507    #[test]
1508    fn test_aggregator_attribute() {
1509        let parser = BgpProtocol;
1510        let mut context = ParseContext::new(1);
1511        context.insert_hint("dst_port", 179);
1512
1513        // AGGREGATOR with 2-byte AS
1514        let path_attrs = [
1515            0x40,
1516            path_attr_type::ORIGIN,
1517            1,
1518            origin_type::IGP,
1519            0x40,
1520            path_attr_type::AS_PATH,
1521            0,
1522            0x40,
1523            path_attr_type::NEXT_HOP,
1524            4,
1525            10,
1526            0,
1527            0,
1528            1,
1529            0xC0,
1530            path_attr_type::AGGREGATOR,
1531            6, // Optional, Transitive
1532            0xFD,
1533            0xE9, // AS 65001
1534            192,
1535            168,
1536            1,
1537            1, // Aggregator IP
1538        ];
1539
1540        let mut msg = Vec::new();
1541        msg.extend_from_slice(&BGP_MARKER);
1542
1543        let total_len = 19 + 2 + 2 + path_attrs.len();
1544        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1545        msg.push(message_type::UPDATE);
1546
1547        msg.extend_from_slice(&0u16.to_be_bytes());
1548        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1549        msg.extend_from_slice(&path_attrs);
1550
1551        let result = parser.parse(&msg, &context);
1552
1553        assert!(result.is_ok());
1554        assert_eq!(
1555            result.get("aggregator_as"),
1556            Some(&FieldValue::UInt32(65001))
1557        );
1558        assert_eq!(
1559            result.get("aggregator_ip"),
1560            Some(&FieldValue::OwnedString(CompactString::new("192.168.1.1")))
1561        );
1562    }
1563
1564    // Test 22: Extended length path attribute
1565    #[test]
1566    fn test_extended_length_attribute() {
1567        let parser = BgpProtocol;
1568        let mut context = ParseContext::new(1);
1569        context.insert_hint("dst_port", 179);
1570
1571        // Build a path with extended length flag
1572        let mut path_attrs = Vec::new();
1573
1574        // ORIGIN with standard length
1575        path_attrs.extend_from_slice(&[0x40, path_attr_type::ORIGIN, 1, origin_type::EGP]);
1576
1577        // AS_PATH with extended length flag (0x10)
1578        // Long AS_PATH with multiple ASNs
1579        path_attrs.push(0x50); // Transitive + Extended length
1580        path_attrs.push(path_attr_type::AS_PATH);
1581        path_attrs.extend_from_slice(&14u16.to_be_bytes()); // Length as 2 bytes
1582        path_attrs.push(as_path_segment_type::AS_SEQUENCE);
1583        path_attrs.push(6); // 6 ASNs
1584        for asn in [65001u16, 65002, 65003, 65004, 65005, 65006] {
1585            path_attrs.extend_from_slice(&asn.to_be_bytes());
1586        }
1587
1588        // NEXT_HOP
1589        path_attrs.extend_from_slice(&[0x40, path_attr_type::NEXT_HOP, 4, 10, 0, 0, 1]);
1590
1591        let mut msg = Vec::new();
1592        msg.extend_from_slice(&BGP_MARKER);
1593
1594        let total_len = 19 + 2 + 2 + path_attrs.len();
1595        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1596        msg.push(message_type::UPDATE);
1597
1598        msg.extend_from_slice(&0u16.to_be_bytes());
1599        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1600        msg.extend_from_slice(&path_attrs);
1601
1602        let result = parser.parse(&msg, &context);
1603
1604        assert!(result.is_ok());
1605        assert_eq!(
1606            result.get("origin"),
1607            Some(&FieldValue::UInt8(origin_type::EGP))
1608        );
1609        assert_eq!(result.get("origin_name"), Some(&FieldValue::Str("EGP")));
1610        assert_eq!(result.get("as_path_length"), Some(&FieldValue::UInt16(6)));
1611    }
1612
1613    // Test 23: Multiple NLRI prefixes
1614    #[test]
1615    fn test_multiple_nlri_prefixes() {
1616        let parser = BgpProtocol;
1617        let mut context = ParseContext::new(1);
1618        context.insert_hint("dst_port", 179);
1619
1620        let path_attrs = [
1621            0x40,
1622            path_attr_type::ORIGIN,
1623            1,
1624            origin_type::IGP,
1625            0x40,
1626            path_attr_type::AS_PATH,
1627            0,
1628            0x40,
1629            path_attr_type::NEXT_HOP,
1630            4,
1631            10,
1632            0,
1633            0,
1634            1,
1635        ];
1636
1637        // Multiple NLRI: 10.0.0.0/8, 172.16.0.0/16, 192.168.1.0/24
1638        let nlri = [
1639            8, 10, // 10.0.0.0/8
1640            16, 172, 16, // 172.16.0.0/16
1641            24, 192, 168, 1, // 192.168.1.0/24
1642        ];
1643
1644        let mut msg = Vec::new();
1645        msg.extend_from_slice(&BGP_MARKER);
1646
1647        let total_len = 19 + 2 + 2 + path_attrs.len() + nlri.len();
1648        msg.extend_from_slice(&(total_len as u16).to_be_bytes());
1649        msg.push(message_type::UPDATE);
1650
1651        msg.extend_from_slice(&0u16.to_be_bytes());
1652        msg.extend_from_slice(&(path_attrs.len() as u16).to_be_bytes());
1653        msg.extend_from_slice(&path_attrs);
1654        msg.extend_from_slice(&nlri);
1655
1656        let result = parser.parse(&msg, &context);
1657
1658        assert!(result.is_ok());
1659        assert_eq!(result.get("nlri_count"), Some(&FieldValue::UInt16(3)));
1660
1661        if let Some(FieldValue::OwnedString(nlri_str)) = result.get("nlri") {
1662            assert!(nlri_str.contains("10.0.0.0/8"));
1663            assert!(nlri_str.contains("172.16.0.0/16"));
1664            assert!(nlri_str.contains("192.168.1.0/24"));
1665        } else {
1666            panic!("Expected nlri field");
1667        }
1668    }
1669}