Skip to main content

netget/server/turn/
actions.rs

1//! TURN protocol actions implementation
2
3use crate::llm::actions::{
4    protocol_trait::{ActionResult, Protocol, Server},
5    ActionDefinition, Parameter,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub struct TurnProtocol;
15
16impl TurnProtocol {
17    pub fn new() -> Self {
18        Self
19    }
20}
21
22impl Default for TurnProtocol {
23    fn default() -> Self {
24        Self::new()
25    }
26}
27
28// Implement Protocol trait (common functionality)
29impl Protocol for TurnProtocol {
30    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
31        // Deliberately empty. allocate_relay_address and revoke_allocation were
32        // advertised here but execute_action had no arm for either, so calling
33        // one only ever produced "Unknown TURN action".
34        Vec::new()
35    }
36    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
37        vec![
38            send_turn_allocate_response_action(),
39            send_turn_refresh_response_action(),
40            send_turn_create_permission_response_action(),
41            send_turn_channel_bind_response_action(),
42            send_turn_error_response_action(),
43            ignore_request_action(),
44        ]
45    }
46    fn get_startup_parameters(&self) -> Vec<crate::llm::actions::ParameterDefinition> {
47        use crate::llm::actions::ParameterDefinition;
48        vec![
49            ParameterDefinition {
50                name: "relay_ip".to_string(),
51                type_hint: "string".to_string(),
52                description:
53                    "IP address advertised to clients in XOR-RELAYED-ADDRESS. Relay sockets \
54                     are always bound to the server's own listen address; set this only when \
55                     clients reach the relay at a different address (NAT, port forwarding). \
56                     Default: the server's listen address."
57                        .to_string(),
58                required: false,
59                example: json!("203.0.113.5"),
60                default: None,
61            },
62            ParameterDefinition {
63                name: "peer_scope".to_string(),
64                type_hint: "string".to_string(),
65                description: "Which peer addresses this relay may be pointed at. \"any\" \
66                              (default) places no restriction — a client can be permitted to \
67                              relay to 127.0.0.1 or to an RFC 1918 address, which is a UDP \
68                              path into whatever the operator's host can reach. That is the \
69                              right default for a honeypot and on loopback, and it is the \
70                              wrong one anywhere else: set \"public\" to refuse loopback, \
71                              private, link-local (including the 169.254.169.254 metadata \
72                              address), multicast, broadcast and unspecified destinations."
73                    .to_string(),
74                required: false,
75                example: json!("public"),
76                default: None,
77            },
78        ]
79    }
80    fn protocol_name(&self) -> &'static str {
81        "TURN"
82    }
83    fn get_event_types(&self) -> Vec<EventType> {
84        get_turn_event_types()
85    }
86    fn stack_name(&self) -> &'static str {
87        "ETH>IP>UDP>TURN"
88    }
89    fn keywords(&self) -> Vec<&'static str> {
90        vec!["turn"]
91    }
92    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
93        use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
94
95        ProtocolMetadataV2::builder()
96            .connectionless()
97            // Answers on failure: a STUN error response (508 when overloaded, 500 otherwise); no
98            // allocation or permission is ever granted on that path.
99            .answers_on_failure()
100            .state(DevelopmentState::Experimental)
101            .well_known_udp_port(3478)
102            .implementation("Manual TURN protocol (RFC 8656) with a real UDP relay: every granted allocation binds its own socket and forwards traffic both ways")
103            .llm_control("Whether to grant Allocate/Refresh/CreatePermission/ChannelBind, the lifetime, and which peers are permitted (policy, LLM). With no policy configured the server grants nothing with no LLM call. The data plane never calls the LLM")
104            .e2e_testing("Mocked E2E relays a payload between two real UDP peers in both directions (tests/server/turn/e2e_test.rs)")
105            .notes("THIS IS AN OPEN RELAY WHEN A POLICY GRANTS. Read the next three sentences before exposing it. (1) There is NO AUTHENTICATION: REALM, NONCE and MESSAGE-INTEGRITY are not implemented and no action can add them, so 'who may allocate' is entirely whatever answers turn_allocate_request — an instruction like 'grant allocations' means any host that can reach the port gets a relay, with no identity at all. (2) The DEFAULT DESTINATION SCOPE IS UNRESTRICTED (peer_scope=any): a permitted client can be relayed to 127.0.0.1, to RFC 1918 space or to 169.254.169.254, which is a UDP path from a stranger into whatever this host can reach, with the answers relayed back. That default is deliberate — this protocol exists to be pointed at by things under test, and a honeypot TURN server that refuses private destinations is not one — but set the peer_scope=public startup parameter anywhere it is not what you want, and it is NOT what you want on a reachable interface. (3) The only limits that do not depend on the model are a 256-allocation cap, a 3600s maximum lifetime and RFC 8656's fixed permission/channel lifetimes; there is no per-source rate limit and no cap on relayed bytes. With NO operator policy (no instruction, no handler) every control request is fail-closed — grant nothing, no LLM round-trip, no reply — which is why an unconfigured TURN server is safe and a configured one is exactly as safe as its policy. Mechanics: relays UDP only (no TCP allocations, no REQUESTED-ADDRESS-FAMILY, no reservation tokens or EVEN-PORT); Send indications and ChannelData go out the allocation's relay socket to permitted peers and peer traffic comes back as Data indications or ChannelData; the relay address is chosen by NetGet (the socket it actually bound), not by the model, and an action naming any other address is refused with 508. The ack-packet framing and the relay data plane are mechanical and never call the LLM")
106            .build()
107    }
108    fn description(&self) -> &'static str {
109        "TURN relay server for NAT traversal"
110    }
111    fn example_prompt(&self) -> &'static str {
112        "Start a TURN relay server on port 3478 with 10 minute allocations"
113    }
114    fn group_name(&self) -> &'static str {
115        "Proxy & Network"
116    }
117
118    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
119        use crate::llm::actions::StartupExamples;
120
121        // Deterministic: grant every allocation and refresh, echoing the
122        // server-reserved relay address and the client's transaction id, no LLM
123        // call. One script handles both events.
124        let script = r#"import json, sys
125data = json.load(sys.stdin)
126event = data["event"]
127et = data["event_type_id"]
128if et == "turn_allocate_request":
129    actions = [{"type": "send_turn_allocate_response",
130                "transaction_id": event.get("transaction_id"),
131                "relay_address": event.get("relay_address"),
132                "lifetime_seconds": 600}]
133elif et == "turn_refresh_request":
134    actions = [{"type": "send_turn_refresh_response",
135                "transaction_id": event.get("transaction_id"),
136                "lifetime_seconds": 600}]
137else:
138    actions = []
139print(json.dumps({"actions": actions}))"#;
140
141        StartupExamples::new(
142            // LLM mode
143            json!({
144                "type": "open_server",
145                "port": 3478,
146                "base_stack": "turn",
147                "instruction": "TURN relay server for NAT traversal. Allocate relay addresses on request. Grant 600 second lifetimes for allocations."
148            }),
149            // Script mode
150            json!({
151                "type": "open_server",
152                "port": 3478,
153                "base_stack": "turn",
154                "event_handlers": [{
155                    "event_pattern": "turn_allocate_request",
156                    "handler": {
157                        "type": "script",
158                        "language": "python",
159                        "code": script
160                    }
161                }, {
162                    "event_pattern": "turn_refresh_request",
163                    "handler": {
164                        "type": "script",
165                        "language": "python",
166                        "code": script
167                    }
168                }]
169            }),
170            // Static mode
171            json!({
172                "type": "open_server",
173                "port": 3478,
174                "base_stack": "turn",
175                "event_handlers": [{
176                    "event_pattern": "turn_allocate_request",
177                    "handler": {
178                        "type": "static",
179                        "actions": [{
180                            "type": "send_turn_allocate_response",
181                            "relay_address": "{{event.relay_address}}",
182                            "client_address": "{{event.peer_addr}}",
183                            "transaction_id": "{{event.transaction_id}}",
184                            "lifetime_seconds": 600
185                        }]
186                    }
187                }]
188            }),
189        )
190    }
191}
192
193// Implement Server trait (server-specific functionality)
194impl Server for TurnProtocol {
195    fn spawn(
196        &self,
197        ctx: crate::protocol::SpawnContext,
198    ) -> std::pin::Pin<
199        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
200    > {
201        Box::pin(async move {
202            let relay_ip = ctx
203                .startup_params
204                .as_ref()
205                .map(|p| p.get_optional_string("relay_ip"))
206                .transpose()?
207                .flatten();
208
209            let peer_scope = ctx
210                .startup_params
211                .as_ref()
212                .map(|p| p.get_optional_string("peer_scope"))
213                .transpose()?
214                .flatten();
215            let peer_scope = crate::server::turn::PeerScope::parse(peer_scope.as_deref())?;
216
217            use crate::server::turn::TurnServer;
218            TurnServer::spawn_with_llm_actions(
219                ctx.legacy_listen_addr(),
220                ctx.llm_client,
221                ctx.state,
222                ctx.status_tx,
223                ctx.server_id,
224                relay_ip,
225                peer_scope,
226            )
227            .await
228        })
229    }
230    fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
231        let action_type = action
232            .get("type")
233            .and_then(|v| v.as_str())
234            .context("Missing 'type' field in action")?;
235
236        match action_type {
237            "send_turn_allocate_response" => self.execute_send_allocate_response(action),
238            "send_turn_refresh_response" => self.execute_send_refresh_response(action),
239            "send_turn_create_permission_response" => self.execute_send_permission_response(action),
240            "send_turn_channel_bind_response" => self.execute_send_channel_bind_response(action),
241            "send_turn_error_response" => self.execute_send_error_response(action),
242            "ignore_request" => Ok(ActionResult::NoAction),
243            _ => Err(anyhow::anyhow!("Unknown TURN action: {}", action_type)),
244        }
245    }
246}
247
248impl TurnProtocol {
249    /// Read and validate the `transaction_id` field shared by every response action.
250    fn transaction_id_from(action: &serde_json::Value) -> Result<[u8; 12]> {
251        let transaction_id = action
252            .get("transaction_id")
253            .and_then(|v| v.as_str())
254            .context("Missing 'transaction_id' field")?;
255
256        let bytes = hex::decode(transaction_id).context("Invalid transaction_id hex")?;
257
258        let bytes: [u8; 12] = bytes
259            .try_into()
260            .map_err(|_| anyhow::anyhow!("Transaction ID must be 12 bytes (24 hex characters)"))?;
261
262        Ok(bytes)
263    }
264
265    /// Execute TURN allocate response
266    fn execute_send_allocate_response(&self, action: serde_json::Value) -> Result<ActionResult> {
267        let relay_address = action
268            .get("relay_address")
269            .and_then(|v| v.as_str())
270            .context("Missing 'relay_address' field")?;
271
272        let transaction_id = Self::transaction_id_from(&action)?;
273
274        let lifetime_seconds = action
275            .get("lifetime_seconds")
276            .and_then(|v| v.as_u64())
277            .unwrap_or(600) as u32;
278
279        // Parse relay address. The server loop checks this against the socket
280        // it actually bound and refuses the allocation if they differ.
281        let relay_addr: std::net::SocketAddr = relay_address
282            .parse()
283            .context("Invalid relay_address format")?;
284
285        // Optional XOR-MAPPED-ADDRESS (RFC 8656 section 6.3): the client's own
286        // reflexive address, echoed from the event's peer_addr.
287        let client_addr = match action.get("client_address").and_then(|v| v.as_str()) {
288            Some(addr) => Some(
289                addr.parse::<std::net::SocketAddr>()
290                    .context("Invalid client_address format")?,
291            ),
292            None => None,
293        };
294
295        // Note: allocation tracking is handled in the TURN server's main loop,
296        // which owns the relay socket this response advertises.
297        let packet = Self::build_allocate_response(
298            &transaction_id,
299            relay_addr,
300            lifetime_seconds,
301            client_addr,
302        )?;
303
304        Ok(ActionResult::Output(packet))
305    }
306
307    /// Execute TURN refresh response
308    fn execute_send_refresh_response(&self, action: serde_json::Value) -> Result<ActionResult> {
309        let transaction_id = Self::transaction_id_from(&action)?;
310
311        let lifetime_seconds = action
312            .get("lifetime_seconds")
313            .and_then(|v| v.as_u64())
314            .unwrap_or(600) as u32;
315
316        let packet = Self::build_refresh_response(&transaction_id, lifetime_seconds)?;
317
318        Ok(ActionResult::Output(packet))
319    }
320
321    /// Execute TURN create permission response
322    fn execute_send_permission_response(&self, action: serde_json::Value) -> Result<ActionResult> {
323        let transaction_id = Self::transaction_id_from(&action)?;
324
325        // The permitted peers themselves are applied by the server loop, which
326        // owns the allocation table; this only builds the acknowledgement.
327        let packet = Self::build_permission_response(&transaction_id)?;
328
329        Ok(ActionResult::Output(packet))
330    }
331
332    /// Execute TURN channel bind response
333    fn execute_send_channel_bind_response(
334        &self,
335        action: serde_json::Value,
336    ) -> Result<ActionResult> {
337        let transaction_id = Self::transaction_id_from(&action)?;
338
339        // Channel number and peer come from the request; the server loop binds
340        // them when it sees this action.
341        let packet = Self::build_success_response(&transaction_id, 9)?;
342
343        Ok(ActionResult::Output(packet))
344    }
345
346    /// Execute TURN error response
347    fn execute_send_error_response(&self, action: serde_json::Value) -> Result<ActionResult> {
348        // Range-check before narrowing: `65536 as u16` is 0, which encodes an ERROR-CODE
349        // attribute with class 0 - not an error at all. RFC 8489 section 14.8 encodes the
350        // code as a class/number pair, so only 300-699 is representable.
351        let error_code_raw = action
352            .get("error_code")
353            .and_then(|v| v.as_u64())
354            .unwrap_or(400);
355        if !(300..700).contains(&error_code_raw) {
356            return Err(anyhow::anyhow!(
357                "error_code {error_code_raw} is outside the 300-699 range RFC 8489 section \
358                 14.8 encodes as a class/number pair"
359            ));
360        }
361        let error_code = error_code_raw as u16;
362
363        let reason = action
364            .get("reason")
365            .and_then(|v| v.as_str())
366            .unwrap_or("Bad Request");
367
368        let transaction_id = Self::transaction_id_from(&action)?;
369
370        // The error response must carry the same method as the request it
371        // answers; hardcoding Allocate meant a Refresh or CreatePermission
372        // failure came back as an Allocate error, which clients ignore.
373        let method_name = action
374            .get("method")
375            .and_then(|v| v.as_str())
376            .unwrap_or("allocate");
377        let method = match method_name.to_ascii_lowercase().as_str() {
378            "allocate" => 3,
379            "refresh" => 4,
380            "create_permission" | "createpermission" => 8,
381            "channel_bind" | "channelbind" => 9,
382            other => {
383                return Err(anyhow::anyhow!(
384                    "Unknown 'method' value {other:?}. Valid values are \"allocate\", \
385                     \"refresh\", \"create_permission\" and \"channel_bind\"."
386                ))
387            }
388        };
389
390        let packet = Self::build_error_response(&transaction_id, method, error_code, reason)?;
391
392        Ok(ActionResult::Output(packet))
393    }
394
395    /// STUN message type for a (method, class) pair (RFC 8489 section 5).
396    fn message_type(method: u16, class: u16) -> u16 {
397        let class_bits = ((class & 0x2) << 7) | ((class & 0x1) << 4);
398        (method & 0x000F) | ((method & 0x0070) << 1) | ((method & 0x0F80) << 2) | class_bits
399    }
400
401    /// Start a STUN message: type, placeholder length, magic cookie, transaction ID.
402    fn start_message(message_type: u16, transaction_id: &[u8; 12]) -> Vec<u8> {
403        let mut packet = Vec::with_capacity(32);
404        packet.extend_from_slice(&message_type.to_be_bytes());
405        packet.extend_from_slice(&0u16.to_be_bytes()); // length placeholder
406        packet.extend_from_slice(&0x2112A442u32.to_be_bytes());
407        packet.extend_from_slice(transaction_id);
408        packet
409    }
410
411    /// Write the attribute length into the header once all attributes are added.
412    fn finish_message(packet: &mut [u8]) {
413        let attributes_length = (packet.len() - 20) as u16;
414        packet[2..4].copy_from_slice(&attributes_length.to_be_bytes());
415    }
416
417    /// Build TURN allocate response packet
418    fn build_allocate_response(
419        transaction_id: &[u8; 12],
420        relay_addr: std::net::SocketAddr,
421        lifetime_seconds: u32,
422        client_addr: Option<std::net::SocketAddr>,
423    ) -> Result<Vec<u8>> {
424        // 0x0103 = Allocate Success Response (class 2 = success)
425        let mut packet = Self::start_message(Self::message_type(3, 2), transaction_id);
426
427        // XOR-RELAYED-ADDRESS (0x0016)
428        Self::add_xor_address_attribute(&mut packet, 0x0016, relay_addr, transaction_id)?;
429
430        // XOR-MAPPED-ADDRESS (0x0020), when the caller echoed the client address
431        if let Some(client_addr) = client_addr {
432            Self::add_xor_address_attribute(&mut packet, 0x0020, client_addr, transaction_id)?;
433        }
434
435        Self::add_lifetime_attribute(&mut packet, lifetime_seconds)?;
436        Self::add_software_attribute(&mut packet, "NetGet TURN/1.0")?;
437
438        Self::finish_message(&mut packet);
439        Ok(packet)
440    }
441
442    /// Build TURN refresh response packet
443    fn build_refresh_response(transaction_id: &[u8; 12], lifetime_seconds: u32) -> Result<Vec<u8>> {
444        // 0x0104 = Refresh Success Response
445        let mut packet = Self::start_message(Self::message_type(4, 2), transaction_id);
446        Self::add_lifetime_attribute(&mut packet, lifetime_seconds)?;
447        Self::finish_message(&mut packet);
448        Ok(packet)
449    }
450
451    /// Build TURN create permission response packet
452    fn build_permission_response(transaction_id: &[u8; 12]) -> Result<Vec<u8>> {
453        Self::build_success_response(transaction_id, 8)
454    }
455
456    /// Build a success response carrying no method-specific attributes.
457    fn build_success_response(transaction_id: &[u8; 12], method: u16) -> Result<Vec<u8>> {
458        let mut packet = Self::start_message(Self::message_type(method, 2), transaction_id);
459        Self::add_software_attribute(&mut packet, "NetGet TURN/1.0")?;
460        Self::finish_message(&mut packet);
461        Ok(packet)
462    }
463
464    /// Build a STUN Binding success response (XOR-MAPPED-ADDRESS of the client).
465    pub(crate) fn build_binding_response(
466        transaction_id: &[u8; 12],
467        client_addr: std::net::SocketAddr,
468    ) -> Result<Vec<u8>> {
469        let mut packet = Self::start_message(Self::message_type(1, 2), transaction_id);
470        Self::add_xor_address_attribute(&mut packet, 0x0020, client_addr, transaction_id)?;
471        Self::add_software_attribute(&mut packet, "NetGet TURN/1.0")?;
472        Self::finish_message(&mut packet);
473        Ok(packet)
474    }
475
476    /// Build a Data indication carrying relayed peer traffic to the client
477    /// (RFC 8656 section 11.3). Sent by the relay task, not by an action: the
478    /// data plane must not cost an LLM round-trip per packet.
479    pub(crate) fn build_data_indication(
480        peer_addr: std::net::SocketAddr,
481        payload: &[u8],
482    ) -> Result<Vec<u8>> {
483        // Indications carry their own transaction ID; nothing correlates them.
484        let transaction_id: [u8; 12] = rand::random();
485
486        // 0x0017 = Data Indication (method 7, class 1 = indication)
487        let mut packet = Self::start_message(Self::message_type(7, 1), &transaction_id);
488
489        // XOR-PEER-ADDRESS (0x0012)
490        Self::add_xor_address_attribute(&mut packet, 0x0012, peer_addr, &transaction_id)?;
491
492        // DATA (0x0013)
493        packet.extend_from_slice(&0x0013u16.to_be_bytes());
494        let len = u16::try_from(payload.len())
495            .map_err(|_| anyhow::anyhow!("Relayed payload too large for a DATA attribute"))?;
496        packet.extend_from_slice(&len.to_be_bytes());
497        packet.extend_from_slice(payload);
498        Self::add_padding(&mut packet);
499
500        Self::finish_message(&mut packet);
501        Ok(packet)
502    }
503
504    /// Build a ChannelData frame (RFC 8656 section 12.4).
505    pub(crate) fn build_channel_data(channel_number: u16, payload: &[u8]) -> Vec<u8> {
506        let mut packet = Vec::with_capacity(4 + payload.len() + 3);
507        packet.extend_from_slice(&channel_number.to_be_bytes());
508        packet.extend_from_slice(&(payload.len() as u16).to_be_bytes());
509        packet.extend_from_slice(payload);
510        // ChannelData over UDP need not be padded, but padding is permitted and
511        // keeps the framing identical over any transport.
512        Self::add_padding(&mut packet);
513        packet
514    }
515
516    /// Build TURN error response packet
517    pub(crate) fn build_error_response(
518        transaction_id: &[u8; 12],
519        method: u16,
520        error_code: u16,
521        reason: &str,
522    ) -> Result<Vec<u8>> {
523        // Class = 3 (error response)
524        let mut packet = Self::start_message(Self::message_type(method, 3), transaction_id);
525        Self::add_error_code_attribute(&mut packet, error_code, reason)?;
526        Self::finish_message(&mut packet);
527        Ok(packet)
528    }
529
530    /// Add XOR-ed address attribute (XOR-RELAYED-ADDRESS, XOR-PEER-ADDRESS, etc.)
531    fn add_xor_address_attribute(
532        packet: &mut Vec<u8>,
533        attr_type: u16,
534        addr: std::net::SocketAddr,
535        transaction_id: &[u8],
536    ) -> Result<()> {
537        packet.extend_from_slice(&attr_type.to_be_bytes());
538
539        let attr_start = packet.len();
540        packet.extend_from_slice(&0u16.to_be_bytes()); // Placeholder
541
542        let value_start = packet.len();
543
544        let magic_cookie = 0x2112A442u32;
545
546        match addr {
547            std::net::SocketAddr::V4(addr_v4) => {
548                packet.push(0x00); // Reserved
549                packet.push(0x01); // IPv4
550
551                // XOR port
552                let xor_port = addr_v4.port() ^ (magic_cookie >> 16) as u16;
553                packet.extend_from_slice(&xor_port.to_be_bytes());
554
555                // XOR address
556                let ip_bytes = addr_v4.ip().octets();
557                let magic_bytes = magic_cookie.to_be_bytes();
558                for i in 0..4 {
559                    packet.push(ip_bytes[i] ^ magic_bytes[i]);
560                }
561            }
562            std::net::SocketAddr::V6(addr_v6) => {
563                packet.push(0x00); // Reserved
564                packet.push(0x02); // IPv6
565
566                let xor_port = addr_v6.port() ^ (magic_cookie >> 16) as u16;
567                packet.extend_from_slice(&xor_port.to_be_bytes());
568
569                let ip_bytes = addr_v6.ip().octets();
570                let magic_bytes = magic_cookie.to_be_bytes();
571
572                for i in 0..4 {
573                    packet.push(ip_bytes[i] ^ magic_bytes[i]);
574                }
575                for i in 0..12 {
576                    packet.push(ip_bytes[i + 4] ^ transaction_id[i]);
577                }
578            }
579        }
580
581        let value_length = (packet.len() - value_start) as u16;
582        packet[attr_start..attr_start + 2].copy_from_slice(&value_length.to_be_bytes());
583
584        Self::add_padding(packet);
585        Ok(())
586    }
587
588    /// Add LIFETIME attribute
589    fn add_lifetime_attribute(packet: &mut Vec<u8>, lifetime_seconds: u32) -> Result<()> {
590        // Attribute Type: 0x000D (LIFETIME)
591        packet.extend_from_slice(&0x000Du16.to_be_bytes());
592
593        // Attribute Length: 4 bytes
594        packet.extend_from_slice(&4u16.to_be_bytes());
595
596        // Lifetime value
597        packet.extend_from_slice(&lifetime_seconds.to_be_bytes());
598
599        Ok(())
600    }
601
602    /// Add SOFTWARE attribute
603    fn add_software_attribute(packet: &mut Vec<u8>, software: &str) -> Result<()> {
604        packet.extend_from_slice(&0x8022u16.to_be_bytes());
605
606        let software_bytes = software.as_bytes();
607        let length = software_bytes.len() as u16;
608
609        packet.extend_from_slice(&length.to_be_bytes());
610        packet.extend_from_slice(software_bytes);
611
612        Self::add_padding(packet);
613        Ok(())
614    }
615
616    /// Add ERROR-CODE attribute
617    fn add_error_code_attribute(packet: &mut Vec<u8>, error_code: u16, reason: &str) -> Result<()> {
618        packet.extend_from_slice(&0x0009u16.to_be_bytes());
619
620        let attr_start = packet.len();
621        packet.extend_from_slice(&0u16.to_be_bytes());
622
623        let value_start = packet.len();
624
625        packet.extend_from_slice(&0u16.to_be_bytes()); // Reserved
626        let class = (error_code / 100) as u8;
627        let number = (error_code % 100) as u8;
628        packet.push(class);
629        packet.push(number);
630        packet.extend_from_slice(reason.as_bytes());
631
632        let value_length = (packet.len() - value_start) as u16;
633        packet[attr_start..attr_start + 2].copy_from_slice(&value_length.to_be_bytes());
634
635        Self::add_padding(packet);
636        Ok(())
637    }
638
639    /// Add padding to align to 4-byte boundary
640    fn add_padding(packet: &mut Vec<u8>) {
641        let remainder = packet.len() % 4;
642        if remainder != 0 {
643            let padding = 4 - remainder;
644            packet.extend_from_slice(&vec![0u8; padding]);
645        }
646    }
647}
648
649// Action definitions
650
651fn send_turn_allocate_response_action() -> ActionDefinition {
652    ActionDefinition {
653        name: "send_turn_allocate_response".to_string(),
654        description: "Grant the allocation: NetGet starts relaying on the reserved relay socket \
655                      and tells the client its address"
656            .to_string(),
657        parameters: vec![
658            Parameter {
659                name: "relay_address".to_string(),
660                type_hint: "string".to_string(),
661                description: "MUST be exactly the event's relay_address (a static handler writes \
662                              \"{{event.relay_address}}\"; the value in the example below is only \
663                              a placeholder showing the host:port shape). NetGet has already \
664                              bound this UDP socket; it is the only address peer traffic can \
665                              arrive on. Any other value is refused with a 508 error, because a \
666                              relay address nobody listens on silently breaks the client."
667                    .to_string(),
668                required: true,
669            },
670            Parameter {
671                name: "transaction_id".to_string(),
672                type_hint: "string".to_string(),
673                description: "Transaction ID from the request: exactly 24 hex characters \
674                              (12 bytes, RFC 8489). A static handler writes \
675                              \"{{event.transaction_id}}\"; an LLM answer must carry the \
676                              event's actual value, because the client discards a response \
677                              whose transaction ID does not match."
678                    .to_string(),
679                required: true,
680            },
681            Parameter {
682                name: "client_address".to_string(),
683                type_hint: "string".to_string(),
684                description: "Client's own address as seen by the server, i.e. \
685                              \"{{event.peer_addr}}\". Sent back as XOR-MAPPED-ADDRESS."
686                    .to_string(),
687                required: false,
688            },
689            Parameter {
690                name: "lifetime_seconds".to_string(),
691                type_hint: "number".to_string(),
692                description: "Allocation lifetime in seconds. Default: 600, capped at 3600. The \
693                              relay socket is closed when it expires."
694                    .to_string(),
695                required: false,
696            },
697            Parameter {
698                name: "allocation_id".to_string(),
699                type_hint: "string".to_string(),
700                description: "Unique allocation identifier. Defaults to transaction_id".to_string(),
701                required: false,
702            },
703        ],
704        // Concrete values, not "{{event.…}}" placeholders: that substitution only
705        // happens for static event handlers, so a model that copies the template
706        // literally puts the braces on the wire and the executor refuses them.
707        // Both addresses must come from the event; the transaction ID is 12 bytes.
708        example: json!({
709            "type": "send_turn_allocate_response",
710            "relay_address": "192.0.2.10:49160",
711            "client_address": "198.51.100.20:54321",
712            "transaction_id": "0123456789abcdef01234567",
713            "lifetime_seconds": 600
714        }),
715        log_template: Some(
716            LogTemplate::new()
717                .with_info("-> TURN allocate {relay_address}")
718                .with_debug(
719                    "TURN allocate_response: relay={relay_address}, lifetime={lifetime_seconds}s",
720                ),
721        ),
722    }
723}
724
725fn send_turn_refresh_response_action() -> ActionDefinition {
726    ActionDefinition {
727        name: "send_turn_refresh_response".to_string(),
728        description: "Send TURN refresh response to extend allocation lifetime".to_string(),
729        parameters: vec![
730            Parameter {
731                name: "transaction_id".to_string(),
732                type_hint: "string".to_string(),
733                description: "Transaction ID from the request: exactly 24 hex characters \
734                              (12 bytes, RFC 8489). A static handler writes \
735                              \"{{event.transaction_id}}\"; an LLM answer must carry the \
736                              event's actual value, because the client discards a response \
737                              whose transaction ID does not match."
738                    .to_string(),
739                required: true,
740            },
741            Parameter {
742                name: "lifetime_seconds".to_string(),
743                type_hint: "number".to_string(),
744                description: "New lifetime in seconds. Default: 600".to_string(),
745                required: false,
746            },
747        ],
748        example: json!({
749            "type": "send_turn_refresh_response",
750            "transaction_id": "0123456789abcdef01234567",
751            "lifetime_seconds": 600
752        }),
753        log_template: Some(
754            LogTemplate::new()
755                .with_info("-> TURN refresh ({lifetime_seconds}s)")
756                .with_debug("TURN refresh_response: lifetime={lifetime_seconds}s"),
757        ),
758    }
759}
760
761fn send_turn_create_permission_response_action() -> ActionDefinition {
762    ActionDefinition {
763        name: "send_turn_create_permission_response".to_string(),
764        description: "Permit peers to exchange relayed traffic with this client. Only permitted \
765                      peers are relayed in either direction."
766            .to_string(),
767        parameters: vec![
768            Parameter {
769                name: "transaction_id".to_string(),
770                type_hint: "string".to_string(),
771                description: "Transaction ID from the request: exactly 24 hex characters \
772                              (12 bytes, RFC 8489). A static handler writes \
773                              \"{{event.transaction_id}}\"; an LLM answer must carry the \
774                              event's actual value, because the client discards a response \
775                              whose transaction ID does not match."
776                    .to_string(),
777                required: true,
778            },
779            Parameter {
780                name: "peer_addresses".to_string(),
781                type_hint: "array".to_string(),
782                description: "Subset of the request's peer_addresses to permit, e.g. \
783                              [\"198.51.100.10:5000\"]. Omit to permit every peer the request \
784                              named. Addresses the request did not name are ignored. Permissions \
785                              match on IP address (RFC 8656) and last 5 minutes."
786                    .to_string(),
787                required: false,
788            },
789        ],
790        example: json!({
791            "type": "send_turn_create_permission_response",
792            "transaction_id": "0123456789abcdef01234567"
793        }),
794        log_template: Some(
795            LogTemplate::new()
796                .with_info("-> TURN permission created")
797                .with_debug("TURN create_permission_response"),
798        ),
799    }
800}
801
802fn send_turn_channel_bind_response_action() -> ActionDefinition {
803    ActionDefinition {
804        name: "send_turn_channel_bind_response".to_string(),
805        description: "Bind the requested channel number to the requested peer. Traffic to and \
806                      from that peer is then framed as ChannelData instead of Send/Data \
807                      indications, and the peer is permitted."
808            .to_string(),
809        parameters: vec![Parameter {
810            name: "transaction_id".to_string(),
811            type_hint: "string".to_string(),
812            description: "Transaction ID from the request: exactly 24 hex characters \
813                          (12 bytes, RFC 8489). A static handler writes \
814                          \"{{event.transaction_id}}\"; an LLM answer must carry the event's \
815                          actual value, because the client discards a response whose \
816                          transaction ID does not match."
817                .to_string(),
818            required: true,
819        }],
820        example: json!({
821            "type": "send_turn_channel_bind_response",
822            "transaction_id": "0123456789abcdef01234567"
823        }),
824        log_template: Some(
825            LogTemplate::new()
826                .with_info("-> TURN channel bound")
827                .with_debug("TURN channel_bind_response"),
828        ),
829    }
830}
831
832fn send_turn_error_response_action() -> ActionDefinition {
833    ActionDefinition {
834        name: "send_turn_error_response".to_string(),
835        description: "Send TURN error response".to_string(),
836        parameters: vec![
837            Parameter {
838                name: "error_code".to_string(),
839                type_hint: "number".to_string(),
840                description: "TURN error code (e.g., 400, 401, 403, 508)".to_string(),
841                required: true,
842            },
843            Parameter {
844                name: "reason".to_string(),
845                type_hint: "string".to_string(),
846                description: "Error reason phrase".to_string(),
847                required: true,
848            },
849            Parameter {
850                name: "transaction_id".to_string(),
851                type_hint: "string".to_string(),
852                description: "Transaction ID from the request, hex-encoded (24 hex chars). Must match the request or the client will discard the response.".to_string(),
853                required: true,
854            },
855            Parameter {
856                name: "method".to_string(),
857                type_hint: "string".to_string(),
858                description: "Which request this error answers: \"allocate\" (default), \"refresh\", \"create_permission\" or \"channel_bind\". Must match the request's method or the client ignores the error.".to_string(),
859                required: false,
860            },
861        ],
862        example: json!({
863            "type": "send_turn_error_response",
864            "error_code": 508,
865            "reason": "Insufficient Capacity",
866            "transaction_id": "0123456789abcdef01234567",
867            "method": "allocate"
868        }),
869        log_template: Some(
870            LogTemplate::new()
871                .with_info("-> TURN error {error_code}: {reason}")
872                .with_debug("TURN error_response: {error_code} {reason}"),
873        ),
874    }
875}
876
877fn ignore_request_action() -> ActionDefinition {
878    ActionDefinition {
879        name: "ignore_request".to_string(),
880        description: "Silently ignore the TURN request (no response)".to_string(),
881        parameters: vec![],
882        example: json!({
883            "type": "ignore_request"
884        }),
885        log_template: Some(
886            LogTemplate::new()
887                .with_info("TURN request ignored")
888                .with_debug("TURN ignore_request"),
889        ),
890    }
891}
892
893// Event types
894
895/// Fields every TURN event carries.
896fn common_event_parameters() -> Vec<Parameter> {
897    vec![
898        Parameter {
899            name: "transaction_id".to_string(),
900            type_hint: "string".to_string(),
901            description: "STUN/TURN transaction ID from the request, hex-encoded (24 hex chars = 12 bytes). MUST be copied into the response: clients discard any reply whose transaction ID differs from the request they sent.".to_string(),
902            required: true,
903        },
904        Parameter {
905            // The name says "peer" and the value is the *client*. Renaming it would break
906            // every handler interpolating {{event.peer_addr}}, so the description has to carry
907            // the whole warning - and it must, because on turn_create_permission_request this
908            // field sits directly beside `peer_addresses`, which really is the peer list. A
909            // model asked to permit "the peer" reads the singular, more obvious-looking name
910            // and permits the client's own address instead of the one it was asked about.
911            name: "peer_addr".to_string(),
912            type_hint: "string".to_string(),
913            description: "The CLIENT's IP:port as seen by the server - despite the name, this is never a peer to relay to. On a create-permission or channel-bind request, the peer the client is asking about is in `peer_addresses`, not here."
914                .to_string(),
915            required: true,
916        },
917        Parameter {
918            name: "local_addr".to_string(),
919            type_hint: "string".to_string(),
920            description: "Server's listening IP:port".to_string(),
921            required: true,
922        },
923        Parameter {
924            name: "message_type".to_string(),
925            type_hint: "string".to_string(),
926            description: "Decoded TURN message type, e.g. \"AllocateRequest\"".to_string(),
927            required: true,
928        },
929        Parameter {
930            name: "bytes_received".to_string(),
931            type_hint: "number".to_string(),
932            description: "Size of the received datagram in bytes".to_string(),
933            required: true,
934        },
935        Parameter {
936            name: "existing_allocations".to_string(),
937            type_hint: "array".to_string(),
938            description: "Allocations currently held by this client: allocation_id, relay_address, lifetime_seconds, expires_in_seconds, permitted_peers, channels".to_string(),
939            required: true,
940        },
941    ]
942}
943
944fn with_extra(mut params: Vec<Parameter>, extra: Vec<Parameter>) -> Vec<Parameter> {
945    params.extend(extra);
946    params
947}
948
949pub static TURN_ALLOCATE_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
950    EventType::new(
951        "turn_allocate_request",
952        "TURN allocate request received from client",
953        // Concrete values, not "{{event.…}}" placeholders: that substitution only happens
954        // for a static event handler, so a model that copies the template literally puts
955        // the braces on the wire and the executor refuses them. The action examples in
956        // this file were already corrected for exactly this reason; these event templates
957        // were the other half of the same fix and were missed.
958        json!({
959            "type": "send_turn_allocate_response",
960            "relay_address": "192.0.2.10:49160",
961            "client_address": "198.51.100.20:54321",
962            "transaction_id": "0123456789abcdef01234567",
963            "lifetime_seconds": 600
964        }),
965    )
966    .with_parameters(with_extra(
967        common_event_parameters(),
968        vec![
969            Parameter {
970                name: "relay_address".to_string(),
971                type_hint: "string".to_string(),
972                description: "IP:port of the UDP relay socket NetGet has already bound for this request. Copy it verbatim into send_turn_allocate_response: it is where peers will send traffic for this client, and no other address will work. The socket is closed again if the allocation is not granted.".to_string(),
973                required: true,
974            },
975            Parameter {
976                name: "requested_lifetime_seconds".to_string(),
977                type_hint: "number".to_string(),
978                description: "Lifetime the client asked for (LIFETIME attribute), or null if it did not ask. You are free to grant less.".to_string(),
979                required: false,
980            },
981            Parameter {
982                name: "requested_transport".to_string(),
983                type_hint: "string".to_string(),
984                description: "Transport the client asked to relay: \"udp\", \"tcp\", or null. Only UDP is relayed; TCP requests are refused with 442 before this event fires.".to_string(),
985                required: false,
986            },
987        ],
988    ))
989    .with_actions(vec![
990        send_turn_allocate_response_action(),
991        send_turn_error_response_action(),
992        ignore_request_action(),
993    ])
994    .with_log_template(
995        LogTemplate::new()
996            .with_info("TURN allocate request")
997            .with_debug("TURN allocate request")
998            .with_trace("TURN allocate: {json_pretty(.)}"),
999    )
1000});
1001
1002pub static TURN_REFRESH_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1003    EventType::new(
1004        "turn_refresh_request",
1005        "TURN refresh request received from client",
1006        // Concrete values, not "{{event.…}}" placeholders: that substitution only happens
1007        // for a static event handler, so a model that copies the template literally puts
1008        // the braces on the wire and the executor refuses them. The action examples in
1009        // this file were already corrected for exactly this reason; these event templates
1010        // were the other half of the same fix and were missed.
1011        json!({
1012            "type": "send_turn_refresh_response",
1013            "transaction_id": "0123456789abcdef01234567",
1014            "lifetime_seconds": 600
1015        }),
1016    )
1017    .with_parameters(with_extra(
1018        common_event_parameters(),
1019        vec![Parameter {
1020            name: "requested_lifetime_seconds".to_string(),
1021            type_hint: "number".to_string(),
1022            description: "Lifetime the client asked for, or null. Zero means the client wants the allocation deleted; answering with lifetime_seconds 0 closes the relay socket.".to_string(),
1023            required: false,
1024        }],
1025    ))
1026    .with_actions(vec![
1027        send_turn_refresh_response_action(),
1028        send_turn_error_response_action(),
1029        ignore_request_action(),
1030    ])
1031    .with_log_template(
1032        LogTemplate::new()
1033            .with_info("TURN refresh request")
1034            .with_debug("TURN refresh request")
1035            .with_trace("TURN refresh: {json_pretty(.)}"),
1036    )
1037});
1038
1039pub static TURN_CREATE_PERMISSION_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1040    EventType::new(
1041        "turn_create_permission_request",
1042        "TURN create permission request received from client",
1043        // Concrete values, not "{{event.…}}" placeholders: that substitution only happens
1044        // for a static event handler, so a model that copies the template literally puts
1045        // the braces on the wire and the executor refuses them. The action examples in
1046        // this file were already corrected for exactly this reason; these event templates
1047        // were the other half of the same fix and were missed.
1048        json!({
1049            "type": "send_turn_create_permission_response",
1050            "transaction_id": "0123456789abcdef01234567"
1051        }),
1052    )
1053    .with_parameters(with_extra(
1054        common_event_parameters(),
1055        vec![Parameter {
1056            name: "peer_addresses".to_string(),
1057            type_hint: "array".to_string(),
1058            description: "Peer IP:port values the client asks permission for - these, and not `peer_addr` (which is the client itself), are what the response must permit. Until a peer is permitted, nothing is relayed to or from it."
1059                .to_string(),
1060            required: true,
1061        }],
1062    ))
1063    .with_actions(vec![
1064        send_turn_create_permission_response_action(),
1065        send_turn_error_response_action(),
1066        ignore_request_action(),
1067    ])
1068    .with_log_template(
1069        LogTemplate::new()
1070            .with_info("TURN create permission")
1071            .with_debug("TURN create permission")
1072            .with_trace("TURN create permission: {json_pretty(.)}"),
1073    )
1074});
1075
1076pub static TURN_CHANNEL_BIND_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1077    EventType::new(
1078        "turn_channel_bind_request",
1079        "TURN channel bind request received from client",
1080        // Concrete values, not "{{event.…}}" placeholders: that substitution only happens
1081        // for a static event handler, so a model that copies the template literally puts
1082        // the braces on the wire and the executor refuses them. The action examples in
1083        // this file were already corrected for exactly this reason; these event templates
1084        // were the other half of the same fix and were missed.
1085        json!({
1086            "type": "send_turn_channel_bind_response",
1087            "transaction_id": "0123456789abcdef01234567"
1088        }),
1089    )
1090    .with_parameters(with_extra(
1091        common_event_parameters(),
1092        vec![
1093            Parameter {
1094                name: "channel_number".to_string(),
1095                type_hint: "number".to_string(),
1096                description: "Channel number the client wants bound (0x4000-0x7FFF). Out-of-range values are refused with 400 before this event fires.".to_string(),
1097                required: true,
1098            },
1099            Parameter {
1100                name: "peer_address".to_string(),
1101                type_hint: "string".to_string(),
1102                description: "Peer IP:port to bind the channel to. Granting the bind also permits that peer.".to_string(),
1103                required: true,
1104            },
1105        ],
1106    ))
1107    .with_actions(vec![
1108        send_turn_channel_bind_response_action(),
1109        send_turn_error_response_action(),
1110        ignore_request_action(),
1111    ])
1112    .with_log_template(
1113        LogTemplate::new()
1114            .with_info("TURN channel bind request")
1115            .with_debug("TURN channel bind request")
1116            .with_trace("TURN channel bind: {json_pretty(.)}"),
1117    )
1118});
1119
1120fn get_turn_event_types() -> Vec<EventType> {
1121    vec![
1122        TURN_ALLOCATE_REQUEST_EVENT.clone(),
1123        TURN_REFRESH_REQUEST_EVENT.clone(),
1124        TURN_CREATE_PERMISSION_REQUEST_EVENT.clone(),
1125        TURN_CHANNEL_BIND_REQUEST_EVENT.clone(),
1126    ]
1127}