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 dst_ip == config.gateway_ipv4 {
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)
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 dst_ip == config.gateway_ipv6 {
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)
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.rx_ring.push(frame).is_ok() {
482        shared.add_rx_bytes(frame_len);
483        shared.rx_wake.wake();
484        tracing::debug!(dst = %dst_ip, seq_no = reply_seq, frame_len, "ICMPv4 echo reply injected");
485    } else {
486        tracing::debug!("ICMP echo reply dropped — rx_ring full");
487    }
488
489    Ok(())
490}
491
492/// Send one ICMPv6 echo request, receive reply, and inject a guest frame.
493#[allow(clippy::too_many_arguments)]
494async fn icmpv6_echo_task(
495    dst_ip: Ipv6Addr,
496    guest_src_ip: Ipv6Addr,
497    guest_ident: u16,
498    seq_no: u16,
499    echo_data: Vec<u8>,
500    shared: Arc<SharedState>,
501    gateway_mac: EthernetAddress,
502    guest_mac: EthernetAddress,
503) -> std::io::Result<()> {
504    let socket = open_icmp_socket_v6(dst_ip)?;
505
506    let icmp_repr = Icmpv6Repr::EchoRequest {
507        ident: guest_ident,
508        seq_no,
509        data: &echo_data,
510    };
511    let mut icmp_buf = vec![0u8; icmp_repr.buffer_len()];
512    // For SOCK_DGRAM+IPPROTO_ICMPV6, the kernel computes the checksum,
513    // so the addresses used here for emit are only for serialization.
514    icmp_repr.emit(
515        &guest_src_ip,
516        &dst_ip,
517        &mut Icmpv6Packet::new_unchecked(&mut icmp_buf),
518        &smoltcp::phy::ChecksumCapabilities::default(),
519    );
520
521    socket.send(&icmp_buf).await?;
522
523    let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
524    let n = tokio::time::timeout(ECHO_TIMEOUT, socket.recv(&mut recv_buf))
525        .await
526        .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "ICMPv6 echo timeout"))??;
527
528    let (reply_seq, reply_data) = parse_icmpv6_echo_reply(&recv_buf[..n], dst_ip, guest_src_ip)?;
529
530    let frame = construct_icmpv6_echo_reply(
531        dst_ip,
532        guest_src_ip,
533        guest_ident,
534        reply_seq,
535        reply_data,
536        gateway_mac,
537        guest_mac,
538    );
539
540    let frame_len = frame.len();
541    if shared.rx_ring.push(frame).is_ok() {
542        shared.add_rx_bytes(frame_len);
543        shared.rx_wake.wake();
544        tracing::debug!(dst = %dst_ip, seq_no = reply_seq, frame_len, "ICMPv6 echo reply injected");
545    } else {
546        tracing::debug!("ICMPv6 echo reply dropped — rx_ring full");
547    }
548
549    Ok(())
550}
551
552/// Parse an ICMPv4 Echo Reply from a host ping socket receive buffer.
553///
554/// Some hosts return a bare ICMP message while others prepend the IPv4 header.
555fn parse_icmpv4_echo_reply(buf: &[u8]) -> std::io::Result<(u16, &[u8])> {
556    if let Ok(reply_icmp) = Icmpv4Packet::new_checked(buf)
557        && let Ok(Icmpv4Repr::EchoReply {
558            ident: _,
559            seq_no,
560            data,
561        }) = Icmpv4Repr::parse(&reply_icmp, &smoltcp::phy::ChecksumCapabilities::default())
562    {
563        return Ok((seq_no, data));
564    }
565
566    let reply_icmp = Icmpv4Packet::new_checked(extract_ipv4_icmp_payload(buf)?)
567        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
568    let Icmpv4Repr::EchoReply {
569        ident: _,
570        seq_no,
571        data,
572    } = Icmpv4Repr::parse(&reply_icmp, &smoltcp::phy::ChecksumCapabilities::default())
573        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
574    else {
575        return Err(std::io::Error::new(
576            std::io::ErrorKind::InvalidData,
577            "host ICMPv4 reply was not an echo reply",
578        ));
579    };
580
581    Ok((seq_no, data))
582}
583
584/// Parse an ICMPv6 Echo Reply from a host ping socket receive buffer.
585///
586/// Some hosts return a bare ICMPv6 message while others may prepend an IPv6
587/// header. The checksum is validated against the expected remote/guest pair.
588fn parse_icmpv6_echo_reply(
589    buf: &[u8],
590    remote_ip: Ipv6Addr,
591    guest_ip: Ipv6Addr,
592) -> std::io::Result<(u16, &[u8])> {
593    if let Ok(reply_icmp) = Icmpv6Packet::new_checked(buf)
594        && let Ok(Icmpv6Repr::EchoReply {
595            ident: _,
596            seq_no,
597            data,
598        }) = Icmpv6Repr::parse(
599            &remote_ip,
600            &guest_ip,
601            &reply_icmp,
602            &smoltcp::phy::ChecksumCapabilities::default(),
603        )
604    {
605        return Ok((seq_no, data));
606    }
607
608    let reply_icmp = Icmpv6Packet::new_checked(extract_ipv6_icmp_payload(buf)?)
609        .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
610    let Icmpv6Repr::EchoReply {
611        ident: _,
612        seq_no,
613        data,
614    } = Icmpv6Repr::parse(
615        &remote_ip,
616        &guest_ip,
617        &reply_icmp,
618        &smoltcp::phy::ChecksumCapabilities::default(),
619    )
620    .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?
621    else {
622        return Err(std::io::Error::new(
623            std::io::ErrorKind::InvalidData,
624            "host ICMPv6 reply was not an echo reply",
625        ));
626    };
627
628    Ok((seq_no, data))
629}
630
631/// Extract the ICMP payload from an IPv4-framed host ping-socket reply.
632///
633/// Some hosts prepend an IPv4 header that is not a fully self-consistent
634/// wire packet, so this parser intentionally validates only the fields we
635/// need to locate the embedded ICMP payload.
636fn extract_ipv4_icmp_payload(buf: &[u8]) -> std::io::Result<&[u8]> {
637    if buf.len() < IPV4_HDR_LEN {
638        return Err(std::io::Error::new(
639            std::io::ErrorKind::InvalidData,
640            "host ICMPv4 reply was shorter than an IPv4 header",
641        ));
642    }
643
644    let version = buf[0] >> 4;
645    let header_len = usize::from(buf[0] & 0x0f) * 4;
646    if version != 4 || header_len < IPV4_HDR_LEN || header_len > buf.len() {
647        return Err(std::io::Error::new(
648            std::io::ErrorKind::InvalidData,
649            "host ICMPv4 reply did not contain a usable IPv4 header",
650        ));
651    }
652    if buf[9] != IpProtocol::Icmp.into() {
653        return Err(std::io::Error::new(
654            std::io::ErrorKind::InvalidData,
655            "host ICMPv4 reply did not contain an ICMP payload",
656        ));
657    }
658
659    Ok(&buf[header_len..])
660}
661
662/// Extract the ICMPv6 payload from an IPv6-framed host ping-socket reply.
663fn extract_ipv6_icmp_payload(buf: &[u8]) -> std::io::Result<&[u8]> {
664    if buf.len() < IPV6_HDR_LEN {
665        return Err(std::io::Error::new(
666            std::io::ErrorKind::InvalidData,
667            "host ICMPv6 reply was shorter than an IPv6 header",
668        ));
669    }
670
671    let version = buf[0] >> 4;
672    if version != 6 {
673        return Err(std::io::Error::new(
674            std::io::ErrorKind::InvalidData,
675            "host ICMPv6 reply did not contain a usable IPv6 header",
676        ));
677    }
678    if buf[6] != IpProtocol::Icmpv6.into() {
679        return Err(std::io::Error::new(
680            std::io::ErrorKind::InvalidData,
681            "host ICMPv6 reply did not contain an ICMPv6 payload",
682        ));
683    }
684
685    Ok(&buf[IPV6_HDR_LEN..])
686}
687
688/// Construct an Ethernet + IPv4 + ICMPv4 Echo Reply frame for the guest.
689fn construct_icmpv4_echo_reply(
690    src_ip: Ipv4Addr,
691    dst_ip: Ipv4Addr,
692    ident: u16,
693    seq_no: u16,
694    data: &[u8],
695    gateway_mac: EthernetAddress,
696    guest_mac: EthernetAddress,
697) -> Vec<u8> {
698    let icmp_repr = Icmpv4Repr::EchoReply {
699        ident,
700        seq_no,
701        data,
702    };
703    let ipv4_repr = Ipv4Repr {
704        src_addr: src_ip,
705        dst_addr: dst_ip,
706        next_header: IpProtocol::Icmp,
707        payload_len: icmp_repr.buffer_len(),
708        hop_limit: 64,
709    };
710    let frame_len = ETH_HDR_LEN + ipv4_repr.buffer_len() + icmp_repr.buffer_len();
711    let mut buf = vec![0u8; frame_len];
712
713    // Ethernet header.
714    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
715    EthernetRepr {
716        src_addr: gateway_mac,
717        dst_addr: guest_mac,
718        ethertype: EthernetProtocol::Ipv4,
719    }
720    .emit(&mut eth_frame);
721
722    // IPv4 header.
723    ipv4_repr.emit(
724        &mut Ipv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN..ETH_HDR_LEN + IPV4_HDR_LEN]),
725        &smoltcp::phy::ChecksumCapabilities::default(),
726    );
727
728    // ICMP header + payload.
729    icmp_repr.emit(
730        &mut Icmpv4Packet::new_unchecked(&mut buf[ETH_HDR_LEN + IPV4_HDR_LEN..]),
731        &smoltcp::phy::ChecksumCapabilities::default(),
732    );
733
734    buf
735}
736
737/// Construct an Ethernet + IPv6 + ICMPv6 Echo Reply frame for the guest.
738fn construct_icmpv6_echo_reply(
739    src_ip: Ipv6Addr,
740    dst_ip: Ipv6Addr,
741    ident: u16,
742    seq_no: u16,
743    data: &[u8],
744    gateway_mac: EthernetAddress,
745    guest_mac: EthernetAddress,
746) -> Vec<u8> {
747    let icmp_repr = Icmpv6Repr::EchoReply {
748        ident,
749        seq_no,
750        data,
751    };
752    let frame_len = ETH_HDR_LEN + IPV6_HDR_LEN + icmp_repr.buffer_len();
753    let mut buf = vec![0u8; frame_len];
754
755    // Ethernet header.
756    let mut eth_frame = EthernetFrame::new_unchecked(&mut buf);
757    EthernetRepr {
758        src_addr: gateway_mac,
759        dst_addr: guest_mac,
760        ethertype: EthernetProtocol::Ipv6,
761    }
762    .emit(&mut eth_frame);
763
764    // IPv6 header.
765    Ipv6Repr {
766        src_addr: src_ip,
767        dst_addr: dst_ip,
768        next_header: IpProtocol::Icmpv6,
769        payload_len: icmp_repr.buffer_len(),
770        hop_limit: 64,
771    }
772    .emit(&mut Ipv6Packet::new_unchecked(
773        &mut buf[ETH_HDR_LEN..ETH_HDR_LEN + IPV6_HDR_LEN],
774    ));
775
776    // ICMPv6 header + payload (checksum computed from src/dst addresses).
777    icmp_repr.emit(
778        &src_ip,
779        &dst_ip,
780        &mut Icmpv6Packet::new_unchecked(&mut buf[ETH_HDR_LEN + IPV6_HDR_LEN..]),
781        &smoltcp::phy::ChecksumCapabilities::default(),
782    );
783
784    buf
785}
786
787//--------------------------------------------------------------------------------------------------
788// Tests
789//--------------------------------------------------------------------------------------------------
790
791#[cfg(test)]
792mod tests {
793    use super::*;
794
795    use smoltcp::phy::ChecksumCapabilities;
796
797    #[test]
798    fn construct_icmpv4_reply_roundtrips() {
799        let frame = construct_icmpv4_echo_reply(
800            Ipv4Addr::new(8, 8, 8, 8),
801            Ipv4Addr::new(100, 96, 0, 2),
802            0x1234,
803            0x0001,
804            b"hello",
805            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
806            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
807        );
808
809        let eth = EthernetFrame::new_checked(&frame).unwrap();
810        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv4);
811        assert_eq!(
812            eth.src_addr(),
813            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01])
814        );
815        assert_eq!(
816            eth.dst_addr(),
817            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02])
818        );
819
820        let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
821        assert_eq!(Ipv4Addr::from(ipv4.src_addr()), Ipv4Addr::new(8, 8, 8, 8));
822        assert_eq!(
823            Ipv4Addr::from(ipv4.dst_addr()),
824            Ipv4Addr::new(100, 96, 0, 2)
825        );
826        assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
827
828        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
829        let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
830        assert_eq!(
831            repr,
832            Icmpv4Repr::EchoReply {
833                ident: 0x1234,
834                seq_no: 0x0001,
835                data: b"hello",
836            }
837        );
838    }
839
840    #[test]
841    fn construct_icmpv6_reply_roundtrips() {
842        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
843        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
844        let frame = construct_icmpv6_echo_reply(
845            src,
846            dst,
847            0x5678,
848            0x0002,
849            b"v6ping",
850            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
851            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
852        );
853
854        let eth = EthernetFrame::new_checked(&frame).unwrap();
855        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv6);
856
857        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
858        assert_eq!(ipv6.next_header(), IpProtocol::Icmpv6);
859
860        let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
861        let repr = Icmpv6Repr::parse(
862            &src.into(),
863            &dst.into(),
864            &icmp,
865            &ChecksumCapabilities::default(),
866        )
867        .unwrap();
868        assert_eq!(
869            repr,
870            Icmpv6Repr::EchoReply {
871                ident: 0x5678,
872                seq_no: 0x0002,
873                data: b"v6ping",
874            }
875        );
876
877        // Verify ICMPv6 checksum is non-zero (mandatory per RFC 8200).
878        assert_ne!(icmp.checksum(), 0, "ICMPv6 checksum must not be zero");
879        assert!(
880            icmp.verify_checksum(
881                &smoltcp::wire::Ipv6Address::from(src),
882                &smoltcp::wire::Ipv6Address::from(dst),
883            ),
884            "ICMPv6 checksum must be valid"
885        );
886    }
887
888    #[test]
889    fn construct_icmpv4_reply_preserves_ident_and_seqno() {
890        let frame = construct_icmpv4_echo_reply(
891            Ipv4Addr::new(1, 2, 3, 4),
892            Ipv4Addr::new(10, 0, 0, 2),
893            0xABCD,
894            0xEF01,
895            b"test-payload",
896            EthernetAddress([0; 6]),
897            EthernetAddress([0; 6]),
898        );
899
900        let eth = EthernetFrame::new_checked(&frame).unwrap();
901        let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
902        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
903        let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
904        assert_eq!(
905            repr,
906            Icmpv4Repr::EchoReply {
907                ident: 0xABCD,
908                seq_no: 0xEF01,
909                data: b"test-payload",
910            }
911        );
912    }
913
914    #[test]
915    fn construct_icmpv6_reply_preserves_ident_and_seqno() {
916        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
917        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
918        let frame = construct_icmpv6_echo_reply(
919            src,
920            dst,
921            0xBEEF,
922            0xCAFE,
923            b"test6",
924            EthernetAddress([0; 6]),
925            EthernetAddress([0; 6]),
926        );
927
928        let eth = EthernetFrame::new_checked(&frame).unwrap();
929        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
930        let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
931        let repr = Icmpv6Repr::parse(
932            &src.into(),
933            &dst.into(),
934            &icmp,
935            &ChecksumCapabilities::default(),
936        )
937        .unwrap();
938        assert_eq!(
939            repr,
940            Icmpv6Repr::EchoReply {
941                ident: 0xBEEF,
942                seq_no: 0xCAFE,
943                data: b"test6",
944            }
945        );
946    }
947
948    #[test]
949    fn probe_does_not_panic() {
950        // Result depends on host — just verify it doesn't panic.
951        let _ = probe_icmp_socket_v4();
952        let _ = probe_icmp_socket_v6();
953    }
954
955    #[test]
956    fn parse_icmpv4_reply_accepts_bare_icmp() {
957        let icmp_repr = Icmpv4Repr::EchoReply {
958            ident: 0x1234,
959            seq_no: 0x0001,
960            data: b"hello",
961        };
962        let mut buf = vec![0u8; icmp_repr.buffer_len()];
963        icmp_repr.emit(
964            &mut Icmpv4Packet::new_unchecked(&mut buf),
965            &ChecksumCapabilities::default(),
966        );
967
968        let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
969        assert_eq!(seq_no, 0x0001);
970        assert_eq!(data, b"hello");
971    }
972
973    #[test]
974    fn parse_icmpv4_reply_accepts_ipv4_plus_icmp() {
975        let frame = construct_icmpv4_echo_reply(
976            Ipv4Addr::new(8, 8, 8, 8),
977            Ipv4Addr::new(100, 96, 0, 2),
978            0x1234,
979            0x0001,
980            b"hello",
981            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
982            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
983        );
984        let eth = EthernetFrame::new_checked(&frame).unwrap();
985
986        let (seq_no, data) = parse_icmpv4_echo_reply(eth.payload()).unwrap();
987        assert_eq!(seq_no, 0x0001);
988        assert_eq!(data, b"hello");
989    }
990
991    #[test]
992    fn parse_icmpv4_reply_accepts_macos_ping_socket_shape() {
993        let buf = [
994            0x45, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x01, 0x73, 0xef, 0x08, 0x08,
995            0x08, 0x08, 0xc0, 0xa8, 0x01, 0x35, 0x00, 0x00, 0xa9, 0xf8, 0x12, 0x34, 0x00, 0x01,
996            0x68, 0x65, 0x6c, 0x6c, 0x6f,
997        ];
998
999        let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
1000        assert_eq!(seq_no, 0x0001);
1001        assert_eq!(data, b"hello");
1002    }
1003
1004    #[test]
1005    fn parse_icmpv6_reply_accepts_bare_icmpv6() {
1006        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
1007        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
1008        let icmp_repr = Icmpv6Repr::EchoReply {
1009            ident: 0x1234,
1010            seq_no: 0x0002,
1011            data: b"hello6",
1012        };
1013        let mut buf = vec![0u8; icmp_repr.buffer_len()];
1014        icmp_repr.emit(
1015            &src.into(),
1016            &dst.into(),
1017            &mut Icmpv6Packet::new_unchecked(&mut buf),
1018            &ChecksumCapabilities::default(),
1019        );
1020
1021        let (seq_no, data) = parse_icmpv6_echo_reply(&buf, src, dst).unwrap();
1022        assert_eq!(seq_no, 0x0002);
1023        assert_eq!(data, b"hello6");
1024    }
1025
1026    #[test]
1027    fn parse_icmpv6_reply_accepts_ipv6_plus_icmpv6() {
1028        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
1029        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
1030        let frame = construct_icmpv6_echo_reply(
1031            src,
1032            dst,
1033            0x5678,
1034            0x0002,
1035            b"v6ping",
1036            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1037            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1038        );
1039        let eth = EthernetFrame::new_checked(&frame).unwrap();
1040
1041        let (seq_no, data) = parse_icmpv6_echo_reply(eth.payload(), src, dst).unwrap();
1042        assert_eq!(seq_no, 0x0002);
1043        assert_eq!(data, b"v6ping");
1044    }
1045}