Skip to main content

netget/client/bootp/
actions.rs

1//! BOOTP client protocol actions implementation
2
3use crate::llm::actions::{
4    client_trait::{Client, ClientActionResult},
5    protocol_trait::Protocol,
6    ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// BOOTP client connected event (sent when UDP socket is bound and ready)
15pub static BOOTP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16    EventType::new(
17        "bootp_connected",
18        "BOOTP client ready to send requests",
19        json!({"type": "placeholder", "event_id": "bootp_connected"}),
20    )
21    .with_parameters(vec![Parameter {
22        name: "server_addr".to_string(),
23        type_hint: "string".to_string(),
24        description: "BOOTP/DHCP server address".to_string(),
25        required: true,
26    }])
27});
28
29/// BOOTP reply received event
30pub static BOOTP_REPLY_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31    EventType::new(
32        "bootp_reply_received",
33        "BOOTP reply received from server",
34        json!({"type": "placeholder", "event_id": "bootp_reply_received"}),
35    )
36    .with_parameters(vec![
37        Parameter {
38            name: "assigned_ip".to_string(),
39            type_hint: "string".to_string(),
40            description: "IP address assigned by BOOTP server (yiaddr)".to_string(),
41            required: true,
42        },
43        Parameter {
44            name: "server_ip".to_string(),
45            type_hint: "string".to_string(),
46            description: "BOOTP server IP address (siaddr)".to_string(),
47            required: true,
48        },
49        Parameter {
50            name: "boot_filename".to_string(),
51            type_hint: "string".to_string(),
52            description: "Boot file name (e.g., 'pxelinux.0')".to_string(),
53            required: false,
54        },
55        Parameter {
56            name: "gateway_ip".to_string(),
57            type_hint: "string".to_string(),
58            description: "Gateway IP address (giaddr)".to_string(),
59            required: false,
60        },
61    ])
62});
63
64/// BOOTP client protocol action handler
65#[derive(Default)]
66pub struct BootpClientProtocol;
67
68impl BootpClientProtocol {
69    pub fn new() -> Self {
70        Self
71    }
72}
73
74// Implement Protocol trait (common functionality)
75impl Protocol for BootpClientProtocol {
76    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
77        vec![
78            ActionDefinition {
79                name: "send_bootp_request".to_string(),
80                description: "Send a BOOTP request to discover boot server and IP address"
81                    .to_string(),
82                parameters: vec![
83                    Parameter {
84                        name: "client_mac".to_string(),
85                        type_hint: "string".to_string(),
86                        description: "Client MAC address (format: 00:11:22:33:44:55)".to_string(),
87                        required: true,
88                    },
89                    Parameter {
90                        name: "broadcast".to_string(),
91                        type_hint: "boolean".to_string(),
92                        description: "Use broadcast (true) or unicast (false)".to_string(),
93                        required: false,
94                    },
95                ],
96                example: json!({
97                    "type": "send_bootp_request",
98                    "client_mac": "00:11:22:33:44:55",
99                    "broadcast": true
100                }),
101                log_template: None,
102            },
103            ActionDefinition {
104                name: "disconnect".to_string(),
105                description: "Close the BOOTP client".to_string(),
106                parameters: vec![],
107                example: json!({
108                    "type": "disconnect"
109                }),
110                log_template: None,
111            },
112        ]
113    }
114    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
115        vec![
116            ActionDefinition {
117                name: "send_bootp_request".to_string(),
118                description: "Send another BOOTP request in response to a reply".to_string(),
119                parameters: vec![
120                    Parameter {
121                        name: "client_mac".to_string(),
122                        type_hint: "string".to_string(),
123                        description: "Client MAC address (format: 00:11:22:33:44:55)".to_string(),
124                        required: true,
125                    },
126                    Parameter {
127                        name: "broadcast".to_string(),
128                        type_hint: "boolean".to_string(),
129                        description: "Use broadcast (true) or unicast (false)".to_string(),
130                        required: false,
131                    },
132                ],
133                example: json!({
134                    "type": "send_bootp_request",
135                    "client_mac": "00:11:22:33:44:55",
136                    "broadcast": true
137                }),
138                log_template: None,
139            },
140            ActionDefinition {
141                name: "wait_for_more".to_string(),
142                description: "Wait for more BOOTP replies before responding".to_string(),
143                parameters: vec![],
144                example: json!({
145                    "type": "wait_for_more"
146                }),
147                log_template: None,
148            },
149        ]
150    }
151    fn protocol_name(&self) -> &'static str {
152        "BOOTP"
153    }
154    fn get_event_types(&self) -> Vec<EventType> {
155        vec![
156            EventType::new(
157                "bootp_connected",
158                "Triggered when BOOTP client is ready",
159                json!({"type": "placeholder", "event_id": "bootp_connected"}),
160            ),
161            EventType::new(
162                "bootp_reply_received",
163                "Triggered when BOOTP reply is received from server",
164                json!({"type": "placeholder", "event_id": "bootp_reply_received"}),
165            ),
166        ]
167    }
168    fn stack_name(&self) -> &'static str {
169        "ETH>IP>UDP>BOOTP"
170    }
171    fn keywords(&self) -> Vec<&'static str> {
172        vec!["bootp", "bootp client", "bootstrap protocol"]
173    }
174    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
175        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
176
177        ProtocolMetadataV2::builder()
178            .state(DevelopmentState::Experimental)
179            .implementation("dhcproto crate for BOOTP packet encoding/decoding")
180            .llm_control("Full control over BOOTP requests and reply interpretation")
181            .e2e_testing("dnsmasq or isc-dhcp-server for BOOTP testing")
182            .build()
183    }
184    fn description(&self) -> &'static str {
185        "BOOTP client for diskless workstation boot discovery (PXE boot, TFTP server location)"
186    }
187    fn example_prompt(&self) -> &'static str {
188        "Connect to BOOTP server at 192.168.1.1:67 and request boot information for MAC 00:11:22:33:44:55"
189    }
190    fn group_name(&self) -> &'static str {
191        "Network Services"
192    }
193
194    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
195        use crate::llm::actions::StartupExamples;
196        use serde_json::json;
197
198        StartupExamples::new(
199            // LLM mode: LLM controls BOOTP discovery
200            json!({
201                "type": "open_client",
202                "remote_addr": "192.168.1.1:67",
203                "base_stack": "bootp",
204                "instruction": "Request boot information for MAC 00:11:22:33:44:55 and report the boot server and filename"
205            }),
206            // Script mode: Code-based deterministic responses
207            json!({
208                "type": "open_client",
209                "remote_addr": "192.168.1.1:67",
210                "base_stack": "bootp",
211                "event_handlers": [{
212                    "event_pattern": "bootp_reply_received",
213                    "handler": {
214                        "type": "script",
215                        "language": "python",
216                        "code": "<bootp_client_handler>"
217                    }
218                }]
219            }),
220            // Static mode: Fixed BOOTP request on connect
221            json!({
222                "type": "open_client",
223                "remote_addr": "192.168.1.1:67",
224                "base_stack": "bootp",
225                "event_handlers": [
226                    {
227                        "event_pattern": "bootp_connected",
228                        "handler": {
229                            "type": "static",
230                            "actions": [{
231                                "type": "send_bootp_request",
232                                "client_mac": "00:11:22:33:44:55",
233                                "broadcast": true
234                            }]
235                        }
236                    },
237                    {
238                        "event_pattern": "bootp_reply_received",
239                        "handler": {
240                            "type": "static",
241                            "actions": [{
242                                "type": "disconnect"
243                            }]
244                        }
245                    }
246                ]
247            }),
248        )
249    }
250}
251
252// Implement Client trait (client-specific functionality)
253impl Client for BootpClientProtocol {
254    fn connect(
255        &self,
256        ctx: crate::protocol::ConnectContext,
257    ) -> std::pin::Pin<
258        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
259    > {
260        Box::pin(async move {
261            use crate::client::bootp::BootpClient;
262            BootpClient::connect_with_llm_actions(
263                ctx.remote_addr,
264                ctx.llm_client,
265                ctx.state,
266                ctx.status_tx,
267                ctx.client_id,
268            )
269            .await
270        })
271    }
272    fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
273        let action_type = action
274            .get("type")
275            .and_then(|v| v.as_str())
276            .context("Missing 'type' field in action")?;
277
278        match action_type {
279            "send_bootp_request" => {
280                let client_mac = action
281                    .get("client_mac")
282                    .and_then(|v| v.as_str())
283                    .context("Missing 'client_mac' field")?;
284
285                let broadcast = action
286                    .get("broadcast")
287                    .and_then(|v| v.as_bool())
288                    .unwrap_or(true);
289
290                Ok(ClientActionResult::Custom {
291                    name: "send_bootp_request".to_string(),
292                    data: json!({
293                        "client_mac": client_mac,
294                        "broadcast": broadcast,
295                    }),
296                })
297            }
298            "disconnect" => Ok(ClientActionResult::Disconnect),
299            "wait_for_more" => Ok(ClientActionResult::WaitForMore),
300            _ => Err(anyhow::anyhow!(
301                "Unknown BOOTP client action: {}",
302                action_type
303            )),
304        }
305    }
306}