Skip to main content

netget/server/arp/
actions.rs

1//! ARP protocol actions implementation
2
3use crate::llm::actions::{
4    protocol_trait::{ActionResult, Protocol, Server},
5    ActionDefinition, Parameter, ParameterDefinition,
6};
7use crate::protocol::log_template::LogTemplate;
8use crate::protocol::EventType;
9use crate::state::app_state::AppState;
10use anyhow::{Context, Result};
11use serde_json::json;
12use std::sync::LazyLock;
13
14/// Name of the loopback interface on this platform.
15///
16/// Linux and Windows call it `lo`; macOS and the BSDs call it `lo0`. Hardcoding `lo` made the
17/// default binding unresolvable on macOS ("Device 'lo' not found").
18#[cfg(any(
19    target_os = "macos",
20    target_os = "ios",
21    target_os = "freebsd",
22    target_os = "openbsd",
23    target_os = "netbsd",
24    target_os = "dragonfly"
25))]
26const DEFAULT_LOOPBACK_INTERFACE: &str = "lo0";
27#[cfg(not(any(
28    target_os = "macos",
29    target_os = "ios",
30    target_os = "freebsd",
31    target_os = "openbsd",
32    target_os = "netbsd",
33    target_os = "dragonfly"
34)))]
35const DEFAULT_LOOPBACK_INTERFACE: &str = "lo";
36
37/// ARP protocol action handler
38pub struct ArpProtocol;
39
40impl ArpProtocol {
41    pub fn new() -> Self {
42        Self
43    }
44}
45
46// Implement Protocol trait (common functionality)
47impl Protocol for ArpProtocol {
48    fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
49        // ARP uses interface-based binding (loopback by default)
50        Some(crate::protocol::BindingDefaults::interface_based(
51            DEFAULT_LOOPBACK_INTERFACE,
52        ))
53    }
54
55    fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
56        // Interface is now provided via flexible binding system, not startup params
57        vec![]
58    }
59    fn get_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
60        vec![]
61    }
62    fn get_sync_actions(&self) -> Vec<ActionDefinition> {
63        vec![send_arp_reply_action(), ignore_arp_action()]
64    }
65    fn protocol_name(&self) -> &'static str {
66        "ARP"
67    }
68    fn get_event_types(&self) -> Vec<EventType> {
69        get_arp_event_types()
70    }
71    fn stack_name(&self) -> &'static str {
72        "ETH>ARP"
73    }
74    fn keywords(&self) -> Vec<&'static str> {
75        vec!["arp", "address resolution"]
76    }
77    fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
78        use crate::protocol::metadata::{
79            DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
80        };
81
82        ProtocolMetadataV2::builder()
83            .state(DevelopmentState::Experimental)
84            // ARP is connectionless: it has no session, and every packet stands alone. The
85            // server registers no connection entries today, so the 10s idle sweep has nothing
86            // to reap either way — the flag is declared because it is true, and so that a
87            // later per-remote entry does not leak by default.
88            .connectionless()
89            // Deliberately silent: ARP has no error reply. An ARP reply writes a MAC into the
90            // requester's neighbour cache, so a fabricated one is cache poisoning; an unanswered
91            // request is what every host that does not own the address already does.
92            .deliberately_silent()
93            .privilege_requirement(PrivilegeRequirement::PacketCapture)
94            .implementation("libpcap (pcap crate) + pnet for ARP packet handling")
95            .llm_control("Optional: which MAC to advertise for a queried IP is a policy decision (LLM); with no mapping configured the server answers nothing with no LLM call")
96            .e2e_testing(
97                "tests/server/arp/frame_codec_test.rs runs unprivileged in every ordinary test \
98                 run: it asserts the emitted reply field by field against the Ethernet II + RFC \
99                 826 layout, that the declared send_arp_reply example executes to those same \
100                 bytes, and that nine malformed actions are refused rather than panicking. \
101                 tests/capture_startup_reports_failure_test.rs asserts spawn() returns Err for \
102                 an unknown device and for missing capture privilege, also unprivileged. \
103                 tests/server/arp/e2e_test.rs crafts real ARP frames with pnet and reads the \
104                 replies back off the wire with pcap, but is #[ignore]d for capture privilege \
105                 and has never been run in CI — so NO test has observed a captured ARP request \
106                 becoming a reply on a wire.",
107            )
108            .notes(
109                "Requires root/CAP_NET_RAW (or /dev/bpf* access) for promiscuous mode and packet \
110                 injection. Startup failure is reported: spawn_with_llm awaits both pcap handles \
111                 and the 'arp' BPF filter, so a privilege failure lands in ServerStatus::Error \
112                 rather than Running. UNVERIFIED: the request/reply path has only ever been \
113                 exercised by the #[ignore]d test. Loopback is not useful - ARP is not observable \
114                 there; pass a real NIC. An ARP reply is NOT wire-determined — the MAC advertised \
115                 is a policy choice (spoofing/honeypot/custom mapping), like DNS/DHCP — so with no \
116                 operator policy (no instruction, no handler) the server answers nothing and takes \
117                 NO LLM round-trip per captured packet; the LLM is consulted only when the \
118                 operator supplies the mapping. Zero-LLM path is compile-verified; e2e needs a NIC.",
119            )
120            .build()
121    }
122    fn description(&self) -> &'static str {
123        "ARP (Address Resolution Protocol) server"
124    }
125    fn example_prompt(&self) -> &'static str {
126        "Listen for ARP requests on eth0"
127    }
128    fn group_name(&self) -> &'static str {
129        "Core"
130    }
131
132    fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
133        use crate::llm::actions::StartupExamples;
134
135        // Deterministic: reply to every ARP request with a fixed MAC, swapping
136        // sender/target, no LLM call.
137        let script = r#"import json, sys
138data = json.load(sys.stdin)
139event = data["event"]
140if data["event_type_id"] == "arp_request_received":
141    actions = [{"type": "send_arp_reply",
142                "sender_ip": event.get("target_ip", ""),
143                "sender_mac": "02:00:00:00:00:01",
144                "target_ip": event.get("sender_ip", ""),
145                "target_mac": event.get("sender_mac", "")}]
146else:
147    actions = []
148print(json.dumps({"actions": actions}))"#;
149
150        StartupExamples::new(
151            json!({
152                "type": "open_server",
153                "interface": "eth0",
154                "base_stack": "arp",
155                "instruction": "ARP responder that handles address resolution requests"
156            }),
157            json!({
158                "type": "open_server",
159                "interface": "eth0",
160                "base_stack": "arp",
161                "event_handlers": [{
162                    "event_pattern": "arp_request_received",
163                    "handler": {
164                        "type": "script",
165                        "language": "python",
166                        "code": script
167                    }
168                }]
169            }),
170            json!({
171                "type": "open_server",
172                "interface": "eth0",
173                "base_stack": "arp",
174                "event_handlers": [{
175                    "event_pattern": "arp_request_received",
176                    "handler": {
177                        "type": "static",
178                        "actions": [{
179                            "type": "ignore_arp"
180                        }]
181                    }
182                }]
183            }),
184        )
185    }
186}
187
188// Implement Server trait (server-specific functionality)
189impl Server for ArpProtocol {
190    fn spawn(
191        &self,
192        ctx: crate::protocol::SpawnContext,
193    ) -> std::pin::Pin<
194        Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
195    > {
196        Box::pin(async move {
197            use crate::server::arp::ArpServer;
198
199            // ARP uses interface-based binding
200            // Extract interface from context (defaults already applied)
201            let interface = ctx
202                .interface()
203                .context("ARP requires network interface")?
204                .to_string();
205
206            // Get listen addr before consuming ctx fields
207            let listen_addr = ctx.legacy_listen_addr();
208
209            // Spawn the ARP server
210            let _interface_name = ArpServer::spawn_with_llm(
211                interface,
212                ctx.llm_client,
213                ctx.state,
214                ctx.status_tx,
215                ctx.server_id,
216            )
217            .await?;
218
219            // ARP doesn't bind to a socket, so return a dummy address
220            // The listen_addr from context is just a placeholder
221            Ok(listen_addr)
222        })
223    }
224    fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
225        let action_type = action
226            .get("type")
227            .and_then(|v| v.as_str())
228            .context("Missing 'type' field in action")?;
229
230        match action_type {
231            "send_arp_reply" => self.execute_send_arp_reply(action),
232            "ignore_arp" => Ok(ActionResult::NoAction),
233            _ => Err(anyhow::anyhow!("Unknown ARP action: {}", action_type)),
234        }
235    }
236}
237
238impl ArpProtocol {
239    /// Execute send_arp_reply action
240    fn execute_send_arp_reply(&self, action: serde_json::Value) -> Result<ActionResult> {
241        let sender_mac = action
242            .get("sender_mac")
243            .and_then(|v| v.as_str())
244            .context("Missing 'sender_mac' parameter")?;
245
246        let sender_ip = action
247            .get("sender_ip")
248            .and_then(|v| v.as_str())
249            .context("Missing 'sender_ip' parameter")?;
250
251        let target_mac = action
252            .get("target_mac")
253            .and_then(|v| v.as_str())
254            .context("Missing 'target_mac' parameter")?;
255
256        let target_ip = action
257            .get("target_ip")
258            .and_then(|v| v.as_str())
259            .context("Missing 'target_ip' parameter")?;
260
261        // Parse MAC addresses
262        let sender_mac_parsed =
263            parse_mac_address(sender_mac).context("Invalid sender_mac format")?;
264        let target_mac_parsed =
265            parse_mac_address(target_mac).context("Invalid target_mac format")?;
266
267        // Parse IP addresses
268        let sender_ip_parsed: std::net::Ipv4Addr =
269            sender_ip.parse().context("Invalid sender_ip format")?;
270        let target_ip_parsed: std::net::Ipv4Addr =
271            target_ip.parse().context("Invalid target_ip format")?;
272
273        // Build ARP reply packet
274        use crate::server::arp::ArpServer;
275        let packet = ArpServer::build_arp_reply(
276            sender_mac_parsed,
277            sender_ip_parsed,
278            target_mac_parsed,
279            target_ip_parsed,
280        );
281
282        Ok(ActionResult::Output(packet))
283    }
284}
285
286/// Parse a MAC address string (e.g., "aa:bb:cc:dd:ee:ff") into pnet MacAddr
287fn parse_mac_address(mac_str: &str) -> Result<pnet::util::MacAddr> {
288    let parts: Vec<&str> = mac_str.split(':').collect();
289    if parts.len() != 6 {
290        return Err(anyhow::anyhow!(
291            "MAC address must have 6 octets separated by colons"
292        ));
293    }
294
295    let octets: Result<Vec<u8>> = parts
296        .iter()
297        .map(|s| u8::from_str_radix(s, 16).context("Invalid hex in MAC address"))
298        .collect();
299
300    let octets = octets?;
301    Ok(pnet::util::MacAddr::new(
302        octets[0], octets[1], octets[2], octets[3], octets[4], octets[5],
303    ))
304}
305
306/// Action definition for send_arp_reply
307fn send_arp_reply_action() -> ActionDefinition {
308    ActionDefinition {
309        name: "send_arp_reply".to_string(),
310        description: "Send an ARP reply packet in response to an ARP request".to_string(),
311        parameters: vec![
312            Parameter {
313                name: "sender_mac".to_string(),
314                type_hint: "string".to_string(),
315                description: "MAC address to send from (format: aa:bb:cc:dd:ee:ff)".to_string(),
316                required: true,
317            },
318            Parameter {
319                name: "sender_ip".to_string(),
320                type_hint: "string".to_string(),
321                description: "IP address to send from (format: X.X.X.X)".to_string(),
322                required: true,
323            },
324            Parameter {
325                name: "target_mac".to_string(),
326                type_hint: "string".to_string(),
327                description: "Target MAC address (format: aa:bb:cc:dd:ee:ff)".to_string(),
328                required: true,
329            },
330            Parameter {
331                name: "target_ip".to_string(),
332                type_hint: "string".to_string(),
333                description: "Target IP address (format: X.X.X.X)".to_string(),
334                required: true,
335            },
336        ],
337        example: json!({
338            "type": "send_arp_reply",
339            "sender_mac": "aa:bb:cc:dd:ee:ff",
340            "sender_ip": "192.168.1.100",
341            "target_mac": "11:22:33:44:55:66",
342            "target_ip": "192.168.1.1"
343        }),
344        log_template: Some(
345            LogTemplate::new()
346                .with_info("-> ARP reply {sender_ip} is-at {sender_mac}")
347                .with_debug(
348                    "ARP send_arp_reply: {sender_mac}/{sender_ip} -> {target_mac}/{target_ip}",
349                ),
350        ),
351    }
352}
353
354/// Action definition for ignore_arp
355fn ignore_arp_action() -> ActionDefinition {
356    ActionDefinition {
357        name: "ignore_arp".to_string(),
358        description: "Ignore this ARP packet (no action taken)".to_string(),
359        parameters: vec![],
360        example: json!({
361            "type": "ignore_arp"
362        }),
363        log_template: Some(
364            LogTemplate::new()
365                .with_info("-> ARP ignored")
366                .with_debug("ARP ignore_arp"),
367        ),
368    }
369}
370
371// ============================================================================
372// ARP Event Type Constants
373// ============================================================================
374
375pub static ARP_REQUEST_RECEIVED_EVENT: LazyLock<EventType> = LazyLock::new(|| {
376    EventType::new(
377        "arp_request_received",
378        "ARP request or reply packet received from network interface",
379        json!({
380            "type": "send_arp_reply",
381            "sender_mac": "aa:bb:cc:dd:ee:ff",
382            "sender_ip": "192.168.1.100",
383            "target_mac": "11:22:33:44:55:66",
384            "target_ip": "192.168.1.1"
385        }),
386    )
387    .with_parameters(vec![
388        Parameter {
389            name: "operation".to_string(),
390            type_hint: "string".to_string(),
391            description: "ARP operation (REQUEST or REPLY)".to_string(),
392            required: true,
393        },
394        Parameter {
395            name: "sender_mac".to_string(),
396            type_hint: "string".to_string(),
397            description: "Sender MAC address".to_string(),
398            required: true,
399        },
400        Parameter {
401            name: "sender_ip".to_string(),
402            type_hint: "string".to_string(),
403            description: "Sender IP address".to_string(),
404            required: true,
405        },
406        Parameter {
407            name: "target_mac".to_string(),
408            type_hint: "string".to_string(),
409            description: "Target MAC address".to_string(),
410            required: true,
411        },
412        Parameter {
413            name: "target_ip".to_string(),
414            type_hint: "string".to_string(),
415            description: "Target IP address".to_string(),
416            required: true,
417        },
418        Parameter {
419            name: "packet_hex".to_string(),
420            type_hint: "string".to_string(),
421            description: "Hexadecimal representation of the full packet data".to_string(),
422            required: false,
423        },
424    ])
425    .with_actions(vec![send_arp_reply_action(), ignore_arp_action()])
426    .with_log_template(
427        LogTemplate::new()
428            .with_info("ARP {operation} {sender_ip} ({sender_mac}) -> {target_ip}")
429            .with_debug("ARP {operation}: {sender_mac}/{sender_ip} -> {target_mac}/{target_ip}")
430            .with_trace("ARP packet: {json_pretty(.)}"),
431    )
432});
433
434pub fn get_arp_event_types() -> Vec<EventType> {
435    vec![ARP_REQUEST_RECEIVED_EVENT.clone()]
436}