Skip to main content

netget/client/bfd/
actions.rs

1//! What the model can do as a BFD client: bring a session up with one router (the active
2//! role) and steer it — timers, AdminDown, back up — as its state changes.
3use 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}