Skip to main content

fidl_fuchsia_net_interfaces_ext/
reachability.rs

1// Copyright 2020 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::{
6    Address, EventWithInterest, FieldInterests, PortClass, Properties, PropertiesAndState, Update,
7    UpdateResult, WatcherOperationError,
8};
9
10use fidl_fuchsia_net as fnet;
11use fidl_fuchsia_net_interfaces as fnet_interfaces;
12use futures::{Stream, TryStreamExt};
13use net_types::{LinkLocalAddress as _, ScopeableAddress as _};
14use std::collections::{HashMap, HashSet};
15use thiserror::Error;
16
17/// Returns true iff the supplied [`Properties`] (expected to be fully populated)
18/// appears to provide network connectivity, i.e. is not loopback, is online, and has a default
19/// route and a globally routable address for either IPv4 or IPv6. An IPv4 address is assumed to be
20/// globally routable if it's not link-local. An IPv6 address is assumed to be globally routable if
21/// it has global scope.
22pub fn is_globally_routable<I: FieldInterests>(
23    &Properties {
24        ref port_class,
25        online,
26        ref addresses,
27        has_default_ipv4_route,
28        has_default_ipv6_route,
29        ..
30    }: &Properties<I>,
31) -> bool {
32    match port_class {
33        // TODO(https://fxbug.dev/389732915): In the presence of particular TPROXY/NAT configs
34        // early-returning here might be incorrect, as we could potentially be providing upstream
35        // connectivity over loopback or blackhole interfaces.
36        PortClass::Loopback | PortClass::Blackhole => return false,
37        PortClass::Virtual
38        | PortClass::Ethernet
39        | PortClass::WlanClient
40        | PortClass::WlanAp
41        | PortClass::Ppp
42        | PortClass::Bridge
43        | PortClass::Lowpan => {}
44    }
45    if !online {
46        return false;
47    }
48    if !has_default_ipv4_route && !has_default_ipv6_route {
49        return false;
50    }
51    addresses.iter().any(
52        |Address {
53             addr: fnet::Subnet { addr, prefix_len: _ },
54             valid_until: _,
55             preferred_lifetime_info: _,
56             assignment_state,
57         }| {
58            let assigned = match assignment_state {
59                fnet_interfaces::AddressAssignmentState::Assigned => true,
60                fnet_interfaces::AddressAssignmentState::Tentative
61                | fnet_interfaces::AddressAssignmentState::Unavailable => false,
62            };
63            assigned
64                && match addr {
65                    fnet::IpAddress::Ipv4(fnet::Ipv4Address { addr }) => {
66                        has_default_ipv4_route
67                            && !net_types::ip::Ipv4Addr::new(*addr).is_link_local()
68                    }
69                    fnet::IpAddress::Ipv6(fnet::Ipv6Address { addr }) => {
70                        has_default_ipv6_route
71                            && net_types::ip::Ipv6Addr::from_bytes(*addr).scope()
72                                == net_types::ip::Ipv6Scope::Global
73                    }
74                }
75        },
76    )
77}
78
79/// Wraps `event_stream` and returns a stream which yields the reachability
80/// status as a bool (true iff there exists an interface with properties that
81/// satisfy [`is_globally_routable`]) whenever it changes. The first item the
82/// returned stream yields is the reachability status of the first interface
83/// discovered through an `Added` or `Existing` event on `event_stream`.
84///
85/// Note that `event_stream` must be created from a watcher with interest in the
86/// appropriate fields, such as one created from
87/// [`crate::event_stream_from_state`].
88pub fn to_reachability_stream<I: FieldInterests>(
89    event_stream: impl Stream<Item = Result<EventWithInterest<I>, fidl::Error>>,
90) -> impl Stream<Item = Result<bool, WatcherOperationError<(), HashMap<u64, PropertiesAndState<(), I>>>>>
91{
92    let mut if_map = HashMap::<u64, _>::new();
93    let mut reachable = None;
94    let mut reachable_ids = HashSet::new();
95    event_stream.map_err(WatcherOperationError::EventStream).try_filter_map(move |event| {
96        futures::future::ready(if_map.update(event).map_err(WatcherOperationError::Update).map(
97            |changed: UpdateResult<'_, (), _>| {
98                let reachable_ids_changed = match changed {
99                    UpdateResult::Existing { properties, state: _ }
100                    | UpdateResult::Added { properties, state: _ }
101                    | UpdateResult::Changed { previous: _, current: properties, state: _ }
102                        if is_globally_routable(properties) =>
103                    {
104                        reachable_ids.insert(properties.id)
105                    }
106                    UpdateResult::Existing { .. } | UpdateResult::Added { .. } => false,
107                    UpdateResult::Changed { previous: _, current: properties, state: _ } => {
108                        reachable_ids.remove(&properties.id)
109                    }
110                    UpdateResult::Removed(PropertiesAndState { properties, state: _ }) => {
111                        reachable_ids.remove(&properties.id)
112                    }
113                    UpdateResult::NoChange => return None,
114                };
115                // If the stream hasn't yielded anything yet, do so even if the set of reachable
116                // interfaces hasn't changed.
117                if reachable.is_none() {
118                    reachable = Some(!reachable_ids.is_empty());
119                    return reachable;
120                } else if reachable_ids_changed {
121                    let new_reachable = Some(!reachable_ids.is_empty());
122                    if reachable != new_reachable {
123                        reachable = new_reachable;
124                        return reachable;
125                    }
126                }
127                None
128            },
129        ))
130    })
131}
132
133/// Reachability status stream operational errors.
134#[derive(Error, Debug)]
135pub enum OperationError<S: std::fmt::Debug, B: Update<S> + std::fmt::Debug> {
136    #[error("watcher operation error: {0}")]
137    Watcher(WatcherOperationError<S, B>),
138    #[error("reachability status stream ended unexpectedly")]
139    UnexpectedEnd,
140}
141
142/// Returns a future which resolves when any network interface observed through `event_stream`
143/// has properties which satisfy [`is_globally_routable`].
144pub async fn wait_for_reachability<I: FieldInterests>(
145    event_stream: impl Stream<Item = Result<EventWithInterest<I>, fidl::Error>>,
146) -> Result<(), OperationError<(), HashMap<u64, PropertiesAndState<(), I>>>> {
147    futures::pin_mut!(event_stream);
148    let rtn = to_reachability_stream(event_stream)
149        .map_err(OperationError::Watcher)
150        .try_filter_map(|reachable| futures::future::ok(if reachable { Some(()) } else { None }))
151        .try_next()
152        .await
153        .and_then(|item| item.ok_or_else(|| OperationError::UnexpectedEnd));
154    rtn
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    use crate::{AllInterest, PositiveMonotonicInstant, PreferredLifetimeInfo};
162
163    use anyhow::Context as _;
164    use fidl_fuchsia_hardware_network as fnetwork;
165    use futures::FutureExt as _;
166    use net_declare::fidl_subnet;
167    use std::convert::TryInto as _;
168    use zx_types as zx;
169
170    const IPV4_LINK_LOCAL: fnet::Subnet = fidl_subnet!("169.254.0.1/16");
171    const IPV6_LINK_LOCAL: fnet::Subnet = fidl_subnet!("fe80::1/64");
172    const IPV4_GLOBAL: fnet::Subnet = fidl_subnet!("192.168.0.1/16");
173    const IPV6_GLOBAL: fnet::Subnet = fidl_subnet!("100::1/64");
174
175    fn valid_interface(id: u64) -> fnet_interfaces::Properties {
176        fnet_interfaces::Properties {
177            id: Some(id),
178            name: Some("test1".to_string()),
179            port_class: Some(fnet_interfaces::PortClass::Device(fnetwork::PortClass::Ethernet)),
180            online: Some(true),
181            addresses: Some(vec![
182                fnet_interfaces::Address {
183                    addr: Some(IPV4_GLOBAL),
184                    valid_until: Some(zx::ZX_TIME_INFINITE),
185                    assignment_state: Some(fnet_interfaces::AddressAssignmentState::Assigned),
186                    preferred_lifetime_info: Some(
187                        PreferredLifetimeInfo::preferred_forever().into(),
188                    ),
189                    __source_breaking: Default::default(),
190                },
191                fnet_interfaces::Address {
192                    addr: Some(IPV4_LINK_LOCAL),
193                    valid_until: Some(zx::ZX_TIME_INFINITE),
194                    assignment_state: Some(fnet_interfaces::AddressAssignmentState::Assigned),
195                    preferred_lifetime_info: Some(
196                        PreferredLifetimeInfo::preferred_forever().into(),
197                    ),
198                    __source_breaking: Default::default(),
199                },
200                fnet_interfaces::Address {
201                    addr: Some(IPV6_GLOBAL),
202                    valid_until: Some(zx::ZX_TIME_INFINITE),
203                    assignment_state: Some(fnet_interfaces::AddressAssignmentState::Assigned),
204                    preferred_lifetime_info: Some(
205                        PreferredLifetimeInfo::preferred_forever().into(),
206                    ),
207                    __source_breaking: Default::default(),
208                },
209                fnet_interfaces::Address {
210                    addr: Some(IPV6_LINK_LOCAL),
211                    valid_until: Some(zx::ZX_TIME_INFINITE),
212                    assignment_state: Some(fnet_interfaces::AddressAssignmentState::Assigned),
213                    preferred_lifetime_info: Some(
214                        PreferredLifetimeInfo::preferred_forever().into(),
215                    ),
216                    __source_breaking: Default::default(),
217                },
218            ]),
219            has_default_ipv4_route: Some(true),
220            has_default_ipv6_route: Some(true),
221            ..Default::default()
222        }
223    }
224
225    #[test]
226    fn test_is_globally_routable() -> Result<(), anyhow::Error> {
227        const ID: u64 = 1;
228        const ASSIGNED_ADDR: Address<AllInterest> = Address {
229            addr: IPV4_GLOBAL,
230            valid_until: PositiveMonotonicInstant::INFINITE_FUTURE,
231            preferred_lifetime_info: PreferredLifetimeInfo::preferred_forever(),
232            assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
233        };
234        // These combinations are not globally routable.
235        assert!(!is_globally_routable(&Properties::<AllInterest> {
236            port_class: PortClass::Loopback,
237            ..valid_interface(ID).try_into()?
238        }));
239        assert!(!is_globally_routable(&Properties::<AllInterest> {
240            online: false,
241            ..valid_interface(ID).try_into()?
242        }));
243        assert!(!is_globally_routable(&Properties::<AllInterest> {
244            addresses: vec![],
245            ..valid_interface(ID).try_into()?
246        }));
247        assert!(!is_globally_routable(&Properties::<AllInterest> {
248            has_default_ipv4_route: false,
249            has_default_ipv6_route: false,
250            ..valid_interface(ID).try_into()?
251        }));
252        assert!(!is_globally_routable(&Properties::<AllInterest> {
253            addresses: vec![Address { addr: IPV4_GLOBAL, ..ASSIGNED_ADDR }],
254            has_default_ipv4_route: false,
255            ..valid_interface(ID).try_into()?
256        }));
257        assert!(!is_globally_routable(&Properties::<AllInterest> {
258            addresses: vec![Address { addr: IPV6_GLOBAL, ..ASSIGNED_ADDR }],
259            has_default_ipv6_route: false,
260            ..valid_interface(ID).try_into()?
261        }));
262        assert!(!is_globally_routable(&Properties::<AllInterest> {
263            addresses: vec![Address { addr: IPV6_LINK_LOCAL, ..ASSIGNED_ADDR }],
264            has_default_ipv6_route: true,
265            ..valid_interface(ID).try_into()?
266        }));
267        assert!(!is_globally_routable(&Properties::<AllInterest> {
268            addresses: vec![Address { addr: IPV4_LINK_LOCAL, ..ASSIGNED_ADDR }],
269            has_default_ipv4_route: true,
270            ..valid_interface(ID).try_into()?
271        }));
272
273        // These combinations are globally routable.
274        assert!(is_globally_routable::<AllInterest>(&valid_interface(ID).try_into()?));
275        assert!(is_globally_routable::<AllInterest>(&Properties {
276            addresses: vec![Address { addr: IPV4_GLOBAL, ..ASSIGNED_ADDR }],
277            has_default_ipv4_route: true,
278            has_default_ipv6_route: false,
279            ..valid_interface(ID).try_into()?
280        }));
281        assert!(is_globally_routable::<AllInterest>(&Properties {
282            addresses: vec![Address { addr: IPV6_GLOBAL, ..ASSIGNED_ADDR }],
283            has_default_ipv4_route: false,
284            has_default_ipv6_route: true,
285            ..valid_interface(ID).try_into()?
286        }));
287        Ok(())
288    }
289
290    #[test]
291    fn test_to_reachability_stream() -> Result<(), anyhow::Error> {
292        let (sender, receiver) = futures::channel::mpsc::unbounded();
293        let mut reachability_stream = to_reachability_stream::<AllInterest>(receiver);
294        for (event, want) in vec![
295            (fnet_interfaces::Event::Idle(fnet_interfaces::Empty {}), None),
296            // Added events
297            (
298                fnet_interfaces::Event::Added(fnet_interfaces::Properties {
299                    online: Some(false),
300                    ..valid_interface(1)
301                }),
302                Some(false),
303            ),
304            (fnet_interfaces::Event::Added(valid_interface(2)), Some(true)),
305            (
306                fnet_interfaces::Event::Added(fnet_interfaces::Properties {
307                    online: Some(false),
308                    ..valid_interface(3)
309                }),
310                None,
311            ),
312            // Changed events
313            (
314                fnet_interfaces::Event::Changed(fnet_interfaces::Properties {
315                    id: Some(2),
316                    online: Some(false),
317                    ..Default::default()
318                }),
319                Some(false),
320            ),
321            (
322                fnet_interfaces::Event::Changed(fnet_interfaces::Properties {
323                    id: Some(1),
324                    online: Some(true),
325                    ..Default::default()
326                }),
327                Some(true),
328            ),
329            (
330                fnet_interfaces::Event::Changed(fnet_interfaces::Properties {
331                    id: Some(3),
332                    online: Some(true),
333                    ..Default::default()
334                }),
335                None,
336            ),
337            // Removed events
338            (fnet_interfaces::Event::Removed(1), None),
339            (fnet_interfaces::Event::Removed(3), Some(false)),
340            (fnet_interfaces::Event::Removed(2), None),
341        ] {
342            let () =
343                sender.unbounded_send(Ok(event.clone().into())).context("failed to send event")?;
344            let got = reachability_stream.try_next().now_or_never();
345            if let Some(want_reachable) = want {
346                let r = got.ok_or_else(|| {
347                    anyhow::anyhow!("reachability status stream unexpectedly yielded nothing")
348                })?;
349                let item = r.context("reachability status stream error")?;
350                let got_reachable = item.ok_or_else(|| {
351                    anyhow::anyhow!("reachability status stream ended unexpectedly")
352                })?;
353                assert_eq!(got_reachable, want_reachable);
354            } else {
355                if got.is_some() {
356                    panic!(
357                        "got {:?} from reachability stream after event {:?}, \
358                        want None as reachability status should not have changed",
359                        got, event
360                    );
361                }
362            }
363        }
364        Ok(())
365    }
366}