Skip to main content

bgpkit_parser/parser/bgp/
dissect.rs

1//! Best-effort, Wireshark-style dissection of BGP messages.
2//!
3//! This module walks the same wire grammar the parsers consume, but instead of
4//! producing model structs it produces a [`DissectionNode`] tree in which every
5//! field carries its byte range. It is a separate pass over already-received
6//! bytes: nothing here runs on the default parsing hot path, and a dissector
7//! never fails — on truncated or malformed input the tree simply stops at the
8//! last field that could be walked, which is exactly the "show me where
9//! parsing died" experience a byte-level inspector wants.
10
11use crate::models::{AsnLength, DissectionNode};
12use std::net::{Ipv4Addr, Ipv6Addr};
13
14/// IANA path attribute names for the type codes this dissector labels.
15fn attr_name(code: u8) -> &'static str {
16    match code {
17        1 => "ORIGIN",
18        2 => "AS_PATH",
19        3 => "NEXT_HOP",
20        4 => "MULTI_EXIT_DISC",
21        5 => "LOCAL_PREF",
22        6 => "ATOMIC_AGGREGATE",
23        7 => "AGGREGATOR",
24        8 => "COMMUNITIES",
25        9 => "ORIGINATOR_ID",
26        10 => "CLUSTER_LIST",
27        14 => "MP_REACH_NLRI",
28        15 => "MP_UNREACH_NLRI",
29        16 => "EXTENDED_COMMUNITIES",
30        17 => "AS4_PATH",
31        18 => "AS4_AGGREGATOR",
32        22 => "PMSI_TUNNEL",
33        23 => "TUNNEL_ENCAPSULATION",
34        24 => "TRAFFIC_ENGINEERING",
35        25 => "IPV6_EXTENDED_COMMUNITIES",
36        26 => "AIGP",
37        27 => "PE_DISTINGUISHER_LABELS",
38        29 => "BGP-LS",
39        32 => "LARGE_COMMUNITY",
40        33 => "BGPSEC_PATH",
41        35 => "ONLY_TO_CUSTOMER",
42        37 => "SFP",
43        38 => "BFD_DISCRIMINATOR",
44        40 => "BGP_PREFIX_SID",
45        41 => "BIER",
46        _ => "ATTRIBUTE",
47    }
48}
49
50pub(crate) fn read_u16(data: &[u8], pos: usize) -> Option<u16> {
51    let hi = *data.get(pos)?;
52    let lo = *data.get(pos + 1)?;
53    Some(u16::from_be_bytes([hi, lo]))
54}
55
56fn read_u32(data: &[u8], pos: usize) -> Option<u32> {
57    let b: [u8; 4] = data.get(pos..pos + 4)?.try_into().ok()?;
58    Some(u32::from_be_bytes(b))
59}
60
61fn render_prefix(afi_v4: bool, plen: u8, octets: &[u8]) -> String {
62    if afi_v4 {
63        let mut octets = octets.to_vec();
64        octets.resize(4, 0);
65        format!(
66            "{}.{}.{}.{}/{}",
67            octets[0], octets[1], octets[2], octets[3], plen
68        )
69    } else {
70        let mut octets = octets.to_vec();
71        octets.resize(16, 0);
72        let segs: Vec<u16> = octets
73            .chunks(2)
74            .map(|c| u16::from_be_bytes([c[0], c[1]]))
75            .collect();
76        let s = &segs;
77        format!(
78            "{}/{}",
79            Ipv6Addr::new(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]),
80            plen
81        )
82    }
83}
84
85/// Dissect a standalone BGP message (offsets relative to the message start).
86///
87/// `asn_len` controls how AS numbers inside AS_PATH/AGGREGATOR values are
88/// rendered (2- or 4-octet encoding); `add_path` indicates ADD-PATH encoded
89/// NLRI (RFC 7911).
90pub fn dissect_bgp_message(data: &[u8], asn_len: &AsnLength, add_path: bool) -> DissectionNode {
91    dissect_bgp_message_base(data, 0, asn_len, add_path)
92}
93
94/// Dissect a BGP message embedded at `base` within a larger buffer (e.g. an
95/// MRT BGP4MP record), so that all tree offsets share one coordinate space
96/// with the enclosing container.
97pub(crate) fn dissect_bgp_message_base(
98    data: &[u8],
99    base: u32,
100    asn_len: &AsnLength,
101    add_path: bool,
102) -> DissectionNode {
103    if data.len() < 19 {
104        let mut root = DissectionNode::new(
105            "bgp",
106            format!("BGP message (truncated, {} of 19 header bytes)", data.len()),
107            base,
108            data.len() as u32,
109        );
110        if !data.is_empty() {
111            root.children.push(DissectionNode::new(
112                "bgp.header",
113                format!("Header (truncated, {} of 19 bytes)", data.len()),
114                base,
115                data.len() as u32,
116            ));
117        }
118        return root;
119    }
120
121    let length = u16::from_be_bytes([data[16], data[17]]);
122    let msg_type = data[18];
123
124    // RFC 4271: the declared length bounds the message. Trailing buffer
125    // bytes are not message fields; a declaration beyond the available
126    // bytes is noted explicitly and the walk continues over what is
127    // present.
128    let declared_body = (length as usize).saturating_sub(19);
129    let available_body = data.len() - 19;
130    let body_len = declared_body.min(available_body);
131    let message_len = 19 + body_len;
132
133    let mut root = DissectionNode::new(
134        "bgp",
135        format!("BGP message ({message_len} bytes)"),
136        base,
137        message_len as u32,
138    );
139
140    let mut header = DissectionNode::new("bgp.header", "Header", base, 19);
141    let all_ones = data[..16].iter().all(|b| *b == 0xFF);
142    header.children.push(DissectionNode::new(
143        "bgp.header.marker",
144        if all_ones {
145            "Marker (all ones)".to_string()
146        } else {
147            "Marker (NOT all ones — invalid per RFC 4271)".to_string()
148        },
149        base,
150        16,
151    ));
152    header.children.push(DissectionNode::new(
153        "bgp.header.length",
154        format!("Length: {length}"),
155        base + 16,
156        2,
157    ));
158    let type_name = match msg_type {
159        1 => "OPEN",
160        2 => "UPDATE",
161        3 => "NOTIFICATION",
162        4 => "KEEPALIVE",
163        5 => "ROUTE-REFRESH",
164        _ => "UNKNOWN",
165    };
166    header.children.push(DissectionNode::new(
167        "bgp.header.type",
168        format!("Type: {type_name} ({msg_type})"),
169        base + 18,
170        1,
171    ));
172    root.children.push(header);
173
174    if declared_body > available_body {
175        root.children.push(DissectionNode::new(
176            "bgp.truncated",
177            format!(
178                "Declared length {length} exceeds the {} available bytes",
179                data.len()
180            ),
181            base + message_len as u32,
182            0,
183        ));
184    }
185
186    let body = &data[19..19 + body_len];
187    let body_base = base + 19;
188    match msg_type {
189        1 => root.children.push(dissect_open(body, body_base)),
190        2 => root
191            .children
192            .push(dissect_update(body, body_base, asn_len, add_path)),
193        3 => root.children.push(dissect_notification(body, body_base)),
194        4 => {}
195        5 => root.children.push(dissect_route_refresh(body, body_base)),
196        _ => root.children.push(DissectionNode::new(
197            "bgp.body",
198            format!("Unknown message type ({} bytes)", body.len()),
199            body_base,
200            body.len() as u32,
201        )),
202    }
203
204    if available_body > body_len {
205        root.children.push(DissectionNode::new(
206            "bgp.trailing",
207            format!(
208                "Trailing bytes beyond the declared message ({} bytes)",
209                available_body - body_len
210            ),
211            base + message_len as u32,
212            (available_body - body_len) as u32,
213        ));
214    }
215    root
216}
217
218fn dissect_update(data: &[u8], base: u32, asn_len: &AsnLength, add_path: bool) -> DissectionNode {
219    let mut update = DissectionNode::new("bgp.update", "UPDATE", base, data.len() as u32);
220
221    if data.len() < 2 {
222        if !data.is_empty() {
223            update.children.push(DissectionNode::new(
224                "bgp.update.withdrawn_routes.length",
225                "Withdrawn routes length (truncated)",
226                base,
227                data.len() as u32,
228            ));
229        }
230        return update;
231    }
232
233    let withdrawn_len = u16::from_be_bytes([data[0], data[1]]) as usize;
234    update.children.push(DissectionNode::new(
235        "bgp.update.withdrawn_routes.length",
236        format!("Withdrawn routes length: {withdrawn_len}"),
237        base,
238        2,
239    ));
240
241    let mut pos = 2usize;
242    if pos + withdrawn_len > data.len() {
243        update.children.push(DissectionNode::new(
244            "bgp.update.withdrawn_routes",
245            format!(
246                "Withdrawn routes (truncated: {} of {} bytes)",
247                data.len() - pos,
248                withdrawn_len
249            ),
250            base + pos as u32,
251            (data.len() - pos) as u32,
252        ));
253        return update;
254    }
255    if withdrawn_len > 0 {
256        let mut section = DissectionNode::new(
257            "bgp.update.withdrawn_routes",
258            format!("Withdrawn routes ({withdrawn_len} bytes)"),
259            base + 2,
260            withdrawn_len as u32,
261        );
262        section.children = dissect_nlri(&data[2..2 + withdrawn_len], base + 2, true, add_path);
263        update.children.push(section);
264    }
265    pos = 2 + withdrawn_len;
266
267    if pos + 2 > data.len() {
268        if pos < data.len() {
269            update.children.push(DissectionNode::new(
270                "bgp.update.path_attributes.length",
271                "Total path attribute length (truncated)",
272                base + pos as u32,
273                (data.len() - pos) as u32,
274            ));
275        }
276        return update;
277    }
278    let attr_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
279    update.children.push(DissectionNode::new(
280        "bgp.update.path_attributes.length",
281        format!("Total path attribute length: {attr_len}"),
282        base + pos as u32,
283        2,
284    ));
285    pos += 2;
286
287    let attr_end = (pos + attr_len).min(data.len());
288    if attr_end > pos {
289        let mut section = DissectionNode::new(
290            "bgp.update.path_attributes",
291            format!("Path attributes ({attr_len} bytes)"),
292            base + pos as u32,
293            (attr_end - pos) as u32,
294        );
295        section.children =
296            dissect_attributes(&data[pos..attr_end], base + pos as u32, asn_len, add_path);
297        update.children.push(section);
298    } else if attr_len > 0 {
299        update.children.push(DissectionNode::new(
300            "bgp.update.path_attributes",
301            format!("Path attributes (truncated: declared {attr_len} bytes)"),
302            base + pos as u32,
303            0,
304        ));
305    }
306    pos = attr_end;
307
308    if pos < data.len() {
309        let mut section = DissectionNode::new(
310            "bgp.update.nlri",
311            format!(
312                "Network Layer Reachability Information ({} bytes)",
313                data.len() - pos
314            ),
315            base + pos as u32,
316            (data.len() - pos) as u32,
317        );
318        section.children = dissect_nlri(&data[pos..], base + pos as u32, true, add_path);
319        update.children.push(section);
320    }
321
322    update
323}
324
325/// Walk a TLV list of path attributes (RFC 4271 Section 4.3).
326fn dissect_attributes(
327    data: &[u8],
328    base: u32,
329    asn_len: &AsnLength,
330    add_path: bool,
331) -> Vec<DissectionNode> {
332    let mut nodes = Vec::new();
333    let mut pos = 0usize;
334    let asn_size = match asn_len {
335        AsnLength::Bits16 => 2,
336        AsnLength::Bits32 => 4,
337    };
338
339    while pos + 3 <= data.len() {
340        let flags = data[pos];
341        let code = data[pos + 1];
342        let extended = flags & 0x10 != 0;
343        let header_len = if extended { 4 } else { 3 };
344        if pos + header_len > data.len() {
345            break;
346        }
347        let value_len = if extended {
348            match read_u16(data, pos + 2) {
349                Some(v) => v as usize,
350                None => break,
351            }
352        } else {
353            data[pos + 2] as usize
354        };
355        let value_start = pos + header_len;
356        let value_end = value_start + value_len;
357        if value_end > data.len() {
358            // Truncated attribute: emit the header plus whatever value bytes
359            // are present, then stop — the length field can no longer be
360            // trusted to walk past this point.
361            let attr_end = data.len();
362            let mut node = DissectionNode::new(
363                format!("bgp.attr.{code}"),
364                format!(
365                    "{} (type {code}) — truncated ({} of {} value bytes)",
366                    attr_name(code),
367                    attr_end - value_start,
368                    value_len
369                ),
370                base + pos as u32,
371                (attr_end - pos) as u32,
372            );
373            node.children.push(DissectionNode::new(
374                "bgp.attr.flags",
375                format!("Flags: 0x{flags:02X}"),
376                base + pos as u32,
377                1,
378            ));
379            nodes.push(node);
380            break;
381        }
382
383        let total = header_len + value_len;
384        let mut node = DissectionNode::new(
385            format!("bgp.attr.{code}"),
386            format!("{} (type {code}), {value_len} bytes", attr_name(code)),
387            base + pos as u32,
388            total as u32,
389        );
390        node.children.push(DissectionNode::new(
391            "bgp.attr.flags",
392            format!("Flags: 0x{flags:02X}"),
393            base + pos as u32,
394            1,
395        ));
396        node.children.push(DissectionNode::new(
397            "bgp.attr.type",
398            format!("Type: {code} ({})", attr_name(code)),
399            base + pos as u32 + 1,
400            1,
401        ));
402        node.children.push(DissectionNode::new(
403            "bgp.attr.length",
404            format!("Length: {value_len}"),
405            base + pos as u32 + 2,
406            (header_len - 2) as u32,
407        ));
408        let value_base = base + value_start as u32;
409        let mut value_node = DissectionNode::new(
410            "bgp.attr.value",
411            format!("Value ({value_len} bytes)"),
412            value_base,
413            value_len as u32,
414        );
415        value_node.children = dissect_attr_value(
416            code,
417            &data[value_start..value_end],
418            value_base,
419            asn_size,
420            add_path,
421        );
422        node.children.push(value_node);
423
424        nodes.push(node);
425        pos = value_end;
426    }
427
428    nodes
429}
430
431/// Dissect the value body of one path attribute at showcase depth: AS_PATH
432/// segments, community entries, MP_REACH/MP_UNREACH structure, and the fixed
433/// u32 fields. Other attributes keep a single value node.
434fn dissect_attr_value(
435    code: u8,
436    value: &[u8],
437    base: u32,
438    asn_size: usize,
439    add_path: bool,
440) -> Vec<DissectionNode> {
441    let mut nodes = Vec::new();
442    match code {
443        1 => {
444            // ORIGIN
445            if let Some(v) = value.first() {
446                let name = match v {
447                    0 => "IGP",
448                    1 => "EGP",
449                    2 => "INCOMPLETE",
450                    _ => "INVALID",
451                };
452                nodes.push(DissectionNode::new(
453                    "bgp.attr.origin",
454                    format!("Origin: {name} ({v})"),
455                    base,
456                    1,
457                ));
458            }
459        }
460        2 | 17 => {
461            // AS_PATH / AS4_PATH: segments of type(1) + count(1) + count*asn.
462            // AS4_PATH (RFC 6793) always carries 4-octet ASNs regardless of
463            // the session's AS width.
464            let asn_size = if code == 17 { 4 } else { asn_size };
465            let mut pos = 0usize;
466            while pos + 2 <= value.len() {
467                let seg_type = value[pos];
468                let count = value[pos + 1] as usize;
469                let seg_len = 2 + count * asn_size;
470                if pos + seg_len > value.len() {
471                    break;
472                }
473                let name = match seg_type {
474                    1 => "AS_SET",
475                    2 => "AS_SEQUENCE",
476                    3 => "AS_CONFED_SEQUENCE",
477                    4 => "AS_CONFED_SET",
478                    _ => "UNKNOWN",
479                };
480                let mut asns = Vec::with_capacity(count);
481                for i in 0..count {
482                    let at = pos + 2 + i * asn_size;
483                    let asn = if asn_size == 2 {
484                        read_u16(value, at).unwrap_or(0) as u32
485                    } else {
486                        read_u32(value, at).unwrap_or(0)
487                    };
488                    asns.push(asn.to_string());
489                }
490                nodes.push(DissectionNode::new(
491                    "bgp.attr.as_path.segment",
492                    format!("{name}: {}", asns.join(" ")),
493                    base + pos as u32,
494                    seg_len as u32,
495                ));
496                pos += seg_len;
497            }
498        }
499        3 => {
500            if let Some(ip) = value.get(0..4) {
501                let ipv4 = Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]);
502                nodes.push(DissectionNode::new(
503                    "bgp.attr.next_hop",
504                    format!("Next hop: {ipv4}"),
505                    base,
506                    value.len() as u32,
507                ));
508            }
509        }
510        4 | 5 | 35 => {
511            if let Some(v) = read_u32(value, 0) {
512                let name = match code {
513                    4 => "Multi-exit discriminator",
514                    5 => "Local preference",
515                    _ => "Only to customer",
516                };
517                nodes.push(DissectionNode::new(
518                    "bgp.attr.u32",
519                    format!("{name}: {v}"),
520                    base,
521                    value.len() as u32,
522                ));
523            }
524        }
525        7 | 18 => {
526            // AGGREGATOR: ASN (variable on the wire) + router ID (4)
527            if value.len() > 4 {
528                let asn_len_field = value.len() - 4;
529                let asn = if asn_len_field == 4 {
530                    read_u32(value, 0).unwrap_or(0)
531                } else {
532                    read_u16(value, 0).unwrap_or(0) as u32
533                };
534                nodes.push(DissectionNode::new(
535                    "bgp.attr.aggregator.asn",
536                    format!("Aggregator ASN: {asn}"),
537                    base,
538                    asn_len_field as u32,
539                ));
540                let id = Ipv4Addr::new(
541                    value[asn_len_field],
542                    value[asn_len_field + 1],
543                    value[asn_len_field + 2],
544                    value[asn_len_field + 3],
545                );
546                nodes.push(DissectionNode::new(
547                    "bgp.attr.aggregator.id",
548                    format!("Aggregator router ID: {id}"),
549                    base + asn_len_field as u32,
550                    4,
551                ));
552            }
553        }
554        8 => {
555            // COMMUNITIES: 4-byte entries
556            let mut pos = 0usize;
557            let mut idx = 1;
558            while pos + 4 <= value.len() {
559                let asn = read_u16(value, pos).unwrap_or(0);
560                let val = read_u16(value, pos + 2).unwrap_or(0);
561                nodes.push(DissectionNode::new(
562                    "bgp.attr.communities.entry",
563                    format!("Community [{idx}]: {asn}:{val}"),
564                    base + pos as u32,
565                    4,
566                ));
567                pos += 4;
568                idx += 1;
569            }
570        }
571        9 => {
572            if let Some(ip) = value.get(0..4) {
573                nodes.push(DissectionNode::new(
574                    "bgp.attr.originator_id",
575                    format!("Originator ID: {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]),
576                    base,
577                    value.len() as u32,
578                ));
579            }
580        }
581        10 => {
582            // CLUSTER_LIST: 4-byte entries
583            let mut pos = 0usize;
584            let mut idx = 1;
585            while pos + 4 <= value.len() {
586                let id = read_u32(value, pos).unwrap_or(0);
587                nodes.push(DissectionNode::new(
588                    "bgp.attr.cluster_list.entry",
589                    format!("Cluster ID [{idx}]: {id}"),
590                    base + pos as u32,
591                    4,
592                ));
593                pos += 4;
594                idx += 1;
595            }
596        }
597        14 => {
598            // MP_REACH_NLRI: AFI(2) SAFI(1) NH-len(1) NH(var) reserved(1) NLRI
599            if value.len() >= 4 {
600                let afi = read_u16(value, 0).unwrap_or(0);
601                let afi_name = match afi {
602                    1 => "IPv4",
603                    2 => "IPv6",
604                    25 => "L2VPN",
605                    _ => "unknown",
606                };
607                nodes.push(DissectionNode::new(
608                    "bgp.attr.mp_reach.afi",
609                    format!("AFI: {afi} ({afi_name})"),
610                    base,
611                    2,
612                ));
613                nodes.push(DissectionNode::new(
614                    "bgp.attr.mp_reach.safi",
615                    format!("SAFI: {}", value[2]),
616                    base + 2,
617                    1,
618                ));
619                let nh_len = value[3] as usize;
620                nodes.push(DissectionNode::new(
621                    "bgp.attr.mp_reach.next_hop_length",
622                    format!("Next hop length: {nh_len}"),
623                    base + 3,
624                    1,
625                ));
626                let mut pos = 4usize;
627                if nh_len > 0 && pos + nh_len > value.len() {
628                    // Truncated next hop: the reserved byte and the NLRI
629                    // cannot be located reliably, so stop walking here.
630                    nodes.push(DissectionNode::new(
631                        "bgp.attr.mp_reach.next_hop",
632                        format!(
633                            "Next hop (truncated: {} of {} bytes)",
634                            value.len() - pos,
635                            nh_len
636                        ),
637                        base + pos as u32,
638                        (value.len() - pos) as u32,
639                    ));
640                    return nodes;
641                }
642                if nh_len > 0 {
643                    nodes.push(DissectionNode::new(
644                        "bgp.attr.mp_reach.next_hop",
645                        format!("Next hop ({} bytes)", nh_len),
646                        base + pos as u32,
647                        nh_len as u32,
648                    ));
649                    pos += nh_len;
650                }
651                if pos < value.len() {
652                    nodes.push(DissectionNode::new(
653                        "bgp.attr.mp_reach.reserved",
654                        "Reserved",
655                        base + pos as u32,
656                        1,
657                    ));
658                    pos += 1;
659                }
660                if pos < value.len() {
661                    nodes.extend(dissect_nlri(
662                        &value[pos..],
663                        base + pos as u32,
664                        afi == 1,
665                        add_path,
666                    ));
667                }
668            }
669        }
670        15 => {
671            // MP_UNREACH_NLRI: AFI(2) SAFI(1) withdrawn NLRI
672            if value.len() >= 3 {
673                let afi = read_u16(value, 0).unwrap_or(0);
674                nodes.push(DissectionNode::new(
675                    "bgp.attr.mp_unreach.afi",
676                    format!("AFI: {afi}"),
677                    base,
678                    2,
679                ));
680                nodes.push(DissectionNode::new(
681                    "bgp.attr.mp_unreach.safi",
682                    format!("SAFI: {}", value[2]),
683                    base + 2,
684                    1,
685                ));
686                nodes.extend(dissect_nlri(&value[3..], base + 3, afi == 1, add_path));
687            }
688        }
689        16 => {
690            // EXTENDED_COMMUNITIES: 8-byte entries
691            let mut pos = 0usize;
692            let mut idx = 1;
693            while pos + 8 <= value.len() {
694                nodes.push(DissectionNode::new(
695                    "bgp.attr.ext_communities.entry",
696                    format!(
697                        "Extended community [{idx}]: {}",
698                        render_ext_community(&value[pos..pos + 8])
699                    ),
700                    base + pos as u32,
701                    8,
702                ));
703                pos += 8;
704                idx += 1;
705            }
706        }
707        25 => {
708            // IPV6_ADDRESS_SPECIFIC_EXTENDED_COMMUNITIES: 20-byte entries
709            let mut pos = 0usize;
710            let mut idx = 1;
711            while pos + 20 <= value.len() {
712                nodes.push(DissectionNode::new(
713                    "bgp.attr.ipv6_ext_communities.entry",
714                    format!("IPv6 extended community [{idx}]"),
715                    base + pos as u32,
716                    20,
717                ));
718                pos += 20;
719                idx += 1;
720            }
721        }
722        26 => {
723            // AIGP: TLV of type(1) + length(2, includes header) + value
724            let mut pos = 0usize;
725            while pos + 3 <= value.len() {
726                let tlv_type = value[pos];
727                // RFC 7311: type(1) + length(2, includes the 3-byte header)
728                let tlv_len = read_u16(value, pos + 1).unwrap_or(0) as usize;
729                if tlv_len < 3 || pos + tlv_len > value.len() {
730                    break;
731                }
732                nodes.push(DissectionNode::new(
733                    "bgp.attr.aigp.entry",
734                    format!("AIGP TLV: type {tlv_type}, {tlv_len} bytes"),
735                    base + pos as u32,
736                    tlv_len as u32,
737                ));
738                pos += tlv_len;
739            }
740        }
741        32 => {
742            // LARGE_COMMUNITY: 12-byte entries
743            let mut pos = 0usize;
744            let mut idx = 1;
745            while pos + 12 <= value.len() {
746                let ga = read_u32(value, pos).unwrap_or(0);
747                let l1 = read_u32(value, pos + 4).unwrap_or(0);
748                let l2 = read_u32(value, pos + 8).unwrap_or(0);
749                nodes.push(DissectionNode::new(
750                    "bgp.attr.large_communities.entry",
751                    format!("Large community [{idx}]: {ga}:{l1}:{l2}"),
752                    base + pos as u32,
753                    12,
754                ));
755                pos += 12;
756                idx += 1;
757            }
758        }
759        _ => {}
760    }
761    nodes
762}
763
764fn render_ext_community(entry: &[u8]) -> String {
765    let type_high = entry[0];
766    let subtype = entry[1];
767    let value = &entry[2..8];
768    match type_high & 0x0F {
769        0x00 => {
770            let asn = u16::from_be_bytes([value[0], value[1]]);
771            let val = u32::from_be_bytes([value[2], value[3], value[4], value[5]]);
772            format!("type 0x{type_high:02X} subtype 0x{subtype:02X}: {asn}:{val}")
773        }
774        0x01 => {
775            let ip = Ipv4Addr::new(value[0], value[1], value[2], value[3]);
776            let val = u16::from_be_bytes([value[4], value[5]]);
777            format!("type 0x{type_high:02X} subtype 0x{subtype:02X}: {ip}:{val}")
778        }
779        0x02 => {
780            let asn = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
781            let val = u16::from_be_bytes([value[4], value[5]]);
782            format!("type 0x{type_high:02X} subtype 0x{subtype:02X}: {asn}:{val}")
783        }
784        _ => {
785            let hex: Vec<String> = value.iter().map(|b| format!("{b:02X}")).collect();
786            format!(
787                "type 0x{type_high:02X} subtype 0x{subtype:02X}: {}",
788                hex.join(" ")
789            )
790        }
791    }
792}
793
794/// Walk an NLRI section of variable-length prefixes (1 length byte +
795/// ceil(len/8) address octets), with optional ADD-PATH path identifiers.
796fn dissect_nlri(data: &[u8], base: u32, afi_v4: bool, add_path: bool) -> Vec<DissectionNode> {
797    let mut nodes = Vec::new();
798    let mut pos = 0usize;
799    while pos < data.len() {
800        let entry_start = pos;
801        if add_path {
802            if pos + 5 > data.len() {
803                break;
804            }
805            pos += 4; // path identifier
806        }
807        let plen = match data.get(pos) {
808            Some(v) => *v,
809            None => break,
810        };
811        let octets = (plen as usize).div_ceil(8);
812        pos += 1;
813        if pos + octets > data.len() {
814            break;
815        }
816        let label = render_prefix(afi_v4, plen, &data[pos..pos + octets]);
817        nodes.push(DissectionNode::new(
818            "bgp.nlri.prefix",
819            label,
820            base + entry_start as u32,
821            (pos + octets - entry_start) as u32,
822        ));
823        pos += octets;
824    }
825    nodes
826}
827
828fn dissect_open(data: &[u8], base: u32) -> DissectionNode {
829    let mut open = DissectionNode::new("bgp.open", "OPEN", base, data.len() as u32);
830    if data.len() < 10 {
831        if !data.is_empty() {
832            open.children.push(DissectionNode::new(
833                "bgp.open.truncated",
834                format!("Truncated OPEN ({} of 10 fixed bytes)", data.len()),
835                base,
836                data.len() as u32,
837            ));
838        }
839        return open;
840    }
841
842    open.children.push(DissectionNode::new(
843        "bgp.open.version",
844        format!("Version: {}", data[0]),
845        base,
846        1,
847    ));
848    let asn = u16::from_be_bytes([data[1], data[2]]);
849    open.children.push(DissectionNode::new(
850        "bgp.open.asn",
851        format!("My AS: {asn}"),
852        base + 1,
853        2,
854    ));
855    let hold = u16::from_be_bytes([data[3], data[4]]);
856    open.children.push(DissectionNode::new(
857        "bgp.open.hold_time",
858        format!("Hold time: {hold}s"),
859        base + 3,
860        2,
861    ));
862    open.children.push(DissectionNode::new(
863        "bgp.open.bgp_identifier",
864        format!(
865            "BGP identifier: {}.{}.{}.{}",
866            data[5], data[6], data[7], data[8]
867        ),
868        base + 5,
869        4,
870    ));
871    let opt_len = data[9];
872    open.children.push(DissectionNode::new(
873        "bgp.open.opt_params_len",
874        format!("Optional parameters length: {opt_len}"),
875        base + 9,
876        1,
877    ));
878
879    // Optional parameters; RFC 9072 extended framing starts with type 255
880    // followed by a 2-byte aggregate length and 2-byte per-param lengths.
881    let mut pos = 10usize;
882    let mut extended = false;
883    let mut first = true;
884    while pos + 2 <= data.len() {
885        let param_type = data[pos];
886        if first && param_type == 255 && opt_len != 0 {
887            extended = true;
888            if pos + 3 > data.len() {
889                break;
890            }
891            let ext_len = read_u16(data, pos + 1).unwrap_or(0);
892            open.children.push(DissectionNode::new(
893                "bgp.open.ext_params_len",
894                format!("Extended optional parameters length: {ext_len} (RFC 9072)"),
895                base + pos as u32,
896                3,
897            ));
898            pos += 3;
899            first = false;
900            if ext_len == 0 {
901                break;
902            }
903            continue;
904        }
905        first = false;
906
907        let len_size = if extended { 2 } else { 1 };
908        let value_len = if extended {
909            match read_u16(data, pos + 1) {
910                Some(v) => v as usize,
911                None => break,
912            }
913        } else {
914            *data.get(pos + 1).unwrap_or(&0) as usize
915        };
916        let header_len = 1 + len_size;
917        if pos + header_len + value_len > data.len() {
918            break;
919        }
920
921        let mut param = DissectionNode::new(
922            "bgp.open.param",
923            if param_type == 2 {
924                "Capabilities (RFC 3392)".to_string()
925            } else {
926                format!("Optional parameter (type {param_type})")
927            },
928            base + pos as u32,
929            (header_len + value_len) as u32,
930        );
931        param.children.push(DissectionNode::new(
932            "bgp.open.param.type",
933            format!("Type: {param_type}"),
934            base + pos as u32,
935            1,
936        ));
937        param.children.push(DissectionNode::new(
938            "bgp.open.param.length",
939            format!("Length: {value_len}"),
940            base + pos as u32 + 1,
941            len_size as u32,
942        ));
943
944        let value = &data[pos + header_len..pos + header_len + value_len];
945        let value_base = base + (pos + header_len) as u32;
946        if param_type == 2 {
947            // Capabilities: code(1) + length(1) + value
948            let mut cpos = 0usize;
949            while cpos + 2 <= value.len() {
950                let cap_code = value[cpos];
951                let cap_len = value[cpos + 1] as usize;
952                if cpos + 2 + cap_len > value.len() {
953                    break;
954                }
955                let mut cap = DissectionNode::new(
956                    "bgp.open.capability",
957                    format!("Capability: {} ({cap_code})", capability_name(cap_code)),
958                    value_base + cpos as u32,
959                    (2 + cap_len) as u32,
960                );
961                cap.children.push(DissectionNode::new(
962                    "bgp.open.capability.code",
963                    format!("Code: {cap_code} ({})", capability_name(cap_code)),
964                    value_base + cpos as u32,
965                    1,
966                ));
967                cap.children.push(DissectionNode::new(
968                    "bgp.open.capability.length",
969                    format!("Length: {cap_len}"),
970                    value_base + cpos as u32 + 1,
971                    1,
972                ));
973                param.children.push(cap);
974                cpos += 2 + cap_len;
975            }
976        } else {
977            param.children.push(DissectionNode::new(
978                "bgp.open.param.value",
979                format!("Value ({value_len} bytes)"),
980                value_base,
981                value_len as u32,
982            ));
983        }
984
985        open.children.push(param);
986        pos += header_len + value_len;
987    }
988
989    open
990}
991
992fn capability_name(code: u8) -> &'static str {
993    match code {
994        1 => "Multiprotocol Extensions",
995        2 => "Route Refresh",
996        4 => "Multiple Routes to a Destination",
997        5 => "Extended Next Hop Encoding",
998        6 => "BGP Extended Message",
999        7 => "BGP Role",
1000        64 => "Graceful Restart",
1001        65 => "Support for 4-octet AS Number",
1002        66 => "Support for Add-Path",
1003        67 => "Enhanced Route Refresh",
1004        68 => "Long-Lived Graceful Restart",
1005        _ => "Unknown",
1006    }
1007}
1008
1009fn dissect_notification(data: &[u8], base: u32) -> DissectionNode {
1010    let mut notification =
1011        DissectionNode::new("bgp.notification", "NOTIFICATION", base, data.len() as u32);
1012    if let Some(code) = data.first() {
1013        notification.children.push(DissectionNode::new(
1014            "bgp.notification.code",
1015            format!("Error code: {code}"),
1016            base,
1017            1,
1018        ));
1019    }
1020    if data.len() >= 2 {
1021        notification.children.push(DissectionNode::new(
1022            "bgp.notification.subcode",
1023            format!("Error subcode: {}", data[1]),
1024            base + 1,
1025            1,
1026        ));
1027    }
1028    if data.len() > 2 {
1029        notification.children.push(DissectionNode::new(
1030            "bgp.notification.data",
1031            format!("Data ({} bytes)", data.len() - 2),
1032            base + 2,
1033            (data.len() - 2) as u32,
1034        ));
1035    }
1036    notification
1037}
1038
1039fn dissect_route_refresh(data: &[u8], base: u32) -> DissectionNode {
1040    let mut refresh = DissectionNode::new(
1041        "bgp.route_refresh",
1042        "ROUTE-REFRESH",
1043        base,
1044        data.len() as u32,
1045    );
1046    if data.len() >= 4 {
1047        let afi = read_u16(data, 0).unwrap_or(0);
1048        refresh.children.push(DissectionNode::new(
1049            "bgp.route_refresh.afi",
1050            format!("AFI: {afi}"),
1051            base,
1052            2,
1053        ));
1054        refresh.children.push(DissectionNode::new(
1055            "bgp.route_refresh.subtype",
1056            format!("Subtype: {}", data[2]),
1057            base + 2,
1058            1,
1059        ));
1060        refresh.children.push(DissectionNode::new(
1061            "bgp.route_refresh.safi",
1062            format!("SAFI: {}", data[3]),
1063            base + 3,
1064            1,
1065        ));
1066    }
1067    if data.len() > 4 {
1068        refresh.children.push(DissectionNode::new(
1069            "bgp.route_refresh.data",
1070            format!("ORF data ({} bytes)", data.len() - 4),
1071            base + 4,
1072            (data.len() - 4) as u32,
1073        ));
1074    }
1075    refresh
1076}
1077
1078#[cfg(test)]
1079mod tests {
1080    use super::*;
1081
1082    /// marker(16) + length(2) + type(1) + body
1083    fn bgp_wire(msg_type: u8, body: &[u8]) -> Vec<u8> {
1084        let mut wire = vec![0xFF; 16];
1085        let total = (19 + body.len()) as u16;
1086        wire.extend_from_slice(&total.to_be_bytes());
1087        wire.push(msg_type);
1088        wire.extend_from_slice(body);
1089        wire
1090    }
1091
1092    /// ORIGIN + AS_PATH (2 ASNs, 4-octet) + NEXT_HOP + COMMUNITIES (2)
1093    /// + LARGE_COMMUNITY (1), announced prefix 203.0.113.0/24.
1094    fn sample_update_body() -> Vec<u8> {
1095        let mut body = Vec::new();
1096        // withdrawn routes: one prefix 192.0.2.0/24
1097        let withdrawn: [u8; 4] = [24, 192, 0, 2];
1098        body.extend_from_slice(&(withdrawn.len() as u16).to_be_bytes());
1099        body.extend_from_slice(&withdrawn);
1100
1101        let mut attrs = Vec::new();
1102        attrs.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]); // ORIGIN = IGP
1103                                                            // AS_PATH: AS_SEQUENCE with 2 4-octet ASNs
1104        attrs.extend_from_slice(&[0x40, 0x02, 0x0A, 0x02, 0x02]);
1105        attrs.extend_from_slice(&65001u32.to_be_bytes());
1106        attrs.extend_from_slice(&65002u32.to_be_bytes());
1107        // NEXT_HOP
1108        attrs.extend_from_slice(&[0x40, 0x03, 0x04, 192, 0, 2, 254]);
1109        // COMMUNITIES: 64512:100, 64512:200
1110        let mut comms = Vec::new();
1111        comms.extend_from_slice(&64512u16.to_be_bytes());
1112        comms.extend_from_slice(&100u16.to_be_bytes());
1113        comms.extend_from_slice(&64512u16.to_be_bytes());
1114        comms.extend_from_slice(&200u16.to_be_bytes());
1115        attrs.push(0xC0);
1116        attrs.push(0x08);
1117        attrs.push(comms.len() as u8);
1118        attrs.extend_from_slice(&comms);
1119        // LARGE_COMMUNITY: 64496:1:2
1120        attrs.push(0xC0);
1121        attrs.push(0x20);
1122        attrs.push(12);
1123        attrs.extend_from_slice(&64496u32.to_be_bytes());
1124        attrs.extend_from_slice(&1u32.to_be_bytes());
1125        attrs.extend_from_slice(&2u32.to_be_bytes());
1126
1127        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1128        body.extend_from_slice(&attrs);
1129        // NLRI: 203.0.113.0/24
1130        body.extend_from_slice(&[24, 203, 0, 113]);
1131        body
1132    }
1133
1134    fn find_node<'a>(node: &'a DissectionNode, field: &str) -> &'a DissectionNode {
1135        node.find(field)
1136            .unwrap_or_else(|| panic!("node {field} not found in tree"))
1137    }
1138
1139    #[test]
1140    fn dissect_update_field_offsets() {
1141        let wire = bgp_wire(2, &sample_update_body());
1142        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1143
1144        assert_eq!(tree.field, "bgp");
1145        assert_eq!(tree.offset, 0);
1146        assert_eq!(tree.length, wire.len() as u32);
1147
1148        let marker = find_node(&tree, "bgp.header.marker");
1149        assert_eq!((marker.offset, marker.length), (0, 16));
1150        let length = find_node(&tree, "bgp.header.length");
1151        assert_eq!((length.offset, length.length), (16, 2));
1152        assert_eq!(length.label, format!("Length: {}", wire.len()));
1153        let msg_type = find_node(&tree, "bgp.header.type");
1154        assert_eq!((msg_type.offset, msg_type.length), (18, 1));
1155
1156        // withdrawn length at 19, prefixes at 21
1157        let wlen = find_node(&tree, "bgp.update.withdrawn_routes.length");
1158        assert_eq!((wlen.offset, wlen.length), (19, 2));
1159        assert_eq!(wlen.label, "Withdrawn routes length: 4");
1160        let wr = find_node(&tree, "bgp.update.withdrawn_routes");
1161        assert_eq!((wr.offset, wr.length), (21, 4));
1162        let wprefix = find_node(&tree, "bgp.nlri.prefix");
1163        assert_eq!((wprefix.offset, wprefix.length), (21, 4));
1164        assert_eq!(wprefix.label, "192.0.2.0/24");
1165
1166        // attr section
1167        let alen = find_node(&tree, "bgp.update.path_attributes.length");
1168        assert_eq!((alen.offset, alen.length), (25, 2));
1169        let attrs = find_node(&tree, "bgp.update.path_attributes");
1170        assert_eq!(attrs.offset, 27);
1171
1172        // AS_PATH attr: 4 header + 10 value... actually 3 header + 10 value
1173        let as_path = find_node(&tree, "bgp.attr.2");
1174        assert_eq!((as_path.offset, as_path.length), (31, 13));
1175        let segment = find_node(&tree, "bgp.attr.as_path.segment");
1176        assert_eq!((segment.offset, segment.length), (34, 10));
1177        assert_eq!(segment.label, "AS_SEQUENCE: 65001 65002");
1178
1179        // COMMUNITIES attr: header 3 + 8 value
1180        let comms = find_node(&tree, "bgp.attr.8");
1181        assert_eq!((comms.offset, comms.length), (51, 11));
1182        let mut entries = Vec::new();
1183        tree.find_all("bgp.attr.communities.entry", &mut entries);
1184        assert_eq!(entries.len(), 2);
1185        assert_eq!((entries[0].offset, entries[0].length), (54, 4));
1186        assert_eq!(entries[0].label, "Community [1]: 64512:100");
1187        assert_eq!(entries[1].label, "Community [2]: 64512:200");
1188
1189        // LARGE_COMMUNITY attr
1190        let large = find_node(&tree, "bgp.attr.32");
1191        assert_eq!((large.offset, large.length), (62, 15));
1192        let entry = find_node(&tree, "bgp.attr.large_communities.entry");
1193        assert_eq!(entry.label, "Large community [1]: 64496:1:2");
1194
1195        // NLRI
1196        let nlri = find_node(&tree, "bgp.update.nlri");
1197        assert_eq!(nlri.offset, 77);
1198        assert_eq!(nlri.length, 4);
1199    }
1200
1201    #[test]
1202    fn dissect_update_mp_reach() {
1203        // MP_REACH_NLRI for IPv6: afi=2, safi=1, nh_len=16, nh, reserved, prefix
1204        let mut value = Vec::new();
1205        value.extend_from_slice(&2u16.to_be_bytes()); // AFI
1206        value.push(1); // SAFI
1207        value.push(16); // next hop length
1208        value.extend_from_slice(&[
1209            0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01,
1210        ]);
1211        value.push(0); // reserved
1212        value.extend_from_slice(&[32, 0x20, 0x01, 0x0d, 0xb8]); // 2001:db8::/32
1213
1214        let mut attrs = Vec::new();
1215        attrs.push(0x80);
1216        attrs.push(14);
1217        attrs.push(value.len() as u8);
1218        attrs.extend_from_slice(&value);
1219
1220        let mut body = Vec::new();
1221        body.extend_from_slice(&0u16.to_be_bytes()); // no withdrawn
1222        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1223        body.extend_from_slice(&attrs);
1224
1225        let wire = bgp_wire(2, &body);
1226        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1227
1228        let mp_reach = find_node(&tree, "bgp.attr.14");
1229        let afi = find_node(&tree, "bgp.attr.mp_reach.afi");
1230        assert_eq!(afi.label, "AFI: 2 (IPv6)");
1231        let nh = find_node(&tree, "bgp.attr.mp_reach.next_hop");
1232        assert_eq!(nh.length, 16);
1233        let prefix = tree.find("bgp.update.nlri");
1234        assert!(prefix.is_none(), "IPv6 NLRI lives inside MP_REACH");
1235        let mut prefixes = Vec::new();
1236        tree.find_all("bgp.nlri.prefix", &mut prefixes);
1237        assert_eq!(prefixes.len(), 1);
1238        assert_eq!(prefixes[0].label, "2001:db8::/32");
1239        assert_eq!(mp_reach.length, 3 + value.len() as u32);
1240    }
1241
1242    #[test]
1243    fn dissect_update_truncated_attribute_is_partial() {
1244        // AS_PATH attribute declaring more value than present
1245        let mut body = Vec::new();
1246        body.extend_from_slice(&0u16.to_be_bytes());
1247        let attrs = [0x40u8, 0x02, 0x0A, 0x02, 0x02, 0x0F, 0x9A]; // declares 10, has 4
1248        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1249        body.extend_from_slice(&attrs);
1250
1251        let wire = bgp_wire(2, &body);
1252        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1253
1254        let as_path = find_node(&tree, "bgp.attr.2");
1255        assert!(as_path.label.contains("truncated"));
1256        assert_eq!(as_path.length, attrs.len() as u32);
1257    }
1258
1259    #[test]
1260    fn dissect_open_capabilities() {
1261        // version 4, as 65001, hold 180, id 1.2.3.4, one capability param
1262        // containing MP Extensions (code 1) + 4-octet AS (code 65)
1263        let mut caps = Vec::new();
1264        caps.extend_from_slice(&[1, 4, 0, 1, 0, 1]); // afi 1 safi 1
1265        caps.extend_from_slice(&[65, 4, 0, 0, 0xFD, 0xE9]); // asn 65001
1266
1267        let mut body = Vec::new();
1268        body.push(4);
1269        body.extend_from_slice(&65001u16.to_be_bytes());
1270        body.extend_from_slice(&180u16.to_be_bytes());
1271        body.extend_from_slice(&[1, 2, 3, 4]);
1272        body.push(2 + caps.len() as u8); // opt params len
1273        body.push(2); // param type: capabilities
1274        body.push(caps.len() as u8);
1275        body.extend_from_slice(&caps);
1276
1277        let wire = bgp_wire(1, &body);
1278        let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
1279
1280        let version = find_node(&tree, "bgp.open.version");
1281        assert_eq!((version.offset, version.length), (19, 1));
1282        let asn = find_node(&tree, "bgp.open.asn");
1283        assert_eq!(asn.label, "My AS: 65001");
1284        let mut capabilities = Vec::new();
1285        tree.find_all("bgp.open.capability", &mut capabilities);
1286        assert_eq!(capabilities.len(), 2);
1287        assert_eq!(
1288            capabilities[0].label,
1289            "Capability: Multiprotocol Extensions (1)"
1290        );
1291        assert_eq!(
1292            capabilities[1].label,
1293            "Capability: Support for 4-octet AS Number (65)"
1294        );
1295        assert_eq!(capabilities[0].offset, 19 + 10 + 2);
1296    }
1297
1298    #[test]
1299    fn dissect_notification_and_route_refresh() {
1300        let notification = bgp_wire(3, &[6, 5, 0xDE, 0xAD]);
1301        let tree = dissect_bgp_message(&notification, &AsnLength::Bits16, false);
1302        let code = find_node(&tree, "bgp.notification.code");
1303        assert_eq!((code.offset, code.length), (19, 1));
1304        let data = find_node(&tree, "bgp.notification.data");
1305        assert_eq!(data.length, 2);
1306
1307        let refresh = bgp_wire(5, &[0, 1, 0, 1, 0xDE, 0xAD, 0xBE]);
1308        let tree = dissect_bgp_message(&refresh, &AsnLength::Bits16, false);
1309        let subtype = find_node(&tree, "bgp.route_refresh.subtype");
1310        assert_eq!(subtype.label, "Subtype: 0");
1311        let orf = find_node(&tree, "bgp.route_refresh.data");
1312        assert_eq!(orf.length, 3);
1313    }
1314
1315    #[test]
1316    fn dissect_keepalive_and_truncated_header() {
1317        let keepalive = bgp_wire(4, &[]);
1318        let tree = dissect_bgp_message(&keepalive, &AsnLength::Bits16, false);
1319        assert_eq!(tree.children.len(), 1, "header only, no body node");
1320        assert!(tree.find("bgp.header.type").is_some());
1321
1322        let truncated = dissect_bgp_message(&[0xFF; 7], &AsnLength::Bits16, false);
1323        let header = find_node(&truncated, "bgp.header");
1324        assert!(header.label.contains("truncated"));
1325    }
1326
1327    #[test]
1328    fn dissect_attribute_value_zoo() {
1329        // One UPDATE exercising every attribute-value walker not covered by
1330        // the offset test: aggregator, originator ID, cluster list, MED,
1331        // local pref, OTC, ext communities, IPv6 ext communities, AIGP, and
1332        // an unknown attribute type.
1333        let mut attrs = Vec::new();
1334        let push_attr = |attrs: &mut Vec<u8>, flags: u8, code: u8, value: &[u8]| {
1335            attrs.push(flags);
1336            attrs.push(code);
1337            attrs.push(value.len() as u8);
1338            attrs.extend_from_slice(value);
1339        };
1340
1341        // ORIGIN = EGP
1342        push_attr(&mut attrs, 0x40, 1, &[0x01]);
1343        // NEXT_HOP
1344        push_attr(&mut attrs, 0x40, 3, &[10, 0, 0, 1]);
1345        // MED
1346        push_attr(&mut attrs, 0x80, 4, &100u32.to_be_bytes());
1347        // LOCAL_PREF
1348        push_attr(&mut attrs, 0x40, 5, &200u32.to_be_bytes());
1349        // ATOMIC_AGGREGATE (empty value)
1350        push_attr(&mut attrs, 0x40, 6, &[]);
1351        // AGGREGATOR: 2-octet ASN + router id
1352        push_attr(&mut attrs, 0xC0, 7, &[0xFC, 0x80, 1, 2, 3, 4]);
1353        // ORIGINATOR_ID
1354        push_attr(&mut attrs, 0x80, 9, &[10, 0, 0, 9]);
1355        // CLUSTER_LIST: two IDs
1356        push_attr(&mut attrs, 0x80, 10, &0xAABBCCDDu32.to_be_bytes());
1357        // EXTENDED_COMMUNITIES: two-octet-AS route target + opaque
1358        push_attr(
1359            &mut attrs,
1360            0xC0,
1361            16,
1362            &[0x00, 0x02, 0xFC, 0x80, 0, 0, 0, 100],
1363        );
1364        push_attr(&mut attrs, 0xC0, 16, &[0x03, 0x04, 0, 0, 0, 0, 0, 1]);
1365        // AS4_AGGREGATOR: 4-octet ASN + router id
1366        let as4_aggregator: Vec<u8> =
1367            [4200000000u32.to_be_bytes().to_vec(), vec![1, 2, 3, 4]].concat();
1368        push_attr(&mut attrs, 0xC0, 18, &as4_aggregator);
1369        // IPV6 ext community (20 bytes)
1370        push_attr(&mut attrs, 0xC0, 25, &[0x00; 20]);
1371        // AIGP: one metric TLV, type 1, length 11 (3 header + 8 metric)
1372        push_attr(
1373            &mut attrs,
1374            0x80,
1375            26,
1376            &[0x01, 0x00, 0x0B, 0, 0, 0, 0, 0, 0, 0, 100],
1377        );
1378        // OTC
1379        push_attr(&mut attrs, 0xC0, 35, &64512u32.to_be_bytes());
1380        // unknown attribute type 99
1381        push_attr(&mut attrs, 0x80, 99, &[0xAB, 0xCD]);
1382
1383        let mut body = Vec::new();
1384        body.extend_from_slice(&0u16.to_be_bytes());
1385        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1386        body.extend_from_slice(&attrs);
1387        let wire = bgp_wire(2, &body);
1388        let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
1389
1390        assert_eq!(find_node(&tree, "bgp.attr.origin").label, "Origin: EGP (1)");
1391        assert_eq!(
1392            find_node(&tree, "bgp.attr.next_hop").label,
1393            "Next hop: 10.0.0.1"
1394        );
1395        assert_eq!(
1396            find_node(&tree, "bgp.attr.u32").label,
1397            "Multi-exit discriminator: 100"
1398        );
1399        assert_eq!(
1400            find_node(&tree, "bgp.attr.aggregator.asn").label,
1401            "Aggregator ASN: 64640"
1402        );
1403        assert_eq!(
1404            find_node(&tree, "bgp.attr.aggregator.id").label,
1405            "Aggregator router ID: 1.2.3.4"
1406        );
1407        assert_eq!(
1408            find_node(&tree, "bgp.attr.originator_id").label,
1409            "Originator ID: 10.0.0.9"
1410        );
1411        let cluster = find_node(&tree, "bgp.attr.cluster_list.entry");
1412        assert!(cluster.label.contains("2864434397"));
1413        let mut ext = Vec::new();
1414        tree.find_all("bgp.attr.ext_communities.entry", &mut ext);
1415        assert_eq!(ext.len(), 2);
1416        assert_eq!(
1417            ext[0].label,
1418            "Extended community [1]: type 0x00 subtype 0x02: 64640:100"
1419        );
1420        assert!(ext[1].label.contains("type 0x03 subtype 0x04"));
1421        assert_eq!(
1422            find_node(&tree, "bgp.attr.ipv6_ext_communities.entry").label,
1423            "IPv6 extended community [1]"
1424        );
1425        // AIGP metric TLV: type 1, 11 bytes total (regression: length read
1426        // at the byte right after the type)
1427        let aigp = find_node(&tree, "bgp.attr.aigp.entry");
1428        assert_eq!(aigp.label, "AIGP TLV: type 1, 11 bytes");
1429        assert_eq!(
1430            (aigp.offset, aigp.length),
1431            (find_node(&tree, "bgp.attr.26").offset + 3, 11)
1432        );
1433        // OTC renders through the u32 walker
1434        let mut u32s = Vec::new();
1435        tree.find_all("bgp.attr.u32", &mut u32s);
1436        assert!(u32s.iter().any(|n| n.label == "Only to customer: 64512"));
1437        // unknown type keeps the generic name
1438        assert!(find_node(&tree, "bgp.attr.99")
1439            .label
1440            .starts_with("ATTRIBUTE (type 99)"));
1441    }
1442
1443    #[test]
1444    fn dissect_as4_path_uses_four_octet_asns_even_on_16bit_sessions() {
1445        // AS4_PATH (type 17) with 4-octet ASNs, dissected with Bits16:
1446        // segments must still be walked with 4-octet width (RFC 6793).
1447        let value: Vec<u8> = [
1448            &[0x02, 0x02][..],
1449            &65001u32.to_be_bytes(),
1450            &65002u32.to_be_bytes(),
1451        ]
1452        .concat();
1453        let mut attrs = Vec::new();
1454        attrs.extend_from_slice(&[0x40, 17, value.len() as u8]);
1455        attrs.extend_from_slice(&value);
1456
1457        let mut body = Vec::new();
1458        body.extend_from_slice(&0u16.to_be_bytes());
1459        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1460        body.extend_from_slice(&attrs);
1461        let wire = bgp_wire(2, &body);
1462        let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
1463
1464        let segment = find_node(&tree, "bgp.attr.as_path.segment");
1465        assert_eq!((segment.offset, segment.length), (23 + 3, 10));
1466        assert_eq!(segment.label, "AS_SEQUENCE: 65001 65002");
1467    }
1468
1469    #[test]
1470    fn dissect_extended_length_attribute() {
1471        // Flags with the extended-length bit (0x10) carry a 2-byte length.
1472        let mut value = vec![0u8; 300];
1473        value[0] = 1; // valid ORIGIN payload size irrelevant; generic walk
1474        let mut attrs = Vec::new();
1475        attrs.push(0x50); // optional + transitive + extended length
1476        attrs.push(99);
1477        attrs.extend_from_slice(&(value.len() as u16).to_be_bytes());
1478        attrs.extend_from_slice(&value);
1479
1480        let mut body = Vec::new();
1481        body.extend_from_slice(&0u16.to_be_bytes());
1482        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1483        body.extend_from_slice(&attrs);
1484        let wire = bgp_wire(2, &body);
1485        let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
1486
1487        let attr = find_node(&tree, "bgp.attr.99");
1488        assert_eq!(attr.length, 4 + 300);
1489        let length = find_node(&tree, "bgp.attr.length");
1490        assert_eq!(length.label, "Length: 300");
1491        assert_eq!(length.length, 2);
1492    }
1493
1494    #[test]
1495    fn dissect_declared_length_bounds_body() {
1496        // KEEPALIVE followed by 5 garbage bytes: the garbage must surface as
1497        // a trailing node, not as message fields.
1498        let mut wire = bgp_wire(4, &[]);
1499        wire.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
1500        let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
1501
1502        assert_eq!(tree.length, 19, "root bounded to the declared message");
1503        let trailing = find_node(&tree, "bgp.trailing");
1504        assert_eq!((trailing.offset, trailing.length), (19, 5));
1505
1506        // Declared UPDATE length shorter than the buffer: extra NLRI bytes
1507        // beyond the declaration land in trailing, not as fake prefixes.
1508        let body = sample_update_body();
1509        let mut wire = bgp_wire(2, &body);
1510        // shrink the declared length by the 4-byte NLRI
1511        let full_len = u16::from_be_bytes([wire[16], wire[17]]);
1512        let shorter = full_len - 4;
1513        wire[16] = (shorter >> 8) as u8;
1514        wire[17] = (shorter & 0xFF) as u8;
1515        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1516        assert!(tree.find("bgp.update.nlri").is_none());
1517        assert_eq!(find_node(&tree, "bgp.trailing").length, 4);
1518
1519        // Declaration beyond the available bytes: explicit truncated note.
1520        let mut wire = bgp_wire(2, &body);
1521        wire.truncate(wire.len() - 10);
1522        let inflated = wire.len() as u16 + 20;
1523        wire[16] = (inflated >> 8) as u8;
1524        wire[17] = (inflated & 0xFF) as u8;
1525        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1526        let truncated = find_node(&tree, "bgp.truncated");
1527        assert!(truncated.label.contains("exceeds"));
1528    }
1529
1530    #[test]
1531    fn dissect_mp_unreach_and_truncated_next_hop() {
1532        // MP_UNREACH: AFI/SAFI + withdrawn prefixes
1533        let mut value = Vec::new();
1534        value.extend_from_slice(&2u16.to_be_bytes()); // AFI IPv6
1535        value.push(1); // SAFI
1536        value.extend_from_slice(&[32, 0x20, 0x01, 0x0d, 0xb8]); // 2001:db8::/32
1537        let mut attrs = Vec::new();
1538        attrs.extend_from_slice(&[0x80, 15, value.len() as u8]);
1539        attrs.extend_from_slice(&value);
1540        let mut body = Vec::new();
1541        body.extend_from_slice(&0u16.to_be_bytes());
1542        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1543        body.extend_from_slice(&attrs);
1544        let wire = bgp_wire(2, &body);
1545        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1546
1547        assert!(tree.find("bgp.attr.mp_unreach.afi").is_some());
1548        let mut prefixes = Vec::new();
1549        tree.find_all("bgp.nlri.prefix", &mut prefixes);
1550        assert_eq!(prefixes[0].label, "2001:db8::/32");
1551
1552        // MP_REACH declaring a next hop longer than the value: the walk must
1553        // stop at the truncated next-hop node and not fabricate NLRI.
1554        let mut value = Vec::new();
1555        value.extend_from_slice(&2u16.to_be_bytes());
1556        value.push(1);
1557        value.push(16); // next hop length: 16...
1558        value.extend_from_slice(&[0x20, 0x01]); // ...but only 2 bytes present
1559        let mut attrs = Vec::new();
1560        attrs.extend_from_slice(&[0x80, 14, value.len() as u8]);
1561        attrs.extend_from_slice(&value);
1562        let mut body = Vec::new();
1563        body.extend_from_slice(&0u16.to_be_bytes());
1564        body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
1565        body.extend_from_slice(&attrs);
1566        let wire = bgp_wire(2, &body);
1567        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1568
1569        let next_hop = find_node(&tree, "bgp.attr.mp_reach.next_hop");
1570        assert!(next_hop.label.contains("truncated: 2 of 16"));
1571        assert!(tree.find("bgp.attr.mp_reach.reserved").is_none());
1572        assert!(tree.find("bgp.nlri.prefix").is_none());
1573    }
1574
1575    #[test]
1576    fn dissect_open_extended_params_and_raw_param() {
1577        // RFC 9072 extended optional parameters + a non-capability param.
1578        let mut caps = Vec::new();
1579        caps.extend_from_slice(&[65, 4]);
1580        caps.extend_from_slice(&65001u32.to_be_bytes()); // 4-octet AS capability
1581                                                         // RFC 9072 extended framing: 2-byte per-parameter lengths
1582        let caps_param = [
1583            vec![2u8],
1584            (caps.len() as u16).to_be_bytes().to_vec(),
1585            caps.clone(),
1586        ]
1587        .concat();
1588        let raw_param = [
1589            vec![254u8],
1590            3u16.to_be_bytes().to_vec(),
1591            vec![0xAA, 0xBB, 0xCC],
1592        ]
1593        .concat();
1594        let mut params = Vec::new();
1595        params.push(255);
1596        let params_len = 3 + caps_param.len() + raw_param.len();
1597        params.extend_from_slice(&(params_len as u16).to_be_bytes());
1598        params.extend_from_slice(&caps_param);
1599        params.extend_from_slice(&raw_param);
1600
1601        let mut body = Vec::new();
1602        body.push(4);
1603        body.extend_from_slice(&65001u16.to_be_bytes());
1604        body.extend_from_slice(&180u16.to_be_bytes());
1605        body.extend_from_slice(&[1, 2, 3, 4]);
1606        body.push(0xFF); // non-extended opt len marker for RFC 9072
1607        body.extend_from_slice(&params);
1608
1609        let wire = bgp_wire(1, &body);
1610        let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
1611
1612        let ext = find_node(&tree, "bgp.open.ext_params_len");
1613        assert!(ext.label.contains("RFC 9072"));
1614        assert!(tree.find("bgp.open.capability").is_some());
1615        let raw = find_node(&tree, "bgp.open.param.value");
1616        assert_eq!(raw.length, 3);
1617    }
1618
1619    #[test]
1620    fn dissect_truncated_update_sections() {
1621        // Withdrawn section declaring more bytes than present
1622        let mut body = Vec::new();
1623        body.extend_from_slice(&8u16.to_be_bytes()); // 8 declared
1624        body.extend_from_slice(&[24, 192, 0, 2]); // 4 present
1625        let wire = bgp_wire(2, &body);
1626        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1627        let wr = find_node(&tree, "bgp.update.withdrawn_routes");
1628        assert!(wr.label.contains("truncated: 4 of 8"));
1629
1630        // Truncated attribute-length field itself: the body ends mid-field
1631        let mut body = Vec::new();
1632        body.extend_from_slice(&0u16.to_be_bytes());
1633        body.push(0x40); // only 1 byte of the 2-byte length field
1634        let wire = bgp_wire(2, &body);
1635        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1636        let alen = find_node(&tree, "bgp.update.path_attributes.length");
1637        assert_eq!(alen.label, "Total path attribute length (truncated)");
1638
1639        // NLRI truncated mid-prefix: the partial prefix byte is dropped
1640        let mut body = Vec::new();
1641        body.extend_from_slice(&0u16.to_be_bytes());
1642        body.extend_from_slice(&0u16.to_be_bytes());
1643        body.extend_from_slice(&[24, 203, 0]); // /24 needs one more octet
1644        let wire = bgp_wire(2, &body);
1645        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
1646        assert!(tree.find("bgp.nlri.prefix").is_none());
1647    }
1648
1649    #[test]
1650    fn dissect_addpath_nlri() {
1651        // ADD-PATH NLRI: path id (4) + prefix length + octets
1652        let nlri = [0, 0, 0, 7, 24, 203, 0, 113];
1653        let mut body = Vec::new();
1654        body.extend_from_slice(&0u16.to_be_bytes());
1655        body.extend_from_slice(&0u16.to_be_bytes());
1656        body.extend_from_slice(&nlri);
1657
1658        let wire = bgp_wire(2, &body);
1659        let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, true);
1660        let mut prefixes = Vec::new();
1661        tree.find_all("bgp.nlri.prefix", &mut prefixes);
1662        assert_eq!(prefixes.len(), 1);
1663        assert_eq!((prefixes[0].offset, prefixes[0].length), (23, 8));
1664        assert_eq!(prefixes[0].label, "203.0.113.0/24");
1665    }
1666}