Skip to main content

netget/client/arp/
actions.rs

1//! ARP client protocol actions implementation
2
3use 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
14/// ARP client started event
15pub 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
29/// ARP response received event
30pub 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/// ARP client protocol action handler
71#[derive(Default)]
72pub struct ArpClientProtocol;
73
74impl ArpClientProtocol {
75    pub fn new() -> Self {
76        Self
77    }
78}
79
80// Implement Protocol trait (common functionality)
81impl 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            // Both pcap handles - the capture and the injection one - need root, read
277            // access to /dev/bpf* on macOS/BSD, or CAP_NET_RAW on Linux. `PacketCapture`,
278            // not `RawSockets`: this goes through libpcap, not a SOCK_RAW socket, and the
279            // datalink client (the same pcap shape) declares the same. Note that
280            // `client_startup` does not currently gate on this the way `server_startup`
281            // does, so it is documentation for the operator and the TUI rather than a
282            // check - `connect()` awaiting the pcap open is what actually refuses.
283            .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            // LLM mode: LLM handles ARP client
322            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            // Script mode: Code-based ARP handling
332            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            // Static mode: Fixed ARP request
349            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
373// Implement Client trait (client-specific functionality)
374impl 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}