Skip to main content

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}