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