Skip to main content

netget/client/turn/
mod.rs

1//! TURN client implementation
2pub mod actions;
3
4pub use actions::TurnClientProtocol;
5
6use anyhow::{Context, Result};
7use std::collections::HashMap;
8use std::net::SocketAddr;
9use std::sync::Arc;
10use tokio::net::UdpSocket;
11use tokio::sync::{mpsc, Mutex};
12use tracing::{debug, error, info, trace, warn};
13
14use crate::client::llm_budget::call_llm_for_client;
15use crate::client::turn::actions::{
16    TURN_CLIENT_ALLOCATED_EVENT, TURN_CLIENT_CONNECTED_EVENT, TURN_CLIENT_DATA_RECEIVED_EVENT,
17    TURN_CLIENT_PERMISSION_CREATED_EVENT, TURN_CLIENT_REFRESHED_EVENT,
18};
19use crate::llm::ollama_client::OllamaClient;
20use crate::llm::ClientLlmResult;
21use crate::logging::emit::Log;
22use crate::protocol::Event;
23use crate::state::app_state::AppState;
24use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
25use crate::state::{AccessLogOwner, ClientId, ClientStatus};
26
27/// Connection state for LLM processing
28#[derive(Debug, Clone, PartialEq)]
29enum ConnectionState {
30    Idle,
31    Processing,
32    Accumulating,
33}
34
35/// Per-client data for LLM handling
36struct ClientData {
37    state: ConnectionState,
38    queued_events: Vec<Event>,
39    memory: String,
40    relay_address: Option<SocketAddr>,
41    /// Transaction ID (hex) -> the peer a CreatePermission request named.
42    ///
43    /// A CreatePermission Success Response carries no XOR-PEER-ADDRESS — RFC 8656
44    /// section 9.4 says it is empty — so the only way to know which peer was
45    /// permitted is to remember what we asked for. Without this the
46    /// `turn_permission_created` event reported `"peer_address": "unknown"` on a
47    /// field its own declaration marks `required: true`, i.e. a fabricated
48    /// constant where the model expected a fact.
49    ///
50    /// Bounded at `MAX_PENDING_PERMISSIONS` so a model issuing create_permission
51    /// in a loop cannot grow it without limit; a request evicted early simply
52    /// reports the peer as unknown again, which is the old behaviour and no worse.
53    pending_permissions: HashMap<String, SocketAddr>,
54}
55
56/// Cap on outstanding CreatePermission requests remembered for correlation.
57const MAX_PENDING_PERMISSIONS: usize = 256;
58
59/// TURN client that connects to a remote TURN server
60pub struct TurnClient;
61
62impl TurnClient {
63    /// Connect to a TURN server with integrated LLM actions
64    pub async fn connect_with_llm_actions(
65        remote_addr: String,
66        llm_client: OllamaClient,
67        app_state: Arc<AppState>,
68        status_tx: mpsc::UnboundedSender<String>,
69        client_id: ClientId,
70    ) -> Result<SocketAddr> {
71        // Parse remote address
72        let remote_sock_addr: SocketAddr = remote_addr
73            .parse()
74            .context(format!("Invalid TURN server address: {}", remote_addr))?;
75
76        // Create UDP socket (TURN uses UDP transport)
77        let socket = UdpSocket::bind("0.0.0.0:0")
78            .await
79            .context("Failed to bind UDP socket")?;
80
81        let local_addr = socket.local_addr()?;
82
83        info!(
84            "TURN client {} bound to {} (server: {})",
85            client_id, local_addr, remote_sock_addr
86        );
87
88        // Update client state
89        app_state
90            .update_client_status(client_id, ClientStatus::Connected)
91            .await;
92        Log::new(Some(&status_tx)).info(format!(
93            "TURN client {} connected to {}",
94            client_id, remote_sock_addr
95        ));
96        let _ = status_tx.send("__UPDATE_UI__".to_string());
97
98        // Send initial connected event to LLM
99        let protocol = Arc::new(crate::client::turn::actions::TurnClientProtocol::new());
100        let event = Event::new(
101            &TURN_CLIENT_CONNECTED_EVENT,
102            serde_json::json!({
103                "remote_addr": remote_sock_addr.to_string(),
104            }),
105        );
106
107        let socket_arc = Arc::new(socket);
108
109        // Initialize client data
110        let client_data = Arc::new(Mutex::new(ClientData {
111            state: ConnectionState::Idle,
112            queued_events: Vec::new(),
113            memory: String::new(),
114            relay_address: None,
115            pending_permissions: HashMap::new(),
116        }));
117
118        // Command channel for injected actions (the dashboard's [ send ]).
119        // Registered BEFORE the connected-event LLM call: a manual `*` routing rule can
120        // park that call for minutes, and the operator must be able to reach the client
121        // while it waits. It gets its own task rather than a `select!` arm in the read
122        // loop, so an injected Allocate/Send does not queue behind an in-flight LLM call;
123        // the socket is an `Arc<UdpSocket>` and `send_to` takes `&self`, so both tasks can
124        // write to it.
125        let command_rx =
126            crate::client::command_support::register_command_channel(&app_state, client_id).await;
127        let cmd_task = tokio::spawn(Self::command_loop(
128            command_rx,
129            socket_arc.clone(),
130            remote_sock_addr,
131            protocol.clone(),
132            client_data.clone(),
133            client_id,
134            app_state.clone(),
135            status_tx.clone(),
136        ));
137        app_state.register_client_task(client_id, cmd_task).await;
138
139        if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
140            match call_llm_for_client(
141                &llm_client,
142                &app_state,
143                client_id.to_string(),
144                &instruction,
145                "",
146                Some(&event),
147                protocol.as_ref(),
148                &status_tx,
149            )
150            .await
151            {
152                Ok(result) => {
153                    debug!(
154                        "TURN client {} initial LLM call returned {} actions",
155                        client_id,
156                        result.actions.len()
157                    );
158
159                    // Run them. They were counted, logged and dropped, so a client told
160                    // to "connect and allocate a relay" connected and then sat there --
161                    // nothing ever reached the socket, and the server saw no request at
162                    // all. They go through `handle_action_result`, the same function the
163                    // read loop and the injected-command path use, so the wire encoding
164                    // exists once.
165                    use crate::llm::actions::client_trait::Client;
166                    for action in result.actions {
167                        let executed = match protocol.as_ref().execute_action(action) {
168                            Ok(executed) => executed,
169                            Err(e) => {
170                                error!("TURN client {} rejected action: {}", client_id, e);
171                                continue;
172                            }
173                        };
174                        if let Err(e) = Self::handle_action_result(
175                            executed,
176                            &socket_arc,
177                            remote_sock_addr,
178                            &client_data,
179                            &status_tx,
180                            client_id,
181                        )
182                        .await
183                        {
184                            error!("TURN client {} initial action failed: {}", client_id, e);
185                        }
186                    }
187                }
188                Err(e) => {
189                    error!("TURN client {} initial LLM call failed: {}", client_id, e);
190                }
191            }
192        }
193
194        // Spawn read loop for receiving TURN responses
195        let socket_clone = socket_arc.clone();
196        let llm_clone = llm_client.clone();
197        let app_state_clone = app_state.clone();
198        let status_clone = status_tx.clone();
199        let client_data_clone = client_data.clone();
200        let protocol_clone = protocol.clone();
201
202        // Registered with AppState so stop_client can abort this task —
203        // dropping a JoinHandle only detaches it in Tokio.
204        let task_registrar = app_state.clone();
205        let task_handle = tokio::spawn(async move {
206            let mut buffer = vec![0u8; 2048]; // TURN messages typically < 2KB
207
208            loop {
209                match socket_clone.recv_from(&mut buffer).await {
210                    Ok((n, peer_addr)) => {
211                        let data = buffer[..n].to_vec();
212                        trace!(
213                            "TURN client {} received {} bytes from {}",
214                            client_id,
215                            n,
216                            peer_addr
217                        );
218
219                        // Parse TURN/STUN message
220                        let (transaction_id, message_type, _is_valid) =
221                            Self::parse_turn_header(&data);
222
223                        let transaction_id_hex = transaction_id
224                            .map(|tid| hex::encode(tid))
225                            .unwrap_or_default();
226
227                        debug!(
228                            "TURN client {} received {} (transaction: {})",
229                            client_id, message_type, transaction_id_hex
230                        );
231
232                        // Determine event based on message type
233                        let event = match message_type.as_str() {
234                            "AllocateResponse" => {
235                                // Extract relay address from XOR-RELAYED-ADDRESS attribute
236                                if let Some(relay_addr) = Self::extract_xor_relayed_address(&data) {
237                                    let lifetime = Self::extract_lifetime(&data).unwrap_or(600);
238
239                                    // Store relay address
240                                    client_data_clone.lock().await.relay_address = Some(relay_addr);
241
242                                    // Also on the client record, so the relay this
243                                    // allocation actually got is visible to the dashboard,
244                                    // to MCP and to anything reading `protocol_data`. It
245                                    // used to live only in this task's private `ClientData`,
246                                    // where nothing outside the read loop could see it —
247                                    // which is also why no test could observe whether the
248                                    // response had been parsed at all.
249                                    app_state_clone
250                                        .with_client_mut(client_id, |client| {
251                                            client.set_protocol_field(
252                                                "relay_address".to_string(),
253                                                serde_json::json!(relay_addr.to_string()),
254                                            );
255                                        })
256                                        .await;
257
258                                    Some(Event::new(
259                                        &TURN_CLIENT_ALLOCATED_EVENT,
260                                        serde_json::json!({
261                                            "relay_address": relay_addr.to_string(),
262                                            "lifetime_seconds": lifetime,
263                                            "transaction_id": transaction_id_hex,
264                                        }),
265                                    ))
266                                } else {
267                                    None
268                                }
269                            }
270                            "RefreshResponse" => {
271                                let lifetime = Self::extract_lifetime(&data).unwrap_or(600);
272                                Some(Event::new(
273                                    &TURN_CLIENT_REFRESHED_EVENT,
274                                    serde_json::json!({
275                                        "lifetime_seconds": lifetime,
276                                    }),
277                                ))
278                            }
279                            "CreatePermissionResponse" => {
280                                // RFC 8656 section 9.4: the Success Response is empty, so
281                                // the peer is recovered from the transaction ID we recorded
282                                // when the request went out. This used to report the literal
283                                // string "unknown" for a field declared `required: true`.
284                                let peer_address = client_data_clone
285                                    .lock()
286                                    .await
287                                    .pending_permissions
288                                    .remove(&transaction_id_hex);
289
290                                match peer_address {
291                                    Some(peer) => Some(Event::new(
292                                        &TURN_CLIENT_PERMISSION_CREATED_EVENT,
293                                        serde_json::json!({
294                                            "peer_address": peer.to_string(),
295                                            "transaction_id": transaction_id_hex,
296                                        }),
297                                    )),
298                                    None => {
299                                        // Nothing we sent matches this transaction ID, so
300                                        // either it is a stray or a forgery. Raising the
301                                        // event with a made-up peer would tell the model a
302                                        // permission exists that it never asked for.
303                                        warn!(
304                                            "TURN client {} ignoring CreatePermission response \
305                                             for unknown transaction {}",
306                                            client_id, transaction_id_hex
307                                        );
308                                        None
309                                    }
310                                }
311                            }
312                            "DataIndication" => {
313                                // Extract peer address and data from DATA and XOR-PEER-ADDRESS
314                                if let (Some(peer_addr), Some(relay_data)) = (
315                                    Self::extract_xor_peer_address(&data),
316                                    Self::extract_data_attribute(&data),
317                                ) {
318                                    Some(Event::new(
319                                        &TURN_CLIENT_DATA_RECEIVED_EVENT,
320                                        serde_json::json!({
321                                            "peer_address": peer_addr.to_string(),
322                                            "data_hex": hex::encode(&relay_data),
323                                            "data_length": relay_data.len(),
324                                        }),
325                                    ))
326                                } else {
327                                    None
328                                }
329                            }
330                            "AllocateError" | "RefreshError" | "CreatePermissionError" => {
331                                let error_code = Self::extract_error_code(&data).unwrap_or(400);
332                                Log::new(Some(&status_clone)).error(format!(
333                                    "TURN client {} received error: {} (code: {})",
334                                    client_id, message_type, error_code
335                                ));
336                                None
337                            }
338                            _ => {
339                                debug!(
340                                    "TURN client {} ignoring message type: {}",
341                                    client_id, message_type
342                                );
343                                None
344                            }
345                        };
346
347                        if let Some(event) = event {
348                            // Handle event with LLM
349                            let mut client_data_lock = client_data_clone.lock().await;
350
351                            match client_data_lock.state {
352                                ConnectionState::Idle => {
353                                    // Process immediately
354                                    client_data_lock.state = ConnectionState::Processing;
355                                    drop(client_data_lock);
356
357                                    // Call LLM
358                                    if let Some(instruction) =
359                                        app_state_clone.get_instruction_for_client(client_id).await
360                                    {
361                                        // Copy the memory out and drop the guard BEFORE the
362                                        // call. `&client_data_clone.lock().await.memory` as a
363                                        // match scrutinee kept the guard alive for the whole
364                                        // round-trip *and* the whole arm, so the re-lock in
365                                        // the Ok arm below deadlocked this read loop against
366                                        // itself on a non-reentrant tokio Mutex. It never
367                                        // fired only because `memory_updates` is currently
368                                        // hardcoded `None` in `action_helper` — a latent hang
369                                        // waiting on an unrelated feature being finished.
370                                        let memory = client_data_clone.lock().await.memory.clone();
371                                        match call_llm_for_client(
372                                            &llm_clone,
373                                            &app_state_clone,
374                                            client_id.to_string(),
375                                            &instruction,
376                                            &memory,
377                                            Some(&event),
378                                            protocol_clone.as_ref(),
379                                            &status_clone,
380                                        )
381                                        .await
382                                        {
383                                            Ok(ClientLlmResult {
384                                                actions,
385                                                memory_updates,
386                                            }) => {
387                                                // Update memory
388                                                if let Some(mem) = memory_updates {
389                                                    client_data_clone.lock().await.memory = mem;
390                                                }
391
392                                                // Execute actions
393                                                for action in actions {
394                                                    use crate::llm::actions::client_trait::Client;
395                                                    match protocol_clone
396                                                        .as_ref()
397                                                        .execute_action(action)
398                                                    {
399                                                        Ok(action_result) => {
400                                                            match Self::handle_action_result(
401                                                                action_result,
402                                                                &socket_clone,
403                                                                remote_sock_addr,
404                                                                &client_data_clone,
405                                                                &status_clone,
406                                                                client_id,
407                                                            )
408                                                            .await
409                                                            {
410                                                                Ok(outcome) => debug!(
411                                                                    "TURN client {} action outcome: {:?}",
412                                                                    client_id, outcome
413                                                                ),
414                                                                Err(e) => error!("TURN client {} action execution failed: {}", client_id, e),
415                                                            }
416                                                        }
417                                                        Err(e) => {
418                                                            error!("TURN client {} action parsing failed: {}", client_id, e);
419                                                        }
420                                                    }
421                                                }
422                                            }
423                                            Err(e) => {
424                                                error!(
425                                                    "LLM error for TURN client {}: {}",
426                                                    client_id, e
427                                                );
428                                            }
429                                        }
430                                    }
431
432                                    // Process queued events if any
433                                    let mut client_data_lock = client_data_clone.lock().await;
434                                    if !client_data_lock.queued_events.is_empty() {
435                                        client_data_lock.queued_events.clear();
436                                    }
437                                    client_data_lock.state = ConnectionState::Idle;
438                                }
439                                ConnectionState::Processing => {
440                                    // Queue event
441                                    client_data_lock.queued_events.push(event);
442                                    client_data_lock.state = ConnectionState::Accumulating;
443                                }
444                                ConnectionState::Accumulating => {
445                                    // Continue queuing
446                                    client_data_lock.queued_events.push(event);
447                                }
448                            }
449                        }
450                    }
451                    Err(e) => {
452                        error!("TURN client {} read error: {}", client_id, e);
453                        app_state_clone
454                            .update_client_status(client_id, ClientStatus::Error(e.to_string()))
455                            .await;
456                        let _ = status_clone.send("__UPDATE_UI__".to_string());
457                        break;
458                    }
459                }
460            }
461            // Every exit path lands here: drop the command handle so the dashboard stops
462            // offering [ send ] on a dead client. This also closes the command channel,
463            // which ends `command_loop`.
464            app_state_clone.remove_client_handle(client_id).await;
465            let _ = status_clone.send("__UPDATE_UI__".to_string());
466        });
467        task_registrar
468            .register_client_task(client_id, task_handle)
469            .await;
470
471        Ok(local_addr)
472    }
473
474    /// Drain injected commands until the channel closes (client removed, or the read loop
475    /// exited and dropped the handle) or an injected `disconnect` ends the session.
476    ///
477    /// The generic `command_support::handle_stream_client_command` cannot run this client's
478    /// vocabulary - every TURN verb yields `ClientActionResult::Custom` and has to be encoded
479    /// as a STUN/TURN message - so the action goes through [`Self::handle_action_result`],
480    /// the same function the LLM path uses, and the outcome is recorded and replied exactly
481    /// the way the generic arm does it.
482    #[allow(clippy::too_many_arguments)]
483    async fn command_loop(
484        mut command_rx: mpsc::Receiver<ClientCommand>,
485        socket: Arc<UdpSocket>,
486        remote_addr: SocketAddr,
487        protocol: Arc<TurnClientProtocol>,
488        client_data: Arc<Mutex<ClientData>>,
489        client_id: ClientId,
490        app_state: Arc<AppState>,
491        status_tx: mpsc::UnboundedSender<String>,
492    ) {
493        use crate::llm::actions::client_trait::Client;
494        use crate::llm::actions::protocol_trait::Protocol;
495
496        while let Some(command) = command_rx.recv().await {
497            let action = command.action.clone();
498            let outcome = match protocol.as_ref().execute_action(action.clone()) {
499                Err(e) => Ok(ClientSendOutcome::Rejected {
500                    error: e.to_string(),
501                }),
502                Ok(action_result) => {
503                    Self::handle_action_result(
504                        action_result,
505                        &socket,
506                        remote_addr,
507                        &client_data,
508                        &status_tx,
509                        client_id,
510                    )
511                    .await
512                }
513            };
514
515            let outcome_json = match &outcome {
516                Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
517                Err(e) => serde_json::json!({"error": e.to_string()}),
518            };
519            app_state
520                .record_access_log(
521                    AccessLogOwner::Client(client_id.as_u32()),
522                    protocol.protocol_name(),
523                    None,
524                    "injected_action",
525                    action,
526                    vec![outcome_json],
527                )
528                .await;
529
530            let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
531            if let Err(e) = &outcome {
532                error!("TURN client {} injected action failed: {}", client_id, e);
533                let _ = status_tx.send(format!(
534                    "[WARN] Client {} injected action failed: {}",
535                    client_id, e
536                ));
537            }
538            let _ = status_tx.send("__UPDATE_UI__".to_string());
539            crate::client::command_support::reply(command, outcome);
540
541            if disconnect {
542                // The Refresh(lifetime=0) that deletes the allocation has already gone out
543                // (that is what `handle_action_result` does for Disconnect). TURN runs over
544                // an unconnected UDP socket, so there is nothing further to close: stop
545                // accepting commands, drop the handle and mark the client disconnected. The
546                // recv loop's socket is released when the client is removed (`stop_client`
547                // aborts both registered tasks).
548                app_state.remove_client_handle(client_id).await;
549                app_state
550                    .update_client_status(client_id, ClientStatus::Disconnected)
551                    .await;
552                let _ = status_tx.send("__UPDATE_UI__".to_string());
553                break;
554            }
555        }
556    }
557
558    /// Encode one executed action as a TURN message and put it on the wire.
559    ///
560    /// Shared by the LLM path and injected commands so the STUN/TURN encoding exists exactly
561    /// once. The returned [`ClientSendOutcome`] reports what actually reached the wire:
562    /// `Sent` carries the real datagram length, `Executed` means the result produced no TURN
563    /// message, and `Err` means the datagram could not be built or sent.
564    async fn handle_action_result(
565        action_result: crate::llm::actions::client_trait::ClientActionResult,
566        socket: &Arc<UdpSocket>,
567        remote_addr: SocketAddr,
568        client_data: &Arc<Mutex<ClientData>>,
569        status_tx: &mpsc::UnboundedSender<String>,
570        client_id: ClientId,
571    ) -> Result<ClientSendOutcome> {
572        use crate::llm::actions::client_trait::ClientActionResult;
573
574        match action_result {
575            ClientActionResult::Custom { name, data } => match name.as_str() {
576                "allocate" => {
577                    let lifetime = data
578                        .get("lifetime_seconds")
579                        .and_then(|v| v.as_u64())
580                        .unwrap_or(600);
581
582                    let message = Self::build_allocate_request(
583                        u32::try_from(lifetime).context("TURN lifetime exceeds u32")?,
584                    )?;
585                    let sent = socket.send_to(&message, remote_addr).await?;
586
587                    Log::new(Some(status_tx)).debug(format!(
588                        "TURN client {} sent Allocate request (lifetime: {}s)",
589                        client_id, lifetime
590                    ));
591                    Ok(ClientSendOutcome::Sent { bytes_sent: sent })
592                }
593                "create_permission" => {
594                    let peer_address = data
595                        .get("peer_address")
596                        .and_then(|v| v.as_str())
597                        .context("Missing peer_address")?;
598
599                    let peer_addr: SocketAddr =
600                        peer_address.parse().context("Invalid peer_address")?;
601
602                    let (message, transaction_id) =
603                        Self::build_create_permission_request(peer_addr)?;
604
605                    // Remember the peer before the datagram leaves, so a fast
606                    // response cannot arrive at the read loop first and find
607                    // nothing to correlate against.
608                    {
609                        let mut state = client_data.lock().await;
610                        if state.pending_permissions.len() >= MAX_PENDING_PERMISSIONS {
611                            state.pending_permissions.clear();
612                        }
613                        state.pending_permissions.insert(transaction_id, peer_addr);
614                    }
615
616                    let sent = socket.send_to(&message, remote_addr).await?;
617
618                    Log::new(Some(status_tx)).debug(format!(
619                        "TURN client {} sent CreatePermission for {}",
620                        client_id, peer_addr
621                    ));
622                    Ok(ClientSendOutcome::Sent { bytes_sent: sent })
623                }
624                "send_indication" => {
625                    let peer_address = data
626                        .get("peer_address")
627                        .and_then(|v| v.as_str())
628                        .context("Missing peer_address")?;
629
630                    let peer_addr: SocketAddr =
631                        peer_address.parse().context("Invalid peer_address")?;
632
633                    let send_data = data
634                        .get("data")
635                        .and_then(|v| v.as_array())
636                        .map(|arr| crate::client::wire_values::bytes(arr))
637                        .context("Missing or invalid data")??;
638
639                    let message = Self::build_send_indication(peer_addr, &send_data)?;
640                    let sent = socket.send_to(&message, remote_addr).await?;
641
642                    Log::new(Some(status_tx)).debug(format!(
643                        "TURN client {} sent {} bytes via SendIndication to {}",
644                        client_id,
645                        send_data.len(),
646                        peer_addr
647                    ));
648                    Ok(ClientSendOutcome::Sent { bytes_sent: sent })
649                }
650                "refresh" => {
651                    let lifetime = data
652                        .get("lifetime_seconds")
653                        .and_then(|v| v.as_u64())
654                        .unwrap_or(600);
655
656                    let message = Self::build_refresh_request(
657                        u32::try_from(lifetime).context("TURN lifetime exceeds u32")?,
658                    )?;
659                    let sent = socket.send_to(&message, remote_addr).await?;
660
661                    Log::new(Some(status_tx)).debug(format!(
662                        "TURN client {} sent Refresh request (lifetime: {}s)",
663                        client_id, lifetime
664                    ));
665                    Ok(ClientSendOutcome::Sent { bytes_sent: sent })
666                }
667                other => {
668                    debug!("TURN client {} unknown custom action: {}", client_id, other);
669                    Ok(ClientSendOutcome::Executed {
670                        detail: format!("custom result '{other}' builds no TURN message"),
671                    })
672                }
673            },
674            ClientActionResult::Disconnect => {
675                // Send Refresh with lifetime=0 to delete allocation
676                let message = Self::build_refresh_request(0)?;
677                socket.send_to(&message, remote_addr).await?;
678
679                Log::new(Some(status_tx)).info(format!(
680                    "TURN client {} disconnecting (sent Refresh with lifetime=0)",
681                    client_id
682                ));
683                Ok(ClientSendOutcome::Disconnected)
684            }
685            other => Ok(ClientSendOutcome::Executed {
686                detail: format!("{other:?} sends no TURN message"),
687            }),
688        }
689    }
690
691    /// Build TURN Allocate Request message
692    fn build_allocate_request(lifetime_seconds: u32) -> Result<Vec<u8>> {
693        let mut message = Vec::new();
694
695        // STUN message type: Allocate Request (0x0003)
696        message.extend_from_slice(&0x0003u16.to_be_bytes());
697
698        // Message length (will update later)
699        let length_pos = message.len();
700        message.extend_from_slice(&0u16.to_be_bytes());
701
702        // Magic cookie
703        message.extend_from_slice(&0x2112A442u32.to_be_bytes());
704
705        // Transaction ID (12 random bytes)
706        let transaction_id: Vec<u8> = (0..12).map(|_| rand::random::<u8>()).collect();
707        message.extend_from_slice(&transaction_id);
708
709        // LIFETIME attribute (0x000D)
710        let attr_type = 0x000Du16;
711        let attr_length = 4u16;
712        message.extend_from_slice(&attr_type.to_be_bytes());
713        message.extend_from_slice(&attr_length.to_be_bytes());
714        message.extend_from_slice(&lifetime_seconds.to_be_bytes());
715
716        // REQUESTED-TRANSPORT attribute (0x0019) - UDP (17)
717        let attr_type = 0x0019u16;
718        let attr_length = 4u16;
719        message.extend_from_slice(&attr_type.to_be_bytes());
720        message.extend_from_slice(&attr_length.to_be_bytes());
721        message.push(17); // UDP protocol number
722        message.extend_from_slice(&[0, 0, 0]); // Reserved
723
724        // Update message length
725        let total_length =
726            u16::try_from(message.len() - 20).context("TURN message exceeds u16 length")?;
727        message[length_pos..length_pos + 2].copy_from_slice(&total_length.to_be_bytes());
728
729        Ok(message)
730    }
731
732    /// Build TURN CreatePermission Request message.
733    ///
734    /// Returns the message and its transaction ID (hex), because the Success
735    /// Response is empty and correlating the reply back to a peer is only
736    /// possible if the caller keeps that ID.
737    fn build_create_permission_request(peer_addr: SocketAddr) -> Result<(Vec<u8>, String)> {
738        let mut message = Vec::new();
739
740        // STUN message type: CreatePermission Request (0x0008)
741        message.extend_from_slice(&0x0008u16.to_be_bytes());
742
743        // Message length (will update later)
744        let length_pos = message.len();
745        message.extend_from_slice(&0u16.to_be_bytes());
746
747        // Magic cookie
748        message.extend_from_slice(&0x2112A442u32.to_be_bytes());
749
750        // Transaction ID
751        let transaction_id: Vec<u8> = (0..12).map(|_| rand::random::<u8>()).collect();
752        message.extend_from_slice(&transaction_id);
753
754        // XOR-PEER-ADDRESS attribute (0x0012)
755        Self::add_xor_peer_address(&mut message, peer_addr, &transaction_id)?;
756
757        // Update message length
758        let total_length =
759            u16::try_from(message.len() - 20).context("TURN message exceeds u16 length")?;
760        message[length_pos..length_pos + 2].copy_from_slice(&total_length.to_be_bytes());
761
762        Ok((message, hex::encode(&transaction_id)))
763    }
764
765    /// Build TURN SendIndication message
766    fn build_send_indication(peer_addr: SocketAddr, data: &[u8]) -> Result<Vec<u8>> {
767        let mut message = Vec::new();
768
769        // STUN message type: SendIndication (0x0016)
770        message.extend_from_slice(&0x0016u16.to_be_bytes());
771
772        // Message length (will update later)
773        let length_pos = message.len();
774        message.extend_from_slice(&0u16.to_be_bytes());
775
776        // Magic cookie
777        message.extend_from_slice(&0x2112A442u32.to_be_bytes());
778
779        // Transaction ID
780        let transaction_id: Vec<u8> = (0..12).map(|_| rand::random::<u8>()).collect();
781        message.extend_from_slice(&transaction_id);
782
783        // XOR-PEER-ADDRESS attribute (0x0012)
784        Self::add_xor_peer_address(&mut message, peer_addr, &transaction_id)?;
785
786        // DATA attribute (0x0013)
787        let attr_type = 0x0013u16;
788        let attr_length = u16::try_from(data.len()).context("TURN data exceeds u16 length")?;
789        message.extend_from_slice(&attr_type.to_be_bytes());
790        message.extend_from_slice(&attr_length.to_be_bytes());
791        message.extend_from_slice(data);
792
793        // Add padding if needed (attributes must be 4-byte aligned)
794        let padding = (4 - (data.len() % 4)) % 4;
795        message.extend_from_slice(&vec![0u8; padding]);
796
797        // Update message length
798        let total_length =
799            u16::try_from(message.len() - 20).context("TURN message exceeds u16 length")?;
800        message[length_pos..length_pos + 2].copy_from_slice(&total_length.to_be_bytes());
801
802        Ok(message)
803    }
804
805    /// Build TURN Refresh Request message
806    fn build_refresh_request(lifetime_seconds: u32) -> Result<Vec<u8>> {
807        let mut message = Vec::new();
808
809        // STUN message type: Refresh Request (0x0004)
810        message.extend_from_slice(&0x0004u16.to_be_bytes());
811
812        // Message length (will update later)
813        let length_pos = message.len();
814        message.extend_from_slice(&0u16.to_be_bytes());
815
816        // Magic cookie
817        message.extend_from_slice(&0x2112A442u32.to_be_bytes());
818
819        // Transaction ID
820        let transaction_id: Vec<u8> = (0..12).map(|_| rand::random::<u8>()).collect();
821        message.extend_from_slice(&transaction_id);
822
823        // LIFETIME attribute (0x000D)
824        let attr_type = 0x000Du16;
825        let attr_length = 4u16;
826        message.extend_from_slice(&attr_type.to_be_bytes());
827        message.extend_from_slice(&attr_length.to_be_bytes());
828        message.extend_from_slice(&lifetime_seconds.to_be_bytes());
829
830        // Update message length
831        let total_length =
832            u16::try_from(message.len() - 20).context("TURN message exceeds u16 length")?;
833        message[length_pos..length_pos + 2].copy_from_slice(&total_length.to_be_bytes());
834
835        Ok(message)
836    }
837
838    /// Add XOR-PEER-ADDRESS attribute to message
839    fn add_xor_peer_address(
840        message: &mut Vec<u8>,
841        peer_addr: SocketAddr,
842        transaction_id: &[u8],
843    ) -> Result<()> {
844        let attr_type = 0x0012u16;
845
846        match peer_addr {
847            SocketAddr::V4(addr) => {
848                let attr_length = 8u16; // Family (2) + Port (2) + IPv4 (4)
849                message.extend_from_slice(&attr_type.to_be_bytes());
850                message.extend_from_slice(&attr_length.to_be_bytes());
851
852                // Reserved byte + Family (0x01 for IPv4)
853                message.push(0x00);
854                message.push(0x01);
855
856                // X-Port (port XOR'd with most significant 16 bits of magic cookie)
857                let port = addr.port();
858                let xor_port = port ^ 0x2112;
859                message.extend_from_slice(&xor_port.to_be_bytes());
860
861                // X-Address (IP XOR'd with magic cookie)
862                let ip_bytes = addr.ip().octets();
863                let magic_cookie = 0x2112A442u32.to_be_bytes();
864                for i in 0..4 {
865                    message.push(ip_bytes[i] ^ magic_cookie[i]);
866                }
867            }
868            SocketAddr::V6(addr) => {
869                let attr_length = 20u16; // Family (2) + Port (2) + IPv6 (16)
870                message.extend_from_slice(&attr_type.to_be_bytes());
871                message.extend_from_slice(&attr_length.to_be_bytes());
872
873                // Reserved byte + Family (0x02 for IPv6)
874                message.push(0x00);
875                message.push(0x02);
876
877                // X-Port
878                let port = addr.port();
879                let xor_port = port ^ 0x2112;
880                message.extend_from_slice(&xor_port.to_be_bytes());
881
882                // X-Address (IPv6 XOR'd with magic cookie + transaction ID)
883                let ip_bytes = addr.ip().octets();
884                let magic_cookie = 0x2112A442u32.to_be_bytes();
885
886                for i in 0..4 {
887                    message.push(ip_bytes[i] ^ magic_cookie[i]);
888                }
889                for i in 4..16 {
890                    message.push(ip_bytes[i] ^ transaction_id[i - 4]);
891                }
892            }
893        }
894
895        Ok(())
896    }
897
898    /// Parse TURN/STUN message header (similar to server implementation)
899    fn parse_turn_header(data: &[u8]) -> (Option<Vec<u8>>, String, bool) {
900        if data.len() < 20 {
901            return (None, "invalid".to_string(), false);
902        }
903
904        // Check magic cookie
905        let magic_cookie = u32::from_be_bytes([data[4], data[5], data[6], data[7]]);
906        if magic_cookie != 0x2112A442 {
907            return (None, "invalid".to_string(), false);
908        }
909
910        // Extract message type
911        let message_type_raw = u16::from_be_bytes([data[0], data[1]]);
912
913        // RFC 8489 section 5: the 14-bit type interleaves method and class as
914        //   0b00 M11 M10 M9 M8 M7 C1 M6 M5 M4 C0 M3 M2 M1 M0
915        // so C0 is bit 4, C1 is bit 8, and class == C1<<1 | C0.
916        //
917        // This used to read `((raw & 0x0110) >> 4) | ((raw & 0x0100) >> 7)`, which
918        // shifts bit 8 by 4 rather than 8 and works out to C0 + 18*C1. Every class
919        // with C1 set therefore decoded to 18 or 19 and fell through the table
920        // below to "Unknown". Since a *client* receives nothing but responses and
921        // indications, that meant this client could not parse a single reply: an
922        // Allocate success (0x0103) decoded to 18, `relay_address` was never
923        // stored, and turn_allocated / turn_refreshed / turn_permission_created /
924        // turn_data_received could not fire however correct the server was. The
925        // sibling server has this right (`src/server/turn/mod.rs`); the two
926        // disagreed, and the client was the wrong one.
927        //
928        // The table below was wrong to match: it expected class 1 for a success
929        // response and 2 for an error, which are the *indication* and *success*
930        // values. Both are corrected together, because fixing either alone leaves
931        // the client just as deaf.
932        let c0 = (message_type_raw >> 4) & 0x1;
933        let c1 = (message_type_raw >> 8) & 0x1;
934        let class = (c1 << 1) | c0;
935        let method = (message_type_raw & 0x000F)
936            | ((message_type_raw & 0x00E0) >> 1)
937            | ((message_type_raw & 0x3E00) >> 2);
938
939        // Class: 0 = request, 1 = indication, 2 = success response, 3 = error.
940        let message_type = match (class, method) {
941            (0, 3) => "AllocateRequest",
942            (2, 3) => "AllocateResponse",
943            (3, 3) => "AllocateError",
944            (0, 4) => "RefreshRequest",
945            (2, 4) => "RefreshResponse",
946            (3, 4) => "RefreshError",
947            (0, 8) => "CreatePermissionRequest",
948            (2, 8) => "CreatePermissionResponse",
949            (3, 8) => "CreatePermissionError",
950            (1, 6) => "SendIndication",
951            (1, 7) => "DataIndication",
952            _ => "Unknown",
953        };
954
955        let transaction_id = data[8..20].to_vec();
956
957        (Some(transaction_id), message_type.to_string(), true)
958    }
959
960    /// Walk the attributes of a STUN/TURN message, bounded by what actually arrived.
961    ///
962    /// **This is the only place attribute offsets are computed.** Four hand-rolled
963    /// copies of this walk used to exist — one each in `extract_xor_address`,
964    /// `extract_lifetime`, `extract_data_attribute` and `extract_error_code` — and
965    /// every one of them bounded the cursor against `20 + message_length`, a
966    /// 16-bit field the sender chooses, while indexing a buffer capped at 2048
967    /// bytes. Twenty bytes on the wire —
968    /// `00 07 FF FF 21 12 A4 42 <12-byte transaction id>` — index `data[20]` on a
969    /// 20-byte slice and **panic**. `tokio::spawn` swallows that panic, so the
970    /// read loop died silently while `AppState` went on reporting the client
971    /// `Connected` and the dashboard went on offering `[ send ]`: permanently deaf,
972    /// with nothing in the UI to say so. The socket is bound to `0.0.0.0:0` and the
973    /// loop never checks that a datagram came from the TURN server, so any host
974    /// that could reach the ephemeral port could send it.
975    ///
976    /// The rule, which the sibling server already followed, is to trust the
977    /// shorter of the declared length and what arrived.
978    fn attributes(data: &[u8]) -> Vec<(u16, &[u8])> {
979        if data.len() < 20 {
980            return Vec::new();
981        }
982
983        let declared = u16::from_be_bytes([data[2], data[3]]) as usize;
984        let end = 20usize.saturating_add(declared).min(data.len());
985
986        let mut attributes = Vec::new();
987        let mut pos = 20usize;
988        while pos + 4 <= end {
989            let attr_type = u16::from_be_bytes([data[pos], data[pos + 1]]);
990            let attr_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
991            let value_start = pos + 4;
992            let value_end = match value_start.checked_add(attr_len) {
993                Some(v) if v <= end => v,
994                // Truncated or lying length: stop, keep what we parsed.
995                _ => break,
996            };
997            attributes.push((attr_type, &data[value_start..value_end]));
998
999            // Attributes are padded to a 4-byte boundary. A zero-length attribute
1000            // still advances by the 4-byte header, so this cannot spin.
1001            let padded = attr_len.saturating_add(3) & !3usize;
1002            pos = match value_start.checked_add(padded) {
1003                Some(p) => p,
1004                None => break,
1005            };
1006        }
1007        attributes
1008    }
1009
1010    /// First attribute of `attr_type`, if present.
1011    fn attribute<'a>(data: &'a [u8], attr_type: u16) -> Option<&'a [u8]> {
1012        Self::attributes(data)
1013            .into_iter()
1014            .find(|(t, _)| *t == attr_type)
1015            .map(|(_, v)| v)
1016    }
1017
1018    /// Extract XOR-RELAYED-ADDRESS attribute from TURN message
1019    fn extract_xor_relayed_address(data: &[u8]) -> Option<SocketAddr> {
1020        Self::extract_xor_address(data, 0x0016) // XOR-RELAYED-ADDRESS = 0x0016
1021    }
1022
1023    /// Extract XOR-PEER-ADDRESS attribute from TURN message
1024    fn extract_xor_peer_address(data: &[u8]) -> Option<SocketAddr> {
1025        Self::extract_xor_address(data, 0x0012) // XOR-PEER-ADDRESS = 0x0012
1026    }
1027
1028    /// Extract XOR'd address attribute from TURN message
1029    fn extract_xor_address(data: &[u8], attr_type: u16) -> Option<SocketAddr> {
1030        let attr_data = Self::attribute(data, attr_type)?;
1031        if attr_data.len() < 4 {
1032            return None;
1033        }
1034
1035        let family = attr_data[1];
1036        let xor_port = u16::from_be_bytes([attr_data[2], attr_data[3]]);
1037        let port = xor_port ^ 0x2112;
1038
1039        match family {
1040            0x01 => {
1041                // IPv4
1042                if attr_data.len() < 8 {
1043                    return None;
1044                }
1045
1046                let magic_cookie = 0x2112A442u32.to_be_bytes();
1047                let xor_ip = &attr_data[4..8];
1048                let ip_bytes = [
1049                    xor_ip[0] ^ magic_cookie[0],
1050                    xor_ip[1] ^ magic_cookie[1],
1051                    xor_ip[2] ^ magic_cookie[2],
1052                    xor_ip[3] ^ magic_cookie[3],
1053                ];
1054
1055                let ip = std::net::Ipv4Addr::from(ip_bytes);
1056                Some(SocketAddr::from((ip, port)))
1057            }
1058            0x02 => {
1059                // IPv6
1060                if attr_data.len() < 20 {
1061                    return None;
1062                }
1063
1064                let magic_cookie = 0x2112A442u32.to_be_bytes();
1065                let transaction_id = &data[8..20];
1066                let xor_ip = &attr_data[4..20];
1067
1068                let mut ip_bytes = [0u8; 16];
1069                for i in 0..4 {
1070                    ip_bytes[i] = xor_ip[i] ^ magic_cookie[i];
1071                }
1072                for i in 4..16 {
1073                    ip_bytes[i] = xor_ip[i] ^ transaction_id[i - 4];
1074                }
1075
1076                let ip = std::net::Ipv6Addr::from(ip_bytes);
1077                Some(SocketAddr::from((ip, port)))
1078            }
1079            _ => None,
1080        }
1081    }
1082
1083    /// Extract LIFETIME attribute from TURN message
1084    fn extract_lifetime(data: &[u8]) -> Option<u32> {
1085        let value = Self::attribute(data, 0x000D)?;
1086        if value.len() < 4 {
1087            return None;
1088        }
1089        Some(u32::from_be_bytes([value[0], value[1], value[2], value[3]]))
1090    }
1091
1092    /// Extract DATA attribute from TURN message
1093    fn extract_data_attribute(data: &[u8]) -> Option<Vec<u8>> {
1094        Self::attribute(data, 0x0013).map(|v| v.to_vec())
1095    }
1096
1097    /// Extract ERROR-CODE attribute from TURN message
1098    ///
1099    /// RFC 8489 section 14.8: two reserved bytes, then a 3-bit class and a
1100    /// number 0-99.
1101    fn extract_error_code(data: &[u8]) -> Option<u16> {
1102        let value = Self::attribute(data, 0x0009)?;
1103        if value.len() < 4 {
1104            return None;
1105        }
1106        let class = (value[2] & 0x07) as u16;
1107        let number = value[3] as u16;
1108        Some(class * 100 + number)
1109    }
1110}