Skip to main content

reachability_core/
ping.rs

1// Copyright 2019 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use async_trait::async_trait;
6use fuchsia_async::{self as fasync, TimeoutExt as _};
7use futures::{FutureExt as _, SinkExt as _, TryFutureExt as _, TryStreamExt as _};
8use net_types::ip::{Ipv4, Ipv6};
9use std::net::SocketAddr;
10
11const PING_MESSAGE: &str = "Hello from reachability monitor!";
12const SEQ_MIN: u16 = 1;
13const SEQ_MAX: u16 = 3;
14const TIMEOUT: fasync::MonotonicDuration = fasync::MonotonicDuration::from_seconds(1);
15
16#[derive(Debug, thiserror::Error)]
17pub enum PingError {
18    #[error("failed to create socket")]
19    CreateSocket(#[source] std::io::Error),
20    #[error("failed to bind socket to device {interface_name}")]
21    BindSocket {
22        interface_name: String,
23        #[source]
24        err: std::io::Error,
25    },
26    #[error("failed to send ping (seq={seq})")]
27    SendPing {
28        seq: u16,
29        #[source]
30        err: ping::PingError,
31    },
32    #[error("timed out sending ping (seq={seq})")]
33    SendPingTimeout { seq: u16 },
34    #[error("failed to receive ping")]
35    ReceivePing(#[source] ping::PingError),
36    #[error("ping reply stream ended unexpectedly")]
37    StreamEndedUnexpectedly,
38    #[error("received unexpected ping sequence number; got: {got}, want: {min}..={max}")]
39    UnexpectedSequenceNumber { got: u16, min: u16, max: u16 },
40    #[error("no ping reply received")]
41    NoReply,
42}
43
44impl PingError {
45    /// Returns a short, simplified string describing the error.
46    /// Each string should only take at most 14 characters, else the row labels for the
47    /// `gateway_ping_results` and `internet_ping_results` time series in internal visualization
48    /// tool would be truncated.
49    pub fn short_name(&self) -> String {
50        let (name, os_err) = match self {
51            Self::CreateSocket(err) => ("CreateSock", err.raw_os_error()),
52            Self::BindSocket { err, .. } => ("BindSock", err.raw_os_error()),
53            Self::SendPing { err, .. } => {
54                let os_err = match err {
55                    ping::PingError::Send(io) => io.raw_os_error(),
56                    ping::PingError::Recv(io) => io.raw_os_error(),
57                    _ => None,
58                };
59                ("SendPing", os_err)
60            }
61            Self::SendPingTimeout { .. } => return "SendTimeout".to_string(),
62            Self::ReceivePing(err) => {
63                let os_err = match err {
64                    ping::PingError::Send(io) => io.raw_os_error(),
65                    ping::PingError::Recv(io) => io.raw_os_error(),
66                    _ => None,
67                };
68                ("RecvPing", os_err)
69            }
70            Self::StreamEndedUnexpectedly => return "StreamEnded".to_string(),
71            Self::UnexpectedSequenceNumber { .. } => return "BadSeqNum".to_string(),
72            Self::NoReply => return "NoReply".to_string(),
73        };
74
75        if let Some(code) = os_err { format!("{name}_{code}") } else { name.to_string() }
76    }
77}
78
79pub(crate) fn ping_result_short_name(result: &Result<(), PingError>) -> String {
80    match result {
81        Ok(()) => "Success".to_string(),
82        Err(e) => format!("e_{}", e.short_name()),
83    }
84}
85
86async fn ping<I>(interface_name: &str, addr: I::SockAddr) -> Result<(), PingError>
87where
88    I: ping::FuchsiaIpExt,
89    I::SockAddr: std::fmt::Display + Copy,
90{
91    let socket = ping::new_icmp_socket::<I>().map_err(PingError::CreateSocket)?;
92    let () = socket
93        .bind_device(Some(interface_name.as_bytes()))
94        .map_err(|err| PingError::BindSocket { interface_name: interface_name.to_string(), err })?;
95    let (mut sink, mut stream) = ping::new_unicast_sink_and_stream::<
96        I,
97        _,
98        { PING_MESSAGE.len() + ping::ICMP_HEADER_LEN },
99    >(&socket, &addr, PING_MESSAGE.as_bytes());
100
101    for seq in SEQ_MIN..=SEQ_MAX {
102        let deadline = fasync::MonotonicInstant::after(TIMEOUT);
103        let () = sink
104            .send(seq)
105            .map_err(|err| PingError::SendPing { seq, err })
106            .on_timeout(deadline, || Err(PingError::SendPingTimeout { seq }))
107            .await?;
108        if match stream.try_next().map(Some).on_timeout(deadline, || None).await {
109            None => Ok(false),
110            Some(Err(err)) => Err(PingError::ReceivePing(err)),
111            Some(Ok(None)) => Err(PingError::StreamEndedUnexpectedly),
112            Some(Ok(Some(got))) if got >= SEQ_MIN && got <= seq => Ok(true),
113            Some(Ok(Some(got))) => {
114                Err(PingError::UnexpectedSequenceNumber { got, min: SEQ_MIN, max: seq })
115            }
116        }? {
117            return Ok(());
118        }
119    }
120    Err(PingError::NoReply)
121}
122
123/// Trait that can send ICMP echo requests, and receive and validate replies.
124#[async_trait]
125pub trait Ping {
126    /// Returns `Ok(())` if the address is reachable, or a `PingError` otherwise.
127    async fn ping(&self, interface_name: &str, addr: SocketAddr) -> Result<(), PingError>;
128}
129
130pub struct Pinger;
131
132#[async_trait]
133impl Ping for Pinger {
134    async fn ping(&self, interface_name: &str, addr: SocketAddr) -> Result<(), PingError> {
135        match addr {
136            SocketAddr::V4(addr_v4) => ping::<Ipv4>(interface_name, addr_v4).await,
137            SocketAddr::V6(addr_v6) => ping::<Ipv6>(interface_name, addr_v6).await,
138        }
139    }
140}