1use crate::llm::actions::{
2 client_trait::{Client, ClientActionResult},
3 protocol_trait::Protocol,
4 ActionDefinition, Parameter, ParameterDefinition,
5};
6use crate::protocol::{ConnectContext, EventType};
7use crate::server::bmp::actions::{action, parameter};
8use anyhow::{bail, ensure, Result};
9use serde_json::{json, Value};
10use std::sync::LazyLock;
11
12#[derive(Default)]
13pub struct BmpClientProtocol;
14impl BmpClientProtocol {
15 pub fn new() -> Self {
16 Self
17 }
18}
19
20fn peer_address() -> Parameter {
21 parameter(
22 "peer_address",
23 "string",
24 "The monitored peer, as announced by bmp_peer_up",
25 true,
26 )
27}
28
29fn peer_up() -> ActionDefinition {
30 action(
31 "bmp_peer_up",
32 "Report that a BGP session came up; Rust builds both OPEN messages (four-octet AS capability) from the ASNs and identifiers",
33 vec![
34 parameter("peer", "object", "{address, asn, bgp_id, type: global|rd_instance|local_instance|loc_rib (default global), distinguisher (e.g. 65000:1), post_policy, adj_rib_out, four_octet_as (default true)}", true),
35 parameter("local_address", "string", "This router's address on the session", true),
36 parameter("local_asn", "number", "This router's AS number", true),
37 parameter("local_bgp_id", "string", "This router's BGP identifier (IPv4 form)", true),
38 parameter("local_port", "number", "This router's TCP port (default 179)", false),
39 parameter("remote_port", "number", "The peer's TCP port (default 179)", false),
40 parameter("hold_time", "number", "Hold time in both OPENs (default 90)", false),
41 ],
42 json!({"type": "bmp_peer_up", "peer": {"address": "192.0.2.2", "asn": 65002, "bgp_id": "192.0.2.2"}, "local_address": "192.0.2.1", "local_asn": 65001, "local_bgp_id": "192.0.2.1"}),
43 )
44}
45
46fn route_monitoring() -> ActionDefinition {
47 action(
48 "bmp_route_monitoring",
49 "Report routes received from a peer as one BGP UPDATE built by Rust: IPv4 announcements with their attributes and/or withdrawals",
50 vec![
51 peer_address(),
52 parameter("announce", "array", "IPv4 prefixes announced, e.g. [\"203.0.113.0/24\"]", false),
53 parameter("withdraw", "array", "IPv4 prefixes withdrawn", false),
54 parameter("next_hop", "string", "Next hop for the announcements (required with announce)", false),
55 parameter("as_path", "array", "AS numbers, nearest first", false),
56 parameter("origin", "string", "IGP (default), EGP or INCOMPLETE", false),
57 parameter("med", "number", "MULTI_EXIT_DISC metric for the announcements (lower is preferred)", false),
58 parameter("local_pref", "number", "LOCAL_PREF for the announcements (higher is preferred)", false),
59 parameter("communities", "array", "Communities as \"asn:value\"", false),
60 ],
61 json!({"type": "bmp_route_monitoring", "peer_address": "192.0.2.2", "announce": ["203.0.113.0/24"], "next_hop": "192.0.2.2", "as_path": [65002], "communities": ["65002:100"]}),
62 )
63}
64
65fn statistics() -> ActionDefinition {
66 action(
67 "bmp_statistics",
68 "Report counters for a peer: each by IANA name, with afi and safi for the per-AFI/SAFI gauges",
69 vec![
70 peer_address(),
71 parameter("counters", "array", "[{type: e.g. rejected_prefixes | adj_rib_in_routes | loc_rib_routes_per_afi_safi, value, afi, safi}]", true),
72 ],
73 json!({"type": "bmp_statistics", "peer_address": "192.0.2.2", "counters": [{"type": "rejected_prefixes", "value": 3}, {"type": "adj_rib_in_routes", "value": 1200}]}),
74 )
75}
76
77fn peer_down() -> ActionDefinition {
78 action(
79 "bmp_peer_down",
80 "Report that a peer's BGP session went down, with the reason and its NOTIFICATION or FSM event",
81 vec![
82 peer_address(),
83 parameter("reason", "string", "local_notification, local_no_notification, remote_notification, remote_no_data, peer_deconfigured or local_system_closed", true),
84 parameter("notification", "object", "{code, subcode} for a *_notification reason", false),
85 parameter("fsm_event", "number", "The FSM event for local_no_notification", false),
86 ],
87 json!({"type": "bmp_peer_down", "peer_address": "192.0.2.2", "reason": "remote_notification", "notification": {"code": 6, "subcode": 2}}),
88 )
89}
90
91fn termination() -> ActionDefinition {
92 action(
93 "bmp_termination",
94 "End the BMP session with a Termination message and close the connection",
95 vec![
96 parameter("reason", "string", "administratively_closed (default), unspecified, out_of_resources, redundant_connection or permanently_administratively_closed", false),
97 parameter("message", "string", "A free-form string", false),
98 ],
99 json!({"type": "bmp_termination", "reason": "administratively_closed", "message": "maintenance"}),
100 )
101}
102
103fn disconnect() -> ActionDefinition {
104 action(
105 "disconnect",
106 "Close the connection without a Termination message",
107 vec![],
108 json!({"type": "disconnect"}),
109 )
110}
111
112pub fn all_actions() -> Vec<ActionDefinition> {
113 vec![
114 peer_up(),
115 route_monitoring(),
116 statistics(),
117 peer_down(),
118 termination(),
119 disconnect(),
120 ]
121}
122
123pub static CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
124 EventType::new(
125 "bmp_connected",
126 "Connected to the collector and sent the Initiation message; report peers and routes",
127 peer_up().example.clone(),
128 )
129 .with_parameters(vec![
130 parameter("collector", "string", "The collector's address", true),
131 parameter("sys_name", "string", "The sysName this exporter sent", true),
132 ])
133 .with_actions(all_actions())
134});
135
136pub static CLOSED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
137 EventType::new(
138 "bmp_collector_closed",
139 "The collector closed the connection",
140 json!({"type": "disconnect"}),
141 )
142 .with_parameters(vec![parameter(
143 "reported",
144 "number",
145 "How many messages this exporter sent",
146 true,
147 )])
148 .with_no_actions()
149});
150
151impl Protocol for BmpClientProtocol {
152 fn protocol_name(&self) -> &'static str {
153 "BMP"
154 }
155 fn stack_name(&self) -> &'static str {
156 "ETH>IP>TCP>BMP"
157 }
158 fn keywords(&self) -> Vec<&'static str> {
159 vec!["bmp", "bmp exporter", "bgp monitoring", "rfc7854"]
160 }
161 fn description(&self) -> &'static str {
162 "BGP Monitoring Protocol exporter: reports peers, routes and statistics to a BMP collector"
163 }
164 fn get_async_actions(&self, _: &crate::state::app_state::AppState) -> Vec<ActionDefinition> {
165 all_actions()
166 }
167 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
168 vec![]
169 }
170 fn get_event_types(&self) -> Vec<EventType> {
171 vec![CONNECTED_EVENT.clone(), CLOSED_EVENT.clone()]
172 }
173 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
174 let p =
175 |name: &str, description: &str, example: Value, default: Value| ParameterDefinition {
176 name: name.into(),
177 type_hint: "string".into(),
178 description: description.into(),
179 required: false,
180 example,
181 default: Some(default),
182 };
183 vec![
184 p(
185 "sys_name",
186 "sysName sent in the Initiation message",
187 json!("edge-1"),
188 json!(super::DEFAULT_SYS_NAME),
189 ),
190 p(
191 "sys_descr",
192 "sysDescr sent in the Initiation message",
193 json!("lab router"),
194 json!(super::DEFAULT_SYS_DESCR),
195 ),
196 ]
197 }
198 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
199 use crate::protocol::metadata::*;
200 ProtocolMetadataV2::builder()
201 .state(DevelopmentState::Experimental)
202 .privilege_requirement(PrivilegeRequirement::None)
203 .implementation("The collector's BMP codec as an exporter; OPEN and UPDATE PDUs built by netgauze through the bgp feature's wire module")
204 .llm_control("Which peers come up and go down, the routes and counters reported for them, and when the session ends")
205 .e2e_testing("tests/client/bmp: gobmp 1.1.0 (independent, Go) collects and parses every message type the exporter sends")
206 .notes("IPv4 unicast routes only; no Route Mirroring; peers are remembered by address from bmp_peer_up until bmp_peer_down.")
207 .build()
208 }
209 fn example_prompt(&self) -> &'static str {
210 "Report to the BMP collector at 127.0.0.1:11019 that peer 192.0.2.2 came up and announced 203.0.113.0/24"
211 }
212 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
213 let llm = json!({"type":"open_client","protocol":"bmp","remote_addr":"127.0.0.1:11019","instruction":"Bring peer 192.0.2.2 (AS65002) up and report 203.0.113.0/24 from it"});
214 let mut static_example = llm.clone();
215 static_example["event_handlers"] = json!([{"event_pattern":"bmp_connected","handler":{"type":"static","actions":[peer_up().example, route_monitoring().example]}}]);
216 let mut scripted = llm.clone();
217 scripted["event_handlers"] = json!([{"event_pattern":"bmp_connected","handler":{"type":"script","language":"python","code":"import json\npeer={'address':'192.0.2.2','asn':65002,'bgp_id':'192.0.2.2'}\nprint(json.dumps({'actions':[{'type':'bmp_peer_up','peer':peer,'local_address':'192.0.2.1','local_asn':65001,'local_bgp_id':'192.0.2.1'},{'type':'bmp_route_monitoring','peer_address':'192.0.2.2','announce':['203.0.113.0/24'],'next_hop':'192.0.2.2','as_path':[65002]}]}))"}}]);
218 crate::llm::actions::StartupExamples::new(llm, scripted, static_example)
219 }
220 fn group_name(&self) -> &'static str {
221 "Routing"
222 }
223}
224
225impl Client for BmpClientProtocol {
226 fn connect(
227 &self,
228 ctx: ConnectContext,
229 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
230 {
231 Box::pin(super::connect(ctx))
232 }
233 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
234 ensure!(
235 crate::utils::json_budget::within_budget(&v, 1024 * 1024, 100_000, 16),
236 "action exceeds the BMP bounds"
237 );
238 match v["type"].as_str() {
239 Some("disconnect") => return Ok(ClientActionResult::Disconnect),
240 Some(
241 t @ ("bmp_peer_up"
242 | "bmp_route_monitoring"
243 | "bmp_statistics"
244 | "bmp_peer_down"
245 | "bmp_termination"),
246 ) => {
247 super::Peers::default().validate(t, &v)?;
249 }
250 _ => bail!("Unknown BMP client action"),
251 }
252 Ok(ClientActionResult::Custom {
253 name: v["type"].as_str().unwrap_or_default().into(),
254 data: v,
255 })
256 }
257}