1pub mod actions;
3
4pub use actions::DhcpClientProtocol;
5
6use crate::client::llm_budget::call_llm_for_client;
7use crate::llm::actions::client_trait::{Client, ClientActionResult};
8use crate::llm::ollama_client::OllamaClient;
9use crate::llm::ClientLlmResult;
10use crate::protocol::Event;
11use crate::state::app_state::AppState;
12use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
13use crate::state::{AccessLogOwner, ClientId, ClientStatus};
14use anyhow::{Context, Result};
15use std::net::SocketAddr;
16use std::sync::Arc;
17use tokio::net::UdpSocket;
18use tokio::sync::{mpsc, Mutex};
19use tracing::{debug, error, info, trace};
20
21use actions::{DHCP_CLIENT_CONNECTED_EVENT, DHCP_CLIENT_RESPONSE_RECEIVED_EVENT};
22
23use dhcproto::{v4, Decodable, Decoder};
24
25#[derive(Debug, Clone, PartialEq)]
27enum ConnectionState {
28 Idle,
29 Processing,
30}
31
32struct ClientData {
34 state: ConnectionState,
35 memory: String,
36}
37
38enum Applied {
42 Sent(usize),
44 Executed(String),
46 Disconnect,
48}
49
50pub struct DhcpClient;
52
53impl DhcpClient {
54 pub async fn connect_with_llm_actions(
56 remote_addr: String,
57 llm_client: OllamaClient,
58 app_state: Arc<AppState>,
59 status_tx: mpsc::UnboundedSender<String>,
60 client_id: ClientId,
61 ) -> Result<SocketAddr> {
62 let server_addr: SocketAddr = remote_addr.parse().context("Invalid DHCP server address")?;
64
65 let socket = Arc::new(match UdpSocket::bind("0.0.0.0:68").await {
75 Ok(s) => s,
76 Err(e) => {
77 tracing::warn!(
78 "DHCP client {} could not bind port 68 ({}); using an ephemeral port \
79 - replies broadcast to port 68 will not be received",
80 client_id,
81 e
82 );
83 UdpSocket::bind("0.0.0.0:0")
84 .await
85 .context("Failed to bind a UDP socket for the DHCP client")?
86 }
87 });
88
89 socket.set_broadcast(true)?;
91
92 let local_addr = socket.local_addr()?;
93
94 info!(
95 "DHCP client {} bound to {}, targeting server {}",
96 client_id, local_addr, server_addr
97 );
98 let _ = status_tx.send(format!(
99 "[CLIENT] DHCP client {} connected, targeting server {}",
100 client_id, server_addr
101 ));
102
103 app_state
105 .update_client_status(client_id, ClientStatus::Connected)
106 .await;
107 let _ = status_tx.send("__UPDATE_UI__".to_string());
108
109 let client_data = Arc::new(Mutex::new(ClientData {
111 state: ConnectionState::Idle,
112 memory: String::new(),
113 }));
114
115 let event = Event::new(
117 &DHCP_CLIENT_CONNECTED_EVENT,
118 serde_json::json!({
119 "server_addr": server_addr.to_string(),
120 "local_addr": local_addr.to_string()
121 }),
122 );
123
124 debug!("DHCP client {} calling LLM for connected event", client_id);
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(DhcpClientProtocol::new());
139
140 match call_llm_for_client(
141 &llm_client,
142 &app_state,
143 client_id.to_string(),
144 &instruction,
145 &client_data.lock().await.memory,
146 Some(&event),
147 protocol.as_ref(),
148 &status_tx,
149 )
150 .await
151 {
152 Ok(ClientLlmResult {
153 actions,
154 memory_updates,
155 }) => {
156 if let Some(mem) = memory_updates {
158 client_data.lock().await.memory = mem;
159 }
160
161 for action in actions {
163 match protocol.as_ref().execute_action(action) {
164 Ok(action_result) => {
165 match Self::apply_action_result(
166 action_result,
167 &socket,
168 server_addr,
169 client_id,
170 )
171 .await
172 {
173 Ok(Applied::Disconnect) => {
174 info!("DHCP client {} disconnecting", client_id);
175 app_state.remove_client_handle(client_id).await;
176 return Ok(local_addr);
177 }
178 Ok(_) => {}
179 Err(e) => {
180 error!(
181 "DHCP client {} could not send after connect: {}",
182 client_id, e
183 );
184 }
185 }
186 }
187 Err(e) => {
188 error!("DHCP client {} action execution failed: {}", client_id, e);
189 }
190 }
191 }
192 }
193 Err(e) => {
194 error!("DHCP client {} LLM call failed: {}", client_id, e);
195 }
196 }
197 }
198
199 let socket_clone = socket.clone();
201 let llm_clone = llm_client.clone();
202 let state_clone = app_state.clone();
203 let status_clone = status_tx.clone();
204 let client_data_clone = client_data.clone();
205
206 let task_registrar = app_state.clone();
209 let task_handle = tokio::spawn(async move {
210 let mut buffer = vec![0u8; 1500];
211 let cmd_protocol = Arc::new(DhcpClientProtocol::new());
212
213 loop {
214 let recv_result = tokio::select! {
218 result = socket_clone.recv_from(&mut buffer) => result,
219 Some(cmd) = command_rx.recv() => {
220 if Self::handle_injected_command(
221 cmd,
222 &socket_clone,
223 server_addr,
224 &cmd_protocol,
225 &state_clone,
226 &status_clone,
227 client_id,
228 )
229 .await
230 {
231 state_clone
232 .update_client_status(client_id, ClientStatus::Disconnected)
233 .await;
234 let _ = status_clone.send(format!(
235 "[CLIENT] DHCP client {} disconnected (injected action)",
236 client_id
237 ));
238 let _ = status_clone.send("__UPDATE_UI__".to_string());
239 break;
240 }
241 continue;
242 }
243 };
244
245 match recv_result {
246 Ok((n, peer_addr)) => {
247 let data = buffer[..n].to_vec();
248
249 debug!(
250 "DHCP client {} received {} bytes from {}",
251 client_id, n, peer_addr
252 );
253 trace!("DHCP response (hex): {}", hex::encode(&data));
254
255 let mut client_data_lock = client_data_clone.lock().await;
257
258 match client_data_lock.state {
259 ConnectionState::Idle => {
260 client_data_lock.state = ConnectionState::Processing;
262 drop(client_data_lock);
263
264 let parsed_info = Self::parse_dhcp_response(&data);
266
267 let event_data = if let Some((message_type, details)) = parsed_info
268 {
269 serde_json::json!({
270 "message_type": message_type,
271 "details": details
272 })
273 } else {
274 serde_json::json!({
275 "message_type": "unknown",
276 "data_hex": hex::encode(&data),
277 "data_length": n
278 })
279 };
280
281 let event =
282 Event::new(&DHCP_CLIENT_RESPONSE_RECEIVED_EVENT, event_data);
283
284 if let Some(instruction) =
286 state_clone.get_instruction_for_client(client_id).await
287 {
288 let protocol = Arc::new(DhcpClientProtocol::new());
289
290 match call_llm_for_client(
291 &llm_clone,
292 &state_clone,
293 client_id.to_string(),
294 &instruction,
295 &client_data_clone.lock().await.memory,
296 Some(&event),
297 protocol.as_ref(),
298 &status_clone,
299 )
300 .await
301 {
302 Ok(ClientLlmResult {
303 actions,
304 memory_updates,
305 }) => {
306 if let Some(mem) = memory_updates {
308 client_data_clone.lock().await.memory = mem;
309 }
310
311 for action in actions {
313 match protocol.as_ref().execute_action(action) {
314 Ok(action_result) => {
315 match Self::apply_action_result(
316 action_result,
317 &socket_clone,
318 peer_addr,
319 client_id,
320 )
321 .await
322 {
323 Ok(Applied::Disconnect) => {
324 info!(
325 "DHCP client {} disconnecting",
326 client_id
327 );
328 state_clone
329 .update_client_status(
330 client_id,
331 ClientStatus::Disconnected,
332 )
333 .await;
334 let _ = status_clone.send(
335 "__UPDATE_UI__".to_string(),
336 );
337 break;
338 }
339 Ok(_) => {}
340 Err(e) => {
341 error!("DHCP client {} failed to send: {}", client_id, e);
342 }
343 }
344 }
345 Err(e) => {
346 error!("DHCP client {} action execution failed: {}", client_id, e);
347 }
348 }
349 }
350 }
351 Err(e) => {
352 error!(
353 "DHCP client {} LLM call failed: {}",
354 client_id, e
355 );
356 }
357 }
358 }
359
360 client_data_clone.lock().await.state = ConnectionState::Idle;
362 }
363 ConnectionState::Processing => {
364 debug!(
366 "DHCP client {} is processing, queueing response",
367 client_id
368 );
369 }
370 }
371 }
372 Err(e) => {
373 error!("DHCP client {} receive error: {}", client_id, e);
374 state_clone
375 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
376 .await;
377 let _ = status_clone.send("__UPDATE_UI__".to_string());
378 break;
379 }
380 }
381 }
382
383 state_clone.remove_client_handle(client_id).await;
386 let _ = status_clone.send("__UPDATE_UI__".to_string());
387 });
388 task_registrar
389 .register_client_task(client_id, task_handle)
390 .await;
391
392 Ok(local_addr)
393 }
394
395 async fn apply_action_result(
402 result: ClientActionResult,
403 socket: &Arc<UdpSocket>,
404 unicast_target: SocketAddr,
405 client_id: ClientId,
406 ) -> Result<Applied> {
407 match result {
408 ClientActionResult::Custom { name, data } => {
409 {
410 let (packet, label) = match name.as_str() {
411 "dhcp_discover" => (Self::build_discover_packet(&data)?, "DISCOVER"),
412 "dhcp_request" => (Self::build_request_packet(&data)?, "REQUEST"),
413 "dhcp_inform" => (Self::build_inform_packet(&data)?, "INFORM"),
416 _ => {
417 return Ok(Applied::Executed(format!(
418 "custom result '{name}' is not a DHCP wire verb"
419 )))
420 }
421 };
422
423 let broadcast = label != "INFORM"
424 && data
425 .get("broadcast")
426 .and_then(|v| v.as_bool())
427 .unwrap_or(true);
428 let target: SocketAddr = if broadcast {
429 "255.255.255.255:67"
430 .parse()
431 .expect("literal broadcast address")
432 } else {
433 unicast_target
434 };
435
436 let sent = socket.send_to(&packet, target).await?;
439 debug!(
440 "DHCP client {} sent {} ({} bytes) to {}",
441 client_id, label, sent, target
442 );
443 trace!("DHCP {} (hex): {}", label, hex::encode(&packet));
444 Ok(Applied::Sent(sent))
445 }
446 }
447 ClientActionResult::Disconnect => Ok(Applied::Disconnect),
448 ClientActionResult::WaitForMore => Ok(Applied::Executed("wait_for_more".to_string())),
449 ClientActionResult::NoAction => Ok(Applied::Executed("no_action".to_string())),
450 other => Ok(Applied::Executed(format!("no wire effect: {other:?}"))),
451 }
452 }
453
454 async fn handle_injected_command(
463 command: ClientCommand,
464 socket: &Arc<UdpSocket>,
465 unicast_target: SocketAddr,
466 protocol: &Arc<DhcpClientProtocol>,
467 app_state: &Arc<AppState>,
468 status_tx: &mpsc::UnboundedSender<String>,
469 client_id: ClientId,
470 ) -> bool {
471 use crate::llm::actions::protocol_trait::Protocol;
472
473 let action = command.action.clone();
474 let outcome = match protocol.as_ref().execute_action(action.clone()) {
475 Err(e) => Ok(ClientSendOutcome::Rejected {
476 error: e.to_string(),
477 }),
478 Ok(result) => Self::apply_action_result(result, socket, unicast_target, client_id)
479 .await
480 .map(|applied| match applied {
481 Applied::Sent(bytes_sent) => ClientSendOutcome::Sent { bytes_sent },
482 Applied::Executed(detail) => ClientSendOutcome::Executed { detail },
483 Applied::Disconnect => ClientSendOutcome::Disconnected,
484 }),
485 };
486
487 let outcome_json = match &outcome {
488 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
489 Err(e) => serde_json::json!({"error": e.to_string()}),
490 };
491 app_state
492 .record_access_log(
493 AccessLogOwner::Client(client_id.as_u32()),
494 protocol.protocol_name(),
495 None,
496 "injected_action",
497 action,
498 vec![outcome_json],
499 )
500 .await;
501
502 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
503 if let Err(e) = &outcome {
504 error!("DHCP client {} injected action failed: {}", client_id, e);
505 let _ = status_tx.send(format!(
506 "[WARN] Client {} injected action failed: {}",
507 client_id, e
508 ));
509 }
510 let _ = status_tx.send("__UPDATE_UI__".to_string());
511 crate::client::command_support::reply(command, outcome);
512 disconnect
513 }
514
515 fn build_discover_packet(params: &serde_json::Value) -> Result<Vec<u8>> {
516 use dhcproto::Encodable;
517 use std::net::Ipv4Addr;
518
519 let xid = rand::random::<u32>();
521
522 let mac_str = params
524 .get("mac_address")
525 .and_then(|v| v.as_str())
526 .unwrap_or("00:00:00:00:00:00");
527
528 let chaddr = Self::parse_mac_address(mac_str)?;
529
530 let mut msg = v4::Message::default();
532 msg.set_opcode(v4::Opcode::BootRequest)
533 .set_xid(xid)
534 .set_flags(v4::Flags::default().set_broadcast())
535 .set_chaddr(&chaddr);
536
537 msg.opts_mut()
539 .insert(v4::DhcpOption::MessageType(v4::MessageType::Discover));
540
541 if let Some(requested_ip) = params.get("requested_ip").and_then(|v| v.as_str()) {
543 if let Ok(ip) = requested_ip.parse::<Ipv4Addr>() {
544 msg.opts_mut()
545 .insert(v4::DhcpOption::RequestedIpAddress(ip));
546 }
547 }
548
549 let bytes = msg.to_vec()?;
551 Ok(bytes)
552 }
553
554 fn build_request_packet(params: &serde_json::Value) -> Result<Vec<u8>> {
555 use dhcproto::Encodable;
556 use std::net::Ipv4Addr;
557
558 let xid = rand::random::<u32>();
560
561 let mac_str = params
563 .get("mac_address")
564 .and_then(|v| v.as_str())
565 .unwrap_or("00:00:00:00:00:00");
566
567 let chaddr = Self::parse_mac_address(mac_str)?;
568
569 let requested_ip = params
571 .get("requested_ip")
572 .and_then(|v| v.as_str())
573 .context("Missing 'requested_ip' parameter")?
574 .parse::<Ipv4Addr>()?;
575
576 let server_ip = params
578 .get("server_ip")
579 .and_then(|v| v.as_str())
580 .map(|s| s.parse::<Ipv4Addr>())
581 .transpose()?;
582
583 let mut msg = v4::Message::default();
585 msg.set_opcode(v4::Opcode::BootRequest)
586 .set_xid(xid)
587 .set_flags(v4::Flags::default().set_broadcast())
588 .set_chaddr(&chaddr);
589
590 msg.opts_mut()
592 .insert(v4::DhcpOption::MessageType(v4::MessageType::Request));
593 msg.opts_mut()
594 .insert(v4::DhcpOption::RequestedIpAddress(requested_ip));
595
596 if let Some(server) = server_ip {
597 msg.opts_mut()
598 .insert(v4::DhcpOption::ServerIdentifier(server));
599 }
600
601 let bytes = msg.to_vec()?;
603 Ok(bytes)
604 }
605
606 fn build_inform_packet(params: &serde_json::Value) -> Result<Vec<u8>> {
607 use dhcproto::Encodable;
608 use std::net::Ipv4Addr;
609
610 let xid = rand::random::<u32>();
612
613 let mac_str = params
615 .get("mac_address")
616 .and_then(|v| v.as_str())
617 .unwrap_or("00:00:00:00:00:00");
618
619 let chaddr = Self::parse_mac_address(mac_str)?;
620
621 let current_ip = params
623 .get("current_ip")
624 .and_then(|v| v.as_str())
625 .context("Missing 'current_ip' parameter")?
626 .parse::<Ipv4Addr>()?;
627
628 let mut msg = v4::Message::default();
630 msg.set_opcode(v4::Opcode::BootRequest)
631 .set_xid(xid)
632 .set_ciaddr(current_ip) .set_chaddr(&chaddr);
634
635 msg.opts_mut()
637 .insert(v4::DhcpOption::MessageType(v4::MessageType::Inform));
638
639 let bytes = msg.to_vec()?;
641 Ok(bytes)
642 }
643
644 fn parse_mac_address(mac_str: &str) -> Result<Vec<u8>> {
645 let parts: Vec<&str> = mac_str.split(':').collect();
646 if parts.len() != 6 {
647 anyhow::bail!("Invalid MAC address format: {}", mac_str);
648 }
649
650 let mut mac = Vec::with_capacity(16); for part in parts {
652 let byte = u8::from_str_radix(part, 16).context("Invalid hex in MAC address")?;
653 mac.push(byte);
654 }
655
656 while mac.len() < 16 {
658 mac.push(0);
659 }
660
661 Ok(mac)
662 }
663
664 fn parse_dhcp_response(data: &[u8]) -> Option<(String, serde_json::Value)> {
665 use std::net::Ipv4Addr;
666
667 match v4::Message::decode(&mut Decoder::new(data)) {
668 Ok(msg) => {
669 if msg.hlen() as usize > 16 {
676 tracing::warn!(
677 "Dropping DHCP response with invalid hlen {} (max 16)",
678 msg.hlen()
679 );
680 return None;
681 }
682
683 let message_type = msg.opts().get(v4::OptionCode::MessageType).and_then(|opt| {
685 if let v4::DhcpOption::MessageType(mt) = opt {
686 Some(*mt)
687 } else {
688 None
689 }
690 });
691
692 let message_type_str = message_type
693 .as_ref()
694 .map(|mt| format!("{:?}", mt))
695 .unwrap_or_else(|| "Unknown".to_string());
696
697 let offered_ip = msg.yiaddr();
699 let server_ip = msg
700 .opts()
701 .get(v4::OptionCode::ServerIdentifier)
702 .and_then(|opt| {
703 if let v4::DhcpOption::ServerIdentifier(ip) = opt {
704 Some(*ip)
705 } else {
706 None
707 }
708 });
709
710 let subnet_mask = msg.opts().get(v4::OptionCode::SubnetMask).and_then(|opt| {
711 if let v4::DhcpOption::SubnetMask(mask) = opt {
712 Some(*mask)
713 } else {
714 None
715 }
716 });
717
718 let router = msg.opts().get(v4::OptionCode::Router).and_then(|opt| {
719 if let v4::DhcpOption::Router(routers) = opt {
720 routers.first().copied()
721 } else {
722 None
723 }
724 });
725
726 let dns_servers =
727 msg.opts()
728 .get(v4::OptionCode::DomainNameServer)
729 .and_then(|opt| {
730 if let v4::DhcpOption::DomainNameServer(dns) = opt {
731 Some(dns.clone())
732 } else {
733 None
734 }
735 });
736
737 let lease_time = msg
738 .opts()
739 .get(v4::OptionCode::AddressLeaseTime)
740 .and_then(|opt| {
741 if let v4::DhcpOption::AddressLeaseTime(time) = opt {
742 Some(*time)
743 } else {
744 None
745 }
746 });
747
748 let mut details = serde_json::json!({
750 "transaction_id": format!("0x{:08x}", msg.xid()),
751 "client_mac": hex::encode(msg.chaddr())
752 });
753
754 if offered_ip != Ipv4Addr::UNSPECIFIED {
755 details["offered_ip"] = serde_json::json!(offered_ip.to_string());
756 }
757
758 if let Some(server) = server_ip {
759 details["server_ip"] = serde_json::json!(server.to_string());
760 }
761
762 if let Some(mask) = subnet_mask {
763 details["subnet_mask"] = serde_json::json!(mask.to_string());
764 }
765
766 if let Some(gw) = router {
767 details["router"] = serde_json::json!(gw.to_string());
768 }
769
770 if let Some(dns) = dns_servers {
771 let dns_strs: Vec<String> = dns.iter().map(|ip| ip.to_string()).collect();
772 details["dns_servers"] = serde_json::json!(dns_strs);
773 }
774
775 if let Some(lease) = lease_time {
776 details["lease_time"] = serde_json::json!(lease);
777 }
778
779 Some((message_type_str, details))
780 }
781 Err(e) => {
782 tracing::warn!("Failed to parse DHCP response: {}", e);
783 None
784 }
785 }
786 }
787}