1pub mod actions;
3
4pub use actions::BootpClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::{IpAddr, Ipv4Addr, SocketAddr};
8use std::sync::Arc;
9use tokio::net::UdpSocket;
10use tokio::sync::{mpsc, Mutex};
11use tracing::{error, info, trace, warn};
12
13use crate::client::bootp::actions::{BOOTP_CLIENT_CONNECTED_EVENT, BOOTP_REPLY_RECEIVED_EVENT};
14use crate::client::llm_budget::call_llm_for_client;
15use crate::llm::actions::client_trait::{Client, ClientActionResult};
16use crate::llm::ollama_client::OllamaClient;
17use crate::llm::ClientLlmResult;
18use crate::protocol::Event;
19use crate::state::app_state::AppState;
20use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
21use crate::state::{AccessLogOwner, ClientId, ClientStatus};
22
23use dhcproto::v4;
24
25#[derive(Debug, Clone, PartialEq)]
27enum ConnectionState {
28 Idle,
29 Processing,
30 Accumulating,
31}
32
33struct ClientData {
35 state: ConnectionState,
36 queued_replies: Vec<BootpReply>,
37 memory: String,
38}
39
40#[derive(Debug, Clone)]
42struct BootpReply {
43 assigned_ip: Ipv4Addr,
44 server_ip: Ipv4Addr,
45 gateway_ip: Ipv4Addr,
46 boot_filename: String,
47}
48
49enum Applied {
53 Sent(usize),
55 Executed(String),
57 Disconnect,
59}
60
61pub struct BootpClient;
63
64impl BootpClient {
65 pub async fn connect_with_llm_actions(
67 remote_addr: String,
68 llm_client: OllamaClient,
69 app_state: Arc<AppState>,
70 status_tx: mpsc::UnboundedSender<String>,
71 client_id: ClientId,
72 ) -> Result<SocketAddr> {
73 let server_addr: SocketAddr = remote_addr
75 .parse()
76 .context("Invalid BOOTP server address")?;
77
78 let socket = match UdpSocket::bind("0.0.0.0:68").await {
81 Ok(s) => s,
82 Err(e) => {
83 warn!(
84 "Failed to bind to port 68 (requires privileges): {}. Binding to random port.",
85 e
86 );
87 UdpSocket::bind("0.0.0.0:0")
88 .await
89 .context("Failed to bind UDP socket")?
90 }
91 };
92
93 socket
95 .set_broadcast(true)
96 .context("Failed to enable broadcast")?;
97
98 let local_addr = socket.local_addr()?;
99
100 info!(
101 "BOOTP client {} bound to {} (server: {})",
102 client_id, local_addr, server_addr
103 );
104
105 app_state
107 .update_client_status(client_id, ClientStatus::Connected)
108 .await;
109 let _ = status_tx.send(format!("[CLIENT] BOOTP client {} connected", client_id));
110 let _ = status_tx.send("__UPDATE_UI__".to_string());
111
112 let socket_arc = Arc::new(socket);
113
114 let client_data = Arc::new(Mutex::new(ClientData {
116 state: ConnectionState::Idle,
117 queued_replies: Vec::new(),
118 memory: String::new(),
119 }));
120
121 let socket_clone = Arc::clone(&socket_arc);
123 let app_state_clone = Arc::clone(&app_state);
124 let status_tx_clone = status_tx.clone();
125
126 let mut command_rx =
134 crate::client::command_support::register_command_channel(&app_state, client_id).await;
135
136 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
138 let protocol = Arc::new(crate::client::bootp::actions::BootpClientProtocol::new());
139 let event = Event::new(
140 &BOOTP_CLIENT_CONNECTED_EVENT,
141 serde_json::json!({
142 "server_addr": server_addr.to_string(),
143 }),
144 );
145
146 match call_llm_for_client(
147 &llm_client,
148 &app_state,
149 client_id.to_string(),
150 &instruction,
151 &client_data.lock().await.memory,
152 Some(&event),
153 protocol.as_ref(),
154 &status_tx,
155 )
156 .await
157 {
158 Ok(ClientLlmResult {
159 actions,
160 memory_updates,
161 }) => {
162 if let Some(mem) = memory_updates {
164 client_data.lock().await.memory = mem;
165 }
166
167 for action in actions {
169 if let Err(e) = Self::execute_bootp_action(
170 action,
171 &socket_arc,
172 server_addr,
173 &protocol,
174 &status_tx,
175 client_id,
176 )
177 .await
178 {
179 error!("Failed to execute initial BOOTP action: {}", e);
180 }
181 }
182 }
183 Err(e) => {
184 error!(
185 "Initial LLM call failed for BOOTP client {}: {}",
186 client_id, e
187 );
188 }
189 }
190 }
191
192 let task_registrar = app_state.clone();
196 let task_handle = tokio::spawn(async move {
197 let mut buffer = vec![0u8; 1500]; let cmd_protocol = Arc::new(crate::client::bootp::actions::BootpClientProtocol::new());
199
200 loop {
201 let recv_result = tokio::select! {
205 result = socket_clone.recv_from(&mut buffer) => result,
206 Some(cmd) = command_rx.recv() => {
207 if Self::handle_injected_command(
208 cmd,
209 &socket_clone,
210 server_addr,
211 &cmd_protocol,
212 &app_state_clone,
213 &status_tx_clone,
214 client_id,
215 )
216 .await
217 {
218 app_state_clone
219 .update_client_status(client_id, ClientStatus::Disconnected)
220 .await;
221 let _ = status_tx_clone.send(format!(
222 "[CLIENT] BOOTP client {} disconnected (injected action)",
223 client_id
224 ));
225 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
226 break;
227 }
228 continue;
229 }
230 };
231
232 match recv_result {
233 Ok((n, peer)) => {
234 trace!(
235 "BOOTP client {} received {} bytes from {}",
236 client_id,
237 n,
238 peer
239 );
240
241 match Self::parse_bootp_reply(&buffer[..n]) {
243 Ok(reply) => {
244 info!(
245 "BOOTP client {} received reply: IP={}, Server={}, Boot={}",
246 client_id,
247 reply.assigned_ip,
248 reply.server_ip,
249 reply.boot_filename
250 );
251
252 let mut client_data_lock = client_data.lock().await;
254
255 match client_data_lock.state {
256 ConnectionState::Idle => {
257 client_data_lock.state = ConnectionState::Processing;
259 drop(client_data_lock);
260
261 if let Some(instruction) = app_state_clone
263 .get_instruction_for_client(client_id)
264 .await
265 {
266 let protocol = Arc::new(crate::client::bootp::actions::BootpClientProtocol::new());
267 let event = Event::new(
268 &BOOTP_REPLY_RECEIVED_EVENT,
269 serde_json::json!({
270 "assigned_ip": reply.assigned_ip.to_string(),
271 "server_ip": reply.server_ip.to_string(),
272 "boot_filename": reply.boot_filename,
273 "gateway_ip": reply.gateway_ip.to_string(),
274 }),
275 );
276
277 match call_llm_for_client(
278 &llm_client,
279 &app_state_clone,
280 client_id.to_string(),
281 &instruction,
282 &client_data.lock().await.memory,
283 Some(&event),
284 protocol.as_ref(),
285 &status_tx_clone,
286 )
287 .await
288 {
289 Ok(ClientLlmResult {
290 actions,
291 memory_updates,
292 }) => {
293 if let Some(mem) = memory_updates {
295 client_data.lock().await.memory = mem;
296 }
297
298 for action in actions {
300 if let Err(e) = Self::execute_bootp_action(
301 action,
302 &socket_clone,
303 server_addr,
304 &protocol,
305 &status_tx_clone,
306 client_id,
307 )
308 .await
309 {
310 error!("Failed to execute BOOTP action: {}", e);
311 }
312 }
313 }
314 Err(e) => {
315 error!(
316 "LLM error for BOOTP client {}: {}",
317 client_id, e
318 );
319 }
320 }
321 }
322
323 let mut client_data_lock = client_data.lock().await;
325 if !client_data_lock.queued_replies.is_empty() {
326 client_data_lock.queued_replies.clear();
327 }
328 client_data_lock.state = ConnectionState::Idle;
329 }
330 ConnectionState::Processing => {
331 client_data_lock.queued_replies.push(reply);
333 client_data_lock.state = ConnectionState::Accumulating;
334 }
335 ConnectionState::Accumulating => {
336 client_data_lock.queued_replies.push(reply);
338 }
339 }
340 }
341 Err(e) => {
342 warn!("Failed to parse BOOTP reply: {}", e);
343 }
344 }
345 }
346 Err(e) => {
347 error!("BOOTP client {} recv error: {}", client_id, e);
348 app_state_clone
349 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
350 .await;
351 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
352 break;
353 }
354 }
355 }
356
357 app_state_clone.remove_client_handle(client_id).await;
360 let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
361 });
362 task_registrar
363 .register_client_task(client_id, task_handle)
364 .await;
365
366 Ok(local_addr)
367 }
368
369 async fn execute_bootp_action(
372 action: serde_json::Value,
373 socket: &Arc<UdpSocket>,
374 server_addr: SocketAddr,
375 protocol: &Arc<BootpClientProtocol>,
376 status_tx: &mpsc::UnboundedSender<String>,
377 client_id: ClientId,
378 ) -> Result<Applied> {
379 let result = protocol.as_ref().execute_action(action)?;
380 Self::apply_action_result(result, socket, server_addr, status_tx, client_id).await
381 }
382
383 async fn apply_action_result(
387 result: ClientActionResult,
388 socket: &Arc<UdpSocket>,
389 server_addr: SocketAddr,
390 status_tx: &mpsc::UnboundedSender<String>,
391 client_id: ClientId,
392 ) -> Result<Applied> {
393 match result {
394 ClientActionResult::Custom { name, data } if name == "send_bootp_request" => {
395 let client_mac = data["client_mac"].as_str().context("Missing client_mac")?;
396 let broadcast = data["broadcast"].as_bool().unwrap_or(true);
397
398 let mac_bytes = Self::parse_mac(client_mac)?;
400
401 let bootp_request = Self::build_bootp_request(&mac_bytes)?;
403
404 let target = if broadcast {
406 SocketAddr::new(IpAddr::V4(Ipv4Addr::BROADCAST), server_addr.port())
407 } else {
408 server_addr
409 };
410
411 let sent = socket.send_to(&bootp_request, target).await?;
414 trace!("BOOTP client {} sent request to {}", client_id, target);
415 let _ = status_tx.send(format!("[CLIENT] BOOTP request sent to {}", target));
416 Ok(Applied::Sent(sent))
417 }
418 ClientActionResult::Custom { name, .. } => Ok(Applied::Executed(format!(
419 "custom result '{name}' is not a BOOTP wire verb"
420 ))),
421 ClientActionResult::Disconnect => {
422 info!("BOOTP client {} disconnecting", client_id);
423 Ok(Applied::Disconnect)
426 }
427 ClientActionResult::WaitForMore => Ok(Applied::Executed("wait_for_more".to_string())),
428 ClientActionResult::NoAction => Ok(Applied::Executed("no_action".to_string())),
429 other => Ok(Applied::Executed(format!("no wire effect: {other:?}"))),
430 }
431 }
432
433 async fn handle_injected_command(
442 command: ClientCommand,
443 socket: &Arc<UdpSocket>,
444 server_addr: SocketAddr,
445 protocol: &Arc<BootpClientProtocol>,
446 app_state: &Arc<AppState>,
447 status_tx: &mpsc::UnboundedSender<String>,
448 client_id: ClientId,
449 ) -> bool {
450 use crate::llm::actions::protocol_trait::Protocol;
451
452 let action = command.action.clone();
453 let outcome = match protocol.as_ref().execute_action(action.clone()) {
454 Err(e) => Ok(ClientSendOutcome::Rejected {
455 error: e.to_string(),
456 }),
457 Ok(result) => {
458 Self::apply_action_result(result, socket, server_addr, status_tx, client_id)
459 .await
460 .map(|applied| match applied {
461 Applied::Sent(bytes_sent) => ClientSendOutcome::Sent { bytes_sent },
462 Applied::Executed(detail) => ClientSendOutcome::Executed { detail },
463 Applied::Disconnect => ClientSendOutcome::Disconnected,
464 })
465 }
466 };
467
468 let outcome_json = match &outcome {
469 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
470 Err(e) => serde_json::json!({"error": e.to_string()}),
471 };
472 app_state
473 .record_access_log(
474 AccessLogOwner::Client(client_id.as_u32()),
475 protocol.protocol_name(),
476 None,
477 "injected_action",
478 action,
479 vec![outcome_json],
480 )
481 .await;
482
483 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
484 if let Err(e) = &outcome {
485 error!("BOOTP client {} injected action failed: {}", client_id, e);
486 let _ = status_tx.send(format!(
487 "[WARN] Client {} injected action failed: {}",
488 client_id, e
489 ));
490 }
491 let _ = status_tx.send("__UPDATE_UI__".to_string());
492 crate::client::command_support::reply(command, outcome);
493 disconnect
494 }
495
496 fn parse_mac(mac_str: &str) -> Result<[u8; 6]> {
498 let parts: Vec<&str> = mac_str.split(':').collect();
499 if parts.len() != 6 {
500 return Err(anyhow::anyhow!("Invalid MAC address format"));
501 }
502
503 let mut mac = [0u8; 6];
504 for (i, part) in parts.iter().enumerate() {
505 mac[i] = u8::from_str_radix(part, 16).context("Invalid hex digit in MAC address")?;
506 }
507
508 Ok(mac)
509 }
510
511 fn build_bootp_request(client_mac: &[u8; 6]) -> Result<Vec<u8>> {
513 {
514 let mut msg = v4::Message::new(
515 Ipv4Addr::UNSPECIFIED, Ipv4Addr::UNSPECIFIED, Ipv4Addr::UNSPECIFIED, Ipv4Addr::UNSPECIFIED, client_mac,
520 );
521 msg.set_opcode(v4::Opcode::BootRequest);
522 msg.set_flags(v4::Flags::default().set_broadcast()); msg.set_xid(rand::random::<u32>());
526
527 use dhcproto::Encodable;
528 msg.to_vec().context("Failed to encode BOOTP request")
529 }
530 }
531
532 fn parse_bootp_reply(data: &[u8]) -> Result<BootpReply> {
534 {
535 use dhcproto::decoder::Decoder;
536 use dhcproto::Decodable;
537
538 let mut decoder = Decoder::new(data);
539 let msg = v4::Message::decode(&mut decoder).context("Failed to decode BOOTP reply")?;
540
541 if msg.opcode() != v4::Opcode::BootReply {
543 return Err(anyhow::anyhow!("Not a BOOTP reply"));
544 }
545
546 let boot_filename = msg
547 .fname()
548 .and_then(|bytes| std::str::from_utf8(bytes).ok())
549 .unwrap_or("")
550 .trim_end_matches('\0')
551 .to_string();
552
553 Ok(BootpReply {
554 assigned_ip: msg.yiaddr(),
555 server_ip: msg.siaddr(),
556 gateway_ip: msg.giaddr(),
557 boot_filename,
558 })
559 }
560 }
561}