1use std::{
4 fmt::Debug,
5 net::IpAddr,
6 sync::{Arc, Mutex},
7 time::Duration,
8};
9
10use anyhow::{Context, Result};
11use surge_ping::{Client, Config, IcmpPacket, PingIdentifier, PingSequence, ICMP};
12use tracing::debug;
13
14use crate::defaults::timeouts::DEFAULT_PINGER_TIMEOUT as DEFAULT_TIMEOUT;
15
16#[derive(Debug, thiserror::Error)]
18pub enum PingError {
19 #[error("Error creating ping client")]
21 Client(#[from] anyhow::Error),
22 #[error("Error sending ping")]
24 Ping(#[from] surge_ping::SurgeError),
25}
26
27#[derive(Debug, Clone, Default)]
30pub struct Pinger(Arc<Inner>);
31
32impl Debug for Inner {
33 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34 f.debug_struct("Inner").finish()
35 }
36}
37
38#[derive(Default)]
39struct Inner {
40 client_v6: Mutex<Option<Client>>,
41 client_v4: Mutex<Option<Client>>,
42}
43
44impl Pinger {
45 pub fn new() -> Self {
47 Default::default()
48 }
49
50 fn get_client(&self, kind: ICMP) -> Result<Client> {
55 let client = match kind {
56 ICMP::V4 => {
57 let mut opt_client = self.0.client_v4.lock().unwrap();
58 match *opt_client {
59 Some(ref client) => client.clone(),
60 None => {
61 let cfg = Config::builder().kind(kind).build();
62 let client = Client::new(&cfg).context("failed to create IPv4 pinger")?;
63 *opt_client = Some(client.clone());
64 client
65 }
66 }
67 }
68 ICMP::V6 => {
69 let mut opt_client = self.0.client_v6.lock().unwrap();
70 match *opt_client {
71 Some(ref client) => client.clone(),
72 None => {
73 let cfg = Config::builder().kind(kind).build();
74 let client = Client::new(&cfg).context("failed to create IPv6 pinger")?;
75 *opt_client = Some(client.clone());
76 client
77 }
78 }
79 }
80 };
81 Ok(client)
82 }
83
84 pub async fn send(&self, addr: IpAddr, data: &[u8]) -> Result<Duration, PingError> {
86 let client = match addr {
87 IpAddr::V4(_) => self.get_client(ICMP::V4).map_err(PingError::Client)?,
88 IpAddr::V6(_) => self.get_client(ICMP::V6).map_err(PingError::Client)?,
89 };
90 let ident = PingIdentifier(rand::random());
91 debug!(%addr, %ident, "Creating pinger");
92 let mut pinger = client.pinger(addr, ident).await;
93 pinger.timeout(DEFAULT_TIMEOUT); match pinger.ping(PingSequence(0), data).await? {
95 (IcmpPacket::V4(packet), dur) => {
96 debug!(
97 "{} bytes from {}: icmp_seq={} ttl={:?} time={:0.2?}",
98 packet.get_size(),
99 packet.get_source(),
100 packet.get_sequence(),
101 packet.get_ttl(),
102 dur
103 );
104 Ok(dur)
105 }
106
107 (IcmpPacket::V6(packet), dur) => {
108 debug!(
109 "{} bytes from {}: icmp_seq={} hlim={} time={:0.2?}",
110 packet.get_size(),
111 packet.get_source(),
112 packet.get_sequence(),
113 packet.get_max_hop_limit(),
114 dur
115 );
116 Ok(dur)
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use std::net::{Ipv4Addr, Ipv6Addr};
125
126 use tracing::error;
127
128 use super::*;
129
130 #[tokio::test]
131 #[ignore] async fn test_ping_google() -> Result<()> {
133 let _guard = iroh_test::logging::setup();
134
135 let pinger = Pinger::new();
139
140 let dur = pinger.send("8.8.8.8".parse()?, &[1u8; 8]).await?;
142 assert!(!dur.is_zero());
143
144 match pinger
146 .send("2001:4860:4860:0:0:0:0:8888".parse()?, &[1u8; 8])
147 .await
148 {
149 Ok(dur) => {
150 assert!(!dur.is_zero());
151 }
152 Err(err) => {
153 tracing::error!("IPv6 is not available: {:?}", err);
154 }
155 }
156
157 Ok(())
158 }
159
160 #[tokio::test]
162 async fn test_ping_localhost() {
163 let _guard = iroh_test::logging::setup();
164
165 let pinger = Pinger::new();
166
167 match pinger.send(Ipv4Addr::LOCALHOST.into(), b"data").await {
168 Ok(duration) => {
169 assert!(!duration.is_zero());
170 }
171 Err(PingError::Client(err)) => {
172 error!("no ping permissions: {err:#}");
174 }
175 Err(PingError::Ping(err)) => {
176 panic!("ping failed: {err:#}");
177 }
178 }
179
180 match pinger.send(Ipv6Addr::LOCALHOST.into(), b"data").await {
181 Ok(duration) => {
182 assert!(!duration.is_zero());
183 }
184 Err(PingError::Client(err)) => {
185 error!("no ping permissions: {err:#}");
187 }
188 Err(PingError::Ping(err)) => {
189 error!("ping failed, probably no IPv6 stack: {err:#}");
190 }
191 }
192 }
193}