Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
[package]
name = "ping"
version = "0.8.0"
authors = ["AN Long <aisk1988@gmail.com>"]
authors = ["An Long <aisk1988@gmail.com>"]
license = "MIT"
description = "Simple and naive ping implementation in Rust."
repository = "https://github.com/aisk/rust-ping"
categories = ["network-programming", "os"]
keywords = ["ping", "system", "ICMP"]
edition = "2024"

[features]
default = []
tokio = ["dep:tokio"]

[dependencies]
socket2 = { version = "0.6", features = ["all"] }
thiserror = ">=1.0, <=2.1"
rand = ">=0.8, <=0.9"
tokio = { version = "1", features = ["net", "rt", "time"], optional = true }

[dev-dependencies]
libc = "0.2"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,33 @@ fn main() {
}
```

## Optional Tokio support

Tokio-based asynchronous sending is available behind the optional `tokio` feature. The feature is disabled by default, so synchronous users do not pull Tokio into their dependency graph.

```toml
[dependencies]
ping = { version = "0.9", features = ["tokio"] }
```

```rust
use std::time::Duration;

#[tokio::main]
async fn main() {
let target_ip = "8.8.8.8".parse().unwrap();
let result = ping::new(target_ip)
.timeout(Duration::from_secs(2))
.send_async()
.await
.expect("ping failed");

println!("round-trip time: {:?}", result.rtt);
}
```

On Unix, `send_async` uses a nonblocking socket registered with Tokio's reactor, so each in-flight ping does not occupy a thread. Windows currently uses Tokio's blocking task pool as a compatibility implementation; native asynchronous Windows socket support may be added in a future release.

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
Expand Down
8 changes: 7 additions & 1 deletion src/ping.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ use socket2::{Domain, Protocol, Socket, Type};
use crate::errors::Error;
use crate::packet::{EchoReply, EchoRequest, ICMP_HEADER_SIZE, IcmpV4, IcmpV6, IpV4Packet};

#[cfg(feature = "tokio")]
mod async_ping;

const TOKEN_SIZE: usize = 24;
const ECHO_REQUEST_BUFFER_SIZE: usize = ICMP_HEADER_SIZE + TOKEN_SIZE;
type Token = [u8; TOKEN_SIZE];
Expand Down Expand Up @@ -245,7 +248,10 @@ pub mod dgramsock {
}
}

#[deprecated(since = "0.8.0", note = "use `Ping::new` builder and `Ping::send` instead")]
#[deprecated(
since = "0.8.0",
note = "use `Ping::new` builder and `Ping::send` instead"
)]
pub fn ping(
addr: IpAddr,
timeout: Option<Duration>,
Expand Down
160 changes: 160 additions & 0 deletions src/ping/async_ping.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use crate::errors::Error;

#[cfg(unix)]
use std::net::{IpAddr, SocketAddr};
#[cfg(unix)]
use std::time::{Duration, Instant};

#[cfg(unix)]
use socket2::Type;
#[cfg(unix)]
use tokio::io::unix::AsyncFd;

#[cfg(not(unix))]
use super::ping_with_socktype;
use super::{Ping, PingResult};
#[cfg(unix)]
use super::{Token, create_socket, decode_reply, prepare_request};

#[cfg(unix)]
#[allow(deprecated)]
async fn ping_with_socktype_async(
socket_type: Type,
addr: IpAddr,
timeout: Option<Duration>,
ttl: Option<u32>,
ident: Option<u16>,
seq_cnt: Option<u16>,
payload: Option<&Token>,
bind_device: Option<&str>,
) -> Result<PingResult, Error> {
let timeout = timeout.unwrap_or(Duration::from_secs(4));
let dest = SocketAddr::new(addr, 0);
let (request_bytes, request_payload) = prepare_request(addr, ident, seq_cnt, payload)?;
let socket = create_socket(socket_type, addr, ttl, bind_device)?;
socket.set_nonblocking(true)?;
let socket = AsyncFd::new(socket)?;
let started_at = Instant::now();

let operation = async {
loop {
let mut ready = socket.writable().await?;
match ready.try_io(|inner| inner.get_ref().send_to(&request_bytes, &dest.into())) {
Ok(result) => {
result?;
break;
}
Err(_would_block) => continue,
}
}

loop {
let mut ready = socket.readable().await?;
let mut buffer = [0u8; 2048];
let received = ready.try_io(|inner| {
// socket2 0.6 receives into MaybeUninit because the kernel may
// initialize only the returned prefix of the buffer.
inner.get_ref().recv_from(unsafe {
std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut std::mem::MaybeUninit<u8>,
buffer.len(),
)
})
});
let (n, src_addr) = match received {
Ok(result) => result?,
Err(_would_block) => continue,
};
let source_ip = src_addr.as_socket().map(|s| s.ip()).unwrap_or(addr);

let Some((reply, recv_ttl)) = decode_reply(addr, &buffer[..n]) else {
continue;
};
if reply.payload == request_payload {
return Ok(PingResult {
rtt: started_at.elapsed(),
ident: reply.ident,
seq_cnt: reply.seq_cnt,
payload: reply.payload.to_vec(),
source: source_ip,
target: addr,
ttl: recv_ttl,
});
}
}
};

tokio::time::timeout(timeout, operation)
.await
.map_err(|_| {
Error::from(std::io::Error::new(
std::io::ErrorKind::TimedOut,
"ping request timed out",
))
})?
}

impl Ping<'_> {
/// Sends the echo request asynchronously using Tokio.
///
/// This method is available when the crate's `tokio` feature is enabled.
/// On Unix this uses a nonblocking socket registered with Tokio's reactor.
/// On Windows it currently falls back to Tokio's blocking thread pool.
/// All builder options and the return value are identical to
/// [`send`](Ping::send).
///
/// ```no_run
/// # #[cfg(feature = "tokio")]
/// # #[tokio::main]
/// # async fn main() {
/// let target = "8.8.8.8".parse().unwrap();
/// let result = ping::new(target).send_async().await.expect("ping failed");
/// println!("{:?}", result.rtt);
/// # }
/// ```
pub async fn send_async(&self) -> Result<PingResult, Error> {
#[cfg(unix)]
return ping_with_socktype_async(
self.socket_type.into(),
self.addr,
self.timeout,
self.ttl,
self.ident,
self.seq_cnt,
self.payload,
#[cfg(any(target_os = "linux", target_os = "android"))]
self.bind_device,
#[cfg(not(any(target_os = "linux", target_os = "android")))]
None,
)
.await;

// Windows support currently keeps the portable blocking implementation
// off Tokio's runtime worker threads.
#[cfg(not(unix))]
{
let socket_type = self.socket_type;
let addr = self.addr;
let timeout = self.timeout;
let ttl = self.ttl;
let ident = self.ident;
let seq_cnt = self.seq_cnt;
let payload = self.payload.copied();

tokio::task::spawn_blocking(move || {
ping_with_socktype(
socket_type.into(),
addr,
timeout,
ttl,
ident,
seq_cnt,
payload.as_ref(),
None,
)
})
.await
.map_err(|_| Error::InternalError)?
}
}
}
17 changes: 17 additions & 0 deletions tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,23 @@ fn builder_api2() {
ping::new(addr).timeout(timeout).ttl(42).send().unwrap();
}

#[cfg(feature = "tokio")]
#[tokio::test]
async fn async_builder_api() {
skip_if_no_capability!();
let addr = "127.0.0.1".parse().unwrap();
let timeout = Duration::from_secs(1);
let result = ping::new(addr)
.timeout(timeout)
.ttl(42)
.send_async()
.await
.unwrap();

assert_eq!(result.source, addr);
assert!(result.rtt <= timeout);
}

#[test]
#[cfg(any(target_os = "linux", target_os = "android"))]
fn bind_device() {
Expand Down
Loading