use crate::error::BgpValidationWarning;
use crate::models::*;
use crate::parser::bgp::dissect::dissect_bgp_message;
use crate::parser::bgp::messages::parse_bgp_message;
use crate::parser::bmp::error::ParserBmpError;
use crate::parser::bmp::messages::*;
use crate::parser::bmp::{parse_bmp_msg, parse_openbmp_header};
use crate::parser::mrt::dissect::dissect_mrt_record;
use crate::parser::mrt::mrt_elem::Elementor;
use crate::parser::mrt::mrt_record::{chunk_mrt_record, parse_mrt_record};
use crate::parser::rislive::{
parse_ris_live_message, parse_ris_live_message_raw_full, RisLiveRawFull,
};
use bytes::Bytes;
use std::io::Cursor;
use std::net::{IpAddr, Ipv4Addr};
use wasm_bindgen::prelude::*;
#[derive(serde::Serialize)]
#[serde(tag = "type")]
enum WasmBmpMessage {
RouteMonitoring {
#[serde(flatten)]
base: WasmMessageBase,
#[serde(rename = "peerHeader")]
peer_header: WasmPeerHeader,
elems: Vec<BgpElem>,
},
PeerUpNotification {
#[serde(flatten)]
base: WasmMessageBase,
#[serde(rename = "peerHeader")]
peer_header: WasmPeerHeader,
#[serde(rename = "localIp")]
local_ip: String,
#[serde(rename = "localPort")]
local_port: u16,
#[serde(rename = "remotePort")]
remote_port: u16,
},
PeerDownNotification {
#[serde(flatten)]
base: WasmMessageBase,
#[serde(rename = "peerHeader")]
peer_header: WasmPeerHeader,
reason: String,
},
InitiationMessage {
#[serde(flatten)]
base: WasmMessageBase,
tlvs: Vec<WasmTlv>,
},
TerminationMessage {
#[serde(flatten)]
base: WasmMessageBase,
tlvs: Vec<WasmTlv>,
},
StatisticsReport {
#[serde(flatten)]
base: WasmMessageBase,
#[serde(rename = "peerHeader")]
peer_header: WasmPeerHeader,
},
RouteMirroringMessage {
#[serde(flatten)]
base: WasmMessageBase,
#[serde(rename = "peerHeader")]
peer_header: WasmPeerHeader,
},
}
#[derive(serde::Serialize)]
struct WasmMessageBase {
#[serde(rename = "openBmpHeader")]
openbmp_header: Option<WasmOpenBmpHeader>,
timestamp: f64,
}
#[derive(serde::Serialize)]
struct WasmOpenBmpHeader {
#[serde(rename = "routerIp")]
router_ip: String,
#[serde(rename = "routerGroup")]
router_group: Option<String>,
#[serde(rename = "adminId")]
admin_id: String,
timestamp: f64,
}
#[derive(serde::Serialize)]
struct WasmPeerHeader {
#[serde(rename = "peerIp")]
peer_ip: String,
#[serde(rename = "peerAsn")]
peer_asn: u32,
#[serde(rename = "peerBgpId")]
peer_bgp_id: String,
#[serde(rename = "peerType")]
peer_type: String,
#[serde(rename = "isPostPolicy")]
is_post_policy: bool,
#[serde(rename = "isAdjRibOut")]
is_adj_rib_out: bool,
timestamp: f64,
}
#[derive(serde::Serialize)]
struct WasmTlv {
#[serde(rename = "type")]
tlv_type: String,
value: String,
}
fn make_openbmp_header(header: &crate::parser::bmp::openbmp::OpenBmpHeader) -> WasmOpenBmpHeader {
WasmOpenBmpHeader {
router_ip: header.router_ip.to_string(),
router_group: header.router_group.clone(),
admin_id: header.admin_id.clone(),
timestamp: header.timestamp,
}
}
fn make_peer_header(pph: &BmpPerPeerHeader) -> WasmPeerHeader {
let (is_post_policy, is_adj_rib_out) = match pph.peer_flags {
PerPeerFlags::PeerFlags(f) => (f.is_post_policy(), f.is_adj_rib_out()),
PerPeerFlags::LocalRibPeerFlags(_) => (false, false),
};
WasmPeerHeader {
peer_ip: pph.peer_ip.to_string(),
peer_asn: pph.peer_asn.into(),
peer_bgp_id: pph.peer_bgp_id.to_string(),
peer_type: format!("{:?}", pph.peer_type),
is_post_policy,
is_adj_rib_out,
timestamp: pph.timestamp,
}
}
fn build_bmp_result(
bmp_msg: BmpMessage,
openbmp_header: Option<WasmOpenBmpHeader>,
timestamp: f64,
) -> WasmBmpMessage {
let base = WasmMessageBase {
openbmp_header,
timestamp,
};
match (bmp_msg.per_peer_header, bmp_msg.message_body) {
(Some(pph), BmpMessageBody::RouteMonitoring(m)) => {
let elems =
Elementor::bgp_to_elems(m.bgp_message, timestamp, &pph.peer_ip, &pph.peer_asn);
WasmBmpMessage::RouteMonitoring {
base,
peer_header: make_peer_header(&pph),
elems,
}
}
(Some(pph), BmpMessageBody::PeerUpNotification(m)) => WasmBmpMessage::PeerUpNotification {
base,
peer_header: make_peer_header(&pph),
local_ip: m.local_addr.to_string(),
local_port: m.local_port,
remote_port: m.remote_port,
},
(Some(pph), BmpMessageBody::PeerDownNotification(m)) => {
WasmBmpMessage::PeerDownNotification {
base,
peer_header: make_peer_header(&pph),
reason: format!("{:?}", m.reason),
}
}
(_, BmpMessageBody::InitiationMessage(m)) => WasmBmpMessage::InitiationMessage {
base,
tlvs: m
.tlvs
.into_iter()
.map(|t| WasmTlv {
tlv_type: format!("{:?}", t.info_type),
value: t.info,
})
.collect(),
},
(_, BmpMessageBody::TerminationMessage(m)) => WasmBmpMessage::TerminationMessage {
base,
tlvs: m
.tlvs
.into_iter()
.map(|t| WasmTlv {
tlv_type: format!("{:?}", t.info_type),
value: format!("{:?}", t.info_value),
})
.collect(),
},
(Some(pph), BmpMessageBody::StatsReport(_)) => WasmBmpMessage::StatisticsReport {
base,
peer_header: make_peer_header(&pph),
},
(Some(pph), BmpMessageBody::RouteMirroring(_)) => WasmBmpMessage::RouteMirroringMessage {
base,
peer_header: make_peer_header(&pph),
},
(None, BmpMessageBody::StatsReport(_)) => WasmBmpMessage::StatisticsReport {
base,
peer_header: make_peer_header(&BmpPerPeerHeader::default()),
},
(None, BmpMessageBody::RouteMirroring(_)) => WasmBmpMessage::RouteMirroringMessage {
base,
peer_header: make_peer_header(&BmpPerPeerHeader::default()),
},
(None, BmpMessageBody::RouteMonitoring(_)) => WasmBmpMessage::RouteMonitoring {
base,
peer_header: make_peer_header(&BmpPerPeerHeader::default()),
elems: vec![],
},
(None, BmpMessageBody::PeerUpNotification(m)) => WasmBmpMessage::PeerUpNotification {
base,
peer_header: make_peer_header(&BmpPerPeerHeader::default()),
local_ip: m.local_addr.to_string(),
local_port: m.local_port,
remote_port: m.remote_port,
},
(None, BmpMessageBody::PeerDownNotification(m)) => WasmBmpMessage::PeerDownNotification {
base,
peer_header: make_peer_header(&BmpPerPeerHeader::default()),
reason: format!("{:?}", m.reason),
},
}
}
fn parse_openbmp_message_core(data: &[u8]) -> Result<Option<String>, String> {
let mut bytes = Bytes::from(data.to_vec());
let header = match parse_openbmp_header(&mut bytes) {
Ok(h) => h,
Err(ParserBmpError::UnsupportedOpenBmpMessage) => return Ok(None),
Err(e) => return Err(e.to_string()),
};
let bmp_msg = parse_bmp_msg(&mut bytes).map_err(|e| e.to_string())?;
let result = build_bmp_result(
bmp_msg,
Some(make_openbmp_header(&header)),
header.timestamp,
);
serde_json::to_string(&result)
.map(Some)
.map_err(|e| e.to_string())
}
fn parse_bmp_message_core(data: &[u8], timestamp: f64) -> Result<String, String> {
let mut bytes = Bytes::from(data.to_vec());
let bmp_msg = parse_bmp_msg(&mut bytes).map_err(|e| e.to_string())?;
let result = build_bmp_result(bmp_msg, None, timestamp);
serde_json::to_string(&result).map_err(|e| e.to_string())
}
#[derive(serde::Serialize)]
struct MrtRecordResult {
elems: Vec<BgpElem>,
#[serde(rename = "bytesRead")]
bytes_read: u32,
}
use std::cell::RefCell;
thread_local! {
static MRT_ELEMENTOR: RefCell<Elementor> = RefCell::new(Elementor::default());
}
fn parse_mrt_record_core(data: &[u8]) -> Result<String, String> {
if data.is_empty() {
return Ok(String::new());
}
let mut cursor = Cursor::new(data);
let record = match parse_mrt_record(&mut cursor) {
Ok(r) => r,
Err(_) => return Ok(String::new()), };
let bytes_read = cursor.position() as u32;
let elems = MRT_ELEMENTOR.with(|e| e.borrow_mut().record_to_elems(record));
let result = MrtRecordResult { elems, bytes_read };
serde_json::to_string(&result).map_err(|e| e.to_string())
}
fn parse_bgp_update_core(data: &[u8]) -> Result<String, String> {
let mut bytes = Bytes::from(data.to_vec());
let msg =
parse_bgp_message(&mut bytes, false, &AsnLength::Bits32).map_err(|e| e.to_string())?;
let elems = Elementor::bgp_to_elems(
msg,
0.0,
&IpAddr::V4(Ipv4Addr::UNSPECIFIED),
&Asn::default(),
);
serde_json::to_string(&elems).map_err(|e| e.to_string())
}
fn parse_ris_live_message_json_core(msg_str: &str) -> Result<String, String> {
let elems = parse_ris_live_message(msg_str).map_err(|e| e.to_string())?;
serde_json::to_string(&elems).map_err(|e| e.to_string())
}
fn parse_ris_live_message_raw_core(msg_str: &str) -> Result<RisLiveRawFull, String> {
parse_ris_live_message_raw_full(msg_str).map_err(|e| e.to_string())
}
#[derive(serde::Serialize)]
struct BgpUpdateFull {
elems: Vec<BgpElem>,
attributes: Attributes,
#[serde(rename = "validationWarnings")]
validation_warnings: Vec<BgpValidationWarning>,
}
fn parse_bgp_update_full_core(data: &[u8]) -> Result<String, String> {
let mut bytes = Bytes::from(data.to_vec());
let msg =
parse_bgp_message(&mut bytes, false, &AsnLength::Bits32).map_err(|e| e.to_string())?;
let (attributes, validation_warnings) = match &msg {
BgpMessage::Update(update) => (
update.attributes.clone(),
update.attributes.validation_warnings().to_vec(),
),
_ => (Attributes::default(), vec![]),
};
let elems = Elementor::bgp_to_elems(
msg,
0.0,
&IpAddr::V4(Ipv4Addr::UNSPECIFIED),
&Asn::default(),
);
let result = BgpUpdateFull {
elems,
attributes,
validation_warnings,
};
serde_json::to_string(&result).map_err(|e| e.to_string())
}
fn dissect_bgp_message_core(data: &[u8], four_byte_asn: bool) -> Result<String, String> {
let asn_len = if four_byte_asn {
AsnLength::Bits32
} else {
AsnLength::Bits16
};
let tree = dissect_bgp_message(data, &asn_len, false);
serde_json::to_string(&tree).map_err(|e| e.to_string())
}
#[derive(serde::Serialize)]
struct DissectMrtResult {
tree: DissectionNode,
#[serde(rename = "bytesRead")]
bytes_read: u32,
}
fn dissect_mrt_record_core(data: &[u8]) -> Result<String, String> {
if data.is_empty() {
return Ok(String::new());
}
let mut cursor = Cursor::new(data);
let raw = match chunk_mrt_record(&mut cursor) {
Ok(raw) => raw,
Err(_) => return Ok(String::new()),
};
let bytes_read = cursor.position() as u32;
let tree = dissect_mrt_record(&raw);
let result = DissectMrtResult { tree, bytes_read };
serde_json::to_string(&result).map_err(|e| e.to_string())
}
#[wasm_bindgen(js_name = "parseOpenBmpMessage")]
pub fn parse_openbmp_message(data: &[u8]) -> Result<String, JsError> {
match parse_openbmp_message_core(data) {
Ok(Some(json)) => Ok(json),
Ok(None) => Ok(String::new()),
Err(e) => Err(JsError::new(&e)),
}
}
#[wasm_bindgen(js_name = "parseBmpMessage")]
pub fn parse_bmp_message(data: &[u8], timestamp: f64) -> Result<String, JsError> {
parse_bmp_message_core(data, timestamp).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "resetMrtParser")]
pub fn reset_mrt_parser() {
MRT_ELEMENTOR.with(|e| *e.borrow_mut() = Elementor::default());
}
#[wasm_bindgen(js_name = "parseMrtRecord")]
pub fn parse_mrt_record_wasm(data: &[u8]) -> Result<String, JsError> {
parse_mrt_record_core(data).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "parseBgpUpdate")]
pub fn parse_bgp_update(data: &[u8]) -> Result<String, JsError> {
parse_bgp_update_core(data).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "parseRisLiveMessageJson")]
pub fn parse_ris_live_message_json_wasm(msg_str: &str) -> Result<String, JsError> {
parse_ris_live_message_json_core(msg_str).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "parseRisLiveMessageRaw")]
pub fn parse_ris_live_message_raw_wasm(msg_str: &str) -> Result<String, JsError> {
let full = parse_ris_live_message_raw_core(msg_str).map_err(|e| JsError::new(&e))?;
serde_json::to_string(&full).map_err(|e| JsError::new(&e.to_string()))
}
#[wasm_bindgen(js_name = "parseBgpUpdateFull")]
pub fn parse_bgp_update_full(data: &[u8]) -> Result<String, JsError> {
parse_bgp_update_full_core(data).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "dissectBgpMessage")]
pub fn dissect_bgp_message_wasm(data: &[u8], four_byte_asn: bool) -> Result<String, JsError> {
dissect_bgp_message_core(data, four_byte_asn).map_err(|e| JsError::new(&e))
}
#[wasm_bindgen(js_name = "dissectMrtRecord")]
pub fn dissect_mrt_record_wasm(data: &[u8]) -> Result<String, JsError> {
dissect_mrt_record_core(data).map_err(|e| JsError::new(&e))
}
#[cfg(test)]
mod tests {
use super::*;
fn make_bmp_initiation(sys_name: &str) -> Vec<u8> {
let tlv_type: u16 = 2; let tlv_len = sys_name.len() as u16;
let total_len = 6 + 4 + sys_name.len();
let mut buf = Vec::with_capacity(total_len);
buf.push(3); buf.extend_from_slice(&(total_len as u32).to_be_bytes()); buf.push(4); buf.extend_from_slice(&tlv_type.to_be_bytes());
buf.extend_from_slice(&tlv_len.to_be_bytes());
buf.extend_from_slice(sys_name.as_bytes());
buf
}
fn make_bmp_peer_up() -> Vec<u8> {
let peer_header_len = 42;
let bgp_open_len = 29u16;
let body_len = 16 + 2 + 2 + (bgp_open_len as usize) * 2;
let total_len = 6 + peer_header_len + body_len;
let mut buf = Vec::with_capacity(total_len);
buf.push(3); buf.extend_from_slice(&(total_len as u32).to_be_bytes());
buf.push(3);
buf.push(0); buf.push(0); buf.extend_from_slice(&[0u8; 8]); buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&[10, 0, 0, 1]); buf.extend_from_slice(&65000u32.to_be_bytes()); buf.extend_from_slice(&[10, 0, 0, 1]); buf.extend_from_slice(&1000u32.to_be_bytes()); buf.extend_from_slice(&0u32.to_be_bytes());
buf.extend_from_slice(&[0u8; 12]); buf.extend_from_slice(&[192, 168, 1, 1]); buf.extend_from_slice(&179u16.to_be_bytes()); buf.extend_from_slice(&12345u16.to_be_bytes());
let open = make_bgp_open(65000, [10, 0, 0, 1]);
buf.extend_from_slice(&open);
buf.extend_from_slice(&open);
buf
}
fn make_bgp_open(my_as: u16, bgp_id: [u8; 4]) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&[0xFF; 16]); buf.extend_from_slice(&29u16.to_be_bytes()); buf.push(1); buf.push(4); buf.extend_from_slice(&my_as.to_be_bytes());
buf.extend_from_slice(&180u16.to_be_bytes()); buf.extend_from_slice(&bgp_id);
buf.push(0); buf
}
fn make_bgp_update_announce(prefix: [u8; 4], prefix_len: u8, next_hop: [u8; 4]) -> Vec<u8> {
let origin_attr = [0x40, 1, 1, 0]; #[rustfmt::skip]
let as_path_attr = [
0x40, 2, 6, 2, 1, 0, 0, 0xFD, 0xE9, ];
let next_hop_attr = [
0x40,
3,
4,
next_hop[0],
next_hop[1],
next_hop[2],
next_hop[3],
];
let prefix_bytes = (prefix_len as usize).div_ceil(8);
let nlri_len = 1 + prefix_bytes; let path_attr_len = origin_attr.len() + as_path_attr.len() + next_hop_attr.len();
let body_len = 2 + 2 + path_attr_len + nlri_len; let total_len = 19 + body_len;
let mut buf = Vec::new();
buf.extend_from_slice(&[0xFF; 16]); buf.extend_from_slice(&(total_len as u16).to_be_bytes());
buf.push(2); buf.extend_from_slice(&0u16.to_be_bytes()); buf.extend_from_slice(&(path_attr_len as u16).to_be_bytes());
buf.extend_from_slice(&origin_attr);
buf.extend_from_slice(&as_path_attr);
buf.extend_from_slice(&next_hop_attr);
buf.push(prefix_len);
buf.extend_from_slice(&prefix[..prefix_bytes]);
buf
}
#[test]
fn test_parse_bmp_initiation() {
let data = make_bmp_initiation("test-router");
let json = parse_bmp_message_core(&data, 1000.0).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["type"], "InitiationMessage");
assert_eq!(v["timestamp"], 1000.0);
assert!(v["openBmpHeader"].is_null());
assert_eq!(v["tlvs"][0]["type"], "SysName");
assert_eq!(v["tlvs"][0]["value"], "test-router");
}
#[test]
fn test_parse_bmp_peer_up() {
let data = make_bmp_peer_up();
let json = parse_bmp_message_core(&data, 1000.0).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["type"], "PeerUpNotification");
assert_eq!(v["peerHeader"]["peerIp"], "10.0.0.1");
assert_eq!(v["peerHeader"]["peerAsn"], 65000);
assert_eq!(v["localIp"], "192.168.1.1");
assert_eq!(v["localPort"], 179);
assert_eq!(v["remotePort"], 12345);
}
#[test]
fn test_parse_bmp_invalid_data() {
let result = parse_bmp_message_core(&[0, 1, 2], 0.0);
assert!(result.is_err());
}
#[test]
fn test_parse_bgp_update() {
let data = make_bgp_update_announce([10, 0, 0, 0], 24, [192, 168, 1, 1]);
let json = parse_bgp_update_core(&data).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(v.is_array());
let elems = v.as_array().unwrap();
assert_eq!(elems.len(), 1);
assert_eq!(elems[0]["type"], "ANNOUNCE");
assert_eq!(elems[0]["prefix"], "10.0.0.0/24");
assert_eq!(elems[0]["next_hop"], "192.168.1.1");
}
#[test]
fn test_parse_bgp_update_invalid() {
let result = parse_bgp_update_core(&[0xFF; 16]);
assert!(result.is_err());
}
const RIS_LIVE_MSG: &str = r#"{
"type": "ris_message",
"data": {
"timestamp": 1636245154.8,
"peer": "2001:7f8:b:100:1d1:a520:1333:74",
"peer_asn": "201333",
"id": "10-183678-175313836",
"host": "rrc10",
"type": "UPDATE",
"path": [201333, 6762, 174, 20473],
"community": [[6762, 30], [6762, 14400]],
"origin": "igp",
"announcements": [
{"next_hop": "2001:7f8:b:100:1d1:a520:1333:74", "prefixes": ["2a0e:97c7::/48"]}
],
"raw": "FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF0086020000006F4001010040021202040003127500001A6A000000AE00004FF980040400000000C008081A6A001E1A6A3840800E4100020120200107F8000B010001D1A52013330074FE800000000000000217A3FFFEFE290500302A0E97C70000302A0E97C600FE302A10CC4217B7302A10CC421FEB"
}
}"#;
#[test]
fn test_parse_ris_live_message_json() {
let json = parse_ris_live_message_json_core(RIS_LIVE_MSG).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let elems = v.as_array().unwrap();
assert_eq!(elems.len(), 1);
assert_eq!(elems[0]["type"], "ANNOUNCE");
assert_eq!(elems[0]["prefix"], "2a0e:97c7::/48");
assert_eq!(elems[0]["peer_asn"], 201333);
}
#[test]
fn test_parse_ris_live_message_raw() {
let json = parse_ris_live_message_raw_core(RIS_LIVE_MSG)
.map(|full| serde_json::to_string(&full).unwrap())
.unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["meta"]["host"], "rrc10");
assert_eq!(v["meta"]["peerAsn"], 201333);
assert_eq!(v["meta"]["timestamp"], 1636245154.8);
let elems = v["elems"].as_array().unwrap();
assert_eq!(elems.len(), 4);
assert_eq!(elems[0]["type"], "ANNOUNCE");
assert_eq!(elems[0]["prefix"], "2a0e:97c7::/48");
let attributes = v["attributes"].as_array().unwrap();
assert!(attributes
.iter()
.any(|a| a["value"].get("Communities").is_some()));
assert!(attributes
.iter()
.any(|a| a["value"].get("MpReachNlri").is_some()));
assert!(v["validationWarnings"].is_array());
}
#[test]
fn test_parse_ris_live_message_raw_missing_raw() {
let msg = r#"{"type": "ris_message", "data": {
"timestamp": 1636245154.8, "peer": "192.0.2.1", "peer_asn": "64496",
"id": "00-192-0-2-1-1", "host": "rrc00", "type": "KEEPALIVE"}}"#;
assert!(parse_ris_live_message_raw_core(msg).is_err());
}
#[test]
fn test_parse_bgp_update_full() {
let v: serde_json::Value = serde_json::from_str(RIS_LIVE_MSG).unwrap();
let raw = hex::decode(v["data"]["raw"].as_str().unwrap()).unwrap();
let json = parse_bgp_update_full_core(&raw).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert!(!v["elems"].as_array().unwrap().is_empty());
let attributes = v["attributes"].as_array().unwrap();
assert!(attributes
.iter()
.any(|a| a["value"].get("MpReachNlri").is_some()));
assert!(attributes
.iter()
.any(|a| a["value"].get("Communities").is_some()));
assert!(v["validationWarnings"].is_array());
}
#[test]
fn test_dissect_bgp_message_core_tree() {
let v: serde_json::Value = serde_json::from_str(RIS_LIVE_MSG).unwrap();
let raw = hex::decode(v["data"]["raw"].as_str().unwrap()).unwrap();
let json = dissect_bgp_message_core(&raw, true).unwrap();
let tree: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(tree["field"], "bgp");
assert_eq!(tree["children"][0]["field"], "bgp.header");
assert_eq!(tree["children"][0]["children"][0]["offset"], 0);
assert_eq!(tree["children"][0]["children"][0]["length"], 16);
let fields: Vec<&str> = collect_fields(&tree);
assert!(fields.contains(&"bgp.attr.2"));
assert!(fields.contains(&"bgp.attr.14"));
assert!(fields.contains(&"bgp.attr.8"));
assert!(fields.contains(&"bgp.attr.mp_reach.next_hop"));
}
#[test]
fn test_dissect_bgp_message_core_partial_on_garbage() {
let json = dissect_bgp_message_core(&[0xFF; 7], true).unwrap();
let tree: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(tree["field"], "bgp");
assert_eq!(
tree["children"][0]["label"],
"Header (truncated, 7 of 19 bytes)"
);
}
#[test]
fn test_dissect_mrt_record_core() {
let v: serde_json::Value = serde_json::from_str(RIS_LIVE_MSG).unwrap();
let bgp = hex::decode(v["data"]["raw"].as_str().unwrap()).unwrap();
let mut body = Vec::new();
body.extend_from_slice(&201333u32.to_be_bytes());
body.extend_from_slice(&6762u32.to_be_bytes());
body.extend_from_slice(&0u16.to_be_bytes());
body.extend_from_slice(&2u16.to_be_bytes()); body.extend_from_slice(&[
0x20, 0x01, 0x07, 0xF8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x4E,
]); body.extend_from_slice(&[
0x20, 0x01, 0x07, 0xF8, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x4F,
]); body.extend_from_slice(&bgp);
let mut wire = Vec::new();
wire.extend_from_slice(&1_700_000_000u32.to_be_bytes());
wire.extend_from_slice(&16u16.to_be_bytes());
wire.extend_from_slice(&4u16.to_be_bytes());
wire.extend_from_slice(&(body.len() as u32).to_be_bytes());
wire.extend_from_slice(&body);
let json = dissect_mrt_record_core(&wire).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["bytesRead"], wire.len() as u64);
let fields: Vec<&str> = collect_fields(&v["tree"]);
assert!(fields.contains(&"mrt.header.type"));
assert!(fields.contains(&"mrt.bgp4mp.peer_asn"));
assert!(fields.contains(&"bgp.header.marker"));
assert!(fields.contains(&"bgp.attr.14"));
assert_eq!(dissect_mrt_record_core(&[]).unwrap(), "");
assert_eq!(dissect_mrt_record_core(&[0, 0, 0]).unwrap(), "");
}
fn collect_fields(value: &serde_json::Value) -> Vec<&str> {
let mut fields = Vec::new();
fn walk<'a>(value: &'a serde_json::Value, out: &mut Vec<&'a str>) {
if let Some(field) = value["field"].as_str() {
out.push(field);
}
if let Some(children) = value["children"].as_array() {
for child in children {
walk(child, out);
}
}
}
walk(value, &mut fields);
fields
}
#[test]
fn test_parse_ris_live_message_raw_malformed_json() {
assert!(parse_ris_live_message_raw_core("not json").is_err());
assert!(parse_ris_live_message_json_core("not json").is_err());
}
#[test]
fn test_parse_mrt_record_empty() {
let result = parse_mrt_record_core(&[]).unwrap();
assert_eq!(result, ""); }
#[test]
fn test_parse_mrt_record_invalid() {
let result = parse_mrt_record_core(&[0, 1, 2]).unwrap();
assert_eq!(result, ""); }
#[test]
fn test_parse_openbmp_unsupported() {
let mut data = Vec::new();
data.extend_from_slice(b"OBMP"); data.extend_from_slice(&1u16.to_be_bytes()); data.extend_from_slice(&[0; 6]); data.push(100); let result = parse_openbmp_message_core(&data);
match result {
Ok(None) => {} Err(_) => {} Ok(Some(_)) => panic!("expected None or Err for non-router OpenBMP message"),
}
}
#[test]
fn test_make_peer_header_default() {
let pph = BmpPerPeerHeader::default();
let header = make_peer_header(&pph);
assert_eq!(header.peer_asn, 0);
assert_eq!(header.peer_ip, "0.0.0.0");
assert!(!header.is_post_policy);
assert!(!header.is_adj_rib_out);
}
#[test]
fn test_bgp_update_without_otc_serializes_null() {
let data = make_bgp_update_announce([10, 0, 0, 0], 24, [192, 168, 1, 1]);
let json = parse_bgp_update_core(&data).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let elems = v.as_array().unwrap();
assert_eq!(elems.len(), 1);
let otc = &elems[0]["only_to_customer"];
assert!(
otc.is_null(),
"only_to_customer should be null for messages without OTC, got: {:?}",
otc
);
}
}
#[cfg(test)]
mod otc_tests {
use super::*;
#[test]
fn test_option_asn_serialization() {
let some_asn: Option<Asn> = Some(Asn::new_32bit(12345));
let json = serde_json::to_string(&some_asn).unwrap();
assert_eq!(json, "12345", "Some(Asn) should serialize as numeric value");
let none_asn: Option<Asn> = None;
let json = serde_json::to_string(&none_asn).unwrap();
assert_eq!(json, "null", "None should serialize as null");
let elem_with_otc = BgpElem {
only_to_customer: Some(Asn::new_32bit(12345)),
..Default::default()
};
let json = serde_json::to_string(&elem_with_otc).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v.get("only_to_customer").unwrap(), 12345);
let elem_without_otc = BgpElem {
only_to_customer: None,
..Default::default()
};
let json = serde_json::to_string(&elem_without_otc).unwrap();
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
let otc_field = v.get("only_to_customer").unwrap();
assert!(
otc_field.is_null(),
"BgpElem without OTC should have null, got: {:?}",
otc_field
);
}
}