use std::net::{IpAddr, SocketAddr};
use std::time::{Duration, Instant};
use rand::random;
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];
fn remaining_timeout(started_at: Instant, timeout: Duration) -> std::io::Result<Duration> {
timeout
.checked_sub(started_at.elapsed())
.ok_or_else(|| std::io::Error::new(std::io::ErrorKind::TimedOut, "ping request timed out"))
}
fn prepare_request(
addr: IpAddr,
ident: Option<u16>,
seq_cnt: Option<u16>,
payload: Option<&Token>,
) -> Result<([u8; ECHO_REQUEST_BUFFER_SIZE], Token), Error> {
let payload = payload.copied().unwrap_or_else(random);
let request = EchoRequest {
ident: ident.unwrap_or_else(random),
seq_cnt: seq_cnt.unwrap_or(1),
payload: &payload,
};
let mut bytes = [0; ECHO_REQUEST_BUFFER_SIZE];
let encoded = if addr.is_ipv4() {
request.encode::<IcmpV4>(&mut bytes)
} else {
request.encode::<IcmpV6>(&mut bytes)
};
encoded.map_err(|_| Error::InternalError)?;
Ok((bytes, payload))
}
fn create_socket(
socket_type: Type,
addr: IpAddr,
ttl: Option<u32>,
bind_device: Option<&str>,
) -> Result<Socket, Error> {
let socket = if addr.is_ipv4() {
Socket::new(Domain::IPV4, socket_type, Some(Protocol::ICMPV4))?
} else {
Socket::new(Domain::IPV6, socket_type, Some(Protocol::ICMPV6))?
};
if addr.is_ipv4() {
socket.set_ttl_v4(ttl.unwrap_or(64))?;
} else {
socket.set_unicast_hops_v6(ttl.unwrap_or(64))?;
}
#[allow(unused)]
if let Some(device) = bind_device {
#[cfg(any(target_os = "linux", target_os = "android"))]
socket.bind_device(Some(device.as_bytes()))?;
#[cfg(not(any(target_os = "linux", target_os = "android")))]
eprintln!("Warning: bind_device is only supported on Linux and Android platforms");
}
Ok(socket)
}
fn decode_reply(addr: IpAddr, packet: &[u8]) -> Option<(EchoReply<'_>, Option<u8>)> {
if addr.is_ipv4() {
if packet.len() == ECHO_REQUEST_BUFFER_SIZE {
EchoReply::decode::<IcmpV4>(packet)
.ok()
.map(|reply| (reply, None))
} else {
let ipv4_packet = IpV4Packet::decode(packet).ok()?;
let ttl = Some(ipv4_packet.ttl);
EchoReply::decode::<IcmpV4>(ipv4_packet.data)
.ok()
.map(|reply| (reply, ttl))
}
} else {
EchoReply::decode::<IcmpV6>(packet)
.ok()
.map(|reply| (reply, None))
}
}
#[derive(Clone, Copy, Debug)]
pub enum SocketType {
RAW,
DGRAM,
}
impl From<SocketType> for Type {
fn from(socket_type: SocketType) -> Self {
match socket_type {
SocketType::RAW => Type::RAW,
SocketType::DGRAM => Type::DGRAM,
}
}
}
#[derive(Debug)]
#[non_exhaustive]
pub struct PingResult {
pub rtt: Duration,
pub ident: u16,
pub seq_cnt: u16,
pub payload: Vec<u8>,
pub source: IpAddr,
#[deprecated(since = "0.7.1", note = "use `source` instead")]
pub target: IpAddr,
pub ttl: Option<u8>,
}
#[allow(deprecated)]
fn ping_with_socktype(
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 = match timeout {
Some(timeout) => timeout,
None => 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_write_timeout(Some(timeout))?;
let started_at = Instant::now();
socket.send_to(&request_bytes, &dest.into())?;
loop {
socket.set_read_timeout(Some(remaining_timeout(started_at, timeout)?))?;
let mut buffer: [u8; 2048] = [0; 2048];
let (n, src_addr) = socket.recv_from(unsafe {
std::slice::from_raw_parts_mut(
buffer.as_mut_ptr() as *mut std::mem::MaybeUninit<u8>,
buffer.len(),
)
})?;
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,
});
}
}
}
pub mod rawsock {
use super::*;
pub fn ping(
addr: IpAddr,
timeout: Option<Duration>,
ttl: Option<u32>,
ident: Option<u16>,
seq_cnt: Option<u16>,
payload: Option<&Token>,
) -> Result<(), Error> {
ping_with_socktype(Type::RAW, addr, timeout, ttl, ident, seq_cnt, payload, None)?;
Ok(())
}
}
pub mod dgramsock {
use super::*;
pub fn ping(
addr: IpAddr,
timeout: Option<Duration>,
ttl: Option<u32>,
ident: Option<u16>,
seq_cnt: Option<u16>,
payload: Option<&Token>,
) -> Result<(), Error> {
ping_with_socktype(
Type::DGRAM,
addr,
timeout,
ttl,
ident,
seq_cnt,
payload,
None,
)?;
Ok(())
}
}
#[deprecated(
since = "0.8.0",
note = "use `Ping::new` builder and `Ping::send` instead"
)]
pub fn ping(
addr: IpAddr,
timeout: Option<Duration>,
ttl: Option<u32>,
ident: Option<u16>,
seq_cnt: Option<u16>,
payload: Option<&Token>,
) -> Result<(), Error> {
rawsock::ping(addr, timeout, ttl, ident, seq_cnt, payload)?;
Ok(())
}
#[derive(Debug, Clone)]
pub struct Ping<'a> {
socket_type: SocketType,
addr: IpAddr,
timeout: Option<Duration>,
ttl: Option<u32>,
ident: Option<u16>,
seq_cnt: Option<u16>,
payload: Option<&'a Token>,
#[cfg(any(target_os = "linux", target_os = "android"))]
bind_device: Option<&'a str>,
}
impl<'a> Ping<'a> {
pub fn new(addr: IpAddr) -> Self {
let socket_type = if std::env::consts::OS == "windows" {
SocketType::RAW
} else {
SocketType::DGRAM
};
return Ping {
socket_type,
addr,
timeout: None,
ttl: None,
ident: None,
seq_cnt: None,
payload: None,
#[cfg(any(target_os = "linux", target_os = "android"))]
bind_device: None,
};
}
pub fn socket_type(&mut self, socket_type: SocketType) -> &mut Self {
self.socket_type = socket_type;
return self;
}
fn ping_with_socket(&self, sock_type: Type) -> Result<PingResult, Error> {
ping_with_socktype(
sock_type,
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,
)
}
pub fn timeout(&mut self, timeout: Duration) -> &mut Self {
self.timeout = Some(timeout);
return self;
}
pub fn ttl(&mut self, ttl: u32) -> &mut Self {
self.ttl = Some(ttl);
return self;
}
pub fn ident(&mut self, ident: u16) -> &mut Self {
self.ident = Some(ident);
return self;
}
pub fn seq_cnt(&mut self, seq_cnt: u16) -> &mut Self {
self.seq_cnt = Some(seq_cnt);
return self;
}
pub fn payload(&mut self, payload: &'a Token) -> &mut Self {
self.payload = Some(payload);
return self;
}
#[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;
}
pub fn send(&self) -> Result<PingResult, Error> {
self.ping_with_socket(self.socket_type.into())
}
}
pub fn new<'a>(addr: IpAddr) -> Ping<'a> {
return Ping::new(addr);
}