Skip to main content

netget/client/bmp/
actions.rs

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                // Build the message now so a malformed action is refused before anything is sent.
248                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}