use std::net::{IpAddr, Ipv4Addr};
use std::process::Stdio;
#[cfg(unix)]
use std::time::Duration;
use if_addrs::{IfAddr, get_if_addrs};
use tokio::process::Command;
#[cfg(unix)]
use tiny_ping::Pinger;
const HELP: &str = r#"
USAGE:
pingall [FLAGS]
FLAGS:
-i <interface> Interface to search
-d, --dont-resolve Don't attempt to resolve hostnames
-h, --help Prints help information
-r, --raw-socket Open raw socket instead of using system `ping` command. Unix only, requires permissions
-t, --timeout Timeout of pings in seconds (default 1)
"#;
#[derive(Debug)]
pub(crate) struct Args {
pub(crate) interface: Option<String>,
pub(crate) dont_resolve: bool,
pub(crate) raw_socket: bool,
pub(crate) timeout: usize,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub(crate) enum PingBackend {
System,
RawSocket,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(dead_code)]
enum RuntimePlatform {
Unix,
NonUnix,
}
#[cfg(unix)]
fn current_runtime_platform() -> RuntimePlatform {
RuntimePlatform::Unix
}
#[cfg(not(unix))]
fn current_runtime_platform() -> RuntimePlatform {
RuntimePlatform::NonUnix
}
pub(crate) fn raw_socket_supported() -> bool {
current_runtime_platform() == RuntimePlatform::Unix
}
fn select_ping_backend_for(
platform: RuntimePlatform,
raw_socket_requested: bool,
system_ping_exists: bool,
) -> Result<PingBackend, &'static str> {
match platform {
RuntimePlatform::Unix => {
if raw_socket_requested || !system_ping_exists {
Ok(PingBackend::RawSocket)
} else {
Ok(PingBackend::System)
}
}
RuntimePlatform::NonUnix => {
if system_ping_exists {
Ok(PingBackend::System)
} else {
Err(
"system `ping` command not found and raw sockets are unsupported on this platform",
)
}
}
}
}
pub(crate) fn select_ping_backend(
raw_socket_requested: bool,
system_ping_exists: bool,
) -> Result<PingBackend, &'static str> {
select_ping_backend_for(
current_runtime_platform(),
raw_socket_requested,
system_ping_exists,
)
}
pub(crate) fn get_args() -> Args {
let mut pargs = pico_args::Arguments::from_env();
if pargs.contains(["-h", "--help"]) {
print!("{}", HELP);
std::process::exit(0);
}
Args {
interface: pargs.opt_value_from_str("-i").unwrap(),
dont_resolve: pargs.contains(["-d", "--dont-resolve"]),
raw_socket: pargs.contains(["-r", "--raw-socket"]),
timeout: pargs
.value_from_fn(["-t", "--timeout"], str::parse)
.unwrap_or(1),
}
}
pub(crate) fn command_exists(command: &str) -> bool {
which::which(command).is_ok()
}
pub(crate) fn hostname_resolution_supported() -> bool {
if cfg!(target_os = "linux") {
command_exists("avahi-resolve")
} else {
cfg!(windows)
}
}
pub(crate) fn get_addresses(interface: Option<String>) -> Vec<Ipv4Addr> {
let ifaddrs = match get_if_addrs() {
Ok(ifaddrs) => ifaddrs,
Err(_) => {
eprintln!("Failed to get network interfaces");
return Vec::new();
}
};
let addresses = ifaddrs.into_iter().filter_map(|ifaddr| {
if interface.as_ref().is_some_and(|name| ifaddr.name != *name) {
return None;
}
if ifaddr.is_loopback() {
return None;
}
match ifaddr.addr {
IfAddr::V4(addr) => Some(addr.ip),
IfAddr::V6(_) => None,
}
});
if interface.is_some() {
addresses.take(1).collect()
} else {
addresses.collect()
}
}
#[allow(dead_code)]
fn format_hostname(ip_addr: &IpAddr, hostname: &str) -> Option<String> {
let hostname = hostname.trim().trim_end_matches('.');
if hostname.is_empty() || hostname == ip_addr.to_string() {
return None;
}
Some(format!("{}\t{}", ip_addr, hostname))
}
#[cfg(target_os = "linux")]
fn parse_avahi_resolve_output(ip_addr: &IpAddr, output: &[u8]) -> Option<String> {
let output = String::from_utf8_lossy(output);
output.lines().find_map(|line| {
let mut parts = line.split_whitespace();
let ip = parts.next()?;
let hostname = parts.next()?;
if ip == ip_addr.to_string() {
format_hostname(ip_addr, hostname)
} else {
None
}
})
}
#[cfg(target_os = "linux")]
pub(crate) async fn resolve_hostname(ip_addr: &IpAddr) -> Option<String> {
let output = Command::new("avahi-resolve")
.arg("--address")
.arg(ip_addr.to_string())
.stderr(Stdio::null())
.output()
.await
.ok()?;
if output.status.success() && !output.stdout.is_empty() {
parse_avahi_resolve_output(ip_addr, &output.stdout)
} else {
None
}
}
#[cfg(windows)]
pub(crate) async fn resolve_hostname(ip_addr: &IpAddr) -> Option<String> {
let ip_addr = *ip_addr;
let lookup = tokio::task::spawn_blocking(move || dns_lookup::lookup_addr(&ip_addr))
.await
.ok()?
.ok()?;
format_hostname(&ip_addr, &lookup)
}
#[cfg(not(any(target_os = "linux", windows)))]
pub(crate) async fn resolve_hostname(_ip_addr: &IpAddr) -> Option<String> {
None
}
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
#[allow(dead_code)]
enum PingPlatform {
Windows,
Linux,
Macos,
OtherUnix,
}
#[cfg(windows)]
fn current_ping_platform() -> PingPlatform {
PingPlatform::Windows
}
#[cfg(target_os = "linux")]
fn current_ping_platform() -> PingPlatform {
PingPlatform::Linux
}
#[cfg(target_os = "macos")]
fn current_ping_platform() -> PingPlatform {
PingPlatform::Macos
}
#[cfg(all(not(windows), not(target_os = "linux"), not(target_os = "macos")))]
fn current_ping_platform() -> PingPlatform {
PingPlatform::OtherUnix
}
fn system_ping_args(platform: PingPlatform, ip_addr: &IpAddr, timeout: usize) -> Vec<String> {
match platform {
PingPlatform::Windows => vec![
"/n".to_string(),
"1".to_string(),
"/w".to_string(),
timeout.saturating_mul(1000).to_string(),
ip_addr.to_string(),
],
PingPlatform::Linux | PingPlatform::OtherUnix => vec![
"-c".to_string(),
"1".to_string(),
"-W".to_string(),
timeout.to_string(),
ip_addr.to_string(),
],
PingPlatform::Macos => vec![
"-c".to_string(),
"1".to_string(),
"-W".to_string(),
timeout.saturating_mul(1000).to_string(),
ip_addr.to_string(),
],
}
}
pub(crate) async fn system_ping(ip_addr: &IpAddr, timeout: usize) -> bool {
let args = system_ping_args(current_ping_platform(), ip_addr, timeout);
let mut command = match Command::new("ping")
.args(args)
.stdout(Stdio::null())
.stderr(Stdio::null())
.spawn()
{
Ok(cmd) => cmd,
Err(_) => return false, };
match command.wait().await {
Ok(status) => status.success(),
Err(_) => false,
}
}
#[cfg(unix)]
pub(crate) async fn socket_ping(ip_addr: &IpAddr, timeout: usize) -> bool {
if let Ok(mut pinger) = Pinger::new(*ip_addr) {
pinger.timeout(Duration::from_secs(timeout as u64));
return pinger.ping(0).await.is_ok();
}
false
}
#[cfg(not(unix))]
pub(crate) async fn socket_ping(_ip_addr: &IpAddr, _timeout: usize) -> bool {
false
}
#[cfg(unix)]
pub(crate) async fn can_open_raw_socket() -> bool {
let localhost = IpAddr::V4(Ipv4Addr::LOCALHOST);
if let Ok(mut pinger) = Pinger::new(localhost) {
pinger.timeout(Duration::from_secs(1));
return pinger.ping(0).await.is_ok();
}
false
}
#[cfg(not(unix))]
pub(crate) async fn can_open_raw_socket() -> bool {
false
}
#[cfg(test)]
mod tests {
use super::{
PingBackend, PingPlatform, RuntimePlatform, format_hostname, select_ping_backend_for,
system_ping_args,
};
use std::net::{IpAddr, Ipv4Addr};
#[test]
fn windows_ping_args_use_count_and_millisecond_timeout() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(
system_ping_args(PingPlatform::Windows, &ip, 1),
vec!["/n", "1", "/w", "1000", "192.168.1.1"]
);
}
#[test]
fn linux_ping_args_use_count_and_second_timeout() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(
system_ping_args(PingPlatform::Linux, &ip, 1),
vec!["-c", "1", "-W", "1", "192.168.1.1"]
);
}
#[test]
fn macos_ping_args_use_count_and_millisecond_timeout() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 1));
assert_eq!(
system_ping_args(PingPlatform::Macos, &ip, 1),
vec!["-c", "1", "-W", "1000", "192.168.1.1"]
);
}
#[test]
fn ping_backend_variants_stay_distinct() {
assert_ne!(PingBackend::System, PingBackend::RawSocket);
}
#[test]
fn unix_backend_uses_raw_socket_when_requested_or_ping_missing() {
assert_eq!(
select_ping_backend_for(RuntimePlatform::Unix, true, true),
Ok(PingBackend::RawSocket)
);
assert_eq!(
select_ping_backend_for(RuntimePlatform::Unix, false, false),
Ok(PingBackend::RawSocket)
);
assert_eq!(
select_ping_backend_for(RuntimePlatform::Unix, false, true),
Ok(PingBackend::System)
);
}
#[test]
fn non_unix_backend_uses_system_ping_even_when_raw_requested() {
assert_eq!(
select_ping_backend_for(RuntimePlatform::NonUnix, true, true),
Ok(PingBackend::System)
);
}
#[test]
fn non_unix_backend_errors_when_system_ping_is_missing() {
assert!(select_ping_backend_for(RuntimePlatform::NonUnix, false, false).is_err());
}
#[test]
fn hostname_output_matches_existing_ip_hostname_format() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10));
assert_eq!(
format_hostname(&ip, "printer.local."),
Some("192.168.1.10\tprinter.local".to_string())
);
}
#[test]
fn hostname_output_ignores_empty_and_numeric_names() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10));
assert_eq!(format_hostname(&ip, ""), None);
assert_eq!(format_hostname(&ip, "192.168.1.10"), None);
}
#[cfg(target_os = "linux")]
#[test]
fn avahi_output_parses_ip_hostname_line() {
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 10));
assert_eq!(
super::parse_avahi_resolve_output(&ip, b"192.168.1.10\tprinter.local\n"),
Some("192.168.1.10\tprinter.local".to_string())
);
}
}