1use 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
28impl Protocol for TurnProtocol {
30 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
31 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()
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 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 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 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 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
193impl 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 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 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 let relay_addr: std::net::SocketAddr = relay_address
282 .parse()
283 .context("Invalid relay_address format")?;
284
285 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 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 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 fn execute_send_permission_response(&self, action: serde_json::Value) -> Result<ActionResult> {
323 let transaction_id = Self::transaction_id_from(&action)?;
324
325 let packet = Self::build_permission_response(&transaction_id)?;
328
329 Ok(ActionResult::Output(packet))
330 }
331
332 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 let packet = Self::build_success_response(&transaction_id, 9)?;
342
343 Ok(ActionResult::Output(packet))
344 }
345
346 fn execute_send_error_response(&self, action: serde_json::Value) -> Result<ActionResult> {
348 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 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 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 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()); packet.extend_from_slice(&0x2112A442u32.to_be_bytes());
407 packet.extend_from_slice(transaction_id);
408 packet
409 }
410
411 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 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 let mut packet = Self::start_message(Self::message_type(3, 2), transaction_id);
426
427 Self::add_xor_address_attribute(&mut packet, 0x0016, relay_addr, transaction_id)?;
429
430 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 fn build_refresh_response(transaction_id: &[u8; 12], lifetime_seconds: u32) -> Result<Vec<u8>> {
444 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 fn build_permission_response(transaction_id: &[u8; 12]) -> Result<Vec<u8>> {
453 Self::build_success_response(transaction_id, 8)
454 }
455
456 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 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 pub(crate) fn build_data_indication(
480 peer_addr: std::net::SocketAddr,
481 payload: &[u8],
482 ) -> Result<Vec<u8>> {
483 let transaction_id: [u8; 12] = rand::random();
485
486 let mut packet = Self::start_message(Self::message_type(7, 1), &transaction_id);
488
489 Self::add_xor_address_attribute(&mut packet, 0x0012, peer_addr, &transaction_id)?;
491
492 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 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 Self::add_padding(&mut packet);
513 packet
514 }
515
516 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 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 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()); 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); packet.push(0x01); let xor_port = addr_v4.port() ^ (magic_cookie >> 16) as u16;
553 packet.extend_from_slice(&xor_port.to_be_bytes());
554
555 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); packet.push(0x02); 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 fn add_lifetime_attribute(packet: &mut Vec<u8>, lifetime_seconds: u32) -> Result<()> {
590 packet.extend_from_slice(&0x000Du16.to_be_bytes());
592
593 packet.extend_from_slice(&4u16.to_be_bytes());
595
596 packet.extend_from_slice(&lifetime_seconds.to_be_bytes());
598
599 Ok(())
600 }
601
602 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 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()); 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 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
649fn 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 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
893fn 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 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 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 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 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 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}