use std::{
fmt::Debug,
net::IpAddr,
sync::{Arc, Mutex},
time::Duration,
};
use anyhow::{Context, Result};
use surge_ping::{Client, Config, IcmpPacket, PingIdentifier, PingSequence, ICMP};
use tracing::debug;
use crate::defaults::timeouts::DEFAULT_PINGER_TIMEOUT as DEFAULT_TIMEOUT;
#[derive(Debug, thiserror::Error)]
pub enum PingError {
#[error("Error creating ping client")]
Client(#[from] anyhow::Error),
#[error("Error sending ping")]
Ping(#[from] surge_ping::SurgeError),
}
#[derive(Debug, Clone, Default)]
pub struct Pinger(Arc<Inner>);
impl Debug for Inner {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Inner").finish()
}
}
#[derive(Default)]
struct Inner {
client_v6: Mutex<Option<Client>>,
client_v4: Mutex<Option<Client>>,
}
impl Pinger {
pub fn new() -> Self {
Default::default()
}
fn get_client(&self, kind: ICMP) -> Result<Client> {
let client = match kind {
ICMP::V4 => {
let mut opt_client = self.0.client_v4.lock().unwrap();
match *opt_client {
Some(ref client) => client.clone(),
None => {
let cfg = Config::builder().kind(kind).build();
let client = Client::new(&cfg).context("failed to create IPv4 pinger")?;
*opt_client = Some(client.clone());
client
}
}
}
ICMP::V6 => {
let mut opt_client = self.0.client_v6.lock().unwrap();
match *opt_client {
Some(ref client) => client.clone(),
None => {
let cfg = Config::builder().kind(kind).build();
let client = Client::new(&cfg).context("failed to create IPv6 pinger")?;
*opt_client = Some(client.clone());
client
}
}
}
};
Ok(client)
}
pub async fn send(&self, addr: IpAddr, data: &[u8]) -> Result<Duration, PingError> {
let client = match addr {
IpAddr::V4(_) => self.get_client(ICMP::V4).map_err(PingError::Client)?,
IpAddr::V6(_) => self.get_client(ICMP::V6).map_err(PingError::Client)?,
};
let ident = PingIdentifier(rand::random());
debug!(%addr, %ident, "Creating pinger");
let mut pinger = client.pinger(addr, ident).await;
pinger.timeout(DEFAULT_TIMEOUT); match pinger.ping(PingSequence(0), data).await? {
(IcmpPacket::V4(packet), dur) => {
debug!(
"{} bytes from {}: icmp_seq={} ttl={:?} time={:0.2?}",
packet.get_size(),
packet.get_source(),
packet.get_sequence(),
packet.get_ttl(),
dur
);
Ok(dur)
}
(IcmpPacket::V6(packet), dur) => {
debug!(
"{} bytes from {}: icmp_seq={} hlim={} time={:0.2?}",
packet.get_size(),
packet.get_source(),
packet.get_sequence(),
packet.get_max_hop_limit(),
dur
);
Ok(dur)
}
}
}
}
#[cfg(test)]
mod tests {
use std::net::{Ipv4Addr, Ipv6Addr};
use tracing::error;
use super::*;
#[tokio::test]
#[ignore] async fn test_ping_google() -> Result<()> {
let _guard = iroh_test::logging::setup();
let pinger = Pinger::new();
let dur = pinger.send("8.8.8.8".parse()?, &[1u8; 8]).await?;
assert!(!dur.is_zero());
match pinger
.send("2001:4860:4860:0:0:0:0:8888".parse()?, &[1u8; 8])
.await
{
Ok(dur) => {
assert!(!dur.is_zero());
}
Err(err) => {
tracing::error!("IPv6 is not available: {:?}", err);
}
}
Ok(())
}
#[tokio::test]
async fn test_ping_localhost() {
let _guard = iroh_test::logging::setup();
let pinger = Pinger::new();
match pinger.send(Ipv4Addr::LOCALHOST.into(), b"data").await {
Ok(duration) => {
assert!(!duration.is_zero());
}
Err(PingError::Client(err)) => {
error!("no ping permissions: {err:#}");
}
Err(PingError::Ping(err)) => {
panic!("ping failed: {err:#}");
}
}
match pinger.send(Ipv6Addr::LOCALHOST.into(), b"data").await {
Ok(duration) => {
assert!(!duration.is_zero());
}
Err(PingError::Client(err)) => {
error!("no ping permissions: {err:#}");
}
Err(PingError::Ping(err)) => {
error!("ping failed, probably no IPv6 stack: {err:#}");
}
}
}
}