1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
use std::collections::BTreeSet;
use std::io::{IsTerminal, stderr, stdout};
use std::net::IpAddr;
use std::process::Stdio;
use std::sync::Arc;
mod util;
use util::{
PingBackend, can_open_raw_socket, command_exists, get_addresses, get_args,
raw_socket_supported, select_ping_backend, socket_ping, system_ping,
};
use tokio::process::Command;
use tokio::sync::Semaphore;
type PingResults = Vec<tokio::task::JoinHandle<Option<String>>>;
fn main() {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async {
if let Err(e) = run().await {
eprintln!("Error: {}", e);
std::process::exit(1);
}
})
}
async fn run() -> Result<(), Box<dyn std::error::Error>> {
// Parse CLI args.
let args = get_args();
// Whether to attempt to resolve hostnames.
let resolve = if args.dont_resolve {
false
} else if cfg!(target_os = "linux") {
match command_exists("avahi-resolve") {
true => true,
false => {
if stdout().is_terminal() {
eprintln!("`avahi-resolve` not found, hostname resolution disabled");
}
false
}
}
} else {
false
};
if args.raw_socket && !raw_socket_supported() && stderr().is_terminal() {
eprintln!(
"Raw socket mode is unsupported on this platform; falling back to system `ping`."
);
}
// Whether to use system `ping` command, or open a socket ourselves.
let ping_backend = select_ping_backend(args.raw_socket, command_exists("ping"))?;
// Check we have permission to open raw sockets.
if ping_backend == PingBackend::RawSocket
&& !can_open_raw_socket().await
&& stderr().is_terminal()
{
let err_msg = "Either run as root, or run `setcap cap_net_raw+ep $(which pingall)` to allow this app to open raw sockets.";
eprintln!("Error opening raw socket.\n{}", err_msg);
}
// Get our IP address on each interface we'll be checking.
let addresses = get_addresses(args.interface);
// Create a semaphore to limit concurrent operations (prevents "too many open files")
let semaphore = Arc::new(Semaphore::new(150)); // Allow max 50 concurrent pings
// Ping the subnet and record replies.
let mut results = vec![];
for address in addresses {
let octets = address.octets();
let ip_subnet = format!("{}.{}.{}.", octets[0], octets[1], octets[2]);
results.extend(
run_subnet(
&ip_subnet,
resolve,
ping_backend,
args.timeout,
semaphore.clone(),
)
.await?,
);
}
let mut seen = BTreeSet::new();
// Print successful pings.
for ping in results {
match ping.await {
Ok(Some(result)) => {
if !seen.contains(&result) {
println!("{}", result);
seen.insert(result);
}
}
Ok(None) => {
// Ping failed, continue to next
}
Err(e) => {
// Task panicked or was cancelled, log but continue
if stderr().is_terminal() {
eprintln!("Warning: ping task failed: {}", e);
}
}
}
}
Ok(())
}
/// Ping all the IP addresses on a subnet formatted `X.X.X.`.
async fn run_subnet(
subnet: &str,
resolve_hostname: bool,
ping_backend: PingBackend,
timeout: usize,
semaphore: Arc<Semaphore>,
) -> Result<PingResults, Box<dyn std::error::Error>> {
Ok((1..255)
.filter_map(|i| {
// Parse IP address safely
let ip_str = format!("{}{}", subnet, i);
match ip_str.parse() {
Ok(ip_v4) => {
let ip_addr = IpAddr::V4(ip_v4);
let semaphore = semaphore.clone();
Some(tokio::spawn(async move {
// Acquire permit before doing any work
let _permit = match semaphore.acquire().await {
Ok(permit) => permit,
Err(_) => return None, // Semaphore closed, skip this ping
};
// Ping the address.
let success = match ping_backend {
PingBackend::RawSocket => socket_ping(&ip_addr, timeout).await,
PingBackend::System => system_ping(&ip_addr, timeout).await,
};
match (success, resolve_hostname) {
(true, true) => {
// Try to resolve hostname with `avahi-resolve`.
let get_hostname = Command::new("avahi-resolve")
.arg("--address")
.arg(ip_addr.to_string())
.stderr(Stdio::null())
.output();
match get_hostname.await {
Ok(output) => {
if output.status.success() && !output.stdout.is_empty() {
// Send back hostname and IP.
let utf8_out = String::from_utf8_lossy(&output.stdout)
.trim()
.to_string();
Some(utf8_out)
} else {
// Only send back the IP addr.
Some(ip_addr.to_string())
}
}
Err(_) => {
// If avahi-resolve fails, just return the IP
Some(ip_addr.to_string())
}
}
}
(true, false) => Some(ip_addr.to_string()),
_ => None,
}
}))
}
Err(_) => {
// Skip invalid IP addresses silently
None
}
}
})
.collect())
}