1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 ActionDefinition, Parameter, ParameterDefinition,
6};
7use crate::protocol::EventType;
8use crate::state::app_state::AppState;
9use anyhow::{Context, Result};
10use serde_json::json;
11use std::sync::LazyLock;
12
13pub static STUN_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
15 EventType::new(
16 "stun_connected",
17 "STUN client initialized and ready to query external address",
18 json!({
19 "type": "send_binding_request"
20 }),
21 )
22 .with_parameters(vec![
23 Parameter {
24 name: "local_addr".to_string(),
25 type_hint: "string".to_string(),
26 description: "Local UDP socket address".to_string(),
27 required: true,
28 },
29 Parameter {
30 name: "stun_server".to_string(),
31 type_hint: "string".to_string(),
32 description: "STUN server address".to_string(),
33 required: true,
34 },
35 ])
36});
37
38pub static STUN_CLIENT_BINDING_RESPONSE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
40 EventType::new(
41 "stun_binding_response",
42 "STUN binding response received with external address information",
43 json!({
44 "type": "wait_for_more"
45 }),
46 )
47 .with_parameters(vec![
48 Parameter {
49 name: "external_ip".to_string(),
50 type_hint: "string".to_string(),
51 description: "External IP address discovered via STUN".to_string(),
52 required: true,
53 },
54 Parameter {
55 name: "external_port".to_string(),
56 type_hint: "number".to_string(),
57 description: "External port number discovered via STUN".to_string(),
58 required: true,
59 },
60 Parameter {
61 name: "external_addr".to_string(),
62 type_hint: "string".to_string(),
63 description: "Full external address (IP:port)".to_string(),
64 required: true,
65 },
66 Parameter {
67 name: "local_addr".to_string(),
68 type_hint: "string".to_string(),
69 description: "Local UDP socket address".to_string(),
70 required: true,
71 },
72 Parameter {
73 name: "stun_server".to_string(),
74 type_hint: "string".to_string(),
75 description: "STUN server that provided the response".to_string(),
76 required: true,
77 },
78 ])
79});
80
81pub struct StunClientProtocol;
83
84impl StunClientProtocol {
85 pub fn new() -> Self {
86 Self
87 }
88}
89
90impl crate::llm::actions::protocol_trait::Protocol for StunClientProtocol {
91 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
92 vec![]
93 }
94
95 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
96 vec![
97 ActionDefinition {
98 name: "send_binding_request".to_string(),
99 description: "Send a STUN binding request to discover external IP/port".to_string(),
100 parameters: vec![],
101 example: json!({
102 "type": "send_binding_request"
103 }),
104 log_template: None,
105 },
106 ActionDefinition {
107 name: "disconnect".to_string(),
108 description: "Disconnect from the STUN server".to_string(),
109 parameters: vec![],
110 example: json!({
111 "type": "disconnect"
112 }),
113 log_template: None,
114 },
115 ]
116 }
117
118 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
119 vec![
120 ActionDefinition {
121 name: "send_binding_request".to_string(),
122 description: "Send another STUN binding request to refresh external address"
123 .to_string(),
124 parameters: vec![],
125 example: json!({
126 "type": "send_binding_request"
127 }),
128 log_template: None,
129 },
130 ActionDefinition {
131 name: "wait_for_more".to_string(),
132 description: "Wait before sending another binding request".to_string(),
133 parameters: vec![],
134 example: json!({
135 "type": "wait_for_more"
136 }),
137 log_template: None,
138 },
139 ]
140 }
141
142 fn protocol_name(&self) -> &'static str {
143 "STUN"
144 }
145
146 fn get_event_types(&self) -> Vec<EventType> {
147 vec![
148 EventType::new(
149 "stun_connected",
150 "Triggered when STUN client is initialized",
151 json!({"type": "placeholder", "event_id": "stun_connected"}),
152 ),
153 EventType::new(
154 "stun_binding_response",
155 "Triggered when STUN client receives binding response",
156 json!({"type": "placeholder", "event_id": "stun_binding_response"}),
157 ),
158 ]
159 }
160
161 fn stack_name(&self) -> &'static str {
162 "ETH>IP>UDP>STUN"
163 }
164
165 fn keywords(&self) -> Vec<&'static str> {
166 vec![
167 "stun",
168 "stun client",
169 "nat traversal",
170 "external ip",
171 "public ip",
172 ]
173 }
174
175 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
176 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
177
178 ProtocolMetadataV2::builder()
179 .state(DevelopmentState::Experimental)
180 .implementation("stunclient library for NAT traversal discovery")
181 .llm_control("Control when to send binding requests and interpret external address")
182 .e2e_testing("Google STUN servers (stun.l.google.com:19302)")
183 .build()
184 }
185
186 fn description(&self) -> &'static str {
187 "STUN client for discovering external IP address and port behind NAT"
188 }
189
190 fn example_prompt(&self) -> &'static str {
191 "Connect to stun.l.google.com:19302 and discover my external IP address"
192 }
193
194 fn group_name(&self) -> &'static str {
195 "Network Infrastructure"
196 }
197 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
198 use crate::llm::actions::StartupExamples;
199 use serde_json::json;
200
201 StartupExamples::new(
202 json!({
204 "type": "open_client",
205 "remote_addr": "stun.l.google.com:19302",
206 "base_stack": "stun",
207 "instruction": "Discover external IP address via STUN binding request"
208 }),
209 json!({
211 "type": "open_client",
212 "remote_addr": "stun.l.google.com:19302",
213 "base_stack": "stun",
214 "event_handlers": [{
215 "event_pattern": "stun_binding_response",
216 "handler": {
217 "type": "script",
218 "language": "python",
219 "code": "<stun_client_handler>"
220 }
221 }]
222 }),
223 json!({
225 "type": "open_client",
226 "remote_addr": "stun.l.google.com:19302",
227 "base_stack": "stun",
228 "event_handlers": [
229 {
230 "event_pattern": "stun_connected",
231 "handler": {
232 "type": "static",
233 "actions": [{
234 "type": "send_binding_request"
235 }]
236 }
237 },
238 {
239 "event_pattern": "stun_binding_response",
240 "handler": {
241 "type": "static",
242 "actions": [{
243 "type": "disconnect"
244 }]
245 }
246 }
247 ]
248 }),
249 )
250 }
251}
252
253impl Client for StunClientProtocol {
254 fn connect(
255 &self,
256 ctx: crate::protocol::ConnectContext,
257 ) -> std::pin::Pin<
258 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
259 > {
260 Box::pin(async move {
261 use crate::client::stun::StunClient;
262 StunClient::connect_with_llm_actions(
263 ctx.remote_addr,
264 ctx.llm_client,
265 ctx.state,
266 ctx.status_tx,
267 ctx.client_id,
268 )
269 .await
270 })
271 }
272
273 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
274 let action_type = action
275 .get("type")
276 .and_then(|v| v.as_str())
277 .context("Missing 'type' field in action")?;
278
279 match action_type {
280 "send_binding_request" => Ok(ClientActionResult::Custom {
281 name: "send_binding_request".to_string(),
282 data: json!({}),
283 }),
284 "disconnect" => Ok(ClientActionResult::Disconnect),
285 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
286 _ => Err(anyhow::anyhow!(
287 "Unknown STUN client action: {}",
288 action_type
289 )),
290 }
291 }
292}