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