diff --git a/README.md b/README.md index c3d2a78..249ff5e 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,14 @@ fn main() { Ok(_) => println!("Ping successful with custom options!"), Err(e) => eprintln!("Ping failed: {}", e), } - +} ``` To perform a ping using a domain name instead of an IP address, you can use any 3rd-party DNS resolver or [`ToSocketAddrs`](https://doc.rust-lang.org/std/net/trait.ToSocketAddrs.html) from the standard library: ```rust +use std::net::ToSocketAddrs; + fn main() { let address = "www.google.com:0" // use any port, we only need the IP .to_socket_addrs() // convert domain name to socket address iterator diff --git a/src/errors.rs b/src/errors.rs index 2d5cca5..788b4e4 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -1,13 +1,19 @@ use thiserror::Error; +/// Errors that can occur while sending a ping or decoding its reply. #[derive(Debug, Error)] pub enum Error { + /// The target address used an unsupported protocol. #[error("invalid procotol")] InvalidProtocol, + /// An internal error, such as failing to encode the request packet. #[error("internal error")] InternalError, + /// The ICMP echo reply could not be decoded. #[error("Decode echo reply error occurred while processing the ICMP echo reply.")] DecodeEchoReplyError, + /// An underlying I/O error. A timeout is reported here with kind + /// [`ErrorKind::TimedOut`](std::io::ErrorKind::TimedOut). #[error("io error: {error}")] IoError { #[from] diff --git a/src/lib.rs b/src/lib.rs index 764b1ff..eb1fec3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,54 @@ +//! An ICMP echo ("ping") implementation for IPv4 and IPv6. +//! +//! Send an ICMP echo request to a target [`IpAddr`] and wait for the reply or +//! a timeout. +//! +//! # Quick start +//! +//! The main entry point is the [`Ping`] builder, created with [`new`]. +//! Configure the options you need and call [`Ping::send`], which returns a +//! [`PingResult`] describing the reply. +//! +//! ```no_run +//! use std::time::Duration; +//! +//! let target = "8.8.8.8".parse().unwrap(); +//! let result = ping::new(target) +//! .timeout(Duration::from_secs(2)) +//! .ttl(64) +//! .send() +//! .expect("ping failed"); +//! +//! println!("round-trip time: {:?}", result.rtt); +//! ``` +//! +//! # Pinging a host name +//! +//! Only an [`IpAddr`] is accepted. To ping a host name, resolve it first with +//! [`ToSocketAddrs`](std::net::ToSocketAddrs). +//! +//! ```no_run +//! use std::net::ToSocketAddrs; +//! +//! // The port is irrelevant, we only need the resolved IP. +//! let addr = "www.google.com:0" +//! .to_socket_addrs() +//! .unwrap() +//! .next() +//! .unwrap() +//! .ip(); +//! +//! ping::new(addr).send().expect("ping failed"); +//! ``` +//! +//! # Socket types +//! +//! Sending ICMP traffic over a [`RAW`] socket needs elevated privileges, while +//! a [`DGRAM`] socket works unprivileged on most systems. See [`SocketType`] +//! for the per-platform default and how to override it. +//! +//! [`IpAddr`]: std::net::IpAddr + mod errors; mod packet; mod ping; diff --git a/src/ping.rs b/src/ping.rs index 2900df3..e9e3841 100644 --- a/src/ping.rs +++ b/src/ping.rs @@ -11,9 +11,17 @@ const TOKEN_SIZE: usize = 24; const ECHO_REQUEST_BUFFER_SIZE: usize = ICMP_HEADER_SIZE + TOKEN_SIZE; type Token = [u8; TOKEN_SIZE]; +/// The kind of socket used to send the ICMP request. +/// +/// The default depends on the platform. On Windows [`Ping::new`] uses +/// [`RAW`](SocketType::RAW), elsewhere it uses [`DGRAM`](SocketType::DGRAM). +/// Override it with [`Ping::socket_type`]. #[derive(Clone, Copy, Debug)] pub enum SocketType { + /// Raw socket. Needs elevated privileges (root, or `CAP_NET_RAW` on Linux). RAW, + /// Datagram socket. Works without elevated privileges on most systems, but + /// some Linux distributions disable it by default. DGRAM, } @@ -26,9 +34,12 @@ impl From for Type { } } +/// The outcome of a successful ping, returned by [`Ping::send`]. #[derive(Debug)] #[non_exhaustive] pub struct PingResult { + /// The measured round-trip time between sending the request and receiving + /// the matching reply. pub rtt: Duration, /// The ICMP identifier observed in the reply. /// @@ -38,10 +49,14 @@ pub struct PingResult { /// reply, and therefore this field, carries the kernel-chosen value rather /// than the requested one. pub ident: u16, + /// The sequence number echoed back in the reply. pub seq_cnt: u16, + /// The payload token echoed back in the reply, used to match it to the + /// request. pub payload: Vec, /// The actual source IP address from the reply packet. pub source: IpAddr, + /// The target address passed to the ping. #[deprecated(since = "0.7.1", note = "use `source` instead")] pub target: IpAddr, /// The TTL from the reply IP header. Only available for IPv4 RAW sockets; @@ -234,6 +249,16 @@ pub fn ping( Ok(()) } +/// Builder for a single ping. +/// +/// Create one with [`Ping::new`] or [`new`], set any options, then call +/// [`send`](Ping::send). All options are optional and have sensible defaults. +/// +/// ```no_run +/// let target = "8.8.8.8".parse().unwrap(); +/// let result = ping::new(target).send().expect("ping failed"); +/// println!("{:?}", result.rtt); +/// ``` #[derive(Debug, Clone)] pub struct Ping<'a> { socket_type: SocketType, @@ -248,6 +273,9 @@ pub struct Ping<'a> { } impl<'a> Ping<'a> { + /// Creates a builder targeting `addr`, with the default socket type for + /// the current platform ([`RAW`](SocketType::RAW) on Windows, + /// [`DGRAM`](SocketType::DGRAM) elsewhere). pub fn new(addr: IpAddr) -> Self { let socket_type = if std::env::consts::OS == "windows" { SocketType::RAW @@ -267,6 +295,8 @@ impl<'a> Ping<'a> { }; } + /// Overrides the [`SocketType`] used to send the request, replacing the + /// platform default chosen by [`Ping::new`]. pub fn socket_type(&mut self, socket_type: SocketType) -> &mut Self { self.socket_type = socket_type; return self; @@ -288,11 +318,19 @@ impl<'a> Ping<'a> { ) } + /// Sets how long [`send`](Ping::send) waits for a reply before failing. + /// + /// When unset, the timeout defaults to 4 seconds. On timeout, `send` + /// returns an [`Error::IoError`] whose kind is + /// [`ErrorKind::TimedOut`](std::io::ErrorKind::TimedOut). pub fn timeout(&mut self, timeout: Duration) -> &mut Self { self.timeout = Some(timeout); return self; } + /// Sets the IP time-to-live (hop limit) of the request. + /// + /// Defaults to 64 when unset. pub fn ttl(&mut self, ttl: u32) -> &mut Self { self.ttl = Some(ttl); return self; @@ -312,27 +350,47 @@ impl<'a> Ping<'a> { return self; } + /// Sets the ICMP sequence number of the request. + /// + /// Defaults to 1 when unset. pub fn seq_cnt(&mut self, seq_cnt: u16) -> &mut Self { self.seq_cnt = Some(seq_cnt); return self; } + /// Sets the 24-byte payload token carried by the request. + /// + /// The reply is matched to the request by this token, so it acts as the + /// correlation id. When unset, a random token is generated for each ping. pub fn payload(&mut self, payload: &'a Token) -> &mut Self { self.payload = Some(payload); return self; } + /// Binds the socket to a network interface by name (e.g. `"eth0"`), so the + /// request is sent from that interface. + /// + /// Only available on Linux and Android. #[cfg(any(target_os = "linux", target_os = "android"))] pub fn bind_device(&mut self, device: &'a str) -> &mut Self { self.bind_device = Some(device); return self; } + /// Sends the echo request and blocks until a matching reply arrives or the + /// timeout elapses. + /// + /// On success returns a [`PingResult`]. A timeout is reported as an + /// [`Error::IoError`] with kind + /// [`ErrorKind::TimedOut`](std::io::ErrorKind::TimedOut). pub fn send(&self) -> Result { self.ping_with_socket(self.socket_type.into()) } } +/// Creates a [`Ping`] builder targeting `addr`. +/// +/// Shorthand for [`Ping::new`]. pub fn new<'a>(addr: IpAddr) -> Ping<'a> { return Ping::new(addr); }