1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter,
7};
8use crate::protocol::log_template::LogTemplate;
9use crate::protocol::EventType;
10use crate::state::app_state::AppState;
11use anyhow::{anyhow, Context, Result};
12use serde_json::json;
13use std::net::Ipv4Addr;
14use std::sync::LazyLock;
15
16pub static IGMP_CLIENT_CONNECTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
18 EventType::new(
19 "igmp_connected",
20 "IGMP client initialized and ready to join multicast groups",
21 json!({
22 "type": "join_multicast_group",
23 "multicast_addr": "239.1.2.3",
24 "interface_addr": "0.0.0.0"
25 }),
26 )
27 .with_parameters(vec![Parameter {
28 name: "local_addr".to_string(),
29 type_hint: "string".to_string(),
30 description: "Local socket address bound for multicast reception".to_string(),
31 required: true,
32 }])
33});
34
35pub static IGMP_CLIENT_DATA_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
37 EventType::new(
38 "igmp_data_received",
39 "Multicast data received from group",
40 json!({
41 "type": "wait_for_more"
42 }),
43 )
44 .with_parameters(vec![
45 Parameter {
46 name: "data_hex".to_string(),
47 type_hint: "string".to_string(),
48 description: "The multicast data received (as hex string)".to_string(),
49 required: true,
50 },
51 Parameter {
52 name: "data_length".to_string(),
53 type_hint: "number".to_string(),
54 description: "Full length of the datagram in bytes, even when data_hex is truncated"
55 .to_string(),
56 required: true,
57 },
58 Parameter {
59 name: "data_truncated".to_string(),
60 type_hint: "boolean".to_string(),
61 description: "True when data_hex shows only the first 2048 bytes of a larger datagram"
62 .to_string(),
63 required: true,
64 },
65 Parameter {
66 name: "source_addr".to_string(),
67 type_hint: "string".to_string(),
68 description: "Source address of the multicast sender".to_string(),
69 required: true,
70 },
71 ])
72});
73
74pub struct IgmpClientProtocol;
76
77impl IgmpClientProtocol {
78 pub fn new() -> Self {
79 Self
80 }
81}
82
83impl Protocol for IgmpClientProtocol {
85 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
86 vec![
87 ActionDefinition {
88 name: "join_multicast_group".to_string(),
89 description: "Join a multicast group to start receiving multicast traffic"
90 .to_string(),
91 parameters: vec![
92 Parameter {
93 name: "multicast_addr".to_string(),
94 type_hint: "string".to_string(),
95 description: "Multicast group IP address (e.g., 239.1.2.3)".to_string(),
96 required: true,
97 },
98 Parameter {
99 name: "interface_addr".to_string(),
100 type_hint: "string".to_string(),
101 description: "Local interface address (use 0.0.0.0 for any interface)"
102 .to_string(),
103 required: false,
104 },
105 ],
106 example: json!({
107 "type": "join_multicast_group",
108 "multicast_addr": "239.1.2.3",
109 "interface_addr": "0.0.0.0"
110 }),
111 log_template: None,
112 },
113 ActionDefinition {
114 name: "leave_multicast_group".to_string(),
115 description: "Leave a multicast group to stop receiving multicast traffic"
116 .to_string(),
117 parameters: vec![
118 Parameter {
119 name: "multicast_addr".to_string(),
120 type_hint: "string".to_string(),
121 description: "Multicast group IP address to leave".to_string(),
122 required: true,
123 },
124 Parameter {
125 name: "interface_addr".to_string(),
126 type_hint: "string".to_string(),
127 description: "Local interface address (use 0.0.0.0 for any interface)"
128 .to_string(),
129 required: false,
130 },
131 ],
132 example: json!({
133 "type": "leave_multicast_group",
134 "multicast_addr": "239.1.2.3",
135 "interface_addr": "0.0.0.0"
136 }),
137 log_template: None,
138 },
139 ActionDefinition {
140 name: "send_multicast".to_string(),
141 description: "Send data to a multicast group".to_string(),
142 parameters: vec![
143 Parameter {
144 name: "multicast_addr".to_string(),
145 type_hint: "string".to_string(),
146 description: "Multicast group IP address".to_string(),
147 required: true,
148 },
149 Parameter {
150 name: "port".to_string(),
151 type_hint: "number".to_string(),
152 description: "Destination port".to_string(),
153 required: true,
154 },
155 Parameter {
156 name: "data_hex".to_string(),
157 type_hint: "string".to_string(),
158 description: "Hexadecimal encoded data to send".to_string(),
159 required: true,
160 },
161 ],
162 example: json!({
163 "type": "send_multicast",
164 "multicast_addr": "239.1.2.3",
165 "port": 5000,
166 "data_hex": "48656c6c6f"
167 }),
168 log_template: None,
169 },
170 ]
171 }
172 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
173 vec![ActionDefinition {
174 name: "wait_for_more".to_string(),
175 description: "Wait for more multicast data before responding".to_string(),
176 parameters: vec![],
177 example: json!({
178 "type": "wait_for_more"
179 }),
180 log_template: Some(
181 LogTemplate::new()
182 .with_info("-> IGMP wait for more")
183 .with_debug("IGMP wait_for_more"),
184 ),
185 }]
186 }
187 fn get_event_types(&self) -> Vec<EventType> {
188 vec![
189 IGMP_CLIENT_CONNECTED_EVENT.clone(),
190 IGMP_CLIENT_DATA_RECEIVED_EVENT.clone(),
191 ]
192 }
193 fn protocol_name(&self) -> &'static str {
194 "igmp"
195 }
196 fn stack_name(&self) -> &'static str {
197 "Network"
198 }
199 fn keywords(&self) -> Vec<&'static str> {
200 vec!["igmp", "multicast", "igmp client", "multicast client"]
201 }
202 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
203 use crate::protocol::metadata::{DevelopmentState, ProtocolMetadataV2};
204
205 ProtocolMetadataV2::builder()
206 .state(DevelopmentState::Experimental)
207 .implementation("socket2 for multicast join/leave, tokio UdpSocket for data")
208 .llm_control("Join/leave multicast groups, send/receive multicast data")
209 .e2e_testing("UDP sender/receiver for multicast packets")
210 .notes("IPv4 multicast only, uses socket options (no raw IGMP)")
211 .build()
212 }
213 fn description(&self) -> &'static str {
214 "IGMP client for joining/leaving multicast groups and receiving/sending multicast data"
215 }
216 fn example_prompt(&self) -> &'static str {
217 "Join multicast group 239.1.2.3 on port 5000, log all received data, and send 'Hello' to the group"
218 }
219 fn group_name(&self) -> &'static str {
220 "Network"
221 }
222 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
223 use crate::llm::actions::StartupExamples;
224 use serde_json::json;
225
226 StartupExamples::new(
227 json!({
229 "type": "open_client",
230 "remote_addr": "0.0.0.0:5000",
231 "base_stack": "igmp",
232 "instruction": "Join multicast group 239.1.2.3 and log received data"
233 }),
234 json!({
236 "type": "open_client",
237 "remote_addr": "0.0.0.0:5000",
238 "base_stack": "igmp",
239 "event_handlers": [{
240 "event_pattern": "igmp_data_received",
241 "handler": {
242 "type": "script",
243 "language": "python",
244 "code": "<igmp_client_handler>"
245 }
246 }]
247 }),
248 json!({
250 "type": "open_client",
251 "remote_addr": "0.0.0.0:5000",
252 "base_stack": "igmp",
253 "event_handlers": [
254 {
255 "event_pattern": "igmp_connected",
256 "handler": {
257 "type": "static",
258 "actions": [{
259 "type": "join_multicast_group",
260 "multicast_addr": "239.1.2.3",
261 "interface_addr": "0.0.0.0"
262 }]
263 }
264 }
265 ]
266 }),
267 )
268 }
269}
270
271impl Client for IgmpClientProtocol {
273 fn connect(
274 &self,
275 ctx: crate::protocol::ConnectContext,
276 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<std::net::SocketAddr>> + Send>>
277 {
278 Box::pin(async move {
279 use crate::client::igmp::IgmpClient;
280 IgmpClient::connect_with_llm_actions(
281 ctx.remote_addr,
282 ctx.llm_client,
283 ctx.state,
284 ctx.status_tx,
285 ctx.client_id,
286 )
287 .await
288 })
289 }
290 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
291 let action_type = action["type"]
292 .as_str()
293 .ok_or_else(|| anyhow!("Missing 'type' field in action"))?;
294
295 match action_type {
296 "join_multicast_group" => {
297 let multicast_addr = action["multicast_addr"]
298 .as_str()
299 .ok_or_else(|| anyhow!("Missing multicast_addr"))?;
300
301 let interface_addr = action["interface_addr"].as_str().unwrap_or("0.0.0.0");
302
303 let multicast_ip: Ipv4Addr = multicast_addr
305 .parse()
306 .context("Invalid multicast address")?;
307
308 let interface_ip: Ipv4Addr = interface_addr
309 .parse()
310 .context("Invalid interface address")?;
311
312 Ok(ClientActionResult::Custom {
313 name: "join_multicast_group".to_string(),
314 data: json!({
315 "multicast_addr": multicast_ip.to_string(),
316 "interface_addr": interface_ip.to_string(),
317 }),
318 })
319 }
320 "leave_multicast_group" => {
321 let multicast_addr = action["multicast_addr"]
322 .as_str()
323 .ok_or_else(|| anyhow!("Missing multicast_addr"))?;
324
325 let interface_addr = action["interface_addr"].as_str().unwrap_or("0.0.0.0");
326
327 let multicast_ip: Ipv4Addr = multicast_addr
329 .parse()
330 .context("Invalid multicast address")?;
331
332 let interface_ip: Ipv4Addr = interface_addr
333 .parse()
334 .context("Invalid interface address")?;
335
336 Ok(ClientActionResult::Custom {
337 name: "leave_multicast_group".to_string(),
338 data: json!({
339 "multicast_addr": multicast_ip.to_string(),
340 "interface_addr": interface_ip.to_string(),
341 }),
342 })
343 }
344 "send_multicast" => {
345 let multicast_addr = action["multicast_addr"]
346 .as_str()
347 .ok_or_else(|| anyhow!("Missing multicast_addr"))?;
348
349 let multicast_ip: Ipv4Addr = multicast_addr
354 .parse()
355 .context("Invalid multicast address")?;
356
357 let port = action["port"]
358 .as_u64()
359 .ok_or_else(|| anyhow!("Missing or invalid port"))?;
360 let port = u16::try_from(port)
361 .map_err(|_| anyhow!("port {port} is outside the 0-65535 range"))?;
362
363 let data_hex = action["data_hex"]
364 .as_str()
365 .ok_or_else(|| anyhow!("Missing data_hex"))?;
366
367 let data = hex::decode(data_hex).context("Invalid hex data")?;
370
371 Ok(ClientActionResult::Custom {
372 name: "send_multicast".to_string(),
373 data: json!({
374 "multicast_addr": multicast_ip.to_string(),
375 "port": port,
376 "data": data,
377 }),
378 })
379 }
380 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
381 _ => Err(anyhow!("Unknown action type: {}", action_type)),
382 }
383 }
384}