1use crate::llm::actions::{
2 protocol_trait::{ActionResult, Protocol, Server},
3 ActionDefinition, Parameter, ParameterDefinition,
4};
5use crate::protocol::log_template::LogTemplate;
6use crate::protocol::{EventType, SpawnContext};
7use crate::state::app_state::AppState;
8use anyhow::{bail, ensure, Result};
9use serde_json::{json, Value};
10use std::sync::LazyLock;
11
12#[derive(Default)]
13pub struct BmpProtocol;
14impl BmpProtocol {
15 pub fn new() -> Self {
16 Self
17 }
18}
19
20pub fn parameter(name: &str, kind: &str, description: &str, required: bool) -> Parameter {
21 Parameter {
22 name: name.into(),
23 type_hint: kind.into(),
24 description: description.into(),
25 required,
26 }
27}
28
29pub fn action(
30 name: &str,
31 description: &str,
32 parameters: Vec<Parameter>,
33 example: Value,
34) -> ActionDefinition {
35 ActionDefinition {
36 name: name.into(),
37 description: description.into(),
38 parameters,
39 example,
40 log_template: Some(LogTemplate::new().with_info(format!("BMP {name}"))),
41 }
42}
43
44fn continue_action() -> ActionDefinition {
45 action(
46 "bmp_continue",
47 "Keep monitoring this router: the collector reads its next BMP message (a collector never writes to the router)",
48 vec![],
49 json!({"type": "bmp_continue"}),
50 )
51}
52
53fn close_action() -> ActionDefinition {
54 action(
55 "bmp_close",
56 "Stop monitoring this router: the collector closes the BMP session (the router will usually reconnect and resend its tables)",
57 vec![parameter("reason", "string", "Why, for the log", false)],
58 json!({"type": "bmp_close", "reason": "unknown router"}),
59 )
60}
61
62fn answers() -> Vec<ActionDefinition> {
63 vec![continue_action(), close_action()]
64}
65
66fn peer_parameter() -> Parameter {
67 parameter(
68 "peer",
69 "object",
70 "The monitored BGP peer: {type: global|rd_instance|local_instance|loc_rib, address, asn, bgp_id, distinguisher, post_policy, adj_rib_out, four_octet_as, timestamp}",
71 true,
72 )
73}
74
75fn router_parameter() -> Parameter {
76 parameter(
77 "router",
78 "string",
79 "The router's sysName from its Initiation message, or its address",
80 true,
81 )
82}
83
84fn event(id: &str, description: &str, mut params: Vec<Parameter>) -> EventType {
85 params.insert(0, router_parameter());
86 EventType::new(id, description, continue_action().example.clone())
87 .with_parameters(params)
88 .with_actions(answers())
89}
90
91pub static INITIATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
92 EventType::new(
93 "bmp_initiation",
94 "A router opened a BMP session and identified itself; continue to monitor it or close",
95 continue_action().example.clone(),
96 )
97 .with_parameters(vec![
98 parameter("sys_name", "string", "The router's sysName", false),
99 parameter("sys_descr", "string", "The router's sysDescr", false),
100 parameter("strings", "array", "Free-form information strings", true),
101 ])
102 .with_actions(answers())
103});
104pub static PEER_UP_EVENT: LazyLock<EventType> = LazyLock::new(|| {
105 event(
106 "bmp_peer_up",
107 "A BGP session on the router came up",
108 vec![
109 peer_parameter(),
110 parameter(
111 "local_address",
112 "string",
113 "The router's address on the session",
114 true,
115 ),
116 parameter("local_port", "number", "The router's TCP port", true),
117 parameter("remote_port", "number", "The peer's TCP port", true),
118 parameter(
119 "sent_open",
120 "object",
121 "The OPEN the router sent: {asn, hold_time, bgp_id, capabilities}",
122 true,
123 ),
124 parameter("received_open", "object", "The OPEN the peer sent", true),
125 parameter(
126 "information",
127 "array",
128 "Information TLVs: [{type, value}]",
129 true,
130 ),
131 ],
132 )
133});
134pub static ROUTE_MONITORING_EVENT: LazyLock<EventType> = LazyLock::new(|| {
135 event(
136 "bmp_route_monitoring",
137 "Routes the router received from (or advertised to) a peer: one BGP UPDATE",
138 vec![
139 peer_parameter(),
140 parameter("update", "object", "The UPDATE: {nlri, withdrawn_routes, origin, next_hop, as_path, path_attributes, end_of_rib}", false),
141 parameter("decode_error", "string", "Why the embedded BGP message could not be decoded", false),
142 ],
143 )
144});
145pub static STATISTICS_EVENT: LazyLock<EventType> = LazyLock::new(|| {
146 event(
147 "bmp_statistics",
148 "Counters the router keeps for a peer",
149 vec![
150 peer_parameter(),
151 parameter(
152 "counters",
153 "array",
154 "[{type (IANA name), type_code, value, afi, safi}]",
155 true,
156 ),
157 ],
158 )
159});
160pub static PEER_DOWN_EVENT: LazyLock<EventType> = LazyLock::new(|| {
161 event(
162 "bmp_peer_down",
163 "A BGP session on the router went down",
164 vec![
165 peer_parameter(),
166 parameter("reason", "string", "local_notification, local_no_notification, remote_notification, remote_no_data, peer_deconfigured or local_system_closed", true),
167 parameter("reason_code", "number", "The reason's code", true),
168 parameter("notification", "object", "The NOTIFICATION: {code, subcode, name, subcode_name}", false),
169 parameter("fsm_event", "number", "The BGP FSM event that closed the session", false),
170 ],
171 )
172});
173pub static TERMINATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
174 event(
175 "bmp_termination",
176 "The router is ending the BMP session; the collector closes it after the handler answers",
177 vec![
178 parameter("reason", "string", "administratively_closed, unspecified, out_of_resources, redundant_connection or permanently_administratively_closed", false),
179 parameter("strings", "array", "Free-form information strings", true),
180 ],
181 )
182});
183pub static ROUTE_MIRRORING_EVENT: LazyLock<EventType> = LazyLock::new(|| {
184 event(
185 "bmp_route_mirroring",
186 "Verbatim BGP messages the router mirrored from a peer",
187 vec![
188 peer_parameter(),
189 parameter("messages", "array", "[{update} | {bgp_message} | {information: errored_pdu|messages_lost} | {decode_error}]", true),
190 ],
191 )
192});
193
194pub fn all_events() -> Vec<EventType> {
195 vec![
196 INITIATION_EVENT.clone(),
197 PEER_UP_EVENT.clone(),
198 ROUTE_MONITORING_EVENT.clone(),
199 STATISTICS_EVENT.clone(),
200 PEER_DOWN_EVENT.clone(),
201 TERMINATION_EVENT.clone(),
202 ROUTE_MIRRORING_EVENT.clone(),
203 ]
204}
205
206impl Protocol for BmpProtocol {
207 fn protocol_name(&self) -> &'static str {
208 "BMP"
209 }
210 fn stack_name(&self) -> &'static str {
211 "ETH>IP>TCP>BMP"
212 }
213 fn description(&self) -> &'static str {
214 "BGP Monitoring Protocol collector (RFC 7854): routers report peer up/down, routes and statistics"
215 }
216 fn keywords(&self) -> Vec<&'static str> {
217 vec![
218 "bmp",
219 "bgp monitoring",
220 "rfc7854",
221 "bmp collector",
222 "route monitoring",
223 ]
224 }
225 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
226 vec![]
227 }
228 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
229 answers()
230 }
231 fn get_event_types(&self) -> Vec<EventType> {
232 all_events()
233 }
234 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
235 vec![]
236 }
237 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
238 use crate::protocol::metadata::*;
239 ProtocolMetadataV2::builder()
240 .state(DevelopmentState::Experimental)
241 .privilege_requirement(PrivilegeRequirement::None)
242 .implementation("Native BMP v3 framing, per-peer header and TLVs (RFC 7854, RFC 8671 Adj-RIB-Out, RFC 9069 Loc-RIB); embedded BGP OPEN/UPDATE/NOTIFICATION decoded by netgauze through the bgp feature's wire module")
243 .llm_control("Whether to keep monitoring each router after every message it reports")
244 .e2e_testing("tests/server/bmp: GoBGP 4.9.0 (independent, Go) peers with a second GoBGP and exports Initiation, Peer Up, Route Monitoring, Statistics and Peer Down to NetGet")
245 .notes("A collector never writes: the handler's only decision is to keep or close a session, and no answer closes it. IPv4 and IPv6 peers; UPDATE attributes beyond origin, AS path, next hop, MED, local preference and communities are named only. 256 KiB per message, a started message must finish within 60 s, 256 connections. No storage: persist with memory or SQLite.")
246 .request_only("BMP is one-way: a collector never writes to the router")
247 .deliberately_silent()
250 .max_inbound_bytes(super::codec::MAX_MESSAGE)
251 .build()
252 }
253 fn example_prompt(&self) -> &'static str {
254 "BMP collector on port 11019 that logs every route my routers learn"
255 }
256 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
257 let llm = json!({"type":"open_server","base_stack":"bmp","port":11019,"instruction":"Monitor routers; close any whose sysName does not start with edge-"});
258 let mut static_example = llm.clone();
259 static_example["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"static","actions":[{"type":"bmp_continue"}]}}]);
260 let mut scripted = llm.clone();
261 scripted["event_handlers"] = json!([{"event_pattern":"*","handler":{"type":"script","language":"python","code":"import json,sys\ni=json.load(sys.stdin)\ne=i['event']\nok=i['event_type_id']!='bmp_initiation' or str(e.get('sys_name','')).startswith('edge-')\nprint(json.dumps({'actions':[{'type':'bmp_continue'} if ok else {'type':'bmp_close','reason':'unknown router'}]}))"}}]);
262 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
263 }
264 fn group_name(&self) -> &'static str {
265 "Routing"
266 }
267}
268
269impl Server for BmpProtocol {
270 fn spawn(
271 &self,
272 ctx: SpawnContext,
273 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
274 {
275 Box::pin(super::spawn(ctx))
276 }
277 fn execute_action(&self, v: Value) -> Result<ActionResult> {
278 match v["type"].as_str() {
279 Some("bmp_continue") => {}
280 Some("bmp_close") => {
281 if let Some(r) = v.get("reason").filter(|r| !r.is_null()) {
282 ensure!(
283 r.as_str().is_some_and(|r| r.len() <= 256),
284 "reason is text up to 256 bytes"
285 );
286 }
287 }
288 _ => bail!("Unknown BMP collector action"),
289 }
290 Ok(ActionResult::Custom {
291 name: v["type"].as_str().unwrap_or_default().into(),
292 data: v,
293 })
294 }
295}