Skip to main content

netget/server/udp/
mod.rs

1//! UDP server implementation for raw UDP stack
2pub mod actions;
3
4use crate::server::connection::ConnectionId;
5use actions::UdpProtocol;
6use anyhow::Result;
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::net::UdpSocket;
11use tokio::sync::{mpsc, Mutex};
12
13use crate::llm::action_helper::call_llm;
14use crate::llm::ollama_client::OllamaClient;
15use crate::logging::emit::Log;
16use crate::protocol::Event;
17use crate::state::app_state::AppState;
18use actions::UDP_DATAGRAM_RECEIVED_EVENT;
19
20/// UDP server that manages UDP connections
21pub struct UdpServer;
22
23impl UdpServer {
24    /// Spawn UDP server with action-based LLM handling
25    pub async fn spawn_with_llm_actions(
26        listen_addr: SocketAddr,
27        llm_client: OllamaClient,
28        app_state: Arc<AppState>,
29        status_tx: mpsc::UnboundedSender<String>,
30        server_id: crate::state::ServerId,
31    ) -> Result<SocketAddr> {
32        let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
33        let local_addr = socket.local_addr()?;
34        Log::new(Some(&status_tx)).info(format!("UDP server listening on {}", local_addr));
35
36        let protocol = Arc::new(UdpProtocol::with_socket(socket.clone()));
37
38        let task_registrar = app_state.clone();
39        let accept_handle = tokio::spawn(async move {
40            let mut buffer = vec![0u8; 65535]; // Maximum UDP datagram size
41
42            loop {
43                match socket.recv_from(&mut buffer).await {
44                    Ok((n, peer_addr)) => {
45                        let data = buffer[..n].to_vec();
46                        let connection_id =
47                            ConnectionId::new(app_state.get_next_unified_id().await);
48
49                        // Add connection to ServerInstance (UDP "connection" = recent peer)
50                        use crate::state::server::{
51                            ConnectionState as ServerConnectionState, ConnectionStatus,
52                            ProtocolConnectionInfo,
53                        };
54                        let now = crate::utils::clock::Instant::now();
55                        let conn_state = ServerConnectionState {
56                            id: connection_id,
57                            remote_addr: peer_addr,
58                            local_addr,
59                            bytes_sent: 0,
60                            bytes_received: n as u64,
61                            packets_sent: 0,
62                            packets_received: 1,
63                            last_activity: now,
64                            status: ConnectionStatus::Active,
65                            status_changed_at: now,
66                            protocol_info: ProtocolConnectionInfo::empty(),
67                        };
68                        app_state
69                            .add_connection_to_server(server_id, conn_state)
70                            .await;
71                        let _ = status_tx.send("__UPDATE_UI__".to_string());
72
73                        // Datagram summary + full payload are FileOnly: the
74                        // udp_datagram_received event template surfaces the datagram to
75                        // the TUI, so streaming the payload here too would duplicate it
76                        // on the unbounded channel.
77                        {
78                            let log = Log::new(Some(&status_tx));
79                            if data
80                                .iter()
81                                .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace())
82                            {
83                                let data_str = String::from_utf8_lossy(&data);
84                                let preview = if data_str.len() > 100 {
85                                    format!("{}...", &data_str[..100])
86                                } else {
87                                    data_str.to_string()
88                                };
89                                log.debug(format!(
90                                    "UDP received {} bytes from {}: {}",
91                                    n, peer_addr, preview
92                                ));
93                                log.trace(format!("UDP data (text): {:?}", data_str));
94                            } else {
95                                log.debug(format!(
96                                    "UDP received {} bytes from {} (binary data)",
97                                    n, peer_addr
98                                ));
99                                log.trace(format!("UDP data (hex): {}", hex::encode(&data)));
100                            }
101                        }
102
103                        let llm_clone = llm_client.clone();
104                        let state_clone = app_state.clone();
105                        let status_clone = status_tx.clone();
106                        let socket_clone = socket.clone();
107                        let protocol_clone = protocol.clone();
108
109                        // Tracked, not detached: stop_server must abort this task too.
110                        let task_owner = app_state.clone();
111                        task_owner.spawn_server_task(server_id, async move {
112                            let log = Log::new(Some(&status_clone));
113                            // Render the payload the way the model will have to reply in.
114                            //
115                            // This used to be `format!("{:?}", data)` on a Vec<u8>, i.e. the
116                            // model was shown `[72, 101, 108, 108, 111]` for "Hello" and had
117                            // to reconstruct the text from decimal byte codes. Printable
118                            // payloads are now shown as text and binary ones as hex, and
119                            // data_encoding tells the model which it is looking at - and so
120                            // which `encoding` to use in send_udp_response.
121                            const PREVIEW_BYTES: usize = 200;
122                            let shown = &data[..data.len().min(PREVIEW_BYTES)];
123                            let printable = !shown.is_empty()
124                                && shown
125                                    .iter()
126                                    .all(|&b| b.is_ascii_graphic() || b.is_ascii_whitespace());
127                            let (data_encoding, mut data_preview) = if printable {
128                                ("text", String::from_utf8_lossy(shown).into_owned())
129                            } else {
130                                ("hex", hex::encode(shown))
131                            };
132                            if data.len() > PREVIEW_BYTES {
133                                data_preview.push_str("...");
134                            }
135
136                            let event = Event::new(
137                                &UDP_DATAGRAM_RECEIVED_EVENT,
138                                serde_json::json!({
139                                    "peer_address": peer_addr.to_string(),
140                                    "data_length": data.len(),
141                                    "data_encoding": data_encoding,
142                                    "data_preview": data_preview
143                                }),
144                            );
145
146                            log.debug(format!("UDP calling LLM for datagram from {}", peer_addr));
147
148                            match call_llm(
149                                &llm_clone,
150                                &state_clone,
151                                server_id,
152                                None,
153                                &event,
154                                protocol_clone.as_ref(),
155                            )
156                            .await
157                            {
158                                Ok(execution_result) => {
159                                    for message in &execution_result.messages {
160                                        log.info(message);
161                                    }
162
163                                    log.debug(format!(
164                                        "UDP got {} protocol results",
165                                        execution_result.protocol_results.len()
166                                    ));
167
168                                    // Classify *before* `protocol_results` is consumed below.
169                                    //
170                                    // UDP puts nothing on the wire in three of the five
171                                    // outcomes, so the log is the only place they can be told
172                                    // apart: a model that answered `ignore_datagram`, a model
173                                    // that answered nothing at all, and an action the executor
174                                    // refused are all the same silence on the socket - and the
175                                    // same silence a dead backend produces. Without a
176                                    // `decision=` token an operator has no way to tell which
177                                    // of the four they are looking at.
178                                    let action_types: Vec<&str> = execution_result
179                                        .raw_actions
180                                        .iter()
181                                        .filter_map(|a| a.get("type").and_then(|v| v.as_str()))
182                                        .collect();
183                                    let explicit_ignore = action_types.contains(&"ignore_datagram");
184                                    let dispatched_elsewhere =
185                                        action_types.contains(&"send_to_address");
186                                    let failure_summary = execution_result
187                                        .failures
188                                        .iter()
189                                        .map(|f| format!("{}: {}", f.action, f.error))
190                                        .collect::<Vec<_>>()
191                                        .join("; ");
192                                    let mut sent = 0usize;
193                                    let mut send_failed = false;
194
195                                    for protocol_result in execution_result.protocol_results {
196                                        if let Some(output_data) =
197                                            protocol_result.get_all_output().first()
198                                        {
199                                            if let Err(e) =
200                                                socket_clone.send_to(output_data, peer_addr).await
201                                            {
202                                                send_failed = true;
203                                                log.error(format!(
204                                                    "Failed to send UDP response: {}",
205                                                    e
206                                                ));
207                                            } else {
208                                                sent += 1;
209                                                // Sent summary + payload are FileOnly; the
210                                                // access line below carries the TUI.
211                                                if output_data.iter().all(|&b| {
212                                                    b.is_ascii_graphic() || b.is_ascii_whitespace()
213                                                }) {
214                                                    let data_str =
215                                                        String::from_utf8_lossy(output_data);
216                                                    let preview = if data_str.len() > 100 {
217                                                        format!("{}...", &data_str[..100])
218                                                    } else {
219                                                        data_str.to_string()
220                                                    };
221                                                    log.debug(format!(
222                                                        "UDP sent {} bytes to {}: {}",
223                                                        output_data.len(),
224                                                        peer_addr,
225                                                        preview
226                                                    ));
227                                                    log.trace(format!(
228                                                        "UDP sent (text): {:?}",
229                                                        data_str
230                                                    ));
231                                                } else {
232                                                    log.debug(format!(
233                                                        "UDP sent {} bytes to {} (binary data)",
234                                                        output_data.len(),
235                                                        peer_addr
236                                                    ));
237                                                    log.trace(format!(
238                                                        "UDP sent (hex): {}",
239                                                        hex::encode(output_data)
240                                                    ));
241                                                }
242
243                                                // Keep the pseudo-connection's counters live.
244                                                // This server registered every datagram with
245                                                // bytes_received set and then never touched
246                                                // the row again, so the rail's ↑ column and
247                                                // last_activity stayed at their initial values
248                                                // for the whole life of the entry — the module
249                                                // CLAUDE.md claimed "Response increments
250                                                // packets_sent and bytes_sent" and nothing did.
251                                                state_clone
252                                                    .update_connection_stats(
253                                                        server_id,
254                                                        connection_id,
255                                                        None,
256                                                        Some(output_data.len() as u64),
257                                                        None,
258                                                        Some(1),
259                                                    )
260                                                    .await;
261
262                                                log.info(format!(
263                                                    "UDP response to {} ({} bytes)",
264                                                    peer_addr,
265                                                    output_data.len()
266                                                ));
267                                            }
268                                        } else {
269                                            log.debug("UDP protocol result has no output data");
270                                        }
271                                    }
272
273                                    // One decision line per datagram, whatever happened.
274                                    let subject = format!(
275                                        "UDP datagram from {} ({})",
276                                        peer_addr, connection_id
277                                    );
278                                    if sent > 0 {
279                                        log.info(format!(
280                                            "{} decision=model_answer ({} reply datagram(s))",
281                                            subject, sent
282                                        ));
283                                    } else if send_failed {
284                                        log.error(format!(
285                                            "{} decision=protocol_error: the model answered but \
286                                             the reply could not be put on the socket",
287                                            subject
288                                        ));
289                                    } else if !failure_summary.is_empty() {
290                                        log.error(format!(
291                                            "{} decision=fail_closed_bad_action: nothing was sent \
292                                             because the model's action(s) could not be executed \
293                                             ({})",
294                                            subject, failure_summary
295                                        ));
296                                    } else if explicit_ignore {
297                                        log.info(format!(
298                                            "{} decision=model_reject (ignore_datagram: the model \
299                                             chose to send nothing)",
300                                            subject
301                                        ));
302                                    } else if dispatched_elsewhere {
303                                        log.info(format!(
304                                            "{} decision=model_answer (send_to_address: the \
305                                             datagram went to another address, nothing back to \
306                                             this peer)",
307                                            subject
308                                        ));
309                                    } else {
310                                        log.warn(format!(
311                                            "{} decision=model_silent: the model was asked and \
312                                             returned no usable action, so nothing was sent - \
313                                             bare UDP has no error form",
314                                            subject
315                                        ));
316                                    }
317                                }
318                                Err(e) => {
319                                    // Deliberately silent, and the one protocol in this group
320                                    // where that is the right answer.
321                                    //
322                                    // Bare UDP (RFC 768) has no error frame, no transaction
323                                    // identifier and no application semantics: this server does
324                                    // not know what the datagram meant, so it has nothing to
325                                    // say back that the peer could parse. Anything we invented
326                                    // would be indistinguishable from a real reply in whatever
327                                    // protocol the peer is actually speaking - a *worse*
328                                    // failure than silence, because the peer would act on it.
329                                    // Dropping the datagram is also normal, expected UDP
330                                    // behaviour that every UDP client already handles.
331                                    //
332                                    // A protocol layered on UDP that *does* have an error form
333                                    // must answer with it: see DNS (SERVFAIL), STUN (500
334                                    // Binding Error Response) and NTP (Kiss-o'-Death) in this
335                                    // same tree. So the silence is logged loudly here rather
336                                    // than left to be inferred.
337                                    // ERROR on both channels by design (see above and the
338                                    // module CLAUDE.md): the silent drop must be explained.
339                                    // tests/server/udp/llm_failure_test.rs asserts the phrase
340                                    // "no reply possible: bare UDP has no error form" reaches
341                                    // the status stream, so keep it here verbatim.
342                                    //
343                                    // The `decision=` token is what separates this silence from
344                                    // the model's own `ignore_datagram` silence and from a
345                                    // model that simply said nothing. On the wire all three are
346                                    // identical, so the distinction can only live in the log.
347                                    let decision = match crate::utils::wire_failure::WireFailure::classify(&e) {
348                                        crate::utils::wire_failure::WireFailure::Overloaded => {
349                                            "fail_closed_llm_overloaded"
350                                        }
351                                        crate::utils::wire_failure::WireFailure::Unavailable => {
352                                            "fail_closed_llm_error"
353                                        }
354                                    };
355                                    log.error(format!(
356                                        "UDP LLM call failed for datagram from {} ({}) decision={}: {} - no reply possible: bare UDP has no error form",
357                                        peer_addr, connection_id, decision, e
358                                    ));
359                                    if crate::llm::is_overload_error(&e) {
360                                        log.warn(format!(
361                                            "UDP datagram from {} dropped: LLM capacity exhausted",
362                                            peer_addr
363                                        ));
364                                    }
365                                }
366                            }
367                        }).await;
368                    }
369                    Err(e) => {
370                        Log::new(Some(&status_tx)).error(format!("UDP receive error: {}", e));
371                    }
372                }
373            }
374        });
375
376        task_registrar
377            .register_server_task(server_id, accept_handle)
378            .await;
379
380        Ok(local_addr)
381    }
382}
383
384/// Shared UDP socket for sending responses
385pub type SharedUdpSocket = Arc<Mutex<Arc<UdpSocket>>>;
386
387/// Map from connection ID to peer address for UDP responses
388pub type UdpPeerMap = Arc<Mutex<HashMap<ConnectionId, SocketAddr>>>;