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 UDP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "udp_connected",
18 "UDP client socket bound and ready to send/receive",
19 json!({
20 "type": "send_udp_datagram",
21 "data_hex": "48656c6c6f"
22 }),
23 )
24 .with_parameters(vec![
25 Parameter {
26 name: "remote_addr".to_string(),
27 type_hint: "string".to_string(),
28 description: "Default remote server address".to_string(),
29 required: true,
30 },
31 Parameter {
32 name: "local_addr".to_string(),
33 type_hint: "string".to_string(),
34 description: "Local socket address".to_string(),
35 required: true,
36 },
37 ])
38});
39
40pub static UDP_CLIENT_DATAGRAM_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
42 EventType::new(
43 "udp_datagram_received",
44 "Datagram received from UDP server",
45 json!({"type": "placeholder", "event_id": "udp_datagram_received"}),
46 )
47 .with_parameters(vec![
48 Parameter {
49 name: "data_hex".to_string(),
50 type_hint: "string".to_string(),
51 description: "The datagram data (as hex string)".to_string(),
52 required: true,
53 },
54 Parameter {
55 name: "data_length".to_string(),
56 type_hint: "number".to_string(),
57 description: "Length of data in bytes".to_string(),
58 required: true,
59 },
60 Parameter {
61 name: "source_addr".to_string(),
62 type_hint: "string".to_string(),
63 description: "Source address of the datagram".to_string(),
64 required: true,
65 },
66 ])
67});
68
69pub struct UdpClientProtocol;
71
72impl Default for UdpClientProtocol {
73 fn default() -> Self {
74 Self::new()
75 }
76}
77
78impl UdpClientProtocol {
79 pub fn new() -> Self {
80 Self
81 }
82}
83
84impl Protocol for UdpClientProtocol {
86 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
87 vec![
88 ActionDefinition {
89 name: "send_udp_datagram".to_string(),
90 description: "Send a UDP datagram to a specific address".to_string(),
91 parameters: vec![
92 Parameter {
93 name: "data_hex".to_string(),
94 type_hint: "string".to_string(),
95 description: "Hexadecimal encoded data to send".to_string(),
96 required: true,
97 },
98 Parameter {
99 name: "target_addr".to_string(),
100 type_hint: "string".to_string(),
101 description: "Optional target address (defaults to remote_addr)"
102 .to_string(),
103 required: false,
104 },
105 ],
106 example: json!({
107 "type": "send_udp_datagram",
108 "data_hex": "48656c6c6f",
109 "target_addr": "127.0.0.1:8080"
110 }),
111 log_template: None,
112 },
113 ActionDefinition {
114 name: "change_target".to_string(),
115 description: "Change the default target address for datagrams".to_string(),
116 parameters: vec![Parameter {
117 name: "new_target".to_string(),
118 type_hint: "string".to_string(),
119 description: "New default target address".to_string(),
120 required: true,
121 }],
122 example: json!({
123 "type": "change_target",
124 "new_target": "127.0.0.1:9090"
125 }),
126 log_template: None,
127 },
128 ActionDefinition {
129 name: "close_socket".to_string(),
130 description: "Close the UDP socket".to_string(),
131 parameters: vec![],
132 example: json!({
133 "type": "close_socket"
134 }),
135 log_template: None,
136 },
137 ]
138 }
139 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
140 vec![
141 ActionDefinition {
142 name: "send_udp_datagram".to_string(),
143 description: "Send UDP datagram in response to received datagram".to_string(),
144 parameters: vec![
145 Parameter {
146 name: "data_hex".to_string(),
147 type_hint: "string".to_string(),
148 description: "Hexadecimal encoded data to send".to_string(),
149 required: true,
150 },
151 Parameter {
152 name: "target_addr".to_string(),
153 type_hint: "string".to_string(),
154 description:
155 "Optional target address (defaults to source of received datagram)"
156 .to_string(),
157 required: false,
158 },
159 ],
160 example: json!({
161 "type": "send_udp_datagram",
162 "data_hex": "48656c6c6f"
163 }),
164 log_template: None,
165 },
166 ActionDefinition {
167 name: "wait_for_more".to_string(),
168 description: "Wait for more datagrams before responding".to_string(),
169 parameters: vec![],
170 example: json!({
171 "type": "wait_for_more"
172 }),
173 log_template: None,
174 },
175 ]
176 }
177 fn protocol_name(&self) -> &'static str {
178 "UDP"
179 }
180 fn get_event_types(&self) -> Vec<EventType> {
181 vec![
182 EventType::new(
183 "udp_connected",
184 "Triggered when UDP client socket is bound and ready",
185 json!({"type": "placeholder", "event_id": "udp_connected"}),
186 ),
187 EventType::new(
188 "udp_datagram_received",
189 "Triggered when UDP client receives a datagram",
190 json!({"type": "placeholder", "event_id": "udp_datagram_received"}),
191 ),
192 ]
193 }
194 fn stack_name(&self) -> &'static str {
195 "ETH>IP>UDP"
196 }
197 fn keywords(&self) -> Vec<&'static str> {
198 vec!["udp", "udp client", "connect to udp", "datagram"]
199 }
200 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
201 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
202
203 ProtocolMetadataV2::builder()
204 .state(DevelopmentState::Experimental)
205 .implementation("Tokio UdpSocket for connectionless datagram communication")
206 .llm_control("Full control over datagrams, target addresses, and responses")
207 .e2e_testing("nc (netcat) with -u flag as test server")
208 .build()
209 }
210 fn description(&self) -> &'static str {
211 "UDP client for sending and receiving datagrams"
212 }
213 fn example_prompt(&self) -> &'static str {
214 "Connect to UDP at localhost:8080 and send 'HELLO'"
215 }
216 fn group_name(&self) -> &'static str {
217 "Core"
218 }
219
220 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
221 use crate::llm::actions::StartupExamples;
222 use serde_json::json;
223
224 StartupExamples::new(
225 json!({
227 "type": "open_client",
228 "remote_addr": "127.0.0.1:8080",
229 "base_stack": "udp",
230 "instruction": "Send a UDP datagram containing 'HELLO' and echo any responses"
231 }),
232 json!({
234 "type": "open_client",
235 "remote_addr": "127.0.0.1:8080",
236 "base_stack": "udp",
237 "event_handlers": [{
238 "event_pattern": "udp_datagram_received",
239 "handler": {
240 "type": "script",
241 "language": "python",
242 "code": "<udp_handler>"
243 }
244 }]
245 }),
246 json!({
248 "type": "open_client",
249 "remote_addr": "127.0.0.1:8080",
250 "base_stack": "udp",
251 "event_handlers": [{
252 "event_pattern": "udp_connected",
253 "handler": {
254 "type": "static",
255 "actions": [{
256 "type": "send_udp_datagram",
257 "data_hex": "48454c4c4f"
258 }]
259 }
260 }]
261 }),
262 )
263 }
264}
265
266impl Client for UdpClientProtocol {
268 fn connect(
269 &self,
270 ctx: crate::protocol::ConnectContext,
271 ) -> std::pin::Pin<
272 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
273 > {
274 Box::pin(async move {
275 use crate::client::udp::UdpClient;
276 UdpClient::connect_with_llm_actions(
277 ctx.remote_addr,
278 ctx.llm_client,
279 ctx.state,
280 ctx.status_tx,
281 ctx.client_id,
282 )
283 .await
284 })
285 }
286 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
287 let action_type = action
288 .get("type")
289 .and_then(|v| v.as_str())
290 .context("Missing 'type' field in action")?;
291
292 match action_type {
293 "send_udp_datagram" => {
294 let data_hex = action
295 .get("data_hex")
296 .and_then(|v| v.as_str())
297 .context("Missing 'data_hex' field")?;
298
299 let data = hex::decode(data_hex).context("Invalid hex data")?;
300
301 let target_addr = action
303 .get("target_addr")
304 .and_then(|v| v.as_str())
305 .map(|s| s.to_string());
306
307 Ok(ClientActionResult::Custom {
308 name: "send_udp_datagram".to_string(),
309 data: json!({
310 "data": data,
311 "target_addr": target_addr,
312 }),
313 })
314 }
315 "change_target" => {
316 let new_target = action
317 .get("new_target")
318 .and_then(|v| v.as_str())
319 .context("Missing 'new_target' field")?;
320
321 Ok(ClientActionResult::Custom {
322 name: "change_target".to_string(),
323 data: json!({
324 "new_target": new_target,
325 }),
326 })
327 }
328 "close_socket" => Ok(ClientActionResult::Disconnect),
329 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
330 _ => Err(anyhow::anyhow!(
331 "Unknown UDP client action: {}",
332 action_type
333 )),
334 }
335 }
336}