1use 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
24const ECHO_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
30
31const RECV_BUF_SIZE: usize = 1500;
33
34const ETH_HDR_LEN: usize = 14;
36
37const IPV4_HDR_LEN: usize = 20;
39
40const IPV6_HDR_LEN: usize = 40;
42
43#[cfg_attr(windows, allow(dead_code))]
52#[derive(Debug, Clone, Copy)]
53enum EchoBackend {
54 Available,
56 Unavailable,
58}
59
60pub 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
75impl IcmpRelay {
80 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 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(ð, config, policy)
130 }
131 EthernetProtocol::Ipv6 if matches!(self.backend_v6, EchoBackend::Available) => {
132 self.try_relay_icmpv6(ð, config, policy)
133 }
134 _ => false,
135 }
136 }
137}
138
139impl IcmpRelay {
140 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 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; };
171
172 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; }
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 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 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; };
247
248 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#[cfg(unix)]
294fn probe_icmp_socket_v4() -> EchoBackend {
295 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#[cfg(windows)]
307fn probe_icmp_socket_v4() -> EchoBackend {
308 EchoBackend::Unavailable
309}
310
311#[cfg(unix)]
313fn probe_icmp_socket_v6() -> EchoBackend {
314 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#[cfg(windows)]
326fn probe_icmp_socket_v6() -> EchoBackend {
327 EchoBackend::Unavailable
328}
329
330fn 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 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 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 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 let std_sock = unsafe { std::net::UdpSocket::from_raw_fd(fd) };
390 tokio::net::UdpSocket::from_std(std_sock)
391 }
392}
393
394fn 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#[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#[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 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 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 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#[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 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
589fn 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
621fn 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
668fn 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
699fn 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
725fn 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 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_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_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
774fn 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 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 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 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#[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 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 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}