1use crate::llm::actions::{
16 protocol_trait::{ActionResult, Protocol, Server},
17 ActionDefinition, Parameter,
18};
19use crate::protocol::log_template::LogTemplate;
20use crate::protocol::EventType;
21use crate::state::app_state::AppState;
22use anyhow::{bail, Context, Result};
23use serde_json::json;
24use std::net::Ipv4Addr;
25use std::sync::LazyLock;
26
27pub const BGP_MESSAGE_INTENT: &str = "bgp_message";
29
30pub struct BgpProtocol;
32
33impl Default for BgpProtocol {
34 fn default() -> Self {
35 Self::new()
36 }
37}
38
39impl BgpProtocol {
40 pub fn new() -> Self {
41 Self
42 }
43
44 fn intent(data: serde_json::Value) -> ActionResult {
45 ActionResult::Custom {
46 name: BGP_MESSAGE_INTENT.to_string(),
47 data,
48 }
49 }
50
51 fn execute_send_bgp_open(&self, action: serde_json::Value) -> Result<ActionResult> {
52 let my_as = action
53 .get("my_as")
54 .and_then(|v| v.as_u64())
55 .context("send_bgp_open requires my_as")?;
56 let my_as = u32::try_from(my_as)
57 .with_context(|| format!("my_as {my_as} is outside the 1-4294967295 AS range"))?;
58 if my_as == 0 {
59 bail!("my_as must be between 1 and 4294967295");
60 }
61
62 let hold_time = action
63 .get("hold_time")
64 .and_then(|v| v.as_u64())
65 .unwrap_or(180);
66 let hold_time = u16::try_from(hold_time)
67 .with_context(|| format!("hold_time {hold_time} is outside 0-65535"))?;
68 if hold_time != 0 && hold_time < 3 {
69 bail!("hold_time must be 0 (timers disabled) or at least 3 seconds (RFC 4271 6.2)");
70 }
71
72 let router_id = action
73 .get("router_id")
74 .and_then(|v| v.as_str())
75 .context("send_bgp_open requires router_id")?;
76 let router_id: Ipv4Addr = router_id
77 .parse()
78 .with_context(|| format!("router_id {router_id:?} is not an IPv4 address"))?;
79 if router_id.is_unspecified() {
80 bail!("router_id must not be 0.0.0.0 (RFC 4271 requires a valid BGP Identifier)");
81 }
82
83 Ok(Self::intent(json!({
84 "kind": "open",
85 "my_as": my_as,
86 "hold_time": hold_time,
87 "router_id": router_id.to_string(),
88 })))
89 }
90
91 fn execute_send_bgp_keepalive(&self, _action: serde_json::Value) -> Result<ActionResult> {
92 Ok(Self::intent(json!({ "kind": "keepalive" })))
93 }
94
95 fn execute_send_bgp_update(&self, action: serde_json::Value) -> Result<ActionResult> {
96 let prefixes = |key: &str| -> Result<Vec<String>> {
97 let Some(value) = action.get(key) else {
98 return Ok(Vec::new());
99 };
100 if value.is_null() {
101 return Ok(Vec::new());
102 }
103 let array = value
104 .as_array()
105 .with_context(|| format!("{key} must be an array of CIDR prefix strings"))?;
106 array
107 .iter()
108 .map(|v| {
109 let s = v
110 .as_str()
111 .with_context(|| format!("{key} entries must be strings like 10.0.0.0/24"))?
112 .to_string();
113 super::wire::ipv4_unicast(&s)?;
115 Ok(s)
116 })
117 .collect()
118 };
119
120 let withdrawn = prefixes("withdrawn_routes")?;
121 let nlri = prefixes("nlri")?;
122 if withdrawn.is_empty() && nlri.is_empty() {
123 bail!("send_bgp_update needs at least one prefix in nlri or withdrawn_routes");
124 }
125
126 let next_hop = match action.get("next_hop").and_then(|v| v.as_str()) {
127 Some(s) => Some(
128 s.parse::<Ipv4Addr>()
129 .with_context(|| format!("next_hop {s:?} is not an IPv4 address"))?,
130 ),
131 None => None,
132 };
133 if !nlri.is_empty() && next_hop.is_none() {
134 bail!(
135 "send_bgp_update with nlri requires next_hop \
136 (RFC 4271 9.1 makes NEXT_HOP mandatory for an announcement)"
137 );
138 }
139
140 let as_path: Vec<u32> = match action.get("as_path") {
141 Some(v) if !v.is_null() => {
142 let array = v
143 .as_array()
144 .context("as_path must be an array of AS numbers")?;
145 array
146 .iter()
147 .map(|v| {
148 let n = v.as_u64().context("as_path entries must be AS numbers")?;
149 u32::try_from(n).with_context(|| format!("AS number {n} is out of range"))
150 })
151 .collect::<Result<_>>()?
152 }
153 _ => Vec::new(),
154 };
155
156 let origin = match action.get("origin").and_then(|v| v.as_str()) {
157 None => "IGP",
158 Some(o) => match o.to_ascii_uppercase().as_str() {
159 "IGP" => "IGP",
160 "EGP" => "EGP",
161 "INCOMPLETE" => "INCOMPLETE",
162 other => bail!("origin must be IGP, EGP or INCOMPLETE, got {other:?}"),
163 },
164 };
165
166 let u32_field = |key: &str| -> Result<Option<u32>> {
167 match action.get(key) {
168 Some(v) if !v.is_null() => {
169 let n = v
170 .as_u64()
171 .with_context(|| format!("{key} must be a number"))?;
172 Ok(Some(u32::try_from(n).with_context(|| {
173 format!("{key} value {n} exceeds 4294967295")
174 })?))
175 }
176 _ => Ok(None),
177 }
178 };
179
180 Ok(Self::intent(json!({
181 "kind": "update",
182 "withdrawn_routes": withdrawn,
183 "nlri": nlri,
184 "next_hop": next_hop.map(|h| h.to_string()),
185 "as_path": as_path,
186 "origin": origin,
187 "med": u32_field("med")?,
188 "local_pref": u32_field("local_pref")?,
189 })))
190 }
191
192 fn execute_send_bgp_notification(&self, action: serde_json::Value) -> Result<ActionResult> {
193 let error_code = action
194 .get("error_code")
195 .and_then(|v| v.as_u64())
196 .context("send_bgp_notification requires error_code")?;
197 let error_code = u8::try_from(error_code)
198 .with_context(|| format!("error_code {error_code} must be 1-6"))?;
199 if !(1..=6).contains(&error_code) {
200 bail!("error_code must be 1-6 (see RFC 4271 section 6), got {error_code}");
201 }
202
203 let error_subcode = action
204 .get("error_subcode")
205 .and_then(|v| v.as_u64())
206 .unwrap_or(0);
207 let error_subcode = u8::try_from(error_subcode)
208 .with_context(|| format!("error_subcode {error_subcode} must be 0-255"))?;
209
210 let data = match action.get("data") {
213 Some(v) if !v.is_null() => {
214 let s = v
215 .as_str()
216 .context("data must be a hex-encoded string, e.g. \"00b4\"")?;
217 if s.is_empty() {
218 String::new()
219 } else {
220 hex::decode(s).with_context(|| format!("data {s:?} is not valid hex"))?;
221 s.to_string()
222 }
223 }
224 _ => String::new(),
225 };
226
227 Ok(Self::intent(json!({
228 "kind": "notification",
229 "error_code": error_code,
230 "error_subcode": error_subcode,
231 "data": data,
232 })))
233 }
234}
235
236fn send_bgp_open_action() -> ActionDefinition {
241 ActionDefinition {
242 name: "send_bgp_open".to_string(),
243 description: "Accept the peering request by sending our BGP OPEN. NetGet adds the \
244 four-octet AS capability and sends the follow-up KEEPALIVE itself."
245 .to_string(),
246 parameters: vec![
247 Parameter {
248 name: "my_as".to_string(),
249 type_hint: "number".to_string(),
250 description: "Our AS number, 1-4294967295. Values above 65535 are carried in \
251 the four-octet AS capability with AS_TRANS in the legacy field."
252 .to_string(),
253 required: true,
254 },
255 Parameter {
256 name: "hold_time".to_string(),
257 type_hint: "number".to_string(),
258 description: "Proposed hold time in seconds (default 180). Must be 0 or >= 3. \
259 The session uses the smaller of this and the peer's."
260 .to_string(),
261 required: false,
262 },
263 Parameter {
264 name: "router_id".to_string(),
265 type_hint: "string".to_string(),
266 description: "BGP router identifier, IPv4 dotted quad, not 0.0.0.0".to_string(),
267 required: true,
268 },
269 ],
270 example: json!({
271 "type": "send_bgp_open",
272 "my_as": 65001,
273 "hold_time": 180,
274 "router_id": "192.168.1.1"
275 }),
276 log_template: Some(
277 LogTemplate::new()
278 .with_info("-> BGP OPEN AS{my_as} hold={hold_time}s")
279 .with_debug(
280 "BGP send_bgp_open: AS={my_as}, hold_time={hold_time}, router_id={router_id}",
281 ),
282 ),
283 }
284}
285
286fn send_bgp_keepalive_action() -> ActionDefinition {
287 ActionDefinition {
288 name: "send_bgp_keepalive".to_string(),
289 description: "Send a BGP KEEPALIVE. Rarely needed: NetGet sends keepalives on its own \
290 at a third of the negotiated hold time for the life of the session."
291 .to_string(),
292 parameters: vec![],
293 example: json!({ "type": "send_bgp_keepalive" }),
294 log_template: Some(
295 LogTemplate::new()
296 .with_info("-> BGP KEEPALIVE")
297 .with_debug("BGP send_bgp_keepalive"),
298 ),
299 }
300}
301
302fn send_bgp_update_action() -> ActionDefinition {
303 ActionDefinition {
304 name: "send_bgp_update".to_string(),
305 description: "Announce and/or withdraw routes with a BGP UPDATE. This is how routes \
306 reach the peer; NetGet keeps no routing table, so every announcement is \
307 exactly what this action says."
308 .to_string(),
309 parameters: vec![
310 Parameter {
311 name: "nlri".to_string(),
312 type_hint: "array".to_string(),
313 description: "CIDR prefixes to announce, e.g. [\"192.168.0.0/16\"]".to_string(),
314 required: false,
315 },
316 Parameter {
317 name: "withdrawn_routes".to_string(),
318 type_hint: "array".to_string(),
319 description: "CIDR prefixes to withdraw, e.g. [\"10.0.0.0/24\"]".to_string(),
320 required: false,
321 },
322 Parameter {
323 name: "next_hop".to_string(),
324 type_hint: "string".to_string(),
325 description: "Next-hop IPv4 address. Required whenever nlri is non-empty."
326 .to_string(),
327 required: false,
328 },
329 Parameter {
330 name: "as_path".to_string(),
331 type_hint: "array".to_string(),
332 description: "AS numbers forming the AS_SEQUENCE, e.g. [65001]. An empty list \
333 is a locally originated route."
334 .to_string(),
335 required: false,
336 },
337 Parameter {
338 name: "origin".to_string(),
339 type_hint: "string".to_string(),
340 description: "ORIGIN attribute: IGP (default), EGP or INCOMPLETE".to_string(),
341 required: false,
342 },
343 Parameter {
344 name: "med".to_string(),
345 type_hint: "number".to_string(),
346 description: "Optional MULTI_EXIT_DISC metric".to_string(),
347 required: false,
348 },
349 Parameter {
350 name: "local_pref".to_string(),
351 type_hint: "number".to_string(),
352 description: "Optional LOCAL_PREF, meaningful to iBGP peers".to_string(),
353 required: false,
354 },
355 ],
356 example: json!({
357 "type": "send_bgp_update",
358 "nlri": ["10.0.0.0/24"],
359 "next_hop": "192.168.1.1",
360 "as_path": [65001],
361 "origin": "IGP"
362 }),
363 log_template: Some(
364 LogTemplate::new()
365 .with_info("-> BGP UPDATE")
366 .with_debug("BGP send_bgp_update: withdrawn={withdrawn_routes} nlri={nlri}"),
367 ),
368 }
369}
370
371fn send_bgp_notification_action() -> ActionDefinition {
372 ActionDefinition {
373 name: "send_bgp_notification".to_string(),
374 description: "Refuse or tear down the session with a BGP NOTIFICATION. On a bgp_open \
375 event this is the explicit refusal to peer, and it is the only thing that \
376 counts as one."
377 .to_string(),
378 parameters: vec![
379 Parameter {
380 name: "error_code".to_string(),
381 type_hint: "number".to_string(),
382 description: "RFC 4271 error code 1-6: 1 header, 2 OPEN, 3 UPDATE, \
383 4 hold timer expired, 5 FSM, 6 cease"
384 .to_string(),
385 required: true,
386 },
387 Parameter {
388 name: "error_subcode".to_string(),
389 type_hint: "number".to_string(),
390 description: "Error subcode, 0 if unspecified. With code 2: 2 bad peer AS, \
391 3 bad BGP identifier. With code 6: 2 administrative shutdown, \
392 5 connection rejected."
393 .to_string(),
394 required: false,
395 },
396 Parameter {
397 name: "data".to_string(),
398 type_hint: "string".to_string(),
399 description: "Optional diagnostic bytes as a hex string, e.g. \"00b4\". \
400 Decoded as hex; leave it out unless the subcode defines a payload."
401 .to_string(),
402 required: false,
403 },
404 ],
405 example: json!({
406 "type": "send_bgp_notification",
407 "error_code": 6,
408 "error_subcode": 5
409 }),
410 log_template: Some(
411 LogTemplate::new()
412 .with_info("-> BGP NOTIFICATION code={error_code}")
413 .with_debug(
414 "BGP send_bgp_notification: code={error_code}, subcode={error_subcode}",
415 ),
416 ),
417 }
418}
419
420fn wait_for_more_action() -> ActionDefinition {
421 ActionDefinition {
422 name: "wait_for_more".to_string(),
423 description: "Take no action and wait for the next BGP message.".to_string(),
424 parameters: vec![],
425 example: json!({ "type": "wait_for_more" }),
426 log_template: Some(
427 LogTemplate::new()
428 .with_info("-> BGP wait for more")
429 .with_debug("BGP wait_for_more: awaiting additional messages"),
430 ),
431 }
432}
433
434fn bgp_response_actions() -> Vec<ActionDefinition> {
443 vec![
444 send_bgp_open_action(),
445 send_bgp_keepalive_action(),
446 send_bgp_update_action(),
447 send_bgp_notification_action(),
448 wait_for_more_action(),
449 ]
450}
451
452pub static BGP_OPEN_EVENT: LazyLock<EventType> = LazyLock::new(|| {
453 EventType::new(
454 "bgp_open",
455 "BGP OPEN received. Decide whether to peer with this neighbour. Reply with \
456 send_bgp_open to accept, or send_bgp_notification to refuse. If neither is returned, \
457 NetGet sends the OPEN configured at startup.",
458 json!({
459 "type": "send_bgp_open",
460 "my_as": 65001,
461 "hold_time": 180,
462 "router_id": "192.168.1.1"
463 }),
464 )
465 .with_alternative_example(json!({
466 "type": "send_bgp_notification",
467 "error_code": 2,
468 "error_subcode": 2
469 }))
470 .with_actions(bgp_response_actions())
471 .with_log_template(
472 LogTemplate::new()
473 .with_info("BGP OPEN from AS{peer_as} router_id={peer_router_id}")
474 .with_debug(
475 "BGP OPEN: AS={peer_as} hold={peer_hold_time} asn4={peer_supports_four_octet_as}",
476 )
477 .with_trace("BGP OPEN: {json_pretty(.)}"),
478 )
479});
480
481pub static BGP_ESTABLISHED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
488 EventType::new(
489 "bgp_established",
490 "BGP session reached Established. The peer will now accept UPDATEs. Reply with \
491 send_bgp_update to advertise routes, or wait_for_more to advertise nothing.",
492 json!({
493 "type": "send_bgp_update",
494 "nlri": ["10.0.0.0/24"],
495 "next_hop": "192.168.1.1",
496 "as_path": [65001],
497 "origin": "IGP"
498 }),
499 )
500 .with_alternative_example(json!({ "type": "wait_for_more" }))
501 .with_actions(bgp_response_actions())
502 .with_log_template(
503 LogTemplate::new()
504 .with_info("BGP established with AS{peer_as}")
505 .with_debug("BGP established with AS{peer_as} on {connection_id}")
506 .with_trace("BGP established: {json_pretty(.)}"),
507 )
508});
509
510pub static BGP_UPDATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
511 EventType::new(
512 "bgp_update",
513 "BGP UPDATE received, decoded into withdrawn_routes, nlri, origin, next_hop, as_path \
514 and the full path_attributes list. Reply with send_bgp_update to advertise routes back, \
515 or wait_for_more.",
516 json!({ "type": "wait_for_more" }),
517 )
518 .with_actions(bgp_response_actions())
519 .with_log_template(
520 LogTemplate::new()
521 .with_info("BGP UPDATE from AS{peer_as}")
522 .with_debug("BGP UPDATE from AS{peer_as} on {connection_id}")
523 .with_trace("BGP UPDATE: {json_pretty(.)}"),
524 )
525});
526
527pub static BGP_NOTIFICATION_EVENT: LazyLock<EventType> = LazyLock::new(|| {
528 EventType::new(
529 "bgp_notification",
530 "BGP NOTIFICATION received. RFC 4271 forbids replying to one, so the session closes \
531 regardless of what is returned and nothing is written to the socket. Record what \
532 matters with set_memory or append_to_log.",
533 json!({ "type": "wait_for_more" }),
534 )
535 .with_actions(vec![wait_for_more_action()])
546 .with_log_template(
547 LogTemplate::new()
548 .with_info("BGP NOTIFICATION code={error_code} subcode={error_subcode}")
549 .with_debug("BGP NOTIFICATION: {error_name} / {error_subcode_name} from AS{peer_as}")
550 .with_trace("BGP NOTIFICATION: {json_pretty(.)}"),
551 )
552});
553
554impl Protocol for BgpProtocol {
556 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
564 vec![]
565 }
566
567 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
568 bgp_response_actions()
569 }
570
571 fn protocol_name(&self) -> &'static str {
572 "BGP"
573 }
574
575 fn get_event_types(&self) -> Vec<EventType> {
576 vec![
577 BGP_OPEN_EVENT.clone(),
578 BGP_ESTABLISHED_EVENT.clone(),
579 BGP_UPDATE_EVENT.clone(),
580 BGP_NOTIFICATION_EVENT.clone(),
581 ]
582 }
583
584 fn stack_name(&self) -> &'static str {
585 "ETH>IP>TCP>BGP"
586 }
587
588 fn keywords(&self) -> Vec<&'static str> {
589 vec!["bgp", "border gateway"]
590 }
591
592 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
593 use crate::protocol::metadata::{
594 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
595 };
596
597 ProtocolMetadataV2::builder()
598 .state(DevelopmentState::Experimental)
599 .privilege_requirement(PrivilegeRequirement::PrivilegedPort(179))
601 .well_known_port(179)
602 .implementation(
603 "RFC 4271 session over netgauze-bgp-pkt codec; four-octet AS (RFC 6793)",
604 )
605 .llm_control("Optional: the OPEN handshake is mechanical (configured OPEN, no LLM); whether to advertise routes is policy (LLM). With no policy configured the session handshakes on the configured OPEN and advertises nothing, all with no LLM call")
606 .e2e_testing("Two-way wire conformance against netgauze, plus mocked session E2E")
607 .notes(
608 "Session is complete: OPEN validation, capability and hold-time negotiation, \
609 KEEPALIVE cadence, hold-timer expiry, NOTIFICATION on error. The OPEN handshake \
610 is mechanical (fully determined by the configured ASN/router-id/hold-time and a \
611 validated peer), so with no operator policy (no instruction, no handler) it \
612 completes on the configured OPEN with NO LLM round-trip; established/update then \
613 advertise nothing, which is correct with no routing policy. KEEPALIVE cadence \
614 never consulted the LLM. The model is consulted only when the operator opts in. \
615 No RIB by design - routes are whatever a handler advertises with send_bgp_update, \
616 nothing is stored or re-advertised, and there is no best-path selection or \
617 propagation between peers. IPv4 unicast only on the send path. Never peered \
618 against a live BGP daemon.",
619 )
620 .max_inbound_bytes(crate::server::bgp::wire::BGP_MAX_MESSAGE_LEN)
621 .build()
622 }
623
624 fn description(&self) -> &'static str {
625 "BGP routing server"
626 }
627
628 fn example_prompt(&self) -> &'static str {
629 "Start a BGP routing server on port 8179"
630 }
631
632 fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
633 use crate::llm::actions::ParameterDefinition;
634 vec![
635 ParameterDefinition {
636 name: "as_number".to_string(),
637 type_hint: "integer".to_string(),
638 description: "Our Autonomous System Number (1-4294967295). Use private ASNs \
639 (64512-65534) for testing. Values above 65535 are advertised via \
640 the four-octet AS capability."
641 .to_string(),
642 required: false,
643 example: json!(65001),
644 default: None,
645 },
646 ParameterDefinition {
647 name: "router_id".to_string(),
648 type_hint: "string".to_string(),
649 description: "BGP router ID in IPv4 dotted-quad form, not 0.0.0.0".to_string(),
650 required: false,
651 example: json!("192.168.1.1"),
652 default: None,
653 },
654 ParameterDefinition {
655 name: "hold_time".to_string(),
656 type_hint: "integer".to_string(),
657 description: "Hold time we propose, in seconds (default 180). Must be 0 \
658 (timers disabled) or at least 3. Keepalives are sent at a third \
659 of the negotiated value."
660 .to_string(),
661 required: false,
662 example: json!(180),
663 default: None,
664 },
665 ]
666 }
667
668 fn group_name(&self) -> &'static str {
669 "VPN & Routing"
670 }
671
672 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
673 use crate::llm::actions::StartupExamples;
674
675 let script = r#"import json, sys
677data = json.load(sys.stdin)
678event = data["event"]
679if data["event_type_id"] == "bgp_update":
680 actions = [{"type": "send_bgp_keepalive"}]
681else:
682 actions = []
683print(json.dumps({"actions": actions}))"#;
684
685 StartupExamples::new(
686 json!({
688 "type": "open_server",
689 "port": 179,
690 "base_stack": "bgp",
691 "instruction": "Peer with anyone in AS 65000-65535 and advertise 10.0.0.0/24",
692 "startup_params": {
693 "as_number": 65001,
694 "router_id": "192.168.1.1"
695 }
696 }),
697 json!({
699 "type": "open_server",
700 "port": 179,
701 "base_stack": "bgp",
702 "startup_params": {
703 "as_number": 65001,
704 "router_id": "192.168.1.1"
705 },
706 "event_handlers": [{
707 "event_pattern": "bgp_update",
708 "handler": {
709 "type": "script",
710 "language": "python",
711 "code": script
712 }
713 }]
714 }),
715 json!({
717 "type": "open_server",
718 "port": 179,
719 "base_stack": "bgp",
720 "startup_params": {
721 "as_number": 65001,
722 "router_id": "192.168.1.1"
723 },
724 "event_handlers": [
725 {
726 "event_pattern": "bgp_open",
727 "handler": {
728 "type": "static",
729 "actions": [{
730 "type": "send_bgp_open",
731 "my_as": 65001,
732 "hold_time": 180,
733 "router_id": "192.168.1.1"
734 }]
735 }
736 },
737 {
738 "event_pattern": "bgp_established",
739 "handler": {
740 "type": "static",
741 "actions": [{
742 "type": "send_bgp_update",
743 "nlri": ["10.0.0.0/24"],
744 "next_hop": "192.168.1.1",
745 "as_path": [65001],
746 "origin": "IGP"
747 }]
748 }
749 }
750 ]
751 }),
752 )
753 }
754}
755
756impl Server for BgpProtocol {
758 fn spawn(
759 &self,
760 ctx: crate::protocol::SpawnContext,
761 ) -> std::pin::Pin<
762 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
763 > {
764 Box::pin(async move {
765 use crate::server::bgp::BgpServer;
766 BgpServer::spawn_with_llm_actions(
767 ctx.legacy_listen_addr(),
768 ctx.llm_client,
769 ctx.state,
770 ctx.status_tx,
771 ctx.server_id,
772 ctx.startup_params,
773 )
774 .await
775 })
776 }
777
778 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
779 let action_type = action
780 .get("type")
781 .and_then(|v| v.as_str())
782 .context("Missing action type")?;
783
784 match action_type {
785 "send_bgp_open" => self.execute_send_bgp_open(action),
786 "send_bgp_keepalive" => self.execute_send_bgp_keepalive(action),
787 "send_bgp_update" => self.execute_send_bgp_update(action),
788 "send_bgp_notification" => self.execute_send_bgp_notification(action),
789 "wait_for_more" => Ok(ActionResult::WaitForMore),
790 "close_connection" => Ok(ActionResult::CloseConnection),
794 _ => Err(anyhow::anyhow!("Unknown BGP action type: {}", action_type)),
795 }
796 }
797}