1use crate::llm::actions::{
4 protocol_trait::{ActionResult, Protocol, Server},
5 ActionDefinition, Parameter,
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 IcmpProtocol;
39
40impl IcmpProtocol {
41 pub fn new() -> Self {
42 Self
43 }
44}
45
46impl Protocol for IcmpProtocol {
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_async_actions(&self, _state: &AppState) -> Vec<ActionDefinition> {
56 vec![]
57 }
58
59 fn get_sync_actions(&self) -> Vec<ActionDefinition> {
60 vec![
61 send_echo_reply_action(),
62 send_destination_unreachable_action(),
63 send_time_exceeded_action(),
64 ignore_icmp_action(),
66 ]
67 }
68
69 fn protocol_name(&self) -> &'static str {
70 "ICMP"
71 }
72
73 fn get_event_types(&self) -> Vec<EventType> {
74 get_icmp_event_types()
75 }
76
77 fn stack_name(&self) -> &'static str {
78 "IP>ICMP"
79 }
80
81 fn keywords(&self) -> Vec<&'static str> {
82 vec!["icmp", "ping", "echo", "traceroute"]
83 }
84
85 fn metadata(&self) -> crate::protocol::metadata::ProtocolMetadataV2 {
86 use crate::protocol::metadata::{
87 DevelopmentState, PrivilegeRequirement, ProtocolMetadataV2,
88 };
89
90 ProtocolMetadataV2::builder()
91 .state(DevelopmentState::Experimental)
92 .privilege_requirement(PrivilegeRequirement::RawSockets)
93 .connectionless()
94 .deliberately_silent()
98 .implementation("Raw IP sockets + pnet for ICMP packet handling")
99 .llm_control("Full control - can respond to all ICMP message types")
100 .e2e_testing(
101 "tests/server/icmp/packet_codec_test.rs is the only ICMP server test that runs \
102 unprivileged: it asserts every emitted packet field-by-field against RFC 791 \
103 and RFC 792, that hostile action JSON is refused rather than panicking, and \
104 that each advertised example is accepted by its own executor. \
105 tests/capture_startup_reports_failure_test.rs asserts spawn() returns Err \
106 without raw-socket privilege. tests/server/icmp/e2e_test.rs crafts real echo \
107 requests and reads the replies off a raw socket, but is #[ignore]d for \
108 privilege and has never been run in CI.",
109 )
110 .notes(
111 "Requires root/CAP_NET_RAW for raw socket access. Startup failure is reported: \
112 spawn_with_llm awaits the SOCK_RAW sockets, so a privilege failure lands in \
113 ServerStatus::Error rather than Running. UNVERIFIED, and this is the whole \
114 gap: no packet this server builds has ever been observed leaving a real \
115 socket. Until this pass none could have been correct - the send socket did not \
116 set IP_HDRINCL, so the kernel prepended its own header and every reply went out \
117 with our IPv4 header sitting where the ICMP message belongs. That is fixed and \
118 unit-tested up to the syscall, no further. The 'interface' argument is accepted \
119 and ignored - the socket receives ICMP from every interface. Note the kernel \
120 answers echo requests itself, so a userspace reply is a second reply on the \
121 wire.",
122 )
123 .build()
124 }
125
126 fn description(&self) -> &'static str {
127 "ICMP (Internet Control Message Protocol) server"
128 }
129
130 fn example_prompt(&self) -> &'static str {
131 "Listen for ICMP echo requests on eth0"
132 }
133
134 fn group_name(&self) -> &'static str {
135 "Core"
136 }
137
138 fn get_startup_examples(&self) -> crate::llm::actions::StartupExamples {
139 use crate::llm::actions::StartupExamples;
140
141 let script = r#"import json, sys
144data = json.load(sys.stdin)
145event = data["event"]
146if data["event_type_id"] == "icmp_echo_request":
147 actions = [{"type": "send_echo_reply",
148 "source_ip": event.get("destination_ip", ""),
149 "destination_ip": event.get("source_ip", ""),
150 "identifier": event.get("identifier", 0),
151 "sequence": event.get("sequence", 0),
152 "payload_hex": event.get("payload_hex", "")}]
153else:
154 actions = []
155print(json.dumps({"actions": actions}))"#;
156
157 StartupExamples::new(
158 json!({
159 "type": "open_server",
160 "interface": "eth0",
161 "base_stack": "icmp",
162 "instruction": "ICMP server that responds to ping requests"
163 }),
164 json!({
165 "type": "open_server",
166 "interface": "eth0",
167 "base_stack": "icmp",
168 "event_handlers": [{
169 "event_pattern": "icmp_echo_request",
170 "handler": {
171 "type": "script",
172 "language": "python",
173 "code": script
174 }
175 }]
176 }),
177 json!({
178 "type": "open_server",
179 "interface": "eth0",
180 "base_stack": "icmp",
181 "event_handlers": [{
182 "event_pattern": "icmp_echo_request",
183 "handler": {
184 "type": "static",
185 "actions": [{
186 "type": "ignore_icmp"
187 }]
188 }
189 }]
190 }),
191 )
192 }
193}
194
195impl Server for IcmpProtocol {
197 fn spawn(
198 &self,
199 ctx: crate::protocol::SpawnContext,
200 ) -> std::pin::Pin<
201 Box<dyn std::future::Future<Output = anyhow::Result<std::net::SocketAddr>> + Send>,
202 > {
203 Box::pin(async move {
204 use crate::server::icmp::IcmpServer;
205
206 let interface = ctx
209 .interface()
210 .context("ICMP requires network interface")?
211 .to_string();
212
213 let listen_addr = ctx.legacy_listen_addr();
215
216 let _interface_name = IcmpServer::spawn_with_llm(
218 interface,
219 ctx.llm_client,
220 ctx.state,
221 ctx.status_tx,
222 ctx.server_id,
223 )
224 .await?;
225
226 Ok(listen_addr)
229 })
230 }
231
232 fn execute_action(&self, action: serde_json::Value) -> Result<ActionResult> {
233 let action_type = action
234 .get("type")
235 .and_then(|v| v.as_str())
236 .context("Missing 'type' field in action")?;
237
238 match action_type {
239 "send_echo_reply" => self.execute_send_echo_reply(action),
240 "send_destination_unreachable" => self.execute_send_destination_unreachable(action),
241 "send_time_exceeded" => self.execute_send_time_exceeded(action),
242 "ignore_icmp" => Ok(ActionResult::NoAction),
244 _ => Err(anyhow::anyhow!("Unknown ICMP action: {}", action_type)),
245 }
246 }
247}
248
249const MAX_ICMP_PAYLOAD: usize = 65535 - 20 - 8;
252
253fn u16_field(action: &serde_json::Value, name: &str) -> Result<u16> {
259 let raw = action
260 .get(name)
261 .and_then(|v| v.as_u64())
262 .with_context(|| format!("Missing '{name}' parameter"))?;
263 u16::try_from(raw)
264 .with_context(|| format!("'{name}' must be a 16-bit ICMP field (0-65535), got {raw}"))
265}
266
267fn u8_field(action: &serde_json::Value, name: &str, default: Option<u8>) -> Result<u8> {
272 let raw = match action.get(name).and_then(|v| v.as_u64()) {
273 Some(raw) => raw,
274 None => return default.with_context(|| format!("Missing '{name}' parameter")),
275 };
276 u8::try_from(raw).with_context(|| format!("'{name}' must be an ICMP code (0-255), got {raw}"))
277}
278
279fn u16_field_or(spec: &serde_json::Value, name: &str, default: u16) -> Result<u16> {
281 let Some(raw) = spec.get(name).and_then(|v| v.as_u64()) else {
282 return Ok(default);
283 };
284 u16::try_from(raw).with_context(|| format!("'{name}' must be 0-65535, got {raw}"))
285}
286
287fn ones_complement(bytes: &[u8], carry: u32) -> u16 {
289 let mut sum = carry;
290 let mut chunks = bytes.chunks_exact(2);
291 for pair in &mut chunks {
292 sum += u32::from(u16::from_be_bytes([pair[0], pair[1]]));
293 }
294 if let [odd] = chunks.remainder() {
295 sum += u32::from(*odd) << 8;
296 }
297 while sum >> 16 != 0 {
298 sum = (sum & 0xffff) + (sum >> 16);
299 }
300 !(sum as u16)
301}
302
303fn build_quoted_datagram(spec: &serde_json::Value) -> Result<Vec<u8>> {
315 if !spec.is_object() {
316 anyhow::bail!(
317 "'original_packet' must be an object describing the datagram that provoked this \
318 error, e.g. {{\"source_ip\": \"192.168.1.50\", \"destination_ip\": \
319 \"203.0.113.5\", \"protocol\": \"udp\", \"source_port\": 41234, \
320 \"destination_port\": 53}}"
321 );
322 }
323
324 let source: std::net::Ipv4Addr = spec
325 .get("source_ip")
326 .and_then(|v| v.as_str())
327 .context("Missing 'original_packet.source_ip'")?
328 .parse()
329 .context("'original_packet.source_ip' is not an IPv4 address")?;
330 let destination: std::net::Ipv4Addr = spec
331 .get("destination_ip")
332 .and_then(|v| v.as_str())
333 .context("Missing 'original_packet.destination_ip'")?
334 .parse()
335 .context("'original_packet.destination_ip' is not an IPv4 address")?;
336
337 let protocol = match spec.get("protocol") {
340 None | Some(serde_json::Value::Null) => 17u8,
341 Some(serde_json::Value::String(name)) => match name.to_ascii_lowercase().as_str() {
342 "udp" => 17,
343 "tcp" => 6,
344 "icmp" => 1,
345 other => anyhow::bail!(
346 "'original_packet.protocol' is {other:?}; use \"udp\", \"tcp\", \"icmp\", or \
347 supply the whole quotation through 'original_packet_hex'"
348 ),
349 },
350 Some(serde_json::Value::Number(n)) => {
351 let raw = n
352 .as_u64()
353 .context("'original_packet.protocol' must be 0-255")?;
354 u8::try_from(raw).context("'original_packet.protocol' must be 0-255")?
355 }
356 Some(other) => {
357 anyhow::bail!("'original_packet.protocol' must be a string or a number, got {other}")
358 }
359 };
360
361 let ttl = u8_field(spec, "ttl", Some(64))?;
362 let identification = u16_field_or(spec, "identification", 0)?;
363
364 const TOTAL_LEN: u16 = 28;
367 let mut packet = vec![0u8; TOTAL_LEN as usize];
368 packet[0] = 0x45; packet[2..4].copy_from_slice(&TOTAL_LEN.to_be_bytes());
370 packet[4..6].copy_from_slice(&identification.to_be_bytes());
371 packet[8] = ttl;
372 packet[9] = protocol;
373 packet[12..16].copy_from_slice(&source.octets());
374 packet[16..20].copy_from_slice(&destination.octets());
375 let header_checksum = ones_complement(&packet[..20], 0);
376 packet[10..12].copy_from_slice(&header_checksum.to_be_bytes());
377
378 match protocol {
379 17 => {
380 let source_port = u16_field_or(spec, "source_port", 0)?;
381 let destination_port = u16_field_or(spec, "destination_port", 0)?;
382 packet[20..22].copy_from_slice(&source_port.to_be_bytes());
383 packet[22..24].copy_from_slice(&destination_port.to_be_bytes());
384 packet[24..26].copy_from_slice(&8u16.to_be_bytes()); let carry = u32::from(u16::from_be_bytes([packet[12], packet[13]]))
387 + u32::from(u16::from_be_bytes([packet[14], packet[15]]))
388 + u32::from(u16::from_be_bytes([packet[16], packet[17]]))
389 + u32::from(u16::from_be_bytes([packet[18], packet[19]]))
390 + u32::from(protocol)
391 + 8;
392 let checksum = ones_complement(&packet[20..28], carry);
393 let checksum = if checksum == 0 { 0xffff } else { checksum };
396 packet[26..28].copy_from_slice(&checksum.to_be_bytes());
397 }
398 6 => {
399 let source_port = u16_field_or(spec, "source_port", 0)?;
400 let destination_port = u16_field_or(spec, "destination_port", 0)?;
401 let sequence =
404 u32::try_from(spec.get("sequence").and_then(|v| v.as_u64()).unwrap_or(0))
405 .context("'original_packet.sequence' must fit in 32 bits")?;
406 packet[20..22].copy_from_slice(&source_port.to_be_bytes());
407 packet[22..24].copy_from_slice(&destination_port.to_be_bytes());
408 packet[24..28].copy_from_slice(&sequence.to_be_bytes());
409 }
410 1 => {
411 let icmp_type = u8_field(spec, "icmp_type", Some(8))?;
412 let icmp_code = u8_field(spec, "icmp_code", Some(0))?;
413 let identifier = u16_field_or(spec, "identifier", 0)?;
414 let sequence = u16_field_or(spec, "sequence", 0)?;
415 packet[20] = icmp_type;
416 packet[21] = icmp_code;
417 packet[24..26].copy_from_slice(&identifier.to_be_bytes());
418 packet[26..28].copy_from_slice(&sequence.to_be_bytes());
419 let checksum = ones_complement(&packet[20..28], 0);
420 packet[22..24].copy_from_slice(&checksum.to_be_bytes());
421 }
422 other => anyhow::bail!(
423 "'original_packet.protocol' {other} has no structured form here — only udp (17), \
424 tcp (6) and icmp (1) do. Supply the whole quotation through 'original_packet_hex'."
425 ),
426 }
427
428 Ok(packet)
429}
430
431fn quoted_datagram(action: &serde_json::Value) -> Result<Vec<u8>> {
436 let structured = action.get("original_packet").filter(|v| !v.is_null());
437 let raw = action
438 .get("original_packet_hex")
439 .and_then(|v| v.as_str())
440 .filter(|s| !s.is_empty());
441
442 match (structured, raw) {
443 (Some(spec), None) => build_quoted_datagram(spec),
444 (None, Some(encoded)) => hex::decode(encoded).context("Invalid hex in original_packet_hex"),
445 (Some(_), Some(_)) => anyhow::bail!(
446 "Give 'original_packet' or 'original_packet_hex', not both — they describe the same \
447 quoted datagram and nothing can tell which one you meant"
448 ),
449 (None, None) => anyhow::bail!(
450 "Missing the quoted datagram. Describe the packet that provoked this error with \
451 'original_packet' ({{\"source_ip\": …, \"destination_ip\": …, \"protocol\": \
452 \"udp\", \"source_port\": …, \"destination_port\": …}}), or pass the bytes you \
453 captured as 'original_packet_hex'"
454 ),
455 }
456}
457
458impl IcmpProtocol {
459 fn execute_send_echo_reply(&self, action: serde_json::Value) -> Result<ActionResult> {
461 let source_ip = action
462 .get("source_ip")
463 .and_then(|v| v.as_str())
464 .context("Missing 'source_ip' parameter")?;
465
466 let destination_ip = action
467 .get("destination_ip")
468 .and_then(|v| v.as_str())
469 .context("Missing 'destination_ip' parameter")?;
470
471 let identifier = u16_field(&action, "identifier")?;
472 let sequence = u16_field(&action, "sequence")?;
473
474 let payload_hex = action
475 .get("payload_hex")
476 .and_then(|v| v.as_str())
477 .unwrap_or("");
478
479 let payload = if payload_hex.is_empty() {
480 Vec::new()
481 } else {
482 hex::decode(payload_hex).context("Invalid hex in payload_hex")?
483 };
484
485 anyhow::ensure!(
488 payload.len() <= MAX_ICMP_PAYLOAD,
489 "payload_hex decodes to {} bytes; an ICMP echo reply carries at most {}",
490 payload.len(),
491 MAX_ICMP_PAYLOAD
492 );
493
494 let source_ip_parsed: std::net::Ipv4Addr =
496 source_ip.parse().context("Invalid source_ip format")?;
497 let destination_ip_parsed: std::net::Ipv4Addr = destination_ip
498 .parse()
499 .context("Invalid destination_ip format")?;
500
501 use crate::server::icmp::IcmpServer;
503 let packet = IcmpServer::build_echo_reply(
504 source_ip_parsed,
505 destination_ip_parsed,
506 identifier,
507 sequence,
508 &payload,
509 );
510
511 Ok(ActionResult::Output(packet))
512 }
513
514 fn execute_send_destination_unreachable(
516 &self,
517 action: serde_json::Value,
518 ) -> Result<ActionResult> {
519 let source_ip = action
520 .get("source_ip")
521 .and_then(|v| v.as_str())
522 .context("Missing 'source_ip' parameter")?;
523
524 let destination_ip = action
525 .get("destination_ip")
526 .and_then(|v| v.as_str())
527 .context("Missing 'destination_ip' parameter")?;
528
529 let code = u8_field(&action, "code", None)?;
530
531 let original_packet = quoted_datagram(&action)?;
532
533 let source_ip_parsed: std::net::Ipv4Addr =
535 source_ip.parse().context("Invalid source_ip format")?;
536 let destination_ip_parsed: std::net::Ipv4Addr = destination_ip
537 .parse()
538 .context("Invalid destination_ip format")?;
539
540 use crate::server::icmp::IcmpServer;
542 let packet = IcmpServer::build_destination_unreachable(
543 source_ip_parsed,
544 destination_ip_parsed,
545 code,
546 &original_packet,
547 );
548
549 Ok(ActionResult::Output(packet))
550 }
551
552 fn execute_send_time_exceeded(&self, action: serde_json::Value) -> Result<ActionResult> {
554 let source_ip = action
555 .get("source_ip")
556 .and_then(|v| v.as_str())
557 .context("Missing 'source_ip' parameter")?;
558
559 let destination_ip = action
560 .get("destination_ip")
561 .and_then(|v| v.as_str())
562 .context("Missing 'destination_ip' parameter")?;
563
564 let code = u8_field(&action, "code", Some(0))?;
566
567 let original_packet = quoted_datagram(&action)?;
568
569 let source_ip_parsed: std::net::Ipv4Addr =
571 source_ip.parse().context("Invalid source_ip format")?;
572 let destination_ip_parsed: std::net::Ipv4Addr = destination_ip
573 .parse()
574 .context("Invalid destination_ip format")?;
575
576 use crate::server::icmp::IcmpServer;
578 let packet = IcmpServer::build_time_exceeded(
579 source_ip_parsed,
580 destination_ip_parsed,
581 code,
582 &original_packet,
583 );
584
585 Ok(ActionResult::Output(packet))
586 }
587
588 }
636
637fn send_echo_reply_action() -> ActionDefinition {
639 ActionDefinition {
640 name: "send_echo_reply".to_string(),
641 description: "Send an ICMP Echo Reply packet in response to an Echo Request (ping)"
642 .to_string(),
643 parameters: vec![
644 Parameter {
645 name: "source_ip".to_string(),
646 type_hint: "string".to_string(),
647 description: "Source IP address (format: X.X.X.X)".to_string(),
648 required: true,
649 },
650 Parameter {
651 name: "destination_ip".to_string(),
652 type_hint: "string".to_string(),
653 description: "Destination IP address (format: X.X.X.X)".to_string(),
654 required: true,
655 },
656 Parameter {
657 name: "identifier".to_string(),
658 type_hint: "number".to_string(),
659 description: "ICMP identifier (must match request)".to_string(),
660 required: true,
661 },
662 Parameter {
663 name: "sequence".to_string(),
664 type_hint: "number".to_string(),
665 description: "ICMP sequence number (must match request)".to_string(),
666 required: true,
667 },
668 Parameter {
669 name: "payload_hex".to_string(),
670 type_hint: "string".to_string(),
671 description: "Payload data as hex string (must match request)".to_string(),
672 required: false,
673 },
674 ],
675 example: json!({
676 "type": "send_echo_reply",
677 "source_ip": "192.168.1.100",
678 "destination_ip": "192.168.1.50",
679 "identifier": 1234,
680 "sequence": 1,
681 "payload_hex": "48656c6c6f"
682 }),
683 log_template: Some(
684 LogTemplate::new()
685 .with_info("-> ICMP echo reply to {destination_ip}")
686 .with_debug(
687 "ICMP send_echo_reply: dst={destination_ip} id={identifier} seq={sequence}",
688 ),
689 ),
690 }
691}
692
693fn quoted_datagram_parameter() -> Parameter {
695 Parameter {
696 name: "original_packet".to_string(),
697 type_hint: "object".to_string(),
698 description: "The datagram that provoked this error, described as fields: \
699 'source_ip' and 'destination_ip' (required), 'protocol' (\"udp\" \
700 default, \"tcp\", \"icmp\", or an IANA number), 'source_port' and \
701 'destination_port' for udp/tcp, 'ttl' (default 64) and \
702 'identification' (default 0). The server builds the 20-byte IPv4 \
703 header and the 8 transport bytes RFC 792 quotes, and computes both \
704 checksums. Prefer this over 'original_packet_hex' — give exactly one."
705 .to_string(),
706 required: false,
707 }
708}
709
710fn quoted_datagram_hex_parameter() -> Parameter {
712 Parameter {
713 name: "original_packet_hex".to_string(),
714 type_hint: "string".to_string(),
715 description: "Escape hatch: the quoted datagram byte for byte as hex (original IP \
716 header + first 8 bytes of its payload), for bytes you captured rather \
717 than invented. Nothing is computed for you, and a wrong header \
718 checksum makes the peer ignore the error silently. Give this or \
719 'original_packet', not both."
720 .to_string(),
721 required: false,
722 }
723}
724
725fn send_destination_unreachable_action() -> ActionDefinition {
727 ActionDefinition {
728 name: "send_destination_unreachable".to_string(),
729 description: "Send an ICMP Destination Unreachable message".to_string(),
730 parameters: vec![
731 Parameter {
732 name: "source_ip".to_string(),
733 type_hint: "string".to_string(),
734 description: "Source IP address (format: X.X.X.X)".to_string(),
735 required: true,
736 },
737 Parameter {
738 name: "destination_ip".to_string(),
739 type_hint: "string".to_string(),
740 description: "Destination IP address (format: X.X.X.X)".to_string(),
741 required: true,
742 },
743 Parameter {
744 name: "code".to_string(),
745 type_hint: "number".to_string(),
746 description: "Unreachable code: 0=net, 1=host, 2=protocol, 3=port, 4=fragmentation needed, 5=source route failed".to_string(),
747 required: true,
748 },
749 quoted_datagram_parameter(),
750 quoted_datagram_hex_parameter(),
751 ],
752 example: json!({
757 "type": "send_destination_unreachable",
758 "source_ip": "192.168.1.1",
759 "destination_ip": "192.168.1.50",
760 "code": 3,
761 "original_packet": {
762 "source_ip": "192.168.1.50",
763 "destination_ip": "203.0.113.5",
764 "protocol": "udp",
765 "source_port": 41234,
766 "destination_port": 53
767 }
768 }),
769 log_template: Some(
770 LogTemplate::new()
771 .with_info("-> ICMP unreachable code={code} to {destination_ip}")
772 .with_debug("ICMP send_destination_unreachable: dst={destination_ip} code={code}"),
773 ),
774 }
775}
776
777fn send_time_exceeded_action() -> ActionDefinition {
779 ActionDefinition {
780 name: "send_time_exceeded".to_string(),
781 description: "Send an ICMP Time Exceeded message (used in traceroute)".to_string(),
782 parameters: vec![
783 Parameter {
784 name: "source_ip".to_string(),
785 type_hint: "string".to_string(),
786 description: "Source IP address (format: X.X.X.X)".to_string(),
787 required: true,
788 },
789 Parameter {
790 name: "destination_ip".to_string(),
791 type_hint: "string".to_string(),
792 description: "Destination IP address (format: X.X.X.X)".to_string(),
793 required: true,
794 },
795 Parameter {
796 name: "code".to_string(),
797 type_hint: "number".to_string(),
798 description: "Time exceeded code: 0=TTL exceeded in transit, 1=fragment reassembly time exceeded".to_string(),
799 required: false,
800 },
801 quoted_datagram_parameter(),
802 quoted_datagram_hex_parameter(),
803 ],
804 example: json!({
809 "type": "send_time_exceeded",
810 "source_ip": "10.0.0.1",
811 "destination_ip": "192.168.1.50",
812 "code": 0,
813 "original_packet": {
814 "source_ip": "192.168.1.50",
815 "destination_ip": "203.0.113.5",
816 "protocol": "udp",
817 "source_port": 41234,
818 "destination_port": 33434,
819 "ttl": 1
820 }
821 }),
822 log_template: Some(
823 LogTemplate::new()
824 .with_info("-> ICMP TTL exceeded to {destination_ip}")
825 .with_debug("ICMP send_time_exceeded: dst={destination_ip} code={code}"),
826 ),
827 }
828}
829
830fn ignore_icmp_action() -> ActionDefinition {
888 ActionDefinition {
889 name: "ignore_icmp".to_string(),
890 description: "Ignore this ICMP packet (no action taken)".to_string(),
891 parameters: vec![],
892 example: json!({
893 "type": "ignore_icmp"
894 }),
895 log_template: Some(
896 LogTemplate::new()
897 .with_info("-> ICMP ignore")
898 .with_debug("ICMP ignore_icmp"),
899 ),
900 }
901}
902
903pub static ICMP_ECHO_REQUEST_EVENT: LazyLock<EventType> = LazyLock::new(|| {
908 EventType::new(
909 "icmp_echo_request",
910 "ICMP Echo Request (ping) received from network",
911 json!({
912 "type": "send_echo_reply",
913 "source_ip": "192.168.1.100",
914 "destination_ip": "192.168.1.50",
915 "identifier": 1234,
916 "sequence": 1,
917 "payload_hex": "48656c6c6f"
918 }),
919 )
920 .with_parameters(vec![
921 Parameter {
922 name: "source_ip".to_string(),
923 type_hint: "string".to_string(),
924 description: "Source IP address of the ping request".to_string(),
925 required: true,
926 },
927 Parameter {
928 name: "destination_ip".to_string(),
929 type_hint: "string".to_string(),
930 description: "Destination IP address (our server)".to_string(),
931 required: true,
932 },
933 Parameter {
934 name: "identifier".to_string(),
935 type_hint: "number".to_string(),
936 description: "ICMP identifier".to_string(),
937 required: true,
938 },
939 Parameter {
940 name: "sequence".to_string(),
941 type_hint: "number".to_string(),
942 description: "ICMP sequence number".to_string(),
943 required: true,
944 },
945 Parameter {
946 name: "payload_hex".to_string(),
947 type_hint: "string".to_string(),
948 description: "Hexadecimal representation of the payload data".to_string(),
949 required: false,
950 },
951 Parameter {
952 name: "ttl".to_string(),
953 type_hint: "number".to_string(),
954 description: "Time to live from IP header".to_string(),
955 required: false,
956 },
957 ])
958 .with_actions(vec![send_echo_reply_action(), ignore_icmp_action()])
959});
960
961pub static ICMP_OTHER_MESSAGE_EVENT: LazyLock<EventType> = LazyLock::new(|| {
1017 EventType::new(
1018 "icmp_other_message",
1019 "Other ICMP message type received (not echo or timestamp)",
1020 json!({
1021 "type": "send_destination_unreachable",
1022 "source_ip": "192.168.1.1",
1023 "destination_ip": "192.168.1.50",
1024 "code": 3,
1025 "original_packet": {
1026 "source_ip": "192.168.1.50",
1027 "destination_ip": "203.0.113.5",
1028 "protocol": "udp",
1029 "source_port": 41234,
1030 "destination_port": 53
1031 }
1032 }),
1033 )
1034 .with_parameters(vec![
1035 Parameter {
1036 name: "source_ip".to_string(),
1037 type_hint: "string".to_string(),
1038 description: "Source IP address".to_string(),
1039 required: true,
1040 },
1041 Parameter {
1042 name: "destination_ip".to_string(),
1043 type_hint: "string".to_string(),
1044 description: "Destination IP address".to_string(),
1045 required: true,
1046 },
1047 Parameter {
1048 name: "icmp_type".to_string(),
1049 type_hint: "number".to_string(),
1050 description: "ICMP message type".to_string(),
1051 required: true,
1052 },
1053 Parameter {
1054 name: "icmp_code".to_string(),
1055 type_hint: "number".to_string(),
1056 description: "ICMP message code".to_string(),
1057 required: true,
1058 },
1059 Parameter {
1060 name: "packet_hex".to_string(),
1061 type_hint: "string".to_string(),
1062 description: "Full ICMP packet as hex".to_string(),
1063 required: false,
1064 },
1065 ])
1066 .with_actions(vec![
1067 send_destination_unreachable_action(),
1068 send_time_exceeded_action(),
1069 ignore_icmp_action(),
1070 ])
1071});
1072
1073pub fn get_icmp_event_types() -> Vec<EventType> {
1074 vec![
1075 ICMP_ECHO_REQUEST_EVENT.clone(),
1076 ICMP_OTHER_MESSAGE_EVENT.clone(),
1078 ]
1079}