iroh_net/
ping.rs

1//! Allows sending ICMP echo requests to a host in order to determine network latency.
2
3use 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/// Whether this error was because we couldn't create a client or a send error.
17#[derive(Debug, thiserror::Error)]
18pub enum PingError {
19    /// Could not create client, probably bind error.
20    #[error("Error creating ping client")]
21    Client(#[from] anyhow::Error),
22    /// Could not send ping.
23    #[error("Error sending ping")]
24    Ping(#[from] surge_ping::SurgeError),
25}
26
27/// Allows sending ICMP echo requests to a host in order to determine network latency.
28/// Will gracefully handle both IPv4 and IPv6.
29#[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    /// Create a new [Pinger].
46    pub fn new() -> Self {
47        Default::default()
48    }
49
50    /// Lazily create the ping client.
51    ///
52    /// We do this because it means we do not bind a socket until we really try to send a
53    /// ping.  It makes it more transparent to use the pinger.
54    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    /// Send a ping request with associated data, returning the perceived latency.
85    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); // todo: timeout too large for netcheck
94        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] // Doesn't work in CI
132    async fn test_ping_google() -> Result<()> {
133        let _guard = iroh_test::logging::setup();
134
135        // Public DNS addrs from google based on
136        // https://developers.google.com/speed/public-dns/docs/using
137
138        let pinger = Pinger::new();
139
140        // IPv4
141        let dur = pinger.send("8.8.8.8".parse()?, &[1u8; 8]).await?;
142        assert!(!dur.is_zero());
143
144        // IPv6
145        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    // See netcheck::reportgen::tests::test_icmp_probe_eu_relay for permissions to ping.
161    #[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                // We don't have permission, too bad.
173                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                // We don't have permission, too bad.
186                error!("no ping permissions: {err:#}");
187            }
188            Err(PingError::Ping(err)) => {
189                error!("ping failed, probably no IPv6 stack: {err:#}");
190            }
191        }
192    }
193}