1pub mod actions;
3
4pub use actions::StunClientProtocol;
5
6use anyhow::{Context, Result};
7use std::net::SocketAddr;
8use std::sync::Arc;
9use tokio::sync::mpsc;
10use tracing::{error, info};
11
12use crate::client::llm_budget::call_llm_for_client;
13use crate::client::stun::actions::{
14 STUN_CLIENT_BINDING_RESPONSE_EVENT, STUN_CLIENT_CONNECTED_EVENT,
15};
16use crate::llm::actions::client_trait::ClientActionResult;
17use crate::llm::ollama_client::OllamaClient;
18use crate::llm::ClientLlmResult;
19use crate::logging::emit::Log;
20use crate::protocol::Event;
21use crate::state::app_state::AppState;
22use crate::state::client_handles::{ClientCommand, ClientSendOutcome};
23use crate::state::{AccessLogOwner, ClientId, ClientStatus};
24
25pub struct StunClient;
27
28impl StunClient {
29 pub async fn connect_with_llm_actions(
31 remote_addr: String,
32 llm_client: OllamaClient,
33 app_state: Arc<AppState>,
34 status_tx: mpsc::UnboundedSender<String>,
35 client_id: ClientId,
36 ) -> Result<SocketAddr> {
37 info!("STUN client {} initializing for {}", client_id, remote_addr);
38
39 let local_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
41 let udp_socket = tokio::net::UdpSocket::bind(local_addr)
42 .await
43 .context("Failed to bind UDP socket")?;
44
45 let bound_addr = udp_socket.local_addr()?;
46 info!(
47 "STUN client {} bound to local address {}",
48 client_id, bound_addr
49 );
50
51 app_state
53 .with_client_mut(client_id, |client| {
54 client
55 .set_protocol_field("stun_server".to_string(), serde_json::json!(remote_addr));
56 client.set_protocol_field(
57 "local_addr".to_string(),
58 serde_json::json!(bound_addr.to_string()),
59 );
60 })
61 .await;
62
63 app_state
65 .update_client_status(client_id, ClientStatus::Connected)
66 .await;
67 Log::new(Some(&status_tx)).info(format!(
68 "STUN client {} ready for {}",
69 client_id, remote_addr
70 ));
71 let _ = status_tx.send("__UPDATE_UI__".to_string());
72
73 let command_rx =
79 crate::client::command_support::register_command_channel(&app_state, client_id).await;
80
81 if let Some(instruction) = app_state.get_instruction_for_client(client_id).await {
83 let protocol = Arc::new(StunClientProtocol::new());
84 let event = Event::new(
85 &STUN_CLIENT_CONNECTED_EVENT,
86 serde_json::json!({
87 "local_addr": bound_addr.to_string(),
88 "stun_server": remote_addr,
89 }),
90 );
91
92 let llm_clone = llm_client.clone();
93 let app_state_clone = app_state.clone();
94 let status_tx_clone = status_tx.clone();
95
96 let task_registrar = app_state.clone();
99 let task_handle = tokio::spawn(async move {
100 match call_llm_for_client(
101 &llm_clone,
102 &app_state_clone,
103 client_id.to_string(),
104 &instruction,
105 "",
106 Some(&event),
107 protocol.as_ref(),
108 &status_tx_clone,
109 )
110 .await
111 {
112 Ok(ClientLlmResult {
113 actions,
114 memory_updates,
115 }) => {
116 if let Some(mem) = memory_updates {
118 app_state_clone.set_memory_for_client(client_id, mem).await;
119 }
120
121 for action in actions {
123 if let Err(e) = Self::execute_stun_action(
124 client_id,
125 action,
126 app_state_clone.clone(),
127 llm_clone.clone(),
128 status_tx_clone.clone(),
129 )
130 .await
131 {
132 error!("Failed to execute STUN action: {}", e);
133 }
134 }
135 }
136 Err(e) => {
137 error!("LLM error for STUN client {}: {}", client_id, e);
138 }
139 }
140 });
141 task_registrar
142 .register_client_task(client_id, task_handle)
143 .await;
144 }
145
146 let task_registrar = app_state.clone();
152 let task_handle = tokio::spawn(async move {
153 Self::command_loop(command_rx, client_id, llm_client, app_state, status_tx).await;
154 });
155 task_registrar
156 .register_client_task(client_id, task_handle)
157 .await;
158
159 Ok(bound_addr)
160 }
161
162 async fn execute_stun_action(
164 client_id: ClientId,
165 action: serde_json::Value,
166 app_state: Arc<AppState>,
167 llm_client: OllamaClient,
168 status_tx: mpsc::UnboundedSender<String>,
169 ) -> Result<()> {
170 use crate::llm::actions::client_trait::Client;
171 let protocol = Arc::new(StunClientProtocol::new());
172
173 match protocol.as_ref().execute_action(action)? {
174 crate::llm::actions::client_trait::ClientActionResult::Custom { name, data: _ } => {
175 if name == "send_binding_request" {
176 Self::send_binding_request(client_id, app_state, llm_client, status_tx).await?;
177 }
178 }
179 crate::llm::actions::client_trait::ClientActionResult::Disconnect => {
180 app_state
181 .update_client_status(client_id, ClientStatus::Disconnected)
182 .await;
183 Log::new(Some(&status_tx)).info(format!("STUN client {} disconnected", client_id));
184 let _ = status_tx.send("__UPDATE_UI__".to_string());
185 }
186 _ => {}
187 }
188
189 Ok(())
190 }
191
192 pub async fn send_binding_request(
194 client_id: ClientId,
195 app_state: Arc<AppState>,
196 llm_client: OllamaClient,
197 status_tx: mpsc::UnboundedSender<String>,
198 ) -> Result<()> {
199 let discovered = Self::query_external_address(client_id, &app_state, &status_tx).await?;
200 Self::report_binding_response(client_id, &discovered, &app_state, &llm_client, &status_tx)
201 .await;
202 Ok(())
203 }
204
205 async fn query_external_address(
212 client_id: ClientId,
213 app_state: &Arc<AppState>,
214 status_tx: &mpsc::UnboundedSender<String>,
215 ) -> Result<StunDiscovery> {
216 let stun_server = app_state
218 .with_client_mut(client_id, |client| {
219 client
220 .get_protocol_field("stun_server")
221 .and_then(|v| v.as_str())
222 .map(|s| s.to_string())
223 })
224 .await
225 .flatten()
226 .context("No STUN server found")?;
227
228 info!(
229 "STUN client {} sending binding request to {}",
230 client_id, stun_server
231 );
232
233 let stun_sock_addr: SocketAddr = match stun_server.parse::<SocketAddr>() {
247 Ok(addr) => addr,
248 Err(_) => tokio::net::lookup_host(&stun_server)
249 .await
250 .context(format!("Failed to resolve STUN server: {}", stun_server))?
251 .next()
252 .context("No addresses found for STUN server")?,
253 };
254
255 let local_addr: SocketAddr = "0.0.0.0:0".parse().unwrap();
257 let udp_socket = tokio::net::UdpSocket::bind(local_addr)
258 .await
259 .context("Failed to bind UDP socket for STUN query")?;
260
261 let stun_client = stunclient::StunClient::new(stun_sock_addr);
263
264 match stun_client.query_external_address_async(&udp_socket).await {
265 Ok(external_addr) => {
266 info!(
267 "STUN client {} discovered external address: {}",
268 client_id, external_addr
269 );
270
271 let local_addr = udp_socket.local_addr()?;
272
273 Ok(StunDiscovery {
274 external_addr,
275 local_addr,
276 stun_server,
277 })
278 }
279 Err(e) => {
280 Log::new(Some(status_tx)).error(format!(
281 "STUN client {} binding request failed: {}",
282 client_id, e
283 ));
284 Err(e.into())
285 }
286 }
287 }
288
289 async fn report_binding_response(
291 client_id: ClientId,
292 discovered: &StunDiscovery,
293 app_state: &Arc<AppState>,
294 llm_client: &OllamaClient,
295 status_tx: &mpsc::UnboundedSender<String>,
296 ) {
297 let Some(instruction) = app_state.get_instruction_for_client(client_id).await else {
298 return;
299 };
300
301 let protocol = Arc::new(StunClientProtocol::new());
302 let event = Event::new(
303 &STUN_CLIENT_BINDING_RESPONSE_EVENT,
304 serde_json::json!({
305 "external_ip": discovered.external_addr.ip().to_string(),
306 "external_port": discovered.external_addr.port(),
307 "external_addr": discovered.external_addr.to_string(),
308 "local_addr": discovered.local_addr.to_string(),
309 "stun_server": discovered.stun_server,
310 }),
311 );
312
313 let memory = app_state
314 .get_memory_for_client(client_id)
315 .await
316 .unwrap_or_default();
317
318 match call_llm_for_client(
319 llm_client,
320 app_state,
321 client_id.to_string(),
322 &instruction,
323 &memory,
324 Some(&event),
325 protocol.as_ref(),
326 status_tx,
327 )
328 .await
329 {
330 Ok(ClientLlmResult {
331 actions,
332 memory_updates,
333 }) => {
334 if let Some(mem) = memory_updates {
336 app_state.set_memory_for_client(client_id, mem).await;
337 }
338
339 use crate::llm::actions::client_trait::{Client, ClientActionResult};
350 let protocol = crate::client::stun::actions::StunClientProtocol::new();
351 for action in actions {
352 match protocol.execute_action(action.clone()) {
353 Ok(ClientActionResult::Custom { name, .. })
354 if name == "send_binding_request" =>
355 {
356 match Self::query_external_address(client_id, app_state, status_tx)
357 .await
358 {
359 Ok(d) => info!(
360 "STUN client {} follow-up binding request: {}",
361 client_id, d.external_addr
362 ),
363 Err(e) => error!(
364 "STUN client {} follow-up binding request failed: {}",
365 client_id, e
366 ),
367 }
368 }
369 Ok(_) => {}
370 Err(e) => error!(
371 "STUN client {} rejected its own follow-up action: {}",
372 client_id, e
373 ),
374 }
375 }
376 }
377 Err(e) => {
378 error!("LLM error for STUN client {}: {}", client_id, e);
379 }
380 }
381 }
382
383 async fn command_loop(
390 mut command_rx: mpsc::Receiver<ClientCommand>,
391 client_id: ClientId,
392 llm_client: OllamaClient,
393 app_state: Arc<AppState>,
394 status_tx: mpsc::UnboundedSender<String>,
395 ) {
396 use crate::llm::actions::client_trait::Client;
397 use crate::llm::actions::protocol_trait::Protocol;
398
399 let protocol = StunClientProtocol::new();
400
401 loop {
402 let command = tokio::select! {
403 command = command_rx.recv() => match command {
404 Some(command) => command,
405 None => break,
407 },
408 _ = tokio::time::sleep(std::time::Duration::from_secs(5)) => {
409 if app_state.get_client(client_id).await.is_none() {
412 info!("STUN client {} stopped", client_id);
413 break;
414 }
415 continue;
416 }
417 };
418
419 let action = command.action.clone();
420
421 let mut discovered: Option<StunDiscovery> = None;
424 let outcome = match protocol.execute_action(action.clone()) {
425 Err(e) => Ok(ClientSendOutcome::Rejected {
426 error: e.to_string(),
427 }),
428 Ok(ClientActionResult::Custom { name, .. }) if name == "send_binding_request" => {
429 match Self::query_external_address(client_id, &app_state, &status_tx).await {
430 Ok(result) => {
431 let detail = format!(
432 "binding exchange completed via stunclient: external address {} \
433 (byte count not observable - stunclient owns the socket)",
434 result.external_addr
435 );
436 discovered = Some(result);
437 Ok(ClientSendOutcome::Executed { detail })
438 }
439 Err(e) => Err(e),
440 }
441 }
442 Ok(ClientActionResult::Custom { name, .. }) => Ok(ClientSendOutcome::Executed {
443 detail: format!("custom result '{name}' is not a STUN client verb"),
444 }),
445 Ok(ClientActionResult::Disconnect) => Ok(ClientSendOutcome::Disconnected),
446 Ok(ClientActionResult::WaitForMore) => Ok(ClientSendOutcome::Executed {
447 detail: "wait_for_more: nothing sent".to_string(),
448 }),
449 Ok(_) => Ok(ClientSendOutcome::Executed {
450 detail: "action produced no STUN exchange".to_string(),
451 }),
452 };
453
454 let outcome_json = match &outcome {
455 Ok(outcome) => serde_json::to_value(outcome).unwrap_or(serde_json::Value::Null),
456 Err(e) => serde_json::json!({"error": e.to_string()}),
457 };
458 app_state
459 .record_access_log(
460 AccessLogOwner::Client(client_id.as_u32()),
461 protocol.protocol_name(),
462 None,
463 "injected_action",
464 action,
465 vec![outcome_json],
466 )
467 .await;
468
469 let disconnect = matches!(outcome, Ok(ClientSendOutcome::Disconnected));
470 if let Err(e) = &outcome {
471 error!("STUN client {} injected action failed: {}", client_id, e);
472 let _ = status_tx.send(format!(
473 "[WARN] Client {} injected action failed: {}",
474 client_id, e
475 ));
476 }
477 let _ = status_tx.send("__UPDATE_UI__".to_string());
478 crate::client::command_support::reply(command, outcome);
479
480 if disconnect {
481 app_state
482 .update_client_status(client_id, ClientStatus::Disconnected)
483 .await;
484 Log::new(Some(&status_tx)).info(format!(
485 "STUN client {} disconnected (injected action)",
486 client_id
487 ));
488 break;
489 }
490
491 if let Some(result) = discovered {
492 Self::report_binding_response(
493 client_id,
494 &result,
495 &app_state,
496 &llm_client,
497 &status_tx,
498 )
499 .await;
500 }
501 }
502
503 app_state.remove_client_handle(client_id).await;
504 let _ = status_tx.send("__UPDATE_UI__".to_string());
505 }
506}
507
508struct StunDiscovery {
510 external_addr: SocketAddr,
511 local_addr: SocketAddr,
512 stun_server: String,
513}