1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::{ConnectContext, EventType};
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static BGP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
20 EventType::new(
21 "bgp_connected",
22 "BGP session with the peer reached Established (OPEN and KEEPALIVE both exchanged). \
23 This client announces no routes; reply with wait_for_more to keep monitoring, or \
24 send_notification to tear the session down.",
25 json!({"type": "wait_for_more"}),
26 )
27 .with_parameters(vec![
28 Parameter {
29 name: "remote_addr".to_string(),
30 type_hint: "string".to_string(),
31 description: "Remote BGP peer address".to_string(),
32 required: true,
33 },
34 Parameter {
35 name: "peer_as".to_string(),
36 type_hint: "number".to_string(),
37 description: "Peer AS number. Taken from the peer's four-octet-AS capability when \
38 it advertised one, so values above 65535 are reported truthfully."
39 .to_string(),
40 required: true,
41 },
42 Parameter {
43 name: "peer_router_id".to_string(),
44 type_hint: "string".to_string(),
45 description: "Peer BGP router ID".to_string(),
46 required: true,
47 },
48 Parameter {
49 name: "hold_time".to_string(),
50 type_hint: "number".to_string(),
51 description: "Negotiated hold time in seconds (the smaller of the two proposals; \
52 0 means both timers are disabled)"
53 .to_string(),
54 required: true,
55 },
56 Parameter {
57 name: "peer_supports_four_octet_as".to_string(),
58 type_hint: "boolean".to_string(),
59 description: "Whether the peer advertised RFC 6793 four-octet AS support".to_string(),
60 required: true,
61 },
62 ])
63 .with_actions(bgp_client_response_actions())
64});
65
66pub static BGP_CLIENT_UPDATE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
71 EventType::new(
72 "bgp_update_received",
73 "BGP UPDATE received from the peer, decoded into withdrawn_routes, nlri, origin, \
74 next_hop, as_path and the full path_attributes list. This client announces no routes, \
75 so there is no way to answer an UPDATE with one — but the session verbs do reach the \
76 wire: reply with wait_for_more to keep monitoring, or send_notification / disconnect \
77 to tear the peering down over what the peer just advertised.",
78 json!({"type": "wait_for_more"}),
79 )
80 .with_parameters(vec![
81 Parameter {
82 name: "nlri".to_string(),
83 type_hint: "array".to_string(),
84 description: "Announced prefixes in CIDR form, e.g. [\"10.0.0.0/24\"]".to_string(),
85 required: true,
86 },
87 Parameter {
88 name: "withdrawn_routes".to_string(),
89 type_hint: "array".to_string(),
90 description: "Withdrawn prefixes in CIDR form".to_string(),
91 required: true,
92 },
93 Parameter {
94 name: "next_hop".to_string(),
95 type_hint: "string".to_string(),
96 description: "NEXT_HOP attribute, or null when the UPDATE only withdraws".to_string(),
97 required: false,
98 },
99 Parameter {
100 name: "as_path".to_string(),
101 type_hint: "array".to_string(),
102 description: "AS_PATH as a list of AS numbers".to_string(),
103 required: false,
104 },
105 Parameter {
106 name: "origin".to_string(),
107 type_hint: "string".to_string(),
108 description: "ORIGIN attribute: IGP, EGP or Incomplete".to_string(),
109 required: false,
110 },
111 Parameter {
112 name: "path_attributes".to_string(),
113 type_hint: "array".to_string(),
114 description: "Every path attribute, each with its type_name and flags".to_string(),
115 required: true,
116 },
117 Parameter {
118 name: "end_of_rib".to_string(),
119 type_hint: "boolean".to_string(),
120 description: "True when this is an End-of-RIB marker (RFC 4724)".to_string(),
121 required: true,
122 },
123 Parameter {
124 name: "peer_as".to_string(),
125 type_hint: "number".to_string(),
126 description: "AS number of the peer that sent this UPDATE".to_string(),
127 required: false,
128 },
129 ])
130 .with_actions(bgp_client_response_actions())
131});
132
133pub static BGP_CLIENT_NOTIFICATION_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
135 EventType::new(
136 "bgp_notification_received",
137 "BGP NOTIFICATION received from the peer. RFC 4271 forbids answering one, so the \
138 session closes regardless of what is returned and nothing is written to the socket.",
139 json!({"type": "wait_for_more"}),
140 )
141 .with_parameters(vec![
142 Parameter {
143 name: "error_code".to_string(),
144 type_hint: "number".to_string(),
145 description: "BGP error code (RFC 4271 section 6)".to_string(),
146 required: true,
147 },
148 Parameter {
149 name: "error_name".to_string(),
150 type_hint: "string".to_string(),
151 description: "Human-readable error name, e.g. \"OPEN Message Error\"".to_string(),
152 required: true,
153 },
154 Parameter {
155 name: "error_subcode".to_string(),
156 type_hint: "number".to_string(),
157 description: "BGP error subcode".to_string(),
158 required: true,
159 },
160 Parameter {
161 name: "error_subcode_name".to_string(),
162 type_hint: "string".to_string(),
163 description: "Human-readable subcode name, e.g. \"Bad Peer AS\"".to_string(),
164 required: true,
165 },
166 Parameter {
167 name: "peer_as".to_string(),
168 type_hint: "number".to_string(),
169 description: "AS number of the peer, if its OPEN was seen".to_string(),
170 required: false,
171 },
172 ])
173 .with_no_actions()
174});
175
176fn bgp_client_response_actions() -> Vec<ActionDefinition> {
190 vec![
191 ActionDefinition {
192 name: "send_keepalive".to_string(),
193 description: "Send a BGP KEEPALIVE to the peer. Routine keepalives are already \
194 sent automatically at a third of the negotiated hold time; use this \
195 only to send an extra one."
196 .to_string(),
197 parameters: vec![],
198 example: json!({
199 "type": "send_keepalive"
200 }),
201 log_template: None,
202 },
203 ActionDefinition {
204 name: "send_notification".to_string(),
205 description: "Send a BGP NOTIFICATION. RFC 4271 requires the peer to close the \
206 session on receipt, so this ends the peering."
207 .to_string(),
208 parameters: vec![
209 Parameter {
210 name: "error_code".to_string(),
211 type_hint: "number".to_string(),
212 description: "BGP error code, 1-6 (1 Message Header, 2 OPEN, 3 UPDATE, \
213 4 Hold Timer Expired, 5 FSM, 6 Cease)"
214 .to_string(),
215 required: true,
216 },
217 Parameter {
218 name: "error_subcode".to_string(),
219 type_hint: "number".to_string(),
220 description: "BGP error subcode (default 0 = unspecified)".to_string(),
221 required: false,
222 },
223 ],
224 example: json!({
225 "type": "send_notification",
226 "error_code": 6,
227 "error_subcode": 2
228 }),
229 log_template: None,
230 },
231 ActionDefinition {
232 name: "disconnect".to_string(),
233 description: "Close the session gracefully: send NOTIFICATION 6/2 (Cease / \
234 Administrative Shutdown), then close the TCP connection."
235 .to_string(),
236 parameters: vec![],
237 example: json!({
238 "type": "disconnect"
239 }),
240 log_template: None,
241 },
242 ActionDefinition {
243 name: "wait_for_more".to_string(),
244 description: "Do nothing and keep monitoring the session".to_string(),
245 parameters: vec![],
246 example: json!({
247 "type": "wait_for_more"
248 }),
249 log_template: None,
250 },
251 ]
252}
253
254pub struct BgpClientProtocol;
256
257impl BgpClientProtocol {
258 pub fn new() -> Self {
259 Self
260 }
261}
262
263impl Default for BgpClientProtocol {
264 fn default() -> Self {
265 Self::new()
266 }
267}
268
269impl Protocol for BgpClientProtocol {
271 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
272 bgp_client_response_actions()
273 }
274 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
275 bgp_client_response_actions()
276 }
277 fn get_event_types(&self) -> Vec<EventType> {
278 vec![
279 BGP_CLIENT_CONNECTED_EVENT.clone(),
280 BGP_CLIENT_UPDATE_RECEIVED_EVENT.clone(),
281 BGP_CLIENT_NOTIFICATION_RECEIVED_EVENT.clone(),
282 ]
283 }
284 fn protocol_name(&self) -> &'static str {
285 "BGP"
286 }
287 fn stack_name(&self) -> &'static str {
288 "ETH>IP>TCP>BGP"
289 }
290 fn keywords(&self) -> Vec<&'static str> {
291 vec!["bgp", "border gateway"]
292 }
293 fn description(&self) -> &'static str {
294 "BGP routing client (query mode)"
295 }
296 fn example_prompt(&self) -> &'static str {
297 "Connect to BGP peer at 192.168.1.1:179 and query routing table"
298 }
299 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
300 vec![
301 ParameterDefinition {
302 name: "local_as".to_string(),
303 type_hint: "integer".to_string(),
304 description: "Local BGP AS number (can be fake for monitoring)".to_string(),
305 required: false,
306 example: json!(65000),
307 default: None,
308 },
309 ParameterDefinition {
310 name: "router_id".to_string(),
311 type_hint: "string".to_string(),
312 description: "BGP router ID in IPv4 format".to_string(),
313 required: false,
314 example: json!("192.168.1.100"),
315 default: None,
316 },
317 ParameterDefinition {
318 name: "hold_time".to_string(),
319 type_hint: "integer".to_string(),
320 description: "BGP hold time in seconds (default 180)".to_string(),
321 required: false,
322 example: json!(180),
323 default: None,
324 },
325 ]
326 }
327 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
328 use crate::protocol::metadata::{
329 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
330 };
331
332 ProtocolMetadataV2::builder()
333 .state(DevelopmentState::Experimental)
334 .privilege_requirement(PrivilegeRequirement::None)
335 .implementation(
336 "BGP-4 query client (RFC 4271, RFC 6793) over netgauze-bgp-pkt, \
337 sharing src/server/bgp/wire.rs with the BGP server",
338 )
339 .llm_control("Session establishment, route monitoring")
340 .e2e_testing("NetGet BGP server")
341 .notes(
342 "Query mode only, no active route announcement, no RIB. Four-octet ASNs \
343 are advertised and parsed. Keepalives go at a third of the negotiated hold \
344 time and the hold timer is enforced: a peer that sends nothing for the \
345 negotiated hold time earns NOTIFICATION 4/0 and the session closes, proven \
346 end to end in tests/client/bgp/hold_timer_test.rs. Never peered against a \
347 real BGP daemon.",
348 )
349 .build()
350 }
351 fn group_name(&self) -> &'static str {
352 "VPN & Routing"
353 }
354 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
355 use crate::llm::actions::StartupExamples;
356 use serde_json::json;
357
358 StartupExamples::new(
359 json!({
361 "type": "open_client",
362 "remote_addr": "192.168.1.1:179",
363 "base_stack": "bgp",
364 "instruction": "Establish BGP session and monitor routing updates",
365 "startup_params": {
366 "local_as": 65000,
367 "router_id": "192.168.1.100"
368 }
369 }),
370 json!({
372 "type": "open_client",
373 "remote_addr": "192.168.1.1:179",
374 "base_stack": "bgp",
375 "startup_params": {
376 "local_as": 65000,
377 "router_id": "192.168.1.100"
378 },
379 "event_handlers": [{
380 "event_pattern": "bgp_update_received",
381 "handler": {
382 "type": "script",
383 "language": "python",
384 "code": "<bgp_client_handler>"
385 }
386 }]
387 }),
388 json!({
390 "type": "open_client",
391 "remote_addr": "192.168.1.1:179",
392 "base_stack": "bgp",
393 "startup_params": {
394 "local_as": 65000,
395 "router_id": "192.168.1.100",
396 "hold_time": 180
397 },
398 "event_handlers": [
399 {
400 "event_pattern": "bgp_connected",
401 "handler": {
402 "type": "static",
403 "actions": [{
404 "type": "send_keepalive"
405 }]
406 }
407 },
408 {
409 "event_pattern": "bgp_update_received",
410 "handler": {
411 "type": "static",
412 "actions": [{
413 "type": "send_keepalive"
414 }]
415 }
416 }
417 ]
418 }),
419 )
420 }
421}
422
423impl Client for BgpClientProtocol {
425 fn connect(
426 &self,
427 ctx: ConnectContext,
428 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
429 {
430 Box::pin(async move {
431 use crate::client::bgp::BgpClient;
432 BgpClient::connect_with_llm_actions(
433 ctx.remote_addr,
434 ctx.llm_client,
435 ctx.state,
436 ctx.status_tx,
437 ctx.client_id,
438 ctx.startup_params,
439 )
440 .await
441 })
442 }
443 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
448 use crate::server::bgp::wire;
449
450 let action_type = action
451 .get("type")
452 .and_then(|v| v.as_str())
453 .context("Missing 'type' field in action")?;
454
455 match action_type {
456 "send_keepalive" => Ok(ClientActionResult::SendData(wire::encode_keepalive())),
457 "send_notification" => {
458 let error_code_raw = action
461 .get("error_code")
462 .and_then(|v| v.as_u64())
463 .context("send_notification requires error_code (1-6, see RFC 4271 §6)")?;
464 let error_code = u8::try_from(error_code_raw)
465 .ok()
466 .filter(|c| (1..=6).contains(c))
467 .with_context(|| {
468 format!("send_notification error_code must be 1-6, got {error_code_raw}")
469 })?;
470
471 let error_subcode_raw = action
472 .get("error_subcode")
473 .and_then(|v| v.as_u64())
474 .unwrap_or(0);
475 let error_subcode = u8::try_from(error_subcode_raw).with_context(|| {
476 format!(
477 "send_notification error_subcode must be 0-255, got {error_subcode_raw}"
478 )
479 })?;
480
481 Ok(ClientActionResult::SendData(wire::encode_notification(
482 error_code,
483 error_subcode,
484 &[],
485 )?))
486 }
487 "disconnect" => Ok(ClientActionResult::Multiple(vec![
491 ClientActionResult::SendData(wire::encode_notification(
492 wire::ERR_CEASE,
493 2, &[],
495 )?),
496 ClientActionResult::Disconnect,
497 ])),
498 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
499 _ => Err(anyhow::anyhow!(
500 "Unknown BGP client action: {}",
501 action_type
502 )),
503 }
504 }
505}