1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, ParameterDefinition, StartupExamples,
7};
8use crate::protocol::{ConnectContext, EventType};
9use crate::server::bfd::actions::{
10 action, admin_down_action, admin_up_action, auth_params, p, set_timers_action, state_params,
11 timer_startup_params,
12};
13use crate::server::bfd::packet;
14use crate::state::app_state::AppState;
15use anyhow::Result;
16use serde_json::{json, Value};
17use std::sync::LazyLock;
18
19#[derive(Default)]
20pub struct BfdClientProtocol;
21impl BfdClientProtocol {
22 pub fn new() -> Self {
23 Self
24 }
25}
26
27fn disconnect_action() -> ActionDefinition {
28 action(
29 "disconnect",
30 "End the session: stop sending, so the router's detection timer brings it down.",
31 vec![],
32 json!({"type":"disconnect"}),
33 )
34}
35
36pub fn actions() -> Vec<ActionDefinition> {
37 vec![
38 set_timers_action(),
39 admin_down_action(),
40 admin_up_action(),
41 disconnect_action(),
42 ]
43}
44
45pub static STARTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
46 EventType::new(
47 "bfd_session_started",
48 "The client is sending Control packets to the router; the session comes Up when the router answers.",
49 set_timers_action().example.clone(),
50 )
51 .with_parameters(vec![
52 p("peer", "string", "The router's address and BFD port", true),
53 p("local", "string", "The address NetGet sends from and listens on", true),
54 p("local_discriminator", "number", "This side's discriminator", true),
55 p("multihop", "boolean", "Multihop (RFC 5883, port 4784) rather than single-hop (RFC 5881, 3784)", true),
56 ])
57 .with_actions(actions())
58});
59
60pub static STATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
61 EventType::new(
62 "bfd_session_state",
63 "The session with the router changed state. Optionally change timers, take it down, bring it back, or disconnect.",
64 set_timers_action().example.clone(),
65 )
66 .with_parameters(state_params())
67 .with_actions(actions())
68});
69
70impl Protocol for BfdClientProtocol {
71 fn protocol_name(&self) -> &'static str {
72 "BFD"
73 }
74 fn stack_name(&self) -> &'static str {
75 "ETH>IP>UDP>BFD"
76 }
77 fn keywords(&self) -> Vec<&'static str> {
78 vec![
79 "bfd",
80 "bidirectional forwarding detection",
81 "bfd client",
82 "bfd session",
83 ]
84 }
85 fn description(&self) -> &'static str {
86 "BFD client: brings up a BFD session with a router (BIRD, FRR, a vendor box) in the active role and reports every state change"
87 }
88 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
89 actions()
90 }
91 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
92 vec![]
93 }
94 fn get_event_types(&self) -> Vec<EventType> {
95 vec![STARTED_EVENT.clone(), STATE_EVENT.clone()]
96 }
97 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
98 let mut params = vec![
99 ParameterDefinition {
100 name: "local_address".into(),
101 type_hint: "string".into(),
102 description: "The IP to send from and listen on: the address the router has configured as its neighbour (default: the one the route to the router uses)".into(),
103 required: false,
104 example: json!("192.0.2.2"),
105 default: None,
106 },
107 ParameterDefinition {
108 name: "multihop".into(),
109 type_hint: "boolean".into(),
110 description: "Multihop BFD (RFC 5883, port 4784): no TTL-255 check. Defaults to true when remote_addr names port 4784".into(),
111 required: false,
112 example: json!(true),
113 default: None,
114 },
115 ];
116 params.extend(timer_startup_params());
117 params.extend(auth_params());
118 params
119 }
120 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
121 use crate::protocol::metadata::*;
122 ProtocolMetadataV2::builder()
123 .state(DevelopmentState::Experimental)
124 .privilege_requirement(PrivilegeRequirement::None)
125 .implementation("The server's RFC 5880 engine (src/server/bfd/) in the active role: one session to remote_addr, sent from a source port in 49152-65535 with TTL 255, received on local_address at the BFD port beside any routing daemon there (SO_REUSEADDR); single-hop packets with a TTL other than 255 are dropped")
126 .llm_control("The session's timers, taking it administratively down and back up, and ending it, in reaction to each state change")
127 .e2e_testing("tests/client/bfd: BIRD 2 with a static multihop session to NetGet, read back with birdc (Up, timers renegotiated by Poll Sequence, AdminDown and back, keyed MD5)")
128 .notes("No Demand mode, no Echo function. Neither side of the session is the model's per packet: the model is asked when the session starts and when its state changes.")
129 .max_inbound_bytes(packet::MAX_PACKET)
130 .build()
131 }
132 fn example_prompt(&self) -> &'static str {
133 "Bring up a multihop BFD session with the router at 192.0.2.1 and tell me when it goes down"
134 }
135 fn get_startup_examples(&self) -> StartupExamples {
136 let llm = json!({"type":"open_client","protocol":"bfd","remote_addr":"127.0.0.1:4784",
137 "startup_params":{"local_address":"127.0.0.4","desired_min_tx_ms":100,"required_min_rx_ms":100},
138 "instruction":"Keep a BFD session up with the router; once it is Up, slow it to 500 ms"});
139 let mut static_example = llm.clone();
140 static_example["event_handlers"] = json!([
141 {"event_pattern":"*","handler":{"type":"static","actions":[]}}
142 ]);
143 let mut scripted = llm.clone();
144 scripted["event_handlers"] = json!([{"event_pattern":"bfd_session_state","handler":{"type":"script","language":"python",
145 "code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'bfd_set_timers','desired_min_tx_ms':500,'required_min_rx_ms':500}] if e['state']=='Up' and e['required_min_rx_ms']!=500 else []}))"}}]);
146 StartupExamples::new(llm, scripted, static_example)
147 }
148 fn group_name(&self) -> &'static str {
149 "Routing"
150 }
151}
152
153impl Client for BfdClientProtocol {
154 fn connect(
155 &self,
156 ctx: ConnectContext,
157 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
158 {
159 Box::pin(super::connect(ctx))
160 }
161 fn execute_action(&self, v: Value) -> Result<ClientActionResult> {
162 if v["type"] == "disconnect" {
163 return Ok(ClientActionResult::Disconnect);
164 }
165 crate::server::bfd::runner::check_action(&v)?;
166 Ok(ClientActionResult::Custom {
167 name: v["type"].as_str().unwrap_or_default().to_string(),
168 data: v,
169 })
170 }
171}