iroh_net/
ping.rs

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
188
189
190
191
192
193
//! Allows sending ICMP echo requests to a host in order to determine network latency.

use std::{
    fmt::Debug,
    net::IpAddr,
    sync::{Arc, Mutex},
    time::Duration,
};

use anyhow::{Context, Result};
use surge_ping::{Client, Config, IcmpPacket, PingIdentifier, PingSequence, ICMP};
use tracing::debug;

use crate::defaults::timeouts::DEFAULT_PINGER_TIMEOUT as DEFAULT_TIMEOUT;

/// Whether this error was because we couldn't create a client or a send error.
#[derive(Debug, thiserror::Error)]
pub enum PingError {
    /// Could not create client, probably bind error.
    #[error("Error creating ping client")]
    Client(#[from] anyhow::Error),
    /// Could not send ping.
    #[error("Error sending ping")]
    Ping(#[from] surge_ping::SurgeError),
}

/// Allows sending ICMP echo requests to a host in order to determine network latency.
/// Will gracefully handle both IPv4 and IPv6.
#[derive(Debug, Clone, Default)]
pub struct Pinger(Arc<Inner>);

impl Debug for Inner {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Inner").finish()
    }
}

#[derive(Default)]
struct Inner {
    client_v6: Mutex<Option<Client>>,
    client_v4: Mutex<Option<Client>>,
}

impl Pinger {
    /// Create a new [Pinger].
    pub fn new() -> Self {
        Default::default()
    }

    /// Lazily create the ping client.
    ///
    /// We do this because it means we do not bind a socket until we really try to send a
    /// ping.  It makes it more transparent to use the pinger.
    fn get_client(&self, kind: ICMP) -> Result<Client> {
        let client = match kind {
            ICMP::V4 => {
                let mut opt_client = self.0.client_v4.lock().unwrap();
                match *opt_client {
                    Some(ref client) => client.clone(),
                    None => {
                        let cfg = Config::builder().kind(kind).build();
                        let client = Client::new(&cfg).context("failed to create IPv4 pinger")?;
                        *opt_client = Some(client.clone());
                        client
                    }
                }
            }
            ICMP::V6 => {
                let mut opt_client = self.0.client_v6.lock().unwrap();
                match *opt_client {
                    Some(ref client) => client.clone(),
                    None => {
                        let cfg = Config::builder().kind(kind).build();
                        let client = Client::new(&cfg).context("failed to create IPv6 pinger")?;
                        *opt_client = Some(client.clone());
                        client
                    }
                }
            }
        };
        Ok(client)
    }

    /// Send a ping request with associated data, returning the perceived latency.
    pub async fn send(&self, addr: IpAddr, data: &[u8]) -> Result<Duration, PingError> {
        let client = match addr {
            IpAddr::V4(_) => self.get_client(ICMP::V4).map_err(PingError::Client)?,
            IpAddr::V6(_) => self.get_client(ICMP::V6).map_err(PingError::Client)?,
        };
        let ident = PingIdentifier(rand::random());
        debug!(%addr, %ident, "Creating pinger");
        let mut pinger = client.pinger(addr, ident).await;
        pinger.timeout(DEFAULT_TIMEOUT); // todo: timeout too large for netcheck
        match pinger.ping(PingSequence(0), data).await? {
            (IcmpPacket::V4(packet), dur) => {
                debug!(
                    "{} bytes from {}: icmp_seq={} ttl={:?} time={:0.2?}",
                    packet.get_size(),
                    packet.get_source(),
                    packet.get_sequence(),
                    packet.get_ttl(),
                    dur
                );
                Ok(dur)
            }

            (IcmpPacket::V6(packet), dur) => {
                debug!(
                    "{} bytes from {}: icmp_seq={} hlim={} time={:0.2?}",
                    packet.get_size(),
                    packet.get_source(),
                    packet.get_sequence(),
                    packet.get_max_hop_limit(),
                    dur
                );
                Ok(dur)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use std::net::{Ipv4Addr, Ipv6Addr};

    use tracing::error;

    use super::*;

    #[tokio::test]
    #[ignore] // Doesn't work in CI
    async fn test_ping_google() -> Result<()> {
        let _guard = iroh_test::logging::setup();

        // Public DNS addrs from google based on
        // https://developers.google.com/speed/public-dns/docs/using

        let pinger = Pinger::new();

        // IPv4
        let dur = pinger.send("8.8.8.8".parse()?, &[1u8; 8]).await?;
        assert!(!dur.is_zero());

        // IPv6
        match pinger
            .send("2001:4860:4860:0:0:0:0:8888".parse()?, &[1u8; 8])
            .await
        {
            Ok(dur) => {
                assert!(!dur.is_zero());
            }
            Err(err) => {
                tracing::error!("IPv6 is not available: {:?}", err);
            }
        }

        Ok(())
    }

    // See netcheck::reportgen::tests::test_icmp_probe_eu_relay for permissions to ping.
    #[tokio::test]
    async fn test_ping_localhost() {
        let _guard = iroh_test::logging::setup();

        let pinger = Pinger::new();

        match pinger.send(Ipv4Addr::LOCALHOST.into(), b"data").await {
            Ok(duration) => {
                assert!(!duration.is_zero());
            }
            Err(PingError::Client(err)) => {
                // We don't have permission, too bad.
                error!("no ping permissions: {err:#}");
            }
            Err(PingError::Ping(err)) => {
                panic!("ping failed: {err:#}");
            }
        }

        match pinger.send(Ipv6Addr::LOCALHOST.into(), b"data").await {
            Ok(duration) => {
                assert!(!duration.is_zero());
            }
            Err(PingError::Client(err)) => {
                // We don't have permission, too bad.
                error!("no ping permissions: {err:#}");
            }
            Err(PingError::Ping(err)) => {
                error!("ping failed, probably no IPv6 stack: {err:#}");
            }
        }
    }
}