1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static TURN_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "turn_connected",
18 "TURN client successfully connected to server",
19 json!({
20 "type": "allocate_turn_relay",
21 "lifetime_seconds": 600
22 }),
23 )
24 .with_parameters(vec![Parameter {
25 name: "remote_addr".to_string(),
26 type_hint: "string".to_string(),
27 description: "TURN server address".to_string(),
28 required: true,
29 }])
30});
31
32pub static TURN_CLIENT_ALLOCATED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
34 EventType::new(
35 "turn_allocated",
36 "TURN relay address allocated successfully",
37 json!({
38 "type": "create_permission",
39 "peer_address": "192.168.1.100:5000"
40 }),
41 )
42 .with_parameters(vec![
43 Parameter {
44 name: "relay_address".to_string(),
45 type_hint: "string".to_string(),
46 description: "The allocated relay address (IP:port)".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "lifetime_seconds".to_string(),
51 type_hint: "number".to_string(),
52 description: "Allocation lifetime in seconds".to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "transaction_id".to_string(),
57 type_hint: "string".to_string(),
58 description: "Transaction ID (hex)".to_string(),
59 required: true,
60 },
61 ])
62});
63
64pub static TURN_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
66 EventType::new(
67 "turn_data_received",
68 "Data received from peer via TURN relay",
69 json!({
70 "type": "send_turn_data",
71 "peer_address": "192.168.1.100:5000",
72 "data_hex": "48656c6c6f"
73 }),
74 )
75 .with_parameters(vec![
76 Parameter {
77 name: "peer_address".to_string(),
78 type_hint: "string".to_string(),
79 description: "Peer address that sent the data".to_string(),
80 required: true,
81 },
82 Parameter {
83 name: "data_hex".to_string(),
84 type_hint: "string".to_string(),
85 description: "The data received (as hex string)".to_string(),
86 required: true,
87 },
88 Parameter {
89 name: "data_length".to_string(),
90 type_hint: "number".to_string(),
91 description: "Length of data in bytes".to_string(),
92 required: true,
93 },
94 ])
95});
96
97pub static TURN_CLIENT_PERMISSION_CREATED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
99 EventType::new(
100 "turn_permission_created",
101 "Permission created for peer address",
102 json!({
103 "type": "wait_for_more"
104 }),
105 )
106 .with_parameters(vec![
107 Parameter {
108 name: "peer_address".to_string(),
109 type_hint: "string".to_string(),
110 description: "Peer address granted permission. Recovered by correlating the \
111 response's transaction ID with the CreatePermission request that \
112 named this peer — RFC 8656 section 9.4 makes the Success Response \
113 empty, so the server does not repeat it. A response whose \
114 transaction ID matches nothing this client sent raises no event at \
115 all rather than reporting a peer it cannot name."
116 .to_string(),
117 required: true,
118 },
119 Parameter {
120 name: "transaction_id".to_string(),
121 type_hint: "string".to_string(),
122 description: "Transaction ID of the CreatePermission exchange (hex)".to_string(),
123 required: true,
124 },
125 ])
126});
127
128pub static TURN_CLIENT_REFRESHED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
130 EventType::new(
131 "turn_refreshed",
132 "TURN allocation lifetime extended",
133 json!({
136 "type": "refresh_allocation",
137 "lifetime_seconds": 600
138 }),
139 )
140 .with_parameters(vec![Parameter {
141 name: "lifetime_seconds".to_string(),
142 type_hint: "number".to_string(),
143 description: "New lifetime in seconds".to_string(),
144 required: true,
145 }])
146});
147
148pub struct TurnClientProtocol;
150
151impl TurnClientProtocol {
152 pub fn new() -> Self {
153 Self
154 }
155}
156
157impl Protocol for TurnClientProtocol {
159 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
160 vec![
161 ActionDefinition {
162 name: "allocate_turn_relay".to_string(),
163 description: "Request a relay address allocation from TURN server".to_string(),
164 parameters: vec![Parameter {
165 name: "lifetime_seconds".to_string(),
166 type_hint: "number".to_string(),
167 description: "Requested lifetime in seconds (default: 600)".to_string(),
168 required: false,
169 }],
170 example: json!({
171 "type": "allocate_turn_relay",
172 "lifetime_seconds": 600
173 }),
174 log_template: None,
175 },
176 ActionDefinition {
177 name: "create_permission".to_string(),
178 description: "Create permission for a peer address to send/receive data"
179 .to_string(),
180 parameters: vec![Parameter {
181 name: "peer_address".to_string(),
182 type_hint: "string".to_string(),
183 description: "Peer IP:port to grant permission (e.g., '192.168.1.100:5000')"
184 .to_string(),
185 required: true,
186 }],
187 example: json!({
188 "type": "create_permission",
189 "peer_address": "192.168.1.100:5000"
190 }),
191 log_template: None,
192 },
193 ActionDefinition {
194 name: "send_turn_data".to_string(),
195 description: "Send data to peer via TURN relay".to_string(),
196 parameters: vec![
197 Parameter {
198 name: "peer_address".to_string(),
199 type_hint: "string".to_string(),
200 description: "Peer IP:port to send data to".to_string(),
201 required: true,
202 },
203 Parameter {
204 name: "data_hex".to_string(),
205 type_hint: "string".to_string(),
206 description: "Data to send (as hex string)".to_string(),
207 required: true,
208 },
209 ],
210 example: json!({
211 "type": "send_turn_data",
212 "peer_address": "192.168.1.100:5000",
213 "data_hex": "48656c6c6f"
214 }),
215 log_template: None,
216 },
217 ActionDefinition {
218 name: "refresh_allocation".to_string(),
219 description: "Refresh TURN allocation to extend lifetime".to_string(),
220 parameters: vec![Parameter {
221 name: "lifetime_seconds".to_string(),
222 type_hint: "number".to_string(),
223 description: "New lifetime in seconds (0 to delete allocation)".to_string(),
224 required: false,
225 }],
226 example: json!({
227 "type": "refresh_allocation",
228 "lifetime_seconds": 600
229 }),
230 log_template: None,
231 },
232 ActionDefinition {
233 name: "disconnect".to_string(),
234 description: "Disconnect from TURN server".to_string(),
235 parameters: vec![],
236 example: json!({
237 "type": "disconnect"
238 }),
239 log_template: None,
240 },
241 ]
242 }
243 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
244 vec![
245 ActionDefinition {
246 name: "send_turn_data".to_string(),
247 description: "Send data to peer via TURN relay in response to received data"
248 .to_string(),
249 parameters: vec![
250 Parameter {
251 name: "peer_address".to_string(),
252 type_hint: "string".to_string(),
253 description: "Peer IP:port to send data to".to_string(),
254 required: true,
255 },
256 Parameter {
257 name: "data_hex".to_string(),
258 type_hint: "string".to_string(),
259 description: "Data to send (as hex string)".to_string(),
260 required: true,
261 },
262 ],
263 example: json!({
264 "type": "send_turn_data",
265 "peer_address": "192.168.1.100:5000",
266 "data_hex": "48656c6c6f"
267 }),
268 log_template: None,
269 },
270 ActionDefinition {
271 name: "wait_for_more".to_string(),
272 description: "Wait for more data before responding".to_string(),
273 parameters: vec![],
274 example: json!({
275 "type": "wait_for_more"
276 }),
277 log_template: None,
278 },
279 ]
280 }
281 fn protocol_name(&self) -> &'static str {
282 "TURN"
283 }
284 fn get_event_types(&self) -> Vec<EventType> {
295 vec![
296 TURN_CLIENT_CONNECTED_EVENT.clone(),
297 TURN_CLIENT_ALLOCATED_EVENT.clone(),
298 TURN_CLIENT_DATA_RECEIVED_EVENT.clone(),
299 TURN_CLIENT_PERMISSION_CREATED_EVENT.clone(),
300 TURN_CLIENT_REFRESHED_EVENT.clone(),
301 ]
302 }
303 fn stack_name(&self) -> &'static str {
304 "ETH>IP>UDP>STUN/TURN"
305 }
306 fn keywords(&self) -> Vec<&'static str> {
307 vec!["turn", "turn client", "relay", "nat traversal", "webrtc"]
308 }
309 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
310 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
311
312 ProtocolMetadataV2::builder()
313 .state(DevelopmentState::Experimental)
314 .implementation(
318 "Manual TURN client (RFC 8656) on the STUN message format — Allocate, \
319 Refresh, CreatePermission and Send indications are built and parsed here, \
320 not by a library",
321 )
322 .llm_control("Full control over allocations, permissions, and relay data")
323 .e2e_testing(
324 "NetGet TURN server as test server; tests/client/turn/command_channel_test.rs \
325 asserts the datagrams reach a real socket and \
326 tests/client/turn/response_parsing_test.rs asserts an Allocate Success \
327 Response is decoded and that malformed ones do not kill the read loop",
328 )
329 .notes(
330 "remote_addr must be a literal IP:port — it is parsed with \
331 `SocketAddr::parse` and hostnames are rejected. No authentication: \
332 MESSAGE-INTEGRITY, USERNAME, REALM and NONCE are not implemented, so this \
333 cannot talk to a public TURN service, only to one that grants without \
334 credentials. Responses are correlated by transaction ID only for \
335 CreatePermission; an Allocate or Refresh response from any source that can \
336 reach the client's ephemeral port is accepted, so do not point this at an \
337 untrusted network. UDP allocations only: no TCP, no TLS, no ChannelBind, and \
338 no automatic refresh — the model must refresh before the lifetime expires.",
339 )
340 .build()
341 }
342 fn description(&self) -> &'static str {
343 "TURN client for NAT traversal relay"
344 }
345 fn example_prompt(&self) -> &'static str {
346 "Connect to TURN server at 127.0.0.1:3478 and allocate a relay address"
347 }
348 fn group_name(&self) -> &'static str {
349 "Network Infrastructure"
350 }
351 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
352 use crate::llm::actions::StartupExamples;
353 use serde_json::json;
354
355 StartupExamples::new(
356 json!({
358 "type": "open_client",
359 "remote_addr": "127.0.0.1:3478",
360 "base_stack": "turn",
361 "instruction": "Allocate a relay address and create permission for peer 192.168.1.100:5000"
362 }),
363 json!({
365 "type": "open_client",
366 "remote_addr": "127.0.0.1:3478",
367 "base_stack": "turn",
368 "event_handlers": [{
369 "event_pattern": "turn_data_received",
370 "handler": {
371 "type": "script",
372 "language": "python",
373 "code": "<turn_client_handler>"
374 }
375 }]
376 }),
377 json!({
379 "type": "open_client",
380 "remote_addr": "127.0.0.1:3478",
381 "base_stack": "turn",
382 "event_handlers": [
383 {
384 "event_pattern": "turn_connected",
385 "handler": {
386 "type": "static",
387 "actions": [{
388 "type": "allocate_turn_relay",
389 "lifetime_seconds": 600
390 }]
391 }
392 },
393 {
394 "event_pattern": "turn_allocated",
395 "handler": {
396 "type": "static",
397 "actions": [{
398 "type": "create_permission",
399 "peer_address": "192.168.1.100:5000"
400 }]
401 }
402 }
403 ]
404 }),
405 )
406 }
407}
408
409impl Client for TurnClientProtocol {
411 fn connect(
412 &self,
413 ctx: crate::protocol::ConnectContext,
414 ) -> std::pin::Pin<
415 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
416 > {
417 Box::pin(async move {
418 use crate::client::turn::TurnClient;
419 TurnClient::connect_with_llm_actions(
420 ctx.remote_addr,
421 ctx.llm_client,
422 ctx.state,
423 ctx.status_tx,
424 ctx.client_id,
425 )
426 .await
427 })
428 }
429 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
430 let action_type = action
431 .get("type")
432 .and_then(|v| v.as_str())
433 .context("Missing 'type' field in action")?;
434
435 match action_type {
436 "allocate_turn_relay" => {
437 let lifetime = action
438 .get("lifetime_seconds")
439 .and_then(|v| v.as_u64())
440 .unwrap_or(600);
441
442 Ok(ClientActionResult::Custom {
443 name: "allocate".to_string(),
444 data: json!({
445 "lifetime_seconds": lifetime
446 }),
447 })
448 }
449 "create_permission" => {
450 let peer_address = action
451 .get("peer_address")
452 .and_then(|v| v.as_str())
453 .context("Missing 'peer_address' field")?;
454
455 Ok(ClientActionResult::Custom {
456 name: "create_permission".to_string(),
457 data: json!({
458 "peer_address": peer_address
459 }),
460 })
461 }
462 "send_turn_data" => {
463 let peer_address = action
464 .get("peer_address")
465 .and_then(|v| v.as_str())
466 .context("Missing 'peer_address' field")?;
467
468 let data_hex = action
469 .get("data_hex")
470 .and_then(|v| v.as_str())
471 .context("Missing 'data_hex' field")?;
472
473 let data = hex::decode(data_hex).context("Invalid hex data")?;
474
475 Ok(ClientActionResult::Custom {
476 name: "send_indication".to_string(),
477 data: json!({
478 "peer_address": peer_address,
479 "data": data
480 }),
481 })
482 }
483 "refresh_allocation" => {
484 let lifetime = action
485 .get("lifetime_seconds")
486 .and_then(|v| v.as_u64())
487 .unwrap_or(600);
488
489 Ok(ClientActionResult::Custom {
490 name: "refresh".to_string(),
491 data: json!({
492 "lifetime_seconds": lifetime
493 }),
494 })
495 }
496 "disconnect" => Ok(ClientActionResult::Disconnect),
497 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
498 _ => Err(anyhow::anyhow!(
499 "Unknown TURN client action: {}",
500 action_type
501 )),
502 }
503 }
504}