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