Skip to main content

netget/client/stun/
actions.rs

1//! STUN client protocol actions implementation
2
3use 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
13/// STUN client connected event
14pub 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
38/// STUN binding response event
39pub 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
81/// STUN client protocol action handler
82pub 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            // LLM mode: LLM controls STUN client interactions
203            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            // Script mode: Code-based deterministic responses
210            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            // Static mode: Fixed responses
224            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}