Skip to main content

microsandbox_network/
icmp_relay.rs

1//! External ICMP echo-only relay: host probe + reply frame synthesis.
2//!
3//! Relays outbound ICMP Echo Request packets from the guest to the real
4//! network via unprivileged `SOCK_DGRAM + IPPROTO_ICMP` sockets, then
5//! synthesizes Echo Reply frames back into `rx_ring`.
6//!
7//! Only Echo Request/Reply is supported. Non-echo ICMP (traceroute,
8//! destination unreachable, etc.) is intentionally not relayed.
9
10use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
11use std::os::fd::FromRawFd;
12use std::sync::Arc;
13
14use smoltcp::wire::{
15    EthernetAddress, EthernetFrame, EthernetProtocol, EthernetRepr, Icmpv4Packet, Icmpv4Repr,
16    Icmpv6Packet, Icmpv6Repr, IpProtocol, Ipv4Packet, Ipv4Repr, Ipv6Packet, Ipv6Repr,
17};
18
19use crate::policy::{NetworkPolicy, Protocol};
20use crate::shared::SharedState;
21use crate::stack::PollLoopConfig;
22
23//--------------------------------------------------------------------------------------------------
24// Constants
25//--------------------------------------------------------------------------------------------------
26
27/// Timeout for each ICMP echo probe.
28const ECHO_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
29
30/// Receive buffer size for ICMP replies.
31const RECV_BUF_SIZE: usize = 1500;
32
33/// Ethernet header length.
34const ETH_HDR_LEN: usize = 14;
35
36/// IPv4 header length (no options).
37const IPV4_HDR_LEN: usize = 20;
38
39/// IPv6 header length.
40const IPV6_HDR_LEN: usize = 40;
41
42//--------------------------------------------------------------------------------------------------
43// Types
44//--------------------------------------------------------------------------------------------------
45
46/// Whether unprivileged ICMP echo sockets are available on this host.
47///
48/// Probed once at construction, per address family. Availability may differ
49/// between IPv4 and IPv6 on the same host.
50#[derive(Debug, Clone, Copy)]
51enum EchoBackend {
52    /// The address family-specific ping socket probe succeeded.
53    Available,
54    /// Probe failed — ICMP relay is disabled.
55    Unavailable,
56}
57
58/// Relays ICMP echo requests from the guest to the real network via
59/// unprivileged ICMP sockets.
60///
61/// Each echo request spawns a fire-and-forget tokio task. No session
62/// table is needed — ping traffic is low-volume and each probe is
63/// independent.
64pub struct IcmpRelay {
65    shared: Arc<SharedState>,
66    gateway_mac: EthernetAddress,
67    guest_mac: EthernetAddress,
68    tokio_handle: tokio::runtime::Handle,
69    backend_v4: EchoBackend,
70    backend_v6: EchoBackend,
71}
72
73//--------------------------------------------------------------------------------------------------
74// Methods
75//--------------------------------------------------------------------------------------------------
76
77impl IcmpRelay {
78    /// Create a new ICMP relay, probing for unprivileged socket support.
79    pub fn new(
80        shared: Arc<SharedState>,
81        gateway_mac: [u8; 6],
82        guest_mac: [u8; 6],
83        tokio_handle: tokio::runtime::Handle,
84    ) -> Self {
85        let backend_v4 = probe_icmp_socket_v4();
86        let backend_v6 = probe_icmp_socket_v6();
87
88        if matches!(backend_v4, EchoBackend::Unavailable) {
89            tracing::debug!(
90                "unprivileged ICMPv4 echo sockets unavailable — external ICMPv4 relay disabled"
91            );
92        }
93        if matches!(backend_v6, EchoBackend::Unavailable) {
94            tracing::debug!(
95                "unprivileged ICMPv6 echo sockets unavailable — external ICMPv6 relay disabled"
96            );
97        }
98
99        Self {
100            shared,
101            gateway_mac: EthernetAddress(gateway_mac),
102            guest_mac: EthernetAddress(guest_mac),
103            tokio_handle,
104            backend_v4,
105            backend_v6,
106        }
107    }
108
109    /// Try to intercept an outbound frame as an ICMP echo request.
110    ///
111    /// Returns `true` if the frame was consumed (caller should
112    /// `drop_staged_frame()`). Returns `false` if the frame is not an
113    /// ICMP echo request or the backend is unavailable — caller should
114    /// fall through to `classify_frame`.
115    pub fn relay_outbound_if_echo(
116        &self,
117        frame: &[u8],
118        config: &PollLoopConfig,
119        policy: &NetworkPolicy,
120    ) -> bool {
121        let Ok(eth) = EthernetFrame::new_checked(frame) else {
122            return false;
123        };
124
125        match eth.ethertype() {
126            EthernetProtocol::Ipv4 if matches!(self.backend_v4, EchoBackend::Available) => {
127                self.try_relay_icmpv4(&eth, config, policy)
128            }
129            EthernetProtocol::Ipv6 if matches!(self.backend_v6, EchoBackend::Available) => {
130                self.try_relay_icmpv6(&eth, config, policy)
131            }
132            _ => false,
133        }
134    }
135}
136
137impl IcmpRelay {
138    /// Try to relay an ICMPv4 echo request. Returns true if consumed.
139    fn try_relay_icmpv4(
140        &self,
141        eth: &EthernetFrame<&[u8]>,
142        config: &PollLoopConfig,
143        policy: &NetworkPolicy,
144    ) -> bool {
145        let Ok(ipv4) = Ipv4Packet::new_checked(eth.payload()) else {
146            return false;
147        };
148        if ipv4.next_header() != IpProtocol::Icmp {
149            return false;
150        }
151
152        // Gateway echo is already handled upstream — skip.
153        let dst_ip: Ipv4Addr = ipv4.dst_addr();
154        if config.gateway.ipv4 == Some(dst_ip) {
155            return false;
156        }
157
158        let Ok(icmp) = Icmpv4Packet::new_checked(ipv4.payload()) else {
159            return false;
160        };
161        let Ok(Icmpv4Repr::EchoRequest {
162            ident,
163            seq_no,
164            data,
165        }) = Icmpv4Repr::parse(&icmp, &smoltcp::phy::ChecksumCapabilities::default())
166        else {
167            return false; // Not an echo request — fall through.
168        };
169
170        // Policy check.
171        if policy
172            .evaluate_egress_ip(IpAddr::V4(dst_ip), Protocol::Icmpv4, &self.shared)
173            .is_deny()
174        {
175            tracing::debug!(dst = %dst_ip, "ICMP echo denied by policy");
176            return true; // Consumed (silently dropped by policy).
177        }
178
179        let src_ip: Ipv4Addr = ipv4.src_addr();
180        let guest_ident = ident;
181        let echo_data = data.to_vec();
182
183        let shared = self.shared.clone();
184        let gateway_mac = self.gateway_mac;
185        let guest_mac = self.guest_mac;
186
187        tracing::debug!(dst = %dst_ip, seq_no, bytes = echo_data.len(), "relaying ICMPv4 echo request");
188
189        self.tokio_handle.spawn(async move {
190            if let Err(e) = icmpv4_echo_task(
191                dst_ip,
192                src_ip,
193                guest_ident,
194                seq_no,
195                echo_data,
196                shared,
197                gateway_mac,
198                guest_mac,
199            )
200            .await
201            {
202                tracing::debug!(dst = %dst_ip, error = %e, "ICMPv4 echo relay failed");
203            }
204        });
205
206        true
207    }
208
209    /// Try to relay an ICMPv6 echo request. Returns true if consumed.
210    fn try_relay_icmpv6(
211        &self,
212        eth: &EthernetFrame<&[u8]>,
213        config: &PollLoopConfig,
214        policy: &NetworkPolicy,
215    ) -> bool {
216        let Ok(ipv6) = Ipv6Packet::new_checked(eth.payload()) else {
217            return false;
218        };
219        if ipv6.next_header() != IpProtocol::Icmpv6 {
220            return false;
221        }
222
223        // Gateway echo is already handled upstream — skip.
224        let dst_ip: Ipv6Addr = ipv6.dst_addr();
225        if config.gateway.ipv6 == Some(dst_ip) {
226            return false;
227        }
228
229        let Ok(icmp) = Icmpv6Packet::new_checked(ipv6.payload()) else {
230            return false;
231        };
232        let Ok(Icmpv6Repr::EchoRequest {
233            ident,
234            seq_no,
235            data,
236        }) = Icmpv6Repr::parse(
237            &ipv6.src_addr(),
238            &ipv6.dst_addr(),
239            &icmp,
240            &smoltcp::phy::ChecksumCapabilities::default(),
241        )
242        else {
243            return false; // Not an echo request — fall through.
244        };
245
246        // Policy check.
247        if policy
248            .evaluate_egress_ip(IpAddr::V6(dst_ip), Protocol::Icmpv6, &self.shared)
249            .is_deny()
250        {
251            tracing::debug!(dst = %dst_ip, "ICMPv6 echo denied by policy");
252            return true;
253        }
254
255        let src_ip: Ipv6Addr = ipv6.src_addr();
256        let guest_ident = ident;
257        let echo_data = data.to_vec();
258
259        let shared = self.shared.clone();
260        let gateway_mac = self.gateway_mac;
261        let guest_mac = self.guest_mac;
262
263        tracing::debug!(dst = %dst_ip, seq_no, bytes = echo_data.len(), "relaying ICMPv6 echo request");
264
265        self.tokio_handle.spawn(async move {
266            if let Err(e) = icmpv6_echo_task(
267                dst_ip,
268                src_ip,
269                guest_ident,
270                seq_no,
271                echo_data,
272                shared,
273                gateway_mac,
274                guest_mac,
275            )
276            .await
277            {
278                tracing::debug!(dst = %dst_ip, error = %e, "ICMPv6 echo relay failed");
279            }
280        });
281
282        true
283    }
284}
285
286//--------------------------------------------------------------------------------------------------
287// Functions
288//--------------------------------------------------------------------------------------------------
289
290/// Probe whether `SOCK_DGRAM + IPPROTO_ICMP` is available.
291fn probe_icmp_socket_v4() -> EchoBackend {
292    // SAFETY: socket() with valid args; immediately closed on success.
293    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
294    if fd >= 0 {
295        unsafe { libc::close(fd) };
296        EchoBackend::Available
297    } else {
298        EchoBackend::Unavailable
299    }
300}
301
302/// Probe whether `SOCK_DGRAM + IPPROTO_ICMPV6` is available.
303fn probe_icmp_socket_v6() -> EchoBackend {
304    // SAFETY: socket() with valid args; immediately closed on success.
305    let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, libc::IPPROTO_ICMPV6) };
306    if fd >= 0 {
307        unsafe { libc::close(fd) };
308        EchoBackend::Available
309    } else {
310        EchoBackend::Unavailable
311    }
312}
313
314/// Open an unprivileged ICMPv4 socket connected to `dst`.
315///
316/// Uses `SOCK_DGRAM + IPPROTO_ICMP` which the kernel intercepts to
317/// provide unprivileged ping. The socket behaves like a connected UDP
318/// socket but carries ICMP echo payloads.
319///
320/// Note: the kernel rewrites the ICMP identifier field to match the
321/// socket's ephemeral "port" assignment. The caller must restore the
322/// guest's original identifier on the reply.
323fn open_icmp_socket_v4(dst: Ipv4Addr) -> std::io::Result<tokio::net::UdpSocket> {
324    // SAFETY: socket() + fcntl() + connect() with valid args.
325    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
326    if fd < 0 {
327        return Err(std::io::Error::last_os_error());
328    }
329
330    // Set non-blocking + close-on-exec via fcntl (portable across macOS/Linux).
331    if let Err(e) = set_nonblock_cloexec(fd) {
332        unsafe { libc::close(fd) };
333        return Err(e);
334    }
335
336    let addr = libc::sockaddr_in {
337        sin_family: libc::AF_INET as libc::sa_family_t,
338        sin_port: 0,
339        sin_addr: libc::in_addr {
340            s_addr: u32::from(dst).to_be(),
341        },
342        sin_zero: [0; 8],
343        #[cfg(target_os = "macos")]
344        sin_len: std::mem::size_of::<libc::sockaddr_in>() as u8,
345    };
346
347    // SAFETY: connect() with valid sockaddr_in.
348    let ret = unsafe {
349        libc::connect(
350            fd,
351            &addr as *const libc::sockaddr_in as *const libc::sockaddr,
352            std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
353        )
354    };
355    if ret < 0 {
356        let err = std::io::Error::last_os_error();
357        unsafe { libc::close(fd) };
358        return Err(err);
359    }
360
361    // SAFETY: fd is a valid, connected, non-blocking socket.
362    let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
363    tokio::net::UdpSocket::from_std(std_sock)
364}
365
366/// Open an unprivileged ICMPv6 socket connected to `dst`.
367fn open_icmp_socket_v6(dst: Ipv6Addr) -> std::io::Result<tokio::net::UdpSocket> {
368    let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, libc::IPPROTO_ICMPV6) };
369    if fd < 0 {
370        return Err(std::io::Error::last_os_error());
371    }
372
373    if let Err(e) = set_nonblock_cloexec(fd) {
374        unsafe { libc::close(fd) };
375        return Err(e);
376    }
377
378    let addr = libc::sockaddr_in6 {
379        sin6_family: libc::AF_INET6 as libc::sa_family_t,
380        sin6_port: 0,
381        sin6_flowinfo: 0,
382        sin6_addr: libc::in6_addr {
383            s6_addr: dst.octets(),
384        },
385        sin6_scope_id: 0,
386        #[cfg(target_os = "macos")]
387        sin6_len: std::mem::size_of::<libc::sockaddr_in6>() as u8,
388    };
389
390    let ret = unsafe {
391        libc::connect(
392            fd,
393            &addr as *const libc::sockaddr_in6 as *const libc::sockaddr,
394            std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
395        )
396    };
397    if ret < 0 {
398        let err = std::io::Error::last_os_error();
399        unsafe { libc::close(fd) };
400        return Err(err);
401    }
402
403    let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
404    tokio::net::UdpSocket::from_std(std_sock)
405}
406
407/// Set `O_NONBLOCK` and `FD_CLOEXEC` on a file descriptor.
408fn set_nonblock_cloexec(fd: libc::c_int) -> std::io::Result<()> {
409    unsafe {
410        let flags = libc::fcntl(fd, libc::F_GETFL);
411        if flags < 0 {
412            return Err(std::io::Error::last_os_error());
413        }
414        if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
415            return Err(std::io::Error::last_os_error());
416        }
417        let flags = libc::fcntl(fd, libc::F_GETFD);
418        if flags < 0 {
419            return Err(std::io::Error::last_os_error());
420        }
421        if libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0 {
422            return Err(std::io::Error::last_os_error());
423        }
424    }
425    Ok(())
426}
427
428/// Send one ICMPv4 echo request, receive reply, and inject a guest frame.
429#[allow(clippy::too_many_arguments)]
430async fn icmpv4_echo_task(
431    dst_ip: Ipv4Addr,
432    guest_src_ip: Ipv4Addr,
433    guest_ident: u16,
434    seq_no: u16,
435    echo_data: Vec<u8>,
436    shared: Arc<SharedState>,
437    gateway_mac: EthernetAddress,
438    guest_mac: EthernetAddress,
439) -> std::io::Result<()> {
440    let socket = open_icmp_socket_v4(dst_ip)?;
441
442    // Build the ICMP echo request payload.
443    // For SOCK_DGRAM+IPPROTO_ICMP, we send the ICMP header + data
444    // (type, code, checksum, ident, seq_no, data). The kernel
445    // rewrites ident to match the socket's ephemeral assignment.
446    let icmp_repr = Icmpv4Repr::EchoRequest {
447        ident: guest_ident,
448        seq_no,
449        data: &echo_data,
450    };
451    let mut icmp_buf = vec![0u8; icmp_repr.buffer_len()];
452    icmp_repr.emit(
453        &mut Icmpv4Packet::new_unchecked(&mut icmp_buf),
454        &smoltcp::phy::ChecksumCapabilities::default(),
455    );
456
457    socket.send(&icmp_buf).await?;
458
459    // Receive the echo reply. Different hosts may return either:
460    // - a bare ICMP message, or
461    // - an IP packet containing the ICMP message.
462    let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
463    let n = tokio::time::timeout(ECHO_TIMEOUT, socket.recv(&mut recv_buf))
464        .await
465        .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "ICMP echo timeout"))??;
466
467    let (reply_seq, reply_data) = parse_icmpv4_echo_reply(&recv_buf[..n])?;
468
469    // Construct the reply frame with the guest's ORIGINAL ident restored.
470    let frame = construct_icmpv4_echo_reply(
471        dst_ip,
472        guest_src_ip,
473        guest_ident,
474        reply_seq,
475        reply_data,
476        gateway_mac,
477        guest_mac,
478    );
479
480    let frame_len = frame.len();
481    if shared.push_rx_frame_and_wake(frame) {
482        tracing::debug!(dst = %dst_ip, seq_no = reply_seq, frame_len, "ICMPv4 echo reply injected");
483    } else {
484        tracing::debug!("ICMP echo reply dropped — rx_ring full");
485    }
486
487    Ok(())
488}
489
490/// Send one ICMPv6 echo request, receive reply, and inject a guest frame.
491#[allow(clippy::too_many_arguments)]
492async fn icmpv6_echo_task(
493    dst_ip: Ipv6Addr,
494    guest_src_ip: Ipv6Addr,
495    guest_ident: u16,
496    seq_no: u16,
497    echo_data: Vec<u8>,
498    shared: Arc<SharedState>,
499    gateway_mac: EthernetAddress,
500    guest_mac: EthernetAddress,
501) -> std::io::Result<()> {
502    let socket = open_icmp_socket_v6(dst_ip)?;
503
504    let icmp_repr = Icmpv6Repr::EchoRequest {
505        ident: guest_ident,
506        seq_no,
507        data: &echo_data,
508    };
509    let mut icmp_buf = vec![0u8; icmp_repr.buffer_len()];
510    // For SOCK_DGRAM+IPPROTO_ICMPV6, the kernel computes the checksum,
511    // so the addresses used here for emit are only for serialization.
512    icmp_repr.emit(
513        &guest_src_ip,
514        &dst_ip,
515        &mut Icmpv6Packet::new_unchecked(&mut icmp_buf),
516        &smoltcp::phy::ChecksumCapabilities::default(),
517    );
518
519    socket.send(&icmp_buf).await?;
520
521    let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
522    let n = tokio::time::timeout(ECHO_TIMEOUT, socket.recv(&mut recv_buf))
523        .await
524        .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "ICMPv6 echo timeout"))??;
525
526    let (reply_seq, reply_data) = parse_icmpv6_echo_reply(&recv_buf[..n], dst_ip, guest_src_ip)?;
527
528    let frame = construct_icmpv6_echo_reply(
529        dst_ip,
530        guest_src_ip,
531        guest_ident,
532        reply_seq,
533        reply_data,
534        gateway_mac,
535        guest_mac,
536    );
537
538    let frame_len = frame.len();
539    if shared.push_rx_frame_and_wake(frame) {
540        tracing::debug!(dst = %dst_ip, seq_no = reply_seq, frame_len, "ICMPv6 echo reply injected");
541    } else {
542        tracing::debug!("ICMPv6 echo reply dropped — rx_ring full");
543    }
544
545    Ok(())
546}
547
548/// Parse an ICMPv4 Echo Reply from a host ping socket receive buffer.
549///
550/// Some hosts return a bare ICMP message while others prepend the IPv4 header.
551fn parse_icmpv4_echo_reply(buf: &[u8]) -> std::io::Result<(u16, &[u8])> {
552    if let Ok(reply_icmp) = Icmpv4Packet::new_checked(buf)
553        && let Ok(Icmpv4Repr::EchoReply {
554            ident: _,
555            seq_no,
556            data,
557        }) = Icmpv4Repr::parse(&reply_icmp, &smoltcp::phy::ChecksumCapabilities::default())
558    {
559        return Ok((seq_no, data));
560    }
561
562    let reply_icmp = Icmpv4Packet::new_checked(extract_ipv4_icmp_payload(buf)?)
563        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
564    let Icmpv4Repr::EchoReply {
565        ident: _,
566        seq_no,
567        data,
568    } = Icmpv4Repr::parse(&reply_icmp, &smoltcp::phy::ChecksumCapabilities::default())
569        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
570    else {
571        return Err(std::io::Error::new(
572            std::io::ErrorKind::InvalidData,
573            "host ICMPv4 reply was not an echo reply",
574        ));
575    };
576
577    Ok((seq_no, data))
578}
579
580/// Parse an ICMPv6 Echo Reply from a host ping socket receive buffer.
581///
582/// Some hosts return a bare ICMPv6 message while others may prepend an IPv6
583/// header. The checksum is validated against the expected remote/guest pair.
584fn parse_icmpv6_echo_reply(
585    buf: &[u8],
586    remote_ip: Ipv6Addr,
587    guest_ip: Ipv6Addr,
588) -> std::io::Result<(u16, &[u8])> {
589    if let Ok(reply_icmp) = Icmpv6Packet::new_checked(buf)
590        && let Ok(Icmpv6Repr::EchoReply {
591            ident: _,
592            seq_no,
593            data,
594        }) = Icmpv6Repr::parse(
595            &remote_ip,
596            &guest_ip,
597            &reply_icmp,
598            &smoltcp::phy::ChecksumCapabilities::default(),
599        )
600    {
601        return Ok((seq_no, data));
602    }
603
604    let reply_icmp = Icmpv6Packet::new_checked(extract_ipv6_icmp_payload(buf)?)
605        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
606    let Icmpv6Repr::EchoReply {
607        ident: _,
608        seq_no,
609        data,
610    } = Icmpv6Repr::parse(
611        &remote_ip,
612        &guest_ip,
613        &reply_icmp,
614        &smoltcp::phy::ChecksumCapabilities::default(),
615    )
616    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
617    else {
618        return Err(std::io::Error::new(
619            std::io::ErrorKind::InvalidData,
620            "host ICMPv6 reply was not an echo reply",
621        ));
622    };
623
624    Ok((seq_no, data))
625}
626
627/// Extract the ICMP payload from an IPv4-framed host ping-socket reply.
628///
629/// Some hosts prepend an IPv4 header that is not a fully self-consistent
630/// wire packet, so this parser intentionally validates only the fields we
631/// need to locate the embedded ICMP payload.
632fn extract_ipv4_icmp_payload(buf: &[u8]) -> std::io::Result<&[u8]> {
633    if buf.len() < IPV4_HDR_LEN {
634        return Err(std::io::Error::new(
635            std::io::ErrorKind::InvalidData,
636            "host ICMPv4 reply was shorter than an IPv4 header",
637        ));
638    }
639
640    let version = buf[0] >> 4;
641    let header_len = usize::from(buf[0] & 0x0f) * 4;
642    if version != 4 || header_len < IPV4_HDR_LEN || header_len > buf.len() {
643        return Err(std::io::Error::new(
644            std::io::ErrorKind::InvalidData,
645            "host ICMPv4 reply did not contain a usable IPv4 header",
646        ));
647    }
648    if buf[9] != u8::from(IpProtocol::Icmp) {
649        return Err(std::io::Error::new(
650            std::io::ErrorKind::InvalidData,
651            "host ICMPv4 reply did not contain an ICMP payload",
652        ));
653    }
654
655    Ok(&buf[header_len..])
656}
657
658/// Extract the ICMPv6 payload from an IPv6-framed host ping-socket reply.
659fn extract_ipv6_icmp_payload(buf: &[u8]) -> std::io::Result<&[u8]> {
660    if buf.len() < IPV6_HDR_LEN {
661        return Err(std::io::Error::new(
662            std::io::ErrorKind::InvalidData,
663            "host ICMPv6 reply was shorter than an IPv6 header",
664        ));
665    }
666
667    let version = buf[0] >> 4;
668    if version != 6 {
669        return Err(std::io::Error::new(
670            std::io::ErrorKind::InvalidData,
671            "host ICMPv6 reply did not contain a usable IPv6 header",
672        ));
673    }
674    if buf[6] != u8::from(IpProtocol::Icmpv6) {
675        return Err(std::io::Error::new(
676            std::io::ErrorKind::InvalidData,
677            "host ICMPv6 reply did not contain an ICMPv6 payload",
678        ));
679    }
680
681    Ok(&buf[IPV6_HDR_LEN..])
682}
683
684/// Construct an Ethernet + IPv4 + ICMPv4 Echo Reply frame for the guest.
685fn construct_icmpv4_echo_reply(
686    src_ip: Ipv4Addr,
687    dst_ip: Ipv4Addr,
688    ident: u16,
689    seq_no: u16,
690    data: &[u8],
691    gateway_mac: EthernetAddress,
692    guest_mac: EthernetAddress,
693) -> Vec<u8> {
694    let icmp_repr = Icmpv4Repr::EchoReply {
695        ident,
696        seq_no,
697        data,
698    };
699    let ipv4_repr = Ipv4Repr {
700        src_addr: src_ip,
701        dst_addr: dst_ip,
702        next_header: IpProtocol::Icmp,
703        payload_len: icmp_repr.buffer_len(),
704        hop_limit: 64,
705    };
706    let frame_len = ETH_HDR_LEN + ipv4_repr.buffer_len() + icmp_repr.buffer_len();
707    let mut buf = vec![0u8; frame_len];
708
709    // Ethernet header.
710    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
711    EthernetRepr {
712        src_addr: gateway_mac,
713        dst_addr: guest_mac,
714        ethertype: EthernetProtocol::Ipv4,
715    }
716    .emit(&mut eth_frame);
717
718    // IPv4 header.
719    ipv4_repr.emit(
720        &mut Ipv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN..ETH_HDR_LEN + IPV4_HDR_LEN]),
721        &smoltcp::phy::ChecksumCapabilities::default(),
722    );
723
724    // ICMP header + payload.
725    icmp_repr.emit(
726        &mut Icmpv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN + IPV4_HDR_LEN..]),
727        &smoltcp::phy::ChecksumCapabilities::default(),
728    );
729
730    buf
731}
732
733/// Construct an Ethernet + IPv6 + ICMPv6 Echo Reply frame for the guest.
734fn construct_icmpv6_echo_reply(
735    src_ip: Ipv6Addr,
736    dst_ip: Ipv6Addr,
737    ident: u16,
738    seq_no: u16,
739    data: &[u8],
740    gateway_mac: EthernetAddress,
741    guest_mac: EthernetAddress,
742) -> Vec<u8> {
743    let icmp_repr = Icmpv6Repr::EchoReply {
744        ident,
745        seq_no,
746        data,
747    };
748    let frame_len = ETH_HDR_LEN + IPV6_HDR_LEN + icmp_repr.buffer_len();
749    let mut buf = vec![0u8; frame_len];
750
751    // Ethernet header.
752    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
753    EthernetRepr {
754        src_addr: gateway_mac,
755        dst_addr: guest_mac,
756        ethertype: EthernetProtocol::Ipv6,
757    }
758    .emit(&mut eth_frame);
759
760    // IPv6 header.
761    Ipv6Repr {
762        src_addr: src_ip,
763        dst_addr: dst_ip,
764        next_header: IpProtocol::Icmpv6,
765        payload_len: icmp_repr.buffer_len(),
766        hop_limit: 64,
767    }
768    .emit(&mut Ipv6Packet::new_unchecked(
769        &mut buf[ETH_HDR_LEN..ETH_HDR_LEN + IPV6_HDR_LEN],
770    ));
771
772    // ICMPv6 header + payload (checksum computed from src/dst addresses).
773    icmp_repr.emit(
774        &src_ip,
775        &dst_ip,
776        &mut Icmpv6Packet::new_unchecked(&mut buf[ETH_HDR_LEN + IPV6_HDR_LEN..]),
777        &smoltcp::phy::ChecksumCapabilities::default(),
778    );
779
780    buf
781}
782
783//--------------------------------------------------------------------------------------------------
784// Tests
785//--------------------------------------------------------------------------------------------------
786
787#[cfg(test)]
788mod tests {
789    use super::*;
790
791    use smoltcp::phy::ChecksumCapabilities;
792
793    #[test]
794    fn construct_icmpv4_reply_roundtrips() {
795        let frame = construct_icmpv4_echo_reply(
796            Ipv4Addr::new(8, 8, 8, 8),
797            Ipv4Addr::new(100, 96, 0, 2),
798            0x1234,
799            0x0001,
800            b"hello",
801            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
802            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
803        );
804
805        let eth = EthernetFrame::new_checked(&frame).unwrap();
806        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
807        assert_eq!(
808            eth.src_addr(),
809            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
810        );
811        assert_eq!(
812            eth.dst_addr(),
813            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02])
814        );
815
816        let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
817        assert_eq!(ipv4.src_addr(), Ipv4Addr::new(8, 8, 8, 8));
818        assert_eq!(ipv4.dst_addr(), Ipv4Addr::new(100, 96, 0, 2));
819        assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
820
821        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
822        let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
823        assert_eq!(
824            repr,
825            Icmpv4Repr::EchoReply {
826                ident: 0x1234,
827                seq_no: 0x0001,
828                data: b"hello",
829            }
830        );
831    }
832
833    #[test]
834    fn construct_icmpv6_reply_roundtrips() {
835        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
836        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
837        let frame = construct_icmpv6_echo_reply(
838            src,
839            dst,
840            0x5678,
841            0x0002,
842            b"v6ping",
843            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
844            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
845        );
846
847        let eth = EthernetFrame::new_checked(&frame).unwrap();
848        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv6);
849
850        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
851        assert_eq!(ipv6.next_header(), IpProtocol::Icmpv6);
852
853        let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
854        let repr = Icmpv6Repr::parse(&src, &dst, &icmp, &ChecksumCapabilities::default()).unwrap();
855        assert_eq!(
856            repr,
857            Icmpv6Repr::EchoReply {
858                ident: 0x5678,
859                seq_no: 0x0002,
860                data: b"v6ping",
861            }
862        );
863
864        // Verify ICMPv6 checksum is non-zero (mandatory per RFC 8200).
865        assert_ne!(icmp.checksum(), 0, "ICMPv6 checksum must not be zero");
866        assert!(
867            icmp.verify_checksum(&src, &dst,),
868            "ICMPv6 checksum must be valid"
869        );
870    }
871
872    #[test]
873    fn construct_icmpv4_reply_preserves_ident_and_seqno() {
874        let frame = construct_icmpv4_echo_reply(
875            Ipv4Addr::new(1, 2, 3, 4),
876            Ipv4Addr::new(10, 0, 0, 2),
877            0xABCD,
878            0xEF01,
879            b"test-payload",
880            EthernetAddress([0; 6]),
881            EthernetAddress([0; 6]),
882        );
883
884        let eth = EthernetFrame::new_checked(&frame).unwrap();
885        let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
886        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
887        let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
888        assert_eq!(
889            repr,
890            Icmpv4Repr::EchoReply {
891                ident: 0xABCD,
892                seq_no: 0xEF01,
893                data: b"test-payload",
894            }
895        );
896    }
897
898    #[test]
899    fn construct_icmpv6_reply_preserves_ident_and_seqno() {
900        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
901        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
902        let frame = construct_icmpv6_echo_reply(
903            src,
904            dst,
905            0xBEEF,
906            0xCAFE,
907            b"test6",
908            EthernetAddress([0; 6]),
909            EthernetAddress([0; 6]),
910        );
911
912        let eth = EthernetFrame::new_checked(&frame).unwrap();
913        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
914        let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
915        let repr = Icmpv6Repr::parse(&src, &dst, &icmp, &ChecksumCapabilities::default()).unwrap();
916        assert_eq!(
917            repr,
918            Icmpv6Repr::EchoReply {
919                ident: 0xBEEF,
920                seq_no: 0xCAFE,
921                data: b"test6",
922            }
923        );
924    }
925
926    #[test]
927    fn probe_does_not_panic() {
928        // Result depends on host — just verify it doesn't panic.
929        let _ = probe_icmp_socket_v4();
930        let _ = probe_icmp_socket_v6();
931    }
932
933    #[test]
934    fn parse_icmpv4_reply_accepts_bare_icmp() {
935        let icmp_repr = Icmpv4Repr::EchoReply {
936            ident: 0x1234,
937            seq_no: 0x0001,
938            data: b"hello",
939        };
940        let mut buf = vec![0u8; icmp_repr.buffer_len()];
941        icmp_repr.emit(
942            &mut Icmpv4Packet::new_unchecked(&mut buf),
943            &ChecksumCapabilities::default(),
944        );
945
946        let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
947        assert_eq!(seq_no, 0x0001);
948        assert_eq!(data, b"hello");
949    }
950
951    #[test]
952    fn parse_icmpv4_reply_accepts_ipv4_plus_icmp() {
953        let frame = construct_icmpv4_echo_reply(
954            Ipv4Addr::new(8, 8, 8, 8),
955            Ipv4Addr::new(100, 96, 0, 2),
956            0x1234,
957            0x0001,
958            b"hello",
959            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
960            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
961        );
962        let eth = EthernetFrame::new_checked(&frame).unwrap();
963
964        let (seq_no, data) = parse_icmpv4_echo_reply(eth.payload()).unwrap();
965        assert_eq!(seq_no, 0x0001);
966        assert_eq!(data, b"hello");
967    }
968
969    #[test]
970    fn parse_icmpv4_reply_accepts_macos_ping_socket_shape() {
971        let buf = [
972            0x45, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x01, 0x73, 0xef, 0x08, 0x08,
973            0x08, 0x08, 0xc0, 0xa8, 0x01, 0x35, 0x00, 0x00, 0xa9, 0xf8, 0x12, 0x34, 0x00, 0x01,
974            0x68, 0x65, 0x6c, 0x6c, 0x6f,
975        ];
976
977        let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
978        assert_eq!(seq_no, 0x0001);
979        assert_eq!(data, b"hello");
980    }
981
982    #[test]
983    fn parse_icmpv6_reply_accepts_bare_icmpv6() {
984        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
985        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
986        let icmp_repr = Icmpv6Repr::EchoReply {
987            ident: 0x1234,
988            seq_no: 0x0002,
989            data: b"hello6",
990        };
991        let mut buf = vec![0u8; icmp_repr.buffer_len()];
992        icmp_repr.emit(
993            &src,
994            &dst,
995            &mut Icmpv6Packet::new_unchecked(&mut buf),
996            &ChecksumCapabilities::default(),
997        );
998
999        let (seq_no, data) = parse_icmpv6_echo_reply(&buf, src, dst).unwrap();
1000        assert_eq!(seq_no, 0x0002);
1001        assert_eq!(data, b"hello6");
1002    }
1003
1004    #[test]
1005    fn parse_icmpv6_reply_accepts_ipv6_plus_icmpv6() {
1006        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
1007        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
1008        let frame = construct_icmpv6_echo_reply(
1009            src,
1010            dst,
1011            0x5678,
1012            0x0002,
1013            b"v6ping",
1014            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1015            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1016        );
1017        let eth = EthernetFrame::new_checked(&frame).unwrap();
1018
1019        let (seq_no, data) = parse_icmpv6_echo_reply(eth.payload(), src, dst).unwrap();
1020        assert_eq!(seq_no, 0x0002);
1021        assert_eq!(data, b"v6ping");
1022    }
1023}