1pub mod actions;
9pub use actions::TftpClientProtocol;
10
11use crate::client::llm_budget::call_llm_for_client;
12use crate::llm::actions::client_trait::{Client, ClientActionResult};
13use crate::llm::ollama_client::OllamaClient;
14use crate::llm::ClientLlmResult;
15use crate::protocol::Event;
16use crate::state::app_state::AppState;
17use crate::state::{ClientId, ClientStatus};
18use anyhow::{Context, Result};
19use std::net::SocketAddr;
20use std::sync::Arc;
21use tokio::net::UdpSocket;
22use tokio::sync::{mpsc, Mutex};
23use tracing::{debug, error, info, trace, warn};
24
25use actions::{
26 TFTP_CLIENT_ACK_RECEIVED_EVENT, TFTP_CLIENT_CONNECTED_EVENT, TFTP_CLIENT_DATA_RECEIVED_EVENT,
27 TFTP_CLIENT_ERROR_EVENT, TFTP_CLIENT_TRANSFER_COMPLETE_EVENT,
28};
29
30pub const OP_RRQ: u16 = 1;
32pub const OP_WRQ: u16 = 2;
33pub const OP_DATA: u16 = 3;
34pub const OP_ACK: u16 = 4;
35pub const OP_ERROR: u16 = 5;
36
37pub const TFTP_BLOCK_SIZE: usize = 512;
39
40pub fn build_request_packet(opcode: u16, filename: &str, mode: &str) -> Vec<u8> {
44 let mut packet = Vec::with_capacity(4 + filename.len() + mode.len());
45 packet.extend_from_slice(&opcode.to_be_bytes());
46 packet.extend_from_slice(filename.as_bytes());
47 packet.push(0);
48 packet.extend_from_slice(mode.as_bytes());
49 packet.push(0);
50 packet
51}
52
53#[derive(Debug, Clone, PartialEq)]
55pub enum TftpPacket {
56 Data { block: u16, payload: Vec<u8> },
57 Ack { block: u16 },
58 Error { code: u16, message: String },
59}
60
61impl TftpPacket {
62 pub fn decode(data: &[u8]) -> Option<Self> {
65 if data.len() < 4 {
66 return None;
67 }
68 let opcode = u16::from_be_bytes([data[0], data[1]]);
69 let block = u16::from_be_bytes([data[2], data[3]]);
70 match opcode {
71 OP_DATA => Some(TftpPacket::Data {
72 block,
73 payload: data[4..].to_vec(),
74 }),
75 OP_ACK => Some(TftpPacket::Ack { block }),
76 OP_ERROR => {
77 let msg_bytes = &data[4..];
79 let end = msg_bytes
80 .iter()
81 .position(|b| *b == 0)
82 .unwrap_or(msg_bytes.len());
83 Some(TftpPacket::Error {
84 code: block,
85 message: String::from_utf8_lossy(&msg_bytes[..end]).to_string(),
86 })
87 }
88 _ => None,
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq)]
95enum Direction {
96 Read,
97 Write,
98}
99
100struct ClientData {
102 memory: String,
103}
104
105pub struct TftpClient;
106
107impl TftpClient {
108 pub async fn connect_with_llm_actions(
109 remote_addr: String,
110 llm_client: OllamaClient,
111 app_state: Arc<AppState>,
112 status_tx: mpsc::UnboundedSender<String>,
113 client_id: ClientId,
114 ) -> Result<SocketAddr> {
115 let server_addr: SocketAddr = remote_addr
116 .parse()
117 .with_context(|| format!("Invalid TFTP server address: {}", remote_addr))?;
118
119 let socket = Arc::new(
120 UdpSocket::bind("0.0.0.0:0")
121 .await
122 .context("Failed to bind TFTP client UDP socket")?,
123 );
124 let local_addr = socket.local_addr()?;
125
126 info!(
127 "TFTP client {} bound to {} (server {})",
128 client_id, local_addr, server_addr
129 );
130 app_state
131 .update_client_status(client_id, ClientStatus::Connected)
132 .await;
133 let _ = status_tx.send(format!(
134 "[CLIENT] TFTP client {} bound to {}",
135 client_id, local_addr
136 ));
137 let _ = status_tx.send("__UPDATE_UI__".to_string());
138
139 let client_data = Arc::new(Mutex::new(ClientData {
140 memory: String::new(),
141 }));
142 let protocol = Arc::new(TftpClientProtocol::new());
143
144 let transfer_addr = Arc::new(Mutex::new(server_addr));
150
151 let command_rx =
157 crate::client::command_support::register_command_channel(&app_state, client_id).await;
158
159 let cmd_task = tokio::spawn(Self::command_loop(
160 command_rx,
161 socket.clone(),
162 server_addr,
163 transfer_addr.clone(),
164 llm_client.clone(),
165 app_state.clone(),
166 status_tx.clone(),
167 client_id,
168 client_data.clone(),
169 protocol.clone(),
170 ));
171 app_state.register_client_task(client_id, cmd_task).await;
172
173 let event = Event::new(
175 &TFTP_CLIENT_CONNECTED_EVENT,
176 serde_json::json!({
177 "server_addr": server_addr.to_string(),
178 "local_addr": local_addr.to_string(),
179 }),
180 );
181
182 let instruction = app_state
183 .get_instruction_for_client(client_id)
184 .await
185 .unwrap_or_default();
186
187 let memory_snapshot = client_data.lock().await.memory.clone();
188 let llm_result = call_llm_for_client(
189 &llm_client,
190 &app_state,
191 client_id.to_string(),
192 &instruction,
193 &memory_snapshot,
194 Some(&event),
195 protocol.as_ref() as &dyn Client,
196 &status_tx,
197 )
198 .await;
199
200 let actions = match llm_result {
201 Ok(ClientLlmResult {
202 actions,
203 memory_updates,
204 }) => {
205 if let Some(mem) = memory_updates {
206 client_data.lock().await.memory = mem;
207 }
208 actions
209 }
210 Err(e) => {
211 error!("TFTP client {} LLM error on connect: {}", client_id, e);
212 let _ = status_tx.send(format!("[CLIENT] TFTP LLM error on connect: {}", e));
213 Vec::new()
214 }
215 };
216
217 let mut started = false;
220 for action in actions {
221 match protocol.execute_action(action) {
222 Ok(ClientActionResult::Custom { name, data }) => {
223 let filename = data
224 .get("filename")
225 .and_then(|v| v.as_str())
226 .unwrap_or_default()
227 .to_string();
228 let mode = data
229 .get("mode")
230 .and_then(|v| v.as_str())
231 .unwrap_or("octet")
232 .to_string();
233 if filename.is_empty() {
234 warn!("TFTP client {} got {} with empty filename", client_id, name);
235 continue;
236 }
237 let direction = match name.as_str() {
238 "tftp_read_file" => Direction::Read,
239 "tftp_write_file" => Direction::Write,
240 other => {
241 warn!(
242 "TFTP client {} unhandled custom action {}",
243 client_id, other
244 );
245 continue;
246 }
247 };
248
249 match Self::start_transfer(
250 direction,
251 socket.clone(),
252 server_addr,
253 transfer_addr.clone(),
254 filename,
255 mode,
256 llm_client.clone(),
257 app_state.clone(),
258 status_tx.clone(),
259 client_id,
260 instruction.clone(),
261 client_data.clone(),
262 protocol.clone(),
263 )
264 .await
265 {
266 Ok(_) => started = true,
267 Err(e) => {
268 error!("TFTP client {} failed to send request: {}", client_id, e);
269 app_state
270 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
271 .await;
272 }
273 }
274 }
275 Ok(ClientActionResult::Disconnect) => {
276 info!("TFTP client {} disconnecting on model request", client_id);
277 app_state
278 .update_client_status(client_id, ClientStatus::Disconnected)
279 .await;
280 app_state.remove_client_handle(client_id).await;
283 return Ok(local_addr);
284 }
285 Ok(_) => {}
286 Err(e) => {
287 warn!("TFTP client {} rejected action: {}", client_id, e);
288 }
289 }
290 }
291
292 if !started {
293 debug!(
294 "TFTP client {} idle: model requested no transfer",
295 client_id
296 );
297 }
298
299 Ok(local_addr)
300 }
301
302 #[allow(clippy::too_many_arguments)]
308 async fn start_transfer(
309 direction: Direction,
310 socket: Arc<UdpSocket>,
311 server_addr: SocketAddr,
312 transfer_addr: Arc<Mutex<SocketAddr>>,
313 filename: String,
314 mode: String,
315 llm_client: OllamaClient,
316 app_state: Arc<AppState>,
317 status_tx: mpsc::UnboundedSender<String>,
318 client_id: ClientId,
319 instruction: String,
320 client_data: Arc<Mutex<ClientData>>,
321 protocol: Arc<TftpClientProtocol>,
322 ) -> Result<usize> {
323 let opcode = match direction {
324 Direction::Read => OP_RRQ,
325 Direction::Write => OP_WRQ,
326 };
327 let request = build_request_packet(opcode, &filename, &mode);
328
329 *transfer_addr.lock().await = server_addr;
332
333 let sent = socket.send_to(&request, server_addr).await?;
334 debug!(
335 "TFTP client {} sent {} for '{}' ({})",
336 client_id,
337 if opcode == OP_RRQ { "RRQ" } else { "WRQ" },
338 filename,
339 mode
340 );
341 let _ = status_tx.send(format!(
342 "[CLIENT] TFTP {} '{}'",
343 if opcode == OP_RRQ { "RRQ" } else { "WRQ" },
344 filename
345 ));
346
347 let handle = tokio::spawn(Self::run_transfer(
348 socket,
349 transfer_addr,
350 llm_client,
351 app_state.clone(),
352 status_tx,
353 client_id,
354 instruction,
355 client_data,
356 protocol,
357 ));
358 app_state.register_client_task(client_id, handle).await;
359
360 Ok(sent)
361 }
362
363 #[allow(clippy::too_many_arguments)]
366 async fn run_transfer(
367 socket: Arc<UdpSocket>,
368 transfer_addr: Arc<Mutex<SocketAddr>>,
369 llm_client: OllamaClient,
370 app_state: Arc<AppState>,
371 status_tx: mpsc::UnboundedSender<String>,
372 client_id: ClientId,
373 instruction: String,
374 client_data: Arc<Mutex<ClientData>>,
375 protocol: Arc<TftpClientProtocol>,
376 ) {
377 let mut learned_tid = false;
380
381 let mut buffer = vec![0u8; 4 + TFTP_BLOCK_SIZE];
382 let mut total_bytes: u64 = 0;
383 let mut total_blocks: u16 = 0;
384
385 loop {
386 let received = tokio::time::timeout(
387 std::time::Duration::from_secs(5),
388 socket.recv_from(&mut buffer),
389 )
390 .await;
391
392 let (n, peer) = match received {
393 Ok(Ok(pair)) => pair,
394 Ok(Err(e)) => {
395 error!("TFTP client {} socket error: {}", client_id, e);
396 app_state
397 .update_client_status(client_id, ClientStatus::Error(e.to_string()))
398 .await;
399 return;
400 }
401 Err(_) => {
402 warn!("TFTP client {} timed out waiting for a reply", client_id);
403 let _ = status_tx.send("[CLIENT] TFTP timeout".to_string());
404 app_state
405 .update_client_status(client_id, ClientStatus::Disconnected)
406 .await;
407 return;
408 }
409 };
410
411 if !learned_tid {
412 *transfer_addr.lock().await = peer;
413 learned_tid = true;
414 trace!("TFTP client {} learned server TID {}", client_id, peer);
415 }
416
417 let packet = match TftpPacket::decode(&buffer[..n]) {
418 Some(p) => p,
419 None => {
420 debug!("TFTP client {} ignoring undecodable datagram", client_id);
421 continue;
422 }
423 };
424
425 let (event, terminal) = match &packet {
426 TftpPacket::Data { block, payload } => {
427 let is_final = payload.len() < TFTP_BLOCK_SIZE;
428 total_bytes += payload.len() as u64;
429 total_blocks = *block;
430 (
431 Event::new(
432 &TFTP_CLIENT_DATA_RECEIVED_EVENT,
433 serde_json::json!({
434 "block_number": block,
435 "data_hex": hex::encode(payload),
436 "data_length": payload.len(),
437 "is_final": is_final,
438 "total_bytes": total_bytes,
439 }),
440 ),
441 is_final,
442 )
443 }
444 TftpPacket::Ack { block } => {
445 total_blocks = *block;
446 (
447 Event::new(
448 &TFTP_CLIENT_ACK_RECEIVED_EVENT,
449 serde_json::json!({ "block_number": block }),
450 ),
451 false,
452 )
453 }
454 TftpPacket::Error { code, message } => {
455 error!(
456 "TFTP client {} received ERROR {}: {}",
457 client_id, code, message
458 );
459 let _ = status_tx.send(format!("[CLIENT] TFTP error {}: {}", code, message));
460 (
461 Event::new(
462 &TFTP_CLIENT_ERROR_EVENT,
463 serde_json::json!({
464 "error_code": code,
465 "error_message": message,
466 }),
467 ),
468 true,
469 )
470 }
471 };
472
473 let memory_snapshot = client_data.lock().await.memory.clone();
474 let llm_result = call_llm_for_client(
475 &llm_client,
476 &app_state,
477 client_id.to_string(),
478 &instruction,
479 &memory_snapshot,
480 Some(&event),
481 protocol.as_ref() as &dyn Client,
482 &status_tx,
483 )
484 .await;
485
486 let mut disconnect = false;
487 let mut wrote_short_block = false;
488 match llm_result {
489 Ok(ClientLlmResult {
490 actions,
491 memory_updates,
492 }) => {
493 if let Some(mem) = memory_updates {
494 client_data.lock().await.memory = mem;
495 }
496 for action in actions {
497 match protocol.execute_action(action) {
498 Ok(ClientActionResult::SendData(bytes)) => {
499 if bytes.len() >= 4
501 && u16::from_be_bytes([bytes[0], bytes[1]]) == OP_DATA
502 {
503 let payload_len = bytes.len() - 4;
504 total_bytes += payload_len as u64;
505 if payload_len < TFTP_BLOCK_SIZE {
506 wrote_short_block = true;
507 }
508 }
509 let dest = *transfer_addr.lock().await;
510 if let Err(e) = socket.send_to(&bytes, dest).await {
511 error!("TFTP client {} send failed: {}", client_id, e);
512 }
513 }
514 Ok(ClientActionResult::Disconnect) => disconnect = true,
515 Ok(_) => {}
516 Err(e) => {
517 warn!("TFTP client {} rejected action: {}", client_id, e);
518 }
519 }
520 }
521 }
522 Err(e) => {
523 error!("TFTP client {} LLM error: {}", client_id, e);
524 }
525 }
526
527 if terminal || disconnect || wrote_short_block {
528 if !matches!(packet, TftpPacket::Error { .. }) {
529 let complete = Event::new(
530 &TFTP_CLIENT_TRANSFER_COMPLETE_EVENT,
531 serde_json::json!({
532 "total_bytes": total_bytes,
533 "total_blocks": total_blocks,
534 }),
535 );
536 let memory_snapshot = client_data.lock().await.memory.clone();
537 match call_llm_for_client(
547 &llm_client,
548 &app_state,
549 client_id.to_string(),
550 &instruction,
551 &memory_snapshot,
552 Some(&complete),
553 protocol.as_ref() as &dyn Client,
554 &status_tx,
555 )
556 .await
557 {
558 Ok(ClientLlmResult {
559 actions,
560 memory_updates,
561 }) => {
562 if let Some(mem) = memory_updates {
563 client_data.lock().await.memory = mem;
564 }
565 for action in actions {
566 match protocol.execute_action(action) {
567 Ok(ClientActionResult::SendData(bytes)) => {
568 let dest = *transfer_addr.lock().await;
569 if let Err(e) = socket.send_to(&bytes, dest).await {
570 error!(
571 "TFTP client {} send failed after completion: {}",
572 client_id, e
573 );
574 }
575 }
576 Ok(ClientActionResult::Disconnect) => disconnect = true,
577 Ok(_) => {}
578 Err(e) => warn!(
579 "TFTP client {} rejected completion action: {}",
580 client_id, e
581 ),
582 }
583 }
584 }
585 Err(e) => {
586 error!("TFTP client {} LLM error on completion: {}", client_id, e);
587 }
588 }
589 info!(
590 "TFTP client {} transfer complete ({} bytes, {} blocks)",
591 client_id, total_bytes, total_blocks
592 );
593 let _ = status_tx.send(format!(
594 "[CLIENT] TFTP transfer complete: {} bytes",
595 total_bytes
596 ));
597 }
598 app_state
599 .update_client_status(client_id, ClientStatus::Disconnected)
600 .await;
601 let _ = status_tx.send("__UPDATE_UI__".to_string());
602 return;
603 }
604 }
605 }
606
607 #[allow(clippy::too_many_arguments)]
620 async fn command_loop(
621 mut command_rx: mpsc::Receiver<crate::state::client_handles::ClientCommand>,
622 socket: Arc<UdpSocket>,
623 server_addr: SocketAddr,
624 transfer_addr: Arc<Mutex<SocketAddr>>,
625 llm_client: OllamaClient,
626 app_state: Arc<AppState>,
627 status_tx: mpsc::UnboundedSender<String>,
628 client_id: ClientId,
629 client_data: Arc<Mutex<ClientData>>,
630 protocol: Arc<TftpClientProtocol>,
631 ) {
632 use crate::llm::actions::protocol_trait::Protocol;
633 use crate::state::client_handles::ClientSendOutcome;
634 use crate::state::AccessLogOwner;
635
636 while let Some(command) = command_rx.recv().await {
637 let action = command.action.clone();
638 let outcome = match protocol.execute_action(action.clone()) {
639 Err(e) => Ok(ClientSendOutcome::Rejected {
640 error: e.to_string(),
641 }),
642 Ok(ClientActionResult::SendData(bytes)) => {
643 let dest = *transfer_addr.lock().await;
644 match socket.send_to(&bytes, dest).await {
645 Ok(bytes_sent) => Ok(ClientSendOutcome::Sent { bytes_sent }),
646 Err(e) => Err(anyhow::anyhow!("send to {dest} failed: {e}")),
647 }
648 }
649 Ok(ClientActionResult::Custom { name, data }) => {
650 let direction = match name.as_str() {
651 "tftp_read_file" => Some(Direction::Read),
652 "tftp_write_file" => Some(Direction::Write),
653 _ => None,
654 };
655 let filename = data
656 .get("filename")
657 .and_then(|v| v.as_str())
658 .unwrap_or_default()
659 .to_string();
660 let mode = data
661 .get("mode")
662 .and_then(|v| v.as_str())
663 .unwrap_or("octet")
664 .to_string();
665
666 match direction {
667 None => Ok(ClientSendOutcome::Executed {
668 detail: format!("custom result '{name}' is not a TFTP client verb"),
669 }),
670 Some(_) if filename.is_empty() => Ok(ClientSendOutcome::Rejected {
671 error: format!("{name} needs a non-empty 'filename'"),
672 }),
673 Some(direction) => {
674 let instruction = app_state
675 .get_instruction_for_client(client_id)
676 .await
677 .unwrap_or_default();
678 match Self::start_transfer(
679 direction,
680 socket.clone(),
681 server_addr,
682 transfer_addr.clone(),
683 filename,
684 mode,
685 llm_client.clone(),
686 app_state.clone(),
687 status_tx.clone(),
688 client_id,
689 instruction,
690 client_data.clone(),
691 protocol.clone(),
692 )
693 .await
694 {
695 Ok(bytes_sent) => Ok(ClientSendOutcome::Sent { bytes_sent }),
696 Err(e) => Err(e),
697 }
698 }
699 }
700 }
701 Ok(ClientActionResult::Disconnect) => Ok(ClientSendOutcome::Disconnected),
702 Ok(ClientActionResult::WaitForMore) => Ok(ClientSendOutcome::Executed {
703 detail: "wait_for_more: nothing written to the wire".to_string(),
704 }),
705 Ok(_) => Ok(ClientSendOutcome::Executed {
706 detail: "action produced no TFTP packet".to_string(),
707 }),
708 };
709
710 let outcome_json = match &outcome {
711 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
712 Err(e) => serde_json::json!({"error": e.to_string()}),
713 };
714 app_state
715 .record_access_log(
716 AccessLogOwner::Client(client_id.as_u32()),
717 protocol.protocol_name(),
718 None,
719 "injected_action",
720 action,
721 vec![outcome_json],
722 )
723 .await;
724
725 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
726 if let Err(e) = &outcome {
727 error!("TFTP client {} injected action failed: {}", client_id, e);
728 let _ = status_tx.send(format!(
729 "[WARN] Client {} injected action failed: {}",
730 client_id, e
731 ));
732 }
733 let _ = status_tx.send("__UPDATE_UI__".to_string());
734 crate::client::command_support::reply(command, outcome);
735
736 if disconnect {
737 info!("TFTP client {} disconnecting on injected action", client_id);
738 app_state
739 .update_client_status(client_id, ClientStatus::Disconnected)
740 .await;
741 break;
742 }
743 }
744
745 app_state.remove_client_handle(client_id).await;
746 let _ = status_tx.send("__UPDATE_UI__".to_string());
747 }
748}