Skip to main content

netget/server/bmp/
actions.rs

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            // BMP defines no collector-to-router message, so there is nothing to answer with:
248            // a failure closes the session, which is the only signal the router can see.
249            .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}