1use 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#[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
37pub struct ArpProtocol;
39
40impl ArpProtocol {
41 pub fn new() -> Self {
42 Self
43 }
44}
45
46impl Protocol for ArpProtocol {
48 fn default_binding(&self) -> Option<crate::protocol::BindingDefaults> {
49 Some(crate::protocol::BindingDefaults::interface_based(
51 DEFAULT_LOOPBACK_INTERFACE,
52 ))
53 }
54
55 fn get_startup_parameters(&self) -> Vec<ParameterDefinition> {
56 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 .connectionless()
89 .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 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
188impl 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 let interface = ctx
202 .interface()
203 .context("ARP requires network interface")?
204 .to_string();
205
206 let listen_addr = ctx.legacy_listen_addr();
208
209 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 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 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 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 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 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
286fn 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
306fn 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
354fn 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
371pub 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}