use serde::Serialize;
use std::net::IpAddr;
use std::sync::atomic::{AtomicU16, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration;
use tokio::sync::Semaphore;
#[cfg(not(windows))]
mod raw_icmp;
#[cfg(windows)]
mod windows_icmp;
#[cfg(not(windows))]
use raw_icmp::send_icmp_echo_v4;
static PING_SEQ: AtomicU16 = AtomicU16::new(1);
fn next_seq() -> u16 {
PING_SEQ.fetch_add(1, Ordering::Relaxed)
}
static RAW_ICMP_OK: OnceLock<bool> = OnceLock::new();
#[derive(Debug, Clone, Serialize)]
pub struct PingResult {
pub ip: IpAddr,
pub rtt_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub ttl: Option<u8>,
pub alive: bool,
pub seq: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
}
#[derive(Debug, Clone, Copy)]
enum PingBackend {
RawIcmpV4,
TcpConnect,
}
#[derive(Clone)]
pub struct PingScanner {
semaphore: Arc<Semaphore>,
backend: PingBackend,
}
impl PingScanner {
pub fn new(concurrency: usize) -> Self {
let concurrency = concurrency.clamp(1, crate::MAX_CONCURRENCY);
Self {
semaphore: Arc::new(Semaphore::new(concurrency)),
backend: if *RAW_ICMP_OK.get_or_init(can_use_raw_icmpv4) {
PingBackend::RawIcmpV4
} else {
PingBackend::TcpConnect
},
}
}
pub async fn ping(&self, target: IpAddr, timeout_ms: u64) -> PingResult {
let seq = next_seq();
let _permit = match self.semaphore.acquire().await {
Ok(p) => p,
Err(_) => {
return PingResult {
ip: target,
rtt_ms: None,
ttl: None,
alive: false,
seq,
error: Some("ping concurrency semaphore closed".to_string()),
method: None,
};
}
};
match (self.backend, target) {
(PingBackend::RawIcmpV4, IpAddr::V4(_)) => ping_icmpv4(target, timeout_ms, seq).await,
#[cfg(windows)]
(_, IpAddr::V6(v6)) => ping_icmpv6(v6, timeout_ms, seq).await,
_ => ping_tcp_probe(target, timeout_ms, seq).await,
}
}
}
#[cfg(windows)]
fn can_use_raw_icmpv4() -> bool {
true
}
#[cfg(not(windows))]
use raw_icmp::can_use_raw_icmpv4;
async fn ping_icmpv4(target: IpAddr, timeout_ms: u64, seq: u16) -> PingResult {
let handle = tokio::task::spawn_blocking(move || send_icmp_echo_v4(target, timeout_ms, seq));
match handle.await {
Ok(Ok((rtt, ttl))) => PingResult {
ip: target,
rtt_ms: Some(rtt),
ttl,
alive: true,
seq,
error: None,
method: Some("icmpv4".to_string()),
},
Ok(Err(e)) => PingResult {
ip: target,
rtt_ms: None,
ttl: None,
alive: false,
seq,
error: Some(e.to_string()),
method: Some("icmpv4".to_string()),
},
Err(e) => PingResult {
ip: target,
rtt_ms: None,
ttl: None,
alive: false,
seq,
error: Some(format!("ping task failed: {e}")),
method: Some("icmpv4".to_string()),
},
}
}
#[cfg(windows)]
async fn ping_icmpv6(target: std::net::Ipv6Addr, timeout_ms: u64, seq: u16) -> PingResult {
let outcome =
tokio::task::spawn_blocking(move || windows_icmp::send_echo6(target, timeout_ms)).await;
let (rtt_ms, error) = match outcome {
Ok(Ok(rtt)) => (Some(rtt), None),
Ok(Err(e)) => (None, Some(e.to_string())),
Err(e) => (None, Some(format!("ping task failed: {e}"))),
};
PingResult {
ip: IpAddr::V6(target),
alive: rtt_ms.is_some(),
rtt_ms,
ttl: None,
seq,
error,
method: Some("icmpv6".to_string()),
}
}
async fn ping_tcp_probe(target: IpAddr, timeout_ms: u64, seq: u16) -> PingResult {
use std::net::SocketAddr;
use tokio::net::TcpStream;
use tokio::time::timeout;
const PROBE_PORTS: &[u16] = &[80, 443, 22];
let start = std::time::Instant::now();
let mut last_err: Option<String> = None;
for &port in PROBE_PORTS {
let addr = SocketAddr::new(target, port);
let attempt = timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr)).await;
match attempt {
Ok(Ok(_stream)) => {
return PingResult {
ip: target,
rtt_ms: Some(start.elapsed().as_millis() as u64),
ttl: None,
alive: true,
seq,
error: None,
method: Some("tcp-connect".to_string()),
};
}
Ok(Err(e)) => {
if matches!(
e.kind(),
std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::ConnectionReset
) {
return PingResult {
ip: target,
rtt_ms: Some(start.elapsed().as_millis() as u64),
ttl: None,
alive: true,
seq,
error: None,
method: Some("tcp-connect".to_string()),
};
}
last_err = Some(format!("tcp probe port {port}: {e}"));
}
Err(_elapsed) => {
last_err = Some(format!("tcp probe port {port}: timeout"));
}
}
}
PingResult {
ip: target,
rtt_ms: None,
ttl: None,
alive: false,
seq,
error: last_err,
method: Some("tcp-connect".to_string()),
}
}
#[cfg(windows)]
fn send_icmp_echo_v4(
target: IpAddr,
timeout_ms: u64,
seq: u16,
) -> anyhow::Result<(u64, Option<u8>)> {
let _ = seq;
windows_icmp::send_echo(target, timeout_ms)
}