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, &self.shared)
173            .is_deny()
174        {
175            tracing::debug!(dst = %dst_ip, "ICMP echo denied by policy");
176            return true; // Consumed (silently dropped by policy).
177        }
178
179        let src_ip: Ipv4Addr = ipv4.src_addr();
180        let guest_ident = ident;
181        let echo_data = data.to_vec();
182
183        let shared = self.shared.clone();
184        let gateway_mac = self.gateway_mac;
185        let guest_mac = self.guest_mac;
186
187        tracing::debug!(dst = %dst_ip, seq_no, bytes = echo_data.len(), "relaying ICMPv4 echo request");
188
189        self.tokio_handle.spawn(async move {
190            if let Err(e) = icmpv4_echo_task(
191                dst_ip,
192                src_ip,
193                guest_ident,
194                seq_no,
195                echo_data,
196                shared,
197                gateway_mac,
198                guest_mac,
199            )
200            .await
201            {
202                tracing::debug!(dst = %dst_ip, error = %e, "ICMPv4 echo relay failed");
203            }
204        });
205
206        true
207    }
208
209    /// Try to relay an ICMPv6 echo request. Returns true if consumed.
210    fn try_relay_icmpv6(
211        &self,
212        eth: &EthernetFrame<&[u8]>,
213        config: &PollLoopConfig,
214        policy: &NetworkPolicy,
215    ) -> bool {
216        let Ok(ipv6) = Ipv6Packet::new_checked(eth.payload()) else {
217            return false;
218        };
219        if ipv6.next_header() != IpProtocol::Icmpv6 {
220            return false;
221        }
222
223        // Gateway echo is already handled upstream — skip.
224        let dst_ip: Ipv6Addr = ipv6.dst_addr();
225        if 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, &self.shared)
249            .is_deny()
250        {
251            tracing::debug!(dst = %dst_ip, "ICMPv6 echo denied by policy");
252            return true;
253        }
254
255        let src_ip: Ipv6Addr = ipv6.src_addr();
256        let guest_ident = ident;
257        let echo_data = data.to_vec();
258
259        let shared = self.shared.clone();
260        let gateway_mac = self.gateway_mac;
261        let guest_mac = self.guest_mac;
262
263        tracing::debug!(dst = %dst_ip, seq_no, bytes = echo_data.len(), "relaying ICMPv6 echo request");
264
265        self.tokio_handle.spawn(async move {
266            if let Err(e) = icmpv6_echo_task(
267                dst_ip,
268                src_ip,
269                guest_ident,
270                seq_no,
271                echo_data,
272                shared,
273                gateway_mac,
274                guest_mac,
275            )
276            .await
277            {
278                tracing::debug!(dst = %dst_ip, error = %e, "ICMPv6 echo relay failed");
279            }
280        });
281
282        true
283    }
284}
285
286//--------------------------------------------------------------------------------------------------
287// Functions
288//--------------------------------------------------------------------------------------------------
289
290/// Probe whether `SOCK_DGRAM + IPPROTO_ICMP` is available.
291fn probe_icmp_socket_v4() -> EchoBackend {
292    // SAFETY: socket() with valid args; immediately closed on success.
293    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
294    if fd >= 0 {
295        unsafe { libc::close(fd) };
296        EchoBackend::Available
297    } else {
298        EchoBackend::Unavailable
299    }
300}
301
302/// Probe whether `SOCK_DGRAM + IPPROTO_ICMPV6` is available.
303fn probe_icmp_socket_v6() -> EchoBackend {
304    // SAFETY: socket() with valid args; immediately closed on success.
305    let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, libc::IPPROTO_ICMPV6) };
306    if fd >= 0 {
307        unsafe { libc::close(fd) };
308        EchoBackend::Available
309    } else {
310        EchoBackend::Unavailable
311    }
312}
313
314/// Open an unprivileged ICMPv4 socket connected to `dst`.
315///
316/// Uses `SOCK_DGRAM + IPPROTO_ICMP` which the kernel intercepts to
317/// provide unprivileged ping. The socket behaves like a connected UDP
318/// socket but carries ICMP echo payloads.
319///
320/// Note: the kernel rewrites the ICMP identifier field to match the
321/// socket's ephemeral "port" assignment. The caller must restore the
322/// guest's original identifier on the reply.
323fn open_icmp_socket_v4(dst: Ipv4Addr) -> std::io::Result<tokio::net::UdpSocket> {
324    // SAFETY: socket() + fcntl() + connect() with valid args.
325    let fd = unsafe { libc::socket(libc::AF_INET, libc::SOCK_DGRAM, libc::IPPROTO_ICMP) };
326    if fd < 0 {
327        return Err(std::io::Error::last_os_error());
328    }
329
330    // Set non-blocking + close-on-exec via fcntl (portable across macOS/Linux).
331    if let Err(e) = set_nonblock_cloexec(fd) {
332        unsafe { libc::close(fd) };
333        return Err(e);
334    }
335
336    let addr = libc::sockaddr_in {
337        sin_family: libc::AF_INET as libc::sa_family_t,
338        sin_port: 0,
339        sin_addr: libc::in_addr {
340            s_addr: u32::from(dst).to_be(),
341        },
342        sin_zero: [0; 8],
343        #[cfg(target_os = "macos")]
344        sin_len: std::mem::size_of::<libc::sockaddr_in>() as u8,
345    };
346
347    // SAFETY: connect() with valid sockaddr_in.
348    let ret = unsafe {
349        libc::connect(
350            fd,
351            &addr as *const libc::sockaddr_in as *const libc::sockaddr,
352            std::mem::size_of::<libc::sockaddr_in>() as libc::socklen_t,
353        )
354    };
355    if ret < 0 {
356        let err = std::io::Error::last_os_error();
357        unsafe { libc::close(fd) };
358        return Err(err);
359    }
360
361    // SAFETY: fd is a valid, connected, non-blocking socket.
362    let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
363    tokio::net::UdpSocket::from_std(std_sock)
364}
365
366/// Open an unprivileged ICMPv6 socket connected to `dst`.
367fn open_icmp_socket_v6(dst: Ipv6Addr) -> std::io::Result<tokio::net::UdpSocket> {
368    let fd = unsafe { libc::socket(libc::AF_INET6, libc::SOCK_DGRAM, libc::IPPROTO_ICMPV6) };
369    if fd < 0 {
370        return Err(std::io::Error::last_os_error());
371    }
372
373    if let Err(e) = set_nonblock_cloexec(fd) {
374        unsafe { libc::close(fd) };
375        return Err(e);
376    }
377
378    let addr = libc::sockaddr_in6 {
379        sin6_family: libc::AF_INET6 as libc::sa_family_t,
380        sin6_port: 0,
381        sin6_flowinfo: 0,
382        sin6_addr: libc::in6_addr {
383            s6_addr: dst.octets(),
384        },
385        sin6_scope_id: 0,
386        #[cfg(target_os = "macos")]
387        sin6_len: std::mem::size_of::<libc::sockaddr_in6>() as u8,
388    };
389
390    let ret = unsafe {
391        libc::connect(
392            fd,
393            &addr as *const libc::sockaddr_in6 as *const libc::sockaddr,
394            std::mem::size_of::<libc::sockaddr_in6>() as libc::socklen_t,
395        )
396    };
397    if ret < 0 {
398        let err = std::io::Error::last_os_error();
399        unsafe { libc::close(fd) };
400        return Err(err);
401    }
402
403    let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
404    tokio::net::UdpSocket::from_std(std_sock)
405}
406
407/// Set `O_NONBLOCK` and `FD_CLOEXEC` on a file descriptor.
408fn set_nonblock_cloexec(fd: libc::c_int) -> std::io::Result<()> {
409    unsafe {
410        let flags = libc::fcntl(fd, libc::F_GETFL);
411        if flags < 0 {
412            return Err(std::io::Error::last_os_error());
413        }
414        if libc::fcntl(fd, libc::F_SETFL, flags | libc::O_NONBLOCK) < 0 {
415            return Err(std::io::Error::last_os_error());
416        }
417        let flags = libc::fcntl(fd, libc::F_GETFD);
418        if flags < 0 {
419            return Err(std::io::Error::last_os_error());
420        }
421        if libc::fcntl(fd, libc::F_SETFD, flags | libc::FD_CLOEXEC) < 0 {
422            return Err(std::io::Error::last_os_error());
423        }
424    }
425    Ok(())
426}
427
428/// Send one ICMPv4 echo request, receive reply, and inject a guest frame.
429#[allow(clippy::too_many_arguments)]
430async fn icmpv4_echo_task(
431    dst_ip: Ipv4Addr,
432    guest_src_ip: Ipv4Addr,
433    guest_ident: u16,
434    seq_no: u16,
435    echo_data: Vec<u8>,
436    shared: Arc<SharedState>,
437    gateway_mac: EthernetAddress,
438    guest_mac: EthernetAddress,
439) -> std::io::Result<()> {
440    let socket = open_icmp_socket_v4(dst_ip)?;
441
442    // Build the ICMP echo request payload.
443    // For SOCK_DGRAM+IPPROTO_ICMP, we send the ICMP header + data
444    // (type, code, checksum, ident, seq_no, data). The kernel
445    // rewrites ident to match the socket's ephemeral assignment.
446    let icmp_repr = Icmpv4Repr::EchoRequest {
447        ident: guest_ident,
448        seq_no,
449        data: &echo_data,
450    };
451    let mut icmp_buf = vec![0u8; icmp_repr.buffer_len()];
452    icmp_repr.emit(
453        &mut Icmpv4Packet::new_unchecked(&mut icmp_buf),
454        &smoltcp::phy::ChecksumCapabilities::default(),
455    );
456
457    socket.send(&icmp_buf).await?;
458
459    // Receive the echo reply. Different hosts may return either:
460    // - a bare ICMP message, or
461    // - an IP packet containing the ICMP message.
462    let mut recv_buf = vec![0u8; RECV_BUF_SIZE];
463    let n = tokio::time::timeout(ECHO_TIMEOUT, socket.recv(&mut recv_buf))
464        .await
465        .map_err(|_| std::io::Error::new(std::io::ErrorKind::TimedOut, "ICMP echo timeout"))??;
466
467    let (reply_seq, reply_data) = parse_icmpv4_echo_reply(&recv_buf[..n])?;
468
469    // Construct the reply frame with the guest's ORIGINAL ident restored.
470    let frame = construct_icmpv4_echo_reply(
471        dst_ip,
472        guest_src_ip,
473        guest_ident,
474        reply_seq,
475        reply_data,
476        gateway_mac,
477        guest_mac,
478    );
479
480    let frame_len = frame.len();
481    if shared.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] != u8::from(IpProtocol::Icmp) {
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] != u8::from(IpProtocol::Icmpv6) {
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!(ipv4.src_addr(), Ipv4Addr::new(8, 8, 8, 8));
822        assert_eq!(ipv4.dst_addr(), Ipv4Addr::new(100, 96, 0, 2));
823        assert_eq!(ipv4.next_header(), IpProtocol::Icmp);
824
825        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
826        let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
827        assert_eq!(
828            repr,
829            Icmpv4Repr::EchoReply {
830                ident: 0x1234,
831                seq_no: 0x0001,
832                data: b"hello",
833            }
834        );
835    }
836
837    #[test]
838    fn construct_icmpv6_reply_roundtrips() {
839        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
840        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
841        let frame = construct_icmpv6_echo_reply(
842            src,
843            dst,
844            0x5678,
845            0x0002,
846            b"v6ping",
847            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
848            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
849        );
850
851        let eth = EthernetFrame::new_checked(&frame).unwrap();
852        assert_eq!(eth.ethertype(), EthernetProtocol::Ipv6);
853
854        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
855        assert_eq!(ipv6.next_header(), IpProtocol::Icmpv6);
856
857        let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
858        let repr = Icmpv6Repr::parse(&src, &dst, &icmp, &ChecksumCapabilities::default()).unwrap();
859        assert_eq!(
860            repr,
861            Icmpv6Repr::EchoReply {
862                ident: 0x5678,
863                seq_no: 0x0002,
864                data: b"v6ping",
865            }
866        );
867
868        // Verify ICMPv6 checksum is non-zero (mandatory per RFC 8200).
869        assert_ne!(icmp.checksum(), 0, "ICMPv6 checksum must not be zero");
870        assert!(
871            icmp.verify_checksum(&src, &dst,),
872            "ICMPv6 checksum must be valid"
873        );
874    }
875
876    #[test]
877    fn construct_icmpv4_reply_preserves_ident_and_seqno() {
878        let frame = construct_icmpv4_echo_reply(
879            Ipv4Addr::new(1, 2, 3, 4),
880            Ipv4Addr::new(10, 0, 0, 2),
881            0xABCD,
882            0xEF01,
883            b"test-payload",
884            EthernetAddress([0; 6]),
885            EthernetAddress([0; 6]),
886        );
887
888        let eth = EthernetFrame::new_checked(&frame).unwrap();
889        let ipv4 = Ipv4Packet::new_checked(eth.payload()).unwrap();
890        let icmp = Icmpv4Packet::new_checked(ipv4.payload()).unwrap();
891        let repr = Icmpv4Repr::parse(&icmp, &ChecksumCapabilities::default()).unwrap();
892        assert_eq!(
893            repr,
894            Icmpv4Repr::EchoReply {
895                ident: 0xABCD,
896                seq_no: 0xEF01,
897                data: b"test-payload",
898            }
899        );
900    }
901
902    #[test]
903    fn construct_icmpv6_reply_preserves_ident_and_seqno() {
904        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
905        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
906        let frame = construct_icmpv6_echo_reply(
907            src,
908            dst,
909            0xBEEF,
910            0xCAFE,
911            b"test6",
912            EthernetAddress([0; 6]),
913            EthernetAddress([0; 6]),
914        );
915
916        let eth = EthernetFrame::new_checked(&frame).unwrap();
917        let ipv6 = Ipv6Packet::new_checked(eth.payload()).unwrap();
918        let icmp = Icmpv6Packet::new_checked(ipv6.payload()).unwrap();
919        let repr = Icmpv6Repr::parse(&src, &dst, &icmp, &ChecksumCapabilities::default()).unwrap();
920        assert_eq!(
921            repr,
922            Icmpv6Repr::EchoReply {
923                ident: 0xBEEF,
924                seq_no: 0xCAFE,
925                data: b"test6",
926            }
927        );
928    }
929
930    #[test]
931    fn probe_does_not_panic() {
932        // Result depends on host — just verify it doesn't panic.
933        let _ = probe_icmp_socket_v4();
934        let _ = probe_icmp_socket_v6();
935    }
936
937    #[test]
938    fn parse_icmpv4_reply_accepts_bare_icmp() {
939        let icmp_repr = Icmpv4Repr::EchoReply {
940            ident: 0x1234,
941            seq_no: 0x0001,
942            data: b"hello",
943        };
944        let mut buf = vec![0u8; icmp_repr.buffer_len()];
945        icmp_repr.emit(
946            &mut Icmpv4Packet::new_unchecked(&mut buf),
947            &ChecksumCapabilities::default(),
948        );
949
950        let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
951        assert_eq!(seq_no, 0x0001);
952        assert_eq!(data, b"hello");
953    }
954
955    #[test]
956    fn parse_icmpv4_reply_accepts_ipv4_plus_icmp() {
957        let frame = construct_icmpv4_echo_reply(
958            Ipv4Addr::new(8, 8, 8, 8),
959            Ipv4Addr::new(100, 96, 0, 2),
960            0x1234,
961            0x0001,
962            b"hello",
963            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
964            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
965        );
966        let eth = EthernetFrame::new_checked(&frame).unwrap();
967
968        let (seq_no, data) = parse_icmpv4_echo_reply(eth.payload()).unwrap();
969        assert_eq!(seq_no, 0x0001);
970        assert_eq!(data, b"hello");
971    }
972
973    #[test]
974    fn parse_icmpv4_reply_accepts_macos_ping_socket_shape() {
975        let buf = [
976            0x45, 0x00, 0x0d, 0x00, 0x00, 0x00, 0x00, 0x00, 0x75, 0x01, 0x73, 0xef, 0x08, 0x08,
977            0x08, 0x08, 0xc0, 0xa8, 0x01, 0x35, 0x00, 0x00, 0xa9, 0xf8, 0x12, 0x34, 0x00, 0x01,
978            0x68, 0x65, 0x6c, 0x6c, 0x6f,
979        ];
980
981        let (seq_no, data) = parse_icmpv4_echo_reply(&buf).unwrap();
982        assert_eq!(seq_no, 0x0001);
983        assert_eq!(data, b"hello");
984    }
985
986    #[test]
987    fn parse_icmpv6_reply_accepts_bare_icmpv6() {
988        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
989        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
990        let icmp_repr = Icmpv6Repr::EchoReply {
991            ident: 0x1234,
992            seq_no: 0x0002,
993            data: b"hello6",
994        };
995        let mut buf = vec![0u8; icmp_repr.buffer_len()];
996        icmp_repr.emit(
997            &src,
998            &dst,
999            &mut Icmpv6Packet::new_unchecked(&mut buf),
1000            &ChecksumCapabilities::default(),
1001        );
1002
1003        let (seq_no, data) = parse_icmpv6_echo_reply(&buf, src, dst).unwrap();
1004        assert_eq!(seq_no, 0x0002);
1005        assert_eq!(data, b"hello6");
1006    }
1007
1008    #[test]
1009    fn parse_icmpv6_reply_accepts_ipv6_plus_icmpv6() {
1010        let src: Ipv6Addr = "2001:db8::1".parse().unwrap();
1011        let dst: Ipv6Addr = "fd42:6d73:62::2".parse().unwrap();
1012        let frame = construct_icmpv6_echo_reply(
1013            src,
1014            dst,
1015            0x5678,
1016            0x0002,
1017            b"v6ping",
1018            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x01]),
1019            EthernetAddress([0x02, 0x00, 0x00, 0x00, 0x00, 0x02]),
1020        );
1021        let eth = EthernetFrame::new_checked(&frame).unwrap();
1022
1023        let (seq_no, data) = parse_icmpv6_echo_reply(eth.payload(), src, dst).unwrap();
1024        assert_eq!(seq_no, 0x0002);
1025        assert_eq!(data, b"v6ping");
1026    }
1027}