use crate::models::*;
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::mrt_elem::Elementor;
use crate::parser::mrt::mrt_record::parse_mrt_record;
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())
}
#[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))
}
#[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());
}
#[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
);
}
}