netget/server/icmp/mod.rs
1//! ICMP (Internet Control Message Protocol) server implementation
2//!
3//! This module provides functionality to capture and respond to ICMP messages at the network layer.
4//! It uses raw IP sockets via socket2 and pnet for ICMP packet handling.
5
6pub mod actions;
7
8use anyhow::{Context, Result};
9use pnet::packet::icmp::echo_request::EchoRequestPacket;
10// Note: pnet doesn't provide timestamp packet types
11use pnet::packet::icmp::{IcmpCode, IcmpPacket, IcmpTypes, MutableIcmpPacket};
12use pnet::packet::ip::IpNextHeaderProtocols;
13use pnet::packet::ipv4::{Ipv4Packet, MutableIpv4Packet};
14use pnet::packet::Packet;
15use socket2::{Domain, Protocol, Socket, Type};
16use std::net::Ipv4Addr;
17use std::sync::Arc;
18use tokio::sync::mpsc;
19use tracing::{debug, error, info, trace};
20
21use crate::llm::action_helper::call_llm;
22use crate::llm::ollama_client::OllamaClient;
23use crate::protocol::Event;
24use crate::state::app_state::AppState;
25use crate::{console_error, console_info, console_trace};
26use actions::{
27 IcmpProtocol,
28 ICMP_ECHO_REQUEST_EVENT,
29 ICMP_OTHER_MESSAGE_EVENT,
30 // ICMP_TIMESTAMP_REQUEST_EVENT, // TODO: Removed - timestamp support requires pnet timestamp packet types
31};
32
33/// ICMP server that captures and responds to ICMP messages
34pub struct IcmpServer;
35
36impl IcmpServer {
37 /// Spawn ICMP server with integrated LLM handling
38 pub async fn spawn_with_llm(
39 interface: String,
40 llm_client: OllamaClient,
41 app_state: Arc<AppState>,
42 status_tx: mpsc::UnboundedSender<String>,
43 server_id: crate::state::ServerId,
44 ) -> Result<String> {
45 console_info!(
46 status_tx,
47 "Starting ICMP capture on interface: {}",
48 interface
49 );
50
51 // Retained by this function; the receive task takes ownership of `status_tx`.
52 let status_tx_ready = status_tx.clone();
53
54 let protocol = Arc::new(IcmpProtocol::new());
55
56 // ICMP requires raw sockets, so we run it in a blocking task.
57 //
58 // Creating an AF_INET/SOCK_RAW/IPPROTO_ICMP socket needs root on macOS and the BSDs,
59 // and root or CAP_NET_RAW on Linux. The outcome is reported back over a oneshot so
60 // `spawn_with_llm` can return Err and the server is marked Error, rather than reporting
61 // Running while never receiving a packet.
62 let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<Result<()>>();
63
64 // `JoinHandle::abort()` cannot interrupt a thread parked in a blocking receive, so the
65 // receive loop is stopped cooperatively: it polls this flag every iteration, and the
66 // task registered with `register_server_task` below trips it when `stop_server` aborts
67 // it. Without this the raw socket kept receiving (and kept calling the LLM) after the
68 // server was stopped, until the process exited. The socket is non-blocking and the loop
69 // sleeps 10ms on `WouldBlock`, so a stop is noticed within ~10ms.
70 // See `crate::utils::shutdown`.
71 let stop = crate::utils::StopSignal::new();
72 let stop_in_loop = stop.clone();
73 // Retained by this function; the receive task takes ownership of `app_state`.
74 let app_state_reg = app_state.clone();
75
76 let interface_clone = interface.clone();
77 let protocol_clone = protocol.clone();
78 tokio::task::spawn_blocking(move || {
79 let open_sockets = || -> Result<(Socket, Arc<Socket>)> {
80 // Create raw ICMP socket
81 let socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4)).context(
82 "failed to create raw ICMP receive socket \
83 (needs root, or CAP_NET_RAW on Linux)",
84 )?;
85
86 // Set socket to non-blocking for timeout handling
87 socket
88 .set_nonblocking(true)
89 .context("failed to put the raw ICMP socket in non-blocking mode")?;
90
91 // Create a separate socket for sending (to avoid conflicts)
92 let send_socket = Socket::new(Domain::IPV4, Type::RAW, Some(Protocol::ICMPV4))
93 .context("failed to create raw ICMP send socket")?;
94
95 // Every reply this server builds is a *complete* IPv4 packet: 20-byte header
96 // written by `build_echo_reply` / `build_destination_unreachable` /
97 // `build_time_exceeded`, then the ICMP message. Handing that to a
98 // SOCK_RAW/IPPROTO_ICMP socket with IP_HDRINCL off makes the kernel prepend a
99 // header of its own and treat our 20 bytes as the first 20 bytes of the ICMP
100 // *message* - so the peer reads ICMP type 0x45 (69, unassigned) and the reply is
101 // garbage. `IP_HDRINCL` is what says "the buffer starts at the IP header"; pnet's
102 // own Layer3 transport channel sets exactly this option for exactly this reason
103 // (pnet_transport 0.35, `transport_channel`).
104 send_socket
105 .set_header_included_v4(true)
106 .context("failed to set IP_HDRINCL on the raw ICMP send socket")?;
107
108 // The send socket is used from async tasks, so it must never park a Tokio
109 // worker: a raw send only blocks when the socket buffer is full, which is
110 // precisely the hostile case. Dropping a reply under that pressure (and logging
111 // it) is the correct outcome for a datagram protocol.
112 send_socket
113 .set_nonblocking(true)
114 .context("failed to put the raw ICMP send socket in non-blocking mode")?;
115
116 Ok((socket, Arc::new(send_socket)))
117 };
118
119 let (socket, send_socket) = match open_sockets() {
120 Ok(sockets) => {
121 let _ = ready_tx.send(Ok(()));
122 sockets
123 }
124 Err(e) => {
125 console_error!(status_tx, "ICMP raw socket startup failed: {:#}", e);
126 let _ = ready_tx.send(Err(e));
127 return;
128 }
129 };
130
131 // TODO: Bind to specific interface if needed
132 // For now, we receive on all interfaces
133
134 console_info!(
135 status_tx,
136 "ICMP server listening for packets on {}",
137 interface_clone
138 );
139
140 let runtime = tokio::runtime::Handle::current();
141
142 // Receive loop
143 let mut buffer = vec![std::mem::MaybeUninit::uninit(); 65535];
144 loop {
145 if stop_in_loop.is_stopped() {
146 console_info!(
147 status_tx,
148 "ICMP receive loop on {} stopping",
149 interface_clone
150 );
151 break;
152 }
153 // Use recv_from with timeout
154 match socket.recv_from(&mut buffer) {
155 Ok((n, _src_addr)) => {
156 let data = unsafe {
157 std::slice::from_raw_parts(buffer.as_ptr() as *const u8, n).to_vec()
158 };
159
160 // DEBUG: Log raw received data
161 trace!(
162 "Raw socket received {} bytes: {}",
163 n,
164 hex::encode(&data[..std::cmp::min(n, 60)])
165 );
166
167 // Parse IP packet
168 let ip_packet = match Ipv4Packet::new(&data) {
169 Some(p) => p,
170 None => {
171 debug!("Failed to parse IPv4 packet");
172 continue;
173 }
174 };
175
176 // DEBUG: Log IP packet details
177 trace!(
178 "IP header length: {}, payload length: {}",
179 ip_packet.get_header_length() * 4,
180 ip_packet.payload().len()
181 );
182
183 // Check if it's ICMP
184 if ip_packet.get_next_level_protocol() != IpNextHeaderProtocols::Icmp {
185 continue;
186 }
187
188 // DEBUG: Log the payload we're about to parse as ICMP
189 let ip_payload = ip_packet.payload();
190 trace!("IP payload (ICMP data): {}", hex::encode(ip_payload));
191
192 // Tolerate a peer that wrote an IP header onto a socket without
193 // IP_HDRINCL: the kernel then prepends its own header and the peer's
194 // lands where the ICMP message should be, so we receive IP-in-IP. This
195 // used to be described here as "common on loopback"; it is nothing to do
196 // with loopback, it is that sender's bug (netget had it too - see the
197 // IP_HDRINCL note on the send socket above). ICMP type 0x45 does not
198 // exist, so the check cannot misfire on a well-formed message.
199 let icmp_data: Vec<u8>;
200 let icmp_payload = if ip_payload.len() >= 20 && ip_payload[0] == 0x45 {
201 if let Some(inner_ip) = Ipv4Packet::new(ip_payload) {
202 if inner_ip.get_next_level_protocol() == IpNextHeaderProtocols::Icmp
203 {
204 trace!("Detected IP-in-IP encapsulation on loopback, unwrapping inner packet");
205 icmp_data = inner_ip.payload().to_vec();
206 trace!(
207 "Inner IP payload (actual ICMP): {}",
208 hex::encode(&icmp_data)
209 );
210 icmp_data.as_slice()
211 } else {
212 ip_payload
213 }
214 } else {
215 ip_payload
216 }
217 } else {
218 ip_payload
219 };
220
221 // Parse ICMP packet
222 let icmp_packet = match IcmpPacket::new(icmp_payload) {
223 Some(p) => p,
224 None => {
225 debug!("Failed to parse ICMP packet");
226 continue;
227 }
228 };
229
230 // Extract IP information
231 let source_ip = ip_packet.get_source();
232 let dest_ip = ip_packet.get_destination();
233 let ttl = ip_packet.get_ttl();
234 let icmp_type = icmp_packet.get_icmp_type();
235 let icmp_code = icmp_packet.get_icmp_code();
236
237 // DEBUG: Log ICMP type extraction
238 trace!(
239 "ICMP type extracted: {} (raw: {}), code: {}",
240 icmp_type.0,
241 icmp_type.0,
242 icmp_code.0
243 );
244 // Never index a wire-supplied slice directly: this task is the whole
245 // server, so one out-of-range index would kill it silently.
246 trace!(
247 "First 4 bytes of icmp_payload: {}",
248 hex::encode(&icmp_payload[..std::cmp::min(icmp_payload.len(), 4)])
249 );
250
251 // DEBUG: Log summary
252 debug!(
253 "ICMP {} from {} to {} (TTL: {})",
254 icmp_type_to_string(icmp_type),
255 source_ip,
256 dest_ip,
257 ttl
258 );
259 let _ = status_tx.send(format!(
260 "[DEBUG] ICMP {} from {} to {} (TTL: {})",
261 icmp_type_to_string(icmp_type),
262 source_ip,
263 dest_ip,
264 ttl
265 ));
266
267 // TRACE: Log full packet
268 let hex_str = hex::encode(ip_packet.payload());
269 console_trace!(status_tx, "ICMP packet (hex): {}", hex_str);
270
271 let llm_clone = llm_client.clone();
272 let state_clone = app_state.clone();
273 let status_clone = status_tx.clone();
274 let protocol_task_clone = protocol_clone.clone();
275 let send_socket_clone = send_socket.clone();
276 // Store the full ICMP packet (including header) for parsing by specific packet types
277 // Use the unwrapped payload in case of IP-in-IP encapsulation
278 let icmp_packet_data = icmp_payload.to_vec();
279
280 // Spawn async task to handle packet with LLM
281 runtime.spawn(async move {
282 // Build event based on ICMP type
283 let event = match icmp_type {
284 IcmpTypes::EchoRequest => {
285 // Parse echo request
286 if let Some(echo_req) =
287 EchoRequestPacket::new(&icmp_packet_data)
288 {
289 let identifier = echo_req.get_identifier();
290 let sequence = echo_req.get_sequence_number();
291 let payload_hex = hex::encode(echo_req.payload());
292
293 Event::new(
294 &ICMP_ECHO_REQUEST_EVENT,
295 serde_json::json!({
296 "source_ip": source_ip.to_string(),
297 "destination_ip": dest_ip.to_string(),
298 // Numbers, not strings. The event type
299 // declares both as `number` and
300 // `execute_send_echo_reply` reads them with
301 // `as_u64()`, so a stringified id made the
302 // protocol's own scripted startup example
303 // ("identifier": event["identifier"]) fail with
304 // "Missing 'identifier' parameter".
305 "identifier": identifier,
306 "sequence": sequence,
307 "payload_hex": payload_hex,
308 "ttl": ttl,
309 }),
310 )
311 } else {
312 debug!("Failed to parse ICMP echo request");
313 return;
314 }
315 }
316 /* TODO: Timestamp support requires pnet to add timestamp packet types
317 IcmpTypes::Timestamp => {
318 // Parse timestamp request
319 if let Some(ts_req) = TimestampPacket::new(&icmp_payload) {
320 let identifier = ts_req.get_identifier();
321 let sequence = ts_req.get_sequence_number();
322 let originate_timestamp = ts_req.get_originate_timestamp();
323
324 Event::new(
325 &ICMP_TIMESTAMP_REQUEST_EVENT,
326 serde_json::json!({
327 "source_ip": source_ip.to_string(),
328 "destination_ip": dest_ip.to_string(),
329 "identifier": identifier,
330 "sequence": sequence,
331 "originate_timestamp": originate_timestamp,
332 }),
333 )
334 } else {
335 debug!("Failed to parse ICMP timestamp request");
336 return;
337 }
338 }
339 */
340 _ => {
341 // Other ICMP types
342 Event::new(
343 &ICMP_OTHER_MESSAGE_EVENT,
344 serde_json::json!({
345 "source_ip": source_ip.to_string(),
346 "destination_ip": dest_ip.to_string(),
347 "icmp_type": icmp_type.0,
348 "icmp_code": icmp_code.0,
349 "packet_hex": hex::encode(&icmp_packet_data),
350 }),
351 )
352 }
353 };
354
355 debug!(
356 "ICMP calling LLM for {} packet",
357 icmp_type_to_string(icmp_type)
358 );
359 let _ = status_clone.send(format!(
360 "[DEBUG] ICMP calling LLM for {} packet",
361 icmp_type_to_string(icmp_type)
362 ));
363
364 match call_llm(
365 &llm_clone,
366 &state_clone,
367 server_id,
368 None,
369 &event,
370 protocol_task_clone.as_ref(),
371 )
372 .await
373 {
374 Ok(execution_result) => {
375 for message in &execution_result.messages {
376 info!("{}", message);
377 let _ = status_clone.send(format!("[INFO] {}", message));
378 }
379
380 debug!(
381 "ICMP got {} protocol results",
382 execution_result.protocol_results.len()
383 );
384 let _ = status_clone.send(format!(
385 "[DEBUG] ICMP got {} protocol results",
386 execution_result.protocol_results.len()
387 ));
388
389 // An explicit `ignore_icmp` and an empty answer both put
390 // nothing on the wire, but only one of them is a decision.
391 // Keep them apart in the log so an operator can tell a
392 // deliberate silent honeypot from a model that said nothing.
393 let answered_nothing = execution_result.raw_actions.is_empty();
394 let explicit_ignore = !answered_nothing
395 && execution_result.raw_actions.iter().all(|a| {
396 a.get("type").and_then(|v| v.as_str())
397 == Some("ignore_icmp")
398 });
399 let action_failures = execution_result.failures.len();
400 let mut packets_sent = 0usize;
401
402 // Send ICMP replies if any
403 for protocol_result in execution_result.protocol_results {
404 // Every output, not just the first: an answer of two
405 // echo replies used to put one packet on the wire and
406 // drop the other without a word.
407 for mut output_data in protocol_result.get_all_output() {
408 // Send packet via raw socket
409 // Extract destination from the IP header in output_data
410 let dest_ip_of_reply = Ipv4Packet::new(&output_data)
411 .map(|ip_pkt| ip_pkt.get_destination());
412 let Some(reply_dest) = dest_ip_of_reply else {
413 error!("Failed to parse IP packet from LLM output");
414 continue;
415 };
416 let dest_addr =
417 std::net::SocketAddr::from((reply_dest, 0));
418
419 // Last thing before the syscall: on Darwin/FreeBSD
420 // the two fields below must reach the kernel in
421 // host byte order. After this the buffer is no
422 // longer a well-formed RFC 791 header, so nothing
423 // may parse it again.
424 prepare_ipv4_for_raw_send(&mut output_data);
425
426 match send_socket_clone
427 .send_to(&output_data, &dest_addr.into())
428 {
429 Ok(_) => {
430 packets_sent += 1;
431 debug!(
432 "ICMP sent {} bytes to {}",
433 output_data.len(),
434 dest_addr.ip()
435 );
436 let _ = status_clone.send(format!(
437 "[DEBUG] ICMP sent {} bytes to {}",
438 output_data.len(),
439 dest_addr.ip()
440 ));
441
442 trace!(
443 "ICMP reply (hex): {}",
444 hex::encode(&output_data)
445 );
446 let _ = status_clone.send(format!(
447 "[TRACE] ICMP reply (hex): {}",
448 hex::encode(&output_data)
449 ));
450 }
451 Err(e) => {
452 error!("Failed to send ICMP reply: {}", e);
453 let _ = status_clone.send(format!(
454 "[ERROR] Failed to send ICMP reply: {}",
455 e
456 ));
457 }
458 }
459 }
460 }
461
462 // The token is stable so an operator can grep
463 // `decision=fail_closed_` and find every packet that went
464 // unanswered because no usable answer was produced, as
465 // distinct from one the model chose to ignore.
466 let decision = if packets_sent > 0 {
467 "model_reply"
468 } else if explicit_ignore {
469 "model_ignore"
470 } else if answered_nothing {
471 "fail_closed_no_action"
472 } else if action_failures > 0 {
473 "fail_closed_action_error"
474 } else {
475 "fail_closed_no_reply"
476 };
477 let summary = format!(
478 "ICMP {} {} -> {} decision={} sent={}",
479 icmp_type_to_string(icmp_type),
480 source_ip,
481 dest_ip,
482 decision,
483 packets_sent
484 );
485 if decision.starts_with("fail_closed_") {
486 error!("{} (nothing put on the wire)", summary);
487 let _ = status_clone
488 .send(format!("✗ {} (nothing put on the wire)", summary));
489 } else {
490 info!("{}", summary);
491 let _ = status_clone.send(format!("→ {}", summary));
492 }
493 }
494 Err(e) => {
495 // ICMP has no in-band way to say "I cannot answer": RFC 792
496 // defines no failure reply for an echo request, and RFC 1122
497 // §3.2.2 forbids generating an ICMP error in response to an
498 // ICMP error — a synthesised Destination Unreachable would be
499 // a lie about reachability, not a service-unavailable signal.
500 // Silence is therefore the protocol-correct outcome here; what
501 // matters is that it is loud in the log and distinguishable
502 // from an `ignore_icmp` decision. The error text is logged,
503 // never derived into anything a peer could read.
504 let category = if crate::utils::WireFailure::classify(&e)
505 .is_overloaded()
506 {
507 "overloaded"
508 } else {
509 "unavailable"
510 };
511 error!(
512 "ICMP {} {} -> {} decision=fail_closed_llm_error category={} (no reply: ICMP has no failure message): {}",
513 icmp_type_to_string(icmp_type),
514 source_ip,
515 dest_ip,
516 category,
517 e
518 );
519 let _ = status_clone.send(format!(
520 "✗ ICMP {} {} -> {} decision=fail_closed_llm_error category={}: {}",
521 icmp_type_to_string(icmp_type),
522 source_ip,
523 dest_ip,
524 category,
525 e
526 ));
527 }
528 }
529 });
530 }
531 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
532 // No data available, sleep briefly
533 std::thread::sleep(std::time::Duration::from_millis(10));
534 continue;
535 }
536 Err(e) => {
537 console_error!(status_tx, "ICMP receive error: {}", e);
538 break;
539 }
540 }
541 }
542
543 // Close both raw sockets. `send_socket` is an `Arc` shared with in-flight reply
544 // tasks, so it closes once the last of them finishes.
545 drop(socket);
546 drop(send_socket);
547 });
548
549 // Wait for the blocking task to report whether the raw sockets actually opened.
550 match ready_rx.await {
551 Ok(Ok(())) => {}
552 Ok(Err(e)) => return Err(e),
553 Err(_) => {
554 return Err(anyhow::anyhow!(
555 "ICMP receive task for '{}' exited before signalling readiness",
556 interface
557 ))
558 }
559 }
560
561 // Only now that the sockets are genuinely open: `stop_server` aborts this parked task,
562 // which trips `stop` and ends the blocking loop above.
563 app_state_reg
564 .register_server_task(server_id, stop.park_task())
565 .await;
566
567 console_info!(status_tx_ready, "ICMP raw sockets active on {}", interface);
568
569 Ok(interface)
570 }
571
572 /// Helper function to build an ICMP echo reply packet with IP header
573 pub fn build_echo_reply(
574 source_ip: Ipv4Addr,
575 dest_ip: Ipv4Addr,
576 identifier: u16,
577 sequence: u16,
578 payload: &[u8],
579 ) -> Vec<u8> {
580 use pnet::packet::icmp::echo_reply::MutableEchoReplyPacket;
581 use pnet::packet::icmp::IcmpTypes;
582 use pnet::packet::ipv4::checksum;
583
584 // ICMP echo reply: 8 bytes header + payload
585 let icmp_size = 8 + payload.len();
586 let mut icmp_buffer = vec![0u8; icmp_size];
587
588 {
589 let mut echo_reply = MutableEchoReplyPacket::new(&mut icmp_buffer).unwrap();
590 echo_reply.set_icmp_type(IcmpTypes::EchoReply);
591 echo_reply.set_icmp_code(IcmpCode::new(0));
592 echo_reply.set_identifier(identifier);
593 echo_reply.set_sequence_number(sequence);
594 echo_reply.set_payload(payload);
595 }
596
597 // Calculate ICMP checksum
598 let icmp_checksum = {
599 let icmp_packet = MutableIcmpPacket::new(&mut icmp_buffer).unwrap();
600 pnet::packet::icmp::checksum(&icmp_packet.to_immutable())
601 };
602
603 {
604 let mut echo_reply = MutableEchoReplyPacket::new(&mut icmp_buffer).unwrap();
605 echo_reply.set_checksum(icmp_checksum);
606 }
607
608 // Wrap in IP packet
609 let ip_size = 20 + icmp_size;
610 let mut ip_buffer = vec![0u8; ip_size];
611
612 {
613 let mut ip_packet = MutableIpv4Packet::new(&mut ip_buffer).unwrap();
614 ip_packet.set_version(4);
615 ip_packet.set_header_length(5); // 5 * 4 = 20 bytes
616 ip_packet.set_dscp(0);
617 ip_packet.set_ecn(0);
618 ip_packet.set_total_length(ip_size as u16);
619 ip_packet.set_identification(0);
620 ip_packet.set_flags(0);
621 ip_packet.set_fragment_offset(0);
622 ip_packet.set_ttl(64);
623 ip_packet.set_next_level_protocol(IpNextHeaderProtocols::Icmp);
624 ip_packet.set_source(source_ip);
625 ip_packet.set_destination(dest_ip);
626 ip_packet.set_payload(&icmp_buffer);
627
628 // Calculate IP checksum
629 let ip_checksum = checksum(&ip_packet.to_immutable());
630 ip_packet.set_checksum(ip_checksum);
631 }
632
633 ip_buffer
634 }
635
636 /// Helper function to build an ICMP destination unreachable packet
637 pub fn build_destination_unreachable(
638 source_ip: Ipv4Addr,
639 dest_ip: Ipv4Addr,
640 code: u8,
641 original_packet: &[u8],
642 ) -> Vec<u8> {
643 use pnet::packet::icmp::destination_unreachable::MutableDestinationUnreachablePacket;
644 use pnet::packet::icmp::IcmpTypes;
645 use pnet::packet::ipv4::checksum;
646
647 // Take first 28 bytes of original packet (IP header + 8 bytes of data)
648 let payload_len = std::cmp::min(original_packet.len(), 28);
649 let payload = &original_packet[..payload_len];
650
651 // ICMP destination unreachable: 8 bytes header + original packet fragment
652 let icmp_size = 8 + payload.len();
653 let mut icmp_buffer = vec![0u8; icmp_size];
654
655 {
656 let mut dest_unreach =
657 MutableDestinationUnreachablePacket::new(&mut icmp_buffer).unwrap();
658 dest_unreach.set_icmp_type(IcmpTypes::DestinationUnreachable);
659 dest_unreach.set_icmp_code(IcmpCode::new(code));
660 dest_unreach.set_payload(payload);
661 }
662
663 // Calculate ICMP checksum
664 let icmp_checksum = {
665 let icmp_packet = MutableIcmpPacket::new(&mut icmp_buffer).unwrap();
666 pnet::packet::icmp::checksum(&icmp_packet.to_immutable())
667 };
668
669 {
670 let mut dest_unreach =
671 MutableDestinationUnreachablePacket::new(&mut icmp_buffer).unwrap();
672 dest_unreach.set_checksum(icmp_checksum);
673 }
674
675 // Wrap in IP packet
676 let ip_size = 20 + icmp_size;
677 let mut ip_buffer = vec![0u8; ip_size];
678
679 {
680 let mut ip_packet = MutableIpv4Packet::new(&mut ip_buffer).unwrap();
681 ip_packet.set_version(4);
682 ip_packet.set_header_length(5);
683 ip_packet.set_total_length(ip_size as u16);
684 ip_packet.set_ttl(64);
685 ip_packet.set_next_level_protocol(IpNextHeaderProtocols::Icmp);
686 ip_packet.set_source(source_ip);
687 ip_packet.set_destination(dest_ip);
688 ip_packet.set_payload(&icmp_buffer);
689
690 let ip_checksum = checksum(&ip_packet.to_immutable());
691 ip_packet.set_checksum(ip_checksum);
692 }
693
694 ip_buffer
695 }
696
697 /// Helper function to build an ICMP time exceeded packet
698 pub fn build_time_exceeded(
699 source_ip: Ipv4Addr,
700 dest_ip: Ipv4Addr,
701 code: u8,
702 original_packet: &[u8],
703 ) -> Vec<u8> {
704 use pnet::packet::icmp::time_exceeded::MutableTimeExceededPacket;
705 use pnet::packet::icmp::IcmpTypes;
706 use pnet::packet::ipv4::checksum;
707
708 // Take first 28 bytes of original packet
709 let payload_len = std::cmp::min(original_packet.len(), 28);
710 let payload = &original_packet[..payload_len];
711
712 let icmp_size = 8 + payload.len();
713 let mut icmp_buffer = vec![0u8; icmp_size];
714
715 {
716 let mut time_exceeded = MutableTimeExceededPacket::new(&mut icmp_buffer).unwrap();
717 time_exceeded.set_icmp_type(IcmpTypes::TimeExceeded);
718 time_exceeded.set_icmp_code(IcmpCode::new(code));
719 time_exceeded.set_payload(payload);
720 }
721
722 // Calculate ICMP checksum
723 let icmp_checksum = {
724 let icmp_packet = MutableIcmpPacket::new(&mut icmp_buffer).unwrap();
725 pnet::packet::icmp::checksum(&icmp_packet.to_immutable())
726 };
727
728 {
729 let mut time_exceeded = MutableTimeExceededPacket::new(&mut icmp_buffer).unwrap();
730 time_exceeded.set_checksum(icmp_checksum);
731 }
732
733 // Wrap in IP packet
734 let ip_size = 20 + icmp_size;
735 let mut ip_buffer = vec![0u8; ip_size];
736
737 {
738 let mut ip_packet = MutableIpv4Packet::new(&mut ip_buffer).unwrap();
739 ip_packet.set_version(4);
740 ip_packet.set_header_length(5);
741 ip_packet.set_total_length(ip_size as u16);
742 ip_packet.set_ttl(64);
743 ip_packet.set_next_level_protocol(IpNextHeaderProtocols::Icmp);
744 ip_packet.set_source(source_ip);
745 ip_packet.set_destination(dest_ip);
746 ip_packet.set_payload(&icmp_buffer);
747
748 let ip_checksum = checksum(&ip_packet.to_immutable());
749 ip_packet.set_checksum(ip_checksum);
750 }
751
752 ip_buffer
753 }
754
755 /* TODO: Timestamp support requires pnet to add timestamp_reply packet types
756 /// Helper function to build an ICMP timestamp reply packet
757 pub fn build_timestamp_reply(
758 source_ip: Ipv4Addr,
759 dest_ip: Ipv4Addr,
760 identifier: u16,
761 sequence: u16,
762 originate_timestamp: u32,
763 ) -> Vec<u8> {
764 use pnet::packet::icmp::timestamp_reply::MutableTimestampReplyPacket;
765 use pnet::packet::icmp::IcmpTypes;
766 use pnet::packet::ipv4::checksum;
767
768 // Get current time in milliseconds since midnight UT
769 let now = crate::utils::clock::SystemTime::now()
770 .duration_since(crate::utils::clock::UNIX_EPOCH)
771 .unwrap()
772 .as_millis() as u32;
773 let receive_timestamp = now;
774 let transmit_timestamp = now;
775
776 let icmp_size = 20; // Timestamp reply is always 20 bytes
777 let mut icmp_buffer = vec![0u8; icmp_size];
778
779 {
780 let mut ts_reply = MutableTimestampReplyPacket::new(&mut icmp_buffer).unwrap();
781 ts_reply.set_icmp_type(IcmpTypes::TimestampReply);
782 ts_reply.set_icmp_code(IcmpCode::new(0));
783 ts_reply.set_identifier(identifier);
784 ts_reply.set_sequence_number(sequence);
785 ts_reply.set_originate_timestamp(originate_timestamp);
786 ts_reply.set_receive_timestamp(receive_timestamp);
787 ts_reply.set_transmit_timestamp(transmit_timestamp);
788
789 let icmp_packet = MutableIcmpPacket::new(&mut icmp_buffer).unwrap();
790 let checksum = pnet::packet::icmp::checksum(&icmp_packet.to_immutable());
791 ts_reply.set_checksum(checksum);
792 }
793
794 // Wrap in IP packet
795 let ip_size = 20 + icmp_size;
796 let mut ip_buffer = vec![0u8; ip_size];
797
798 {
799 let mut ip_packet = MutableIpv4Packet::new(&mut ip_buffer).unwrap();
800 ip_packet.set_version(4);
801 ip_packet.set_header_length(5);
802 ip_packet.set_total_length(ip_size as u16);
803 ip_packet.set_ttl(64);
804 ip_packet.set_next_level_protocol(IpNextHeaderProtocols::Icmp);
805 ip_packet.set_source(source_ip);
806 ip_packet.set_destination(dest_ip);
807 ip_packet.set_payload(&icmp_buffer);
808
809 let checksum = checksum(&ip_packet.to_immutable());
810 ip_packet.set_checksum(checksum);
811 }
812
813 ip_buffer
814 }
815 */
816}
817
818/// Put a finished IPv4 packet into the form the local kernel expects on an `IP_HDRINCL`
819/// raw socket, immediately before `send_to`.
820///
821/// On Linux every field goes out in network byte order, so this is a no-op. On Darwin and
822/// FreeBSD the raw-IP output path reads `ip_len` and `ip_off` in **host** byte order — XNU's
823/// `rip_output` sanity-checks `ip->ip_len > m->m_pkthdr.len` against the real buffer length, so
824/// a network-order length on a 28-byte packet reads as 7168 and the send fails outright with
825/// `EINVAL`. `pnet_transport` 0.35 does the same conversion for its Layer3 channel (see
826/// `send_to_impl`, `man 4 ip` "Raw IP Sockets"); this is that conversion, without taking on the
827/// whole transport channel.
828///
829/// The header checksum is zeroed rather than recomputed: it was calculated over the
830/// network-order fields and cannot be valid once two of them are byte-swapped, and every raw-IP
831/// implementation either always fills the checksum in (`man 7 raw` on Linux lists "IP Checksum:
832/// always filled in") or fills it when zero. Leaving a stale value is wrong in every case where
833/// zeroing is, and wrong in one case where zeroing is not.
834///
835/// **Not verified against a running kernel.** Nothing in this repo has ever sent on a raw
836/// socket; this is derived from the kernel sources and from pnet's long-standing workaround, and
837/// `tests/server/icmp/packet_codec_test.rs` asserts only that the bytes come out as intended for
838/// the platform being compiled for.
839#[cfg(any(
840 target_os = "macos",
841 target_os = "ios",
842 target_os = "tvos",
843 target_os = "freebsd"
844))]
845pub fn prepare_ipv4_for_raw_send(packet: &mut [u8]) {
846 if packet.len() < 20 {
847 return;
848 }
849 // `to_ne_bytes` is a no-op on a big-endian host, which is what "host byte order" means
850 // there; only little-endian needs the swap.
851 let total_length = u16::from_be_bytes([packet[2], packet[3]]);
852 packet[2..4].copy_from_slice(&total_length.to_ne_bytes());
853 let flags_and_fragment_offset = u16::from_be_bytes([packet[6], packet[7]]);
854 packet[6..8].copy_from_slice(&flags_and_fragment_offset.to_ne_bytes());
855 packet[10] = 0;
856 packet[11] = 0;
857}
858
859/// Linux and everything else: the header already is what the kernel wants.
860#[cfg(not(any(
861 target_os = "macos",
862 target_os = "ios",
863 target_os = "tvos",
864 target_os = "freebsd"
865)))]
866pub fn prepare_ipv4_for_raw_send(_packet: &mut [u8]) {}
867
868/// Convert ICMP type to human-readable string
869fn icmp_type_to_string(icmp_type: pnet::packet::icmp::IcmpType) -> &'static str {
870 match icmp_type {
871 IcmpTypes::EchoReply => "ECHO_REPLY",
872 IcmpTypes::EchoRequest => "ECHO_REQUEST",
873 IcmpTypes::DestinationUnreachable => "DEST_UNREACHABLE",
874 IcmpTypes::SourceQuench => "SOURCE_QUENCH",
875 IcmpTypes::RedirectMessage => "REDIRECT",
876 IcmpTypes::TimeExceeded => "TIME_EXCEEDED",
877 IcmpTypes::ParameterProblem => "PARAMETER_PROBLEM",
878 IcmpTypes::Timestamp => "TIMESTAMP",
879 IcmpTypes::TimestampReply => "TIMESTAMP_REPLY",
880 IcmpTypes::InformationRequest => "INFO_REQUEST",
881 IcmpTypes::InformationReply => "INFO_REPLY",
882 IcmpTypes::AddressMaskRequest => "ADDRMASK_REQUEST",
883 IcmpTypes::AddressMaskReply => "ADDRMASK_REPLY",
884 IcmpTypes::Traceroute => "TRACEROUTE",
885 _ => "UNKNOWN",
886 }
887}