Skip to main content

netget/server/turn/
mod.rs

1//! TURN relay server (RFC 8656)
2//!
3//! Split of responsibilities:
4//!
5//! * **Rust owns the transport and the relay.** Every allocation binds a real
6//!   UDP socket; peer traffic arriving on it is forwarded to the client as a
7//!   Data indication (or a ChannelData frame when a channel is bound), and Send
8//!   indications / ChannelData frames from the client are forwarded to the peer
9//!   from that same socket. The data plane never calls the LLM: one model
10//!   round-trip per relayed packet would be absurd, and permission policy has
11//!   already been decided on the control plane.
12//! * **The LLM owns policy.** Allocate, Refresh, CreatePermission and ChannelBind
13//!   each raise an event; the model (or a script/static handler) decides whether
14//!   to grant, for how long, and for which peers. Nothing is granted by default:
15//!   a missing or unusable answer leaves the reserved socket unused and the
16//!   client with no allocation.
17pub mod actions;
18
19use crate::server::connection::ConnectionId;
20use anyhow::{Context, Result};
21use std::collections::HashMap;
22use std::net::{IpAddr, SocketAddr};
23use std::sync::atomic::{AtomicUsize, Ordering};
24use std::sync::Arc;
25use tokio::net::UdpSocket;
26use tokio::sync::{mpsc, Mutex};
27use tokio::task::JoinHandle;
28use tokio::time::{Duration, Instant};
29use tracing::{debug, error, info, trace, warn};
30
31use crate::llm::action_helper::call_llm;
32use crate::llm::actions::executor::ExecutionResult;
33use crate::llm::ollama_client::OllamaClient;
34use crate::logging::emit::Log;
35use crate::protocol::{Event, EventType};
36use crate::server::TurnProtocol;
37use crate::state::app_state::AppState;
38use crate::state::ServerId;
39use actions::{
40    TURN_ALLOCATE_REQUEST_EVENT, TURN_CHANNEL_BIND_REQUEST_EVENT,
41    TURN_CREATE_PERMISSION_REQUEST_EVENT, TURN_REFRESH_REQUEST_EVENT,
42};
43
44/// STUN magic cookie (RFC 8489 section 5)
45const MAGIC_COOKIE: u32 = 0x2112_A442;
46
47/// Largest datagram accepted on either the TURN socket or a relay socket.
48const RELAY_MTU: usize = 2048;
49
50/// Hard cap on concurrent allocations. TURN is an amplifier and an open relay
51/// by nature; a request that would exceed this is refused with 508 without
52/// consulting the model, because resource exhaustion is not a policy question.
53const MAX_ALLOCATIONS: usize = 256;
54
55/// Permission lifetime (RFC 8656 section 9: fixed at 5 minutes).
56const PERMISSION_LIFETIME: Duration = Duration::from_secs(300);
57
58/// Most peer addresses one allocation may hold permissions for at once.
59///
60/// The permission map is keyed by an IP address **the client chooses**, and expiry was
61/// filter-on-read: `is_permitted` and `permitted_ips` skipped a stale entry, and nothing ever
62/// removed one. So the map only ever grew, and over IPv6 a single client has 2^128 distinct
63/// keys to grow it with — the same shape as the OSPF neighbour map, where the key was a Router
64/// ID the sender picked and a peer spraying Hellos grew the table until the server stopped.
65///
66/// The repair is the same two-part one OSPF took, and the order matters: **expiry is enforced
67/// on write**, so the map holds only permissions that are actually live, and *then* a hard cap
68/// bounds how many can be live at once. Ageing alone is not a bound here the way it is for
69/// OSPF — one CreatePermission may name many peers (RFC 8656 section 9.1 allows repeated
70/// XOR-PEER-ADDRESS attributes), so five minutes is long enough to ask for a great many.
71///
72/// 256 is the same order as [`MAX_ALLOCATIONS`] and far above any real client: a WebRTC peer
73/// creates one permission per remote ICE candidate, which is tens at the outside.
74const MAX_PERMISSIONS: usize = 256;
75
76/// Most channel bindings one allocation may hold at once.
77///
78/// RFC 8656 section 11 restricts channel numbers to 0x4000–0x7FFF, so the number space already
79/// bounds this at 16384 — but 16384 live entries per allocation across [`MAX_ALLOCATIONS`]
80/// allocations is not a bound anybody chose. As with permissions, expiry is enforced on write
81/// so the map holds only live bindings, and the cap says what "many" means.
82const MAX_CHANNELS: usize = 256;
83
84/// Channel binding lifetime (RFC 8656 section 11: 10 minutes).
85const CHANNEL_LIFETIME: Duration = Duration::from_secs(600);
86
87/// Longest allocation lifetime we will grant, whatever the model asks for.
88const MAX_LIFETIME_SECONDS: u32 = 3600;
89
90// STUN/TURN attribute types used here.
91const ATTR_CHANNEL_NUMBER: u16 = 0x000C;
92const ATTR_LIFETIME: u16 = 0x000D;
93const ATTR_XOR_PEER_ADDRESS: u16 = 0x0012;
94const ATTR_DATA: u16 = 0x0013;
95const ATTR_REQUESTED_TRANSPORT: u16 = 0x0019;
96
97/// Which peer addresses this relay may be pointed at.
98///
99/// **TURN is a relay, so its destination set is its blast radius**, and until now
100/// there was no destination set: whatever peer a client named and the policy
101/// permitted, the relay socket sent to. `127.0.0.1:8080`, `10.0.0.5:161`,
102/// `169.254.169.254` — a UDP path from anyone who can reach the TURN port into
103/// whatever the operator's host can reach, with the answers relayed back.
104///
105/// The default stays `Any`, deliberately and not by oversight. This protocol
106/// exists to be pointed at by things under test, its own e2e tests relay between
107/// two loopback sockets, and a honeypot TURN server that refuses private
108/// destinations is not a TURN server. But "deliberate" is only true if it is said
109/// out loud and there is a lever, which is what `Public` is.
110#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
111pub enum PeerScope {
112    /// No restriction. Loopback and private destinations are relayed.
113    #[default]
114    Any,
115    /// Refuse loopback, private, link-local, multicast, broadcast, unspecified
116    /// and documentation-range destinations.
117    Public,
118}
119
120impl PeerScope {
121    /// Parse the `peer_scope` startup parameter. `None` means the default.
122    pub fn parse(value: Option<&str>) -> Result<Self> {
123        match value.map(str::trim) {
124            None | Some("") | Some("any") => Ok(Self::Any),
125            Some("public") => Ok(Self::Public),
126            Some(other) => Err(anyhow::anyhow!(
127                "Invalid peer_scope '{other}': expected \"any\" or \"public\""
128            )),
129        }
130    }
131
132    /// True if traffic may be relayed to `ip`.
133    pub fn allows(&self, ip: IpAddr) -> bool {
134        match self {
135            Self::Any => true,
136            Self::Public => match ip {
137                IpAddr::V4(v4) => {
138                    !(v4.is_loopback()
139                        || v4.is_private()
140                        || v4.is_link_local()
141                        || v4.is_multicast()
142                        || v4.is_broadcast()
143                        || v4.is_unspecified()
144                        || v4.is_documentation()
145                        // 100.64.0.0/10, carrier-grade NAT. `is_shared` is
146                        // unstable, so it is spelled out.
147                        || (v4.octets()[0] == 100 && (64..128).contains(&v4.octets()[1])))
148                }
149                IpAddr::V6(v6) => {
150                    !(v6.is_loopback()
151                        || v6.is_multicast()
152                        || v6.is_unspecified()
153                        // Unique local (fc00::/7) and link-local (fe80::/10).
154                        // `is_unique_local` and `is_unicast_link_local` are
155                        // unstable, so they are spelled out.
156                        || (v6.segments()[0] & 0xfe00) == 0xfc00
157                        || (v6.segments()[0] & 0xffc0) == 0xfe80
158                        // An IPv4-mapped address would otherwise bypass every
159                        // IPv4 rule above.
160                        || v6.to_ipv4_mapped().is_some_and(|v4| {
161                            !PeerScope::Public.allows(IpAddr::V4(v4))
162                        }))
163                }
164            },
165        }
166    }
167
168    fn as_str(&self) -> &'static str {
169        match self {
170            Self::Any => "any",
171            Self::Public => "public",
172        }
173    }
174}
175
176/// Everything about an allocation that both the request path and the relay task
177/// need: when it dies, which peers it may talk to, and its channel bindings.
178///
179/// The expiry lives here rather than beside the socket so the relay task can
180/// enforce it per packet. Keeping it only in the allocation table meant a dead
181/// allocation kept relaying until the 30-second cleanup tick noticed.
182pub struct AllocationState {
183    expires_at: Instant,
184    lifetime_seconds: u32,
185    /// Permitted peer IPs and their expiry. RFC 8656 matches permissions on the
186    /// IP address only, deliberately ignoring the port.
187    permissions: HashMap<IpAddr, Instant>,
188    /// Channel number -> (peer, expiry)
189    channels: HashMap<u16, (SocketAddr, Instant)>,
190}
191
192impl AllocationState {
193    fn new(lifetime_seconds: u32) -> Self {
194        Self {
195            expires_at: Instant::now() + Duration::from_secs(lifetime_seconds as u64),
196            lifetime_seconds,
197            permissions: HashMap::new(),
198            channels: HashMap::new(),
199        }
200    }
201
202    fn is_live(&self) -> bool {
203        self.expires_at > Instant::now()
204    }
205
206    fn refresh(&mut self, lifetime_seconds: u32) {
207        self.expires_at = Instant::now() + Duration::from_secs(lifetime_seconds as u64);
208        self.lifetime_seconds = lifetime_seconds;
209    }
210
211    /// Grant a permission, ageing out expired ones first.
212    ///
213    /// Returns `false` and changes nothing when the allocation is already at
214    /// [`MAX_PERMISSIONS`] live permissions. The prune is what makes the cap a cap rather than
215    /// a permanent ceiling: without it, a client that used up its 256 over an hour would be
216    /// refused forever even though none of them was still valid.
217    fn permit(&mut self, ip: IpAddr) -> bool {
218        let now = Instant::now();
219        self.permissions.retain(|_, expiry| *expiry > now);
220        if !self.permissions.contains_key(&ip) && self.permissions.len() >= MAX_PERMISSIONS {
221            return false;
222        }
223        self.permissions.insert(ip, now + PERMISSION_LIFETIME);
224        true
225    }
226
227    /// How many permissions are live right now, expired ones removed.
228    ///
229    /// Takes `&mut self` deliberately: asking the question is the moment to drop what has
230    /// expired, so a caller that checks capacity can never be told "full" on the strength of
231    /// entries that are already dead.
232    fn live_permissions(&mut self) -> usize {
233        let now = Instant::now();
234        self.permissions.retain(|_, expiry| *expiry > now);
235        self.permissions.len()
236    }
237
238    /// How many further peer addresses would fit inside [`MAX_PERMISSIONS`].
239    fn permission_headroom(&mut self) -> usize {
240        MAX_PERMISSIONS.saturating_sub(self.live_permissions())
241    }
242
243    /// How many channel bindings are live right now, expired ones removed.
244    fn live_channels(&mut self) -> usize {
245        let now = Instant::now();
246        self.channels.retain(|_, (_, expiry)| *expiry > now);
247        self.channels.len()
248    }
249
250    fn is_permitted(&self, ip: &IpAddr) -> bool {
251        self.permissions
252            .get(ip)
253            .is_some_and(|expiry| *expiry > Instant::now())
254    }
255
256    /// Bind a channel, ageing out expired bindings first.
257    ///
258    /// Returns `false` and changes nothing when the allocation is at [`MAX_CHANNELS`] live
259    /// bindings, or when the implied permission would not fit. A binding without its
260    /// permission would relay nothing, so the two succeed or fail together.
261    fn bind_channel(&mut self, number: u16, peer: SocketAddr) -> bool {
262        let now = Instant::now();
263        self.channels.retain(|_, (_, expiry)| *expiry > now);
264        if !self.channels.contains_key(&number) && self.channels.len() >= MAX_CHANNELS {
265            return false;
266        }
267        if !self.permit(peer.ip()) {
268            return false;
269        }
270        self.channels.insert(number, (peer, now + CHANNEL_LIFETIME));
271        true
272    }
273
274    /// Peer bound to `number`, if the binding has not expired.
275    fn channel_peer(&self, number: u16) -> Option<SocketAddr> {
276        self.channels
277            .get(&number)
278            .filter(|(_, expiry)| *expiry > Instant::now())
279            .map(|(peer, _)| *peer)
280    }
281
282    /// Channel bound to `peer`, if any binding has not expired.
283    fn channel_for_peer(&self, peer: &SocketAddr) -> Option<u16> {
284        let now = Instant::now();
285        self.channels
286            .iter()
287            .find(|(_, (bound, expiry))| bound == peer && *expiry > now)
288            .map(|(number, _)| *number)
289    }
290
291    fn permitted_ips(&self) -> Vec<String> {
292        let now = Instant::now();
293        let mut ips: Vec<String> = self
294            .permissions
295            .iter()
296            .filter(|(_, expiry)| **expiry > now)
297            .map(|(ip, _)| ip.to_string())
298            .collect();
299        ips.sort();
300        ips
301    }
302
303    fn bound_channels(&self) -> Vec<serde_json::Value> {
304        let now = Instant::now();
305        let mut bound: Vec<(u16, SocketAddr)> = self
306            .channels
307            .iter()
308            .filter(|(_, (_, expiry))| *expiry > now)
309            .map(|(number, (peer, _))| (*number, *peer))
310            .collect();
311        bound.sort_by_key(|(number, _)| *number);
312        bound
313            .into_iter()
314            .map(|(number, peer)| {
315                serde_json::json!({ "channel_number": number, "peer_address": peer.to_string() })
316            })
317            .collect()
318    }
319}
320
321/// A live TURN allocation: a bound relay socket plus the policy attached to it.
322pub struct TurnAllocation {
323    client_addr: SocketAddr,
324    relay_addr: SocketAddr,
325    relay_socket: Arc<UdpSocket>,
326    #[allow(dead_code)]
327    allocated_at: Instant,
328    state: Arc<Mutex<AllocationState>>,
329    relay_task: JoinHandle<()>,
330}
331
332impl Drop for TurnAllocation {
333    fn drop(&mut self) {
334        // Dropping a JoinHandle detaches the task rather than stopping it, so
335        // without this an expired (or replaced, or server-stopped) allocation
336        // would keep a relay socket bound and keep forwarding peer traffic.
337        self.relay_task.abort();
338    }
339}
340
341/// TURN server: allocation table plus the accept loop.
342pub struct TurnServer {
343    allocations: Arc<Mutex<HashMap<String, TurnAllocation>>>, // Key: allocation_id
344    /// Relay sockets bound for Allocate requests whose policy decision is still
345    /// in flight. Counted towards `MAX_ALLOCATIONS` so a flood of requests
346    /// cannot exhaust file descriptors while the model thinks.
347    reservations: Arc<AtomicUsize>,
348}
349
350/// Decrements the in-flight reservation count however the request ends.
351struct ReservationGuard(Arc<AtomicUsize>);
352
353impl Drop for ReservationGuard {
354    fn drop(&mut self) {
355        self.0.fetch_sub(1, Ordering::Relaxed);
356    }
357}
358
359impl Default for TurnServer {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365/// Everything the per-datagram task needs. Passed as one struct because the
366/// alternative is a twelve-argument function.
367#[derive(Clone)]
368struct TurnContext {
369    server: Arc<TurnServer>,
370    socket: Arc<UdpSocket>,
371    local_addr: SocketAddr,
372    /// IP the relay sockets are bound to.
373    relay_bind_ip: IpAddr,
374    /// IP reported to clients in XOR-RELAYED-ADDRESS. Differs from
375    /// `relay_bind_ip` when the server sits behind NAT (`relay_ip` parameter).
376    advertised_relay_ip: IpAddr,
377    /// Which peer addresses may be relayed to (`peer_scope` parameter).
378    peer_scope: PeerScope,
379    llm_client: OllamaClient,
380    state: Arc<AppState>,
381    status_tx: mpsc::UnboundedSender<String>,
382    protocol: Arc<TurnProtocol>,
383    server_id: ServerId,
384}
385
386impl TurnServer {
387    pub fn new() -> Self {
388        Self {
389            allocations: Arc::new(Mutex::new(HashMap::new())),
390            reservations: Arc::new(AtomicUsize::new(0)),
391        }
392    }
393
394    /// Spawn TURN server with integrated LLM actions
395    pub async fn spawn_with_llm_actions(
396        listen_addr: SocketAddr,
397        llm_client: OllamaClient,
398        app_state: Arc<AppState>,
399        status_tx: mpsc::UnboundedSender<String>,
400        server_id: ServerId,
401        relay_ip: Option<String>,
402        peer_scope: PeerScope,
403    ) -> Result<SocketAddr> {
404        let advertised_relay_ip = match relay_ip {
405            Some(ip) => Some(
406                ip.parse::<IpAddr>()
407                    .with_context(|| format!("Invalid relay_ip '{ip}': expected an IP address"))?,
408            ),
409            None => None,
410        };
411
412        let socket = Arc::new(UdpSocket::bind(listen_addr).await?);
413        let local_addr = socket.local_addr()?;
414        Log::new(Some(&status_tx)).info(format!("TURN server listening on {}", local_addr));
415
416        let relay_bind_ip = local_addr.ip();
417        let advertised_relay_ip = advertised_relay_ip.unwrap_or(relay_bind_ip);
418        if advertised_relay_ip.is_unspecified() {
419            Log::new(Some(&status_tx)).warn(format!(
420                "TURN relay address will be advertised as {} because the server is bound to a \
421                 wildcard address; set the 'relay_ip' startup parameter to the address clients \
422                 should send relayed traffic to",
423                advertised_relay_ip
424            ));
425        }
426
427        let server = Arc::new(Self::new());
428        Self::spawn_cleanup_task(&server, status_tx.clone(), app_state.clone(), server_id).await;
429
430        match peer_scope {
431            PeerScope::Any => Log::new(Some(&status_tx)).warn(
432                "TURN peer_scope=any: this relay will forward to ANY address a permitted \
433                 client names, loopback and RFC 1918 included, which is a UDP path into \
434                 whatever this host can reach. Correct for a honeypot or a local test; set \
435                 peer_scope=public anywhere else",
436            ),
437            PeerScope::Public => Log::new(Some(&status_tx)).info(
438                "TURN peer_scope=public: loopback, private, link-local, multicast and \
439                 broadcast destinations will be refused",
440            ),
441        }
442
443        let ctx = TurnContext {
444            server,
445            socket: socket.clone(),
446            local_addr,
447            relay_bind_ip,
448            advertised_relay_ip,
449            peer_scope,
450            llm_client,
451            state: app_state.clone(),
452            status_tx: status_tx.clone(),
453            protocol: Arc::new(TurnProtocol::new()),
454            server_id,
455        };
456
457        let task_registrar = app_state.clone();
458        let accept_handle = tokio::spawn(async move {
459            let mut buffer = vec![0u8; RELAY_MTU];
460
461            loop {
462                match socket.recv_from(&mut buffer).await {
463                    Ok((n, peer_addr)) => {
464                        let data = buffer[..n].to_vec();
465
466                        if n == buffer.len() {
467                            warn!(
468                                "TURN datagram from {} filled the {}-byte receive buffer and may \
469                                 have been truncated",
470                                peer_addr, RELAY_MTU
471                            );
472                        }
473
474                        Self::register_connection(&ctx, peer_addr, n).await;
475
476                        Log::new(Some(&ctx.status_tx))
477                            .debug(format!("TURN received {} bytes from {}", n, peer_addr));
478                        trace!("TURN data (hex): {}", hex::encode(&data));
479
480                        let ctx = ctx.clone();
481                        // Tracked, not detached: stop_server must abort this task too.
482                        let task_owner = app_state.clone();
483                        task_owner
484                            .spawn_server_task(server_id, async move {
485                                handle_datagram(ctx, data, peer_addr).await;
486                            })
487                            .await;
488                    }
489                    Err(e) => {
490                        Log::new(Some(&status_tx)).error(format!("TURN receive error: {}", e));
491                        break;
492                    }
493                }
494            }
495        });
496
497        task_registrar
498            .register_server_task(server_id, accept_handle)
499            .await;
500
501        Ok(local_addr)
502    }
503
504    async fn register_connection(ctx: &TurnContext, peer_addr: SocketAddr, bytes: usize) {
505        use crate::state::server::{
506            ConnectionState as ServerConnectionState, ConnectionStatus, ProtocolConnectionInfo,
507        };
508
509        let connection_id = ConnectionId::new(ctx.state.get_next_unified_id().await);
510        let now = crate::utils::clock::Instant::now();
511        let conn_state = ServerConnectionState {
512            id: connection_id,
513            remote_addr: peer_addr,
514            local_addr: ctx.local_addr,
515            bytes_sent: 0,
516            bytes_received: bytes as u64,
517            packets_sent: 0,
518            packets_received: 1,
519            last_activity: now,
520            status: ConnectionStatus::Active,
521            status_changed_at: now,
522            protocol_info: ProtocolConnectionInfo::empty(),
523        };
524        ctx.state
525            .add_connection_to_server(ctx.server_id, conn_state)
526            .await;
527        let _ = ctx.status_tx.send("__UPDATE_UI__".to_string());
528    }
529
530    /// Spawn task to periodically clean up expired allocations.
531    ///
532    /// Registered with the server, so `stop_server` aborts it. The weak reference is kept
533    /// as well: it is what ends the task if the server is dropped without a stop, and the
534    /// two are complementary rather than alternatives.
535    async fn spawn_cleanup_task(
536        server: &Arc<Self>,
537        status_tx: mpsc::UnboundedSender<String>,
538        app_state: Arc<AppState>,
539        server_id: crate::state::ServerId,
540    ) {
541        let server = Arc::downgrade(server);
542        // Tracked, not detached: stop_server must abort this task too.
543        let task_owner = app_state.clone();
544        task_owner
545            .spawn_server_task(server_id, async move {
546                let mut interval = tokio::time::interval(Duration::from_secs(30));
547                loop {
548                    interval.tick().await;
549
550                    let Some(server) = server.upgrade() else {
551                        debug!("TURN server stopped, ending allocation cleanup task");
552                        break;
553                    };
554
555                    let mut allocations = server.allocations.lock().await;
556
557                    let mut expired = Vec::new();
558                    for (id, alloc) in allocations.iter() {
559                        if !alloc.state.lock().await.is_live() {
560                            expired.push((id.clone(), alloc.client_addr, alloc.relay_addr));
561                        }
562                    }
563
564                    let removed = expired.len();
565                    for (id, client_addr, relay_addr) in expired {
566                        // Dropping the allocation aborts its relay task and closes
567                        // the relay socket (see `impl Drop for TurnAllocation`).
568                        allocations.remove(&id);
569                        Log::new(Some(&status_tx)).debug(format!(
570                            "TURN expired allocation {} for {} (relay {})",
571                            id, client_addr, relay_addr
572                        ));
573                    }
574
575                    if removed > 0 {
576                        Log::new(Some(&status_tx)).debug(format!(
577                            "TURN cleanup removed {} expired allocations",
578                            removed
579                        ));
580                    }
581                }
582            })
583            .await;
584    }
585
586    /// Allocation belonging to `client_addr`, if any is still live.
587    async fn allocation_for_client(
588        &self,
589        client_addr: SocketAddr,
590    ) -> Option<(String, Arc<UdpSocket>, Arc<Mutex<AllocationState>>)> {
591        let allocations = self.allocations.lock().await;
592        for (id, alloc) in allocations.iter() {
593            if alloc.client_addr != client_addr {
594                continue;
595            }
596            if !alloc.state.lock().await.is_live() {
597                continue;
598            }
599            return Some((id.clone(), alloc.relay_socket.clone(), alloc.state.clone()));
600        }
601        None
602    }
603
604    /// JSON summary of this client's allocations, for the LLM event payload.
605    async fn allocation_summary(&self, client_addr: SocketAddr) -> Vec<serde_json::Value> {
606        let now = Instant::now();
607        let allocations = self.allocations.lock().await;
608        let mut summary = Vec::new();
609        for (id, alloc) in allocations.iter() {
610            if alloc.client_addr != client_addr {
611                continue;
612            }
613            let state = alloc.state.lock().await;
614            if !state.is_live() {
615                continue;
616            }
617            summary.push(serde_json::json!({
618                "allocation_id": id,
619                "relay_address": alloc.relay_addr.to_string(),
620                "lifetime_seconds": state.lifetime_seconds,
621                "expires_in_seconds": state.expires_at.saturating_duration_since(now).as_secs(),
622                "permitted_peers": state.permitted_ips(),
623                "channels": state.bound_channels(),
624            }));
625        }
626        summary
627    }
628}
629
630// ---------------------------------------------------------------------------
631// Message parsing
632// ---------------------------------------------------------------------------
633
634/// A decoded STUN/TURN message. Every field is derived with bounds checks: the
635/// input is an unauthenticated datagram from anyone who can reach the socket,
636/// and a panic in this task would be silent while the server kept reporting
637/// itself as Running.
638#[derive(Debug)]
639struct TurnMessage {
640    class: u16,
641    method: u16,
642    transaction_id: [u8; 12],
643    attributes: Vec<(u16, Vec<u8>)>,
644}
645
646impl TurnMessage {
647    fn parse(data: &[u8]) -> Option<Self> {
648        if data.len() < 20 {
649            return None;
650        }
651
652        let raw_type = u16::from_be_bytes([data[0], data[1]]);
653        // The two most significant bits of a STUN message type are always zero;
654        // anything else is a ChannelData frame or not STUN at all.
655        if raw_type & 0xC000 != 0 {
656            return None;
657        }
658
659        if u32::from_be_bytes([data[4], data[5], data[6], data[7]]) != MAGIC_COOKIE {
660            return None;
661        }
662
663        // RFC 8489 section 5: C0 is bit 4 and C1 is bit 8, class == C1<<1 | C0.
664        let c0 = (raw_type >> 4) & 0x1;
665        let c1 = (raw_type >> 8) & 0x1;
666        let class = (c1 << 1) | c0;
667        let method = (raw_type & 0x000F) | ((raw_type & 0x00E0) >> 1) | ((raw_type & 0x3E00) >> 2);
668
669        let mut transaction_id = [0u8; 12];
670        transaction_id.copy_from_slice(&data[8..20]);
671
672        // Trust the shorter of the declared length and what actually arrived.
673        let declared = u16::from_be_bytes([data[2], data[3]]) as usize;
674        let end = 20usize.saturating_add(declared).min(data.len());
675
676        let mut attributes = Vec::new();
677        let mut pos = 20usize;
678        while pos + 4 <= end {
679            let attr_type = u16::from_be_bytes([data[pos], data[pos + 1]]);
680            let attr_len = u16::from_be_bytes([data[pos + 2], data[pos + 3]]) as usize;
681            let value_start = pos + 4;
682            let value_end = match value_start.checked_add(attr_len) {
683                Some(v) if v <= end => v,
684                // Truncated or lying length: stop, keep what we parsed.
685                _ => break,
686            };
687            attributes.push((attr_type, data[value_start..value_end].to_vec()));
688
689            let padded = attr_len.saturating_add(3) & !3usize;
690            pos = match value_start.checked_add(padded) {
691                Some(p) => p,
692                None => break,
693            };
694        }
695
696        Some(Self {
697            class,
698            method,
699            transaction_id,
700            attributes,
701        })
702    }
703
704    fn attribute(&self, attr_type: u16) -> Option<&[u8]> {
705        self.attributes
706            .iter()
707            .find(|(t, _)| *t == attr_type)
708            .map(|(_, v)| v.as_slice())
709    }
710
711    fn lifetime(&self) -> Option<u32> {
712        let value = self.attribute(ATTR_LIFETIME)?;
713        if value.len() < 4 {
714            return None;
715        }
716        Some(u32::from_be_bytes([value[0], value[1], value[2], value[3]]))
717    }
718
719    fn requested_transport(&self) -> Option<u8> {
720        self.attribute(ATTR_REQUESTED_TRANSPORT)
721            .and_then(|v| v.first().copied())
722    }
723
724    fn channel_number(&self) -> Option<u16> {
725        let value = self.attribute(ATTR_CHANNEL_NUMBER)?;
726        if value.len() < 2 {
727            return None;
728        }
729        Some(u16::from_be_bytes([value[0], value[1]]))
730    }
731
732    fn data(&self) -> Option<&[u8]> {
733        self.attribute(ATTR_DATA)
734    }
735
736    /// All XOR-PEER-ADDRESS attributes. CreatePermission may carry several.
737    fn peer_addresses(&self) -> Vec<SocketAddr> {
738        self.attributes
739            .iter()
740            .filter(|(t, _)| *t == ATTR_XOR_PEER_ADDRESS)
741            .filter_map(|(_, v)| decode_xor_address(v, &self.transaction_id))
742            .collect()
743    }
744
745    fn type_name(&self) -> &'static str {
746        match (self.class, self.method) {
747            (0, 1) => "BindingRequest",
748            (0, 3) => "AllocateRequest",
749            (0, 4) => "RefreshRequest",
750            (0, 8) => "CreatePermissionRequest",
751            (0, 9) => "ChannelBindRequest",
752            (1, 6) => "SendIndication",
753            (1, 7) => "DataIndication",
754            (2, 3) => "AllocateResponse",
755            (2, 4) => "RefreshResponse",
756            (2, 8) => "CreatePermissionResponse",
757            (2, 9) => "ChannelBindResponse",
758            (3, _) => "ErrorResponse",
759            _ => "Unknown",
760        }
761    }
762}
763
764/// Decode an XOR-PEER-ADDRESS / XOR-RELAYED-ADDRESS attribute value.
765fn decode_xor_address(value: &[u8], transaction_id: &[u8; 12]) -> Option<SocketAddr> {
766    if value.len() < 4 {
767        return None;
768    }
769    let family = value[1];
770    let port = u16::from_be_bytes([value[2], value[3]]) ^ (MAGIC_COOKIE >> 16) as u16;
771    let magic = MAGIC_COOKIE.to_be_bytes();
772
773    match family {
774        0x01 => {
775            if value.len() < 8 {
776                return None;
777            }
778            let mut octets = [0u8; 4];
779            for i in 0..4 {
780                octets[i] = value[4 + i] ^ magic[i];
781            }
782            Some(SocketAddr::new(IpAddr::from(octets), port))
783        }
784        0x02 => {
785            if value.len() < 20 {
786                return None;
787            }
788            let mut octets = [0u8; 16];
789            for i in 0..4 {
790                octets[i] = value[4 + i] ^ magic[i];
791            }
792            for i in 0..12 {
793                octets[4 + i] = value[8 + i] ^ transaction_id[i];
794            }
795            Some(SocketAddr::new(IpAddr::from(octets), port))
796        }
797        _ => None,
798    }
799}
800
801/// True for a ChannelData frame (RFC 8656 section 12: channel numbers occupy
802/// 0x4000-0x7FFF, so the top two bits are 0b01).
803fn is_channel_data(data: &[u8]) -> bool {
804    data.len() >= 4 && (data[0] & 0xC0) == 0x40
805}
806
807fn is_valid_channel_number(number: u16) -> bool {
808    (0x4000..=0x7FFF).contains(&number)
809}
810
811// ---------------------------------------------------------------------------
812// Datagram dispatch
813// ---------------------------------------------------------------------------
814
815async fn handle_datagram(ctx: TurnContext, data: Vec<u8>, peer_addr: SocketAddr) {
816    // ChannelData frames are not STUN messages and must be tested first.
817    if is_channel_data(&data) {
818        handle_channel_data(&ctx, &data, peer_addr).await;
819        return;
820    }
821
822    let Some(msg) = TurnMessage::parse(&data) else {
823        Log::new(Some(&ctx.status_tx)).debug(format!("TURN invalid message from {}", peer_addr));
824        return;
825    };
826
827    match (msg.class, msg.method) {
828        // Binding requests carry no policy: the answer is the client's own
829        // address, which is a fact and not something for the model to invent.
830        (0, 1) => match TurnProtocol::build_binding_response(&msg.transaction_id, peer_addr) {
831            Ok(packet) => {
832                let _ = ctx.socket.send_to(&packet, peer_addr).await;
833                Log::new(Some(&ctx.status_tx))
834                    .debug(format!("TURN binding response to {}", peer_addr));
835            }
836            Err(e) => error!("TURN failed to build binding response: {}", e),
837        },
838        (0, 3) => handle_allocate_request(&ctx, &msg, &data, peer_addr).await,
839        (0, 4) => handle_refresh_request(&ctx, &msg, &data, peer_addr).await,
840        (0, 8) => handle_create_permission_request(&ctx, &msg, &data, peer_addr).await,
841        (0, 9) => handle_channel_bind_request(&ctx, &msg, &data, peer_addr).await,
842        (1, 6) => handle_send_indication(&ctx, &msg, peer_addr).await,
843        _ => {
844            Log::new(Some(&ctx.status_tx)).debug(format!(
845                "TURN unhandled message type {} from {}",
846                msg.type_name(),
847                peer_addr
848            ));
849        }
850    }
851}
852
853/// Build the event payload fields every TURN event carries.
854async fn base_event_data(
855    ctx: &TurnContext,
856    msg: &TurnMessage,
857    raw_len: usize,
858    peer_addr: SocketAddr,
859) -> serde_json::Value {
860    serde_json::json!({
861        "peer_addr": peer_addr.to_string(),
862        "local_addr": ctx.local_addr.to_string(),
863        "transaction_id": hex::encode(msg.transaction_id),
864        "message_type": msg.type_name(),
865        "bytes_received": raw_len,
866        "existing_allocations": ctx.server.allocation_summary(peer_addr).await,
867    })
868}
869
870/// Run the policy decision for one control request.
871///
872/// Three outcomes are kept apart, both in the log (`decision=`) and on the wire, because
873/// collapsing them is how a fail-closed protocol turns into a fail-open one:
874///
875/// * `decision=fail_closed_no_policy` — no operator policy, so no LLM call and, deliberately,
876///   **no reply**. Nothing was asked and nothing failed; the request is simply not served.
877/// * `decision=fail_closed_llm_error` — the backend errored. Nothing is granted, *and* the
878///   client is told so with a STUN error response, so it stops retransmitting for ~39s.
879/// * the model answered — the caller inspects the actions; a missing grant action is a
880///   model refusal (`decision=model_reject`, logged at the call sites).
881///
882/// The error response carries a **category only** (`crate::utils::WireFailure`), never the
883/// backend's message: the reason phrase goes on the wire to a stranger, the error goes to the
884/// log. `Overloaded` maps to 508 Insufficient Capacity and `Unavailable` to 500 Server Error —
885/// both in STUN's 5xx "temporarily unable, may retry" class, but distinct so a client can tell
886/// saturation from a broken backend.
887async fn call_llm_for_event(
888    ctx: &TurnContext,
889    event_type: &'static EventType,
890    event_data: serde_json::Value,
891    transaction_id: &[u8; 12],
892    method: u16,
893    peer_addr: SocketAddr,
894) -> Option<ExecutionResult> {
895    let event = Event::new(event_type, event_data);
896
897    // Whether to grant an Allocate/Refresh/CreatePermission/ChannelBind, and to which peers, is
898    // a policy decision, not something the request determines (the ack packet framing is
899    // mechanical, but the grant is not). With no operator policy — no server instruction and no
900    // per-event handler — TURN's own fail-closed default applies: grant nothing. So return None
901    // WITHOUT an LLM round-trip; the caller drops the reserved relay socket exactly as it does
902    // for a refusal or an LLM failure. The model is consulted only when the operator opts in
903    // with the grant policy it should apply.
904    if !operator_wants_dynamic(&ctx.state, ctx.server_id, &event_type.id).await {
905        Log::new(Some(&ctx.status_tx)).debug(format!(
906            "TURN {} decision=fail_closed_no_policy: no operator policy configured (no \
907             instruction or handler), fail-closed with no LLM call",
908            event_type.id
909        ));
910        return None;
911    }
912
913    match call_llm(
914        &ctx.llm_client,
915        &ctx.state,
916        ctx.server_id,
917        None, // TURN is UDP: no persistent connection
918        &event,
919        ctx.protocol.as_ref(),
920    )
921    .await
922    {
923        Ok(result) => {
924            let log = Log::new(Some(&ctx.status_tx));
925            for message in &result.messages {
926                log.info(format!("{}", message));
927            }
928            log.debug(format!("TURN parsed {} actions", result.raw_actions.len()));
929            Some(result)
930        }
931        Err(e) => {
932            // The full error goes to the log and the status stream, where an operator looks.
933            error!(
934                "TURN {} decision=fail_closed_llm_error for {}: {:#}",
935                event_type.id, peer_addr, e
936            );
937            Log::new(Some(&ctx.status_tx)).warn(format!(
938                "TURN {} decision=fail_closed_llm_error for {}: {}",
939                event_type.id, peer_addr, e
940            ));
941
942            // The client gets a category, never the error. Silence here left it retransmitting
943            // for the full STUN Rc/Rm schedule (~39s) before giving up.
944            let (code, reason) = match crate::utils::WireFailure::classify(&e) {
945                crate::utils::WireFailure::Overloaded => {
946                    (508u16, crate::utils::WireFailure::Overloaded.text())
947                }
948                crate::utils::WireFailure::Unavailable => {
949                    (500u16, crate::utils::WireFailure::Unavailable.text())
950                }
951            };
952            send_local_error(ctx, transaction_id, method, code, reason, peer_addr).await;
953            None
954        }
955    }
956}
957
958/// Write every byte the executed actions produced back to the client.
959async fn send_outputs(ctx: &TurnContext, result: &ExecutionResult, peer_addr: SocketAddr) {
960    for protocol_result in &result.protocol_results {
961        for output in protocol_result.get_all_output() {
962            let _ = ctx.socket.send_to(&output, peer_addr).await;
963            let log = Log::new(Some(&ctx.status_tx));
964            log.debug(format!("TURN sent {} bytes to {}", output.len(), peer_addr));
965            log.trace(format!("TURN sent (hex): {}", hex::encode(&output)));
966        }
967    }
968}
969
970/// Send an error response NetGet decided on itself (resource limits, malformed
971/// requests, a model answer we cannot honour).
972async fn send_local_error(
973    ctx: &TurnContext,
974    transaction_id: &[u8; 12],
975    method: u16,
976    code: u16,
977    reason: &str,
978    peer_addr: SocketAddr,
979) {
980    match TurnProtocol::build_error_response(transaction_id, method, code, reason) {
981        Ok(packet) => {
982            let _ = ctx.socket.send_to(&packet, peer_addr).await;
983            Log::new(Some(&ctx.status_tx)).debug(format!(
984                "TURN sent error {} {} to {}",
985                code, reason, peer_addr
986            ));
987        }
988        Err(e) => error!("TURN failed to build error response: {}", e),
989    }
990}
991
992fn find_action<'a>(result: &'a ExecutionResult, name: &str) -> Option<&'a serde_json::Value> {
993    result
994        .raw_actions
995        .iter()
996        .find(|a| a.get("type").and_then(|v| v.as_str()) == Some(name))
997}
998
999// ---------------------------------------------------------------------------
1000// Allocate
1001// ---------------------------------------------------------------------------
1002
1003async fn handle_allocate_request(
1004    ctx: &TurnContext,
1005    msg: &TurnMessage,
1006    raw: &[u8],
1007    peer_addr: SocketAddr,
1008) {
1009    // RFC 8656 section 6.2 step 3: UDP is the only transport we relay.
1010    if let Some(transport) = msg.requested_transport() {
1011        if transport != 17 {
1012            warn!(
1013                "TURN allocate from {} requested transport {} (only UDP/17 is relayed)",
1014                peer_addr, transport
1015            );
1016            send_local_error(
1017                ctx,
1018                &msg.transaction_id,
1019                3,
1020                442,
1021                "Unsupported Transport Protocol",
1022                peer_addr,
1023            )
1024            .await;
1025            return;
1026        }
1027    }
1028
1029    let in_flight = ctx.server.reservations.fetch_add(1, Ordering::Relaxed) + 1;
1030    let _reservation = ReservationGuard(ctx.server.reservations.clone());
1031
1032    if ctx.server.allocations.lock().await.len() + in_flight > MAX_ALLOCATIONS {
1033        warn!(
1034            "TURN refusing allocate from {}: at the {}-allocation cap",
1035            peer_addr, MAX_ALLOCATIONS
1036        );
1037        send_local_error(
1038            ctx,
1039            &msg.transaction_id,
1040            3,
1041            508,
1042            "Insufficient Capacity",
1043            peer_addr,
1044        )
1045        .await;
1046        return;
1047    }
1048
1049    // Reserve a real relay socket *before* asking the model, so the address it
1050    // is told to hand out is one NetGet is actually listening on. If the model
1051    // refuses, the socket is dropped at the end of this function and closed.
1052    let relay_socket = match UdpSocket::bind(SocketAddr::new(ctx.relay_bind_ip, 0)).await {
1053        Ok(s) => Arc::new(s),
1054        Err(e) => {
1055            Log::new(Some(&ctx.status_tx))
1056                .error(format!("TURN could not bind a relay socket: {}", e));
1057            send_local_error(
1058                ctx,
1059                &msg.transaction_id,
1060                3,
1061                508,
1062                "Insufficient Capacity",
1063                peer_addr,
1064            )
1065            .await;
1066            return;
1067        }
1068    };
1069    let bound_addr = match relay_socket.local_addr() {
1070        Ok(addr) => addr,
1071        Err(e) => {
1072            error!("TURN could not read relay socket address: {}", e);
1073            return;
1074        }
1075    };
1076    let relay_addr = SocketAddr::new(ctx.advertised_relay_ip, bound_addr.port());
1077
1078    let mut event_data = base_event_data(ctx, msg, raw.len(), peer_addr).await;
1079    if let Some(obj) = event_data.as_object_mut() {
1080        obj.insert(
1081            "relay_address".to_string(),
1082            serde_json::json!(relay_addr.to_string()),
1083        );
1084        obj.insert(
1085            "requested_lifetime_seconds".to_string(),
1086            match msg.lifetime() {
1087                Some(l) => serde_json::json!(l),
1088                None => serde_json::Value::Null,
1089            },
1090        );
1091        obj.insert(
1092            "requested_transport".to_string(),
1093            match msg.requested_transport() {
1094                Some(17) => serde_json::json!("udp"),
1095                Some(6) => serde_json::json!("tcp"),
1096                Some(other) => serde_json::json!(other),
1097                None => serde_json::Value::Null,
1098            },
1099        );
1100    }
1101
1102    let Some(result) = call_llm_for_event(
1103        ctx,
1104        &TURN_ALLOCATE_REQUEST_EVENT,
1105        event_data,
1106        &msg.transaction_id,
1107        3,
1108        peer_addr,
1109    )
1110    .await
1111    else {
1112        return;
1113    };
1114
1115    let Some(action) = find_action(&result, "send_turn_allocate_response") else {
1116        // No grant: refusal/error/ignore all land here. The reserved socket is
1117        // dropped, so nothing is ever relayed. This is the model answering, so it
1118        // is a policy decision — distinct from a backend failure.
1119        Log::new(Some(&ctx.status_tx)).info(format!(
1120            "TURN allocate decision=model_reject for {}: no send_turn_allocate_response action",
1121            peer_addr
1122        ));
1123        send_outputs(ctx, &result, peer_addr).await;
1124        return;
1125    };
1126
1127    // The relay address is a fact about a socket NetGet bound, not a value the
1128    // model may choose: handing the client an address nobody listens on is the
1129    // exact failure this protocol used to ship. Refuse rather than confirm it.
1130    let claimed = action
1131        .get("relay_address")
1132        .and_then(|v| v.as_str())
1133        .unwrap_or_default();
1134    if claimed != relay_addr.to_string() {
1135        Log::new(Some(&ctx.status_tx)).warn(format!(
1136            "TURN refusing allocation for {}: action relay_address {:?} is not the reserved \
1137             address {} (use {{{{event.relay_address}}}})",
1138            peer_addr, claimed, relay_addr
1139        ));
1140        send_local_error(
1141            ctx,
1142            &msg.transaction_id,
1143            3,
1144            508,
1145            "Insufficient Capacity",
1146            peer_addr,
1147        )
1148        .await;
1149        return;
1150    }
1151
1152    let lifetime = action
1153        .get("lifetime_seconds")
1154        .and_then(|v| v.as_u64())
1155        .unwrap_or(600)
1156        .min(MAX_LIFETIME_SECONDS as u64) as u32;
1157    let allocation_id = action
1158        .get("allocation_id")
1159        .and_then(|v| v.as_str())
1160        .map(|s| s.to_string())
1161        .unwrap_or_else(|| hex::encode(msg.transaction_id));
1162
1163    let state = Arc::new(Mutex::new(AllocationState::new(lifetime)));
1164    let relay_task = spawn_relay_task(
1165        relay_socket.clone(),
1166        ctx.socket.clone(),
1167        peer_addr,
1168        relay_addr,
1169        state.clone(),
1170    );
1171
1172    let allocation = TurnAllocation {
1173        client_addr: peer_addr,
1174        relay_addr,
1175        relay_socket,
1176        allocated_at: Instant::now(),
1177        state,
1178        relay_task,
1179    };
1180
1181    {
1182        let mut allocations = ctx.server.allocations.lock().await;
1183        // One allocation per client 5-tuple: replacing drops the old one, which
1184        // aborts its relay task and closes its socket.
1185        let stale: Vec<String> = allocations
1186            .iter()
1187            .filter(|(id, a)| a.client_addr == peer_addr && *id != &allocation_id)
1188            .map(|(id, _)| id.clone())
1189            .collect();
1190        for id in stale {
1191            warn!(
1192                "TURN replacing existing allocation {} for {}",
1193                id, peer_addr
1194            );
1195            allocations.remove(&id);
1196        }
1197        allocations.insert(allocation_id.clone(), allocation);
1198    }
1199
1200    Log::new(Some(&ctx.status_tx)).info(format!(
1201        "TURN allocation {} for {} relaying on {} for {}s",
1202        allocation_id, peer_addr, relay_addr, lifetime
1203    ));
1204
1205    send_outputs(ctx, &result, peer_addr).await;
1206}
1207
1208// ---------------------------------------------------------------------------
1209// Refresh
1210// ---------------------------------------------------------------------------
1211
1212async fn handle_refresh_request(
1213    ctx: &TurnContext,
1214    msg: &TurnMessage,
1215    raw: &[u8],
1216    peer_addr: SocketAddr,
1217) {
1218    let mut event_data = base_event_data(ctx, msg, raw.len(), peer_addr).await;
1219    if let Some(obj) = event_data.as_object_mut() {
1220        obj.insert(
1221            "requested_lifetime_seconds".to_string(),
1222            match msg.lifetime() {
1223                Some(l) => serde_json::json!(l),
1224                None => serde_json::Value::Null,
1225            },
1226        );
1227    }
1228
1229    let Some(result) = call_llm_for_event(
1230        ctx,
1231        &TURN_REFRESH_REQUEST_EVENT,
1232        event_data,
1233        &msg.transaction_id,
1234        4,
1235        peer_addr,
1236    )
1237    .await
1238    else {
1239        return;
1240    };
1241
1242    if find_action(&result, "send_turn_refresh_response").is_none() {
1243        Log::new(Some(&ctx.status_tx)).info(format!(
1244            "TURN refresh decision=model_reject for {}: no send_turn_refresh_response action",
1245            peer_addr
1246        ));
1247    }
1248
1249    if let Some(action) = find_action(&result, "send_turn_refresh_response") {
1250        let lifetime = action
1251            .get("lifetime_seconds")
1252            .and_then(|v| v.as_u64())
1253            .unwrap_or(600)
1254            .min(MAX_LIFETIME_SECONDS as u64) as u32;
1255
1256        let mut allocations = ctx.server.allocations.lock().await;
1257        if lifetime == 0 {
1258            // RFC 8656 section 7: lifetime 0 deletes the allocation.
1259            let doomed: Vec<String> = allocations
1260                .iter()
1261                .filter(|(_, a)| a.client_addr == peer_addr)
1262                .map(|(id, _)| id.clone())
1263                .collect();
1264            for id in doomed {
1265                allocations.remove(&id);
1266                info!("TURN deleted allocation {} for {}", id, peer_addr);
1267            }
1268        } else {
1269            let mut refreshed = 0usize;
1270            for alloc in allocations.values() {
1271                if alloc.client_addr == peer_addr {
1272                    alloc.state.lock().await.refresh(lifetime);
1273                    refreshed += 1;
1274                }
1275            }
1276            if refreshed == 0 {
1277                warn!(
1278                    "TURN refresh response sent to {} which holds no allocation",
1279                    peer_addr
1280                );
1281            }
1282        }
1283    }
1284
1285    send_outputs(ctx, &result, peer_addr).await;
1286}
1287
1288// ---------------------------------------------------------------------------
1289// CreatePermission
1290// ---------------------------------------------------------------------------
1291
1292async fn handle_create_permission_request(
1293    ctx: &TurnContext,
1294    msg: &TurnMessage,
1295    raw: &[u8],
1296    peer_addr: SocketAddr,
1297) {
1298    let requested_peers = msg.peer_addresses();
1299
1300    // Capacity is decided from the number of peers the request **declares**, before the model
1301    // is asked and before a single entry is inserted. Two reasons it is checked here and not
1302    // at the insert:
1303    //
1304    // * Resource exhaustion is not a policy question — the same reasoning `MAX_ALLOCATIONS`
1305    //   states a few functions up. Asking the model whether to exceed a hard limit invites it
1306    //   to say yes.
1307    // * A request naming 10 000 peers must cost nothing. Inserting until the map filled and
1308    //   then stopping would still have spent a model round-trip on it, and the whole point of
1309    //   bounding the declared size is that the oversized thing never reaches the prompt.
1310    if let Some((_, _, state)) = ctx.server.allocation_for_client(peer_addr).await {
1311        let headroom = state.lock().await.permission_headroom();
1312        if requested_peers.len() > headroom {
1313            Log::new(Some(&ctx.status_tx)).warn(format!(
1314                "TURN create_permission decision=fail_closed_permission_capacity for {}: asked \
1315                 for {} peer(s) with {} of {} slots free; answering 508",
1316                peer_addr,
1317                requested_peers.len(),
1318                headroom,
1319                MAX_PERMISSIONS
1320            ));
1321            send_local_error(
1322                ctx,
1323                &msg.transaction_id,
1324                8,
1325                508,
1326                "Insufficient Capacity",
1327                peer_addr,
1328            )
1329            .await;
1330            return;
1331        }
1332    }
1333
1334    let mut event_data = base_event_data(ctx, msg, raw.len(), peer_addr).await;
1335    if let Some(obj) = event_data.as_object_mut() {
1336        obj.insert(
1337            "peer_addresses".to_string(),
1338            serde_json::json!(requested_peers
1339                .iter()
1340                .map(|p| p.to_string())
1341                .collect::<Vec<_>>()),
1342        );
1343    }
1344
1345    let Some(result) = call_llm_for_event(
1346        ctx,
1347        &TURN_CREATE_PERMISSION_REQUEST_EVENT,
1348        event_data,
1349        &msg.transaction_id,
1350        8,
1351        peer_addr,
1352    )
1353    .await
1354    else {
1355        return;
1356    };
1357
1358    if find_action(&result, "send_turn_create_permission_response").is_none() {
1359        Log::new(Some(&ctx.status_tx)).info(format!(
1360            "TURN create_permission decision=model_reject for {}: no \
1361             send_turn_create_permission_response action",
1362            peer_addr
1363        ));
1364    }
1365
1366    if let Some(action) = find_action(&result, "send_turn_create_permission_response") {
1367        // An explicit list lets the model permit a subset; omitting it permits
1368        // every peer the request named. Peers the request did *not* name are
1369        // never permitted, so a hallucinated address cannot open a hole.
1370        let chosen: Vec<IpAddr> = match action.get("peer_addresses").and_then(|v| v.as_array()) {
1371            Some(list) => list
1372                .iter()
1373                .filter_map(|v| v.as_str())
1374                .filter_map(parse_peer_ip)
1375                .filter(|ip| {
1376                    let known = requested_peers.iter().any(|p| p.ip() == *ip);
1377                    if !known {
1378                        warn!(
1379                            "TURN ignoring permission for {} which {} did not ask for",
1380                            ip, peer_addr
1381                        );
1382                    }
1383                    known
1384                })
1385                .collect(),
1386            None => requested_peers.iter().map(|p| p.ip()).collect(),
1387        };
1388
1389        // The operator's destination policy outranks the model's. `peer_scope` is
1390        // the only thing standing between a granted allocation and a UDP path into
1391        // whatever this host can reach, so a model that permits 127.0.0.1 under
1392        // peer_scope=public is overruled rather than trusted.
1393        let chosen: Vec<IpAddr> = chosen
1394            .into_iter()
1395            .filter(|ip| {
1396                let allowed = ctx.peer_scope.allows(*ip);
1397                if !allowed {
1398                    Log::new(Some(&ctx.status_tx)).warn(format!(
1399                        "TURN decision=peer_scope_reject: refusing permission for {} \
1400                         requested by {} (peer_scope={})",
1401                        ip,
1402                        peer_addr,
1403                        ctx.peer_scope.as_str()
1404                    ));
1405                }
1406                allowed
1407            })
1408            .collect();
1409
1410        if let Some((id, _, state)) = ctx.server.allocation_for_client(peer_addr).await {
1411            let mut state = state.lock().await;
1412            let mut refused = 0usize;
1413            for ip in &chosen {
1414                if !state.permit(*ip) {
1415                    refused += 1;
1416                }
1417            }
1418            // The declared count was checked against the headroom before the model was asked,
1419            // so this can only fire if another request filled the map while this one was in
1420            // flight. It is still not allowed to be silent: a permission the client believes
1421            // it holds and does not is a relay that drops its traffic for no stated reason.
1422            if refused > 0 {
1423                Log::new(Some(&ctx.status_tx)).warn(format!(
1424                    "TURN allocation {id} decision=fail_closed_permission_capacity: {refused} of \
1425                     {} permission(s) for {peer_addr} did not fit under the {MAX_PERMISSIONS} cap",
1426                    chosen.len()
1427                ));
1428            }
1429            Log::new(Some(&ctx.status_tx)).info(format!(
1430                "TURN allocation {} permits {:?} for {} ({} peer(s))",
1431                id,
1432                chosen,
1433                peer_addr,
1434                chosen.len()
1435            ));
1436        } else {
1437            warn!(
1438                "TURN permission response sent to {} which holds no allocation",
1439                peer_addr
1440            );
1441        }
1442    }
1443
1444    send_outputs(ctx, &result, peer_addr).await;
1445}
1446
1447fn parse_peer_ip(value: &str) -> Option<IpAddr> {
1448    if let Ok(addr) = value.parse::<SocketAddr>() {
1449        return Some(addr.ip());
1450    }
1451    value.parse::<IpAddr>().ok()
1452}
1453
1454// ---------------------------------------------------------------------------
1455// ChannelBind
1456// ---------------------------------------------------------------------------
1457
1458async fn handle_channel_bind_request(
1459    ctx: &TurnContext,
1460    msg: &TurnMessage,
1461    raw: &[u8],
1462    peer_addr: SocketAddr,
1463) {
1464    let channel_number = msg.channel_number();
1465    let peer = msg.peer_addresses().into_iter().next();
1466
1467    let (Some(channel_number), Some(peer)) = (channel_number, peer) else {
1468        warn!(
1469            "TURN channel bind from {} missing CHANNEL-NUMBER or XOR-PEER-ADDRESS",
1470            peer_addr
1471        );
1472        send_local_error(ctx, &msg.transaction_id, 9, 400, "Bad Request", peer_addr).await;
1473        return;
1474    };
1475
1476    if !is_valid_channel_number(channel_number) {
1477        warn!(
1478            "TURN channel bind from {} used out-of-range channel {}",
1479            peer_addr, channel_number
1480        );
1481        send_local_error(ctx, &msg.transaction_id, 9, 400, "Bad Request", peer_addr).await;
1482        return;
1483    }
1484
1485    // ChannelBind grants a permission as a side effect (`bind_channel` calls
1486    // `permit`), so it is a second way to point the relay somewhere and needs the
1487    // same destination policy. Refused before the model is asked: whether a
1488    // destination is in scope is the operator's decision, not a policy question.
1489    if !ctx.peer_scope.allows(peer.ip()) {
1490        Log::new(Some(&ctx.status_tx)).warn(format!(
1491            "TURN channel_bind decision=peer_scope_reject: {} asked to bind channel {} to {} \
1492             (peer_scope={})",
1493            peer_addr,
1494            channel_number,
1495            peer,
1496            ctx.peer_scope.as_str()
1497        ));
1498        send_local_error(ctx, &msg.transaction_id, 9, 403, "Forbidden", peer_addr).await;
1499        return;
1500    }
1501
1502    // Same rule as CreatePermission: capacity before the model, not after. A binding also
1503    // grants a permission, so a request that cannot have one cannot have the other either.
1504    if let Some((_, _, state)) = ctx.server.allocation_for_client(peer_addr).await {
1505        let mut state = state.lock().await;
1506        let channel_is_new = state.channel_peer(channel_number).is_none();
1507        if (channel_is_new && state.live_channels() >= MAX_CHANNELS)
1508            || state.permission_headroom() == 0
1509        {
1510            Log::new(Some(&ctx.status_tx)).warn(format!(
1511                "TURN channel_bind decision=fail_closed_permission_capacity: {peer_addr} is at \
1512                 the {MAX_CHANNELS}-channel / {MAX_PERMISSIONS}-permission cap; answering 508"
1513            ));
1514            drop(state);
1515            send_local_error(
1516                ctx,
1517                &msg.transaction_id,
1518                9,
1519                508,
1520                "Insufficient Capacity",
1521                peer_addr,
1522            )
1523            .await;
1524            return;
1525        }
1526    }
1527
1528    let mut event_data = base_event_data(ctx, msg, raw.len(), peer_addr).await;
1529    if let Some(obj) = event_data.as_object_mut() {
1530        obj.insert(
1531            "channel_number".to_string(),
1532            serde_json::json!(channel_number),
1533        );
1534        obj.insert(
1535            "peer_address".to_string(),
1536            serde_json::json!(peer.to_string()),
1537        );
1538    }
1539
1540    let Some(result) = call_llm_for_event(
1541        ctx,
1542        &TURN_CHANNEL_BIND_REQUEST_EVENT,
1543        event_data,
1544        &msg.transaction_id,
1545        9,
1546        peer_addr,
1547    )
1548    .await
1549    else {
1550        return;
1551    };
1552
1553    if find_action(&result, "send_turn_channel_bind_response").is_none() {
1554        Log::new(Some(&ctx.status_tx)).info(format!(
1555            "TURN channel_bind decision=model_reject for {}: no send_turn_channel_bind_response \
1556             action",
1557            peer_addr
1558        ));
1559    }
1560
1561    if find_action(&result, "send_turn_channel_bind_response").is_some() {
1562        if let Some((id, _, state)) = ctx.server.allocation_for_client(peer_addr).await {
1563            if state.lock().await.bind_channel(channel_number, peer) {
1564                Log::new(Some(&ctx.status_tx)).info(format!(
1565                    "TURN allocation {} bound channel {} to {}",
1566                    id, channel_number, peer
1567                ));
1568            } else {
1569                // Only reachable if another request filled the map between the capacity check
1570                // above and here. Never silent: a channel the client believes it holds and
1571                // does not is a relay that drops its traffic for no stated reason.
1572                Log::new(Some(&ctx.status_tx)).warn(format!(
1573                    "TURN allocation {id} decision=fail_closed_permission_capacity: channel \
1574                     {channel_number} -> {peer} did not fit under the {MAX_CHANNELS}-channel / \
1575                     {MAX_PERMISSIONS}-permission cap"
1576                ));
1577            }
1578        } else {
1579            warn!(
1580                "TURN channel bind response sent to {} which holds no allocation",
1581                peer_addr
1582            );
1583        }
1584    }
1585
1586    send_outputs(ctx, &result, peer_addr).await;
1587}
1588
1589// ---------------------------------------------------------------------------
1590// Data plane: client -> peer
1591// ---------------------------------------------------------------------------
1592
1593async fn handle_send_indication(ctx: &TurnContext, msg: &TurnMessage, peer_addr: SocketAddr) {
1594    let Some(peer) = msg.peer_addresses().into_iter().next() else {
1595        debug!(
1596            "TURN send indication from {} has no peer address",
1597            peer_addr
1598        );
1599        return;
1600    };
1601    let Some(payload) = msg.data() else {
1602        debug!("TURN send indication from {} has no DATA", peer_addr);
1603        return;
1604    };
1605
1606    // RFC 8656 section 10.2: indications are never answered with an error;
1607    // anything we cannot relay is silently discarded.
1608    let Some((id, relay_socket, state)) = ctx.server.allocation_for_client(peer_addr).await else {
1609        debug!(
1610            "TURN send indication from {} which holds no allocation, discarded",
1611            peer_addr
1612        );
1613        return;
1614    };
1615
1616    if !state.lock().await.is_permitted(&peer.ip()) {
1617        debug!(
1618            "TURN send indication from {} to unpermitted peer {}, discarded",
1619            peer_addr, peer
1620        );
1621        return;
1622    }
1623
1624    match relay_socket.send_to(payload, peer).await {
1625        Ok(n) => trace!(
1626            "TURN allocation {} relayed {} bytes from {} to {}",
1627            id,
1628            n,
1629            peer_addr,
1630            peer
1631        ),
1632        Err(e) => warn!("TURN relay to {} failed: {}", peer, e),
1633    }
1634}
1635
1636async fn handle_channel_data(ctx: &TurnContext, data: &[u8], peer_addr: SocketAddr) {
1637    if data.len() < 4 {
1638        return;
1639    }
1640    let channel_number = u16::from_be_bytes([data[0], data[1]]);
1641    let length = u16::from_be_bytes([data[2], data[3]]) as usize;
1642    let end = match 4usize.checked_add(length) {
1643        Some(end) if end <= data.len() => end,
1644        _ => {
1645            debug!(
1646                "TURN channel data from {} declares {} bytes but carries {}",
1647                peer_addr,
1648                length,
1649                data.len().saturating_sub(4)
1650            );
1651            return;
1652        }
1653    };
1654    let payload = &data[4..end];
1655
1656    let Some((id, relay_socket, state)) = ctx.server.allocation_for_client(peer_addr).await else {
1657        debug!(
1658            "TURN channel data from {} which holds no allocation, discarded",
1659            peer_addr
1660        );
1661        return;
1662    };
1663
1664    let peer = {
1665        let state = state.lock().await;
1666        match state.channel_peer(channel_number) {
1667            Some(peer) if state.is_permitted(&peer.ip()) => peer,
1668            _ => {
1669                debug!(
1670                    "TURN channel data from {} on unbound channel {}, discarded",
1671                    peer_addr, channel_number
1672                );
1673                return;
1674            }
1675        }
1676    };
1677
1678    match relay_socket.send_to(payload, peer).await {
1679        Ok(n) => trace!(
1680            "TURN allocation {} relayed {} bytes from {} to {} (channel {})",
1681            id,
1682            n,
1683            peer_addr,
1684            peer,
1685            channel_number
1686        ),
1687        Err(e) => warn!("TURN relay to {} failed: {}", peer, e),
1688    }
1689}
1690
1691// ---------------------------------------------------------------------------
1692// Data plane: peer -> client
1693// ---------------------------------------------------------------------------
1694
1695/// Forward everything arriving on the relay socket to the client, as a Data
1696/// indication or a ChannelData frame. Per-packet logging stays on `trace!`
1697/// (file only) because the status channel is unbounded and has no backpressure.
1698fn spawn_relay_task(
1699    relay_socket: Arc<UdpSocket>,
1700    turn_socket: Arc<UdpSocket>,
1701    client_addr: SocketAddr,
1702    relay_addr: SocketAddr,
1703    state: Arc<Mutex<AllocationState>>,
1704) -> JoinHandle<()> {
1705    tokio::spawn(async move {
1706        let mut buffer = vec![0u8; RELAY_MTU];
1707        loop {
1708            let (n, src_addr) = match relay_socket.recv_from(&mut buffer).await {
1709                Ok(v) => v,
1710                Err(e) => {
1711                    debug!("TURN relay socket {} closed: {}", relay_addr, e);
1712                    break;
1713                }
1714            };
1715
1716            let (live, permitted, channel) = {
1717                let state = state.lock().await;
1718                (
1719                    state.is_live(),
1720                    state.is_permitted(&src_addr.ip()),
1721                    state.channel_for_peer(&src_addr),
1722                )
1723            };
1724
1725            if !live {
1726                // The allocation expired; the cleanup tick has not removed it
1727                // yet. Relaying now would outlive the lifetime the client was
1728                // promised.
1729                debug!(
1730                    "TURN relay {} dropped {} bytes from {}: allocation expired",
1731                    relay_addr, n, src_addr
1732                );
1733                continue;
1734            }
1735
1736            if !permitted {
1737                debug!(
1738                    "TURN relay {} dropped {} bytes from unpermitted peer {}",
1739                    relay_addr, n, src_addr
1740                );
1741                continue;
1742            }
1743
1744            let packet = match channel {
1745                Some(number) => TurnProtocol::build_channel_data(number, &buffer[..n]),
1746                None => match TurnProtocol::build_data_indication(src_addr, &buffer[..n]) {
1747                    Ok(packet) => packet,
1748                    Err(e) => {
1749                        warn!("TURN could not build data indication: {}", e);
1750                        continue;
1751                    }
1752                },
1753            };
1754
1755            match turn_socket.send_to(&packet, client_addr).await {
1756                Ok(_) => trace!(
1757                    "TURN relay {} forwarded {} bytes from {} to client {}",
1758                    relay_addr,
1759                    n,
1760                    src_addr,
1761                    client_addr
1762                ),
1763                Err(e) => warn!("TURN could not reach client {}: {}", client_addr, e),
1764            }
1765        }
1766    })
1767}
1768
1769/// Returns true if the operator opted into dynamic (LLM- or handler-driven) responses for this
1770/// server: either a non-empty server instruction was given, or an event handler is configured
1771/// for `event_id`. When false the protocol applies its static default and never consults the
1772/// model — for TURN that default is to grant nothing (fail closed), because whether to grant an
1773/// allocation/permission and to which peers is a policy decision with no configured policy to
1774/// apply.
1775async fn operator_wants_dynamic(state: &AppState, server_id: ServerId, event_id: &str) -> bool {
1776    state
1777        .with_server_mut(server_id, |server| {
1778            let has_instruction = !server.instruction.trim().is_empty();
1779            let has_handler = server
1780                .event_handler_config
1781                .as_ref()
1782                .map(|c| c.find_handler(event_id).is_some())
1783                .unwrap_or(false);
1784            has_instruction || has_handler
1785        })
1786        .await
1787        .unwrap_or(false)
1788}