1use pnet_packet::icmp::{echo_reply, echo_request, IcmpTypes};
2use pnet_packet::ip::IpNextHeaderProtocols;
3use pnet_packet::util::checksum;
4use pnet_packet::Packet;
5use pnet_transport::{transport_channel, TransportChannelType, TransportReceiver};
6use serde::Serialize;
7use std::net::IpAddr;
8use std::sync::atomic::{AtomicU16, Ordering};
9use std::sync::{Arc, OnceLock};
10use std::time::Duration;
11use tokio::sync::Semaphore;
12
13static PING_SEQ: AtomicU16 = AtomicU16::new(1);
20
21fn next_seq() -> u16 {
22 PING_SEQ.fetch_add(1, Ordering::Relaxed)
27}
28
29static RAW_ICMP_OK: OnceLock<bool> = OnceLock::new();
33
34#[derive(Debug, Clone, Serialize)]
35pub struct PingResult {
36 pub ip: IpAddr,
37 pub rtt_ms: Option<u64>,
38 pub alive: bool,
39 pub seq: u16,
42 #[serde(skip_serializing_if = "Option::is_none")]
43 pub error: Option<String>,
44 #[serde(skip_serializing_if = "Option::is_none")]
46 pub method: Option<String>,
47}
48
49#[derive(Debug, Clone, Copy)]
50enum PingBackend {
51 RawIcmpV4,
53 TcpConnect,
55}
56
57#[derive(Clone)]
58pub struct PingScanner {
59 semaphore: Arc<Semaphore>,
60 backend: PingBackend,
61}
62
63impl PingScanner {
64 pub fn new(concurrency: usize) -> Self {
65 let concurrency = concurrency.max(1);
66 Self {
67 semaphore: Arc::new(Semaphore::new(concurrency)),
68 backend: if *RAW_ICMP_OK.get_or_init(can_use_raw_icmpv4) {
69 PingBackend::RawIcmpV4
70 } else {
71 PingBackend::TcpConnect
72 },
73 }
74 }
75
76 pub async fn ping(&self, target: IpAddr, timeout_ms: u64) -> PingResult {
77 let seq = next_seq();
78 let _permit = match self.semaphore.acquire().await {
79 Ok(p) => p,
80 Err(_) => {
81 return PingResult {
82 ip: target,
83 rtt_ms: None,
84 alive: false,
85 seq,
86 error: Some("ping concurrency semaphore closed".to_string()),
87 method: None,
88 };
89 }
90 };
91
92 match (self.backend, target) {
94 (PingBackend::RawIcmpV4, IpAddr::V4(_)) => ping_icmpv4(target, timeout_ms, seq).await,
95 _ => ping_tcp_probe(target, timeout_ms, seq).await,
96 }
97 }
98}
99
100fn can_use_raw_icmpv4() -> bool {
101 let protocol = TransportChannelType::Layer3(IpNextHeaderProtocols::Icmp);
106 transport_channel(4096, protocol).is_ok()
107}
108
109async fn ping_icmpv4(target: IpAddr, timeout_ms: u64, seq: u16) -> PingResult {
110 let handle = tokio::task::spawn_blocking(move || send_icmp_echo_v4(target, timeout_ms, seq));
112 match handle.await {
113 Ok(Ok(rtt)) => PingResult {
114 ip: target,
115 rtt_ms: Some(rtt),
116 alive: true,
117 seq,
118 error: None,
119 method: Some("icmpv4".to_string()),
120 },
121 Ok(Err(e)) => PingResult {
122 ip: target,
123 rtt_ms: None,
124 alive: false,
125 seq,
126 error: Some(e.to_string()),
127 method: Some("icmpv4".to_string()),
128 },
129 Err(e) => PingResult {
130 ip: target,
131 rtt_ms: None,
132 alive: false,
133 seq,
134 error: Some(format!("ping task failed: {e}")),
135 method: Some("icmpv4".to_string()),
136 },
137 }
138}
139
140async fn ping_tcp_probe(target: IpAddr, timeout_ms: u64, seq: u16) -> PingResult {
141 use std::net::SocketAddr;
142 use tokio::net::TcpStream;
143 use tokio::time::timeout;
144
145 const PROBE_PORTS: &[u16] = &[80, 443, 22];
148
149 let start = std::time::Instant::now();
150 let mut last_err: Option<String> = None;
151
152 for &port in PROBE_PORTS {
153 let addr = SocketAddr::new(target, port);
154 let attempt = timeout(Duration::from_millis(timeout_ms), TcpStream::connect(addr)).await;
155 match attempt {
156 Ok(Ok(_stream)) => {
157 return PingResult {
158 ip: target,
159 rtt_ms: Some(start.elapsed().as_millis() as u64),
160 alive: true,
161 seq,
162 error: None,
163 method: Some("tcp-connect".to_string()),
164 };
165 }
166 Ok(Err(e)) => {
167 if matches!(
169 e.kind(),
170 std::io::ErrorKind::ConnectionRefused | std::io::ErrorKind::ConnectionReset
171 ) {
172 return PingResult {
173 ip: target,
174 rtt_ms: Some(start.elapsed().as_millis() as u64),
175 alive: true,
176 seq,
177 error: None,
178 method: Some("tcp-connect".to_string()),
179 };
180 }
181 last_err = Some(format!("tcp probe port {port}: {e}"));
182 }
183 Err(_elapsed) => {
184 last_err = Some(format!("tcp probe port {port}: timeout"));
185 }
186 }
187 }
188
189 PingResult {
190 ip: target,
191 rtt_ms: None,
192 alive: false,
193 seq,
194 error: last_err,
195 method: Some("tcp-connect".to_string()),
196 }
197}
198
199fn send_icmp_echo_v4(target: IpAddr, timeout_ms: u64, seq: u16) -> anyhow::Result<u64> {
204 let protocol = TransportChannelType::Layer3(IpNextHeaderProtocols::Icmp);
205 let (mut tx, mut rx) = transport_channel(4096, protocol)?;
206 configure_read_timeout(&rx, Duration::from_millis(10))?;
207
208 let identifier = std::process::id() as u16;
209
210 let mut buffer = [0u8; 64];
211 let mut echo_packet = echo_request::MutableEchoRequestPacket::new(&mut buffer)
212 .ok_or_else(|| anyhow::anyhow!("failed to build ICMP echo request packet"))?;
213
214 echo_packet.set_icmp_type(IcmpTypes::EchoRequest);
215 echo_packet.set_identifier(identifier);
216 echo_packet.set_sequence_number(seq);
217
218 let checksum = checksum(echo_packet.packet(), 1);
219 echo_packet.set_checksum(checksum);
220
221 let start = std::time::Instant::now();
222 tx.send_to(echo_packet, target)?;
223
224 let mut iter = pnet_transport::icmp_packet_iter(&mut rx);
225 let end_time = start + Duration::from_millis(timeout_ms);
226
227 while std::time::Instant::now() < end_time {
228 match iter.next() {
229 Ok((packet, addr)) => {
230 if addr != target || packet.get_icmp_type() != IcmpTypes::EchoReply {
231 continue;
232 }
233 if let Some(reply) = echo_reply::EchoReplyPacket::new(packet.packet()) {
240 if reply.get_identifier() == identifier && reply.get_sequence_number() == seq {
241 return Ok(start.elapsed().as_millis() as u64);
242 }
243 }
245 }
246 Err(_) => {
247 std::thread::sleep(Duration::from_millis(1));
250 }
251 }
252 }
253
254 Err(anyhow::anyhow!("Timeout"))
255}
256
257fn configure_read_timeout(rx: &TransportReceiver, timeout: Duration) -> anyhow::Result<()> {
258 #[cfg(unix)]
259 {
260 pnet_sys::set_socket_receive_timeout(rx.socket.fd, timeout)
261 .map_err(|e| anyhow::anyhow!("failed to set read timeout: {e}"))?;
262 }
263
264 #[cfg(windows)]
265 {
266 use windows_sys::Win32::Networking::WinSock::{
267 setsockopt, SOCKET_ERROR, SOL_SOCKET, SO_RCVTIMEO,
268 };
269
270 let timeout_ms = timeout.as_millis().min(i32::MAX as u128) as i32;
271 let result = unsafe {
272 setsockopt(
273 rx.socket.fd,
274 SOL_SOCKET,
275 SO_RCVTIMEO,
276 (&timeout_ms as *const i32).cast(),
277 std::mem::size_of::<i32>() as i32,
278 )
279 };
280 if result == SOCKET_ERROR {
281 return Err(anyhow::anyhow!(
282 "failed to set read timeout: {}",
283 std::io::Error::last_os_error()
284 ));
285 }
286 }
287
288 Ok(())
289}