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