From 4fe0dc38bb65f749d55955ef9bd7780b6b2b4c68 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Thu, 13 Nov 2025 18:35:17 +0400 Subject: [PATCH 01/14] feat(status): add BMP FRR configuration Signed-off-by: Sergey Matov --- config/src/internal/routing/bgp.rs | 106 ++++++++++++++++++++++++++- routing/src/bmp/handler.rs | 29 ++++++++ routing/src/bmp/mod.rs | 5 ++ routing/src/bmp/server.rs | 114 +++++++++++++++++++++++++++++ routing/src/frr/renderer/bgp.rs | 56 +++++++++++++- routing/src/lib.rs | 3 + 6 files changed, 307 insertions(+), 6 deletions(-) create mode 100644 routing/src/bmp/handler.rs create mode 100644 routing/src/bmp/mod.rs create mode 100644 routing/src/bmp/server.rs diff --git a/config/src/internal/routing/bgp.rs b/config/src/internal/routing/bgp.rs index 0ce41e61b..d08f40bae 100644 --- a/config/src/internal/routing/bgp.rs +++ b/config/src/internal/routing/bgp.rs @@ -68,7 +68,7 @@ pub struct BgpNeighCapabilities { pub ext_nhop: bool, pub fqdn: bool, pub software_ver: bool, - //ORF + // TODO: ORF } #[derive(Clone, Debug)] @@ -179,6 +179,101 @@ impl Default for BgpOptions { } } +#[derive(Clone, Debug)] +pub enum BmpSource { + Address(IpAddr), + Interface(String), +} + +#[derive(Clone, Debug)] +pub struct BmpOptions { + /// Name for `bmp targets ` + pub target_name: String, + /// Collector host/IP in `bmp connect` + pub connect_host: String, + /// Collector TCP port + pub port: u16, + /// Optional local source (address or interface) + pub source: Option, + /// Optional reconnect backoff (ms) + pub min_retry_ms: Option, + pub max_retry_ms: Option, + /// `bmp stats interval` (ms) + pub stats_interval_ms: u64, + /// Monitoring toggles + pub monitor_ipv4_pre: bool, + pub monitor_ipv4_post: bool, + pub monitor_ipv6_pre: bool, + pub monitor_ipv6_post: bool, +} + +impl Default for BmpOptions { + fn default() -> Self { + Self { + target_name: "bmp1".to_string(), + connect_host: "127.0.0.1".to_string(), + port: 5000, + source: None, + min_retry_ms: Some(1_000), + max_retry_ms: Some(20_000), + stats_interval_ms: 60_000, + monitor_ipv4_pre: true, + monitor_ipv4_post: true, + monitor_ipv6_pre: false, + monitor_ipv6_post: false, + } + } +} + +impl BmpOptions { + #[must_use] + pub fn new, H: Into>( + target_name: T, + connect_host: H, + port: u16, + ) -> Self { + Self { + target_name: target_name.into(), + connect_host: connect_host.into(), + port, + ..Default::default() + } + } + #[must_use] + pub fn set_source_addr(mut self, ip: IpAddr) -> Self { + self.source = Some(BmpSource::Address(ip)); + self + } + #[must_use] + pub fn set_source_interface>(mut self, ifname: S) -> Self { + self.source = Some(BmpSource::Interface(ifname.into())); + self + } + #[must_use] + pub fn set_retry_ms(mut self, min_ms: u64, max_ms: u64) -> Self { + self.min_retry_ms = Some(min_ms); + self.max_retry_ms = Some(max_ms); + self + } + #[must_use] + pub fn set_stats_interval_ms(mut self, ms: u64) -> Self { + self.stats_interval_ms = ms; + self + } + #[must_use] + pub fn monitor_ipv4(mut self, pre: bool, post: bool) -> Self { + self.monitor_ipv4_pre = pre; + self.monitor_ipv4_post = post; + self + } + #[must_use] + pub fn monitor_ipv6(mut self, pre: bool, post: bool) -> Self { + self.monitor_ipv6_pre = pre; + self.monitor_ipv6_post = post; + self + } +} + #[derive(Clone, Debug, Default)] /// A BGP instance config, within a certain VRF pub struct BgpConfig { @@ -190,6 +285,7 @@ pub struct BgpConfig { pub af_ipv4unicast: Option, pub af_ipv6unicast: Option, pub af_l2vpnevpn: Option, + pub bmp: Option, } /* ===== impls: Builders ===== */ @@ -229,7 +325,6 @@ impl AfIpv4Ucast { pub fn set_vrf_imports(&mut self, imports: VrfImports) { self.imports = Some(imports); } - // redistribution is configured by adding one or more redistribute objects pub fn redistribute(&mut self, redistribute: Redistribute) { self.redistribute.push(redistribute); } @@ -574,6 +669,7 @@ impl BgpOptions { self } } + impl BgpConfig { #[must_use] pub fn new(asn: u32) -> Self { @@ -607,4 +703,10 @@ impl BgpConfig { pub fn set_af_ipv6unicast(&mut self, af_ipv6unicast: AfIpv6Ucast) { self.af_ipv6unicast = Some(af_ipv6unicast); } + + /* NEW: attach BMP options */ + pub fn set_bmp_options(&mut self, bmp: BmpOptions) -> &Self { + self.bmp = Some(bmp); + self + } } diff --git a/routing/src/bmp/handler.rs b/routing/src/bmp/handler.rs new file mode 100644 index 000000000..37b2a7d91 --- /dev/null +++ b/routing/src/bmp/handler.rs @@ -0,0 +1,29 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +use async_trait::async_trait; +use netgauze_bmp_pkt::BmpMessage; + +#[async_trait] +pub trait BmpHandler: Send + Sync + 'static { + /// Called for every well-formed BMP message. + async fn on_message(&self, peer: std::net::SocketAddr, msg: BmpMessage); + + /// Called when a connection terminates (EOF / error). + async fn on_disconnect(&self, peer: std::net::SocketAddr, reason: &str) { + let _ = (peer, reason); // no-op + } +} + +pub struct JsonLogHandler; + +#[async_trait::async_trait] +impl BmpHandler for JsonLogHandler { + async fn on_message(&self, peer: std::net::SocketAddr, msg: BmpMessage) { + // BmpMessage implements serde, so this is safe: + match serde_json::to_string(&msg) { + Ok(line) => println!(r#"{{"peer":"{}","bmp":{}}}"#, peer, line), + Err(e) => eprintln!("serialize error from {}: {e}", peer), + } + } +} diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs new file mode 100644 index 000000000..22c72cbbf --- /dev/null +++ b/routing/src/bmp/mod.rs @@ -0,0 +1,5 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +pub mod server; +pub mod handler; \ No newline at end of file diff --git a/routing/src/bmp/server.rs b/routing/src/bmp/server.rs new file mode 100644 index 000000000..8cf36e7a5 --- /dev/null +++ b/routing/src/bmp/server.rs @@ -0,0 +1,114 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +use anyhow::{Context, Result}; +use bytes::BytesMut; +use netgauze_bmp_pkt::codec::BmpCodec; +use netgauze_bmp_pkt::BmpMessage; +use tokio::net::{TcpListener, TcpStream}; +use tokio::task::JoinSet; +use tokio_util::codec::FramedRead; + +use crate::bmp::handler::BmpHandler; + +#[derive(Clone, Debug)] +pub struct BmpServerConfig { + pub bind_addr: std::net::SocketAddr, + pub tcp_nodelay: bool, + pub tcp_recv_buf: Option, + pub max_conns: Option, +} + +impl Default for BmpServerConfig { + fn default() -> Self { + Self { + bind_addr: "0.0.0.0:5000".parse().unwrap(), + tcp_nodelay: true, + tcp_recv_buf: Some(1 << 20), // 1 MiB + max_conns: None, + } + } +} + +pub struct BmpServer { + cfg: BmpServerConfig, + handler: H, +} + +impl BmpServer { + pub fn new(cfg: BmpServerConfig, handler: H) -> Self { + Self { cfg, handler } + } + + pub async fn run(self) -> Result<()> { + let listener = TcpListener::bind(self.cfg.bind_addr) + .await + .with_context(|| format!("bind {}", self.cfg.bind_addr))?; + tracing::info!("BMP server listening on {}", self.cfg.bind_addr); + + let mut tasks = JoinSet::new(); + let mut active = 0usize; + + loop { + let (sock, peer) = listener.accept().await?; + if let Some(cap) = self.cfg.max_conns { + if active >= cap { + tracing::warn!("rejecting {} (max_conns reached)", peer); + continue; + } + } + active += 1; + let cfg = self.cfg.clone(); + let handler = &self.handler; + let handler = handler; // capture by move below + tasks.spawn(handle_peer(sock, peer, cfg, handler)); + // Periodically reap finished tasks + while let Some(ready) = tasks.try_join_next()? { + ready?; + active = active.saturating_sub(1); + } + } + } +} + +async fn handle_peer( + mut sock: TcpStream, + peer: std::net::SocketAddr, + cfg: BmpServerConfig, + handler: &H, +) -> Result<()> { + if cfg.tcp_nodelay { + let _ = sock.set_nodelay(true); + } + if let Some(sz) = cfg.tcp_recv_buf { + let _ = sock.set_recv_buffer_size(sz); + } + + // Framed BMP stream using NetGauze’s codec + let codec = BmpCodec::default(); + let reader = FramedRead::new(sock, codec); + + tokio::pin!(reader); + + // Use a scratch buffer for zero-copy clones if needed + let mut _scratch = BytesMut::new(); + + use futures_util::StreamExt; + let mut reader = reader; + while let Some(frame) = reader.next().await { + match frame { + Ok(BmpMessage::V3(msg)) => { + handler.on_message(peer, BmpMessage::V3(msg)).await; + } + Ok(BmpMessage::V4(msg)) => { + handler.on_message(peer, BmpMessage::V4(msg)).await; + } + Err(e) => { + handler.on_disconnect(peer, &format!("decode error: {e}")).await; + return Ok(()); + } + } + } + handler.on_disconnect(peer, "eof").await; + Ok(()) +} diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index e64dee4af..451108ab4 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -12,6 +12,7 @@ use config::internal::routing::bgp::Redistribute; use config::internal::routing::bgp::VrfImports; use config::internal::routing::bgp::{AfIpv4Ucast, AfIpv6Ucast, AfL2vpnEvpn}; use config::internal::routing::bgp::{BgpNeighCapabilities, Protocol}; +use config::internal::routing::bgp::{BmpOptions, BmpSource}; /* impl Display */ impl Rendered for BgpNeighType { @@ -54,6 +55,50 @@ impl Rendered for Protocol { } } +impl Render for BmpOptions { + type Context = (); + type Output = ConfigBuilder; + fn render(&self, _: &Self::Context) -> Self::Output { + let mut cfg = ConfigBuilder::new(); + cfg += MARKER; + cfg += format!("bmp targets {}", self.target_name); + + // connect line + let mut connect = format!(" bmp connect {} port {}", self.connect_host, self.port); + if let (Some(minr), Some(maxr)) = (self.min_retry_ms, self.max_retry_ms) { + connect.push_str(&format!(" min-retry {minr} max-retry {maxr}")); + } + if let Some(src) = &self.source { + match src { + BmpSource::Address(ip) => connect.push_str(&format!(" source {}", ip)), + BmpSource::Interface(ifn) => connect.push_str(&format!(" source {}", ifn)), + } + } + cfg += connect; + + // monitors + if self.monitor_ipv4_pre { + cfg += " bmp monitor ipv4 unicast pre-policy"; + } + if self.monitor_ipv4_post { + cfg += " bmp monitor ipv4 unicast post-policy"; + } + if self.monitor_ipv6_pre { + cfg += " bmp monitor ipv6 unicast pre-policy"; + } + if self.monitor_ipv6_post { + cfg += " bmp monitor ipv6 unicast post-policy"; + } + + // stats + cfg += format!(" bmp stats interval {}", self.stats_interval_ms); + + cfg += "exit"; + cfg += MARKER; + cfg + } +} + /* utils to render BGP neighbor configs */ fn bgp_neigh_minimal(neigh: &BgpNeighbor, name: &str) -> String { let mut out; @@ -207,7 +252,7 @@ fn bgp_neigh_bool_switches(neigh: &BgpNeighbor, prefix: &str) -> ConfigBuilder { /* extended link bw */ if neigh.remove_private_as { - cfg += format!(" {prefix} remove-private-AS"); + cfg += format!(" {prefix} remove-private-AS");ß } /* extended link bw */ @@ -277,7 +322,6 @@ impl Render for AfIpv4Ucast { cfg += MARKER; cfg += "address-family ipv4 unicast"; - /* activate neighbors in AF */ bgp.neighbors .iter() .filter(|neigh| neigh.ipv4_unicast) @@ -313,7 +357,6 @@ impl Render for AfIpv6Ucast { cfg += MARKER; cfg += "address-family ipv6 unicast"; - /* activate neighbors in AF */ bgp.neighbors .iter() .filter(|neigh| neigh.ipv6_unicast) @@ -349,7 +392,6 @@ impl Render for AfL2vpnEvpn { cfg += MARKER; cfg += "address-family l2vpn evpn"; - /* activate neighbors in AF */ bgp.neighbors .iter() .filter(|neigh| neigh.l2vpn_evpn) @@ -384,6 +426,7 @@ impl Render for AfL2vpnEvpn { if self.default_originate_ipv6 { cfg += " default-originate ipv6"; } + cfg += "exit-address-family"; cfg += MARKER; cfg @@ -480,6 +523,11 @@ impl Render for BgpConfig { .as_ref() .map(|evpn| config += evpn.render(self)); + /* BMP options */ + if let Some(bmp) = &self.bmp { + config += bmp.render(&()); + } + config += "exit"; config += MARKER; config diff --git a/routing/src/lib.rs b/routing/src/lib.rs index 8adcc6a13..0bb3eb253 100644 --- a/routing/src/lib.rs +++ b/routing/src/lib.rs @@ -12,6 +12,7 @@ )] mod atable; +mod bmp; mod cli; mod config; mod errors; @@ -27,6 +28,8 @@ mod routingdb; // re-exports pub use atable::atablerw::AtableReader; +pub use bmp::handler::JsonLogHandler; +pub use bmp::server::{BmpServer, BmpServerConfig}; pub use config::RouterConfig; pub use errors::RouterError; pub use evpn::Vtep; From bee2fee967b321d3d7914d66ff8731b75740588c Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Wed, 3 Dec 2025 19:32:11 +0400 Subject: [PATCH 02/14] feat(status): add bmp v3 renderer Signed-off-by: Sergey Matov --- routing/src/bmp/bmp_render.rs | 263 ++++++++++++++++++++++++++++++++++ routing/src/bmp/mod.rs | 3 +- 2 files changed, 265 insertions(+), 1 deletion(-) create mode 100644 routing/src/bmp/bmp_render.rs diff --git a/routing/src/bmp/bmp_render.rs b/routing/src/bmp/bmp_render.rs new file mode 100644 index 000000000..d1b4ea42f --- /dev/null +++ b/routing/src/bmp/bmp_render.rs @@ -0,0 +1,263 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +use std::collections::HashMap; +use std::net::IpAddr; + +use netgauze_bgp_pkt::{ + nlri::{MPReachNLRI, MPUnreachNLRI}, + wire::deserializer::nlri::RouteType, + BgpMessage, +}; +use netgauze_bmp_pkt::{ + peer::{PeerHeader, PeerType}, + v3::BmpMessageValue as V3, + BmpMessage, +}; + +use crate::config::internal::status::{ + BgpMessageCounters, BgpMessages, BgpNeighborPrefixes, BgpNeighborSessionState, + BgpNeighborStatus, BgpStatus, BgpVrfStatus, DataplaneStatus, +}; + +pub fn hande_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { + // smatov: frr bmp v3 only for now + // but we maybe will extend it later + if let BmpMessage::V3(v) = msg { + handle(status, v); + } +} + + +fn handle(status: &mut DataplaneStatus, m: &V3) { + match m { + V3::PeerUp(n) => { + let (vrf, key) = peer_keys(&n.common_header.peer_header); + let st = ensure_neighbor(status, &vrf, &key); + st.enabled = true; + st.session_state = BgpNeighborSessionState::Established; + st.established_transitions = st.established_transitions.saturating_add(1); + st.peer_as = n.common_header.peer_as().into(); + st.local_as = n.common_header.local_as().into(); + st.remote_router_id = n.common_header.router_id().to_string(); + st.peer_port = n.local_port as u32; + + // PeerUp carries both Open messages (rx+tx) so count both. + bump_msg(&mut st.messages, true, BgpMsgKind::Open); + bump_msg(&mut st.messages, false, BgpMsgKind::Open); + } + V3::PeerDown(n) => { + let (vrf, key) = peer_keys(&n.common_header.peer_header); + let st = ensure_neighbor(status, &vrf, &key); + st.session_state = BgpNeighborSessionState::Idle; + st.last_reset_reason = peer_down_reason_v3(n); + st.connections_dropped = st.connections_dropped.saturating_add(1); + } + V3::RouteMonitoring(rm) => { + let (vrf, key) = peer_keys(&rm.common_header.peer_header); + let st = ensure_neighbor(status, &vrf, &key); + let post_policy = is_post_policy(&rm.common_header.peer_header); + + apply_bgp_pdu( + &mut st.messages, + &mut st.ipv4_unicast_prefixes, + &mut st.ipv6_unicast_prefixes, + &rm.bgp_message, + post_policy, + ); + } + V3::StatisticsReport(_sr) => { + // noop for status for now + } + V3::Initiation(_) | V3::Termination(_) | V3::RouteMirroring(_) => { + // no-op for status + } + } +} + +fn peer_keys(ph: &PeerHeader) -> (String, String) { + let vrf = match ph.peer_type() { + PeerType::GlobalInstance | PeerType::L3vpn => { + if let Some(d) = ph.peer_distinguisher() { + format!("pdx:{d}") // smatov: later add translation from VNI to VRF name here + } else { + "default".to_string() + } + } + _ => "default".to_string(), + }; + let key = match ph.peer_address() { + IpAddr::V4(a) => a.to_string(), + IpAddr::V6(a) => a.to_string(), + }; + (vrf, key) +} + +fn ensure_neighbor<'a>( + status: &'a mut DataplaneStatus, + vrf: &str, + key: &str, +) -> &'a mut BgpNeighborStatus { + if status.bgp.is_none() { + status.set_bgp(BgpStatus { vrfs: HashMap::new() }); + } + let bgp = status.bgp.as_mut().unwrap(); + let vrf_entry = bgp + .vrfs + .entry(vrf.to_string()) + .or_insert_with(|| BgpVrfStatus { neighbors: HashMap::new() }); + vrf_entry.neighbors.entry(key.to_string()).or_insert_with(|| BgpNeighborStatus { + enabled: true, + local_as: 0, + peer_as: 0, + peer_port: 0, + peer_group: String::new(), + remote_router_id: String::new(), + session_state: BgpNeighborSessionState::Idle, + connections_dropped: 0, + established_transitions: 0, + last_reset_reason: String::new(), + messages: Some(BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + }), + ipv4_unicast_prefixes: Some(BgpNeighborPrefixes::default()), + ipv6_unicast_prefixes: Some(BgpNeighborPrefixes::default()), + l2vpn_evpn_prefixes: None, + }) +} + +fn is_post_policy(ph: &PeerHeader) -> bool { + ph.is_post_policy() +} + +#[derive(Clone, Copy)] +enum BgpMsgKind { + Open, + Keepalive, + Notification, + Update, + RouteRefresh, + Capability, +} + +fn bump_msg(messages: &mut Option, received: bool, kind: BgpMsgKind) { + let m = messages.get_or_insert(BgpMessages { received: None, sent: None }); + let ctrs = if received { + m.received.get_or_insert(BgpMessageCounters::new()) + } else { + m.sent.get_or_insert(BgpMessageCounters::new()) + }; + match kind { + BgpMsgKind::Open => ctrs.open = ctrs.open.saturating_add(1), + BgpMsgKind::Keepalive => ctrs.keepalive = ctrs.keepalive.saturating_add(1), + BgpMsgKind::Notification => ctrs.notification = ctrs.notification.saturating_add(1), + BgpMsgKind::Update => ctrs.update = ctrs.update.saturating_add(1), + BgpMsgKind::RouteRefresh => ctrs.route_refresh = ctrs.route_refresh.saturating_add(1), + BgpMsgKind::Capability => ctrs.capability = ctrs.capability.saturating_add(1), + } +} + +fn apply_bgp_pdu( + messages: &mut Option, + v4pfx: &mut Option, + v6pfx: &mut Option, + pdu: &BgpMessage, + post_policy: bool, +) { + match pdu { + BgpMessage::Open(_) => bump_msg(messages, true, BgpMsgKind::Open), + BgpMessage::KeepAlive => bump_msg(messages, true, BgpMsgKind::Keepalive), + BgpMessage::Notification(_) => bump_msg(messages, true, BgpMsgKind::Notification), + BgpMessage::RouteRefresh(_) => bump_msg(messages, true, BgpMsgKind::RouteRefresh), + BgpMessage::Update(upd) => { + bump_msg(messages, true, BgpMsgKind::Update); + + // default nlri v4 + let a4 = upd.nlri.len() as u32; + if a4 > 0 { + let v = v4pfx.get_or_insert_with(Default::default); + if post_policy { + v.sent = v.sent.saturating_add(a4); + } else { + v.received = v.received.saturating_add(a4); + v.received_pre_policy = v.received_pre_policy.saturating_add(a4); + } + } + + // unreach nlri v4 + for attr in &upd.path_attributes { + if let Some(mp) = attr.get_mp_reach_nlri() { + account_mp_reach(mp, post_policy, v4pfx, v6pfx); + } + if let Some(mpu) = attr.get_mp_unreach_nlri() { + account_mp_unreach(mpu, post_policy, v4pfx, v6pfx); + } + } + } + BgpMessage::Unknown(_) => { /* todo: what should we do here? */ } + } +} + +fn account_mp_reach( + mp: &MPReachNLRI, + post_policy: bool, + v4pfx: &mut Option, + v6pfx: &mut Option, +) { + let cnt = mp.nlri().len() as u32; + if cnt == 0 { return; } + match mp.route_type() { + RouteType::Ipv4Unicast => { + let v = v4pfx.get_or_insert_with(Default::default); + if post_policy { v.sent = v.sent.saturating_add(cnt); } + else { + v.received = v.received.saturating_add(cnt); + v.received_pre_policy = v.received_pre_policy.saturating_add(cnt); + } + } + RouteType::Ipv6Unicast => { + let v = v6pfx.get_or_insert_with(Default::default); + if post_policy { v.sent = v.sent.saturating_add(cnt); } + else { + v.received = v.received.saturating_add(cnt); + v.received_pre_policy = v.received_pre_policy.saturating_add(cnt); + } + } + _ => { /* todo: evpn */ } + } +} + +fn account_mp_unreach( + mpu: &MPUnreachNLRI, + _post_policy: bool, + v4pfx: &mut Option, + v6pfx: &mut Option, +) { + let cnt = mpu.nlri().len() as u32; + if cnt == 0 { return; } + match mpu.route_type() { + RouteType::Ipv4Unicast => { + let _v = v4pfx.get_or_insert_with(Default::default); + let _ = cnt; // smatov: add explicit counters later + } + RouteType::Ipv6Unicast => { + let _v = v6pfx.get_or_insert_with(Default::default); + let _ = cnt; + } + _ => {} + } +} + +fn peer_down_reason_v3(n: &netgauze_bmp_pkt::v3::PeerDownNotification) -> String { + use netgauze_bmp_pkt::v3::PeerDownReason as R; + match n.reason { + R::LocalSystemNotification => "local-notification".into(), + R::LocalSystemNoNotification => "local-no-notification".into(), + R::RemoteSystemNotification => "remote-notification".into(), + R::RemoteSystemNoNotification => "remote-no-notification".into(), + R::PeerDeconfigured => "peer-deconfigured".into(), + R::CommunicationLost => "communication-lost".into(), + R::Unknown(v) => format!("unknown({v})"), + } +} diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs index 22c72cbbf..2a5f556c4 100644 --- a/routing/src/bmp/mod.rs +++ b/routing/src/bmp/mod.rs @@ -2,4 +2,5 @@ // Copyright Open Network Fabric Authors pub mod server; -pub mod handler; \ No newline at end of file +pub mod handler; +pub mod bmp_render; \ No newline at end of file From 71f0b9f20d2eb952edecc5c907b64490c6c332a1 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Mon, 8 Dec 2025 12:44:55 +0400 Subject: [PATCH 03/14] feat(status): Populate routing and mgmt with BMP params Signed-off-by: Sergey Matov --- Cargo.lock | 86 ++++++++++++++++++++++++++++++ Cargo.toml | 5 ++ args/src/lib.rs | 68 ++++++++++++++++++++++- config/src/internal/routing/bgp.rs | 1 - dataplane/src/main.rs | 46 ++++++++++++++-- mgmt/src/processor/proc.rs | 50 ++++++++++++++--- routing/Cargo.toml | 2 + routing/src/bmp/bmp_render.rs | 77 +++++++++++++++----------- routing/src/bmp/mod.rs | 74 ++++++++++++++++++++++++- routing/src/bmp/server.rs | 6 ++- routing/src/frr/renderer/bgp.rs | 3 +- routing/src/router/mod.rs | 30 +++++++++++ 12 files changed, 400 insertions(+), 48 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 671534e48..76820f197 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1605,6 +1605,8 @@ dependencies = [ "linkme", "mio", "netdev", + "netgauze-bgp-pkt", + "netgauze-bmp-pkt", "procfs", "rand 0.9.2", "serde", @@ -3490,6 +3492,90 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "netgauze-bgp-pkt" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf8f40cdffa0b68a787a69ced281c74cad461853cb8142b5958132efe31a9dc" +dependencies = [ + "byteorder", + "ipnet", + "netgauze-iana", + "netgauze-locate", + "netgauze-parse-utils", + "netgauze-serde-macros", + "nom", + "serde", + "strum", + "strum_macros", +] + +[[package]] +name = "netgauze-bmp-pkt" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12cc433b4d77a27647e48b72111b8e5c5d9febe4be28322f44719a312b92f4e4" +dependencies = [ + "byteorder", + "bytes", + "chrono", + "either", + "ipnet", + "log", + "netgauze-bgp-pkt", + "netgauze-iana", + "netgauze-locate", + "netgauze-parse-utils", + "netgauze-serde-macros", + "nom", + "serde", + "strum", + "strum_macros", + "tokio-util", +] + +[[package]] +name = "netgauze-iana" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53f6ac1e8add680019185640a93f190533943d554430569aef2348633e9ea8a1" +dependencies = [ + "serde", + "serde_json", + "strum", + "strum_macros", +] + +[[package]] +name = "netgauze-locate" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1edd0fa84c8ec376ed1882bb8f5d237437e2570ab887add7183463d880b9d78" +dependencies = [ + "nom", +] + +[[package]] +name = "netgauze-parse-utils" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6acae71d0c268bbc1178115202dde191cdc706989ef478a4e52ab1cbe0c178d" +dependencies = [ + "netgauze-locate", + "nom", + "serde", +] + +[[package]] +name = "netgauze-serde-macros" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9348ab7cabcac94bedc3f514db83306bfda1c87e87452bdeeeffc933aa85f47" +dependencies = [ + "quote", + "syn 2.0.111", +] + [[package]] name = "netlink-packet-core" version = "0.8.1" diff --git a/Cargo.toml b/Cargo.toml index e780851ca..7c075f5cb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -81,6 +81,7 @@ vpcmap = { path = "./vpcmap", package = "dataplane-vpcmap", features = [] } # External afpacket = { version = "0.2.3", default-features = false, features = [] } ahash = { version = "0.8.12", default-features = false, features = [] } +anyhow = { version = "1.0.93", default-features = false, features = ["std"] } arc-swap = { version = "1.7.1", default-features = false, features = [] } arrayvec = { version = "0.7.6", default-features = false, features = [] } async-trait = { version = "0.1.89", default-features = false, features = [] } @@ -111,6 +112,7 @@ dyn-iter = { version = "1.0.1", default-features = false, features = [] } etherparse = { version = "0.19.0", default-features = false, features = [] } fixin = { git = "https://github.com/githedgehog/fixin", branch = "main", features = [] } futures = { version = "0.3.31", default-features = false, features = [] } +futures-util = { version = "0.3.31", default-features = false, features = ["std"] } hashbrown = { version = "0.16.1", default-features = false, features = [] } hwlocality = { version = "1.0.0-alpha.11", default-features = false, features = [] } hyper = { version = "1.8.1", default-features = false, features = [] } @@ -135,6 +137,8 @@ multi_index_map = { version = "0.15.0", default-features = false, features = [] n-vm = { git = "https://github.com/githedgehog/testn.git", tag = "v0.0.9", default-features = false, features = [], package = "n-vm" } netdev = { version = "0.39.0", default-features = false, features = [] } nix = { version = "0.30.1", default-features = false, features = [] } +netgauze-bgp-pkt = { version = "0.8.0", features = [] } +netgauze-bmp-pkt = { version = "0.8.0", features = ["codec"] } num-derive = { version = "0.4.2", default-features = false, features = [] } num-traits = { version = "0.2.19", default-features = false, features = [] } once_cell = { version = "1.21.3", default-features = false, features = [] } @@ -174,6 +178,7 @@ thiserror = { version = "2.0.17", default-features = false, features = [] } thread_local = { version = "1.1.9", default-features = false, features = [] } tokio = { version = "1.48.0", default-features = false, features = [] } tokio-stream = { version = "0.1.17", default-features = false, features = [] } +tokio-util = { version = "0.7.13", default-features = false, features = ["codec"] } tonic = { version = "0.14.2", default-features = false, features = [] } tracing = { version = "0.1.43", default-features = false, features = [] } tracing-error = { version = "0.2.1", default-features = false, features = [] } diff --git a/args/src/lib.rs b/args/src/lib.rs index 32f2db251..3ec96f67b 100644 --- a/args/src/lib.rs +++ b/args/src/lib.rs @@ -558,6 +558,25 @@ pub struct ConfigServerSection { pub address: GrpcAddress, } +/// BMP server configuration (optional; disabled when absent) +#[derive( + Debug, + PartialEq, + Eq, + serde::Serialize, + rkyv::Serialize, + rkyv::Deserialize, + rkyv::Archive, + CheckBytes, +)] +#[rkyv(attr(derive(PartialEq, Eq, Debug)))] +pub struct BmpConfigSection { + /// Bind address for the BMP server (IP:PORT) + pub address: SocketAddr, + /// Periodic housekeeping/flush interval in milliseconds + pub interval_ms: u64, +} + /// Complete dataplane launch configuration. /// /// This structure contains all configuration parameters needed to initialize and run @@ -574,7 +593,7 @@ pub struct ConfigServerSection { /// 3. **Transfer**: Passed via sealed memfd to the worker process /// 4. **Worker Process**: Calls [`LaunchConfiguration::inherit()`] to access the config /// -// TODO: implement bytecheck::Validate in addition to CheckBytes on all components of the launch config. +/// // TODO: implement bytecheck::Validate in addition to CheckBytes on all components of the launch config. #[derive( Debug, PartialEq, @@ -601,6 +620,8 @@ pub struct LaunchConfiguration { pub tracing: TracingConfigSection, /// Metrics collection configuration pub metrics: MetricsConfigSection, + /// Optional BMP server configuration (None => BMP disabled) + pub bmp: Option, /// Profiling configuration pub profiling: ProfilingConfigSection, } @@ -1152,6 +1173,14 @@ impl TryFrom for LaunchConfiguration { metrics: MetricsConfigSection { address: value.metrics_address(), }, + bmp: if value.bmp_enabled() { + Some(BmpConfigSection { + address: value.bmp_address(), + interval_ms: value.bmp_interval_ms(), + }) + } else { + None + }, profiling: ProfilingConfigSection { pyroscope_url: value.pyroscope_url().map(std::string::ToString::to_string), frequency: ProfilingConfigSection::DEFAULT_FREQUENCY, @@ -1275,6 +1304,28 @@ E.g. default=error,all=info,nat=debug will set the default target to error, and #[arg(long, help = "Set the name of this gateway")] name: Option, + + /// Enable BMP server + #[arg(long, default_value_t = false, help = "Enable BMP server")] + bmp_enable: bool, + + /// BMP bind address + #[arg( + long, + value_name = "IP:PORT", + default_value_t = SocketAddr::from(([0, 0, 0, 0], 5000)), + help = "Bind address for the BMP server" + )] + bmp_address: SocketAddr, + + /// BMP periodic interval for housekeeping/flush (ms) + #[arg( + long, + value_name = "MILLISECONDS", + default_value_t = 10_000, + help = "BMP periodic interval for housekeeping/flush (ms)" + )] + bmp_interval_ms: u64, } impl CmdArgs { @@ -1459,10 +1510,25 @@ impl CmdArgs { self.pyroscope_url.as_ref() } +<<<<<<< HEAD /// Get the name to configure this gateway with. #[must_use] pub fn get_name(&self) -> Option<&String> { self.name.as_ref() +======= + // ===== BMP getters ===== + #[must_use] + pub fn bmp_enabled(&self) -> bool { + self.bmp_enable + } + #[must_use] + pub fn bmp_address(&self) -> SocketAddr { + self.bmp_address + } + #[must_use] + pub fn bmp_interval_ms(&self) -> u64 { + self.bmp_interval_ms +>>>>>>> 7dac51ae (feat(status): Populate routing and mgmt with BMP params) } } diff --git a/config/src/internal/routing/bgp.rs b/config/src/internal/routing/bgp.rs index d08f40bae..32c9efcc2 100644 --- a/config/src/internal/routing/bgp.rs +++ b/config/src/internal/routing/bgp.rs @@ -704,7 +704,6 @@ impl BgpConfig { self.af_ipv6unicast = Some(af_ipv6unicast); } - /* NEW: attach BMP options */ pub fn set_bmp_options(&mut self, bmp: BmpOptions) -> &Self { self.bmp = Some(bmp); self diff --git a/dataplane/src/main.rs b/dataplane/src/main.rs index bc95d53a2..e770334e6 100644 --- a/dataplane/src/main.rs +++ b/dataplane/src/main.rs @@ -26,6 +26,11 @@ use tracectl::{custom_target, get_trace_ctl, trace_target}; use tracing::{error, info, level_filters::LevelFilter}; +use concurrency::syn::{Arc, RwLock}; +use config::internal::routing::bgp::BmpOptions; +use config::internal::status::DataplaneStatus; +use routing::BmpServerParams; + trace_target!("dataplane", LevelFilter::DEBUG, &[]); custom_target!("tonic", LevelFilter::ERROR, &[]); custom_target!("h2", LevelFilter::ERROR, &[]); @@ -94,6 +99,36 @@ fn main() { } init_logging(); + let (bmp_server_params, bmp_client_opts) = if args.bmp_enabled() { + let bind = args.bmp_address(); // SocketAddr + let interval_ms = args.bmp_interval_ms(); // u64 + + info!( + "BMP: enabled, listening on {bind}, interval={}ms", + interval_ms + ); + + // Server params for routing BMP listener + let server = BmpServerParams { + bind_addr: bind, + stats_interval_ms: interval_ms, + }; + + let host = bind.ip().to_string(); + let port = bind.port(); + let client = BmpOptions::new("bmp1", host, port) + .set_retry_ms(interval_ms, interval_ms.saturating_mul(4)) + .set_stats_interval_ms(interval_ms) + .monitor_ipv4(true, true); + + (Some(server), Some(client)) + } else { + info!("BMP: disabled"); + (None, None) + }; + + let dp_status: Arc> = Arc::new(RwLock::new(DataplaneStatus::new())); + let agent_running = args.pyroscope_url().and_then(|url| { match PyroscopeAgent::builder(url.as_str(), "hedgehog-dataplane") .backend(pprof_backend( @@ -116,6 +151,7 @@ fn main() { } } }); + process_tracing_cmds(&args); info!("Starting gateway process..."); @@ -137,18 +173,20 @@ fn main() { }; /* router parameters */ - let Ok(config) = RouterParamsBuilder::default() + let Ok(router_params) = RouterParamsBuilder::default() .cli_sock_path(args.cli_sock_path()) .cpi_sock_path(args.cpi_sock_path()) .frr_agent_path(args.frr_agent_path()) + .bmp(bmp_server_params) + .dp_status(dp_status.clone()) .build() else { error!("Bad router configuration"); panic!("Bad router configuration"); }; - // start the router; returns control-plane handles and a pipeline factory (Arc<... Fn() -> DynPipeline<_> >) - let setup = start_router(config).expect("failed to start router"); + // start the router; returns control-plane handles and a pipeline factory + let setup = start_router(router_params).expect("failed to start router"); MetricsServer::new(args.metrics_address(), setup.stats); @@ -166,6 +204,8 @@ fn main() { natallocatorw: setup.natallocatorw, vpcdtablesw: setup.vpcdtablesw, vpc_stats_store: setup.vpc_stats_store, + dp_status: dp_status.clone(), + bmp_client: bmp_client_opts, }, }) .expect("Failed to start management"); diff --git a/mgmt/src/processor/proc.rs b/mgmt/src/processor/proc.rs index ff106605f..b564de591 100644 --- a/mgmt/src/processor/proc.rs +++ b/mgmt/src/processor/proc.rs @@ -3,7 +3,7 @@ // !Configuration processor -use concurrency::sync::Arc; +use concurrency::sync::{Arc, RwLock}; use std::collections::{HashMap, HashSet}; use tokio::spawn; @@ -20,6 +20,8 @@ use config::{ConfigError, ConfigResult, stringify}; use config::{DeviceConfig, ExternalConfig, GenId, GwConfig, InternalConfig}; use config::{external::overlay::Overlay, internal::device::tracecfg::TracingConfig}; +use config::internal::routing::bgp::BmpOptions; + use crate::processor::confbuild::internal::build_internal_config; use crate::processor::confbuild::router::generate_router_config; use nat::stateful::NatAllocatorWriter; @@ -120,6 +122,12 @@ pub struct ConfigProcessorParams { // store for vpc stats pub vpc_stats_store: Arc, + + // read-handle to shared dataplane status + pub dp_status_r: Arc>, + + // BMP client options for FRR render (None if BMP is disabled) + pub bmp_client: Option, } impl ConfigProcessor { @@ -159,8 +167,18 @@ impl ConfigProcessor { return Err(ConfigError::ConfigAlreadyExists(genid)); } config.validate()?; - let internal = build_internal_config(&config)?; + + // Build internal config from external + let mut internal = build_internal_config(&config)?; + + // ── NEW: inject BMP client options (if provided) into every BGP instance ── + if let Some(ref bmp) = self.proc_params.bmp_client { + inject_bmp_into_internal(&mut internal, bmp.clone()); + } + + // store internal back into config config.set_internal_config(internal); + let e = match self.apply(config).await { Ok(()) => Ok(()), Err(e) => { @@ -178,7 +196,10 @@ impl ConfigProcessor { #[allow(unused)] async fn apply_blank_config(&mut self) -> ConfigResult { let mut blank = GwConfig::blank(); - let internal = build_internal_config(&blank)?; + let mut internal = build_internal_config(&blank)?; + if let Some(ref bmp) = self.proc_params.bmp_client { + inject_bmp_into_internal(&mut internal, bmp.clone()); + } blank.set_internal_config(internal); self.apply(blank).await } @@ -265,7 +286,10 @@ impl ConfigProcessor { /// RPC handler: get dataplane status async fn handle_get_dataplane_status(&mut self) -> ConfigResponse { - let mut status = DataplaneStatus::new(); + let mut status: DataplaneStatus = { + let guard = self.proc_params.dp_status_r.read(); + guard.clone() + }; let stats_store = &self.proc_params.vpc_stats_store; @@ -337,7 +361,8 @@ impl ConfigProcessor { status.add_vpc(name, v); } - // Emit only pair counters with traffic + // Replace peering counters with the latest snapshot (only pairs with traffic) + status.vpc_peering_counters.clear(); for ((src, dst), fs) in pairs_with_traffic { let src_name = name_of .get(&src) @@ -357,14 +382,15 @@ impl ConfigProcessor { dst_vpc: dst_name, packets: fs.ctr.packets, bytes: fs.ctr.bytes, - drops: 0, // TODO: Add this in a later release + drops: 0, pps: fs.rate.pps, bps: fs.rate.bps, }, ); } - // Emit per-VPC total counters (numeric; includes bytes) + // Replace per-VPC totals with the latest snapshot + status.vpc_counters.clear(); for (disc, fs) in vpc_snap { let name = name_of .get(&disc) @@ -656,3 +682,13 @@ async fn apply_gw_config( info!("Successfully applied config for genid {genid}"); Ok(()) } + +fn inject_bmp_into_internal(internal: &mut InternalConfig, bmp: BmpOptions) { + // We attach BMP options to every VRF that has a BGP instance. + // This keeps FRR render simple: wherever we render a 'router bgp ...' we have .bmp = Some(...) + for vrf in internal.vrfs_mut() { + if let Some(bgp) = vrf.bgp_mut() { + bgp.set_bmp_options(bmp.clone()); + } + } +} diff --git a/routing/Cargo.toml b/routing/Cargo.toml index d9f3b1afe..f6390f79b 100644 --- a/routing/Cargo.toml +++ b/routing/Cargo.toml @@ -29,6 +29,8 @@ ipnet = { workspace = true } left-right = { workspace = true } linkme = { workspace = true } mio = { workspace = true, features = ["os-ext", "net"] } +netgauze-bgp-pkt = { workspace = true } +netgauze-bmp-pkt = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } tokio = { workspace = true, features = ["rt", "sync"] } diff --git a/routing/src/bmp/bmp_render.rs b/routing/src/bmp/bmp_render.rs index d1b4ea42f..e3dafdf91 100644 --- a/routing/src/bmp/bmp_render.rs +++ b/routing/src/bmp/bmp_render.rs @@ -5,14 +5,14 @@ use std::collections::HashMap; use std::net::IpAddr; use netgauze_bgp_pkt::{ + BgpMessage, nlri::{MPReachNLRI, MPUnreachNLRI}, wire::deserializer::nlri::RouteType, - BgpMessage, }; use netgauze_bmp_pkt::{ + BmpMessage, peer::{PeerHeader, PeerType}, v3::BmpMessageValue as V3, - BmpMessage, }; use crate::config::internal::status::{ @@ -28,7 +28,6 @@ pub fn hande_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { } } - fn handle(status: &mut DataplaneStatus, m: &V3) { match m { V3::PeerUp(n) => { @@ -99,32 +98,39 @@ fn ensure_neighbor<'a>( key: &str, ) -> &'a mut BgpNeighborStatus { if status.bgp.is_none() { - status.set_bgp(BgpStatus { vrfs: HashMap::new() }); + status.set_bgp(BgpStatus { + vrfs: HashMap::new(), + }); } let bgp = status.bgp.as_mut().unwrap(); let vrf_entry = bgp .vrfs .entry(vrf.to_string()) - .or_insert_with(|| BgpVrfStatus { neighbors: HashMap::new() }); - vrf_entry.neighbors.entry(key.to_string()).or_insert_with(|| BgpNeighborStatus { - enabled: true, - local_as: 0, - peer_as: 0, - peer_port: 0, - peer_group: String::new(), - remote_router_id: String::new(), - session_state: BgpNeighborSessionState::Idle, - connections_dropped: 0, - established_transitions: 0, - last_reset_reason: String::new(), - messages: Some(BgpMessages { - received: Some(BgpMessageCounters::new()), - sent: Some(BgpMessageCounters::new()), - }), - ipv4_unicast_prefixes: Some(BgpNeighborPrefixes::default()), - ipv6_unicast_prefixes: Some(BgpNeighborPrefixes::default()), - l2vpn_evpn_prefixes: None, - }) + .or_insert_with(|| BgpVrfStatus { + neighbors: HashMap::new(), + }); + vrf_entry + .neighbors + .entry(key.to_string()) + .or_insert_with(|| BgpNeighborStatus { + enabled: true, + local_as: 0, + peer_as: 0, + peer_port: 0, + peer_group: String::new(), + remote_router_id: String::new(), + session_state: BgpNeighborSessionState::Idle, + connections_dropped: 0, + established_transitions: 0, + last_reset_reason: String::new(), + messages: Some(BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + }), + ipv4_unicast_prefixes: Some(BgpNeighborPrefixes::default()), + ipv6_unicast_prefixes: Some(BgpNeighborPrefixes::default()), + l2vpn_evpn_prefixes: None, + }) } fn is_post_policy(ph: &PeerHeader) -> bool { @@ -142,7 +148,10 @@ enum BgpMsgKind { } fn bump_msg(messages: &mut Option, received: bool, kind: BgpMsgKind) { - let m = messages.get_or_insert(BgpMessages { received: None, sent: None }); + let m = messages.get_or_insert(BgpMessages { + received: None, + sent: None, + }); let ctrs = if received { m.received.get_or_insert(BgpMessageCounters::new()) } else { @@ -206,20 +215,24 @@ fn account_mp_reach( v6pfx: &mut Option, ) { let cnt = mp.nlri().len() as u32; - if cnt == 0 { return; } + if cnt == 0 { + return; + } match mp.route_type() { RouteType::Ipv4Unicast => { let v = v4pfx.get_or_insert_with(Default::default); - if post_policy { v.sent = v.sent.saturating_add(cnt); } - else { + if post_policy { + v.sent = v.sent.saturating_add(cnt); + } else { v.received = v.received.saturating_add(cnt); v.received_pre_policy = v.received_pre_policy.saturating_add(cnt); } } RouteType::Ipv6Unicast => { let v = v6pfx.get_or_insert_with(Default::default); - if post_policy { v.sent = v.sent.saturating_add(cnt); } - else { + if post_policy { + v.sent = v.sent.saturating_add(cnt); + } else { v.received = v.received.saturating_add(cnt); v.received_pre_policy = v.received_pre_policy.saturating_add(cnt); } @@ -235,7 +248,9 @@ fn account_mp_unreach( v6pfx: &mut Option, ) { let cnt = mpu.nlri().len() as u32; - if cnt == 0 { return; } + if cnt == 0 { + return; + } match mpu.route_type() { RouteType::Ipv4Unicast => { let _v = v4pfx.get_or_insert_with(Default::default); diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs index 2a5f556c4..032eb4c12 100644 --- a/routing/src/bmp/mod.rs +++ b/routing/src/bmp/mod.rs @@ -1,6 +1,76 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -pub mod server; +#![allow(unused)] + +pub mod bmp_render; pub mod handler; -pub mod bmp_render; \ No newline at end of file +pub mod server; + +use std::thread::{self, JoinHandle}; + +use args::BmpServerParams; +use handler::BmpHandler; +use server::{BmpServer, BmpServerConfig}; +use tracing::{debug, error}; + +use concurrency::syn::{Arc, RwLock}; +use config::internal::status::DataplaneStatus; + +/// A BMP handler that updates `DataplaneStatus` via `bmp_render::hande_bmp_message`. +struct StatusUpdateHandler { + dp: Arc>, +} + +#[async_trait::async_trait] +impl BmpHandler for StatusUpdateHandler { + async fn on_message(&self, _peer: std::net::SocketAddr, msg: netgauze_bmp_pkt::BmpMessage) { + if let Ok(mut guard) = self.dp.try_write() { + bmp_render::hande_bmp_message(&mut *guard, &msg); + } else { + // non-blocking: skip if lock not immediately available + } + } + + async fn on_disconnect(&self, _peer: std::net::SocketAddr, _reason: &str) { + // no-op + } +} + +/// Spawn BMP server in a dedicated thread with its own Tokio runtime. +/// Always uses `StatusUpdateHandler` to update `DataplaneStatus`. +pub fn spawn_background( + params: &BmpServerParams, + dp_status: Arc>, +) -> JoinHandle<()> { + let bind = params.bind; + let stats_interval_ms = params.stats_interval_ms; + + thread::Builder::new() + .name("bmp-server".to_string()) + .spawn(move || { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_io() + .enable_time() + .build() + .expect("failed to build BMP tokio runtime"); + + rt.block_on(async move { + let cfg = BmpServerConfig { + bind_addr: bind, + ..Default::default() + }; + + debug!( + "BMP: starting StatusUpdateHandler on {bind}, interval={}ms", + stats_interval_ms + ); + let handler = StatusUpdateHandler { dp: dp_status }; + let srv = BmpServer::new(cfg, handler); + if let Err(e) = srv.run().await { + error!("BMP server exited with error: {e:#}"); + } + }); + }) + .expect("failed to start bmp-server thread") +} diff --git a/routing/src/bmp/server.rs b/routing/src/bmp/server.rs index 8cf36e7a5..178f68275 100644 --- a/routing/src/bmp/server.rs +++ b/routing/src/bmp/server.rs @@ -3,8 +3,8 @@ use anyhow::{Context, Result}; use bytes::BytesMut; -use netgauze_bmp_pkt::codec::BmpCodec; use netgauze_bmp_pkt::BmpMessage; +use netgauze_bmp_pkt::codec::BmpCodec; use tokio::net::{TcpListener, TcpStream}; use tokio::task::JoinSet; use tokio_util::codec::FramedRead; @@ -104,7 +104,9 @@ async fn handle_peer( handler.on_message(peer, BmpMessage::V4(msg)).await; } Err(e) => { - handler.on_disconnect(peer, &format!("decode error: {e}")).await; + handler + .on_disconnect(peer, &format!("decode error: {e}")) + .await; return Ok(()); } } diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index 451108ab4..ee254c63f 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -252,7 +252,8 @@ fn bgp_neigh_bool_switches(neigh: &BgpNeighbor, prefix: &str) -> ConfigBuilder { /* extended link bw */ if neigh.remove_private_as { - cfg += format!(" {prefix} remove-private-AS");ß + cfg += format!(" {prefix} remove-private-AS"); + ß } /* extended link bw */ diff --git a/routing/src/router/mod.rs b/routing/src/router/mod.rs index f743e7f08..6400bc770 100644 --- a/routing/src/router/mod.rs +++ b/routing/src/router/mod.rs @@ -13,10 +13,12 @@ pub(crate) mod rpc_adapt; use derive_builder::Builder; use std::fmt::Display; use std::path::PathBuf; +use std::thread::JoinHandle; use tracing::{debug, error}; use crate::atable::atablerw::{AtableReader, AtableReaderFactory}; use crate::atable::resolver::AtResolver; +use crate::bmp; use crate::errors::RouterError; use crate::fib::fibtable::{FibTableReader, FibTableReaderFactory, FibTableWriter}; use crate::interfaces::iftablerw::{IfTableReader, IfTableReaderFactory, IfTableWriter}; @@ -27,6 +29,13 @@ use args::DEFAULT_DP_UX_PATH; use args::DEFAULT_DP_UX_PATH_CLI; use args::DEFAULT_FRR_AGENT_PATH; +// bring BmpServerParams into scope (already added in args) +use args::BmpServerParams; + +// mandatory dataplane status handle +use concurrency::syn::{Arc, RwLock}; +use config::internal::status::DataplaneStatus; + /// Struct to configure router object. N.B we derive a builder type `RouterConfig` /// and provide defaults for each field. #[derive(Builder, Debug)] @@ -42,6 +51,12 @@ pub struct RouterParams { #[builder(setter(into), default = DEFAULT_FRR_AGENT_PATH.to_string().into())] pub frr_agent_path: PathBuf, + + // Optional BMP server parameters: whether to start server. + #[builder(setter(strip_option), default)] + pub bmp: Option, + // Mandatory dataplane status handle + pub dp_status: Arc>, } impl Display for RouterParams { @@ -62,6 +77,8 @@ pub struct Router { rio_handle: RioHandle, iftr: IfTableReader, fibtr: FibTableReader, + // keep BMP thread alive while Router lives + bmp_handle: Option>, } impl Router { @@ -114,6 +131,17 @@ impl Router { debug!("{name}: Starting router IO..."); let rio_handle = start_rio(&rioconf, fibtw, iftw, atabler)?; + // Start BMP server in background if configured, always with mandatory dp_status + let bmp_handle = if let Some(bmp_params) = ¶ms.bmp { + debug!( + "{name}: Starting BMP server on {} (interval={}ms)", + bmp_params.bind, bmp_params.stats_interval_ms + ); + Some(bmp::spawn_background(bmp_params, params.dp_status.clone())) + } else { + None + }; + debug!("{name}: Successfully started router with parameters:\n{params}"); let router = Router { name: name.to_owned(), @@ -122,6 +150,7 @@ impl Router { rio_handle, iftr, fibtr, + bmp_handle, }; Ok(router) } @@ -133,6 +162,7 @@ impl Router { error!("Failed to stop IO for router '{}': {e}", self.name); } self.resolver.stop(); + debug!("Router '{}' is now stopped", self.name); } From 3bfa302fb6fbdda0e16118f40e0041fbf069d8ba Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Mon, 8 Dec 2025 13:30:52 +0400 Subject: [PATCH 04/14] feat(status): Fix imports and private fields Signed-off-by: Sergey Matov --- Cargo.lock | 5 + routing/Cargo.toml | 8 +- routing/src/bmp/bmp_render.rs | 307 ++++---------------------------- routing/src/bmp/handler.rs | 17 +- routing/src/bmp/mod.rs | 90 ++++------ routing/src/bmp/server.rs | 85 +++++---- routing/src/frr/renderer/bgp.rs | 1 - routing/src/lib.rs | 1 - routing/src/router/mod.rs | 43 +++-- 9 files changed, 168 insertions(+), 389 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 76820f197..e1fbeeb70 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1586,6 +1586,8 @@ name = "dataplane-routing" version = "0.6.0" dependencies = [ "ahash", + "anyhow", + "async-trait", "bitflags 2.10.0", "bolero", "bytes", @@ -1600,6 +1602,7 @@ dependencies = [ "dataplane-tracectl", "derive_builder 0.20.2", "dplane-rpc", + "futures-util", "ipnet", "left-right", "linkme", @@ -1612,6 +1615,8 @@ dependencies = [ "serde", "thiserror 2.0.17", "tokio", + "tokio-stream", + "tokio-util", "tracing", "tracing-test", ] diff --git a/routing/Cargo.toml b/routing/Cargo.toml index f6390f79b..0c30ca381 100644 --- a/routing/Cargo.toml +++ b/routing/Cargo.toml @@ -13,6 +13,7 @@ testing = [] args = { workspace = true } cli = { workspace = true } config = { workspace = true } +concurrency = { workspace = true } dplane-rpc = { workspace = true } left-right-tlcache = { workspace = true } lpm = { workspace = true } @@ -21,10 +22,13 @@ tracectl = { workspace = true } # external ahash = { workspace = true, features = ["no-rng"] } +anyhow = { workspace = true } +async-trait = { workspace = true } bitflags = { workspace = true } bytes = { workspace = true, features = ["serde"] } chrono = { workspace = true, features = ["clock"] } derive_builder = { workspace = true, features = ["default", "std"] } +futures-util = { workspace = true } ipnet = { workspace = true } left-right = { workspace = true } linkme = { workspace = true } @@ -33,7 +37,9 @@ netgauze-bgp-pkt = { workspace = true } netgauze-bmp-pkt = { workspace = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt", "sync"] } +tokio = { workspace = true, features = ["rt", "sync", "net", "macros"] } +tokio-stream = { workspace = true } +tokio-util = { workspace = true, features = ["codec"] } tracing = { workspace = true } # arp resolver diff --git a/routing/src/bmp/bmp_render.rs b/routing/src/bmp/bmp_render.rs index e3dafdf91..54f4ece91 100644 --- a/routing/src/bmp/bmp_render.rs +++ b/routing/src/bmp/bmp_render.rs @@ -1,278 +1,45 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -use std::collections::HashMap; -use std::net::IpAddr; +//! Minimal & safe renderer for NetGauze 0.8.0 BMP messages. +//! This is intentionally conservative to avoid API mismatches. You can +//! extend it to count prefixes/messages per neighbor/VRF once the exact +//! status schema is settled. -use netgauze_bgp_pkt::{ - BgpMessage, - nlri::{MPReachNLRI, MPUnreachNLRI}, - wire::deserializer::nlri::RouteType, -}; -use netgauze_bmp_pkt::{ - BmpMessage, - peer::{PeerHeader, PeerType}, - v3::BmpMessageValue as V3, -}; +use netgauze_bmp_pkt::BmpMessage; -use crate::config::internal::status::{ - BgpMessageCounters, BgpMessages, BgpNeighborPrefixes, BgpNeighborSessionState, - BgpNeighborStatus, BgpStatus, BgpVrfStatus, DataplaneStatus, -}; +// Bring in your status type (the module path below matches your earlier usage) +use config::internal::status::DataplaneStatus; +/// Backward-compat shim for older callers (typo in earlier drafts). +#[inline] pub fn hande_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { - // smatov: frr bmp v3 only for now - // but we maybe will extend it later - if let BmpMessage::V3(v) = msg { - handle(status, v); - } -} - -fn handle(status: &mut DataplaneStatus, m: &V3) { - match m { - V3::PeerUp(n) => { - let (vrf, key) = peer_keys(&n.common_header.peer_header); - let st = ensure_neighbor(status, &vrf, &key); - st.enabled = true; - st.session_state = BgpNeighborSessionState::Established; - st.established_transitions = st.established_transitions.saturating_add(1); - st.peer_as = n.common_header.peer_as().into(); - st.local_as = n.common_header.local_as().into(); - st.remote_router_id = n.common_header.router_id().to_string(); - st.peer_port = n.local_port as u32; - - // PeerUp carries both Open messages (rx+tx) so count both. - bump_msg(&mut st.messages, true, BgpMsgKind::Open); - bump_msg(&mut st.messages, false, BgpMsgKind::Open); - } - V3::PeerDown(n) => { - let (vrf, key) = peer_keys(&n.common_header.peer_header); - let st = ensure_neighbor(status, &vrf, &key); - st.session_state = BgpNeighborSessionState::Idle; - st.last_reset_reason = peer_down_reason_v3(n); - st.connections_dropped = st.connections_dropped.saturating_add(1); - } - V3::RouteMonitoring(rm) => { - let (vrf, key) = peer_keys(&rm.common_header.peer_header); - let st = ensure_neighbor(status, &vrf, &key); - let post_policy = is_post_policy(&rm.common_header.peer_header); - - apply_bgp_pdu( - &mut st.messages, - &mut st.ipv4_unicast_prefixes, - &mut st.ipv6_unicast_prefixes, - &rm.bgp_message, - post_policy, - ); - } - V3::StatisticsReport(_sr) => { - // noop for status for now - } - V3::Initiation(_) | V3::Termination(_) | V3::RouteMirroring(_) => { - // no-op for status - } - } -} - -fn peer_keys(ph: &PeerHeader) -> (String, String) { - let vrf = match ph.peer_type() { - PeerType::GlobalInstance | PeerType::L3vpn => { - if let Some(d) = ph.peer_distinguisher() { - format!("pdx:{d}") // smatov: later add translation from VNI to VRF name here - } else { - "default".to_string() - } - } - _ => "default".to_string(), - }; - let key = match ph.peer_address() { - IpAddr::V4(a) => a.to_string(), - IpAddr::V6(a) => a.to_string(), - }; - (vrf, key) -} - -fn ensure_neighbor<'a>( - status: &'a mut DataplaneStatus, - vrf: &str, - key: &str, -) -> &'a mut BgpNeighborStatus { - if status.bgp.is_none() { - status.set_bgp(BgpStatus { - vrfs: HashMap::new(), - }); - } - let bgp = status.bgp.as_mut().unwrap(); - let vrf_entry = bgp - .vrfs - .entry(vrf.to_string()) - .or_insert_with(|| BgpVrfStatus { - neighbors: HashMap::new(), - }); - vrf_entry - .neighbors - .entry(key.to_string()) - .or_insert_with(|| BgpNeighborStatus { - enabled: true, - local_as: 0, - peer_as: 0, - peer_port: 0, - peer_group: String::new(), - remote_router_id: String::new(), - session_state: BgpNeighborSessionState::Idle, - connections_dropped: 0, - established_transitions: 0, - last_reset_reason: String::new(), - messages: Some(BgpMessages { - received: Some(BgpMessageCounters::new()), - sent: Some(BgpMessageCounters::new()), - }), - ipv4_unicast_prefixes: Some(BgpNeighborPrefixes::default()), - ipv6_unicast_prefixes: Some(BgpNeighborPrefixes::default()), - l2vpn_evpn_prefixes: None, - }) -} - -fn is_post_policy(ph: &PeerHeader) -> bool { - ph.is_post_policy() -} - -#[derive(Clone, Copy)] -enum BgpMsgKind { - Open, - Keepalive, - Notification, - Update, - RouteRefresh, - Capability, -} - -fn bump_msg(messages: &mut Option, received: bool, kind: BgpMsgKind) { - let m = messages.get_or_insert(BgpMessages { - received: None, - sent: None, - }); - let ctrs = if received { - m.received.get_or_insert(BgpMessageCounters::new()) - } else { - m.sent.get_or_insert(BgpMessageCounters::new()) - }; - match kind { - BgpMsgKind::Open => ctrs.open = ctrs.open.saturating_add(1), - BgpMsgKind::Keepalive => ctrs.keepalive = ctrs.keepalive.saturating_add(1), - BgpMsgKind::Notification => ctrs.notification = ctrs.notification.saturating_add(1), - BgpMsgKind::Update => ctrs.update = ctrs.update.saturating_add(1), - BgpMsgKind::RouteRefresh => ctrs.route_refresh = ctrs.route_refresh.saturating_add(1), - BgpMsgKind::Capability => ctrs.capability = ctrs.capability.saturating_add(1), - } -} - -fn apply_bgp_pdu( - messages: &mut Option, - v4pfx: &mut Option, - v6pfx: &mut Option, - pdu: &BgpMessage, - post_policy: bool, -) { - match pdu { - BgpMessage::Open(_) => bump_msg(messages, true, BgpMsgKind::Open), - BgpMessage::KeepAlive => bump_msg(messages, true, BgpMsgKind::Keepalive), - BgpMessage::Notification(_) => bump_msg(messages, true, BgpMsgKind::Notification), - BgpMessage::RouteRefresh(_) => bump_msg(messages, true, BgpMsgKind::RouteRefresh), - BgpMessage::Update(upd) => { - bump_msg(messages, true, BgpMsgKind::Update); - - // default nlri v4 - let a4 = upd.nlri.len() as u32; - if a4 > 0 { - let v = v4pfx.get_or_insert_with(Default::default); - if post_policy { - v.sent = v.sent.saturating_add(a4); - } else { - v.received = v.received.saturating_add(a4); - v.received_pre_policy = v.received_pre_policy.saturating_add(a4); - } - } - - // unreach nlri v4 - for attr in &upd.path_attributes { - if let Some(mp) = attr.get_mp_reach_nlri() { - account_mp_reach(mp, post_policy, v4pfx, v6pfx); - } - if let Some(mpu) = attr.get_mp_unreach_nlri() { - account_mp_unreach(mpu, post_policy, v4pfx, v6pfx); - } - } - } - BgpMessage::Unknown(_) => { /* todo: what should we do here? */ } - } -} - -fn account_mp_reach( - mp: &MPReachNLRI, - post_policy: bool, - v4pfx: &mut Option, - v6pfx: &mut Option, -) { - let cnt = mp.nlri().len() as u32; - if cnt == 0 { - return; - } - match mp.route_type() { - RouteType::Ipv4Unicast => { - let v = v4pfx.get_or_insert_with(Default::default); - if post_policy { - v.sent = v.sent.saturating_add(cnt); - } else { - v.received = v.received.saturating_add(cnt); - v.received_pre_policy = v.received_pre_policy.saturating_add(cnt); - } - } - RouteType::Ipv6Unicast => { - let v = v6pfx.get_or_insert_with(Default::default); - if post_policy { - v.sent = v.sent.saturating_add(cnt); - } else { - v.received = v.received.saturating_add(cnt); - v.received_pre_policy = v.received_pre_policy.saturating_add(cnt); - } - } - _ => { /* todo: evpn */ } - } -} - -fn account_mp_unreach( - mpu: &MPUnreachNLRI, - _post_policy: bool, - v4pfx: &mut Option, - v6pfx: &mut Option, -) { - let cnt = mpu.nlri().len() as u32; - if cnt == 0 { - return; - } - match mpu.route_type() { - RouteType::Ipv4Unicast => { - let _v = v4pfx.get_or_insert_with(Default::default); - let _ = cnt; // smatov: add explicit counters later - } - RouteType::Ipv6Unicast => { - let _v = v6pfx.get_or_insert_with(Default::default); - let _ = cnt; - } - _ => {} - } -} - -fn peer_down_reason_v3(n: &netgauze_bmp_pkt::v3::PeerDownNotification) -> String { - use netgauze_bmp_pkt::v3::PeerDownReason as R; - match n.reason { - R::LocalSystemNotification => "local-notification".into(), - R::LocalSystemNoNotification => "local-no-notification".into(), - R::RemoteSystemNotification => "remote-notification".into(), - R::RemoteSystemNoNotification => "remote-no-notification".into(), - R::PeerDeconfigured => "peer-deconfigured".into(), - R::CommunicationLost => "communication-lost".into(), - R::Unknown(v) => format!("unknown({v})"), - } + handle_bmp_message(status, msg) +} + +/// Primary entry point: update `DataplaneStatus` from a single BMP message. +/// +/// For now, we keep this a no-op body that compiles cleanly with NetGauze 0.8.0, +/// so the server can be integrated first. You can expand this to: +/// - track per-neighbor session state on PeerUp/PeerDown +/// - count UPDATEs / KEEPALIVEs per direction +/// - count v4/v6 NLRI using `BgpMessage::Update { .. }` + `path_attributes()` +/// - derive pre/post-policy from PeerHeader flags (v3) +pub fn handle_bmp_message(_status: &mut DataplaneStatus, _msg: &BmpMessage) { + // Intentionally left as a no-op to keep the build green while + // we align on exact NetGauze accessors across all message types. + // Safe expansion path (sketch): + // + // match msg { + // BmpMessage::V3(v) => match v { + // netgauze_bmp_pkt::v3::BmpMessageValue::PeerUpNotification(n) => { ... } + // netgauze_bmp_pkt::v3::BmpMessageValue::PeerDownNotification(n) => { ... } + // netgauze_bmp_pkt::v3::BmpMessageValue::RouteMonitoring(rm) => { + // let pdu = rm.bgp_message(); + // if let netgauze_bgp_pkt::BgpMessage::Update(upd) = pdu { ... } + // } + // _ => {} + // }, + // BmpMessage::V4(_v) => { /* optional later */ } + // } } diff --git a/routing/src/bmp/handler.rs b/routing/src/bmp/handler.rs index 37b2a7d91..7df902c50 100644 --- a/routing/src/bmp/handler.rs +++ b/routing/src/bmp/handler.rs @@ -10,20 +10,7 @@ pub trait BmpHandler: Send + Sync + 'static { async fn on_message(&self, peer: std::net::SocketAddr, msg: BmpMessage); /// Called when a connection terminates (EOF / error). - async fn on_disconnect(&self, peer: std::net::SocketAddr, reason: &str) { - let _ = (peer, reason); // no-op - } -} - -pub struct JsonLogHandler; - -#[async_trait::async_trait] -impl BmpHandler for JsonLogHandler { - async fn on_message(&self, peer: std::net::SocketAddr, msg: BmpMessage) { - // BmpMessage implements serde, so this is safe: - match serde_json::to_string(&msg) { - Ok(line) => println!(r#"{{"peer":"{}","bmp":{}}}"#, peer, line), - Err(e) => eprintln!("serialize error from {}: {e}", peer), - } + async fn on_disconnect(&self, _peer: std::net::SocketAddr, _reason: &str) { + // no-op } } diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs index 032eb4c12..45c233685 100644 --- a/routing/src/bmp/mod.rs +++ b/routing/src/bmp/mod.rs @@ -1,76 +1,56 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -#![allow(unused)] - pub mod bmp_render; pub mod handler; pub mod server; -use std::thread::{self, JoinHandle}; - -use args::BmpServerParams; -use handler::BmpHandler; -use server::{BmpServer, BmpServerConfig}; -use tracing::{debug, error}; +pub use handler::BmpHandler; +pub use server::{BmpServer, BmpServerConfig}; -use concurrency::syn::{Arc, RwLock}; +use concurrency::sync::{Arc, RwLock}; use config::internal::status::DataplaneStatus; +use netgauze_bmp_pkt::BmpMessage; +use tokio::task::JoinHandle; +use tracing::info; -/// A BMP handler that updates `DataplaneStatus` via `bmp_render::hande_bmp_message`. -struct StatusUpdateHandler { - dp: Arc>, +/// Background BMP server runner that updates shared dataplane status. +pub struct StatusHandler { + dp_status: Arc>, } -#[async_trait::async_trait] -impl BmpHandler for StatusUpdateHandler { - async fn on_message(&self, _peer: std::net::SocketAddr, msg: netgauze_bmp_pkt::BmpMessage) { - if let Ok(mut guard) = self.dp.try_write() { - bmp_render::hande_bmp_message(&mut *guard, &msg); - } else { - // non-blocking: skip if lock not immediately available - } +impl StatusHandler { + pub fn new(dp_status: Arc>) -> Self { + Self { dp_status } } +} - async fn on_disconnect(&self, _peer: std::net::SocketAddr, _reason: &str) { - // no-op +#[async_trait::async_trait] +impl handler::BmpHandler for StatusHandler { + async fn on_message(&self, _peer: std::net::SocketAddr, msg: BmpMessage) { + // Your `concurrency::sync::RwLock` returns Result like std:: + let mut guard = self + .dp_status + .write() + .expect("dataplane status lock poisoned"); + bmp_render::handle_bmp_message(&mut *guard, &msg); } } -/// Spawn BMP server in a dedicated thread with its own Tokio runtime. -/// Always uses `StatusUpdateHandler` to update `DataplaneStatus`. +/// Spawn BMP server in background pub fn spawn_background( - params: &BmpServerParams, + bind: std::net::SocketAddr, dp_status: Arc>, ) -> JoinHandle<()> { - let bind = params.bind; - let stats_interval_ms = params.stats_interval_ms; - - thread::Builder::new() - .name("bmp-server".to_string()) - .spawn(move || { - let rt = tokio::runtime::Builder::new_current_thread() - .enable_io() - .enable_time() - .build() - .expect("failed to build BMP tokio runtime"); - - rt.block_on(async move { - let cfg = BmpServerConfig { - bind_addr: bind, - ..Default::default() - }; - - debug!( - "BMP: starting StatusUpdateHandler on {bind}, interval={}ms", - stats_interval_ms - ); - let handler = StatusUpdateHandler { dp: dp_status }; - let srv = BmpServer::new(cfg, handler); - if let Err(e) = srv.run().await { - error!("BMP server exited with error: {e:#}"); - } - }); - }) - .expect("failed to start bmp-server thread") + tokio::spawn(async move { + info!("starting BMP server on {}", bind); + let cfg = BmpServerConfig { + bind_addr: bind, + ..Default::default() + }; + let srv = BmpServer::new(cfg, StatusHandler::new(dp_status)); + if let Err(e) = srv.run().await { + tracing::error!("bmp server terminated: {e:#}"); + } + }) } diff --git a/routing/src/bmp/server.rs b/routing/src/bmp/server.rs index 178f68275..d80fd5076 100644 --- a/routing/src/bmp/server.rs +++ b/routing/src/bmp/server.rs @@ -1,10 +1,17 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors +//! Minimal BMP server built on NetGauze 0.8.0 +//! - Frames a TCP stream with `BmpCodec` +//! - Yields `netgauze_bmp_pkt::BmpMessage` items +//! - Hands each item to a user-provided `BmpHandler` + use anyhow::{Context, Result}; -use bytes::BytesMut; +use concurrency::sync::Arc; +use futures_util::StreamExt; use netgauze_bmp_pkt::BmpMessage; use netgauze_bmp_pkt::codec::BmpCodec; +use std::net::SocketAddr; use tokio::net::{TcpListener, TcpStream}; use tokio::task::JoinSet; use tokio_util::codec::FramedRead; @@ -13,9 +20,11 @@ use crate::bmp::handler::BmpHandler; #[derive(Clone, Debug)] pub struct BmpServerConfig { - pub bind_addr: std::net::SocketAddr, + pub bind_addr: SocketAddr, pub tcp_nodelay: bool, + /// Reserved for future tuning (no stable API to set TCP recv buf on Tokio stream). pub tcp_recv_buf: Option, + /// Optional cap on simultaneously active peers. pub max_conns: Option, } @@ -24,7 +33,7 @@ impl Default for BmpServerConfig { Self { bind_addr: "0.0.0.0:5000".parse().unwrap(), tcp_nodelay: true, - tcp_recv_buf: Some(1 << 20), // 1 MiB + tcp_recv_buf: Some(1 << 20), max_conns: None, } } @@ -32,12 +41,15 @@ impl Default for BmpServerConfig { pub struct BmpServer { cfg: BmpServerConfig, - handler: H, + handler: Arc, } impl BmpServer { pub fn new(cfg: BmpServerConfig, handler: H) -> Self { - Self { cfg, handler } + Self { + cfg, + handler: Arc::new(handler), + } } pub async fn run(self) -> Result<()> { @@ -46,8 +58,8 @@ impl BmpServer { .with_context(|| format!("bind {}", self.cfg.bind_addr))?; tracing::info!("BMP server listening on {}", self.cfg.bind_addr); - let mut tasks = JoinSet::new(); - let mut active = 0usize; + let mut tasks: JoinSet> = JoinSet::new(); + let mut active: usize = 0; loop { let (sock, peer) = listener.accept().await?; @@ -57,60 +69,61 @@ impl BmpServer { continue; } } - active += 1; + + active = active.saturating_add(1); let cfg = self.cfg.clone(); - let handler = &self.handler; - let handler = handler; // capture by move below - tasks.spawn(handle_peer(sock, peer, cfg, handler)); - // Periodically reap finished tasks - while let Some(ready) = tasks.try_join_next()? { - ready?; - active = active.saturating_sub(1); + let handler = Arc::clone(&self.handler); + + tasks.spawn(async move { handle_peer(sock, peer, cfg, handler).await }); + + // Reap finished connections (non-blocking) + while let Some(joined) = tasks.try_join_next() { + match joined { + Ok(Ok(())) => active = active.saturating_sub(1), + Ok(Err(e)) => { + active = active.saturating_sub(1); + tracing::warn!("bmp task error: {e:#}"); + } + Err(e) => { + active = active.saturating_sub(1); + tracing::warn!("bmp task join error: {e:#}"); + } + } } } } } async fn handle_peer( - mut sock: TcpStream, - peer: std::net::SocketAddr, + sock: TcpStream, + peer: SocketAddr, cfg: BmpServerConfig, - handler: &H, + handler: Arc, ) -> Result<()> { if cfg.tcp_nodelay { let _ = sock.set_nodelay(true); } - if let Some(sz) = cfg.tcp_recv_buf { - let _ = sock.set_recv_buffer_size(sz); - } + // NOTE: cfg.tcp_recv_buf kept for future tuning; Tokio has no stable API to set recv buf. - // Framed BMP stream using NetGauze’s codec + // Frame the stream as BMP let codec = BmpCodec::default(); - let reader = FramedRead::new(sock, codec); - - tokio::pin!(reader); + let mut reader = FramedRead::new(sock, codec); - // Use a scratch buffer for zero-copy clones if needed - let mut _scratch = BytesMut::new(); - - use futures_util::StreamExt; - let mut reader = reader; while let Some(frame) = reader.next().await { match frame { - Ok(BmpMessage::V3(msg)) => { - handler.on_message(peer, BmpMessage::V3(msg)).await; - } - Ok(BmpMessage::V4(msg)) => { - handler.on_message(peer, BmpMessage::V4(msg)).await; + Ok(msg) => { + // netgauze_bmp_pkt::BmpMessage for both v3 and v4 + handler.on_message(peer, msg).await; } Err(e) => { handler - .on_disconnect(peer, &format!("decode error: {e}")) + .on_disconnect(peer, &format!("decode error: {e:?}")) .await; return Ok(()); } } } + handler.on_disconnect(peer, "eof").await; Ok(()) } diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index ee254c63f..cc5857ce4 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -253,7 +253,6 @@ fn bgp_neigh_bool_switches(neigh: &BgpNeighbor, prefix: &str) -> ConfigBuilder { /* extended link bw */ if neigh.remove_private_as { cfg += format!(" {prefix} remove-private-AS"); - ß } /* extended link bw */ diff --git a/routing/src/lib.rs b/routing/src/lib.rs index 0bb3eb253..44f552bed 100644 --- a/routing/src/lib.rs +++ b/routing/src/lib.rs @@ -28,7 +28,6 @@ mod routingdb; // re-exports pub use atable::atablerw::AtableReader; -pub use bmp::handler::JsonLogHandler; pub use bmp::server::{BmpServer, BmpServerConfig}; pub use config::RouterConfig; pub use errors::RouterError; diff --git a/routing/src/router/mod.rs b/routing/src/router/mod.rs index 6400bc770..92eb6948e 100644 --- a/routing/src/router/mod.rs +++ b/routing/src/router/mod.rs @@ -13,9 +13,14 @@ pub(crate) mod rpc_adapt; use derive_builder::Builder; use std::fmt::Display; use std::path::PathBuf; -use std::thread::JoinHandle; use tracing::{debug, error}; +// sockets +use std::net::SocketAddr; + +// keep async task handle for BMP +use tokio::task::JoinHandle; + use crate::atable::atablerw::{AtableReader, AtableReaderFactory}; use crate::atable::resolver::AtResolver; use crate::bmp; @@ -29,13 +34,22 @@ use args::DEFAULT_DP_UX_PATH; use args::DEFAULT_DP_UX_PATH_CLI; use args::DEFAULT_FRR_AGENT_PATH; -// bring BmpServerParams into scope (already added in args) -use args::BmpServerParams; - // mandatory dataplane status handle -use concurrency::syn::{Arc, RwLock}; +use concurrency::sync::{Arc, RwLock}; use config::internal::status::DataplaneStatus; +#[derive(Clone, Debug)] +pub struct BmpServerParams { + /// TCP bind address for the BMP listener + pub bind_addr: SocketAddr, + /// Periodic stats emit interval (milliseconds) + pub stats_interval_ms: u64, + /// Optional reconnect/backoff lower bound (milliseconds) + pub min_retry_ms: Option, + /// Optional reconnect/backoff upper bound (milliseconds) + pub max_retry_ms: Option, +} + /// Struct to configure router object. N.B we derive a builder type `RouterConfig` /// and provide defaults for each field. #[derive(Builder, Debug)] @@ -55,6 +69,7 @@ pub struct RouterParams { // Optional BMP server parameters: whether to start server. #[builder(setter(strip_option), default)] pub bmp: Option, + // Mandatory dataplane status handle pub dp_status: Arc>, } @@ -77,7 +92,7 @@ pub struct Router { rio_handle: RioHandle, iftr: IfTableReader, fibtr: FibTableReader, - // keep BMP thread alive while Router lives + // keep BMP task alive while Router lives bmp_handle: Option>, } @@ -135,9 +150,12 @@ impl Router { let bmp_handle = if let Some(bmp_params) = ¶ms.bmp { debug!( "{name}: Starting BMP server on {} (interval={}ms)", - bmp_params.bind, bmp_params.stats_interval_ms + bmp_params.bind_addr, bmp_params.stats_interval_ms ); - Some(bmp::spawn_background(bmp_params, params.dp_status.clone())) + Some(bmp::spawn_background( + bmp_params.bind_addr, + params.dp_status.clone(), + )) } else { None }; @@ -155,14 +173,19 @@ impl Router { Ok(router) } - /// Stop this router. This stops the router IO thread and drops the interface table, adjacency table - /// vrf table and the fib table. + /// Stop this router. This stops the router IO task and drops the interface table, adjacency table, + /// VRF table and the FIB table. pub fn stop(&mut self) { if let Err(e) = self.rio_handle.finish() { error!("Failed to stop IO for router '{}': {e}", self.name); } self.resolver.stop(); + // Abort BMP server task if running (Tokio handle). + if let Some(handle) = self.bmp_handle.take() { + handle.abort(); + } + debug!("Router '{}' is now stopped", self.name); } From b29279ee326619e9e083cb70a942b3dc0b442865 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Mon, 8 Dec 2025 16:06:03 +0400 Subject: [PATCH 05/14] feat(status): Change BMP injection to per-VRF Signed-off-by: Sergey Matov --- mgmt/src/processor/confbuild/internal.rs | 24 ++++++++++- mgmt/src/processor/proc.rs | 52 +++++++++++------------- 2 files changed, 45 insertions(+), 31 deletions(-) diff --git a/mgmt/src/processor/confbuild/internal.rs b/mgmt/src/processor/confbuild/internal.rs index 469ad0b52..4eb7149f9 100644 --- a/mgmt/src/processor/confbuild/internal.rs +++ b/mgmt/src/processor/confbuild/internal.rs @@ -19,6 +19,9 @@ use crate::processor::confbuild::namegen::{VpcConfigNames, VpcInterfacesNames}; use config::internal::routing::bgp::{AfIpv4Ucast, AfL2vpnEvpn}; use config::internal::routing::bgp::{BgpConfig, BgpOptions, VrfImports}; +// ── NEW: allow injecting BMP config into InternalConfig +use config::internal::routing::bgp::BmpOptions; + use config::internal::routing::prefixlist::{ IpVer, PrefixList, PrefixListAction, PrefixListEntry, PrefixListMatchLen, PrefixListPrefix, }; @@ -264,8 +267,13 @@ fn build_internal_overlay_config( Ok(()) } -/// Top-level function to build internal config from external config -pub fn build_internal_config(config: &GwConfig) -> Result { +/// Top-level function to build internal config from external config, +/// **with optional BMP injection**. +/// Prefer this in mgmt so FRR renderer emits BMP. +pub fn build_internal_config_with_bmp( + config: &GwConfig, + bmp: Option, +) -> Result { let genid = config.genid(); debug!("Building internal config for gen {genid}"); let external = &config.external; @@ -275,6 +283,12 @@ pub fn build_internal_config(config: &GwConfig) -> Result &mut Self` + internal.set_bmp_options(b); + } + /* Build overlay config */ if let Some(bgp) = &external.underlay.vrf.bgp { let asn = bgp.asn; @@ -294,3 +308,9 @@ pub fn build_internal_config(config: &GwConfig) -> Result Result { + build_internal_config_with_bmp(config, None) +} diff --git a/mgmt/src/processor/proc.rs b/mgmt/src/processor/proc.rs index b564de591..fa3d2bff7 100644 --- a/mgmt/src/processor/proc.rs +++ b/mgmt/src/processor/proc.rs @@ -20,9 +20,7 @@ use config::{ConfigError, ConfigResult, stringify}; use config::{DeviceConfig, ExternalConfig, GenId, GwConfig, InternalConfig}; use config::{external::overlay::Overlay, internal::device::tracecfg::TracingConfig}; -use config::internal::routing::bgp::BmpOptions; - -use crate::processor::confbuild::internal::build_internal_config; +use crate::processor::confbuild::internal::build_internal_config_with_bmp; use crate::processor::confbuild::router::generate_router_config; use nat::stateful::NatAllocatorWriter; use nat::stateless::NatTablesWriter; @@ -47,6 +45,9 @@ use stats::VpcStatsStore; use vpcmap::VpcDiscriminant; use vpcmap::map::{VpcMap, VpcMapWriter}; +// ── NEW: bring in BmpOptions so we can pass it to internal builder +use config::internal::routing::bgp::BmpOptions; + /// A request type to the `ConfigProcessor` #[derive(Debug)] pub enum ConfigRequest { @@ -126,8 +127,8 @@ pub struct ConfigProcessorParams { // read-handle to shared dataplane status pub dp_status_r: Arc>, - // BMP client options for FRR render (None if BMP is disabled) - pub bmp_client: Option, + // ── NEW: optional BMP options to inject into InternalConfig + pub bmp_options: Option, } impl ConfigProcessor { @@ -168,15 +169,9 @@ impl ConfigProcessor { } config.validate()?; - // Build internal config from external - let mut internal = build_internal_config(&config)?; - - // ── NEW: inject BMP client options (if provided) into every BGP instance ── - if let Some(ref bmp) = self.proc_params.bmp_client { - inject_bmp_into_internal(&mut internal, bmp.clone()); - } - - // store internal back into config + // ── BMP-aware internal builder + let internal = + build_internal_config_with_bmp(&config, self.proc_params.bmp_options.clone())?; config.set_internal_config(internal); let e = match self.apply(config).await { @@ -196,10 +191,9 @@ impl ConfigProcessor { #[allow(unused)] async fn apply_blank_config(&mut self) -> ConfigResult { let mut blank = GwConfig::blank(); - let mut internal = build_internal_config(&blank)?; - if let Some(ref bmp) = self.proc_params.bmp_client { - inject_bmp_into_internal(&mut internal, bmp.clone()); - } + // ── BMP-aware internal builder (even for blank, so FRR reflects BMP if needed) + let internal = + build_internal_config_with_bmp(&blank, self.proc_params.bmp_options.clone())?; blank.set_internal_config(internal); self.apply(blank).await } @@ -286,8 +280,14 @@ impl ConfigProcessor { /// RPC handler: get dataplane status async fn handle_get_dataplane_status(&mut self) -> ConfigResponse { + // NOTE: std::sync::RwLock::read() returns Result. + // Unwrap the guard, then clone the status. let mut status: DataplaneStatus = { - let guard = self.proc_params.dp_status_r.read(); + let guard = self + .proc_params + .dp_status_r + .read() + .expect("dp_status RwLock poisoned"); guard.clone() }; @@ -676,19 +676,13 @@ async fn apply_gw_config( let pairs = update_stats_vpc_mappings(config, vpcmapw); drop(pairs); // pairs used by caller + // NOTE: If we need to inject/override BMP per-VRF here, prefer adding a helper once + // InternalConfig exposes a mutable VRF accessor. For now, BMP config is built in + // the internal build stage and applied by the routing layer. + /* apply config in router */ apply_router_config(&kernel_vrfs, config, router_ctl).await?; info!("Successfully applied config for genid {genid}"); Ok(()) } - -fn inject_bmp_into_internal(internal: &mut InternalConfig, bmp: BmpOptions) { - // We attach BMP options to every VRF that has a BGP instance. - // This keeps FRR render simple: wherever we render a 'router bgp ...' we have .bmp = Some(...) - for vrf in internal.vrfs_mut() { - if let Some(bgp) = vrf.bgp_mut() { - bgp.set_bmp_options(bmp.clone()); - } - } -} From a5d3369a8641c289ed3a061ad88da7ee2c14d623 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 9 Dec 2025 11:34:27 +0400 Subject: [PATCH 06/14] feat(status): Change BMP from per-VRF to global config Signed-off-by: Sergey Matov --- Cargo.lock | 1 + config/src/internal/routing/bgp.rs | 25 +++++++++++ dataplane/Cargo.toml | 1 + dataplane/src/main.rs | 33 ++++++++------ mgmt/src/processor/confbuild/internal.rs | 56 ++++++++++++++---------- mgmt/src/processor/proc.rs | 11 +++-- routing/src/lib.rs | 2 +- 7 files changed, 87 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1fbeeb70..835b118c0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1146,6 +1146,7 @@ dependencies = [ "ctrlc", "dataplane-args", "dataplane-concurrency", + "dataplane-config", "dataplane-dpdk", "dataplane-dpdk-sysroot-helper", "dataplane-gwname", diff --git a/config/src/internal/routing/bgp.rs b/config/src/internal/routing/bgp.rs index 32c9efcc2..6bf9e565c 100644 --- a/config/src/internal/routing/bgp.rs +++ b/config/src/internal/routing/bgp.rs @@ -205,6 +205,10 @@ pub struct BmpOptions { pub monitor_ipv4_post: bool, pub monitor_ipv6_pre: bool, pub monitor_ipv6_post: bool, + + /// VRFs/views to import into the default BMP instance: + /// renders as multiple `bmp import-vrf-view ` + pub import_vrf_views: Vec, } impl Default for BmpOptions { @@ -221,6 +225,7 @@ impl Default for BmpOptions { monitor_ipv4_post: true, monitor_ipv6_pre: false, monitor_ipv6_post: false, + import_vrf_views: Vec::new(), } } } @@ -272,6 +277,26 @@ impl BmpOptions { self.monitor_ipv6_post = post; self } + + #[must_use] + pub fn add_import_vrf_view>(mut self, vrf: S) -> Self { + self.import_vrf_views.push(vrf.into()); + self + } + + pub fn push_import_vrf_view>(&mut self, vrf: S) { + self.import_vrf_views.push(vrf.into()); + } + + #[must_use] + pub fn set_import_vrf_views(mut self, vrfs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.import_vrf_views = vrfs.into_iter().map(Into::into).collect(); + self + } } #[derive(Clone, Debug, Default)] diff --git a/dataplane/Cargo.toml b/dataplane/Cargo.toml index fe8d430f2..320c130bf 100644 --- a/dataplane/Cargo.toml +++ b/dataplane/Cargo.toml @@ -12,6 +12,7 @@ arrayvec = { workspace = true } axum = { workspace = true, features = ["http1", "tokio"] } axum-server = { workspace = true } concurrency = { workspace = true } +config = { workspace = true } ctrlc = { workspace = true, features = ["termination"] } dpdk = { workspace = true } dyn-iter = { workspace = true } diff --git a/dataplane/src/main.rs b/dataplane/src/main.rs index e770334e6..3ec95d707 100644 --- a/dataplane/src/main.rs +++ b/dataplane/src/main.rs @@ -21,15 +21,14 @@ use pyroscope::PyroscopeAgent; use pyroscope_pprofrs::{PprofConfig, pprof_backend}; use gwname::{get_gw_name, set_gw_name}; -use routing::RouterParamsBuilder; +use routing::{BmpServerParams, RouterParamsBuilder}; use tracectl::{custom_target, get_trace_ctl, trace_target}; use tracing::{error, info, level_filters::LevelFilter}; -use concurrency::syn::{Arc, RwLock}; +use concurrency::sync::{Arc, RwLock}; use config::internal::routing::bgp::BmpOptions; use config::internal::status::DataplaneStatus; -use routing::BmpServerParams; trace_target!("dataplane", LevelFilter::DEBUG, &[]); custom_target!("tonic", LevelFilter::ERROR, &[]); @@ -100,20 +99,23 @@ fn main() { init_logging(); let (bmp_server_params, bmp_client_opts) = if args.bmp_enabled() { - let bind = args.bmp_address(); // SocketAddr - let interval_ms = args.bmp_interval_ms(); // u64 + let bind = args.bmp_address(); + let interval_ms = args.bmp_interval_ms(); info!( "BMP: enabled, listening on {bind}, interval={}ms", interval_ms ); - // Server params for routing BMP listener + // BMP server (for routing crate) let server = BmpServerParams { bind_addr: bind, stats_interval_ms: interval_ms, + min_retry_ms: None, + max_retry_ms: None, }; + // BMP options for FRR (for internal config) let host = bind.ip().to_string(); let port = bind.port(); let client = BmpOptions::new("bmp1", host, port) @@ -173,14 +175,19 @@ fn main() { }; /* router parameters */ - let Ok(router_params) = RouterParamsBuilder::default() + let mut binding = RouterParamsBuilder::default(); + let mut rp_builder = binding .cli_sock_path(args.cli_sock_path()) .cpi_sock_path(args.cpi_sock_path()) .frr_agent_path(args.frr_agent_path()) - .bmp(bmp_server_params) - .dp_status(dp_status.clone()) - .build() - else { + .dp_status(dp_status.clone()); + + // Only set BMP when it's enabled (strip_option setter expects the inner type) + if let Some(server) = bmp_server_params { + rp_builder = rp_builder.bmp(server); + } + + let Ok(router_params) = rp_builder.build() else { error!("Bad router configuration"); panic!("Bad router configuration"); }; @@ -204,8 +211,8 @@ fn main() { natallocatorw: setup.natallocatorw, vpcdtablesw: setup.vpcdtablesw, vpc_stats_store: setup.vpc_stats_store, - dp_status: dp_status.clone(), - bmp_client: bmp_client_opts, + dp_status_r: dp_status.clone(), + bmp_options: bmp_client_opts, }, }) .expect("Failed to start management"); diff --git a/mgmt/src/processor/confbuild/internal.rs b/mgmt/src/processor/confbuild/internal.rs index 4eb7149f9..59d57a791 100644 --- a/mgmt/src/processor/confbuild/internal.rs +++ b/mgmt/src/processor/confbuild/internal.rs @@ -17,11 +17,9 @@ use std::net::Ipv4Addr; use crate::processor::confbuild::namegen::{VpcConfigNames, VpcInterfacesNames}; +use config::internal::routing::bgp::BmpOptions; use config::internal::routing::bgp::{AfIpv4Ucast, AfL2vpnEvpn}; use config::internal::routing::bgp::{BgpConfig, BgpOptions, VrfImports}; -// ── NEW: allow injecting BMP config into InternalConfig -use config::internal::routing::bgp::BmpOptions; - use config::internal::routing::prefixlist::{ IpVer, PrefixList, PrefixListAction, PrefixListEntry, PrefixListMatchLen, PrefixListPrefix, }; @@ -184,12 +182,27 @@ impl VpcRoutingConfigIpv4 { } /// Build BGP config for a VPC VRF -fn vpc_vrf_bgp_config(vpc: &Vpc, asn: u32, router_id: Option) -> BgpConfig { +fn vpc_vrf_bgp_config( + vpc: &Vpc, + asn: u32, + router_id: Option, + bmp: Option<&BmpOptions>, +) -> BgpConfig { let mut bgp = BgpConfig::new(asn).set_vrf_name(vpc.vrf_name()); if let Some(router_id) = router_id { bgp.set_router_id(router_id); } bgp.set_bgp_options(vpc_bgp_options()); + + // If global BMP is provided, clone and add this VRF to its import list, + // then attach that per-VRF BMP to the VRF BGP. The renderer can later + // collate all VRF names and emit `bmp import-vrf-view ` under default BGP. + if let Some(global_bmp) = bmp { + let mut per_vrf_bmp = global_bmp.clone(); + per_vrf_bmp.push_import_vrf_view(vpc.vrf_name()); + bgp.set_bmp_options(per_vrf_bmp); + } + bgp } @@ -225,6 +238,7 @@ fn build_vpc_internal_config( asn: u32, router_id: Option, internal: &mut InternalConfig, + bmp: Option<&BmpOptions>, /* NEW */ ) -> ConfigResult { debug!("Building internal config for vpc '{}'", vpc.name); @@ -232,7 +246,7 @@ fn build_vpc_internal_config( let mut vrf_cfg = vpc_vrf_config(vpc)?; /* build bgp config */ - let mut bgp = vpc_vrf_bgp_config(vpc, asn, router_id); + let mut bgp = vpc_vrf_bgp_config(vpc, asn, router_id, bmp); if vpc.num_peerings() > 0 { let mut vpc_rconfig = VpcRoutingConfigIpv4::new(vpc); // fixme build from scratch / no mut @@ -257,19 +271,23 @@ fn build_internal_overlay_config( asn: u32, router_id: Option, internal: &mut InternalConfig, + bmp: Option<&BmpOptions>, ) -> ConfigResult { debug!("Building overlay config ({} VPCs)", overlay.vpc_table.len()); /* Vpcs and peerings */ for vpc in overlay.vpc_table.values() { - build_vpc_internal_config(vpc, asn, router_id, internal)?; + build_vpc_internal_config(vpc, asn, router_id, internal, bmp)?; } Ok(()) } -/// Top-level function to build internal config from external config, -/// **with optional BMP injection**. -/// Prefer this in mgmt so FRR renderer emits BMP. +/// Public entry — build without BMP +pub fn build_internal_config(config: &GwConfig) -> Result { + build_internal_config_with_bmp(config, None) +} + +/// Public entry — build with BMP (global options replicated per VRF with import list) pub fn build_internal_config_with_bmp( config: &GwConfig, bmp: Option, @@ -283,18 +301,18 @@ pub fn build_internal_config_with_bmp( internal.add_vrf_config(external.underlay.vrf.clone())?; internal.set_vtep(external.underlay.vtep.clone()); - /* Inject global BMP options if provided */ - if let Some(b) = bmp { - // Assumes `InternalConfig::set_bmp_options(BmpOptions) -> &mut Self` - internal.set_bmp_options(b); - } - /* Build overlay config */ if let Some(bgp) = &external.underlay.vrf.bgp { let asn = bgp.asn; let router_id = bgp.router_id; if !external.overlay.vpc_table.is_empty() { - build_internal_overlay_config(&external.overlay, asn, router_id, &mut internal)?; + build_internal_overlay_config( + &external.overlay, + asn, + router_id, + &mut internal, + bmp.as_ref(), /* pass BMP down */ + )?; } else { debug!("The configuration does not specify any VPCs..."); } @@ -308,9 +326,3 @@ pub fn build_internal_config_with_bmp( } Ok(internal) } - -/// Backward-compat shim: builds internal config **without BMP**. -/// Existing call sites keep working; mgmt should switch to `build_internal_config_with_bmp`. -pub fn build_internal_config(config: &GwConfig) -> Result { - build_internal_config_with_bmp(config, None) -} diff --git a/mgmt/src/processor/proc.rs b/mgmt/src/processor/proc.rs index fa3d2bff7..920825abf 100644 --- a/mgmt/src/processor/proc.rs +++ b/mgmt/src/processor/proc.rs @@ -45,7 +45,7 @@ use stats::VpcStatsStore; use vpcmap::VpcDiscriminant; use vpcmap::map::{VpcMap, VpcMapWriter}; -// ── NEW: bring in BmpOptions so we can pass it to internal builder +// bring in BmpOptions to pass through to internal config builder use config::internal::routing::bgp::BmpOptions; /// A request type to the `ConfigProcessor` @@ -127,7 +127,7 @@ pub struct ConfigProcessorParams { // read-handle to shared dataplane status pub dp_status_r: Arc>, - // ── NEW: optional BMP options to inject into InternalConfig + // BMP options to inject into InternalConfig pub bmp_options: Option, } @@ -169,7 +169,7 @@ impl ConfigProcessor { } config.validate()?; - // ── BMP-aware internal builder + // BMP-aware internal builder let internal = build_internal_config_with_bmp(&config, self.proc_params.bmp_options.clone())?; config.set_internal_config(internal); @@ -191,7 +191,7 @@ impl ConfigProcessor { #[allow(unused)] async fn apply_blank_config(&mut self) -> ConfigResult { let mut blank = GwConfig::blank(); - // ── BMP-aware internal builder (even for blank, so FRR reflects BMP if needed) + // BMP-aware internal builder (even for blank, so FRR reflects BMP if needed) let internal = build_internal_config_with_bmp(&blank, self.proc_params.bmp_options.clone())?; blank.set_internal_config(internal); @@ -280,8 +280,7 @@ impl ConfigProcessor { /// RPC handler: get dataplane status async fn handle_get_dataplane_status(&mut self) -> ConfigResponse { - // NOTE: std::sync::RwLock::read() returns Result. - // Unwrap the guard, then clone the status. + // std::sync::RwLock::read() -> Result. Unwrap then clone. let mut status: DataplaneStatus = { let guard = self .proc_params diff --git a/routing/src/lib.rs b/routing/src/lib.rs index 44f552bed..6c05cec5b 100644 --- a/routing/src/lib.rs +++ b/routing/src/lib.rs @@ -45,7 +45,7 @@ pub use rib::encapsulation::{Encapsulation, VxlanEncapsulation}; pub use rib::vrf::{RouterVrfConfig, VrfId}; pub use router::ctl::RouterCtlSender; -pub use router::{Router, RouterParams, RouterParamsBuilder}; +pub use router::{BmpServerParams, Router, RouterParams, RouterParamsBuilder}; pub use cli::pretty_utils::Heading; From 504c4f68a9ff25f51a42b39141cda77cd83448c9 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 9 Dec 2025 13:09:13 +0400 Subject: [PATCH 07/14] feat(status): Update FRR renderer for per-VRF BMP mirroring Signed-off-by: Sergey Matov --- routing/src/frr/renderer/bgp.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index cc5857ce4..ab3cda866 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -63,16 +63,16 @@ impl Render for BmpOptions { cfg += MARKER; cfg += format!("bmp targets {}", self.target_name); - // connect line + // connect line (FRR: `bmp connect HOST port [min-retry ...|max-retry ...] [source-interface IFNAME]`) let mut connect = format!(" bmp connect {} port {}", self.connect_host, self.port); if let (Some(minr), Some(maxr)) = (self.min_retry_ms, self.max_retry_ms) { connect.push_str(&format!(" min-retry {minr} max-retry {maxr}")); } if let Some(src) = &self.source { - match src { - BmpSource::Address(ip) => connect.push_str(&format!(" source {}", ip)), - BmpSource::Interface(ifn) => connect.push_str(&format!(" source {}", ifn)), + if let BmpSource::Interface(ifn) = src { + connect.push_str(&format!(" source-interface {}", ifn)); } + // NOTE: FRR BMP does not support binding by IP address; ignore BmpSource::Address. } cfg += connect; @@ -90,7 +90,7 @@ impl Render for BmpOptions { cfg += " bmp monitor ipv6 unicast post-policy"; } - // stats + // stats (FRR: `bmp stats [interval ]`) cfg += format!(" bmp stats interval {}", self.stats_interval_ms); cfg += "exit"; @@ -502,7 +502,7 @@ impl Render for BgpConfig { config += format!(" bgp router-id {router_id}"); } - /* BGP options: todo */ + /* BGP options */ config += self.options.render(&()); /* BGP neighbors */ @@ -513,7 +513,7 @@ impl Render for BgpConfig { .as_ref() .map(|evpn| config += evpn.render(self)); - /* Address family ipv4 unicast */ + /* Address family ipv6 unicast */ self.af_ipv6unicast .as_ref() .map(|evpn| config += evpn.render(self)); @@ -670,16 +670,16 @@ pub mod tests { /* set the IPv4 unicast config */ bgp.set_af_ipv4unicast(af_ipv4); - /* AF ipv4 unicast */ + /* AF ipv6 unicast */ let mut af_ipv6 = AfIpv6Ucast::new(); - /* configure ipv4 vrf imports */ + /* configure ipv6 vrf imports */ let mut imports = VrfImports::new().set_routemap("Import-into-vrf-1-ipv6"); imports.add_vrf("VPC-2"); imports.add_vrf("VPC-3"); imports.add_vrf("VPC-4"); - /* set the imports for Ipv6 */ + /* set the imports for IPv6 */ af_ipv6.set_vrf_imports(imports); /* add some network */ From 5b10d86dd018d0da4aac76657b7e938b424512b2 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 9 Dec 2025 13:19:51 +0400 Subject: [PATCH 08/14] feat(status): test: add BmpConfig into mgmt tests Signed-off-by: Sergey Matov --- mgmt/src/processor/confbuild/internal.rs | 2 +- mgmt/src/tests/mgmt.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/mgmt/src/processor/confbuild/internal.rs b/mgmt/src/processor/confbuild/internal.rs index 59d57a791..cd4159da0 100644 --- a/mgmt/src/processor/confbuild/internal.rs +++ b/mgmt/src/processor/confbuild/internal.rs @@ -238,7 +238,7 @@ fn build_vpc_internal_config( asn: u32, router_id: Option, internal: &mut InternalConfig, - bmp: Option<&BmpOptions>, /* NEW */ + bmp: Option<&BmpOptions>, ) -> ConfigResult { debug!("Building internal config for vpc '{}'", vpc.name); diff --git a/mgmt/src/tests/mgmt.rs b/mgmt/src/tests/mgmt.rs index 308721940..e59c8321c 100644 --- a/mgmt/src/tests/mgmt.rs +++ b/mgmt/src/tests/mgmt.rs @@ -48,9 +48,12 @@ pub mod test { use tracing::debug; use stats::VpcMapName; - use stats::VpcStatsStore; // <-- added + use stats::VpcStatsStore; use vpcmap::map::VpcMapWriter; + use concurrency::sync::{Arc, RwLock}; + use config::internal::status::DataplaneStatus; + /* OVERLAY config sample builders */ fn sample_vpc_table() -> VpcTable { let mut vpc_table = VpcTable::new(); @@ -395,9 +398,12 @@ pub mod test { /* crate VniTables for dst_vni_lookup */ let vpcdtablesw = VpcDiscTablesWriter::new(); - /* NEW: VPC stats store (Arc) */ + /* VPC stats store (Arc) */ let vpc_stats_store = VpcStatsStore::new(); + let dp_status_r: Arc> = + Arc::new(RwLock::new(DataplaneStatus::new())); + /* build configuration of mgmt config processor */ let processor_config = ConfigProcessorParams { router_ctl, @@ -406,6 +412,8 @@ pub mod test { natallocatorw, vpcdtablesw, vpc_stats_store, + dp_status_r, + bmp_options: None, }; /* start config processor to test the processing of a config. The processor embeds the config database From eaa3467041ebe22f8d5be4533ceda98819a7aae4 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 9 Dec 2025 14:25:55 +0400 Subject: [PATCH 09/14] feat(status): Run BMP in own runtime Signed-off-by: Sergey Matov --- routing/src/bmp/mod.rs | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs index 45c233685..b340ec150 100644 --- a/routing/src/bmp/mod.rs +++ b/routing/src/bmp/mod.rs @@ -12,7 +12,7 @@ use concurrency::sync::{Arc, RwLock}; use config::internal::status::DataplaneStatus; use netgauze_bmp_pkt::BmpMessage; use tokio::task::JoinHandle; -use tracing::info; +use tracing::{error, info}; /// Background BMP server runner that updates shared dataplane status. pub struct StatusHandler { @@ -28,7 +28,7 @@ impl StatusHandler { #[async_trait::async_trait] impl handler::BmpHandler for StatusHandler { async fn on_message(&self, _peer: std::net::SocketAddr, msg: BmpMessage) { - // Your `concurrency::sync::RwLock` returns Result like std:: + // `concurrency::sync::RwLock` mirrors std::sync::RwLock error semantics let mut guard = self .dp_status .write() @@ -37,12 +37,18 @@ impl handler::BmpHandler for StatusHandler { } } -/// Spawn BMP server in background +/// Spawn BMP server in background. +/// +/// This function is safe to call from both async and non-async contexts: +/// - If a Tokio runtime is already present, the task is spawned on it. +/// - If not, a new multi-thread runtime is created and **leaked** for the +/// lifetime of the process so the returned JoinHandle remains valid. pub fn spawn_background( bind: std::net::SocketAddr, dp_status: Arc>, ) -> JoinHandle<()> { - tokio::spawn(async move { + // The future we want to run + let fut = async move { info!("starting BMP server on {}", bind); let cfg = BmpServerConfig { bind_addr: bind, @@ -50,7 +56,21 @@ pub fn spawn_background( }; let srv = BmpServer::new(cfg, StatusHandler::new(dp_status)); if let Err(e) = srv.run().await { - tracing::error!("bmp server terminated: {e:#}"); + error!("bmp server terminated: {e:#}"); } - }) + }; + + // Try to spawn on an existing runtime; if none, create one and leak it. + match tokio::runtime::Handle::try_current() { + Ok(handle) => handle.spawn(fut), + Err(_) => { + // No runtime in scope: build one and leak it for daemon lifetime. + let rt = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .build() + .expect("failed to build Tokio runtime for BMP"); + let rt_static: &'static tokio::runtime::Runtime = Box::leak(Box::new(rt)); + rt_static.spawn(fut) + } + } } From 2bd749e9a2639e7ab29d4977ecb17a42fc22dccc Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 9 Dec 2025 14:50:05 +0400 Subject: [PATCH 10/14] feat(stats): Make sure that BMP builds only for default VRF Signed-off-by: Sergey Matov --- mgmt/src/processor/confbuild/internal.rs | 42 ++++++++++++------------ routing/src/frr/renderer/bgp.rs | 18 ++++++---- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/mgmt/src/processor/confbuild/internal.rs b/mgmt/src/processor/confbuild/internal.rs index cd4159da0..f1ed2db9e 100644 --- a/mgmt/src/processor/confbuild/internal.rs +++ b/mgmt/src/processor/confbuild/internal.rs @@ -181,28 +181,17 @@ impl VpcRoutingConfigIpv4 { } } -/// Build BGP config for a VPC VRF +/// Build BGP config for a VPC VRF (no BMP attached here!) fn vpc_vrf_bgp_config( vpc: &Vpc, asn: u32, router_id: Option, - bmp: Option<&BmpOptions>, ) -> BgpConfig { let mut bgp = BgpConfig::new(asn).set_vrf_name(vpc.vrf_name()); if let Some(router_id) = router_id { bgp.set_router_id(router_id); } bgp.set_bgp_options(vpc_bgp_options()); - - // If global BMP is provided, clone and add this VRF to its import list, - // then attach that per-VRF BMP to the VRF BGP. The renderer can later - // collate all VRF names and emit `bmp import-vrf-view ` under default BGP. - if let Some(global_bmp) = bmp { - let mut per_vrf_bmp = global_bmp.clone(); - per_vrf_bmp.push_import_vrf_view(vpc.vrf_name()); - bgp.set_bmp_options(per_vrf_bmp); - } - bgp } @@ -238,15 +227,14 @@ fn build_vpc_internal_config( asn: u32, router_id: Option, internal: &mut InternalConfig, - bmp: Option<&BmpOptions>, ) -> ConfigResult { debug!("Building internal config for vpc '{}'", vpc.name); /* build VRF config */ let mut vrf_cfg = vpc_vrf_config(vpc)?; - /* build bgp config */ - let mut bgp = vpc_vrf_bgp_config(vpc, asn, router_id, bmp); + /* build bgp config (no BMP here) */ + let mut bgp = vpc_vrf_bgp_config(vpc, asn, router_id); if vpc.num_peerings() > 0 { let mut vpc_rconfig = VpcRoutingConfigIpv4::new(vpc); // fixme build from scratch / no mut @@ -271,13 +259,12 @@ fn build_internal_overlay_config( asn: u32, router_id: Option, internal: &mut InternalConfig, - bmp: Option<&BmpOptions>, ) -> ConfigResult { debug!("Building overlay config ({} VPCs)", overlay.vpc_table.len()); /* Vpcs and peerings */ for vpc in overlay.vpc_table.values() { - build_vpc_internal_config(vpc, asn, router_id, internal, bmp)?; + build_vpc_internal_config(vpc, asn, router_id, internal)?; } Ok(()) } @@ -287,7 +274,7 @@ pub fn build_internal_config(config: &GwConfig) -> Result, @@ -296,9 +283,23 @@ pub fn build_internal_config_with_bmp( debug!("Building internal config for gen {genid}"); let external = &config.external; - /* Build internal config: device and underlay configs are copied as received */ + // Prepare default VRF (possibly inject global BMP with VRF import views) + let mut default_vrf = external.underlay.vrf.clone(); + + // If BMP is provided and default VRF has BGP, attach BMP there and add import views + if let (Some(mut bmp_opts), Some(bgp_default)) = (bmp, default_vrf.bgp.as_mut()) { + // Collect all overlay VRF names to import + for vpc in external.overlay.vpc_table.values() { + // requires BmpOptions::push_import_vrf_view(String) + bmp_opts.push_import_vrf_view(vpc.vrf_name()); + } + // Inject BMP into default VRF BGP + bgp_default.set_bmp_options(bmp_opts); + } + + /* Build internal config: device and underlay configs are copied as received (with adjusted default_vrf) */ let mut internal = InternalConfig::new(external.device.clone()); - internal.add_vrf_config(external.underlay.vrf.clone())?; + internal.add_vrf_config(default_vrf)?; internal.set_vtep(external.underlay.vtep.clone()); /* Build overlay config */ @@ -311,7 +312,6 @@ pub fn build_internal_config_with_bmp( asn, router_id, &mut internal, - bmp.as_ref(), /* pass BMP down */ )?; } else { debug!("The configuration does not specify any VPCs..."); diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index ab3cda866..0f4429df9 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -63,7 +63,7 @@ impl Render for BmpOptions { cfg += MARKER; cfg += format!("bmp targets {}", self.target_name); - // connect line (FRR: `bmp connect HOST port [min-retry ...|max-retry ...] [source-interface IFNAME]`) + // connect line (FRR: `bmp connect HOST port [min-retry ... max-retry ...] [source-interface IFNAME]`) let mut connect = format!(" bmp connect {} port {}", self.connect_host, self.port); if let (Some(minr), Some(maxr)) = (self.min_retry_ms, self.max_retry_ms) { connect.push_str(&format!(" min-retry {minr} max-retry {maxr}")); @@ -72,7 +72,6 @@ impl Render for BmpOptions { if let BmpSource::Interface(ifn) = src { connect.push_str(&format!(" source-interface {}", ifn)); } - // NOTE: FRR BMP does not support binding by IP address; ignore BmpSource::Address. } cfg += connect; @@ -90,9 +89,14 @@ impl Render for BmpOptions { cfg += " bmp monitor ipv6 unicast post-policy"; } - // stats (FRR: `bmp stats [interval ]`) + // stats (FRR: `bmp stats interval `) cfg += format!(" bmp stats interval {}", self.stats_interval_ms); + // import-vrf-view lines (rendered only under default VRF) + for vrf in &self.import_vrf_views { + cfg += format!(" bmp import-vrf-view {}", vrf); + } + cfg += "exit"; cfg += MARKER; cfg @@ -523,9 +527,11 @@ impl Render for BgpConfig { .as_ref() .map(|evpn| config += evpn.render(self)); - /* BMP options */ - if let Some(bmp) = &self.bmp { - config += bmp.render(&()); + /* BMP options: only emit under the default VRF (global BGP context) */ + if self.vrf.is_none() { + if let Some(bmp) = &self.bmp { + config += bmp.render(&()); + } } config += "exit"; From 50bcdccdb05ec38f1146964369a4e0fcd4929256 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 9 Dec 2025 21:04:25 +0400 Subject: [PATCH 11/14] feat(status): BMPv3 implementation and status update Signed-off-by: Sergey Matov --- routing/src/bmp/bmp_render.rs | 236 +++++++++++++++++++++++++++++----- routing/src/bmp/mod.rs | 10 +- 2 files changed, 205 insertions(+), 41 deletions(-) diff --git a/routing/src/bmp/bmp_render.rs b/routing/src/bmp/bmp_render.rs index 54f4ece91..458bad2b6 100644 --- a/routing/src/bmp/bmp_render.rs +++ b/routing/src/bmp/bmp_render.rs @@ -1,15 +1,24 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -//! Minimal & safe renderer for NetGauze 0.8.0 BMP messages. -//! This is intentionally conservative to avoid API mismatches. You can -//! extend it to count prefixes/messages per neighbor/VRF once the exact -//! status schema is settled. +//! Minimal & safe renderer for NetGauze BMP messages into DataplaneStatus. +//! Keeps API usage conservative and aligned with the code you pasted. -use netgauze_bmp_pkt::BmpMessage; +use netgauze_bgp_pkt::BgpMessage; +use netgauze_bmp_pkt::{BmpMessage, BmpPeerType}; +use netgauze_bmp_pkt::v3::{ + BmpMessageValue, PeerDownNotificationMessage, PeerUpNotificationMessage, + RouteMonitoringMessage, StatisticsReportMessage, +}; -// Bring in your status type (the module path below matches your earlier usage) -use config::internal::status::DataplaneStatus; +use config::internal::status::{ + BgpMessageCounters, BgpMessages, BgpNeighborPrefixes, BgpNeighborSessionState, + BgpNeighborStatus, BgpStatus, BgpVrfStatus, DataplaneStatus, +}; + +// ----------------------------------------------------------------------------- +// Public entry points +// ----------------------------------------------------------------------------- /// Backward-compat shim for older callers (typo in earlier drafts). #[inline] @@ -17,29 +26,192 @@ pub fn hande_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { handle_bmp_message(status, msg) } -/// Primary entry point: update `DataplaneStatus` from a single BMP message. -/// -/// For now, we keep this a no-op body that compiles cleanly with NetGauze 0.8.0, -/// so the server can be integrated first. You can expand this to: -/// - track per-neighbor session state on PeerUp/PeerDown -/// - count UPDATEs / KEEPALIVEs per direction -/// - count v4/v6 NLRI using `BgpMessage::Update { .. }` + `path_attributes()` -/// - derive pre/post-policy from PeerHeader flags (v3) -pub fn handle_bmp_message(_status: &mut DataplaneStatus, _msg: &BmpMessage) { - // Intentionally left as a no-op to keep the build green while - // we align on exact NetGauze accessors across all message types. - // Safe expansion path (sketch): - // - // match msg { - // BmpMessage::V3(v) => match v { - // netgauze_bmp_pkt::v3::BmpMessageValue::PeerUpNotification(n) => { ... } - // netgauze_bmp_pkt::v3::BmpMessageValue::PeerDownNotification(n) => { ... } - // netgauze_bmp_pkt::v3::BmpMessageValue::RouteMonitoring(rm) => { - // let pdu = rm.bgp_message(); - // if let netgauze_bgp_pkt::BgpMessage::Update(upd) = pdu { ... } - // } - // _ => {} - // }, - // BmpMessage::V4(_v) => { /* optional later */ } - // } +/// Update `DataplaneStatus` from a single BMP message. +pub fn handle_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { + match msg { + BmpMessage::V3(v) => match v { + BmpMessageValue::PeerUpNotification(pu) => on_peer_up(status, pu), + BmpMessageValue::PeerDownNotification(pd) => on_peer_down(status, pd), + BmpMessageValue::RouteMonitoring(rm) => on_route_monitoring(status, rm), + BmpMessageValue::StatisticsReport(sr) => on_statistics(status, sr), + // The rest are currently ignored + _ => {} + }, + // V4 not handled yet + BmpMessage::V4(_) => {} + } +} + +// ----------------------------------------------------------------------------- +// Helpers: mapping / extraction +// ----------------------------------------------------------------------------- + +fn key_from_peer_header(peer: &netgauze_bmp_pkt::PeerHeader) -> String { + // Build a stable-ish key: "---" + let id = peer.bgp_id(); + let asn = peer.peer_as(); + let ip = peer + .address() + .map(|a| a.to_string()) + .unwrap_or_else(|| "none".to_string()); + let rd = peer + .rd() + .map(|rd| format!("{rd:?}")) + .unwrap_or_else(|| "no-rd".to_string()); + format!("{id}-{asn}-{ip}-{rd}") +} + +fn get_vrf_from_peer_header(peer: &netgauze_bmp_pkt::PeerHeader) -> String { + // If peer has an RD, use it as VRF view name; else "default" + match peer.rd() { + Some(rd) => format!("{rd:?}"), + None => "default".to_string(), + } +} + +fn set_neighbor_session_state(n: &mut BgpNeighborStatus, st: BgpNeighborSessionState) { + n.session_state = st; +} + +fn ensure_bgp(status: &mut DataplaneStatus) -> &mut BgpStatus { + if status.bgp.is_none() { + status.bgp = Some(BgpStatus::default()); + } + status.bgp.as_mut().unwrap() +} + +fn ensure_vrf<'a>(bgp: &'a mut BgpStatus, vrf: &str) -> &'a mut BgpVrfStatus { + bgp.vrfs.entry(vrf.to_string()).or_default() +} + +fn ensure_neighbor<'a>( + vrf: &'a mut BgpVrfStatus, + neigh_key: &str, +) -> &'a mut BgpNeighborStatus { + vrf.neighbors + .entry(neigh_key.to_string()) + .or_insert_with(|| BgpNeighborStatus { + enabled: true, + ..BgpNeighborStatus::default() + }) +} + +fn post_policy_from_peer_type(pt: BmpPeerType) -> bool { + match pt { + BmpPeerType::GlobalInstancePeer { post_policy, .. } => post_policy, + BmpPeerType::RdInstancePeer { post_policy, .. } => post_policy, + BmpPeerType::LocalInstancePeer { post_policy, .. } => post_policy, + BmpPeerType::LocRibInstancePeer { .. } => false, + BmpPeerType::Experimental251 { .. } + | BmpPeerType::Experimental252 { .. } + | BmpPeerType::Experimental253 { .. } + | BmpPeerType::Experimental254 { .. } => false, + } +} + +// ----------------------------------------------------------------------------- +// Handlers +// ----------------------------------------------------------------------------- + +fn on_peer_up(status: &mut DataplaneStatus, pu: &PeerUpNotificationMessage) { + let peer = pu.peer_header(); + let vrf = get_vrf_from_peer_header(peer); + let key = key_from_peer_header(peer); + + let bgp = ensure_bgp(status); + let vrf_s = ensure_vrf(bgp, &vrf); + let neigh = ensure_neighbor(vrf_s, &key); + + // Update some basic fields we know now + neigh.peer_as = peer.peer_as(); + neigh.remote_router_id = peer.bgp_id().to_string(); + neigh.peer_port = pu.remote_port().unwrap_or_default() as u32; + set_neighbor_session_state(neigh, BgpNeighborSessionState::Established); + + // Optional: try to infer local_as from the OPEN we sent (kept simple) + if let BgpMessage::Open(open) = pu.sent_message() { + neigh.local_as = open.my_as() as u32; + } + + // Initialize empty message counters on establishment + if neigh.messages.is_none() { + neigh.messages = Some(BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + }); + } +} + +fn on_peer_down(status: &mut DataplaneStatus, pd: &PeerDownNotificationMessage) { + let peer = pd.peer_header(); + let vrf = get_vrf_from_peer_header(peer); + let key = key_from_peer_header(peer); + + if let Some(bgp) = status.bgp.as_mut() { + if let Some(vrf_s) = bgp.vrfs.get_mut(&vrf) { + if let Some(neigh) = vrf_s.neighbors.get_mut(&key) { + set_neighbor_session_state(neigh, BgpNeighborSessionState::Idle); + neigh.connections_dropped = neigh.connections_dropped.saturating_add(1); + } + } + } +} + +fn on_route_monitoring(status: &mut DataplaneStatus, rm: &RouteMonitoringMessage) { + let peer = rm.peer_header(); + let vrf = get_vrf_from_peer_header(peer); + let key = key_from_peer_header(peer); + + let bgp = ensure_bgp(status); + let vrf_s = ensure_vrf(bgp, &vrf); + let neigh = ensure_neighbor(vrf_s, &key); + + // Ensure message counters exist + let msgs = neigh.messages.get_or_insert_with(|| BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + }); + + // Count UPDATE messages received + if let BgpMessage::Update(_) = rm.update_message() { + if let Some(rcv) = msgs.received.as_mut() { + rcv.update = rcv.update.saturating_add(1); + } + } + + // Very rough pre/post-policy NLRI accounting example + let post = post_policy_from_peer_type(peer.peer_type()); + let pref = neigh + .ipv4_unicast_prefixes + .get_or_insert_with(BgpNeighborPrefixes::default); + + // We don't parse NLRI depth here; increment by 1 as a placeholder per RM message + if post { + pref.received_pre_policy = pref + .received_pre_policy + .saturating_add(0); // post-policy => don't bump pre + pref.received = pref.received.saturating_add(1); + } else { + pref.received_pre_policy = pref.received_pre_policy.saturating_add(1); + pref.received = pref.received.saturating_add(1); + } +} + +fn on_statistics(status: &mut DataplaneStatus, sr: &StatisticsReportMessage) { + let peer = sr.peer_header(); + let vrf = get_vrf_from_peer_header(peer); + let key = key_from_peer_header(peer); + + let bgp = ensure_bgp(status); + let vrf_s = ensure_vrf(bgp, &vrf); + let neigh = ensure_neighbor(vrf_s, &key); + + // Make sure we have message counters present + let _ = neigh.messages.get_or_insert_with(|| BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + }); + + // You can walk `sr.counters()` and map specific statistics to your model later. + // For now, we keep StatisticsReport as a no-op for counters. } diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs index b340ec150..0e2e4f831 100644 --- a/routing/src/bmp/mod.rs +++ b/routing/src/bmp/mod.rs @@ -28,7 +28,6 @@ impl StatusHandler { #[async_trait::async_trait] impl handler::BmpHandler for StatusHandler { async fn on_message(&self, _peer: std::net::SocketAddr, msg: BmpMessage) { - // `concurrency::sync::RwLock` mirrors std::sync::RwLock error semantics let mut guard = self .dp_status .write() @@ -37,12 +36,7 @@ impl handler::BmpHandler for StatusHandler { } } -/// Spawn BMP server in background. -/// -/// This function is safe to call from both async and non-async contexts: -/// - If a Tokio runtime is already present, the task is spawned on it. -/// - If not, a new multi-thread runtime is created and **leaked** for the -/// lifetime of the process so the returned JoinHandle remains valid. +/// Spawn BMP server in background pub fn spawn_background( bind: std::net::SocketAddr, dp_status: Arc>, @@ -60,11 +54,9 @@ pub fn spawn_background( } }; - // Try to spawn on an existing runtime; if none, create one and leak it. match tokio::runtime::Handle::try_current() { Ok(handle) => handle.spawn(fut), Err(_) => { - // No runtime in scope: build one and leak it for daemon lifetime. let rt = tokio::runtime::Builder::new_multi_thread() .enable_all() .build() From 27249e8ec94c87191cd2add59befef5ddde735be Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Thu, 11 Dec 2025 17:00:07 +0400 Subject: [PATCH 12/14] feat(status): style fixes and formatting Signed-off-by: Sergey Matov --- args/src/lib.rs | 9 +- config/src/internal/routing/bgp.rs | 124 +---------------------- config/src/internal/routing/mod.rs | 1 + dataplane/src/main.rs | 27 ++--- mgmt/src/processor/confbuild/internal.rs | 21 ++-- mgmt/src/processor/proc.rs | 7 +- routing/src/bmp/bmp_render.rs | 39 +------ routing/src/bmp/server.rs | 4 +- routing/src/frr/renderer/bgp.rs | 5 +- routing/src/frr/test.rs | 6 ++ routing/src/router/mod.rs | 4 +- 11 files changed, 46 insertions(+), 201 deletions(-) diff --git a/args/src/lib.rs b/args/src/lib.rs index 3ec96f67b..68cbef743 100644 --- a/args/src/lib.rs +++ b/args/src/lib.rs @@ -593,7 +593,7 @@ pub struct BmpConfigSection { /// 3. **Transfer**: Passed via sealed memfd to the worker process /// 4. **Worker Process**: Calls [`LaunchConfiguration::inherit()`] to access the config /// -/// // TODO: implement bytecheck::Validate in addition to CheckBytes on all components of the launch config. +/// TODO: implement bytecheck::Validate in addition to CheckBytes on all components of the launch config. #[derive( Debug, PartialEq, @@ -1309,7 +1309,6 @@ E.g. default=error,all=info,nat=debug will set the default target to error, and #[arg(long, default_value_t = false, help = "Enable BMP server")] bmp_enable: bool, - /// BMP bind address #[arg( long, value_name = "IP:PORT", @@ -1318,7 +1317,6 @@ E.g. default=error,all=info,nat=debug will set the default target to error, and )] bmp_address: SocketAddr, - /// BMP periodic interval for housekeeping/flush (ms) #[arg( long, value_name = "MILLISECONDS", @@ -1510,13 +1508,11 @@ impl CmdArgs { self.pyroscope_url.as_ref() } -<<<<<<< HEAD /// Get the name to configure this gateway with. #[must_use] pub fn get_name(&self) -> Option<&String> { self.name.as_ref() -======= - // ===== BMP getters ===== + } #[must_use] pub fn bmp_enabled(&self) -> bool { self.bmp_enable @@ -1528,7 +1524,6 @@ impl CmdArgs { #[must_use] pub fn bmp_interval_ms(&self) -> u64 { self.bmp_interval_ms ->>>>>>> 7dac51ae (feat(status): Populate routing and mgmt with BMP params) } } diff --git a/config/src/internal/routing/bgp.rs b/config/src/internal/routing/bgp.rs index 6bf9e565c..6d5b67026 100644 --- a/config/src/internal/routing/bgp.rs +++ b/config/src/internal/routing/bgp.rs @@ -9,6 +9,7 @@ use lpm::prefix::Prefix; use std::collections::BTreeSet; use std::net::{IpAddr, Ipv4Addr}; +use super::bmp::BmpOptions; // FRR defaults {datacenter | traditional} #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -68,7 +69,7 @@ pub struct BgpNeighCapabilities { pub ext_nhop: bool, pub fqdn: bool, pub software_ver: bool, - // TODO: ORF + // ORF } #[derive(Clone, Debug)] @@ -179,126 +180,6 @@ impl Default for BgpOptions { } } -#[derive(Clone, Debug)] -pub enum BmpSource { - Address(IpAddr), - Interface(String), -} - -#[derive(Clone, Debug)] -pub struct BmpOptions { - /// Name for `bmp targets ` - pub target_name: String, - /// Collector host/IP in `bmp connect` - pub connect_host: String, - /// Collector TCP port - pub port: u16, - /// Optional local source (address or interface) - pub source: Option, - /// Optional reconnect backoff (ms) - pub min_retry_ms: Option, - pub max_retry_ms: Option, - /// `bmp stats interval` (ms) - pub stats_interval_ms: u64, - /// Monitoring toggles - pub monitor_ipv4_pre: bool, - pub monitor_ipv4_post: bool, - pub monitor_ipv6_pre: bool, - pub monitor_ipv6_post: bool, - - /// VRFs/views to import into the default BMP instance: - /// renders as multiple `bmp import-vrf-view ` - pub import_vrf_views: Vec, -} - -impl Default for BmpOptions { - fn default() -> Self { - Self { - target_name: "bmp1".to_string(), - connect_host: "127.0.0.1".to_string(), - port: 5000, - source: None, - min_retry_ms: Some(1_000), - max_retry_ms: Some(20_000), - stats_interval_ms: 60_000, - monitor_ipv4_pre: true, - monitor_ipv4_post: true, - monitor_ipv6_pre: false, - monitor_ipv6_post: false, - import_vrf_views: Vec::new(), - } - } -} - -impl BmpOptions { - #[must_use] - pub fn new, H: Into>( - target_name: T, - connect_host: H, - port: u16, - ) -> Self { - Self { - target_name: target_name.into(), - connect_host: connect_host.into(), - port, - ..Default::default() - } - } - #[must_use] - pub fn set_source_addr(mut self, ip: IpAddr) -> Self { - self.source = Some(BmpSource::Address(ip)); - self - } - #[must_use] - pub fn set_source_interface>(mut self, ifname: S) -> Self { - self.source = Some(BmpSource::Interface(ifname.into())); - self - } - #[must_use] - pub fn set_retry_ms(mut self, min_ms: u64, max_ms: u64) -> Self { - self.min_retry_ms = Some(min_ms); - self.max_retry_ms = Some(max_ms); - self - } - #[must_use] - pub fn set_stats_interval_ms(mut self, ms: u64) -> Self { - self.stats_interval_ms = ms; - self - } - #[must_use] - pub fn monitor_ipv4(mut self, pre: bool, post: bool) -> Self { - self.monitor_ipv4_pre = pre; - self.monitor_ipv4_post = post; - self - } - #[must_use] - pub fn monitor_ipv6(mut self, pre: bool, post: bool) -> Self { - self.monitor_ipv6_pre = pre; - self.monitor_ipv6_post = post; - self - } - - #[must_use] - pub fn add_import_vrf_view>(mut self, vrf: S) -> Self { - self.import_vrf_views.push(vrf.into()); - self - } - - pub fn push_import_vrf_view>(&mut self, vrf: S) { - self.import_vrf_views.push(vrf.into()); - } - - #[must_use] - pub fn set_import_vrf_views(mut self, vrfs: I) -> Self - where - I: IntoIterator, - S: Into, - { - self.import_vrf_views = vrfs.into_iter().map(Into::into).collect(); - self - } -} - #[derive(Clone, Debug, Default)] /// A BGP instance config, within a certain VRF pub struct BgpConfig { @@ -350,6 +231,7 @@ impl AfIpv4Ucast { pub fn set_vrf_imports(&mut self, imports: VrfImports) { self.imports = Some(imports); } + // redistribution is configured by adding one or more redistribute objects pub fn redistribute(&mut self, redistribute: Redistribute) { self.redistribute.push(redistribute); } diff --git a/config/src/internal/routing/mod.rs b/config/src/internal/routing/mod.rs index fa4eca0aa..325b49305 100644 --- a/config/src/internal/routing/mod.rs +++ b/config/src/internal/routing/mod.rs @@ -5,6 +5,7 @@ pub mod bfd; pub mod bgp; +pub mod bmp; pub mod evpn; pub mod frr; pub mod ospf; diff --git a/dataplane/src/main.rs b/dataplane/src/main.rs index 3ec95d707..13619fa5d 100644 --- a/dataplane/src/main.rs +++ b/dataplane/src/main.rs @@ -27,7 +27,7 @@ use tracectl::{custom_target, get_trace_ctl, trace_target}; use tracing::{error, info, level_filters::LevelFilter}; use concurrency::sync::{Arc, RwLock}; -use config::internal::routing::bgp::BmpOptions; +use config::internal::routing::bmp::BmpOptions; use config::internal::status::DataplaneStatus; trace_target!("dataplane", LevelFilter::DEBUG, &[]); @@ -89,16 +89,8 @@ fn process_tracing_cmds(args: &CmdArgs) { } } -#[allow(clippy::too_many_lines)] -fn main() { - let args = CmdArgs::parse(); - if let Err(e) = init_name(&args) { - eprintln!("Failed to set gateway name: {e}"); - std::process::exit(1); - } - init_logging(); - - let (bmp_server_params, bmp_client_opts) = if args.bmp_enabled() { +fn parse_bmp_params(args: &CmdArgs) -> (Option, Option) { + if args.bmp_enabled() { let bind = args.bmp_address(); let interval_ms = args.bmp_interval_ms(); @@ -127,7 +119,18 @@ fn main() { } else { info!("BMP: disabled"); (None, None) - }; + } +} + +fn main() { + let args = CmdArgs::parse(); + if let Err(e) = init_name(&args) { + eprintln!("Failed to set gateway name: {e}"); + std::process::exit(1); + } + init_logging(); + + let (bmp_server_params, bmp_client_opts) = parse_bmp_params(&args); let dp_status: Arc> = Arc::new(RwLock::new(DataplaneStatus::new())); diff --git a/mgmt/src/processor/confbuild/internal.rs b/mgmt/src/processor/confbuild/internal.rs index f1ed2db9e..6c1c6d741 100644 --- a/mgmt/src/processor/confbuild/internal.rs +++ b/mgmt/src/processor/confbuild/internal.rs @@ -17,9 +17,9 @@ use std::net::Ipv4Addr; use crate::processor::confbuild::namegen::{VpcConfigNames, VpcInterfacesNames}; -use config::internal::routing::bgp::BmpOptions; use config::internal::routing::bgp::{AfIpv4Ucast, AfL2vpnEvpn}; use config::internal::routing::bgp::{BgpConfig, BgpOptions, VrfImports}; +use config::internal::routing::bmp::BmpOptions; use config::internal::routing::prefixlist::{ IpVer, PrefixList, PrefixListAction, PrefixListEntry, PrefixListMatchLen, PrefixListPrefix, }; @@ -181,12 +181,8 @@ impl VpcRoutingConfigIpv4 { } } -/// Build BGP config for a VPC VRF (no BMP attached here!) -fn vpc_vrf_bgp_config( - vpc: &Vpc, - asn: u32, - router_id: Option, -) -> BgpConfig { +/// Build BGP config for a VPC VRF (bmp is applied elsewhere) +fn vpc_vrf_bgp_config(vpc: &Vpc, asn: u32, router_id: Option) -> BgpConfig { let mut bgp = BgpConfig::new(asn).set_vrf_name(vpc.vrf_name()); if let Some(router_id) = router_id { bgp.set_router_id(router_id); @@ -233,7 +229,7 @@ fn build_vpc_internal_config( /* build VRF config */ let mut vrf_cfg = vpc_vrf_config(vpc)?; - /* build bgp config (no BMP here) */ + /* build bgp config */ let mut bgp = vpc_vrf_bgp_config(vpc, asn, router_id); if vpc.num_peerings() > 0 { @@ -269,7 +265,7 @@ fn build_internal_overlay_config( Ok(()) } -/// Public entry — build without BMP +/// without BMP pub fn build_internal_config(config: &GwConfig) -> Result { build_internal_config_with_bmp(config, None) } @@ -307,12 +303,7 @@ pub fn build_internal_config_with_bmp( let asn = bgp.asn; let router_id = bgp.router_id; if !external.overlay.vpc_table.is_empty() { - build_internal_overlay_config( - &external.overlay, - asn, - router_id, - &mut internal, - )?; + build_internal_overlay_config(&external.overlay, asn, router_id, &mut internal)?; } else { debug!("The configuration does not specify any VPCs..."); } diff --git a/mgmt/src/processor/proc.rs b/mgmt/src/processor/proc.rs index 920825abf..b9f7c8389 100644 --- a/mgmt/src/processor/proc.rs +++ b/mgmt/src/processor/proc.rs @@ -46,7 +46,7 @@ use vpcmap::VpcDiscriminant; use vpcmap::map::{VpcMap, VpcMapWriter}; // bring in BmpOptions to pass through to internal config builder -use config::internal::routing::bgp::BmpOptions; +use config::internal::routing::bmp::BmpOptions; /// A request type to the `ConfigProcessor` #[derive(Debug)] @@ -280,7 +280,6 @@ impl ConfigProcessor { /// RPC handler: get dataplane status async fn handle_get_dataplane_status(&mut self) -> ConfigResponse { - // std::sync::RwLock::read() -> Result. Unwrap then clone. let mut status: DataplaneStatus = { let guard = self .proc_params @@ -675,10 +674,6 @@ async fn apply_gw_config( let pairs = update_stats_vpc_mappings(config, vpcmapw); drop(pairs); // pairs used by caller - // NOTE: If we need to inject/override BMP per-VRF here, prefer adding a helper once - // InternalConfig exposes a mutable VRF accessor. For now, BMP config is built in - // the internal build stage and applied by the routing layer. - /* apply config in router */ apply_router_config(&kernel_vrfs, config, router_ctl).await?; diff --git a/routing/src/bmp/bmp_render.rs b/routing/src/bmp/bmp_render.rs index 458bad2b6..64e636d7c 100644 --- a/routing/src/bmp/bmp_render.rs +++ b/routing/src/bmp/bmp_render.rs @@ -1,31 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Open Network Fabric Authors -//! Minimal & safe renderer for NetGauze BMP messages into DataplaneStatus. -//! Keeps API usage conservative and aligned with the code you pasted. +//! BMP message handlers to update internal DataplaneStatus model. use netgauze_bgp_pkt::BgpMessage; -use netgauze_bmp_pkt::{BmpMessage, BmpPeerType}; use netgauze_bmp_pkt::v3::{ BmpMessageValue, PeerDownNotificationMessage, PeerUpNotificationMessage, RouteMonitoringMessage, StatisticsReportMessage, }; +use netgauze_bmp_pkt::{BmpMessage, BmpPeerType}; use config::internal::status::{ BgpMessageCounters, BgpMessages, BgpNeighborPrefixes, BgpNeighborSessionState, BgpNeighborStatus, BgpStatus, BgpVrfStatus, DataplaneStatus, }; -// ----------------------------------------------------------------------------- -// Public entry points -// ----------------------------------------------------------------------------- - -/// Backward-compat shim for older callers (typo in earlier drafts). -#[inline] -pub fn hande_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { - handle_bmp_message(status, msg) -} - /// Update `DataplaneStatus` from a single BMP message. pub fn handle_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { match msg { @@ -41,11 +30,6 @@ pub fn handle_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { BmpMessage::V4(_) => {} } } - -// ----------------------------------------------------------------------------- -// Helpers: mapping / extraction -// ----------------------------------------------------------------------------- - fn key_from_peer_header(peer: &netgauze_bmp_pkt::PeerHeader) -> String { // Build a stable-ish key: "---" let id = peer.bgp_id(); @@ -84,10 +68,7 @@ fn ensure_vrf<'a>(bgp: &'a mut BgpStatus, vrf: &str) -> &'a mut BgpVrfStatus { bgp.vrfs.entry(vrf.to_string()).or_default() } -fn ensure_neighbor<'a>( - vrf: &'a mut BgpVrfStatus, - neigh_key: &str, -) -> &'a mut BgpNeighborStatus { +fn ensure_neighbor<'a>(vrf: &'a mut BgpVrfStatus, neigh_key: &str) -> &'a mut BgpNeighborStatus { vrf.neighbors .entry(neigh_key.to_string()) .or_insert_with(|| BgpNeighborStatus { @@ -109,10 +90,6 @@ fn post_policy_from_peer_type(pt: BmpPeerType) -> bool { } } -// ----------------------------------------------------------------------------- -// Handlers -// ----------------------------------------------------------------------------- - fn on_peer_up(status: &mut DataplaneStatus, pu: &PeerUpNotificationMessage) { let peer = pu.peer_header(); let vrf = get_vrf_from_peer_header(peer); @@ -127,13 +104,10 @@ fn on_peer_up(status: &mut DataplaneStatus, pu: &PeerUpNotificationMessage) { neigh.remote_router_id = peer.bgp_id().to_string(); neigh.peer_port = pu.remote_port().unwrap_or_default() as u32; set_neighbor_session_state(neigh, BgpNeighborSessionState::Established); - - // Optional: try to infer local_as from the OPEN we sent (kept simple) if let BgpMessage::Open(open) = pu.sent_message() { neigh.local_as = open.my_as() as u32; } - // Initialize empty message counters on establishment if neigh.messages.is_none() { neigh.messages = Some(BgpMessages { received: Some(BgpMessageCounters::new()), @@ -187,9 +161,7 @@ fn on_route_monitoring(status: &mut DataplaneStatus, rm: &RouteMonitoringMessage // We don't parse NLRI depth here; increment by 1 as a placeholder per RM message if post { - pref.received_pre_policy = pref - .received_pre_policy - .saturating_add(0); // post-policy => don't bump pre + pref.received_pre_policy = pref.received_pre_policy.saturating_add(0); // post-policy => don't bump pre pref.received = pref.received.saturating_add(1); } else { pref.received_pre_policy = pref.received_pre_policy.saturating_add(1); @@ -212,6 +184,5 @@ fn on_statistics(status: &mut DataplaneStatus, sr: &StatisticsReportMessage) { sent: Some(BgpMessageCounters::new()), }); - // You can walk `sr.counters()` and map specific statistics to your model later. - // For now, we keep StatisticsReport as a no-op for counters. + //TODO: smatov: add more later } diff --git a/routing/src/bmp/server.rs b/routing/src/bmp/server.rs index d80fd5076..cdf11d054 100644 --- a/routing/src/bmp/server.rs +++ b/routing/src/bmp/server.rs @@ -103,8 +103,6 @@ async fn handle_peer( if cfg.tcp_nodelay { let _ = sock.set_nodelay(true); } - // NOTE: cfg.tcp_recv_buf kept for future tuning; Tokio has no stable API to set recv buf. - // Frame the stream as BMP let codec = BmpCodec::default(); let mut reader = FramedRead::new(sock, codec); @@ -112,7 +110,7 @@ async fn handle_peer( while let Some(frame) = reader.next().await { match frame { Ok(msg) => { - // netgauze_bmp_pkt::BmpMessage for both v3 and v4 + // netgauze_bmp_pkt::BmpMessage for both v3 and v4. TODO: smatov: v4 handling handler.on_message(peer, msg).await; } Err(e) => { diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index 0f4429df9..9aa7c5080 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -326,6 +326,7 @@ impl Render for AfIpv4Ucast { cfg += MARKER; cfg += "address-family ipv4 unicast"; + /* activate neighbors in AF */ bgp.neighbors .iter() .filter(|neigh| neigh.ipv4_unicast) @@ -361,6 +362,7 @@ impl Render for AfIpv6Ucast { cfg += MARKER; cfg += "address-family ipv6 unicast"; + /* activate neighbors in AF */ bgp.neighbors .iter() .filter(|neigh| neigh.ipv6_unicast) @@ -396,6 +398,7 @@ impl Render for AfL2vpnEvpn { cfg += MARKER; cfg += "address-family l2vpn evpn"; + /* activate neighbors in AF */ bgp.neighbors .iter() .filter(|neigh| neigh.l2vpn_evpn) @@ -506,7 +509,7 @@ impl Render for BgpConfig { config += format!(" bgp router-id {router_id}"); } - /* BGP options */ + /* BGP options: todo */ config += self.options.render(&()); /* BGP neighbors */ diff --git a/routing/src/frr/test.rs b/routing/src/frr/test.rs index d535c970a..0d3e75324 100644 --- a/routing/src/frr/test.rs +++ b/routing/src/frr/test.rs @@ -127,14 +127,20 @@ pub mod tests { use std::time::Duration; use tracing_test::traced_test; + use config::internal::status::DataplaneStatus; + use tokio::sync::watch; + #[traced_test] #[tokio::test] async fn test_fake_frr_agent() { + let (_dp_status_tx, dp_status_rx) = watch::channel(DataplaneStatus::default()); + /* set router params */ let router_params = RouterParamsBuilder::default() .cpi_sock_path("/tmp/cpi.sock") .cli_sock_path("/tmp/cli.sock") .frr_agent_path("/tmp/frr-agent.sock") + .dp_status(dp_status_rx) .build() .expect("Should succeed due to defaults"); diff --git a/routing/src/router/mod.rs b/routing/src/router/mod.rs index 92eb6948e..607607e84 100644 --- a/routing/src/router/mod.rs +++ b/routing/src/router/mod.rs @@ -173,8 +173,8 @@ impl Router { Ok(router) } - /// Stop this router. This stops the router IO task and drops the interface table, adjacency table, - /// VRF table and the FIB table. + /// Stop this router. This stops the router IO thread and drops the interface table, adjacency table + /// vrf table and the fib table. pub fn stop(&mut self) { if let Err(e) = self.rio_handle.finish() { error!("Failed to stop IO for router '{}': {e}", self.name); From 7548ecd3e13d8c345ab2b7241313c94df3838588 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Tue, 16 Dec 2025 18:17:04 +0400 Subject: [PATCH 13/14] feat(status): Move BMP into separate config file Signed-off-by: Sergey Matov --- config/src/internal/routing/bmp.rs | 134 +++++++++++++++++++++++++++++ mgmt/src/tests/mgmt.rs | 7 +- routing/src/frr/renderer/bgp.rs | 16 +++- routing/src/frr/test.rs | 10 +-- 4 files changed, 158 insertions(+), 9 deletions(-) create mode 100644 config/src/internal/routing/bmp.rs diff --git a/config/src/internal/routing/bmp.rs b/config/src/internal/routing/bmp.rs new file mode 100644 index 000000000..635db02be --- /dev/null +++ b/config/src/internal/routing/bmp.rs @@ -0,0 +1,134 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Open Network Fabric Authors + +//! Dataplane configuration model: BMP (BGP Monitoring Protocol) + +#![allow(unused)] + +use std::net::IpAddr; + +#[derive(Clone, Debug)] +pub enum BmpSource { + Address(IpAddr), + Interface(String), +} + +#[derive(Clone, Debug)] +pub struct BmpOptions { + /// Name for `bmp targets ` + pub target_name: String, + /// Collector host/IP in `bmp connect` + pub connect_host: String, + /// Collector TCP port + pub port: u16, + /// Optional local source (address or interface) + pub source: Option, + /// Optional reconnect backoff (ms) + pub min_retry_ms: Option, + pub max_retry_ms: Option, + /// `bmp stats interval` (ms) + pub stats_interval_ms: u64, + /// Monitoring toggles + pub monitor_ipv4_pre: bool, + pub monitor_ipv4_post: bool, + pub monitor_ipv6_pre: bool, + pub monitor_ipv6_post: bool, + + /// VRFs/views to import into the default BMP instance: + /// renders as multiple `bmp import-vrf-view ` + pub import_vrf_views: Vec, +} + +impl Default for BmpOptions { + fn default() -> Self { + Self { + target_name: "bmp1".to_string(), + connect_host: "127.0.0.1".to_string(), + port: 5000, + source: None, + min_retry_ms: Some(1_000), + max_retry_ms: Some(20_000), + stats_interval_ms: 60_000, + monitor_ipv4_pre: true, + monitor_ipv4_post: true, + monitor_ipv6_pre: false, + monitor_ipv6_post: false, + import_vrf_views: Vec::new(), + } + } +} + +impl BmpOptions { + #[must_use] + pub fn new, H: Into>( + target_name: T, + connect_host: H, + port: u16, + ) -> Self { + Self { + target_name: target_name.into(), + connect_host: connect_host.into(), + port, + ..Default::default() + } + } + + #[must_use] + pub fn set_source_addr(mut self, ip: IpAddr) -> Self { + self.source = Some(BmpSource::Address(ip)); + self + } + + #[must_use] + pub fn set_source_interface>(mut self, ifname: S) -> Self { + self.source = Some(BmpSource::Interface(ifname.into())); + self + } + + #[must_use] + pub fn set_retry_ms(mut self, min_ms: u64, max_ms: u64) -> Self { + self.min_retry_ms = Some(min_ms); + self.max_retry_ms = Some(max_ms); + self + } + + #[must_use] + pub fn set_stats_interval_ms(mut self, ms: u64) -> Self { + self.stats_interval_ms = ms; + self + } + + #[must_use] + pub fn monitor_ipv4(mut self, pre: bool, post: bool) -> Self { + self.monitor_ipv4_pre = pre; + self.monitor_ipv4_post = post; + self + } + + #[must_use] + pub fn monitor_ipv6(mut self, pre: bool, post: bool) -> Self { + self.monitor_ipv6_pre = pre; + self.monitor_ipv6_post = post; + self + } + + #[must_use] + pub fn add_import_vrf_view>(mut self, vrf: S) -> Self { + self.import_vrf_views.push(vrf.into()); + self + } + + pub fn push_import_vrf_view>(&mut self, vrf: S) { + self.import_vrf_views.push(vrf.into()); + } + + #[must_use] + pub fn set_import_vrf_views(mut self, vrfs: I) -> Self + where + I: IntoIterator, + S: Into, + { + self.import_vrf_views = vrfs.into_iter().map(Into::into).collect(); + self + } +} diff --git a/mgmt/src/tests/mgmt.rs b/mgmt/src/tests/mgmt.rs index e59c8321c..44adda608 100644 --- a/mgmt/src/tests/mgmt.rs +++ b/mgmt/src/tests/mgmt.rs @@ -367,11 +367,15 @@ pub mod test { /* build a gw config from a sample external config */ let config = GwConfig::new(external); + let dp_status_r: Arc> = + Arc::new(RwLock::new(DataplaneStatus::new())); + /* build router config */ let router_params = RouterParamsBuilder::default() .cpi_sock_path("/tmp/cpi.sock") .cli_sock_path("/tmp/cli.sock") .frr_agent_path("/tmp/frr-agent.sock") + .dp_status(dp_status_r.clone()) .build() .expect("Should succeed due to defaults"); @@ -401,9 +405,6 @@ pub mod test { /* VPC stats store (Arc) */ let vpc_stats_store = VpcStatsStore::new(); - let dp_status_r: Arc> = - Arc::new(RwLock::new(DataplaneStatus::new())); - /* build configuration of mgmt config processor */ let processor_config = ConfigProcessorParams { router_ctl, diff --git a/routing/src/frr/renderer/bgp.rs b/routing/src/frr/renderer/bgp.rs index 9aa7c5080..0a328a292 100644 --- a/routing/src/frr/renderer/bgp.rs +++ b/routing/src/frr/renderer/bgp.rs @@ -12,7 +12,7 @@ use config::internal::routing::bgp::Redistribute; use config::internal::routing::bgp::VrfImports; use config::internal::routing::bgp::{AfIpv4Ucast, AfIpv6Ucast, AfL2vpnEvpn}; use config::internal::routing::bgp::{BgpNeighCapabilities, Protocol}; -use config::internal::routing::bgp::{BmpOptions, BmpSource}; +use config::internal::routing::bmp::{BmpOptions, BmpSource}; /* impl Display */ impl Rendered for BgpNeighType { @@ -551,6 +551,7 @@ pub mod tests { AfL2vpnEvpn, BgpConfig, BgpNeighbor, NeighSendCommunities, Protocol, Redistribute, VrfImports, }; + use config::internal::routing::bmp::{BmpOptions, BmpSource}; use lpm::prefix::Prefix; use std::net::{IpAddr, Ipv4Addr}; use std::str::FromStr; @@ -573,6 +574,19 @@ pub mod tests { bgp.set_bgp_options(options); + let bmp = BmpOptions::new("bmp1", "127.0.0.1", 5000) + .set_retry_ms(1_000, 20_000) + .set_stats_interval_ms(60_000) + .monitor_ipv4(true, true) + .monitor_ipv6(false, false); + let bmp = BmpOptions { + source: Some(BmpSource::Interface("lo".to_string())), + ..bmp + } + .add_import_vrf_view("VPC-2") + .add_import_vrf_view("VPC-3"); + bgp.set_bmp_options(bmp); + let n1 = BgpNeighbor::new_host(IpAddr::from_str("7.0.0.3").expect("Bad address")) .set_remote_as(65001) .set_description("A neighbor that does not belong to a peer group") diff --git a/routing/src/frr/test.rs b/routing/src/frr/test.rs index 0d3e75324..4f217c17c 100644 --- a/routing/src/frr/test.rs +++ b/routing/src/frr/test.rs @@ -124,23 +124,23 @@ pub mod tests { use super::fake_frr_agent::*; use crate::config::RouterConfig; use crate::{Router, RouterParamsBuilder}; - use std::time::Duration; - use tracing_test::traced_test; - + use concurrency::sync::{Arc, RwLock}; use config::internal::status::DataplaneStatus; + use std::time::Duration; use tokio::sync::watch; + use tracing_test::traced_test; #[traced_test] #[tokio::test] async fn test_fake_frr_agent() { - let (_dp_status_tx, dp_status_rx) = watch::channel(DataplaneStatus::default()); + let dp_status: Arc> = Arc::new(RwLock::new(DataplaneStatus::new())); /* set router params */ let router_params = RouterParamsBuilder::default() .cpi_sock_path("/tmp/cpi.sock") .cli_sock_path("/tmp/cli.sock") .frr_agent_path("/tmp/frr-agent.sock") - .dp_status(dp_status_rx) + .dp_status(dp_status) .build() .expect("Should succeed due to defaults"); From 51e56a872957b6571e61cd3e5a05b07d521e2b39 Mon Sep 17 00:00:00 2001 From: Sergey Matov Date: Wed, 17 Dec 2025 18:28:02 +0400 Subject: [PATCH 14/14] dbg: add debug logs Signed-off-by: Sergey Matov --- routing/src/bmp/bmp_render.rs | 122 +++++++++++++++++++++++++++++++--- routing/src/bmp/mod.rs | 15 +++-- routing/src/bmp/server.rs | 2 + 3 files changed, 123 insertions(+), 16 deletions(-) diff --git a/routing/src/bmp/bmp_render.rs b/routing/src/bmp/bmp_render.rs index 64e636d7c..525587911 100644 --- a/routing/src/bmp/bmp_render.rs +++ b/routing/src/bmp/bmp_render.rs @@ -15,6 +15,8 @@ use config::internal::status::{ BgpNeighborStatus, BgpStatus, BgpVrfStatus, DataplaneStatus, }; +use tracing::debug; + /// Update `DataplaneStatus` from a single BMP message. pub fn handle_bmp_message(status: &mut DataplaneStatus, msg: &BmpMessage) { match msg { @@ -60,15 +62,22 @@ fn set_neighbor_session_state(n: &mut BgpNeighborStatus, st: BgpNeighborSessionS fn ensure_bgp(status: &mut DataplaneStatus) -> &mut BgpStatus { if status.bgp.is_none() { status.bgp = Some(BgpStatus::default()); + debug!("BMP: initialized DataplaneStatus.bgp"); } status.bgp.as_mut().unwrap() } fn ensure_vrf<'a>(bgp: &'a mut BgpStatus, vrf: &str) -> &'a mut BgpVrfStatus { + if !bgp.vrfs.contains_key(vrf) { + debug!("BMP: creating VRF status entry: vrf={vrf}"); + } bgp.vrfs.entry(vrf.to_string()).or_default() } fn ensure_neighbor<'a>(vrf: &'a mut BgpVrfStatus, neigh_key: &str) -> &'a mut BgpNeighborStatus { + if !vrf.neighbors.contains_key(neigh_key) { + debug!("BMP: creating neighbor status entry: key={neigh_key}"); + } vrf.neighbors .entry(neigh_key.to_string()) .or_insert_with(|| BgpNeighborStatus { @@ -95,14 +104,25 @@ fn on_peer_up(status: &mut DataplaneStatus, pu: &PeerUpNotificationMessage) { let vrf = get_vrf_from_peer_header(peer); let key = key_from_peer_header(peer); + let peer_as = peer.peer_as(); + let bgp_id = peer.bgp_id().to_string(); + let peer_addr = peer.address().map(|a| a.to_string()).unwrap_or_else(|| "none".to_string()); + let peer_port = pu.remote_port().unwrap_or_default() as u32; + let bgp = ensure_bgp(status); let vrf_s = ensure_vrf(bgp, &vrf); let neigh = ensure_neighbor(vrf_s, &key); + let prev_state = neigh.session_state; + let prev_peer_as = neigh.peer_as; + let prev_remote_id = neigh.remote_router_id.clone(); + let prev_peer_port = neigh.peer_port; + let prev_local_as = neigh.local_as; + // Update some basic fields we know now - neigh.peer_as = peer.peer_as(); - neigh.remote_router_id = peer.bgp_id().to_string(); - neigh.peer_port = pu.remote_port().unwrap_or_default() as u32; + neigh.peer_as = peer_as; + neigh.remote_router_id = bgp_id.clone(); + neigh.peer_port = peer_port; set_neighbor_session_state(neigh, BgpNeighborSessionState::Established); if let BgpMessage::Open(open) = pu.sent_message() { neigh.local_as = open.my_as() as u32; @@ -113,7 +133,28 @@ fn on_peer_up(status: &mut DataplaneStatus, pu: &PeerUpNotificationMessage) { received: Some(BgpMessageCounters::new()), sent: Some(BgpMessageCounters::new()), }); + debug!( + "BMP: dp_status push: created message counters (peer-up) vrf={} key={}", + vrf, key + ); } + + debug!( + "BMP: dp_status push (peer-up) vrf={} key={} peer_addr={} prev_state={:?} new_state={:?} prev_peer_as={} new_peer_as={} prev_remote_id={} new_remote_id={} prev_peer_port={} new_peer_port={} prev_local_as={} new_local_as={}", + vrf, + key, + peer_addr, + prev_state, + neigh.session_state, + prev_peer_as, + neigh.peer_as, + prev_remote_id, + neigh.remote_router_id, + prev_peer_port, + neigh.peer_port, + prev_local_as, + neigh.local_as, + ); } fn on_peer_down(status: &mut DataplaneStatus, pd: &PeerDownNotificationMessage) { @@ -124,10 +165,29 @@ fn on_peer_down(status: &mut DataplaneStatus, pd: &PeerDownNotificationMessage) if let Some(bgp) = status.bgp.as_mut() { if let Some(vrf_s) = bgp.vrfs.get_mut(&vrf) { if let Some(neigh) = vrf_s.neighbors.get_mut(&key) { + let prev_state = neigh.session_state; + let prev_dropped = neigh.connections_dropped; + set_neighbor_session_state(neigh, BgpNeighborSessionState::Idle); neigh.connections_dropped = neigh.connections_dropped.saturating_add(1); + + debug!( + "BMP: dp_status push (peer-down) vrf={} key={} prev_state={:?} new_state={:?} prev_connections_dropped={} new_connections_dropped={}", + vrf, + key, + prev_state, + neigh.session_state, + prev_dropped, + neigh.connections_dropped + ); + } else { + debug!("BMP: peer-down for unknown neighbor: vrf={} key={}", vrf, key); } + } else { + debug!("BMP: peer-down for unknown vrf: vrf={} key={}", vrf, key); } + } else { + debug!("BMP: peer-down but DataplaneStatus.bgp is None (vrf={} key={})", vrf, key); } } @@ -136,29 +196,46 @@ fn on_route_monitoring(status: &mut DataplaneStatus, rm: &RouteMonitoringMessage let vrf = get_vrf_from_peer_header(peer); let key = key_from_peer_header(peer); + let post = post_policy_from_peer_type(peer.peer_type()); + let bgp = ensure_bgp(status); let vrf_s = ensure_vrf(bgp, &vrf); let neigh = ensure_neighbor(vrf_s, &key); // Ensure message counters exist - let msgs = neigh.messages.get_or_insert_with(|| BgpMessages { - received: Some(BgpMessageCounters::new()), - sent: Some(BgpMessageCounters::new()), + let msgs = neigh.messages.get_or_insert_with(|| { + debug!( + "BMP: dp_status push: created message counters (route-monitoring) vrf={} key={}", + vrf, key + ); + BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + } }); // Count UPDATE messages received + let mut bumped_update = false; + let mut prev_update = None; + let mut new_update = None; + if let BgpMessage::Update(_) = rm.update_message() { if let Some(rcv) = msgs.received.as_mut() { + prev_update = Some(rcv.update); rcv.update = rcv.update.saturating_add(1); + new_update = Some(rcv.update); + bumped_update = true; } } // Very rough pre/post-policy NLRI accounting example - let post = post_policy_from_peer_type(peer.peer_type()); let pref = neigh .ipv4_unicast_prefixes .get_or_insert_with(BgpNeighborPrefixes::default); + let prev_received = pref.received; + let prev_received_pre = pref.received_pre_policy; + // We don't parse NLRI depth here; increment by 1 as a placeholder per RM message if post { pref.received_pre_policy = pref.received_pre_policy.saturating_add(0); // post-policy => don't bump pre @@ -167,6 +244,20 @@ fn on_route_monitoring(status: &mut DataplaneStatus, rm: &RouteMonitoringMessage pref.received_pre_policy = pref.received_pre_policy.saturating_add(1); pref.received = pref.received.saturating_add(1); } + + debug!( + "BMP: dp_status push (route-monitoring) vrf={} key={} post_policy={} update_bumped={} update_prev={:?} update_new={:?} ipv4_received_prev={} ipv4_received_new={} ipv4_received_pre_prev={} ipv4_received_pre_new={}", + vrf, + key, + post, + bumped_update, + prev_update, + new_update, + prev_received, + pref.received, + prev_received_pre, + pref.received_pre_policy + ); } fn on_statistics(status: &mut DataplaneStatus, sr: &StatisticsReportMessage) { @@ -179,10 +270,21 @@ fn on_statistics(status: &mut DataplaneStatus, sr: &StatisticsReportMessage) { let neigh = ensure_neighbor(vrf_s, &key); // Make sure we have message counters present - let _ = neigh.messages.get_or_insert_with(|| BgpMessages { - received: Some(BgpMessageCounters::new()), - sent: Some(BgpMessageCounters::new()), + let _ = neigh.messages.get_or_insert_with(|| { + debug!( + "BMP: dp_status push: created message counters (statistics-report) vrf={} key={}", + vrf, key + ); + BgpMessages { + received: Some(BgpMessageCounters::new()), + sent: Some(BgpMessageCounters::new()), + } }); + debug!( + "BMP: dp_status push (statistics-report) vrf={} key={} (TODO: decode stats later)", + vrf, key + ); + //TODO: smatov: add more later } diff --git a/routing/src/bmp/mod.rs b/routing/src/bmp/mod.rs index 0e2e4f831..070a941e0 100644 --- a/routing/src/bmp/mod.rs +++ b/routing/src/bmp/mod.rs @@ -12,7 +12,7 @@ use concurrency::sync::{Arc, RwLock}; use config::internal::status::DataplaneStatus; use netgauze_bmp_pkt::BmpMessage; use tokio::task::JoinHandle; -use tracing::{error, info}; +use tracing::{error, info, debug}; /// Background BMP server runner that updates shared dataplane status. pub struct StatusHandler { @@ -28,11 +28,14 @@ impl StatusHandler { #[async_trait::async_trait] impl handler::BmpHandler for StatusHandler { async fn on_message(&self, _peer: std::net::SocketAddr, msg: BmpMessage) { - let mut guard = self - .dp_status - .write() - .expect("dataplane status lock poisoned"); - bmp_render::handle_bmp_message(&mut *guard, &msg); + { + let mut guard = self + .dp_status + .write() + .expect("dataplane status lock poisoned"); + bmp_render::handle_bmp_message(&mut *guard, &msg); + } + debug!("BMP: released dataplane status write guard after handling message"); } } diff --git a/routing/src/bmp/server.rs b/routing/src/bmp/server.rs index cdf11d054..a9ed7d67b 100644 --- a/routing/src/bmp/server.rs +++ b/routing/src/bmp/server.rs @@ -16,6 +16,7 @@ use tokio::net::{TcpListener, TcpStream}; use tokio::task::JoinSet; use tokio_util::codec::FramedRead; +use tracing::debug; use crate::bmp::handler::BmpHandler; #[derive(Clone, Debug)] @@ -110,6 +111,7 @@ async fn handle_peer( while let Some(frame) = reader.next().await { match frame { Ok(msg) => { + debug!("BMP: received message from {}: {:?}", peer, msg); // netgauze_bmp_pkt::BmpMessage for both v3 and v4. TODO: smatov: v4 handling handler.on_message(peer, msg).await; }