1use super::packet;
6use super::session::{self, Timers};
7use crate::llm::actions::{
8 protocol_trait::{ActionResult, Protocol, Server},
9 ActionDefinition, Parameter, ParameterDefinition, StartupExamples,
10};
11use crate::protocol::{log_template::LogTemplate, EventType, SpawnContext};
12use crate::state::app_state::AppState;
13use anyhow::{bail, Result};
14use serde_json::{json, Value};
15use std::sync::LazyLock;
16
17pub const ACCEPT: &str = "bfd_accept_session";
18pub const DEFAULT_AUTH_KEY_ID: u8 = 1;
19pub const MAX_SESSIONS: usize = 64;
21
22#[derive(Default, Clone)]
23pub struct BfdProtocol;
24
25impl BfdProtocol {
26 pub fn new() -> Self {
27 Self
28 }
29}
30
31pub fn p(name: &str, type_hint: &str, description: &str, required: bool) -> Parameter {
32 Parameter {
33 name: name.into(),
34 type_hint: type_hint.into(),
35 description: description.into(),
36 required,
37 }
38}
39
40pub fn action(
41 name: &str,
42 description: &str,
43 parameters: Vec<Parameter>,
44 example: Value,
45) -> ActionDefinition {
46 let info = match name {
47 "bfd_accept_session" => "-> BFD session accepted".to_string(),
48 "bfd_set_timers" => {
49 "-> BFD timers tx {desired_min_tx_ms} ms rx {required_min_rx_ms} ms".to_string()
50 }
51 "bfd_admin_down" => "-> BFD session AdminDown".to_string(),
52 "bfd_admin_up" => "-> BFD session back from AdminDown".to_string(),
53 other => format!("-> BFD {}", other.trim_start_matches("bfd_")),
54 };
55 ActionDefinition {
56 name: name.into(),
57 description: description.into(),
58 parameters,
59 example,
60 log_template: Some(LogTemplate::new().with_info(info)),
61 }
62}
63
64pub fn timer_params() -> Vec<Parameter> {
65 vec![
66 p("desired_min_tx_ms", "number", "How often this side wants to send, in ms (10-60000; at least 1000 is sent while the session is not Up)", false),
67 p("required_min_rx_ms", "number", "The fastest this side accepts packets, in ms (10-60000)", false),
68 p("detect_mult", "number", "Packets the peer may miss before the session is declared down (1-255)", false),
69 ]
70}
71
72pub fn accept_action() -> ActionDefinition {
73 action(
74 ACCEPT,
75 "Bring up a BFD session with this peer, optionally with its own timers (otherwise the server's). To refuse, answer nothing: BFD has no refusal message.",
76 timer_params(),
77 json!({"type":ACCEPT,"desired_min_tx_ms":300,"required_min_rx_ms":300,"detect_mult":3}),
78 )
79}
80
81pub fn set_timers_action() -> ActionDefinition {
82 action(
83 "bfd_set_timers",
84 "Change this session's timers; while Up the change is negotiated with a Poll Sequence.",
85 timer_params(),
86 json!({"type":"bfd_set_timers","desired_min_tx_ms":500,"required_min_rx_ms":500,"detect_mult":3}),
87 )
88}
89
90pub fn admin_down_action() -> ActionDefinition {
91 action(
92 "bfd_admin_down",
93 "Take this session administratively down; the peer sees AdminDown and goes Down.",
94 vec![p(
95 "diag",
96 "string",
97 "Diagnostic to send, e.g. administratively_down (default) or path_down",
98 false,
99 )],
100 json!({"type":"bfd_admin_down","diag":"administratively_down"}),
101 )
102}
103
104pub fn admin_up_action() -> ActionDefinition {
105 action(
106 "bfd_admin_up",
107 "Return an AdminDown session to Down, so it comes back up with the peer.",
108 vec![],
109 json!({"type":"bfd_admin_up"}),
110 )
111}
112
113pub fn session_actions() -> Vec<ActionDefinition> {
114 vec![set_timers_action(), admin_down_action(), admin_up_action()]
115}
116
117pub static SESSION_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
118 EventType::new(
119 "bfd_session_request",
120 "A BFD peer with no session here is sending Control packets. Answer bfd_accept_session to bring a session up, or nothing to ignore it.",
121 accept_action().example.clone(),
122 )
123 .with_parameters(vec![
124 p("peer", "string", "The peer's IP address", true),
125 p("peer_discriminator", "number", "The peer's My Discriminator for the session", true),
126 p("peer_state", "string", "The state the peer reports: Down, Init, Up or AdminDown", true),
127 p("desired_min_tx_ms", "number", "How often the peer wants to send, in ms", true),
128 p("required_min_rx_ms", "number", "The fastest the peer accepts packets, in ms", true),
129 p("detect_mult", "number", "The peer's detection multiplier", true),
130 p("authentication", "string", "The authentication type the peer used (simple, keyed_md5, keyed_sha1, ...), or null", false),
131 p("multihop", "boolean", "Whether this is a multihop session (RFC 5883) rather than single-hop (RFC 5881)", true),
132 ])
133 .with_actions(vec![accept_action()])
134});
135
136pub fn state_params() -> Vec<Parameter> {
137 vec![
138 p("peer", "string", "The peer's IP address", true),
139 p("state", "string", "The session's new state: AdminDown, Down, Init or Up", true),
140 p("previous_state", "string", "The state before", true),
141 p("diag", "string", "Why, as a diagnostic name, e.g. control_detection_time_expired, neighbor_signaled_session_down", true),
142 p("remote_state", "string", "The state the peer last reported", true),
143 p("local_discriminator", "number", "This side's discriminator", true),
144 p("remote_discriminator", "number", "The peer's discriminator (0 once the peer has gone silent)", true),
145 p("tx_interval_ms", "number", "The interval this side is sending at", true),
146 p("detection_time_ms", "number", "How long silence takes to bring the session down", true),
147 p("remote_desired_min_tx_ms", "number", "How often the peer wants to send", true),
148 p("remote_required_min_rx_ms", "number", "The fastest the peer accepts packets", true),
149 ]
150}
151
152pub static STATE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
153 EventType::new(
154 "bfd_session_state",
155 "A BFD session changed state. Optionally answer bfd_set_timers, bfd_admin_down or bfd_admin_up.",
156 set_timers_action().example.clone(),
157 )
158 .with_parameters(state_params())
159 .with_actions(session_actions())
160});
161
162pub fn auth_params() -> Vec<ParameterDefinition> {
164 vec![
165 ParameterDefinition {
166 name: "auth_type".into(),
167 type_hint: "string".into(),
168 description: "Authenticate every packet: simple, keyed_md5, meticulous_keyed_md5, keyed_sha1 or meticulous_keyed_sha1 (default: none)".into(),
169 required: false,
170 example: json!("keyed_sha1"),
171 default: None,
172 },
173 ParameterDefinition {
174 name: "auth_key_id".into(),
175 type_hint: "number".into(),
176 description: "The Auth Key ID both sides use, 0-255".into(),
177 required: false,
178 example: json!(1),
179 default: Some(json!(DEFAULT_AUTH_KEY_ID)),
180 },
181 ParameterDefinition {
182 name: "auth_password".into(),
183 type_hint: "string".into(),
184 description: "The shared key: up to 16 bytes (simple, MD5) or 20 (SHA1)".into(),
185 required: false,
186 example: json!("netget-bfd-key"),
187 default: None,
188 },
189 ]
190}
191
192pub fn timer_startup_params() -> Vec<ParameterDefinition> {
194 vec![
195 ParameterDefinition {
196 name: "desired_min_tx_ms".into(),
197 type_hint: "number".into(),
198 description: "How often to send once Up, in ms (10-60000)".into(),
199 required: false,
200 example: json!(100),
201 default: Some(json!(session::DEFAULT_DESIRED_MIN_TX_MS)),
202 },
203 ParameterDefinition {
204 name: "required_min_rx_ms".into(),
205 type_hint: "number".into(),
206 description: "The fastest the peer may send, in ms (10-60000)".into(),
207 required: false,
208 example: json!(100),
209 default: Some(json!(session::DEFAULT_REQUIRED_MIN_RX_MS)),
210 },
211 ParameterDefinition {
212 name: "detect_mult".into(),
213 type_hint: "number".into(),
214 description: "Missed packets before the session goes down (1-255)".into(),
215 required: false,
216 example: json!(5),
217 default: Some(json!(session::DEFAULT_DETECT_MULT)),
218 },
219 ]
220}
221
222pub fn auth_from(
224 params: Option<&crate::protocol::StartupParams>,
225) -> Result<Option<packet::AuthConfig>> {
226 let Some(params) = params else {
227 return Ok(None);
228 };
229 let kind = params.get_optional_string("auth_type")?;
230 let password = params.get_optional_string("auth_password")?;
231 let key_id = params
232 .get_optional_u64("auth_key_id")?
233 .unwrap_or(DEFAULT_AUTH_KEY_ID as u64);
234 match (kind, password) {
235 (None, None) => Ok(None),
236 (Some(kind), Some(password)) => {
237 anyhow::ensure!(key_id <= 255, "auth_key_id must be 0-255");
238 Ok(Some(packet::AuthConfig::new(
239 packet::AuthType::parse(&kind)?,
240 key_id as u8,
241 &password,
242 )?))
243 }
244 _ => bail!("auth_type and auth_password are given together"),
245 }
246}
247
248pub fn timers_from(params: Option<&crate::protocol::StartupParams>) -> Result<Timers> {
250 let mut v = json!({});
251 if let Some(params) = params {
252 for key in ["desired_min_tx_ms", "required_min_rx_ms", "detect_mult"] {
253 if let Some(n) = params.get_optional_u64(key)? {
254 v[key] = json!(n);
255 }
256 }
257 }
258 Timers::from_json(&v, Timers::default())
259}
260
261impl Protocol for BfdProtocol {
262 fn protocol_name(&self) -> &'static str {
263 "BFD"
264 }
265 fn stack_name(&self) -> &'static str {
266 "ETH>IP>UDP>BFD"
267 }
268 fn keywords(&self) -> Vec<&'static str> {
269 vec![
270 "bfd",
271 "bidirectional forwarding detection",
272 "rfc 5880",
273 "liveness",
274 "3784",
275 "4784",
276 ]
277 }
278 fn description(&self) -> &'static str {
279 "BFD speaker (RFC 5880/5881/5883): accepts sessions from routers such as BIRD and FRR, keeps them Up with periodic Control packets, and reports every state change"
280 }
281 fn get_async_actions(&self, _: &AppState) -> Vec<ActionDefinition> {
282 Vec::new()
285 }
286 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
287 let mut all = vec![accept_action()];
288 all.extend(session_actions());
289 all
290 }
291 fn get_event_types(&self) -> Vec<EventType> {
292 vec![SESSION_REQUEST_EVENT.clone(), STATE_EVENT.clone()]
293 }
294 fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
295 Some(crate::protocol::BindingDefaults::port_based("127.0.0.1", 0))
296 }
297 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
298 let mut params = vec![
299 ParameterDefinition {
300 name: "multihop".into(),
301 type_hint: "boolean".into(),
302 description: "Multihop BFD (RFC 5883, port 4784): no TTL-255 check on what arrives. Defaults to true when the server listens on 4784".into(),
303 required: false,
304 example: json!(true),
305 default: None,
306 },
307 ParameterDefinition {
308 name: "max_sessions".into(),
309 type_hint: "number".into(),
310 description: "Sessions held at once; a new peer past this is ignored".into(),
311 required: false,
312 example: json!(16),
313 default: Some(json!(MAX_SESSIONS)),
314 },
315 ];
316 params.extend(timer_startup_params());
317 params.extend(auth_params());
318 params
319 }
320 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
321 use crate::protocol::metadata::*;
322 ProtocolMetadataV2::builder()
323 .state(DevelopmentState::Experimental)
324 .privilege_requirement(PrivilegeRequirement::None)
325 .well_known_udp_port(3784)
326 .deliberately_silent()
327 .implementation("Hand-rolled RFC 5880 Asynchronous mode (src/server/bfd/): the Control packet and all five authentication types, the state machine, jittered periodic transmission, Poll/Final, the Detection Time, and the RFC 5881 TTL-255 check read with IP_RECVTTL; one task per session, sending from a source port in 49152-65535 with TTL 255; passive role (silent until a peer speaks)")
328 .llm_control("Which peers get a session and with what timers, and what to do when a session goes up or down (change timers, take it administratively down or back up)")
329 .e2e_testing("tests/server/bfd: BIRD 2 (multihop sessions brought Up and read back with birdc, timers renegotiated by Poll Sequence, AdminDown, keyed SHA1 with a right and a wrong key); a raw-socket peer for the TTL check and the bounds; the pcap oracle (tshark's bfd dissector) over the packets NetGet sent")
330 .notes("DELIBERATELY SILENT: BFD has no refusal; a peer the model does not accept (or a backend failure) is never answered, so its session stays Down (logged decision=model_silent / fail_closed_*). The model is consulted per session and per state change, never per packet. No Demand mode, no Echo function, no IPv6 TTL (hop limit) check.")
331 .max_inbound_bytes(packet::MAX_PACKET)
332 .build()
333 }
334 fn example_prompt(&self) -> &'static str {
335 "Run BFD on port 4784 and accept sessions from 192.0.2.1 with 300 ms timers"
336 }
337 fn get_startup_examples(&self) -> StartupExamples {
338 let base = json!({"type":"open_server","base_stack":"bfd","port":0,"startup_params":{"multihop":true},
339 "instruction":"Accept BFD sessions from 127.0.0.1 only, with 300 ms timers"});
340 let mut static_example = base.clone();
341 static_example["event_handlers"] = json!([
342 {"event_pattern":"bfd_session_request","handler":{"type":"static","actions":[accept_action().example]}},
343 {"event_pattern":"bfd_session_state","handler":{"type":"static","actions":[]}}
344 ]);
345 let mut scripted = base.clone();
346 scripted["event_handlers"] = json!([{"event_pattern":"bfd_session_request","handler":{"type":"script","language":"python",
347 "code":"import json,sys\ne=json.load(sys.stdin)['event']\nprint(json.dumps({'actions':[{'type':'bfd_accept_session'}] if e['peer']=='127.0.0.1' else []}))"}}]);
348 StartupExamples::new(base, scripted, static_example)
349 }
350 fn group_name(&self) -> &'static str {
351 "Routing"
352 }
353}
354
355impl Server for BfdProtocol {
356 fn spawn(
357 &self,
358 ctx: SpawnContext,
359 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
360 {
361 Box::pin(super::spawn(ctx))
362 }
363 fn execute_action(&self, action: Value) -> Result<ActionResult> {
364 let kind = action["type"].as_str().unwrap_or_default();
365 match kind {
366 ACCEPT => {
367 Timers::from_json(&action, Timers::default())?;
368 }
369 "bfd_set_timers" | "bfd_admin_down" | "bfd_admin_up" => {
370 super::runner::check_action(&action)?;
371 }
372 other => bail!("Unknown BFD action {other:?}"),
373 }
374 Ok(ActionResult::Custom {
376 name: kind.into(),
377 data: action,
378 })
379 }
380}