Skip to main content

netget/client/udp/
mod.rs

1//! UDP client implementation
2pub mod actions;
3
4pub use actions::UdpClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::net::UdpSocket;
10use tokio::sync::{mpsc, Mutex};
11use tracing::{error, info, trace, warn};
12
13use crate::client::llm_budget::call_llm_for_client;
14use crate::client::udp::actions::{UDP_CLIENT_CONNECTED_EVENT, UDP_CLIENT_DATAGRAM_RECEIVED_EVENT};
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
23/// Connection state for LLM processing
24#[derive(Debug, Clone, PartialEq)]
25enum ConnectionState {
26    Idle,
27    Processing,
28    Accumulating,
29}
30
31/// Per-client data for LLM handling
32struct ClientData {
33    state: ConnectionState,
34    queued_datagrams: Vec<(Vec<u8>, SocketAddr)>, // (data, source_addr)
35    memory: String,
36    default_target: SocketAddr,
37}
38
39/// UDP client that sends/receives datagrams
40pub struct UdpClient;
41
42impl UdpClient {
43    /// Bind a UDP socket and integrate with LLM actions
44    pub async fn connect_with_llm_actions(
45        remote_addr: String,
46        llm_client: OllamaClient,
47        app_state: Arc<AppState>,
48        status_tx: mpsc::UnboundedSender<String>,
49        client_id: ClientId,
50    ) -> Result<SocketAddr> {
51        // Parse remote address for default target
52        let default_target: SocketAddr = remote_addr
53            .parse()
54            .context(format!("Failed to parse remote address: {}", remote_addr))?;
55
56        // Bind to local address (0.0.0.0:0 for any available port)
57        let socket = UdpSocket::bind("0.0.0.0:0")
58            .await
59            .context("Failed to bind UDP socket")?;
60
61        let local_addr = socket.local_addr()?;
62
63        info!(
64            "UDP client {} bound to {} (default target: {})",
65            client_id, local_addr, default_target
66        );
67
68        // Update client state
69        app_state
70            .update_client_status(client_id, ClientStatus::Connected)
71            .await;
72        let _ = status_tx.send(format!("[CLIENT] UDP client {} ready", client_id));
73        let _ = status_tx.send("__UPDATE_UI__".to_string());
74
75        // Initialize client data
76        let client_data = Arc::new(Mutex::new(ClientData {
77            state: ConnectionState::Idle,
78            queued_datagrams: Vec::new(),
79            memory: String::new(),
80            default_target,
81        }));
82
83        let socket_arc = Arc::new(socket);
84
85        // Command channel for injected actions (the dashboard's [ send ]).
86        //
87        // Registered BEFORE the connected-event LLM call below: a dashboard-created client
88        // defaults to a `*` -> manual routing rule, so that call can park for minutes waiting
89        // for a human, and [ send ] must work for the whole park.
90        let command_rx =
91            crate::client::command_support::register_command_channel(&app_state, client_id).await;
92
93        // The receive loop's body awaits an LLM call inline, so a `select!` arm there would
94        // stall every injected command for the length of that call. The socket is an
95        // `Arc<UdpSocket>` and `send_to` needs only `&self`, so commands get their own task
96        // and share the socket directly - no write-half mutex required.
97        let cmd_state = app_state.clone();
98        let cmd_status_tx = status_tx.clone();
99        let cmd_socket = socket_arc.clone();
100        let cmd_data = client_data.clone();
101        let cmd_task = tokio::spawn(async move {
102            Self::command_loop(
103                command_rx,
104                cmd_socket,
105                cmd_data,
106                client_id,
107                cmd_state,
108                cmd_status_tx,
109            )
110            .await;
111        });
112        app_state.register_client_task(client_id, cmd_task).await;
113
114        // Call LLM with connected event
115        // Registered with AppState so stop_client can abort this task —
116        // dropping a JoinHandle only detaches it in Tokio.
117        let task_registrar = app_state.clone();
118        let task_handle = tokio::spawn({
119            let socket_arc = socket_arc.clone();
120            let client_data = client_data.clone();
121            let llm_client = llm_client.clone();
122            let app_state = app_state.clone();
123            let status_tx = status_tx.clone();
124
125            async move {
126                // Get instruction for this client
127                if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
128                    let protocol = Arc::new(UdpClientProtocol::new());
129                    let connected_event = Event::new(
130                        &UDP_CLIENT_CONNECTED_EVENT,
131                        serde_json::json!({
132                            "remote_addr": default_target.to_string(),
133                            "local_addr": local_addr.to_string(),
134                        }),
135                    );
136
137                    // Call LLM with connected event
138                    match call_llm_for_client(
139                        &llm_client,
140                        &app_state,
141                        client_id.to_string(),
142                        &instruction,
143                        "",
144                        Some(&connected_event),
145                        protocol.as_ref(),
146                        &status_tx,
147                    )
148                    .await
149                    {
150                        Ok(llm_result) => {
151                            if let Err(e) = Self::handle_llm_result(
152                                llm_result,
153                                &socket_arc,
154                                &client_data,
155                                client_id,
156                                &app_state,
157                                &status_tx,
158                            )
159                            .await
160                            {
161                                error!(
162                                    "Error handling LLM result for UDP client {}: {}",
163                                    client_id, e
164                                );
165                            }
166                        }
167                        Err(e) => {
168                            error!(
169                                "Failed to call LLM for UDP client {} connected event: {}",
170                                client_id, e
171                            );
172                        }
173                    }
174                }
175            }
176        });
177        task_registrar
178            .register_client_task(client_id, task_handle)
179            .await;
180
181        // Spawn receive loop
182        // Registered with AppState so stop_client can abort this task —
183        // dropping a JoinHandle only detaches it in Tokio.
184        let task_registrar = app_state.clone();
185        let task_handle = tokio::spawn({
186            let socket_arc = socket_arc.clone();
187            let client_data = client_data.clone();
188
189            async move {
190                let mut buffer = vec![0u8; 65536]; // Max UDP datagram size
191
192                loop {
193                    match socket_arc.recv_from(&mut buffer).await {
194                        Ok((n, source_addr)) => {
195                            let data = buffer[..n].to_vec();
196                            trace!(
197                                "UDP client {} received {} bytes from {}",
198                                client_id,
199                                n,
200                                source_addr
201                            );
202
203                            // Handle datagram with LLM
204                            let mut client_data_lock = client_data.lock().await;
205
206                            match client_data_lock.state {
207                                ConnectionState::Idle => {
208                                    // Process immediately
209                                    client_data_lock.state = ConnectionState::Processing;
210                                    drop(client_data_lock);
211
212                                    // Process the datagram
213                                    if let Err(e) = Self::process_datagram(
214                                        data,
215                                        source_addr,
216                                        client_id,
217                                        &llm_client,
218                                        &app_state,
219                                        &status_tx,
220                                        &socket_arc,
221                                        &client_data,
222                                    )
223                                    .await
224                                    {
225                                        error!(
226                                            "Error processing UDP datagram for client {}: {}",
227                                            client_id, e
228                                        );
229
230                                        // Reset to Idle on error
231                                        let mut client_data_lock = client_data.lock().await;
232                                        client_data_lock.state = ConnectionState::Idle;
233                                    }
234                                }
235                                ConnectionState::Processing => {
236                                    // Queue the datagram
237                                    trace!(
238                                        "UDP client {} is processing, queuing datagram",
239                                        client_id
240                                    );
241                                    client_data_lock.queued_datagrams.push((data, source_addr));
242                                    drop(client_data_lock);
243                                }
244                                ConnectionState::Accumulating => {
245                                    // Accumulate the datagram
246                                    trace!(
247                                        "UDP client {} is accumulating, adding datagram",
248                                        client_id
249                                    );
250                                    client_data_lock.queued_datagrams.push((data, source_addr));
251                                    drop(client_data_lock);
252                                }
253                            }
254                        }
255                        Err(e) => {
256                            error!("UDP client {} receive error: {}", client_id, e);
257                            app_state
258                                .update_client_status(client_id, ClientStatus::Error(e.to_string()))
259                                .await;
260                            let _ = status_tx
261                                .send(format!("[CLIENT] UDP client {} error: {}", client_id, e));
262                            let _ = status_tx.send("__UPDATE_UI__".to_string());
263                            break;
264                        }
265                    }
266                }
267                // The socket is dead: drop the command handle so the dashboard stops
268                // offering [ send ] into it (a late send then fails fast rather than
269                // queueing into a loop that will never run again).
270                app_state.remove_client_handle(client_id).await;
271                let _ = status_tx.send("__UPDATE_UI__".to_string());
272            }
273        });
274        task_registrar
275            .register_client_task(client_id, task_handle)
276            .await;
277
278        Ok(local_addr)
279    }
280
281    /// Process a received datagram with the LLM
282    async fn process_datagram(
283        data: Vec<u8>,
284        source_addr: SocketAddr,
285        client_id: ClientId,
286        llm_client: &OllamaClient,
287        app_state: &Arc<AppState>,
288        status_tx: &mpsc::UnboundedSender<String>,
289        socket: &Arc<UdpSocket>,
290        client_data: &Arc<Mutex<ClientData>>,
291    ) -> Result<()> {
292        let data_hex = hex::encode(&data);
293        let data_len = data.len();
294
295        // Get instruction for this client
296        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
297            let protocol = Arc::new(UdpClientProtocol::new());
298            let event = Event::new(
299                &UDP_CLIENT_DATAGRAM_RECEIVED_EVENT,
300                serde_json::json!({
301                    "data_hex": data_hex,
302                    "data_length": data_len,
303                    "source_addr": source_addr.to_string(),
304                }),
305            );
306
307            // Get current memory
308            let memory = {
309                let client_data_lock = client_data.lock().await;
310                client_data_lock.memory.clone()
311            };
312
313            // Call LLM
314            let llm_result = call_llm_for_client(
315                llm_client,
316                app_state,
317                client_id.to_string(),
318                &instruction,
319                &memory,
320                Some(&event),
321                protocol.as_ref(),
322                status_tx,
323            )
324            .await?;
325
326            // Handle LLM result
327            Self::handle_llm_result(
328                llm_result,
329                socket,
330                client_data,
331                client_id,
332                app_state,
333                status_tx,
334            )
335            .await?;
336        }
337
338        Ok(())
339    }
340
341    /// Handle LLM result and execute actions
342    async fn handle_llm_result(
343        llm_result: ClientLlmResult,
344        socket: &Arc<UdpSocket>,
345        client_data: &Arc<Mutex<ClientData>>,
346        client_id: ClientId,
347        app_state: &Arc<AppState>,
348        status_tx: &mpsc::UnboundedSender<String>,
349    ) -> Result<()> {
350        // Update memory if needed
351        if let Some(new_memory) = llm_result.memory_updates {
352            let mut client_data_lock = client_data.lock().await;
353            client_data_lock.memory = new_memory;
354        }
355
356        // Execute actions
357        let protocol = Arc::new(UdpClientProtocol::new());
358        for action in llm_result.actions {
359            let action_result = protocol.as_ref().execute_action(action)?;
360            let waits = matches!(action_result, ClientActionResult::WaitForMore);
361
362            match Self::apply_action_result(action_result, socket, client_data, client_id).await? {
363                Applied::Disconnect => {
364                    app_state
365                        .update_client_status(client_id, ClientStatus::Disconnected)
366                        .await;
367                    let _ = status_tx.send(format!("[CLIENT] UDP client {} closed", client_id));
368                    let _ = status_tx.send("__UPDATE_UI__".to_string());
369                    return Ok(());
370                }
371                // `wait_for_more` left the client Accumulating; returning here is what
372                // keeps it there instead of falling through to the Idle reset below.
373                _ if waits => return Ok(()),
374                _ => {}
375            }
376        }
377
378        // Clear queued datagrams and return to Idle
379        // (LLM has already made its decision based on the current event)
380        let mut client_data_lock = client_data.lock().await;
381        if !client_data_lock.queued_datagrams.is_empty() {
382            trace!(
383                "UDP client {} clearing {} queued datagrams",
384                client_id,
385                client_data_lock.queued_datagrams.len()
386            );
387            client_data_lock.queued_datagrams.clear();
388        }
389        client_data_lock.state = ConnectionState::Idle;
390
391        Ok(())
392    }
393
394    /// Put one executed action on the wire.
395    ///
396    /// Shared by the connected-event path, the receive loop and injected commands, so the
397    /// datagram encoding and the target-address resolution exist exactly once and an
398    /// injected action behaves identically to an LLM-produced one.
399    async fn apply_action_result(
400        action_result: ClientActionResult,
401        socket: &Arc<UdpSocket>,
402        client_data: &Arc<Mutex<ClientData>>,
403        client_id: ClientId,
404    ) -> Result<Applied> {
405        match action_result {
406            ClientActionResult::SendData(_) => {
407                // Not used for UDP (we use Custom with target_addr)
408                warn!("SendData action not supported for UDP client, use send_udp_datagram");
409                Ok(Applied::Executed(
410                    "send_data is not a UDP client verb; use send_udp_datagram".to_string(),
411                ))
412            }
413            ClientActionResult::Custom { name, data } => {
414                if name == "send_udp_datagram" {
415                    let data_bytes = crate::client::wire_values::bytes(
416                        data["data"]
417                            .as_array()
418                            .context("Missing 'data' array in send_udp_datagram")?,
419                    )?;
420
421                    let target_addr = if let Some(target) = data["target_addr"].as_str() {
422                        target
423                            .parse::<SocketAddr>()
424                            .context(format!("Invalid target address: {}", target))?
425                    } else {
426                        // Use default target or last source
427                        let client_data_lock = client_data.lock().await;
428                        client_data_lock.default_target
429                    };
430
431                    // Send datagram. `send_to` reports what actually left the socket, so
432                    // the byte count handed back to the caller is the real one.
433                    let sent = socket.send_to(&data_bytes, target_addr).await?;
434                    trace!(
435                        "UDP client {} sent {} bytes to {}",
436                        client_id,
437                        sent,
438                        target_addr
439                    );
440                    Ok(Applied::Sent(sent))
441                } else if name == "change_target" {
442                    let new_target_str = data["new_target"]
443                        .as_str()
444                        .context("Missing 'new_target' in change_target action")?;
445
446                    let new_target = new_target_str
447                        .parse::<SocketAddr>()
448                        .context(format!("Invalid target address: {}", new_target_str))?;
449
450                    let mut client_data_lock = client_data.lock().await;
451                    client_data_lock.default_target = new_target;
452                    info!(
453                        "UDP client {} changed default target to {}",
454                        client_id, new_target
455                    );
456                    Ok(Applied::Executed(format!(
457                        "default target changed to {new_target}; nothing written to the wire"
458                    )))
459                } else {
460                    Ok(Applied::Executed(format!(
461                        "custom result '{name}' is not a UDP client verb"
462                    )))
463                }
464            }
465            ClientActionResult::Disconnect => {
466                info!("UDP client {} closing socket", client_id);
467                Ok(Applied::Disconnect)
468            }
469            ClientActionResult::WaitForMore => {
470                // Change state to Accumulating
471                let mut client_data_lock = client_data.lock().await;
472                client_data_lock.state = ConnectionState::Accumulating;
473                trace!("UDP client {} waiting for more datagrams", client_id);
474                Ok(Applied::Executed(
475                    "wait_for_more: now accumulating, nothing written to the wire".to_string(),
476                ))
477            }
478            ClientActionResult::NoAction => Ok(Applied::Executed(
479                "no_action: nothing written to the wire".to_string(),
480            )),
481            ClientActionResult::Multiple(_) => {
482                warn!("Multiple actions not yet supported in UDP client");
483                Ok(Applied::Executed(
484                    "multiple actions are not yet supported by the UDP client".to_string(),
485                ))
486            }
487        }
488    }
489
490    /// Drain injected commands until the channel closes (the client was removed) or an
491    /// injected `close_socket` ends the session.
492    ///
493    /// The generic `command_support::handle_stream_client_command` cannot serve this client:
494    /// its verbs yield `ClientActionResult::Custom` and the destination is a datagram address,
495    /// not a stream write half. The action still goes through [`Self::apply_action_result`] -
496    /// the same function the LLM path uses - and the outcome is logged and replied exactly the
497    /// way the generic arm does it.
498    async fn command_loop(
499        mut command_rx: mpsc::Receiver<ClientCommand>,
500        socket: Arc<UdpSocket>,
501        client_data: Arc<Mutex<ClientData>>,
502        client_id: ClientId,
503        app_state: Arc<AppState>,
504        status_tx: mpsc::UnboundedSender<String>,
505    ) {
506        use crate::llm::actions::protocol_trait::Protocol;
507
508        let protocol = UdpClientProtocol::new();
509
510        while let Some(command) = command_rx.recv().await {
511            let action = command.action.clone();
512            let outcome = match protocol.execute_action(action.clone()) {
513                Err(e) => Ok(ClientSendOutcome::Rejected {
514                    error: e.to_string(),
515                }),
516                Ok(result) => {
517                    match Self::apply_action_result(result, &socket, &client_data, client_id).await
518                    {
519                        // A UDP datagram either left the socket or it did not; `send_to`
520                        // returns the count, so this is never an estimate.
521                        Ok(Applied::Sent(bytes_sent)) => Ok(ClientSendOutcome::Sent { bytes_sent }),
522                        Ok(Applied::Executed(detail)) => Ok(ClientSendOutcome::Executed { detail }),
523                        Ok(Applied::Disconnect) => Ok(ClientSendOutcome::Disconnected),
524                        Err(e) => Err(e),
525                    }
526                }
527            };
528
529            let outcome_json = match &outcome {
530                Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
531                Err(e) => serde_json::json!({"error": e.to_string()}),
532            };
533            app_state
534                .record_access_log(
535                    AccessLogOwner::Client(client_id.as_u32()),
536                    protocol.protocol_name(),
537                    None,
538                    "injected_action",
539                    action,
540                    vec![outcome_json],
541                )
542                .await;
543
544            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
545            if let Err(e) = &outcome {
546                error!("UDP client {} injected action failed: {}", client_id, e);
547                let _ = status_tx.send(format!(
548                    "[WARN] Client {} injected action failed: {}",
549                    client_id, e
550                ));
551            }
552            let _ = status_tx.send("__UPDATE_UI__".to_string());
553            crate::client::command_support::reply(command, outcome);
554
555            if disconnect {
556                app_state
557                    .update_client_status(client_id, ClientStatus::Disconnected)
558                    .await;
559                let _ = status_tx.send(format!(
560                    "[CLIENT] UDP client {} closed (injected action)",
561                    client_id
562                ));
563                break;
564            }
565        }
566
567        app_state.remove_client_handle(client_id).await;
568        let _ = status_tx.send("__UPDATE_UI__".to_string());
569    }
570}
571
572/// What [`UdpClient::apply_action_result`] did with one action.
573enum Applied {
574    /// Bytes that actually left the socket in a datagram.
575    Sent(usize),
576    /// The action ran but wrote nothing; the string says why.
577    Executed(String),
578    /// The socket should be considered closed.
579    Disconnect,
580}