1use crate::llm::actions::{
4 client_trait::{Client, ClientActionResult},
5 protocol_trait::Protocol,
6 ActionDefinition, Parameter, ParameterDefinition,
7};
8use crate::protocol::{ConnectContext, EventType};
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14pub static ARP_CLIENT_STARTED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
16 EventType::new(
17 "arp_client_started",
18 "ARP client successfully started on network interface",
19 json!({"type": "wait_for_more"}),
20 )
21 .with_parameters(vec![Parameter {
22 name: "interface".to_string(),
23 type_hint: "string".to_string(),
24 description: "Network interface name".to_string(),
25 required: true,
26 }])
27});
28
29pub static ARP_CLIENT_RESPONSE_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
31 EventType::new(
32 "arp_response_received",
33 "ARP response received from network",
34 json!({"type": "send_arp_reply", "sender_mac": "aa:bb:cc:dd:ee:ff", "sender_ip": "192.168.1.100", "target_mac": "11:22:33:44:55:66", "target_ip": "192.168.1.1"}),
35 )
36 .with_parameters(vec![
37 Parameter {
38 name: "operation".to_string(),
39 type_hint: "string".to_string(),
40 description: "ARP operation (REQUEST or REPLY)".to_string(),
41 required: true,
42 },
43 Parameter {
44 name: "sender_mac".to_string(),
45 type_hint: "string".to_string(),
46 description: "Sender MAC address".to_string(),
47 required: true,
48 },
49 Parameter {
50 name: "sender_ip".to_string(),
51 type_hint: "string".to_string(),
52 description: "Sender IP address".to_string(),
53 required: true,
54 },
55 Parameter {
56 name: "target_mac".to_string(),
57 type_hint: "string".to_string(),
58 description: "Target MAC address".to_string(),
59 required: true,
60 },
61 Parameter {
62 name: "target_ip".to_string(),
63 type_hint: "string".to_string(),
64 description: "Target IP address".to_string(),
65 required: true,
66 },
67 ])
68});
69
70#[derive(Default)]
72pub struct ArpClientProtocol;
73
74impl ArpClientProtocol {
75 pub fn new() -> Self {
76 Self
77 }
78}
79
80impl Protocol for ArpClientProtocol {
82 fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
83 vec![
84 ActionDefinition {
85 name: "send_arp_request".to_string(),
86 description: "Send an ARP request (who-has query)".to_string(),
87 parameters: vec![
88 Parameter {
89 name: "sender_mac".to_string(),
90 type_hint: "string".to_string(),
91 description: "Source MAC address (format: aa:bb:cc:dd:ee:ff)".to_string(),
92 required: true,
93 },
94 Parameter {
95 name: "sender_ip".to_string(),
96 type_hint: "string".to_string(),
97 description: "Source IP address".to_string(),
98 required: true,
99 },
100 Parameter {
101 name: "target_ip".to_string(),
102 type_hint: "string".to_string(),
103 description: "Target IP address to query".to_string(),
104 required: true,
105 },
106 ],
107 example: json!({
108 "type": "send_arp_request",
109 "sender_mac": "aa:bb:cc:dd:ee:ff",
110 "sender_ip": "192.168.1.100",
111 "target_ip": "192.168.1.1"
112 }),
113 log_template: None,
114 },
115 ActionDefinition {
116 name: "send_arp_reply".to_string(),
117 description: "Send an ARP reply (gratuitous ARP)".to_string(),
118 parameters: vec![
119 Parameter {
120 name: "sender_mac".to_string(),
121 type_hint: "string".to_string(),
122 description: "Source MAC address".to_string(),
123 required: true,
124 },
125 Parameter {
126 name: "sender_ip".to_string(),
127 type_hint: "string".to_string(),
128 description: "Source IP address".to_string(),
129 required: true,
130 },
131 Parameter {
132 name: "target_mac".to_string(),
133 type_hint: "string".to_string(),
134 description: "Target MAC address".to_string(),
135 required: true,
136 },
137 Parameter {
138 name: "target_ip".to_string(),
139 type_hint: "string".to_string(),
140 description: "Target IP address".to_string(),
141 required: true,
142 },
143 ],
144 example: json!({
145 "type": "send_arp_reply",
146 "sender_mac": "aa:bb:cc:dd:ee:ff",
147 "sender_ip": "192.168.1.100",
148 "target_mac": "11:22:33:44:55:66",
149 "target_ip": "192.168.1.1"
150 }),
151 log_template: None,
152 },
153 ActionDefinition {
154 name: "stop_capture".to_string(),
155 description: "Stop ARP capture and close client".to_string(),
156 parameters: vec![],
157 example: json!({
158 "type": "stop_capture"
159 }),
160 log_template: None,
161 },
162 ]
163 }
164 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
165 vec![
166 ActionDefinition {
167 name: "send_arp_request".to_string(),
168 description: "Send ARP request in response to received ARP packet".to_string(),
169 parameters: vec![
170 Parameter {
171 name: "sender_mac".to_string(),
172 type_hint: "string".to_string(),
173 description: "Source MAC address".to_string(),
174 required: true,
175 },
176 Parameter {
177 name: "sender_ip".to_string(),
178 type_hint: "string".to_string(),
179 description: "Source IP address".to_string(),
180 required: true,
181 },
182 Parameter {
183 name: "target_ip".to_string(),
184 type_hint: "string".to_string(),
185 description: "Target IP to query".to_string(),
186 required: true,
187 },
188 ],
189 example: json!({
190 "type": "send_arp_request",
191 "sender_mac": "aa:bb:cc:dd:ee:ff",
192 "sender_ip": "192.168.1.100",
193 "target_ip": "192.168.1.1"
194 }),
195 log_template: None,
196 },
197 ActionDefinition {
198 name: "send_arp_reply".to_string(),
199 description: "Send ARP reply in response to received ARP packet".to_string(),
200 parameters: vec![
201 Parameter {
202 name: "sender_mac".to_string(),
203 type_hint: "string".to_string(),
204 description: "Source MAC address".to_string(),
205 required: true,
206 },
207 Parameter {
208 name: "sender_ip".to_string(),
209 type_hint: "string".to_string(),
210 description: "Source IP address".to_string(),
211 required: true,
212 },
213 Parameter {
214 name: "target_mac".to_string(),
215 type_hint: "string".to_string(),
216 description: "Target MAC address".to_string(),
217 required: true,
218 },
219 Parameter {
220 name: "target_ip".to_string(),
221 type_hint: "string".to_string(),
222 description: "Target IP address".to_string(),
223 required: true,
224 },
225 ],
226 example: json!({
227 "type": "send_arp_reply",
228 "sender_mac": "aa:bb:cc:dd:ee:ff",
229 "sender_ip": "192.168.1.100",
230 "target_mac": "11:22:33:44:55:66",
231 "target_ip": "192.168.1.1"
232 }),
233 log_template: None,
234 },
235 ActionDefinition {
236 name: "wait_for_more".to_string(),
237 description: "Continue monitoring for ARP packets".to_string(),
238 parameters: vec![],
239 example: json!({
240 "type": "wait_for_more"
241 }),
242 log_template: None,
243 },
244 ]
245 }
246 fn protocol_name(&self) -> &'static str {
247 "ARP"
248 }
249 fn get_event_types(&self) -> Vec<EventType> {
250 vec![
251 EventType::new(
252 "arp_client_started",
253 "Triggered when ARP client starts capturing",
254 json!({"type": "wait_for_more"}),
255 ),
256 EventType::new(
257 "arp_response_received",
258 "Triggered when ARP packet is received",
259 json!({"type": "send_arp_reply", "sender_mac": "aa:bb:cc:dd:ee:ff", "sender_ip": "192.168.1.100", "target_mac": "11:22:33:44:55:66", "target_ip": "192.168.1.1"}),
260 ),
261 ]
262 }
263 fn stack_name(&self) -> &'static str {
264 "ETH>ARP"
265 }
266 fn keywords(&self) -> Vec<&'static str> {
267 vec!["arp", "arp client", "address resolution", "layer 2"]
268 }
269 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
270 use crate::protocol::metadata::{
271 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
272 };
273
274 ProtocolMetadataV2::builder()
275 .state(DevelopmentState::Experimental)
276 .privilege_requirement(PrivilegeRequirement::PacketCapture)
284 .implementation("pcap + pnet for ARP packet capture and injection")
285 .llm_control("Send ARP requests/replies, monitor ARP traffic")
286 .e2e_testing(
287 "tests/client/arp/command_channel_test.rs runs unprivileged in every ordinary \
288 test run: it asserts that a capture which cannot open makes connect() return \
289 Err rather than leaving the client Connected, and drives the injected-command \
290 surface end to end (Sent/Rejected/Disconnected, and the access-log record) by \
291 standing in for the pcap injection thread. Only the branch proving libpcap \
292 itself accepted the frame needs root, and it is #[ignore]d.",
293 )
294 .build()
295 }
296 fn description(&self) -> &'static str {
297 "ARP client for sending ARP requests and monitoring ARP traffic"
298 }
299 fn example_prompt(&self) -> &'static str {
300 "Monitor ARP on eth0 and send who-has query for 192.168.1.1"
301 }
302 fn group_name(&self) -> &'static str {
303 "Core"
304 }
305 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
306 vec![ParameterDefinition {
307 name: "interface".to_string(),
308 type_hint: "string".to_string(),
309 description: "Network interface name (e.g., eth0, en0)".to_string(),
310 required: true,
311 example: json!("eth0"),
312 default: None,
313 }]
314 }
315
316 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
317 use crate::llm::actions::StartupExamples;
318 use serde_json::json;
319
320 StartupExamples::new(
321 json!({
323 "type": "open_client",
324 "remote_addr": "eth0",
325 "base_stack": "arp",
326 "instruction": "Send ARP requests for 192.168.1.0/24 and discover live hosts",
327 "startup_params": {
328 "interface": "eth0"
329 }
330 }),
331 json!({
333 "type": "open_client",
334 "remote_addr": "eth0",
335 "base_stack": "arp",
336 "startup_params": {
337 "interface": "eth0"
338 },
339 "event_handlers": [{
340 "event_pattern": "arp_response_received",
341 "handler": {
342 "type": "script",
343 "language": "python",
344 "code": "<arp_handler>"
345 }
346 }]
347 }),
348 json!({
350 "type": "open_client",
351 "remote_addr": "eth0",
352 "base_stack": "arp",
353 "startup_params": {
354 "interface": "eth0"
355 },
356 "event_handlers": [{
357 "event_pattern": "arp_client_started",
358 "handler": {
359 "type": "static",
360 "actions": [{
361 "type": "send_arp_request",
362 "sender_mac": "aa:bb:cc:dd:ee:ff",
363 "sender_ip": "192.168.1.100",
364 "target_ip": "192.168.1.1"
365 }]
366 }
367 }]
368 }),
369 )
370 }
371}
372
373impl Client for ArpClientProtocol {
375 fn connect(
376 &self,
377 ctx: ConnectContext,
378 ) -> std::pin::Pin<
379 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
380 > {
381 Box::pin(async move {
382 use crate::client::arp::ArpClient;
383 ArpClient::start_with_llm_actions(
384 ctx.remote_addr,
385 ctx.llm_client,
386 ctx.state,
387 ctx.status_tx,
388 ctx.client_id,
389 )
390 .await
391 })
392 }
393 fn execute_action(&self, action: serde_json::Value) -> Result<ClientActionResult> {
394 let action_type = action
395 .get("type")
396 .and_then(|v| v.as_str())
397 .context("Missing 'type' field in action")?;
398
399 match action_type {
400 "send_arp_request" => {
401 let sender_mac = action
402 .get("sender_mac")
403 .and_then(|v| v.as_str())
404 .context("Missing 'sender_mac' field")?;
405 let sender_ip = action
406 .get("sender_ip")
407 .and_then(|v| v.as_str())
408 .context("Missing 'sender_ip' field")?;
409 let target_ip = action
410 .get("target_ip")
411 .and_then(|v| v.as_str())
412 .context("Missing 'target_ip' field")?;
413
414 Ok(ClientActionResult::Custom {
415 name: "send_arp_request".to_string(),
416 data: json!({
417 "sender_mac": sender_mac,
418 "sender_ip": sender_ip,
419 "target_ip": target_ip,
420 }),
421 })
422 }
423 "send_arp_reply" => {
424 let sender_mac = action
425 .get("sender_mac")
426 .and_then(|v| v.as_str())
427 .context("Missing 'sender_mac' field")?;
428 let sender_ip = action
429 .get("sender_ip")
430 .and_then(|v| v.as_str())
431 .context("Missing 'sender_ip' field")?;
432 let target_mac = action
433 .get("target_mac")
434 .and_then(|v| v.as_str())
435 .context("Missing 'target_mac' field")?;
436 let target_ip = action
437 .get("target_ip")
438 .and_then(|v| v.as_str())
439 .context("Missing 'target_ip' field")?;
440
441 Ok(ClientActionResult::Custom {
442 name: "send_arp_reply".to_string(),
443 data: json!({
444 "sender_mac": sender_mac,
445 "sender_ip": sender_ip,
446 "target_mac": target_mac,
447 "target_ip": target_ip,
448 }),
449 })
450 }
451 "stop_capture" => Ok(ClientActionResult::Disconnect),
452 "wait_for_more" => Ok(ClientActionResult::WaitForMore),
453 _ => Err(anyhow::anyhow!(
454 "Unknown ARP client action: {}",
455 action_type
456 )),
457 }
458 }
459}