reachability_core/
dig.rs

1// Copyright 2023 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 crate::FIDL_TIMEOUT_ID;
6use anyhow::anyhow;
7use async_trait::async_trait;
8use futures::TryFutureExt;
9use log::warn;
10use named_timer::NamedTimeoutExt;
11use {fidl_fuchsia_net as fnet, fidl_fuchsia_net_name as fnet_name};
12
13const DNS_FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(90);
14
15async fn dig<F: Fn() -> anyhow::Result<fnet_name::LookupProxy>>(
16    name_lookup_connector: &F,
17    // TODO(https://fxbug.dev/42182624): DNS fetching should be done over the
18    // specified interface. The current implementation of
19    // fidl::fuchsia::net::name::LookupIp does not support this.
20    _interface_name: &str,
21    domain: &str,
22) -> anyhow::Result<crate::ResolvedIps> {
23    let name_lookup = name_lookup_connector()?;
24    let results = name_lookup
25        .lookup_ip(
26            domain,
27            &fnet_name::LookupIpOptions {
28                ipv4_lookup: Some(true),
29                ipv6_lookup: Some(true),
30                ..Default::default()
31            },
32        )
33        .map_err(|e: fidl::Error| anyhow!("lookup_ip call failed: {e:?}"))
34        .on_timeout_named(&FIDL_TIMEOUT_ID, DNS_FIDL_TIMEOUT, || {
35            Err(anyhow!("lookup_ip timed out after {} seconds", DNS_FIDL_TIMEOUT.into_seconds()))
36        })
37        .await
38        .map_err(|e: anyhow::Error| anyhow!("{e:?}"))?
39        .map_err(|e: fnet_name::LookupError| anyhow!("lookup failed: {e:?}"))?;
40
41    if let Some(addresses) = results.addresses {
42        let mut resolved = crate::ResolvedIps::default();
43        for address in addresses {
44            match address {
45                fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr }) => resolved.v4.push(addr.into()),
46                fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr }) => resolved.v6.push(addr.into()),
47            }
48        }
49        Ok(resolved)
50    } else {
51        Err(anyhow!("no ip addresses were resolved"))
52    }
53}
54
55#[async_trait]
56pub trait Dig {
57    async fn dig(&self, interface_name: &str, domain: &str) -> Option<crate::ResolvedIps>;
58}
59
60pub struct Digger<F>(F);
61
62impl Digger<()> {
63    pub fn new() -> Digger<impl Fn() -> anyhow::Result<fnet_name::LookupProxy>> {
64        Digger(|| Ok(fuchsia_component::client::connect_to_protocol::<fnet_name::LookupMarker>()?))
65    }
66}
67
68#[async_trait]
69impl<F: Fn() -> anyhow::Result<fnet_name::LookupProxy> + std::marker::Sync> Dig for Digger<F> {
70    async fn dig(&self, interface_name: &str, domain: &str) -> Option<crate::ResolvedIps> {
71        let r = dig(&self.0, interface_name, &domain).await;
72        match r {
73            Ok(ips) => Some(ips),
74            Err(e) => {
75                warn!("error while digging {domain}: {e:?}");
76                None
77            }
78        }
79    }
80}
81
82#[cfg(test)]
83mod test {
84    use super::*;
85    use assert_matches::assert_matches;
86    use fuchsia_async as fasync;
87    use futures::prelude::*;
88    use futures::task::Poll;
89    use net_declare::fidl_ip;
90    use std::pin::pin;
91    use std::sync::{Arc, Mutex};
92
93    const DNS_DOMAIN: &str = "www.gstatic.com";
94
95    #[fuchsia::test]
96    fn test_dns_lookup_valid_response() {
97        let mut exec = fasync::TestExecutor::new();
98        let server_stream = Arc::new(Mutex::new(None));
99        let stream_ref = server_stream.clone();
100
101        let digger = Digger(move || {
102            let (proxy, server) =
103                fidl::endpoints::create_proxy_and_stream::<fnet_name::LookupMarker>();
104            *stream_ref.lock().unwrap() = Some(server);
105            Ok(proxy)
106        });
107        let dns_lookup_fut = digger.dig("", DNS_DOMAIN);
108        let mut dns_lookup_fut = pin!(dns_lookup_fut);
109        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Pending);
110
111        let mut locked = server_stream.lock().unwrap();
112        let mut server_end_fut = locked.as_mut().unwrap().try_next();
113        let _ = assert_matches!(exec.run_until_stalled(&mut server_end_fut),
114            Poll::Ready(Ok(Some(fnet_name::LookupRequest::LookupIp { responder, hostname, .. }))) => {
115                if DNS_DOMAIN == hostname {
116                    responder.send(Ok(&fnet_name::LookupResult {
117                        addresses: Some(vec![fidl_ip!("1.2.3.1")]), ..Default::default()
118                    }))
119                } else {
120                    responder.send(Err(fnet_name::LookupError::NotFound))
121                }
122            }
123        );
124
125        assert_matches!(exec.run_until_stalled(&mut server_end_fut), Poll::Pending);
126        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Ready(Some(_)));
127    }
128
129    #[fuchsia::test]
130    fn test_dns_lookup_net_name_error() {
131        let mut exec = fasync::TestExecutor::new();
132
133        let server_stream = Arc::new(Mutex::new(None));
134        let stream_ref = server_stream.clone();
135
136        let digger = Digger(move || {
137            let (proxy, server) =
138                fidl::endpoints::create_proxy_and_stream::<fnet_name::LookupMarker>();
139            *stream_ref.lock().unwrap() = Some(server);
140            Ok(proxy)
141        });
142        let dns_lookup_fut = digger.dig("", DNS_DOMAIN);
143        let mut dns_lookup_fut = pin!(dns_lookup_fut);
144        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Pending);
145
146        let mut locked = server_stream.lock().unwrap();
147        let mut server_end_fut = locked.as_mut().unwrap().try_next();
148        let _ = assert_matches!(exec.run_until_stalled(&mut server_end_fut),
149            Poll::Ready(Ok(Some(fnet_name::LookupRequest::LookupIp { responder, .. }))) => {
150                // Send a not found error regardless of the hostname
151                responder.send(Err(fnet_name::LookupError::NotFound))
152            }
153        );
154
155        assert_matches!(exec.run_until_stalled(&mut server_end_fut), Poll::Pending);
156        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Ready(None));
157    }
158
159    #[fuchsia::test]
160    fn test_dns_lookup_timed_out() {
161        let mut exec = fasync::TestExecutor::new_with_fake_time();
162        exec.set_fake_time(fasync::MonotonicInstant::from_nanos(0));
163
164        let server_stream = Arc::new(Mutex::new(None));
165        let stream_ref = server_stream.clone();
166
167        let digger = Digger(move || {
168            let (proxy, server) =
169                fidl::endpoints::create_proxy_and_stream::<fnet_name::LookupMarker>();
170            *stream_ref.lock().unwrap() = Some(server);
171            Ok(proxy)
172        });
173        let dns_lookup_fut = digger.dig("", DNS_DOMAIN);
174        let mut dns_lookup_fut = pin!(dns_lookup_fut);
175        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Pending);
176
177        let mut locked = server_stream.lock().unwrap();
178        let mut server_end_fut = locked.as_mut().unwrap().try_next();
179        assert_matches!(exec.run_until_stalled(&mut server_end_fut), Poll::Ready { .. });
180
181        exec.set_fake_time(fasync::MonotonicInstant::after(DNS_FIDL_TIMEOUT));
182        assert!(exec.wake_expired_timers());
183
184        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Ready(None));
185    }
186
187    #[fuchsia::test]
188    fn test_dns_lookup_fidl_error() {
189        let mut exec = fasync::TestExecutor::new_with_fake_time();
190
191        let digger = Digger(move || {
192            let (proxy, _) = fidl::endpoints::create_proxy_and_stream::<fnet_name::LookupMarker>();
193            Ok(proxy)
194        });
195        let dns_lookup_fut = digger.dig("", DNS_DOMAIN);
196        let mut dns_lookup_fut = pin!(dns_lookup_fut);
197
198        assert_matches!(exec.run_until_stalled(&mut dns_lookup_fut), Poll::Ready(None));
199    }
200}