blob: a1c3e8a0753a4e277b371753f968403472927500 [file] [log] [blame]
// Copyright 2021 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#![deny(missing_docs)]
//! Helpers to ping an IPv4 or IPv6 address by sending ICMP echo requests and
//! waiting for ICMP echo replies.
//!
//! Functionality in this crate relies on [ICMP sockets], a kind of socket where
//! each payload read/written contains ICMP headers.
//!
//! As a starting point, see [`new_unicast_sink_and_stream`], which is built
//! on top of the other facilities in the crate and models pinging as sending
//! an ICMP echo request whenever a value is sent to the sink, and a stream
//! which yields an item for every echo reply received.
//!
//! [ICMP sockets]: https://lwn.net/Articles/422330/
#[cfg(target_os = "fuchsia")]
mod fuchsia;
#[cfg(target_os = "fuchsia")]
pub use fuchsia::{new_icmp_socket, IpExt as FuchsiaIpExt};
use futures::{ready, Sink, SinkExt as _, Stream, TryStreamExt as _};
use net_types::ip::{Ip, Ipv4, Ipv6};
use std::{
marker::PhantomData,
pin::Pin,
task::{Context, Poll},
};
use thiserror::Error;
use zerocopy::{byteorder::network_endian::U16, AsBytes, FromBytes, FromZeros, NoCell, Unaligned};
/// The number of bytes of an ICMP (v4 or v6) header.
pub const ICMP_HEADER_LEN: usize = std::mem::size_of::<IcmpHeader>();
/// ICMP header representation.
#[repr(C)]
#[derive(FromZeros, FromBytes, AsBytes, NoCell, Unaligned, Debug, PartialEq, Eq, Clone)]
struct IcmpHeader {
r#type: u8,
code: u8,
checksum: U16,
id: U16,
sequence: U16,
}
impl IcmpHeader {
fn new<I: IpExt>(sequence: u16) -> Self {
Self {
r#type: I::ECHO_REQUEST_TYPE,
code: 0,
checksum: 0.into(),
id: 0.into(),
sequence: sequence.into(),
}
}
}
/// Ping error.
#[derive(Debug, Error)]
pub enum PingError {
/// Send error.
#[error("send error")]
Send(#[source] std::io::Error),
/// Send length mismatch.
#[error("wrong number of bytes sent, got: {got}, want: {want}")]
SendLength {
/// Number of bytes sent.
got: usize,
/// Number of bytes expected to be sent.
want: usize,
},
/// Recv error.
#[error("recv error")]
Recv(#[source] std::io::Error),
/// ICMP header parsing error.
#[error("failed to parse ICMP header")]
Parse,
/// Reply type mismatch.
#[error("wrong reply type, got: {got}, want: {want}")]
ReplyType {
/// ICMP type received in reply.
got: u8,
/// ICMP type expected in reply.
want: u8,
},
/// Reply code mismatch.
#[error("non-zero reply code: {0}")]
ReplyCode(u8),
/// ICMP message body mismatch.
#[error("reply message body mismatch, got: {got:?}, want: {want:?}")]
Body {
/// Body received in reply.
got: Vec<u8>,
/// Body expected in reply.
want: Vec<u8>,
},
}
/// Addresses which can be converted from `socket2::SockAddr`.
///
/// This trait exists to get around not being able to implement the foreign trait
/// `TryFrom<socket2::SockAddr>` for the foreign types `std::net::SocketAddr(V4|V6)?`.
pub trait TryFromSockAddr: Sized {
/// Try to convert from `socket2::SockAddr`.
fn try_from(value: socket2::SockAddr) -> std::io::Result<Self>;
}
impl TryFromSockAddr for std::net::SocketAddrV4 {
fn try_from(addr: socket2::SockAddr) -> std::io::Result<Self> {
addr.as_socket_ipv4().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("socket address is not v4 {:?}", addr),
)
})
}
}
impl TryFromSockAddr for std::net::SocketAddrV6 {
fn try_from(addr: socket2::SockAddr) -> std::io::Result<Self> {
addr.as_socket_ipv6().ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::Other,
format!("socket address is not v6 {:?}", addr),
)
})
}
}
/// Trait for IP protocol versions.
pub trait IpExt: Ip + Unpin {
/// IP Socket address type.
type SockAddr: Into<socket2::SockAddr>
+ TryFromSockAddr
+ Clone
+ Copy
+ Unpin
+ PartialEq
+ std::fmt::Debug
+ std::fmt::Display
+ Eq
+ std::hash::Hash;
/// ICMP socket domain.
const DOMAIN: socket2::Domain;
/// ICMP socket protocol.
const PROTOCOL: socket2::Protocol;
/// ICMP echo request type.
const ECHO_REQUEST_TYPE: u8;
/// ICMP echo reply type.
const ECHO_REPLY_TYPE: u8;
}
// TODO(https://fxbug.dev/323955204): Implement ext trait on net_types::ip::Ipv4
// instead and remove the Ipv4 type.
impl IpExt for Ipv4 {
type SockAddr = std::net::SocketAddrV4;
const DOMAIN: socket2::Domain = socket2::Domain::IPV4;
const PROTOCOL: socket2::Protocol = socket2::Protocol::ICMPV4;
const ECHO_REQUEST_TYPE: u8 = 8;
const ECHO_REPLY_TYPE: u8 = 0;
}
// TODO(https://fxbug.dev/323955204): Implement ext trait on net_types::ip::Ipv6
// instead and remove the Ipv6 type.
impl IpExt for Ipv6 {
type SockAddr = std::net::SocketAddrV6;
const DOMAIN: socket2::Domain = socket2::Domain::IPV6;
const PROTOCOL: socket2::Protocol = socket2::Protocol::ICMPV6;
const ECHO_REQUEST_TYPE: u8 = 128;
const ECHO_REPLY_TYPE: u8 = 129;
}
/// Async ICMP socket.
pub trait IcmpSocket<I>: Unpin
where
I: IpExt,
{
/// Async method for receiving an ICMP packet.
///
/// Upon successful return, `buf` will contain an ICMP packet.
fn async_recv_from(
&self,
buf: &mut [u8],
cx: &mut Context<'_>,
) -> Poll<std::io::Result<(usize, I::SockAddr)>>;
/// Async method for sending an ICMP packet.
///
/// `bufs` must contain a valid ICMP packet.
fn async_send_to_vectored(
&self,
bufs: &[std::io::IoSlice<'_>],
addr: &I::SockAddr,
cx: &mut Context<'_>,
) -> Poll<std::io::Result<usize>>;
/// Binds this to an interface so that packets can only flow in/out via the specified
/// interface.
///
/// If `interface` is `None`, the binding is removed.
fn bind_device(&self, interface: Option<&[u8]>) -> std::io::Result<()>;
}
/// Parameters of a ping request/reply.
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct PingData<I: IpExt> {
/// The destination address of a ping request; or the source address of a ping reply.
pub addr: I::SockAddr,
/// The sequence number in the ICMP header.
pub sequence: u16,
/// The body of the echo request/reply.
pub body: Vec<u8>,
}
// TODO(https://github.com/rust-lang/rust/issues/76560): Define N as the length of the message body
// rather than the length of the ICMP packet.
/// Create a ping sink and stream for pinging a unicast destination with the same body for every
/// packet.
///
/// Echo replies received with a source address not equal to `addr` will be silently dropped. Echo
/// replies with a body not equal to `body` will result in an error on the stream.
pub fn new_unicast_sink_and_stream<'a, I, S, const N: usize>(
socket: &'a S,
addr: &'a I::SockAddr,
body: &'a [u8],
) -> (impl Sink<u16, Error = PingError> + 'a, impl Stream<Item = Result<u16, PingError>> + 'a)
where
I: IpExt,
S: IcmpSocket<I>,
{
(
PingSink::new(socket).with(move |sequence| {
futures::future::ok(PingData { addr: addr.clone(), sequence, body: body.to_vec() })
}),
PingStream::<I, S, N>::new(socket).try_filter_map(
move |PingData { addr: got_addr, sequence, body: got_body }| {
futures::future::ready(if got_addr == *addr {
if got_body == body {
Ok(Some(sequence))
} else {
Err(PingError::Body { got: got_body, want: body.to_vec() })
}
} else {
Ok(None)
})
},
),
)
}
// TODO(https://github.com/rust-lang/rust/issues/76560): Define N as the length of the message body
// rather than the length of the ICMP packet.
/// Stream of received ping replies.
pub struct PingStream<'a, I, S, const N: usize>
where
I: IpExt,
S: IcmpSocket<I>,
{
socket: &'a S,
recv_buf: [u8; N],
_marker: PhantomData<I>,
}
impl<'a, I, S, const N: usize> PingStream<'a, I, S, N>
where
I: IpExt,
S: IcmpSocket<I>,
{
/// Construct a stream from an `IcmpSocket`.
///
/// `N` must be set to the length of the largest ICMP body expected
/// to be received plus the 8 bytes of overhead due to the ICMP
/// header, otherwise received packets may be truncated. Note
/// that this does not need to include the 8-byte overhead of
/// the ICMP header.
pub fn new(socket: &'a S) -> Self {
Self { socket, recv_buf: [0; N], _marker: PhantomData::<I> }
}
}
impl<'a, I, S, const N: usize> futures::stream::Stream for PingStream<'a, I, S, N>
where
I: IpExt,
S: IcmpSocket<I>,
{
type Item = Result<PingData<I>, PingError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let ping_stream = Pin::into_inner(self);
let buf = &mut ping_stream.recv_buf[..];
let socket = &ping_stream.socket;
Poll::Ready(Some(
ready!(socket.async_recv_from(buf, cx))
.map_err(PingError::Recv)
.and_then(|(len, addr)| verify_packet::<I>(addr, &ping_stream.recv_buf[..len])),
))
}
}
/// Sink for sending ping requests.
pub struct PingSink<'a, I, S>
where
I: IpExt,
S: IcmpSocket<I>,
{
socket: &'a S,
packet: Option<(I::SockAddr, IcmpHeader, Vec<u8>)>,
_marker: PhantomData<I>,
}
impl<'a, I, S> PingSink<'a, I, S>
where
I: IpExt,
S: IcmpSocket<I>,
{
/// Construct a sink from an `IcmpSocket`.
pub fn new(socket: &'a S) -> Self {
Self { socket, packet: None, _marker: PhantomData::<I> }
}
}
impl<'a, I, S> futures::sink::Sink<PingData<I>> for PingSink<'a, I, S>
where
I: IpExt,
S: IcmpSocket<I>,
{
type Error = PingError;
fn poll_ready(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.poll_flush(cx)
}
fn start_send(
mut self: Pin<&mut Self>,
PingData { addr, sequence, body }: PingData<I>,
) -> Result<(), Self::Error> {
let header = IcmpHeader::new::<I>(sequence);
assert_eq!(
self.packet.replace((addr, header, body)),
None,
"start_send called while element has yet to be flushed"
);
Ok(())
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(match &self.packet {
Some((addr, header, body)) => {
match ready!(self.socket.async_send_to_vectored(
&[
std::io::IoSlice::new(header.as_bytes()),
std::io::IoSlice::new(body.as_bytes()),
],
addr,
cx
)) {
Ok(got) => {
let want = std::mem::size_of_val(&header) + body.len();
if got != want {
Err(PingError::SendLength { got, want })
} else {
self.packet = None;
Ok(())
}
}
Err(e) => Err(PingError::Send(e)),
}
}
None => Ok(()),
})
}
fn poll_close(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.poll_flush(cx)
}
}
fn verify_packet<I: IpExt>(addr: I::SockAddr, packet: &[u8]) -> Result<PingData<I>, PingError> {
let (reply, body): (zerocopy::Ref<_, IcmpHeader>, _) =
zerocopy::Ref::new_unaligned_from_prefix(packet).ok_or_else(|| PingError::Parse)?;
// The identifier cannot be verified, since ICMP socket implementations rewrites the field on
// send and uses its value to demultiplex packets for delivery to sockets on receive.
//
// Also, don't bother verifying the checksum, since ICMP socket implementations must have
// verified the checksum since the code and identifier fields must be inspected. Also, the
// ICMPv6 checksum computation includes a pseudo header which includes the src and dst
// addresses, and the dst/local address is not readily available.
let &IcmpHeader { r#type, code, checksum: _, id: _, sequence } = reply.into_ref();
if r#type != I::ECHO_REPLY_TYPE {
return Err(PingError::ReplyType { got: r#type, want: I::ECHO_REPLY_TYPE });
}
if code != 0 {
return Err(PingError::ReplyCode(code));
}
Ok(PingData { addr, sequence: sequence.into(), body: body.to_vec() })
}
#[cfg(test)]
mod test {
use super::{IcmpHeader, IcmpSocket, Ipv4, Ipv6, PingData, PingSink, PingStream};
use futures::{FutureExt as _, SinkExt as _, StreamExt as _, TryStreamExt as _};
use net_declare::{std_socket_addr_v4, std_socket_addr_v6};
use std::{
cell::RefCell,
collections::VecDeque,
task::{Context, Poll},
};
use zerocopy::AsBytes as _;
// A fake impl of a IcmpSocket which computes and buffers a reply when `send_to` is called,
// which is then returned when `recv_from` is called. The order in which replies are returned
// is guaranteed to be FIFO.
#[derive(Default, Debug)]
struct FakeSocket<I: IpExt> {
// NB: interior mutability is necessary here because the `IcmpSocket` trait's methods
// operate on &self.
buffer: RefCell<VecDeque<(Vec<u8>, I::SockAddr)>>,
}
impl<I: IpExt> FakeSocket<I> {
fn new() -> Self {
Self { buffer: RefCell::new(VecDeque::new()) }
}
}
impl<I: IpExt> IcmpSocket<I> for FakeSocket<I> {
fn async_recv_from(
&self,
buf: &mut [u8],
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<(usize, I::SockAddr)>> {
Poll::Ready(
self.buffer
.borrow_mut()
.pop_front()
.ok_or_else(|| {
std::io::Error::new(
std::io::ErrorKind::WouldBlock,
"fake socket request buffer is empty",
)
})
.and_then(|(reply, addr)| {
if buf.len() < reply.len() {
Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"recv buffer too small, got: {}, want: {}",
buf.len(),
reply.len()
),
))
} else {
buf[..reply.len()].copy_from_slice(&reply);
Ok((reply.len(), addr))
}
}),
)
}
fn async_send_to_vectored(
&self,
bufs: &[std::io::IoSlice<'_>],
addr: &I::SockAddr,
_cx: &mut Context<'_>,
) -> Poll<std::io::Result<usize>> {
let mut buf = bufs
.iter()
.map(|io_slice| io_slice.as_bytes())
.flatten()
.copied()
.collect::<Vec<u8>>();
let (mut header, _): (zerocopy::Ref<_, IcmpHeader>, _) =
match zerocopy::Ref::new_unaligned_from_prefix(&mut buf[..]) {
Some(layout_verified) => layout_verified,
None => {
return Poll::Ready(Err(std::io::Error::new(
std::io::ErrorKind::InvalidInput,
"failed to parse ICMP header from provided bytes",
)))
}
};
header.r#type = I::ECHO_REPLY_TYPE;
let len = buf.len();
let () = self.buffer.borrow_mut().push_back((buf, addr.clone()));
Poll::Ready(Ok(len))
}
fn bind_device(&self, interface: Option<&[u8]>) -> std::io::Result<()> {
panic!("unexpected call to bind_device({:?})", interface);
}
}
trait IpExt: super::IpExt {
// NB: This is only a function because there is no way to create a constant for any of the
// socket address types.
fn test_addr() -> Self::SockAddr;
}
impl IpExt for Ipv4 {
fn test_addr() -> Self::SockAddr {
// A port must be specified in the socket addr, but it is irrelevant for ICMP sockets,
// so just set it to 0.
std_socket_addr_v4!("1.2.3.4:0")
}
}
impl IpExt for Ipv6 {
fn test_addr() -> Self::SockAddr {
// A port must be specified in the socket addr, but it is irrelevant for ICMP sockets,
// so just set it to 0.
std_socket_addr_v6!("[abcd::1]:0")
}
}
const PING_MESSAGE: &str = "Hello from ping library unit test!";
const PING_COUNT: u16 = 3;
const PING_SEQ_RANGE: std::ops::RangeInclusive<u16> = 1..=PING_COUNT;
#[test]
fn test_ipv4() {
test_ping::<Ipv4>();
}
#[test]
fn test_ipv6() {
test_ping::<Ipv6>();
}
fn test_ping<I: IpExt>() {
let socket = FakeSocket::<I>::new();
let packets = PING_SEQ_RANGE
.into_iter()
.map(|sequence| PingData {
addr: I::test_addr(),
sequence,
body: PING_MESSAGE.as_bytes().to_vec(),
})
.collect::<Vec<_>>();
let packet_stream = futures::stream::iter(packets.iter().cloned());
let () = PingSink::new(&socket)
.send_all(&mut packet_stream.map(Ok))
.now_or_never()
.expect("ping request send blocked unexpectedly")
.expect("ping send error");
let replies =
PingStream::<_, _, { PING_MESSAGE.len() + std::mem::size_of::<IcmpHeader>() }>::new(
&socket,
)
.take(PING_COUNT.into())
.try_collect::<Vec<_>>()
.now_or_never()
.expect("ping reply stream blocked unexpectedly")
.expect("failed to collect ping reply stream");
assert_eq!(packets, replies);
}
}