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