Skip to main content

netget/server/stun/
mod.rs

1//! STUN server implementation
2pub mod actions;
3
4use crate::server::connection::ConnectionId;
5use anyhow::Result;
6use std::net::SocketAddr;
7use std::sync::Arc;
8use tokio::net::UdpSocket;
9use tokio::sync::mpsc;
10use tracing::debug;
11
12use crate::llm::action_helper::call_llm;
13use crate::llm::actions::protocol_trait::{ActionResult, Server};
14use crate::llm::ollama_client::OllamaClient;
15use crate::logging::emit::Log;
16use crate::protocol::Event;
17use crate::server::StunProtocol;
18use crate::state::app_state::AppState;
19use crate::{console_debug, console_trace};
20use actions::STUN_BINDING_REQUEST_EVENT;
21
22/// STUN server that handles binding requests
23pub struct StunServer;
24
25impl StunServer {
26    /// Spawn STUN server with integrated LLM actions
27    pub async fn spawn_with_llm_actions(
28        listen_addr: SocketAddr,
29        llm_client: OllamaClient,
30        app_state: Arc<AppState>,
31        status_tx: mpsc::UnboundedSender<String>,
32        server_id: crate::state::ServerId,
33    ) -> Result<SocketAddr> {
34        let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
35        let local_addr = socket.local_addr()?;
36        let log = Log::new(Some(&status_tx));
37        log.info(format!(
38            "STUN server (action-based) listening on {}",
39            local_addr
40        ));
41        log.debug(format!(
42            "STUN socket bound - requested: {}, actual: {}",
43            listen_addr, local_addr
44        ));
45
46        let protocol = Arc::new(StunProtocol::new());
47
48        let task_registrar = app_state.clone();
49        let accept_handle = tokio::spawn(async move {
50            let mut buffer = vec![0u8; 2048]; // STUN messages are typically < 2KB
51
52            Log::new(Some(&status_tx)).debug("STUN receive loop started, waiting for packets...");
53
54            loop {
55                debug!("STUN calling recv_from...");
56                Log::new(Some(&status_tx)).trace("STUN about to call recv_from (iteration)");
57                match socket.recv_from(&mut buffer).await {
58                    Ok((n, peer_addr)) => {
59                        Log::new(Some(&status_tx)).trace(format!(
60                            "STUN recv_from returned OK: {} bytes from {}",
61                            n, peer_addr
62                        ));
63                        let data = buffer[..n].to_vec();
64                        let connection_id =
65                            ConnectionId::new(app_state.get_next_unified_id().await);
66
67                        // Add connection to ServerInstance (STUN "connection" = transaction)
68                        use crate::state::server::{
69                            ConnectionState as ServerConnectionState, ConnectionStatus,
70                            ProtocolConnectionInfo,
71                        };
72                        let now = crate::utils::clock::Instant::now();
73                        let conn_state = ServerConnectionState {
74                            id: connection_id,
75                            remote_addr: peer_addr,
76                            local_addr,
77                            bytes_sent: 0,
78                            bytes_received: n as u64,
79                            packets_sent: 0,
80                            packets_received: 1,
81                            last_activity: now,
82                            status: ConnectionStatus::Active,
83                            status_changed_at: now,
84                            protocol_info: ProtocolConnectionInfo::empty(),
85                        };
86                        app_state
87                            .add_connection_to_server(server_id, conn_state)
88                            .await;
89                        let _ = status_tx.send("__UPDATE_UI__".to_string());
90
91                        // DEBUG: Log summary
92                        console_debug!(status_tx, "STUN received {} bytes from {}", n, peer_addr);
93
94                        // TRACE: Log full payload
95                        let hex_str = hex::encode(&data);
96                        console_trace!(status_tx, "STUN data (hex): {}", hex_str);
97
98                        let llm_clone = llm_client.clone();
99                        let state_clone = app_state.clone();
100                        let status_clone = status_tx.clone();
101                        let socket_clone = socket.clone();
102                        let protocol_clone = protocol.clone();
103
104                        // Tracked, not detached: stop_server must abort this task too.
105                        let task_owner = app_state.clone();
106                        task_owner.spawn_server_task(server_id, async move {
107                            // Parse STUN message to extract transaction ID and message type
108                            let (transaction_id, message_type, is_valid) =
109                                Self::parse_stun_header(&data);
110
111                            if !is_valid {
112                                // Decided by the protocol, before any model call: RFC 8489
113                                // says a server answers Binding *Requests* and nothing else,
114                                // and answering anything else makes this a reflector.
115                                Log::new(Some(&status_clone)).debug(format!(
116                                    "STUN dropped an unservable {} message from {} \
117                                     decision=protocol_error — only Binding requests are \
118                                     answered (see parse_stun_header)",
119                                    message_type, peer_addr
120                                ));
121                                return;
122                            }
123
124                            let transaction_id_hex =
125                                transaction_id.as_ref().map(hex::encode).unwrap_or_default();
126
127                            // Create STUN binding request event
128                            let event_data = serde_json::json!({
129                                "peer_addr": peer_addr.to_string(),
130                                "local_addr": local_addr.to_string(),
131                                "transaction_id": transaction_id_hex,
132                                "message_type": message_type,
133                                "bytes_received": data.len()
134                            });
135
136                            let event = Event::new(&STUN_BINDING_REQUEST_EVENT, event_data);
137
138                            // A STUN Binding response is 100% mechanical: reflect the source
139                            // address into XOR-MAPPED-ADDRESS and echo the 96-bit transaction ID.
140                            // There is nothing to decide, so by default we answer statically with
141                            // NO LLM round-trip. The model is consulted only when the operator
142                            // opts in — a server instruction or a per-event handler — which is
143                            // how one asks for non-standard behaviour (e.g. lying about the
144                            // mapped address for NAT testing).
145                            let wants_dynamic = operator_wants_dynamic(
146                                &state_clone,
147                                server_id,
148                                &event.event_type.id,
149                            )
150                            .await;
151
152                            if !wants_dynamic {
153                                send_static_binding_response(
154                                    protocol_clone.as_ref(),
155                                    socket_clone.as_ref(),
156                                    peer_addr,
157                                    &transaction_id_hex,
158                                    &status_clone,
159                                    "static_default",
160                                )
161                                .await;
162                                return;
163                            }
164
165                            Log::new(Some(&status_clone)).debug(format!(
166                                "STUN calling LLM for binding request from {}",
167                                peer_addr
168                            ));
169
170                            match call_llm(
171                                &llm_clone,
172                                &state_clone,
173                                server_id,
174                                None, // STUN uses UDP, no persistent connection
175                                &event,
176                                protocol_clone.as_ref(),
177                            )
178                            .await
179                            {
180                                Ok(execution_result) => {
181                                    let log = Log::new(Some(&status_clone));
182
183                                    // Display messages from LLM
184                                    for message in &execution_result.messages {
185                                        log.info(message);
186                                    }
187
188                                    log.debug(format!(
189                                        "STUN parsed {} actions",
190                                        execution_result.raw_actions.len()
191                                    ));
192
193                                    // Process protocol results
194                                    log.debug(format!(
195                                        "STUN got {} protocol results",
196                                        execution_result.protocol_results.len()
197                                    ));
198
199                                    // Classify the answer before consuming it: `ignore_request`
200                                    // is the model deciding to say nothing, which must stay
201                                    // distinguishable from the model having said nothing.
202                                    let action_count = execution_result.raw_actions.len();
203                                    let explicitly_ignored =
204                                        execution_result.raw_actions.iter().any(|a| {
205                                            a.get("type").and_then(|v| v.as_str())
206                                                == Some("ignore_request")
207                                        });
208                                    let mut datagrams_sent = 0usize;
209
210                                    for protocol_result in execution_result.protocol_results {
211                                        if let Some(output_data) =
212                                            protocol_result.get_all_output().first()
213                                        {
214                                            let _ =
215                                                socket_clone.send_to(output_data, peer_addr).await;
216                                            datagrams_sent += 1;
217
218                                            // DEBUG: Log summary
219                                            log.debug(format!(
220                                                "STUN sent {} bytes to {}",
221                                                output_data.len(),
222                                                peer_addr
223                                            ));
224
225                                            // TRACE: Log full payload
226                                            let hex_str = hex::encode(output_data);
227                                            log.trace(format!("STUN sent (hex): {}", hex_str));
228
229                                            let _ = status_clone.send(format!(
230                                                "→ STUN response to {} ({} bytes)",
231                                                peer_addr,
232                                                output_data.len()
233                                            ));
234                                        } else {
235                                            log.debug("STUN protocol result has no output data");
236                                        }
237                                    }
238
239                                    // One grep-able line per request saying how it was decided.
240                                    // Silence and refusal both leave the client waiting out its
241                                    // retransmission schedule, so the log is the only place the
242                                    // two can be told apart.
243                                    if datagrams_sent > 0 {
244                                        log.info(format!(
245                                            "STUN binding request from {} ({}) \
246                                             decision=model_answer ({} datagram(s) sent)",
247                                            peer_addr, connection_id, datagrams_sent
248                                        ));
249                                    } else if explicitly_ignored {
250                                        log.info(format!(
251                                            "STUN binding request from {} ({}) \
252                                             decision=model_reject (ignore_request: the model \
253                                             chose to send nothing)",
254                                            peer_addr, connection_id
255                                        ));
256                                    } else {
257                                        log.warn(format!(
258                                            "STUN binding request from {} ({}) \
259                                             decision=model_silent actions={} — nothing was \
260                                             sent and the client will time out",
261                                            peer_addr, connection_id, action_count
262                                        ));
263                                    }
264                                }
265                                Err(e) => {
266                                    // Fail closed to the correct static default, not to a
267                                    // permissive or misleading answer. The operator opted into
268                                    // LLM control and the LLM failed; the mechanical response
269                                    // (the client's real reflected address) is the safe fallback
270                                    // here — a STUN Binding response is never a credential or an
271                                    // approval — so send it and say so, rather than dropping the
272                                    // request or inventing an address.
273                                    //
274                                    // The token is deliberately NOT `fail_closed_*`: the peer
275                                    // gets an affirmative Binding Success Response here, and a
276                                    // tag claiming otherwise would be a lie. The error and its
277                                    // overload category go to the log only — never onto the
278                                    // wire, where a STUN message has no free-text field anyway.
279                                    let category =
280                                        match crate::utils::wire_failure::WireFailure::classify(&e)
281                                        {
282                                            crate::utils::wire_failure::WireFailure::Overloaded => {
283                                                "overloaded"
284                                            }
285                                            crate::utils::wire_failure::WireFailure::Unavailable => {
286                                                "unavailable"
287                                            }
288                                        };
289                                    Log::new(Some(&status_clone)).error(format!(
290                                        "✗ STUN LLM call failed for request from {} ({}) \
291                                         category={}: {}",
292                                        peer_addr, connection_id, category, e
293                                    ));
294                                    send_static_binding_response(
295                                        protocol_clone.as_ref(),
296                                        socket_clone.as_ref(),
297                                        peer_addr,
298                                        &transaction_id_hex,
299                                        &status_clone,
300                                        "static_fallback_llm_error",
301                                    )
302                                    .await;
303                                }
304                            }
305                        }).await;
306                    }
307                    Err(e) => {
308                        Log::new(Some(&status_tx)).error(format!("STUN receive error: {}", e));
309                        break;
310                    }
311                }
312            }
313        });
314
315        task_registrar
316            .register_server_task(server_id, accept_handle)
317            .await;
318
319        Ok(local_addr)
320    }
321
322    /// Parse STUN message header to extract transaction ID and message type.
323    ///
324    /// Returns `(transaction_id, message_type_string, servable)`. `servable` is
325    /// **not** "this parsed" — it is "this server may answer it". Everything
326    /// else is dropped without a byte leaving the socket, because on UDP a reply
327    /// goes to whatever source address the datagram claimed.
328    fn parse_stun_header(data: &[u8]) -> (Option<Vec<u8>>, String, bool) {
329        // STUN message header is 20 bytes minimum
330        if data.len() < 20 {
331            return (None, "invalid".to_string(), false);
332        }
333
334        // RFC 8489 section 5: the two most significant bits of a STUN message
335        // type are always zero. Anything else is not STUN — 0b01 there is a TURN
336        // ChannelData frame — and must not be answered.
337        if u16::from_be_bytes([data[0], data[1]]) & 0xC000 != 0 {
338            return (None, "invalid".to_string(), false);
339        }
340
341        // Check magic cookie (bytes 4-7 should be 0x2112A442)
342        let magic_cookie = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
343        if magic_cookie != 0x2112A442 {
344            return (None, "invalid".to_string(), false);
345        }
346
347        // RFC 8489 section 5: the message length counts the attributes only and
348        // is always a multiple of 4. A header whose declared length overruns the
349        // datagram is malformed, and answering it would mean answering a source
350        // address on the strength of a header we already know is lying.
351        let declared_length = u16::from_be_bytes([data[2], data[3]]) as usize;
352        if declared_length % 4 != 0 || 20usize.saturating_add(declared_length) > data.len() {
353            return (None, "invalid".to_string(), false);
354        }
355
356        // Extract message type (first 2 bytes)
357        let message_type_raw = u16::from_be_bytes([data[0], data[1]]);
358
359        // RFC 8489 section 5: the 14-bit type interleaves method and class bits as
360        //   0b00 M11 M10 M9 M8 M7 C1 M6 M5 M4 C0 M3 M2 M1 M0
361        // so C0 is bit 4 and C1 is bit 8, and class == C1<<1 | C0.
362        //
363        // The previous expression, ((raw & 0x0110) >> 4) | ((raw & 0x0100) >> 7),
364        // works out to C0 + 18*C1, which happens to be right only when C1 is 0
365        // (requests and indications). Success and error responses decoded to 18
366        // and 19, so their arms below were unreachable and every response was
367        // labelled "Unknown".
368        let c0 = (message_type_raw >> 4) & 0x1;
369        let c1 = (message_type_raw >> 8) & 0x1;
370        let class = (c1 << 1) | c0;
371        let method = (message_type_raw & 0x000F)
372            | ((message_type_raw & 0x00E0) >> 1)
373            | ((message_type_raw & 0x3E00) >> 2);
374
375        // Class values: 0 = request, 1 = indication, 2 = success response,
376        // 3 = error response.
377        let message_type = match (class, method) {
378            (0, 1) => "BindingRequest",
379            (1, 1) => "BindingIndication",
380            (2, 1) => "BindingResponse",
381            (3, 1) => "BindingError",
382            _ => "Unknown",
383        };
384
385        // Extract transaction ID (12 bytes, from byte 8 to 19)
386        let transaction_id = data[8..20].to_vec();
387
388        // Only a Binding *Request* is served. That is RFC 8489 — section 6.3.2
389        // discards an indication without replying, and a server never receives a
390        // response for a transaction it did not start — and it is also what keeps
391        // this server from being a reflector.
392        //
393        // Until now any message carrying the magic cookie was answered, a Binding
394        // *Response* included. One spoofed datagram naming a second NetGet STUN
395        // server as its source therefore set the two answering each other's
396        // answers, with nothing in either to stop it. Aimed at any other UDP
397        // listener the same spoof makes this a ~2.4x amplifier (a 20-byte request
398        // produces a 48-byte reply) for as long as the attacker keeps sending.
399        // Dropping every class but Request costs nothing a real client does.
400        let servable = class == 0 && method == 1;
401
402        (Some(transaction_id), message_type.to_string(), servable)
403    }
404}
405
406/// Returns true if the operator opted into dynamic (LLM- or handler-driven) responses for
407/// this server: either a non-empty server instruction was given, or an event handler is
408/// configured for `event_id`. When false the protocol answers with a correct static default
409/// and never consults the model — a STUN Binding response is fully determined by the request.
410async fn operator_wants_dynamic(
411    state: &AppState,
412    server_id: crate::state::ServerId,
413    event_id: &str,
414) -> bool {
415    state
416        .with_server_mut(server_id, |server| {
417            let has_instruction = !server.instruction.trim().is_empty();
418            let has_handler = server
419                .event_handler_config
420                .as_ref()
421                .map(|c| c.find_handler(event_id).is_some())
422                .unwrap_or(false);
423            has_instruction || has_handler
424        })
425        .await
426        .unwrap_or(false)
427}
428
429/// Build and send the mechanical STUN Binding Success Response with no LLM involvement:
430/// XOR-MAPPED-ADDRESS = the client's own source address, transaction ID echoed verbatim.
431///
432/// `decision` is the grep-able token for *why* this response is the one being sent —
433/// `static_default` (the operator never opted into model control, so no model was asked) or
434/// `static_fallback_llm_error` (they did, and the backend failed). It is emitted on the same
435/// line as the bytes, so one line per request says both what went out and what decided it.
436async fn send_static_binding_response(
437    protocol: &StunProtocol,
438    socket: &UdpSocket,
439    peer_addr: SocketAddr,
440    transaction_id_hex: &str,
441    status: &mpsc::UnboundedSender<String>,
442    decision: &str,
443) {
444    let action = serde_json::json!({
445        "type": "send_stun_binding_response",
446        "transaction_id": transaction_id_hex,
447        "mapped_address": peer_addr.to_string(),
448        "xor_mapped_address": true
449    });
450    match protocol.execute_action(action) {
451        Ok(ActionResult::Output(bytes)) => {
452            let _ = socket.send_to(&bytes, peer_addr).await;
453            debug!(
454                "STUN sent static binding response ({} bytes) to {}",
455                bytes.len(),
456                peer_addr
457            );
458            Log::new(Some(status)).info(format!(
459                "→ STUN static binding response to {} ({} bytes) decision={}",
460                peer_addr,
461                bytes.len(),
462                decision
463            ));
464        }
465        Ok(_) => {
466            Log::new(Some(status)).warn(format!(
467                "STUN static binding response produced no output for {} \
468                 decision=fail_closed_no_action (nothing sent)",
469                peer_addr
470            ));
471        }
472        Err(e) => {
473            // Our own mechanical response failed to build, so nothing goes out at all. The
474            // client sees a lost datagram; the log is the only record that it was not.
475            Log::new(Some(status)).error(format!(
476                "✗ STUN failed to build the static binding response for {} \
477                 decision=fail_closed_action_error: {}",
478                peer_addr, e
479            ));
480        }
481    }
482}