use crate::models::{AsnLength, DissectionNode};
use std::net::{Ipv4Addr, Ipv6Addr};
fn attr_name(code: u8) -> &'static str {
match code {
1 => "ORIGIN",
2 => "AS_PATH",
3 => "NEXT_HOP",
4 => "MULTI_EXIT_DISC",
5 => "LOCAL_PREF",
6 => "ATOMIC_AGGREGATE",
7 => "AGGREGATOR",
8 => "COMMUNITIES",
9 => "ORIGINATOR_ID",
10 => "CLUSTER_LIST",
14 => "MP_REACH_NLRI",
15 => "MP_UNREACH_NLRI",
16 => "EXTENDED_COMMUNITIES",
17 => "AS4_PATH",
18 => "AS4_AGGREGATOR",
22 => "PMSI_TUNNEL",
23 => "TUNNEL_ENCAPSULATION",
24 => "TRAFFIC_ENGINEERING",
25 => "IPV6_EXTENDED_COMMUNITIES",
26 => "AIGP",
27 => "PE_DISTINGUISHER_LABELS",
29 => "BGP-LS",
32 => "LARGE_COMMUNITY",
33 => "BGPSEC_PATH",
35 => "ONLY_TO_CUSTOMER",
37 => "SFP",
38 => "BFD_DISCRIMINATOR",
40 => "BGP_PREFIX_SID",
41 => "BIER",
_ => "ATTRIBUTE",
}
}
pub(crate) fn read_u16(data: &[u8], pos: usize) -> Option<u16> {
let hi = *data.get(pos)?;
let lo = *data.get(pos + 1)?;
Some(u16::from_be_bytes([hi, lo]))
}
fn read_u32(data: &[u8], pos: usize) -> Option<u32> {
let b: [u8; 4] = data.get(pos..pos + 4)?.try_into().ok()?;
Some(u32::from_be_bytes(b))
}
fn render_prefix(afi_v4: bool, plen: u8, octets: &[u8]) -> String {
if afi_v4 {
let mut octets = octets.to_vec();
octets.resize(4, 0);
format!(
"{}.{}.{}.{}/{}",
octets[0], octets[1], octets[2], octets[3], plen
)
} else {
let mut octets = octets.to_vec();
octets.resize(16, 0);
let segs: Vec<u16> = octets
.chunks(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
.collect();
let s = &segs;
format!(
"{}/{}",
Ipv6Addr::new(s[0], s[1], s[2], s[3], s[4], s[5], s[6], s[7]),
plen
)
}
}
pub fn dissect_bgp_message(data: &[u8], asn_len: &AsnLength, add_path: bool) -> DissectionNode {
dissect_bgp_message_base(data, 0, asn_len, add_path)
}
pub(crate) fn dissect_bgp_message_base(
data: &[u8],
base: u32,
asn_len: &AsnLength,
add_path: bool,
) -> DissectionNode {
if data.len() < 19 {
let mut root = DissectionNode::new(
"bgp",
format!("BGP message (truncated, {} of 19 header bytes)", data.len()),
base,
data.len() as u32,
);
if !data.is_empty() {
root.children.push(DissectionNode::new(
"bgp.header",
format!("Header (truncated, {} of 19 bytes)", data.len()),
base,
data.len() as u32,
));
}
return root;
}
let length = u16::from_be_bytes([data[16], data[17]]);
let msg_type = data[18];
let declared_body = (length as usize).saturating_sub(19);
let available_body = data.len() - 19;
let body_len = declared_body.min(available_body);
let message_len = 19 + body_len;
let mut root = DissectionNode::new(
"bgp",
format!("BGP message ({message_len} bytes)"),
base,
message_len as u32,
);
let mut header = DissectionNode::new("bgp.header", "Header", base, 19);
let all_ones = data[..16].iter().all(|b| *b == 0xFF);
header.children.push(DissectionNode::new(
"bgp.header.marker",
if all_ones {
"Marker (all ones)".to_string()
} else {
"Marker (NOT all ones — invalid per RFC 4271)".to_string()
},
base,
16,
));
header.children.push(DissectionNode::new(
"bgp.header.length",
format!("Length: {length}"),
base + 16,
2,
));
let type_name = match msg_type {
1 => "OPEN",
2 => "UPDATE",
3 => "NOTIFICATION",
4 => "KEEPALIVE",
5 => "ROUTE-REFRESH",
_ => "UNKNOWN",
};
header.children.push(DissectionNode::new(
"bgp.header.type",
format!("Type: {type_name} ({msg_type})"),
base + 18,
1,
));
root.children.push(header);
if declared_body > available_body {
root.children.push(DissectionNode::new(
"bgp.truncated",
format!(
"Declared length {length} exceeds the {} available bytes",
data.len()
),
base + message_len as u32,
0,
));
}
let body = &data[19..19 + body_len];
let body_base = base + 19;
match msg_type {
1 => root.children.push(dissect_open(body, body_base)),
2 => root
.children
.push(dissect_update(body, body_base, asn_len, add_path)),
3 => root.children.push(dissect_notification(body, body_base)),
4 => {}
5 => root.children.push(dissect_route_refresh(body, body_base)),
_ => root.children.push(DissectionNode::new(
"bgp.body",
format!("Unknown message type ({} bytes)", body.len()),
body_base,
body.len() as u32,
)),
}
if available_body > body_len {
root.children.push(DissectionNode::new(
"bgp.trailing",
format!(
"Trailing bytes beyond the declared message ({} bytes)",
available_body - body_len
),
base + message_len as u32,
(available_body - body_len) as u32,
));
}
root
}
fn dissect_update(data: &[u8], base: u32, asn_len: &AsnLength, add_path: bool) -> DissectionNode {
let mut update = DissectionNode::new("bgp.update", "UPDATE", base, data.len() as u32);
if data.len() < 2 {
if !data.is_empty() {
update.children.push(DissectionNode::new(
"bgp.update.withdrawn_routes.length",
"Withdrawn routes length (truncated)",
base,
data.len() as u32,
));
}
return update;
}
let withdrawn_len = u16::from_be_bytes([data[0], data[1]]) as usize;
update.children.push(DissectionNode::new(
"bgp.update.withdrawn_routes.length",
format!("Withdrawn routes length: {withdrawn_len}"),
base,
2,
));
let mut pos = 2usize;
if pos + withdrawn_len > data.len() {
update.children.push(DissectionNode::new(
"bgp.update.withdrawn_routes",
format!(
"Withdrawn routes (truncated: {} of {} bytes)",
data.len() - pos,
withdrawn_len
),
base + pos as u32,
(data.len() - pos) as u32,
));
return update;
}
if withdrawn_len > 0 {
let mut section = DissectionNode::new(
"bgp.update.withdrawn_routes",
format!("Withdrawn routes ({withdrawn_len} bytes)"),
base + 2,
withdrawn_len as u32,
);
section.children = dissect_nlri(&data[2..2 + withdrawn_len], base + 2, true, add_path);
update.children.push(section);
}
pos = 2 + withdrawn_len;
if pos + 2 > data.len() {
if pos < data.len() {
update.children.push(DissectionNode::new(
"bgp.update.path_attributes.length",
"Total path attribute length (truncated)",
base + pos as u32,
(data.len() - pos) as u32,
));
}
return update;
}
let attr_len = u16::from_be_bytes([data[pos], data[pos + 1]]) as usize;
update.children.push(DissectionNode::new(
"bgp.update.path_attributes.length",
format!("Total path attribute length: {attr_len}"),
base + pos as u32,
2,
));
pos += 2;
let attr_end = (pos + attr_len).min(data.len());
if attr_end > pos {
let mut section = DissectionNode::new(
"bgp.update.path_attributes",
format!("Path attributes ({attr_len} bytes)"),
base + pos as u32,
(attr_end - pos) as u32,
);
section.children =
dissect_attributes(&data[pos..attr_end], base + pos as u32, asn_len, add_path);
update.children.push(section);
} else if attr_len > 0 {
update.children.push(DissectionNode::new(
"bgp.update.path_attributes",
format!("Path attributes (truncated: declared {attr_len} bytes)"),
base + pos as u32,
0,
));
}
pos = attr_end;
if pos < data.len() {
let mut section = DissectionNode::new(
"bgp.update.nlri",
format!(
"Network Layer Reachability Information ({} bytes)",
data.len() - pos
),
base + pos as u32,
(data.len() - pos) as u32,
);
section.children = dissect_nlri(&data[pos..], base + pos as u32, true, add_path);
update.children.push(section);
}
update
}
fn dissect_attributes(
data: &[u8],
base: u32,
asn_len: &AsnLength,
add_path: bool,
) -> Vec<DissectionNode> {
let mut nodes = Vec::new();
let mut pos = 0usize;
let asn_size = match asn_len {
AsnLength::Bits16 => 2,
AsnLength::Bits32 => 4,
};
while pos + 3 <= data.len() {
let flags = data[pos];
let code = data[pos + 1];
let extended = flags & 0x10 != 0;
let header_len = if extended { 4 } else { 3 };
if pos + header_len > data.len() {
break;
}
let value_len = if extended {
match read_u16(data, pos + 2) {
Some(v) => v as usize,
None => break,
}
} else {
data[pos + 2] as usize
};
let value_start = pos + header_len;
let value_end = value_start + value_len;
if value_end > data.len() {
let attr_end = data.len();
let mut node = DissectionNode::new(
format!("bgp.attr.{code}"),
format!(
"{} (type {code}) — truncated ({} of {} value bytes)",
attr_name(code),
attr_end - value_start,
value_len
),
base + pos as u32,
(attr_end - pos) as u32,
);
node.children.push(DissectionNode::new(
"bgp.attr.flags",
format!("Flags: 0x{flags:02X}"),
base + pos as u32,
1,
));
nodes.push(node);
break;
}
let total = header_len + value_len;
let mut node = DissectionNode::new(
format!("bgp.attr.{code}"),
format!("{} (type {code}), {value_len} bytes", attr_name(code)),
base + pos as u32,
total as u32,
);
node.children.push(DissectionNode::new(
"bgp.attr.flags",
format!("Flags: 0x{flags:02X}"),
base + pos as u32,
1,
));
node.children.push(DissectionNode::new(
"bgp.attr.type",
format!("Type: {code} ({})", attr_name(code)),
base + pos as u32 + 1,
1,
));
node.children.push(DissectionNode::new(
"bgp.attr.length",
format!("Length: {value_len}"),
base + pos as u32 + 2,
(header_len - 2) as u32,
));
let value_base = base + value_start as u32;
let mut value_node = DissectionNode::new(
"bgp.attr.value",
format!("Value ({value_len} bytes)"),
value_base,
value_len as u32,
);
value_node.children = dissect_attr_value(
code,
&data[value_start..value_end],
value_base,
asn_size,
add_path,
);
node.children.push(value_node);
nodes.push(node);
pos = value_end;
}
nodes
}
fn dissect_attr_value(
code: u8,
value: &[u8],
base: u32,
asn_size: usize,
add_path: bool,
) -> Vec<DissectionNode> {
let mut nodes = Vec::new();
match code {
1 => {
if let Some(v) = value.first() {
let name = match v {
0 => "IGP",
1 => "EGP",
2 => "INCOMPLETE",
_ => "INVALID",
};
nodes.push(DissectionNode::new(
"bgp.attr.origin",
format!("Origin: {name} ({v})"),
base,
1,
));
}
}
2 | 17 => {
let asn_size = if code == 17 { 4 } else { asn_size };
let mut pos = 0usize;
while pos + 2 <= value.len() {
let seg_type = value[pos];
let count = value[pos + 1] as usize;
let seg_len = 2 + count * asn_size;
if pos + seg_len > value.len() {
break;
}
let name = match seg_type {
1 => "AS_SET",
2 => "AS_SEQUENCE",
3 => "AS_CONFED_SEQUENCE",
4 => "AS_CONFED_SET",
_ => "UNKNOWN",
};
let mut asns = Vec::with_capacity(count);
for i in 0..count {
let at = pos + 2 + i * asn_size;
let asn = if asn_size == 2 {
read_u16(value, at).unwrap_or(0) as u32
} else {
read_u32(value, at).unwrap_or(0)
};
asns.push(asn.to_string());
}
nodes.push(DissectionNode::new(
"bgp.attr.as_path.segment",
format!("{name}: {}", asns.join(" ")),
base + pos as u32,
seg_len as u32,
));
pos += seg_len;
}
}
3 => {
if let Some(ip) = value.get(0..4) {
let ipv4 = Ipv4Addr::new(ip[0], ip[1], ip[2], ip[3]);
nodes.push(DissectionNode::new(
"bgp.attr.next_hop",
format!("Next hop: {ipv4}"),
base,
value.len() as u32,
));
}
}
4 | 5 | 35 => {
if let Some(v) = read_u32(value, 0) {
let name = match code {
4 => "Multi-exit discriminator",
5 => "Local preference",
_ => "Only to customer",
};
nodes.push(DissectionNode::new(
"bgp.attr.u32",
format!("{name}: {v}"),
base,
value.len() as u32,
));
}
}
7 | 18 => {
if value.len() > 4 {
let asn_len_field = value.len() - 4;
let asn = if asn_len_field == 4 {
read_u32(value, 0).unwrap_or(0)
} else {
read_u16(value, 0).unwrap_or(0) as u32
};
nodes.push(DissectionNode::new(
"bgp.attr.aggregator.asn",
format!("Aggregator ASN: {asn}"),
base,
asn_len_field as u32,
));
let id = Ipv4Addr::new(
value[asn_len_field],
value[asn_len_field + 1],
value[asn_len_field + 2],
value[asn_len_field + 3],
);
nodes.push(DissectionNode::new(
"bgp.attr.aggregator.id",
format!("Aggregator router ID: {id}"),
base + asn_len_field as u32,
4,
));
}
}
8 => {
let mut pos = 0usize;
let mut idx = 1;
while pos + 4 <= value.len() {
let asn = read_u16(value, pos).unwrap_or(0);
let val = read_u16(value, pos + 2).unwrap_or(0);
nodes.push(DissectionNode::new(
"bgp.attr.communities.entry",
format!("Community [{idx}]: {asn}:{val}"),
base + pos as u32,
4,
));
pos += 4;
idx += 1;
}
}
9 => {
if let Some(ip) = value.get(0..4) {
nodes.push(DissectionNode::new(
"bgp.attr.originator_id",
format!("Originator ID: {}.{}.{}.{}", ip[0], ip[1], ip[2], ip[3]),
base,
value.len() as u32,
));
}
}
10 => {
let mut pos = 0usize;
let mut idx = 1;
while pos + 4 <= value.len() {
let id = read_u32(value, pos).unwrap_or(0);
nodes.push(DissectionNode::new(
"bgp.attr.cluster_list.entry",
format!("Cluster ID [{idx}]: {id}"),
base + pos as u32,
4,
));
pos += 4;
idx += 1;
}
}
14 => {
if value.len() >= 4 {
let afi = read_u16(value, 0).unwrap_or(0);
let afi_name = match afi {
1 => "IPv4",
2 => "IPv6",
25 => "L2VPN",
_ => "unknown",
};
nodes.push(DissectionNode::new(
"bgp.attr.mp_reach.afi",
format!("AFI: {afi} ({afi_name})"),
base,
2,
));
nodes.push(DissectionNode::new(
"bgp.attr.mp_reach.safi",
format!("SAFI: {}", value[2]),
base + 2,
1,
));
let nh_len = value[3] as usize;
nodes.push(DissectionNode::new(
"bgp.attr.mp_reach.next_hop_length",
format!("Next hop length: {nh_len}"),
base + 3,
1,
));
let mut pos = 4usize;
if nh_len > 0 && pos + nh_len > value.len() {
nodes.push(DissectionNode::new(
"bgp.attr.mp_reach.next_hop",
format!(
"Next hop (truncated: {} of {} bytes)",
value.len() - pos,
nh_len
),
base + pos as u32,
(value.len() - pos) as u32,
));
return nodes;
}
if nh_len > 0 {
nodes.push(DissectionNode::new(
"bgp.attr.mp_reach.next_hop",
format!("Next hop ({} bytes)", nh_len),
base + pos as u32,
nh_len as u32,
));
pos += nh_len;
}
if pos < value.len() {
nodes.push(DissectionNode::new(
"bgp.attr.mp_reach.reserved",
"Reserved",
base + pos as u32,
1,
));
pos += 1;
}
if pos < value.len() {
nodes.extend(dissect_nlri(
&value[pos..],
base + pos as u32,
afi == 1,
add_path,
));
}
}
}
15 => {
if value.len() >= 3 {
let afi = read_u16(value, 0).unwrap_or(0);
nodes.push(DissectionNode::new(
"bgp.attr.mp_unreach.afi",
format!("AFI: {afi}"),
base,
2,
));
nodes.push(DissectionNode::new(
"bgp.attr.mp_unreach.safi",
format!("SAFI: {}", value[2]),
base + 2,
1,
));
nodes.extend(dissect_nlri(&value[3..], base + 3, afi == 1, add_path));
}
}
16 => {
let mut pos = 0usize;
let mut idx = 1;
while pos + 8 <= value.len() {
nodes.push(DissectionNode::new(
"bgp.attr.ext_communities.entry",
format!(
"Extended community [{idx}]: {}",
render_ext_community(&value[pos..pos + 8])
),
base + pos as u32,
8,
));
pos += 8;
idx += 1;
}
}
25 => {
let mut pos = 0usize;
let mut idx = 1;
while pos + 20 <= value.len() {
nodes.push(DissectionNode::new(
"bgp.attr.ipv6_ext_communities.entry",
format!("IPv6 extended community [{idx}]"),
base + pos as u32,
20,
));
pos += 20;
idx += 1;
}
}
26 => {
let mut pos = 0usize;
while pos + 3 <= value.len() {
let tlv_type = value[pos];
let tlv_len = read_u16(value, pos + 1).unwrap_or(0) as usize;
if tlv_len < 3 || pos + tlv_len > value.len() {
break;
}
nodes.push(DissectionNode::new(
"bgp.attr.aigp.entry",
format!("AIGP TLV: type {tlv_type}, {tlv_len} bytes"),
base + pos as u32,
tlv_len as u32,
));
pos += tlv_len;
}
}
32 => {
let mut pos = 0usize;
let mut idx = 1;
while pos + 12 <= value.len() {
let ga = read_u32(value, pos).unwrap_or(0);
let l1 = read_u32(value, pos + 4).unwrap_or(0);
let l2 = read_u32(value, pos + 8).unwrap_or(0);
nodes.push(DissectionNode::new(
"bgp.attr.large_communities.entry",
format!("Large community [{idx}]: {ga}:{l1}:{l2}"),
base + pos as u32,
12,
));
pos += 12;
idx += 1;
}
}
_ => {}
}
nodes
}
fn render_ext_community(entry: &[u8]) -> String {
let type_high = entry[0];
let subtype = entry[1];
let value = &entry[2..8];
match type_high & 0x0F {
0x00 => {
let asn = u16::from_be_bytes([value[0], value[1]]);
let val = u32::from_be_bytes([value[2], value[3], value[4], value[5]]);
format!("type 0x{type_high:02X} subtype 0x{subtype:02X}: {asn}:{val}")
}
0x01 => {
let ip = Ipv4Addr::new(value[0], value[1], value[2], value[3]);
let val = u16::from_be_bytes([value[4], value[5]]);
format!("type 0x{type_high:02X} subtype 0x{subtype:02X}: {ip}:{val}")
}
0x02 => {
let asn = u32::from_be_bytes([value[0], value[1], value[2], value[3]]);
let val = u16::from_be_bytes([value[4], value[5]]);
format!("type 0x{type_high:02X} subtype 0x{subtype:02X}: {asn}:{val}")
}
_ => {
let hex: Vec<String> = value.iter().map(|b| format!("{b:02X}")).collect();
format!(
"type 0x{type_high:02X} subtype 0x{subtype:02X}: {}",
hex.join(" ")
)
}
}
}
fn dissect_nlri(data: &[u8], base: u32, afi_v4: bool, add_path: bool) -> Vec<DissectionNode> {
let mut nodes = Vec::new();
let mut pos = 0usize;
while pos < data.len() {
let entry_start = pos;
if add_path {
if pos + 5 > data.len() {
break;
}
pos += 4; }
let plen = match data.get(pos) {
Some(v) => *v,
None => break,
};
let octets = (plen as usize).div_ceil(8);
pos += 1;
if pos + octets > data.len() {
break;
}
let label = render_prefix(afi_v4, plen, &data[pos..pos + octets]);
nodes.push(DissectionNode::new(
"bgp.nlri.prefix",
label,
base + entry_start as u32,
(pos + octets - entry_start) as u32,
));
pos += octets;
}
nodes
}
fn dissect_open(data: &[u8], base: u32) -> DissectionNode {
let mut open = DissectionNode::new("bgp.open", "OPEN", base, data.len() as u32);
if data.len() < 10 {
if !data.is_empty() {
open.children.push(DissectionNode::new(
"bgp.open.truncated",
format!("Truncated OPEN ({} of 10 fixed bytes)", data.len()),
base,
data.len() as u32,
));
}
return open;
}
open.children.push(DissectionNode::new(
"bgp.open.version",
format!("Version: {}", data[0]),
base,
1,
));
let asn = u16::from_be_bytes([data[1], data[2]]);
open.children.push(DissectionNode::new(
"bgp.open.asn",
format!("My AS: {asn}"),
base + 1,
2,
));
let hold = u16::from_be_bytes([data[3], data[4]]);
open.children.push(DissectionNode::new(
"bgp.open.hold_time",
format!("Hold time: {hold}s"),
base + 3,
2,
));
open.children.push(DissectionNode::new(
"bgp.open.bgp_identifier",
format!(
"BGP identifier: {}.{}.{}.{}",
data[5], data[6], data[7], data[8]
),
base + 5,
4,
));
let opt_len = data[9];
open.children.push(DissectionNode::new(
"bgp.open.opt_params_len",
format!("Optional parameters length: {opt_len}"),
base + 9,
1,
));
let mut pos = 10usize;
let mut extended = false;
let mut first = true;
while pos + 2 <= data.len() {
let param_type = data[pos];
if first && param_type == 255 && opt_len != 0 {
extended = true;
if pos + 3 > data.len() {
break;
}
let ext_len = read_u16(data, pos + 1).unwrap_or(0);
open.children.push(DissectionNode::new(
"bgp.open.ext_params_len",
format!("Extended optional parameters length: {ext_len} (RFC 9072)"),
base + pos as u32,
3,
));
pos += 3;
first = false;
if ext_len == 0 {
break;
}
continue;
}
first = false;
let len_size = if extended { 2 } else { 1 };
let value_len = if extended {
match read_u16(data, pos + 1) {
Some(v) => v as usize,
None => break,
}
} else {
*data.get(pos + 1).unwrap_or(&0) as usize
};
let header_len = 1 + len_size;
if pos + header_len + value_len > data.len() {
break;
}
let mut param = DissectionNode::new(
"bgp.open.param",
if param_type == 2 {
"Capabilities (RFC 3392)".to_string()
} else {
format!("Optional parameter (type {param_type})")
},
base + pos as u32,
(header_len + value_len) as u32,
);
param.children.push(DissectionNode::new(
"bgp.open.param.type",
format!("Type: {param_type}"),
base + pos as u32,
1,
));
param.children.push(DissectionNode::new(
"bgp.open.param.length",
format!("Length: {value_len}"),
base + pos as u32 + 1,
len_size as u32,
));
let value = &data[pos + header_len..pos + header_len + value_len];
let value_base = base + (pos + header_len) as u32;
if param_type == 2 {
let mut cpos = 0usize;
while cpos + 2 <= value.len() {
let cap_code = value[cpos];
let cap_len = value[cpos + 1] as usize;
if cpos + 2 + cap_len > value.len() {
break;
}
let mut cap = DissectionNode::new(
"bgp.open.capability",
format!("Capability: {} ({cap_code})", capability_name(cap_code)),
value_base + cpos as u32,
(2 + cap_len) as u32,
);
cap.children.push(DissectionNode::new(
"bgp.open.capability.code",
format!("Code: {cap_code} ({})", capability_name(cap_code)),
value_base + cpos as u32,
1,
));
cap.children.push(DissectionNode::new(
"bgp.open.capability.length",
format!("Length: {cap_len}"),
value_base + cpos as u32 + 1,
1,
));
param.children.push(cap);
cpos += 2 + cap_len;
}
} else {
param.children.push(DissectionNode::new(
"bgp.open.param.value",
format!("Value ({value_len} bytes)"),
value_base,
value_len as u32,
));
}
open.children.push(param);
pos += header_len + value_len;
}
open
}
fn capability_name(code: u8) -> &'static str {
match code {
1 => "Multiprotocol Extensions",
2 => "Route Refresh",
4 => "Multiple Routes to a Destination",
5 => "Extended Next Hop Encoding",
6 => "BGP Extended Message",
7 => "BGP Role",
64 => "Graceful Restart",
65 => "Support for 4-octet AS Number",
66 => "Support for Add-Path",
67 => "Enhanced Route Refresh",
68 => "Long-Lived Graceful Restart",
_ => "Unknown",
}
}
fn dissect_notification(data: &[u8], base: u32) -> DissectionNode {
let mut notification =
DissectionNode::new("bgp.notification", "NOTIFICATION", base, data.len() as u32);
if let Some(code) = data.first() {
notification.children.push(DissectionNode::new(
"bgp.notification.code",
format!("Error code: {code}"),
base,
1,
));
}
if data.len() >= 2 {
notification.children.push(DissectionNode::new(
"bgp.notification.subcode",
format!("Error subcode: {}", data[1]),
base + 1,
1,
));
}
if data.len() > 2 {
notification.children.push(DissectionNode::new(
"bgp.notification.data",
format!("Data ({} bytes)", data.len() - 2),
base + 2,
(data.len() - 2) as u32,
));
}
notification
}
fn dissect_route_refresh(data: &[u8], base: u32) -> DissectionNode {
let mut refresh = DissectionNode::new(
"bgp.route_refresh",
"ROUTE-REFRESH",
base,
data.len() as u32,
);
if data.len() >= 4 {
let afi = read_u16(data, 0).unwrap_or(0);
refresh.children.push(DissectionNode::new(
"bgp.route_refresh.afi",
format!("AFI: {afi}"),
base,
2,
));
refresh.children.push(DissectionNode::new(
"bgp.route_refresh.subtype",
format!("Subtype: {}", data[2]),
base + 2,
1,
));
refresh.children.push(DissectionNode::new(
"bgp.route_refresh.safi",
format!("SAFI: {}", data[3]),
base + 3,
1,
));
}
if data.len() > 4 {
refresh.children.push(DissectionNode::new(
"bgp.route_refresh.data",
format!("ORF data ({} bytes)", data.len() - 4),
base + 4,
(data.len() - 4) as u32,
));
}
refresh
}
#[cfg(test)]
mod tests {
use super::*;
fn bgp_wire(msg_type: u8, body: &[u8]) -> Vec<u8> {
let mut wire = vec![0xFF; 16];
let total = (19 + body.len()) as u16;
wire.extend_from_slice(&total.to_be_bytes());
wire.push(msg_type);
wire.extend_from_slice(body);
wire
}
fn sample_update_body() -> Vec<u8> {
let mut body = Vec::new();
let withdrawn: [u8; 4] = [24, 192, 0, 2];
body.extend_from_slice(&(withdrawn.len() as u16).to_be_bytes());
body.extend_from_slice(&withdrawn);
let mut attrs = Vec::new();
attrs.extend_from_slice(&[0x40, 0x01, 0x01, 0x00]); attrs.extend_from_slice(&[0x40, 0x02, 0x0A, 0x02, 0x02]);
attrs.extend_from_slice(&65001u32.to_be_bytes());
attrs.extend_from_slice(&65002u32.to_be_bytes());
attrs.extend_from_slice(&[0x40, 0x03, 0x04, 192, 0, 2, 254]);
let mut comms = Vec::new();
comms.extend_from_slice(&64512u16.to_be_bytes());
comms.extend_from_slice(&100u16.to_be_bytes());
comms.extend_from_slice(&64512u16.to_be_bytes());
comms.extend_from_slice(&200u16.to_be_bytes());
attrs.push(0xC0);
attrs.push(0x08);
attrs.push(comms.len() as u8);
attrs.extend_from_slice(&comms);
attrs.push(0xC0);
attrs.push(0x20);
attrs.push(12);
attrs.extend_from_slice(&64496u32.to_be_bytes());
attrs.extend_from_slice(&1u32.to_be_bytes());
attrs.extend_from_slice(&2u32.to_be_bytes());
body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
body.extend_from_slice(&[24, 203, 0, 113]);
body
}
fn find_node<'a>(node: &'a DissectionNode, field: &str) -> &'a DissectionNode {
node.find(field)
.unwrap_or_else(|| panic!("node {field} not found in tree"))
}
#[test]
fn dissect_update_field_offsets() {
let wire = bgp_wire(2, &sample_update_body());
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
assert_eq!(tree.field, "bgp");
assert_eq!(tree.offset, 0);
assert_eq!(tree.length, wire.len() as u32);
let marker = find_node(&tree, "bgp.header.marker");
assert_eq!((marker.offset, marker.length), (0, 16));
let length = find_node(&tree, "bgp.header.length");
assert_eq!((length.offset, length.length), (16, 2));
assert_eq!(length.label, format!("Length: {}", wire.len()));
let msg_type = find_node(&tree, "bgp.header.type");
assert_eq!((msg_type.offset, msg_type.length), (18, 1));
let wlen = find_node(&tree, "bgp.update.withdrawn_routes.length");
assert_eq!((wlen.offset, wlen.length), (19, 2));
assert_eq!(wlen.label, "Withdrawn routes length: 4");
let wr = find_node(&tree, "bgp.update.withdrawn_routes");
assert_eq!((wr.offset, wr.length), (21, 4));
let wprefix = find_node(&tree, "bgp.nlri.prefix");
assert_eq!((wprefix.offset, wprefix.length), (21, 4));
assert_eq!(wprefix.label, "192.0.2.0/24");
let alen = find_node(&tree, "bgp.update.path_attributes.length");
assert_eq!((alen.offset, alen.length), (25, 2));
let attrs = find_node(&tree, "bgp.update.path_attributes");
assert_eq!(attrs.offset, 27);
let as_path = find_node(&tree, "bgp.attr.2");
assert_eq!((as_path.offset, as_path.length), (31, 13));
let segment = find_node(&tree, "bgp.attr.as_path.segment");
assert_eq!((segment.offset, segment.length), (34, 10));
assert_eq!(segment.label, "AS_SEQUENCE: 65001 65002");
let comms = find_node(&tree, "bgp.attr.8");
assert_eq!((comms.offset, comms.length), (51, 11));
let mut entries = Vec::new();
tree.find_all("bgp.attr.communities.entry", &mut entries);
assert_eq!(entries.len(), 2);
assert_eq!((entries[0].offset, entries[0].length), (54, 4));
assert_eq!(entries[0].label, "Community [1]: 64512:100");
assert_eq!(entries[1].label, "Community [2]: 64512:200");
let large = find_node(&tree, "bgp.attr.32");
assert_eq!((large.offset, large.length), (62, 15));
let entry = find_node(&tree, "bgp.attr.large_communities.entry");
assert_eq!(entry.label, "Large community [1]: 64496:1:2");
let nlri = find_node(&tree, "bgp.update.nlri");
assert_eq!(nlri.offset, 77);
assert_eq!(nlri.length, 4);
}
#[test]
fn dissect_update_mp_reach() {
let mut value = Vec::new();
value.extend_from_slice(&2u16.to_be_bytes()); value.push(1); value.push(16); value.extend_from_slice(&[
0x20, 0x01, 0x0d, 0xb8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01,
]);
value.push(0); value.extend_from_slice(&[32, 0x20, 0x01, 0x0d, 0xb8]);
let mut attrs = Vec::new();
attrs.push(0x80);
attrs.push(14);
attrs.push(value.len() as u8);
attrs.extend_from_slice(&value);
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes()); body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
let mp_reach = find_node(&tree, "bgp.attr.14");
let afi = find_node(&tree, "bgp.attr.mp_reach.afi");
assert_eq!(afi.label, "AFI: 2 (IPv6)");
let nh = find_node(&tree, "bgp.attr.mp_reach.next_hop");
assert_eq!(nh.length, 16);
let prefix = tree.find("bgp.update.nlri");
assert!(prefix.is_none(), "IPv6 NLRI lives inside MP_REACH");
let mut prefixes = Vec::new();
tree.find_all("bgp.nlri.prefix", &mut prefixes);
assert_eq!(prefixes.len(), 1);
assert_eq!(prefixes[0].label, "2001:db8::/32");
assert_eq!(mp_reach.length, 3 + value.len() as u32);
}
#[test]
fn dissect_update_truncated_attribute_is_partial() {
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
let attrs = [0x40u8, 0x02, 0x0A, 0x02, 0x02, 0x0F, 0x9A]; body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
let as_path = find_node(&tree, "bgp.attr.2");
assert!(as_path.label.contains("truncated"));
assert_eq!(as_path.length, attrs.len() as u32);
}
#[test]
fn dissect_open_capabilities() {
let mut caps = Vec::new();
caps.extend_from_slice(&[1, 4, 0, 1, 0, 1]); caps.extend_from_slice(&[65, 4, 0, 0, 0xFD, 0xE9]);
let mut body = Vec::new();
body.push(4);
body.extend_from_slice(&65001u16.to_be_bytes());
body.extend_from_slice(&180u16.to_be_bytes());
body.extend_from_slice(&[1, 2, 3, 4]);
body.push(2 + caps.len() as u8); body.push(2); body.push(caps.len() as u8);
body.extend_from_slice(&caps);
let wire = bgp_wire(1, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
let version = find_node(&tree, "bgp.open.version");
assert_eq!((version.offset, version.length), (19, 1));
let asn = find_node(&tree, "bgp.open.asn");
assert_eq!(asn.label, "My AS: 65001");
let mut capabilities = Vec::new();
tree.find_all("bgp.open.capability", &mut capabilities);
assert_eq!(capabilities.len(), 2);
assert_eq!(
capabilities[0].label,
"Capability: Multiprotocol Extensions (1)"
);
assert_eq!(
capabilities[1].label,
"Capability: Support for 4-octet AS Number (65)"
);
assert_eq!(capabilities[0].offset, 19 + 10 + 2);
}
#[test]
fn dissect_notification_and_route_refresh() {
let notification = bgp_wire(3, &[6, 5, 0xDE, 0xAD]);
let tree = dissect_bgp_message(¬ification, &AsnLength::Bits16, false);
let code = find_node(&tree, "bgp.notification.code");
assert_eq!((code.offset, code.length), (19, 1));
let data = find_node(&tree, "bgp.notification.data");
assert_eq!(data.length, 2);
let refresh = bgp_wire(5, &[0, 1, 0, 1, 0xDE, 0xAD, 0xBE]);
let tree = dissect_bgp_message(&refresh, &AsnLength::Bits16, false);
let subtype = find_node(&tree, "bgp.route_refresh.subtype");
assert_eq!(subtype.label, "Subtype: 0");
let orf = find_node(&tree, "bgp.route_refresh.data");
assert_eq!(orf.length, 3);
}
#[test]
fn dissect_keepalive_and_truncated_header() {
let keepalive = bgp_wire(4, &[]);
let tree = dissect_bgp_message(&keepalive, &AsnLength::Bits16, false);
assert_eq!(tree.children.len(), 1, "header only, no body node");
assert!(tree.find("bgp.header.type").is_some());
let truncated = dissect_bgp_message(&[0xFF; 7], &AsnLength::Bits16, false);
let header = find_node(&truncated, "bgp.header");
assert!(header.label.contains("truncated"));
}
#[test]
fn dissect_attribute_value_zoo() {
let mut attrs = Vec::new();
let push_attr = |attrs: &mut Vec<u8>, flags: u8, code: u8, value: &[u8]| {
attrs.push(flags);
attrs.push(code);
attrs.push(value.len() as u8);
attrs.extend_from_slice(value);
};
push_attr(&mut attrs, 0x40, 1, &[0x01]);
push_attr(&mut attrs, 0x40, 3, &[10, 0, 0, 1]);
push_attr(&mut attrs, 0x80, 4, &100u32.to_be_bytes());
push_attr(&mut attrs, 0x40, 5, &200u32.to_be_bytes());
push_attr(&mut attrs, 0x40, 6, &[]);
push_attr(&mut attrs, 0xC0, 7, &[0xFC, 0x80, 1, 2, 3, 4]);
push_attr(&mut attrs, 0x80, 9, &[10, 0, 0, 9]);
push_attr(&mut attrs, 0x80, 10, &0xAABBCCDDu32.to_be_bytes());
push_attr(
&mut attrs,
0xC0,
16,
&[0x00, 0x02, 0xFC, 0x80, 0, 0, 0, 100],
);
push_attr(&mut attrs, 0xC0, 16, &[0x03, 0x04, 0, 0, 0, 0, 0, 1]);
let as4_aggregator: Vec<u8> =
[4200000000u32.to_be_bytes().to_vec(), vec![1, 2, 3, 4]].concat();
push_attr(&mut attrs, 0xC0, 18, &as4_aggregator);
push_attr(&mut attrs, 0xC0, 25, &[0x00; 20]);
push_attr(
&mut attrs,
0x80,
26,
&[0x01, 0x00, 0x0B, 0, 0, 0, 0, 0, 0, 0, 100],
);
push_attr(&mut attrs, 0xC0, 35, &64512u32.to_be_bytes());
push_attr(&mut attrs, 0x80, 99, &[0xAB, 0xCD]);
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
assert_eq!(find_node(&tree, "bgp.attr.origin").label, "Origin: EGP (1)");
assert_eq!(
find_node(&tree, "bgp.attr.next_hop").label,
"Next hop: 10.0.0.1"
);
assert_eq!(
find_node(&tree, "bgp.attr.u32").label,
"Multi-exit discriminator: 100"
);
assert_eq!(
find_node(&tree, "bgp.attr.aggregator.asn").label,
"Aggregator ASN: 64640"
);
assert_eq!(
find_node(&tree, "bgp.attr.aggregator.id").label,
"Aggregator router ID: 1.2.3.4"
);
assert_eq!(
find_node(&tree, "bgp.attr.originator_id").label,
"Originator ID: 10.0.0.9"
);
let cluster = find_node(&tree, "bgp.attr.cluster_list.entry");
assert!(cluster.label.contains("2864434397"));
let mut ext = Vec::new();
tree.find_all("bgp.attr.ext_communities.entry", &mut ext);
assert_eq!(ext.len(), 2);
assert_eq!(
ext[0].label,
"Extended community [1]: type 0x00 subtype 0x02: 64640:100"
);
assert!(ext[1].label.contains("type 0x03 subtype 0x04"));
assert_eq!(
find_node(&tree, "bgp.attr.ipv6_ext_communities.entry").label,
"IPv6 extended community [1]"
);
let aigp = find_node(&tree, "bgp.attr.aigp.entry");
assert_eq!(aigp.label, "AIGP TLV: type 1, 11 bytes");
assert_eq!(
(aigp.offset, aigp.length),
(find_node(&tree, "bgp.attr.26").offset + 3, 11)
);
let mut u32s = Vec::new();
tree.find_all("bgp.attr.u32", &mut u32s);
assert!(u32s.iter().any(|n| n.label == "Only to customer: 64512"));
assert!(find_node(&tree, "bgp.attr.99")
.label
.starts_with("ATTRIBUTE (type 99)"));
}
#[test]
fn dissect_as4_path_uses_four_octet_asns_even_on_16bit_sessions() {
let value: Vec<u8> = [
&[0x02, 0x02][..],
&65001u32.to_be_bytes(),
&65002u32.to_be_bytes(),
]
.concat();
let mut attrs = Vec::new();
attrs.extend_from_slice(&[0x40, 17, value.len() as u8]);
attrs.extend_from_slice(&value);
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
let segment = find_node(&tree, "bgp.attr.as_path.segment");
assert_eq!((segment.offset, segment.length), (23 + 3, 10));
assert_eq!(segment.label, "AS_SEQUENCE: 65001 65002");
}
#[test]
fn dissect_extended_length_attribute() {
let mut value = vec![0u8; 300];
value[0] = 1; let mut attrs = Vec::new();
attrs.push(0x50); attrs.push(99);
attrs.extend_from_slice(&(value.len() as u16).to_be_bytes());
attrs.extend_from_slice(&value);
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
let attr = find_node(&tree, "bgp.attr.99");
assert_eq!(attr.length, 4 + 300);
let length = find_node(&tree, "bgp.attr.length");
assert_eq!(length.label, "Length: 300");
assert_eq!(length.length, 2);
}
#[test]
fn dissect_declared_length_bounds_body() {
let mut wire = bgp_wire(4, &[]);
wire.extend_from_slice(&[0xDE, 0xAD, 0xBE, 0xEF, 0x42]);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
assert_eq!(tree.length, 19, "root bounded to the declared message");
let trailing = find_node(&tree, "bgp.trailing");
assert_eq!((trailing.offset, trailing.length), (19, 5));
let body = sample_update_body();
let mut wire = bgp_wire(2, &body);
let full_len = u16::from_be_bytes([wire[16], wire[17]]);
let shorter = full_len - 4;
wire[16] = (shorter >> 8) as u8;
wire[17] = (shorter & 0xFF) as u8;
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
assert!(tree.find("bgp.update.nlri").is_none());
assert_eq!(find_node(&tree, "bgp.trailing").length, 4);
let mut wire = bgp_wire(2, &body);
wire.truncate(wire.len() - 10);
let inflated = wire.len() as u16 + 20;
wire[16] = (inflated >> 8) as u8;
wire[17] = (inflated & 0xFF) as u8;
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
let truncated = find_node(&tree, "bgp.truncated");
assert!(truncated.label.contains("exceeds"));
}
#[test]
fn dissect_mp_unreach_and_truncated_next_hop() {
let mut value = Vec::new();
value.extend_from_slice(&2u16.to_be_bytes()); value.push(1); value.extend_from_slice(&[32, 0x20, 0x01, 0x0d, 0xb8]); let mut attrs = Vec::new();
attrs.extend_from_slice(&[0x80, 15, value.len() as u8]);
attrs.extend_from_slice(&value);
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
assert!(tree.find("bgp.attr.mp_unreach.afi").is_some());
let mut prefixes = Vec::new();
tree.find_all("bgp.nlri.prefix", &mut prefixes);
assert_eq!(prefixes[0].label, "2001:db8::/32");
let mut value = Vec::new();
value.extend_from_slice(&2u16.to_be_bytes());
value.push(1);
value.push(16); value.extend_from_slice(&[0x20, 0x01]); let mut attrs = Vec::new();
attrs.extend_from_slice(&[0x80, 14, value.len() as u8]);
attrs.extend_from_slice(&value);
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&(attrs.len() as u16).to_be_bytes());
body.extend_from_slice(&attrs);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
let next_hop = find_node(&tree, "bgp.attr.mp_reach.next_hop");
assert!(next_hop.label.contains("truncated: 2 of 16"));
assert!(tree.find("bgp.attr.mp_reach.reserved").is_none());
assert!(tree.find("bgp.nlri.prefix").is_none());
}
#[test]
fn dissect_open_extended_params_and_raw_param() {
let mut caps = Vec::new();
caps.extend_from_slice(&[65, 4]);
caps.extend_from_slice(&65001u32.to_be_bytes()); let caps_param = [
vec![2u8],
(caps.len() as u16).to_be_bytes().to_vec(),
caps.clone(),
]
.concat();
let raw_param = [
vec![254u8],
3u16.to_be_bytes().to_vec(),
vec![0xAA, 0xBB, 0xCC],
]
.concat();
let mut params = Vec::new();
params.push(255);
let params_len = 3 + caps_param.len() + raw_param.len();
params.extend_from_slice(&(params_len as u16).to_be_bytes());
params.extend_from_slice(&caps_param);
params.extend_from_slice(&raw_param);
let mut body = Vec::new();
body.push(4);
body.extend_from_slice(&65001u16.to_be_bytes());
body.extend_from_slice(&180u16.to_be_bytes());
body.extend_from_slice(&[1, 2, 3, 4]);
body.push(0xFF); body.extend_from_slice(¶ms);
let wire = bgp_wire(1, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits16, false);
let ext = find_node(&tree, "bgp.open.ext_params_len");
assert!(ext.label.contains("RFC 9072"));
assert!(tree.find("bgp.open.capability").is_some());
let raw = find_node(&tree, "bgp.open.param.value");
assert_eq!(raw.length, 3);
}
#[test]
fn dissect_truncated_update_sections() {
let mut body = Vec::new();
body.extend_from_slice(&8u16.to_be_bytes()); body.extend_from_slice(&[24, 192, 0, 2]); let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
let wr = find_node(&tree, "bgp.update.withdrawn_routes");
assert!(wr.label.contains("truncated: 4 of 8"));
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.push(0x40); let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
let alen = find_node(&tree, "bgp.update.path_attributes.length");
assert_eq!(alen.label, "Total path attribute length (truncated)");
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&[24, 203, 0]); let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, false);
assert!(tree.find("bgp.nlri.prefix").is_none());
}
#[test]
fn dissect_addpath_nlri() {
let nlri = [0, 0, 0, 7, 24, 203, 0, 113];
let mut body = Vec::new();
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&nlri);
let wire = bgp_wire(2, &body);
let tree = dissect_bgp_message(&wire, &AsnLength::Bits32, true);
let mut prefixes = Vec::new();
tree.find_all("bgp.nlri.prefix", &mut prefixes);
assert_eq!(prefixes.len(), 1);
assert_eq!((prefixes[0].offset, prefixes[0].length), (23, 8));
assert_eq!(prefixes[0].label, "203.0.113.0/24");
}
}