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 log::{info, warn};
9use net_types::ip::{Ipv4, Ipv6};
10use std::net::SocketAddr;
11
12const PING_MESSAGE: &str = "Hello from reachability monitor!";
13const SEQ_MIN: u16 = 1;
14const SEQ_MAX: u16 = 3;
15const TIMEOUT: fasync::MonotonicDuration = fasync::MonotonicDuration::from_seconds(1);
16
17#[derive(Debug, thiserror::Error)]
18pub enum PingError {
19    #[error("failed to create socket")]
20    CreateSocket(#[source] std::io::Error),
21    #[error("failed to bind socket to device {interface_name}")]
22    BindSocket {
23        interface_name: String,
24        #[source]
25        err: std::io::Error,
26    },
27    #[error("failed to send ping (seq={seq})")]
28    SendPing {
29        seq: u16,
30        #[source]
31        err: ping::PingError,
32    },
33    #[error("timed out sending ping (seq={seq})")]
34    SendPingTimeout { seq: u16 },
35    #[error("failed to receive ping")]
36    ReceivePing(#[source] ping::PingError),
37    #[error("ping reply stream ended unexpectedly")]
38    StreamEndedUnexpectedly,
39    #[error("received unexpected ping sequence number; got: {got}, want: {min}..={max}")]
40    UnexpectedSequenceNumber { got: u16, min: u16, max: u16 },
41    #[error("no ping reply received")]
42    NoReply,
43}
44
45impl PingError {
46    /// Returns a short, simplified string describing the error.
47    /// Each string should only take at most 14 characters, else the row labels for the
48    /// `gateway_ping_results` and `internet_ping_results` time series in internal visualization
49    /// tool would be truncated.
50    pub fn short_name(&self) -> String {
51        let (name, os_err) = match self {
52            Self::CreateSocket(err) => ("CreateSock", err.raw_os_error()),
53            Self::BindSocket { err, .. } => ("BindSock", err.raw_os_error()),
54            Self::SendPing { err, .. } => {
55                let os_err = match err {
56                    ping::PingError::Send(io) => io.raw_os_error(),
57                    ping::PingError::Recv(io) => io.raw_os_error(),
58                    _ => None,
59                };
60                ("SendPing", os_err)
61            }
62            Self::SendPingTimeout { .. } => return "SendTimeout".to_string(),
63            Self::ReceivePing(err) => {
64                let os_err = match err {
65                    ping::PingError::Send(io) => io.raw_os_error(),
66                    ping::PingError::Recv(io) => io.raw_os_error(),
67                    _ => None,
68                };
69                ("RecvPing", os_err)
70            }
71            Self::StreamEndedUnexpectedly => return "StreamEnded".to_string(),
72            Self::UnexpectedSequenceNumber { .. } => return "BadSeqNum".to_string(),
73            Self::NoReply => return "NoReply".to_string(),
74        };
75
76        if let Some(code) = os_err { format!("{name}_{code}") } else { name.to_string() }
77    }
78}
79
80pub(crate) fn ping_result_short_name(result: &Result<(), PingError>) -> String {
81    match result {
82        Ok(()) => "Success".to_string(),
83        Err(e) => format!("e_{}", e.short_name()),
84    }
85}
86
87async fn ping<I>(interface_name: &str, addr: I::SockAddr) -> Result<(), PingError>
88where
89    I: ping::FuchsiaIpExt,
90    I::SockAddr: std::fmt::Display + Copy,
91{
92    let socket = ping::new_icmp_socket::<I>().map_err(PingError::CreateSocket)?;
93    let () = socket
94        .bind_device(Some(interface_name.as_bytes()))
95        .map_err(|err| PingError::BindSocket { interface_name: interface_name.to_string(), err })?;
96    let (mut sink, mut stream) = ping::new_unicast_sink_and_stream::<
97        I,
98        _,
99        { PING_MESSAGE.len() + ping::ICMP_HEADER_LEN },
100    >(&socket, &addr, PING_MESSAGE.as_bytes());
101
102    for seq in SEQ_MIN..=SEQ_MAX {
103        let deadline = fasync::MonotonicInstant::after(TIMEOUT);
104        let () = sink
105            .send(seq)
106            .map_err(|err| PingError::SendPing { seq, err })
107            .on_timeout(deadline, || Err(PingError::SendPingTimeout { seq }))
108            .await?;
109        if match stream.try_next().map(Some).on_timeout(deadline, || None).await {
110            None => Ok(false),
111            Some(Err(err)) => Err(PingError::ReceivePing(err)),
112            Some(Ok(None)) => Err(PingError::StreamEndedUnexpectedly),
113            Some(Ok(Some(got))) if got >= SEQ_MIN && got <= seq => Ok(true),
114            Some(Ok(Some(got))) => {
115                Err(PingError::UnexpectedSequenceNumber { got, min: SEQ_MIN, max: seq })
116            }
117        }? {
118            return Ok(());
119        }
120    }
121    Err(PingError::NoReply)
122}
123
124/// Trait that can send ICMP echo requests, and receive and validate replies.
125#[async_trait]
126pub trait Ping {
127    /// Returns `Ok(())` if the address is reachable, or a `PingError` otherwise.
128    async fn ping(&self, interface_name: &str, addr: SocketAddr) -> Result<(), PingError>;
129}
130
131pub struct Pinger;
132
133#[async_trait]
134impl Ping for Pinger {
135    async fn ping(&self, interface_name: &str, addr: SocketAddr) -> Result<(), PingError> {
136        let r = match addr {
137            SocketAddr::V4(addr_v4) => ping::<Ipv4>(interface_name, addr_v4).await,
138            SocketAddr::V6(addr_v6) => ping::<Ipv6>(interface_name, addr_v6).await,
139        };
140        match r {
141            Ok(()) => Ok(()),
142            Err(e) => {
143                // Check to see if the error is due to the host/network being
144                // unreachable. In that case, this error is likely unconcerning
145                // and signifies a network may not have connectivity across
146                // one of the IP protocols, which can be common for home
147                // network configurations.
148                let mut source_opt: Option<&(dyn std::error::Error + 'static)> = Some(&e);
149                let mut is_unreachable = false;
150                while let Some(source) = source_opt {
151                    if let Some(io_error) = source.downcast_ref::<std::io::Error>() {
152                        if io_error.raw_os_error() == Some(libc::ENETUNREACH)
153                            || io_error.raw_os_error() == Some(libc::EHOSTUNREACH)
154                        {
155                            is_unreachable = true;
156                            break;
157                        }
158                    }
159                    source_opt = source.source();
160                }
161
162                if is_unreachable {
163                    info!("error while pinging {}: {:#}", addr, e);
164                } else {
165                    warn!("error while pinging {}: {:#}", addr, e);
166                }
167                Err(e)
168            }
169        }
170    }
171}