Skip to main content

reachability_core/
lib.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
5#![deny(clippy::unused_async)]
6
7pub mod dig;
8pub mod fetch;
9mod inspect;
10mod neighbor_cache;
11pub mod ping;
12pub mod route_table;
13pub mod telemetry;
14pub mod watchdog;
15
16#[cfg(test)]
17mod testutil;
18
19use crate::route_table::{Route, RouteTable};
20use crate::telemetry::processors::link_properties_state::LinkProperties;
21use crate::telemetry::{TelemetryEvent, TelemetrySender};
22use anyhow::anyhow;
23use fidl_fuchsia_net as fnet;
24use fidl_fuchsia_net_ext::{self as fnet_ext, IpExt};
25use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
26use fuchsia_async as fasync;
27use fuchsia_inspect::{Inspector, Node as InspectNode};
28use futures::channel::mpsc;
29use inspect::InspectInfo;
30use log::{debug, error, info};
31use named_timer::DeadlineId;
32use net_declare::{fidl_subnet, std_ip};
33use net_types::ScopeableAddress as _;
34use num_derive::FromPrimitive;
35use std::collections::HashSet;
36use std::collections::hash_map::{Entry, HashMap};
37
38use std::net::IpAddr;
39
40pub use neighbor_cache::{InterfaceNeighborCache, NeighborCache};
41
42const IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS: std::net::IpAddr = std_ip!("8.8.8.8");
43const IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS: std::net::IpAddr = std_ip!("2001:4860:4860::8888");
44const UNSPECIFIED_V4: fidl_fuchsia_net::Subnet = fidl_subnet!("0.0.0.0/0");
45const UNSPECIFIED_V6: fidl_fuchsia_net::Subnet = fidl_subnet!("::0/0");
46const GSTATIC: &'static str = "www.gstatic.com";
47const GENERATE_204: &'static str = "/generate_204";
48// Gstatic has a TTL of 300 seconds, therefore, we will perform a lookup every
49// 300 seconds since we won't get any better indication of DNS function.
50// TODO(https://fxbug.dev/42072067): Dynamically query TTL based on the domain's DNS record
51const DNS_PROBE_PERIOD: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(300);
52
53// Timeout ID for the fake clock component that restrains the integration tests from reaching the
54// FIDL timeout and subsequently failing. Shared by the eventloop and integration library.
55pub const FIDL_TIMEOUT_ID: DeadlineId<'static> =
56    DeadlineId::new("reachability", "fidl-request-timeout");
57
58/// `Stats` keeps the monitoring service statistic counters.
59#[derive(Debug, Default, Clone)]
60pub struct Stats {
61    /// `events` is the number of events received.
62    pub events: u64,
63    /// `state_updates` is the number of times reachability state has changed.
64    pub state_updates: HashMap<Id, u64>,
65}
66
67// TODO(dpradilla): consider splitting the state in l2 state and l3 state, as there can be multiple
68/// `LinkState` represents the layer 2 and layer 3 state
69#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy, FromPrimitive)]
70#[repr(u8)]
71pub enum LinkState {
72    /// State not yet determined.
73    #[default]
74    None = 1,
75    /// Interface no longer present.
76    Removed = 5,
77    /// Interface is down.
78    Down = 10,
79    /// Interface is up, no packets seen yet.
80    Up = 15,
81    /// L3 Interface is up, local neighbors seen.
82    Local = 20,
83    /// L3 Interface is up, local gateway configured and reachable.
84    Gateway = 25,
85    /// Expected response seen from reachability test URL.
86    Internet = 30,
87}
88
89impl LinkState {
90    fn log_state_vals_inspect(node: &InspectNode, name: &str) {
91        let child = node.create_child(name);
92        for i in LinkState::None as u32..=LinkState::Internet as u32 {
93            match <LinkState as num_traits::FromPrimitive>::from_u32(i) {
94                Some(state) => child.record_string(i.to_string(), format!("{:?}", state)),
95                None => (),
96            }
97        }
98        node.record(child);
99    }
100}
101
102/// `ApplicationState` represents the layer 7 state
103#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)]
104pub struct ApplicationState {
105    pub dns_resolved: bool,
106    pub http_fetch_succeeded: bool,
107}
108
109/// `State` represents the reachability state.
110#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Clone, Copy)]
111pub struct State {
112    pub link: LinkState,
113    pub application: ApplicationState,
114}
115
116impl From<LinkState> for State {
117    fn from(link: LinkState) -> Self {
118        State { link, ..Default::default() }
119    }
120}
121
122impl LinkState {
123    fn has_interface_up(&self) -> bool {
124        match self {
125            LinkState::None | LinkState::Removed | LinkState::Down => false,
126            LinkState::Up | LinkState::Local | LinkState::Gateway | LinkState::Internet => true,
127        }
128    }
129
130    fn has_internet(&self) -> bool {
131        match self {
132            LinkState::None
133            | LinkState::Removed
134            | LinkState::Down
135            | LinkState::Up
136            | LinkState::Local
137            | LinkState::Gateway => false,
138            LinkState::Internet => true,
139        }
140    }
141
142    fn has_gateway(&self) -> bool {
143        match self {
144            LinkState::None
145            | LinkState::Removed
146            | LinkState::Down
147            | LinkState::Up
148            | LinkState::Local => false,
149            LinkState::Gateway | LinkState::Internet => true,
150        }
151    }
152}
153
154impl State {
155    fn set_link_state(&mut self, link: LinkState) {
156        *self = State { link, ..Default::default() };
157    }
158
159    fn has_interface_up(&self) -> bool {
160        self.link.has_interface_up()
161    }
162
163    fn has_internet(&self) -> bool {
164        self.link.has_internet()
165    }
166
167    fn has_gateway(&self) -> bool {
168        self.link.has_gateway()
169    }
170
171    fn has_dns(&self) -> bool {
172        self.application.dns_resolved
173    }
174
175    fn has_http(&self) -> bool {
176        self.application.http_fetch_succeeded
177    }
178}
179
180impl std::fmt::Display for LinkState {
181    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
182        f.write_str(match self {
183            LinkState::None => "None",
184            LinkState::Removed => "Removed",
185            LinkState::Down => "Down",
186            LinkState::Up => "Up",
187            LinkState::Local => "Local",
188            LinkState::Gateway => "Gateway",
189            LinkState::Internet => "Internet",
190        })
191    }
192}
193
194impl std::fmt::Display for State {
195    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
196        write!(
197            f,
198            "{} (dns: {}, http: {})",
199            self.link, self.application.dns_resolved, self.application.http_fetch_succeeded
200        )
201    }
202}
203
204impl std::str::FromStr for LinkState {
205    type Err = ();
206
207    fn from_str(s: &str) -> Result<Self, Self::Err> {
208        match s {
209            "None" => Ok(Self::None),
210            "Removed" => Ok(Self::Removed),
211            "Down" => Ok(Self::Down),
212            "Up" => Ok(Self::Up),
213            "Local" => Ok(Self::Local),
214            "Gateway" => Ok(Self::Gateway),
215            "Internet" => Ok(Self::Internet),
216            _ => Err(()),
217        }
218    }
219}
220
221#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
222pub enum Proto {
223    IPv4,
224    IPv6,
225}
226impl std::fmt::Display for Proto {
227    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228        f.write_str(match self {
229            Proto::IPv4 => "IPv4",
230            Proto::IPv6 => "IPv6",
231        })
232    }
233}
234
235/// A trait for types containing reachability state that should be compared without the timestamp.
236trait StateEq {
237    /// Returns true iff `self` and `other` have equivalent reachability state.
238    fn compare_state(&self, other: &Self) -> bool;
239}
240
241/// `StateEvent` records a state and the time it was reached.
242// NB PartialEq is derived only for tests to avoid unintentionally making a comparison that
243// includes the timestamp.
244#[derive(Debug, Clone, Copy)]
245#[cfg_attr(test, derive(PartialEq))]
246struct StateEvent {
247    /// `state` is the current reachability state.
248    state: State,
249    /// The time of this event.
250    time: fasync::MonotonicInstant,
251}
252
253impl StateEvent {
254    /// Overwrite `self` with `other` if the state is different, returning the previous and current
255    /// values (which may be equal).
256    fn update(&mut self, other: Self) -> Delta<Self> {
257        let previous = Some(*self);
258        if self.state != other.state {
259            *self = other;
260        }
261        Delta { previous, current: *self }
262    }
263}
264
265impl StateEq for StateEvent {
266    fn compare_state(&self, &Self { state, time: _ }: &Self) -> bool {
267        self.state == state
268    }
269}
270
271impl std::fmt::Display for StateEvent {
272    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
273        std::fmt::Display::fmt(&self.state, f)
274    }
275}
276
277#[derive(Clone, Debug, PartialEq)]
278struct Delta<T> {
279    current: T,
280    previous: Option<T>,
281}
282
283impl<T: StateEq> Delta<T> {
284    fn change_observed(&self) -> bool {
285        match &self.previous {
286            Some(previous) => !previous.compare_state(&self.current),
287            None => true,
288        }
289    }
290}
291
292// NB PartialEq is derived only for tests to avoid unintentionally making a comparison that
293// includes the timestamp in `StateEvent`.
294#[derive(Debug)]
295#[cfg_attr(test, derive(PartialEq))]
296struct StateDelta {
297    port: IpVersions<Delta<StateEvent>>,
298    system: IpVersions<Delta<SystemState>>,
299}
300
301#[derive(Clone, Default, Debug, PartialEq)]
302pub struct IpVersions<T> {
303    ipv4: T,
304    ipv6: T,
305}
306
307impl<T> IpVersions<T> {
308    fn with_version<F: FnMut(Proto, &T)>(&self, mut f: F) {
309        let () = f(Proto::IPv4, &self.ipv4);
310        let () = f(Proto::IPv6, &self.ipv6);
311    }
312}
313
314impl IpVersions<Option<SystemState>> {
315    fn state(&self) -> IpVersions<Option<State>> {
316        IpVersions {
317            ipv4: self.ipv4.map(|s| s.state.state),
318            ipv6: self.ipv6.map(|s| s.state.state),
319        }
320    }
321}
322
323impl IpVersions<Option<State>> {
324    fn has_interface_up(&self) -> bool {
325        self.satisfies(State::has_interface_up)
326    }
327
328    fn has_internet(&self) -> bool {
329        self.satisfies(State::has_internet)
330    }
331
332    fn has_dns(&self) -> bool {
333        self.satisfies(State::has_dns)
334    }
335
336    fn has_http(&self) -> bool {
337        self.satisfies(State::has_http)
338    }
339
340    fn has_gateway(&self) -> bool {
341        self.satisfies(State::has_gateway)
342    }
343
344    fn satisfies<F>(&self, f: F) -> bool
345    where
346        F: Fn(&State) -> bool,
347    {
348        return [self.ipv4, self.ipv6].iter().filter_map(|state| state.as_ref()).any(f);
349    }
350}
351
352type Id = u64;
353
354// NB PartialEq is derived only for tests to avoid unintentionally making a comparison that
355// includes the timestamp in `StateEvent`.
356#[derive(Copy, Clone, Debug)]
357#[cfg_attr(test, derive(PartialEq))]
358struct SystemState {
359    id: Id,
360    state: StateEvent,
361}
362
363impl SystemState {
364    fn max(self, other: Self) -> Self {
365        if other.state.state > self.state.state { other } else { self }
366    }
367}
368
369impl StateEq for SystemState {
370    fn compare_state(&self, &Self { id, state: StateEvent { state, time: _ } }: &Self) -> bool {
371        self.id == id && self.state.state == state
372    }
373}
374
375impl std::fmt::Display for SystemState {
376    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
377        write!(f, "{} via Interface [{}]", self.state, self.id)
378    }
379}
380
381/// `StateInfo` keeps the reachability state.
382// NB PartialEq is derived only for tests to avoid unintentionally making a comparison that
383// includes the timestamp in `StateEvent`.
384#[derive(Debug, Default, Clone)]
385#[cfg_attr(test, derive(PartialEq))]
386pub struct StateInfo {
387    /// Mapping from interface ID to reachability information.
388    per_interface: HashMap<Id, IpVersions<StateEvent>>,
389    /// Interface IDs with the best reachability state per IP version.
390    system: IpVersions<Option<Id>>,
391}
392
393impl StateInfo {
394    /// Get the reachability info associated with an interface.
395    fn get(&self, id: Id) -> Option<&IpVersions<StateEvent>> {
396        self.per_interface.get(&id)
397    }
398
399    /// Get the system-wide IPv4 reachability info.
400    fn get_system_ipv4(&self) -> Option<SystemState> {
401        self.system.ipv4.map(|id| SystemState {
402            id,
403            state: self
404                .get(id)
405                .unwrap_or_else(|| {
406                    panic!("inconsistent system IPv4 state: no interface with ID {:?}", id)
407                })
408                .ipv4,
409        })
410    }
411
412    /// Get the system-wide IPv6 reachability info.
413    fn get_system_ipv6(&self) -> Option<SystemState> {
414        self.system.ipv6.map(|id| SystemState {
415            id,
416            state: self
417                .get(id)
418                .unwrap_or_else(|| {
419                    panic!("inconsistent system IPv6 state: no interface with ID {:?}", id)
420                })
421                .ipv6,
422        })
423    }
424
425    fn get_system(&self) -> IpVersions<Option<SystemState>> {
426        IpVersions { ipv4: self.get_system_ipv4(), ipv6: self.get_system_ipv6() }
427    }
428
429    pub fn system_has_internet(&self) -> bool {
430        self.get_system().state().has_internet()
431    }
432
433    pub fn system_has_gateway(&self) -> bool {
434        self.get_system().state().has_gateway()
435    }
436
437    pub fn system_has_dns(&self) -> bool {
438        self.get_system().state().has_dns()
439    }
440
441    pub fn system_has_http(&self) -> bool {
442        self.get_system().state().has_http()
443    }
444
445    /// Report the duration of the current state for each interface and each protocol.
446    fn report(&self) {
447        let time = fasync::MonotonicInstant::now();
448        debug!("system reachability state IPv4 {:?}", self.get_system_ipv4());
449        debug!("system reachability state IPv6 {:?}", self.get_system_ipv6());
450        for (id, IpVersions { ipv4, ipv6 }) in self.per_interface.iter() {
451            debug!(
452                "reachability state {:?} IPv4 {:?} with duration {:?}",
453                id,
454                ipv4,
455                time - ipv4.time
456            );
457            debug!(
458                "reachability state {:?} IPv6 {:?} with duration {:?}",
459                id,
460                ipv6,
461                time - ipv6.time
462            );
463        }
464    }
465
466    /// Update interface `id` with its new reachability info.
467    ///
468    /// Returns the protocols and their new reachability states iff a change was observed.
469    fn update(&mut self, id: Id, new_reachability: IpVersions<StateEvent>) -> StateDelta {
470        let previous_system_ipv4 = self.get_system_ipv4();
471        let previous_system_ipv6 = self.get_system_ipv6();
472        let port = match self.per_interface.entry(id) {
473            Entry::Occupied(mut occupied) => {
474                let IpVersions { ipv4, ipv6 } = occupied.get_mut();
475                let IpVersions { ipv4: new_ipv4, ipv6: new_ipv6 } = new_reachability;
476
477                IpVersions { ipv4: ipv4.update(new_ipv4), ipv6: ipv6.update(new_ipv6) }
478            }
479            Entry::Vacant(vacant) => {
480                let IpVersions { ipv4, ipv6 } = vacant.insert(new_reachability);
481                IpVersions {
482                    ipv4: Delta { previous: None, current: *ipv4 },
483                    ipv6: Delta { previous: None, current: *ipv6 },
484                }
485            }
486        };
487
488        let IpVersions { ipv4: system_ipv4, ipv6: system_ipv6 } = self.per_interface.iter().fold(
489            {
490                let IpVersions {
491                    ipv4: Delta { previous: _, current: curr_ipv4 },
492                    ipv6: Delta { previous: _, current: curr_ipv6 },
493                } = port;
494                // Prioritize the `previous` system state as the initial `SystemState` when it is
495                // present and holds state for a different interface than the one we're updating.
496                // This prevents the `SystemState` from flipping between interfaces when multiple
497                // interfaces have the same state.
498                let ipv4 = previous_system_ipv4
499                    .map(|prev| {
500                        if prev.id != id {
501                            SystemState { id: prev.id, state: prev.state }
502                        } else {
503                            SystemState { id, state: curr_ipv4 }
504                        }
505                    })
506                    .unwrap_or(SystemState { id, state: curr_ipv4 });
507                let ipv6 = previous_system_ipv6
508                    .map(|prev| {
509                        if prev.id != id {
510                            SystemState { id: prev.id, state: prev.state }
511                        } else {
512                            SystemState { id, state: curr_ipv6 }
513                        }
514                    })
515                    .unwrap_or(SystemState { id, state: curr_ipv6 });
516                IpVersions { ipv4, ipv6 }
517            },
518            |IpVersions { ipv4: system_ipv4, ipv6: system_ipv6 },
519             (&id, &IpVersions { ipv4, ipv6 })| {
520                IpVersions {
521                    ipv4: system_ipv4.max(SystemState { id, state: ipv4 }),
522                    ipv6: system_ipv6.max(SystemState { id, state: ipv6 }),
523                }
524            },
525        );
526
527        self.system = IpVersions { ipv4: Some(system_ipv4.id), ipv6: Some(system_ipv6.id) };
528
529        StateDelta {
530            port,
531            system: IpVersions {
532                ipv4: Delta { previous: previous_system_ipv4, current: system_ipv4 },
533                ipv6: Delta { previous: previous_system_ipv6, current: system_ipv6 },
534            },
535        }
536    }
537}
538
539/// Provides a view into state for a specific system interface.
540#[derive(Copy, Clone, Debug)]
541pub struct InterfaceView<'a> {
542    pub properties: &'a fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
543    pub routes: &'a RouteTable,
544    pub neighbors: Option<&'a InterfaceNeighborCache>,
545}
546
547/// `NetworkCheckerOutcome` contains values indicating whether a network check completed or needs
548/// resumption.
549#[derive(Debug)]
550pub enum NetworkCheckerOutcome {
551    /// The network check must be resumed via a call to `resume` to complete.
552    MustResume,
553    /// The network check is finished and the reachability state for the specified interface has
554    /// been updated. A new network check can begin on the same interface via `begin`.
555    Complete,
556}
557
558/// A Network Checker is a re-entrant, asynchronous state machine that monitors availability of
559/// networks over a given network interface.
560pub trait NetworkChecker {
561    /// `begin` starts a re-entrant, asynchronous network check on the supplied interface. It
562    /// returns whether the network check was completed, must be resumed, or if the supplied
563    /// interface already had an ongoing network check.
564    fn begin(&mut self, view: InterfaceView<'_>) -> Result<NetworkCheckerOutcome, anyhow::Error>;
565
566    /// `resume` continues a network check that was not yet completed.
567    fn resume(
568        &mut self,
569        cookie: NetworkCheckCookie,
570        result: NetworkCheckResult,
571    ) -> Result<NetworkCheckerOutcome, anyhow::Error>;
572}
573
574// States involved in `Monitor`'s implementation of NetworkChecker.
575#[derive(Debug, Default)]
576enum NetworkCheckState {
577    // `Begin` starts a new network check. This state analyzes link properties. It can transition
578    // to `PingGateway` when a default gateway is configured on the interface, to `PingInternet`
579    // when off-link routes are configured but no default gateway, and `Idle` if analyzing link
580    // properties allows determining that connectivity past the local network is not possible.
581    #[default]
582    Begin,
583    // `PingGateway` sends a ping to each of the available gateways with a default route. It can
584    // transition to `PingInternet` when a healthy gateway is detected through neighbor discovery,
585    // or when at least one gateway ping successfully returns, and `Idle` if no healthy gateway is
586    // detected and no gateway pings successfully return.
587    PingGateway,
588    // `PingInternet` sends a ping to an IPv4 and IPv6 external address. It can only transition to
589    // `ResolveDns` after it has completed internet pings.
590    PingInternet,
591    // `ResolveDns` makes a DNS request for the provided domain and then transitions to `FetchHttp`
592    // after it has completed. If DNS_PROBE_PERIOD has not passed, the results will still be
593    // cached, and this will transition immediately to `FetchHttp`.
594    ResolveDns,
595    // `FetchHttp` fetches a URL over http. It can only transition to `Idle` after it has
596    // completed all of the http requests.
597    FetchHttp,
598    // `Idle` terminates a network check. The system is ready to begin processing another network
599    // check for interface associated with this check.
600    Idle,
601}
602impl std::fmt::Display for NetworkCheckState {
603    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
604        match self {
605            NetworkCheckState::Begin => write!(f, "Begin"),
606            NetworkCheckState::PingGateway => write!(f, "Ping Gateway"),
607            NetworkCheckState::PingInternet => write!(f, "Ping Internet"),
608            NetworkCheckState::ResolveDns => write!(f, "Resolve DNS"),
609            NetworkCheckState::FetchHttp => write!(f, "Fetch URL"),
610            NetworkCheckState::Idle => write!(f, "Idle"),
611        }
612    }
613}
614
615#[derive(Debug, Clone, Default)]
616pub struct ResolvedIps {
617    v4: Vec<std::net::Ipv4Addr>,
618    v6: Vec<std::net::Ipv6Addr>,
619}
620
621struct PersistentNetworkCheckContext {
622    // Map of resolved IP addresses indexed by domain name.
623    resolved_addrs: HashMap<String, ResolvedIps>,
624    // Dns Resolve Time
625    resolved_time: zx::MonotonicInstant,
626    // Context about the interface, that enables telemetry.
627    telemetry: TelemetryContext,
628}
629
630impl Default for PersistentNetworkCheckContext {
631    fn default() -> Self {
632        Self {
633            resolved_addrs: Default::default(),
634            resolved_time: zx::MonotonicInstant::INFINITE_PAST,
635            telemetry: Default::default(),
636        }
637    }
638}
639
640impl From<TelemetryContext> for PersistentNetworkCheckContext {
641    fn from(value: TelemetryContext) -> Self {
642        Self {
643            resolved_addrs: Default::default(),
644            resolved_time: zx::MonotonicInstant::INFINITE_PAST,
645            telemetry: value,
646        }
647    }
648}
649
650// Information about the interface that is important for telemetry,
651// and is not tied to a specific instance of a network check.
652#[derive(Clone, Default)]
653struct TelemetryContext {
654    // The interface identifiers derived from the interface's PortClass. Used
655    // to determine which TimeSeries are applicable to the current interface.
656    interface_identifiers: Vec<telemetry::processors::InterfaceIdentifier>,
657    has_v4_address: bool,
658    has_default_ipv4_route: bool,
659    has_v6_address: bool,
660    has_default_ipv6_route: bool,
661}
662
663impl TelemetryContext {
664    fn new(
665        port_class: fnet_interfaces_ext::PortClass,
666        addresses: &Vec<fnet_interfaces_ext::Address<fnet_interfaces_ext::DefaultInterest>>,
667        has_default_ipv4_route: bool,
668        has_default_ipv6_route: bool,
669    ) -> Self {
670        let interface_identifiers = telemetry::processors::identifiers_from_port_class(port_class);
671        // Whether the interface has a globally routable v4 / v6 address.
672        // v6 address must not be link local.
673        let (has_v4_address, has_v6_address) = {
674            addresses.iter().fold((false, false), |(mut has_v4, mut has_v6), addr| {
675                match addr.addr.addr {
676                    fnet::IpAddress::Ipv4(_) => {
677                        has_v4 = true;
678                    }
679                    fnet::IpAddress::Ipv6(v6) => {
680                        has_v6 = has_v6 || !v6.is_unicast_link_local();
681                    }
682                };
683                (has_v4, has_v6)
684            })
685        };
686        Self {
687            interface_identifiers,
688            has_v4_address,
689            has_default_ipv4_route,
690            has_v6_address,
691            has_default_ipv6_route,
692        }
693    }
694}
695
696// Contains all information related to a network check on an interface.
697struct NetworkCheckContext {
698    // The current status of the state machine.
699    checker_state: NetworkCheckState,
700    // The list of addresses to ping (either gateway or internet).
701    ping_addrs: Vec<std::net::SocketAddr>,
702    // The quantity of pings sent.
703    pings_expected: usize,
704    // The quantity of pings that have been received.
705    pings_completed: usize,
706    // The quantity of fetches that have been completed.
707    fetches_expected: usize,
708    // The quantity of fetches that have been completed.
709    fetches_completed: usize,
710    // The current calculated state.
711    discovered_state: IpVersions<State>,
712    // Whether the network check should ping internet regardless of if the gateway pings fail.
713    always_ping_internet: bool,
714    // Whether an online router was discoverable via neighbor discovery.
715    router_discoverable: IpVersions<bool>,
716    // Whether the gateway successfully responded to pings.
717    gateway_pingable: IpVersions<bool>,
718    // Context that persists between check cycles
719    persistent_context: PersistentNetworkCheckContext,
720    // TODO(https://fxbug.dev/42074525): Add tombstone marker to inform NetworkCheck that the interface has
721    // been removed and we no longer need to run checks on this interface. This can occur when
722    // receiving an interface removed event, but a network check for that interface is still in
723    // progress.
724}
725
726impl NetworkCheckContext {
727    fn set_global_link_state(&mut self, link: LinkState) {
728        self.discovered_state.ipv4.set_link_state(link);
729        self.discovered_state.ipv6.set_link_state(link);
730    }
731
732    fn initiate_ping(
733        &mut self,
734        id: Id,
735        interface_name: &str,
736        network_check_sender: &mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
737        new_state: NetworkCheckState,
738        addrs: Vec<std::net::SocketAddr>,
739    ) {
740        self.checker_state = new_state;
741        self.ping_addrs = addrs;
742        self.pings_expected = self.ping_addrs.len();
743        self.pings_completed = 0;
744        self.ping_addrs
745            .iter()
746            .map(|addr| {
747                let action = NetworkCheckAction::Ping(PingParameters {
748                    interface_name: interface_name.to_string(),
749                    addr: addr.clone(),
750                });
751                (action, NetworkCheckCookie { id })
752            })
753            .for_each(|message| match network_check_sender.unbounded_send(message) {
754                Ok(()) => {}
755                Err(e) => {
756                    debug!("unable to send network check internet msg: {:?}", e)
757                }
758            });
759    }
760}
761
762impl Default for NetworkCheckContext {
763    // Create a context for an interface's network check.
764    fn default() -> Self {
765        NetworkCheckContext {
766            checker_state: Default::default(),
767            ping_addrs: Vec::new(),
768            pings_expected: 0usize,
769            pings_completed: 0usize,
770            fetches_expected: 0usize,
771            fetches_completed: 0usize,
772            discovered_state: IpVersions {
773                ipv4: State { link: LinkState::None, ..Default::default() },
774                ipv6: State { link: LinkState::None, ..Default::default() },
775            },
776            always_ping_internet: true,
777            router_discoverable: Default::default(),
778            gateway_pingable: Default::default(),
779            persistent_context: Default::default(),
780        }
781    }
782}
783
784impl From<TelemetryContext> for NetworkCheckContext {
785    fn from(value: TelemetryContext) -> Self {
786        NetworkCheckContext {
787            persistent_context: PersistentNetworkCheckContext::from(value),
788            ..Default::default()
789        }
790    }
791}
792
793/// NetworkCheckCookie is an opaque type used to continue an asynchronous network check.
794#[derive(Clone)]
795pub struct NetworkCheckCookie {
796    /// The interface id.
797    id: Id,
798}
799
800#[derive(Debug)]
801pub enum NetworkCheckResult {
802    Ping { parameters: PingParameters, result: Result<(), ping::PingError> },
803    ResolveDns { parameters: ResolveDnsParameters, ips: Option<ResolvedIps> },
804    Fetch { parameters: FetchParameters, result: Result<u16, fetch::FetchError> },
805}
806
807#[derive(Debug, Clone)]
808pub struct PingParameters {
809    /// The name of the interface sending the ping.
810    pub interface_name: std::string::String,
811    /// The address to ping.
812    pub addr: std::net::SocketAddr,
813}
814
815#[derive(Debug, Clone)]
816pub struct ResolveDnsParameters {
817    /// The name of the interface sending the ping.
818    pub interface_name: std::string::String,
819    /// The domain to resolve.
820    pub domain: String,
821}
822
823#[derive(Debug, Clone)]
824pub struct FetchParameters {
825    /// The name of the interface sending the ping.
826    pub interface_name: std::string::String,
827    /// The http domain, sent with the Host header to the server.
828    pub domain: std::string::String,
829    /// The DNS Resolved IP address for the fetch server.
830    pub ip: std::net::IpAddr,
831    /// Path to send request to.
832    pub path: String,
833    /// The expected HTTP status codes.
834    pub expected_statuses: Vec<u16>,
835}
836
837impl NetworkCheckResult {
838    fn interface_name(&self) -> &str {
839        match self {
840            NetworkCheckResult::Ping {
841                parameters: PingParameters { interface_name, .. }, ..
842            } => interface_name,
843            NetworkCheckResult::ResolveDns {
844                parameters: ResolveDnsParameters { interface_name, .. },
845                ..
846            } => interface_name,
847            NetworkCheckResult::Fetch {
848                parameters: FetchParameters { interface_name, .. },
849                ..
850            } => interface_name,
851        }
852    }
853
854    fn ping_result(self) -> Option<(PingParameters, Result<(), ping::PingError>)> {
855        match self {
856            NetworkCheckResult::Ping { parameters, result } => Some((parameters, result)),
857            _ => None,
858        }
859    }
860
861    fn resolve_dns_result(self) -> Option<(ResolveDnsParameters, Option<ResolvedIps>)> {
862        match self {
863            NetworkCheckResult::ResolveDns { parameters, ips } => Some((parameters, ips)),
864            _ => None,
865        }
866    }
867
868    fn fetch_result(self) -> Option<(FetchParameters, Result<u16, fetch::FetchError>)> {
869        match self {
870            NetworkCheckResult::Fetch { parameters, result } => Some((parameters, result)),
871            _ => None,
872        }
873    }
874}
875
876/// `NetworkCheckAction` describes the action to be completed before resuming the network check.
877#[derive(Debug, Clone)]
878pub enum NetworkCheckAction {
879    Ping(PingParameters),
880    ResolveDns(ResolveDnsParameters),
881    Fetch(FetchParameters),
882}
883
884pub trait TimeProvider {
885    fn now(&mut self) -> zx::MonotonicInstant;
886}
887
888#[derive(Debug, Default)]
889pub struct MonotonicInstant;
890impl TimeProvider for MonotonicInstant {
891    fn now(&mut self) -> zx::MonotonicInstant {
892        zx::MonotonicInstant::get()
893    }
894}
895
896/// `Monitor` monitors the reachability state.
897pub struct Monitor<Time = MonotonicInstant> {
898    state: StateInfo,
899    stats: Stats,
900    inspector: Option<&'static Inspector>,
901    system_node: Option<InspectInfo>,
902    nodes: HashMap<Id, InspectInfo>,
903    telemetry_sender: Option<TelemetrySender>,
904    /// In `Monitor`'s implementation of NetworkChecker, the sender is used to dispatch network
905    /// checks to the eventloop to be run concurrently. The network check then will be resumed with
906    /// the result of the `NetworkCheckAction`.
907    network_check_sender: mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
908    interface_context: HashMap<Id, NetworkCheckContext>,
909    time_provider: Time,
910}
911
912impl<Time: TimeProvider + Default> Monitor<Time> {
913    /// Create the monitoring service.
914    pub fn new(
915        network_check_sender: mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
916    ) -> anyhow::Result<Self> {
917        Ok(Monitor {
918            state: Default::default(),
919            stats: Default::default(),
920            inspector: None,
921            system_node: None,
922            nodes: HashMap::new(),
923            telemetry_sender: None,
924            network_check_sender,
925            interface_context: HashMap::new(),
926            time_provider: Default::default(),
927        })
928    }
929}
930
931impl<Time> Monitor<Time> {
932    /// Create the monitoring service.
933    pub fn new_with_time_provider(
934        network_check_sender: mpsc::UnboundedSender<(NetworkCheckAction, NetworkCheckCookie)>,
935        time_provider: Time,
936    ) -> anyhow::Result<Self> {
937        Ok(Monitor {
938            state: Default::default(),
939            stats: Default::default(),
940            inspector: None,
941            system_node: None,
942            nodes: HashMap::new(),
943            telemetry_sender: None,
944            network_check_sender,
945            interface_context: HashMap::new(),
946            time_provider,
947        })
948    }
949}
950
951impl<Time: TimeProvider> Monitor<Time> {
952    pub fn state(&self) -> &StateInfo {
953        &self.state
954    }
955
956    /// Reports all information.
957    pub fn report_state(&self) {
958        self.state.report();
959        debug!("reachability stats {:?}", self.stats);
960    }
961
962    /// Sets the inspector.
963    pub fn set_inspector(&mut self, inspector: &'static Inspector) {
964        self.inspector = Some(inspector);
965
966        let system_node = InspectInfo::new(inspector.root(), "system", "");
967        self.system_node = Some(system_node);
968
969        LinkState::log_state_vals_inspect(inspector.root(), "state_vals");
970    }
971
972    pub fn set_telemetry_sender(&mut self, telemetry_sender: TelemetrySender) {
973        self.telemetry_sender = Some(telemetry_sender);
974    }
975
976    fn interface_node(&mut self, id: Id, name: &str) -> Option<&mut InspectInfo> {
977        self.inspector.map(move |inspector| {
978            self.nodes.entry(id).or_insert_with_key(|id| {
979                InspectInfo::new(inspector.root(), &format!("{:?}", id), name)
980            })
981        })
982    }
983
984    fn update_state_from_context(
985        &mut self,
986        id: Id,
987        name: &str,
988    ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
989        let ctx = self.interface_context.get_mut(&id).ok_or_else(|| {
990            anyhow!(
991                "attempting to update state with context but context for id {} does not exist",
992                id
993            )
994        })?;
995
996        ctx.checker_state = NetworkCheckState::Idle;
997
998        if let Some(IpVersions { ipv4, ipv6 }) = self.state.get(id) {
999            if ipv4.state.link == LinkState::Removed && ipv6.state.link == LinkState::Removed {
1000                debug!("interface {} was removed, skipping state update", id);
1001                return Ok(NetworkCheckerOutcome::Complete);
1002            }
1003        }
1004
1005        let info = IpVersions {
1006            ipv4: StateEvent {
1007                state: ctx.discovered_state.ipv4,
1008                time: fasync::MonotonicInstant::now(),
1009            },
1010            ipv6: StateEvent {
1011                state: ctx.discovered_state.ipv6,
1012                time: fasync::MonotonicInstant::now(),
1013            },
1014        };
1015
1016        let gateway_event_v4 = TelemetryEvent::GatewayProbe {
1017            gateway_discoverable: ctx.router_discoverable.ipv4,
1018            gateway_pingable: ctx.gateway_pingable.ipv4,
1019            internet_available: ctx.discovered_state.ipv4.has_internet(),
1020        };
1021        let gateway_event_v6 = TelemetryEvent::GatewayProbe {
1022            gateway_discoverable: ctx.router_discoverable.ipv6,
1023            gateway_pingable: ctx.gateway_pingable.ipv6,
1024            internet_available: ctx.discovered_state.ipv6.has_internet(),
1025        };
1026
1027        if let Some(telemetry_sender) = &mut self.telemetry_sender {
1028            telemetry_sender.send(gateway_event_v4);
1029            telemetry_sender.send(gateway_event_v6);
1030            telemetry_sender.send(TelemetryEvent::SystemStateUpdate {
1031                update: telemetry::SystemStateUpdate {
1032                    system_state: self.state.get_system().state(),
1033                },
1034            });
1035            let telemetry_context = &ctx.persistent_context.telemetry;
1036            let interface_identifiers = &telemetry_context.interface_identifiers;
1037            telemetry_sender.send(TelemetryEvent::LinkPropertiesUpdate {
1038                interface_identifiers: interface_identifiers.clone(),
1039                link_properties: IpVersions {
1040                    ipv4: LinkProperties {
1041                        has_address: telemetry_context.has_v4_address,
1042                        has_default_route: telemetry_context.has_default_ipv4_route,
1043                        has_dns: ctx.discovered_state.ipv4.has_dns(),
1044                        has_http_reachability: ctx.discovered_state.ipv4.has_http(),
1045                    },
1046                    ipv6: LinkProperties {
1047                        has_address: telemetry_context.has_v6_address,
1048                        has_default_route: telemetry_context.has_default_ipv6_route,
1049                        has_dns: ctx.discovered_state.ipv6.has_dns(),
1050                        has_http_reachability: ctx.discovered_state.ipv6.has_http(),
1051                    },
1052                },
1053            });
1054            telemetry_sender.send(TelemetryEvent::LinkStateUpdate {
1055                interface_identifiers: interface_identifiers.clone(),
1056                link_state: IpVersions {
1057                    ipv4: ctx.discovered_state.ipv4.link,
1058                    ipv6: ctx.discovered_state.ipv6.link,
1059                },
1060            });
1061        }
1062
1063        let () = self.update_state(id, &name, info);
1064        Ok(NetworkCheckerOutcome::Complete)
1065    }
1066
1067    /// Update state based on the new reachability info.
1068    fn update_state(&mut self, id: Id, name: &str, reachability: IpVersions<StateEvent>) {
1069        let StateDelta { port, system } = self.state.update(id, reachability);
1070
1071        let () = port.with_version(|proto, delta| {
1072            if delta.change_observed() {
1073                let &Delta { previous, current } = delta;
1074                if let Some(previous) = previous {
1075                    info!(
1076                        "Interface [{}] updated --> {} current: {}, previous: {}",
1077                        id, proto, current, previous
1078                    );
1079                } else {
1080                    info!("New interface [{}] --> {}: {}", id, proto, current);
1081                }
1082                let () = log_state(self.interface_node(id, name), proto, current.state);
1083                *self.stats.state_updates.entry(id).or_insert(0) += 1;
1084            }
1085        });
1086
1087        let () = system.with_version(|proto, delta| {
1088            if delta.change_observed() {
1089                let &Delta { previous, current } = delta;
1090                if let Some(previous) = previous {
1091                    info!(
1092                        "System reachability updated --> {} current: {}, previous: {}",
1093                        proto, current, previous
1094                    );
1095                } else {
1096                    info!("Initial system reachability --> {}: {}", proto, current);
1097                }
1098                let () = log_state(self.system_node.as_mut(), proto, current.state.state);
1099            }
1100        });
1101    }
1102
1103    /// Handle an interface removed event.
1104    pub fn handle_interface_removed(
1105        &mut self,
1106        fnet_interfaces_ext::Properties { id, name, .. }: fnet_interfaces_ext::Properties<
1107            fnet_interfaces_ext::DefaultInterest,
1108        >,
1109    ) {
1110        let time = fasync::MonotonicInstant::now();
1111        if let Some(mut reachability) = self.state.get(id.into()).cloned() {
1112            reachability.ipv4 = StateEvent {
1113                state: State { link: LinkState::Removed, ..Default::default() },
1114                time,
1115            };
1116            reachability.ipv6 = StateEvent {
1117                state: State { link: LinkState::Removed, ..Default::default() },
1118                time,
1119            };
1120            let () = self.update_state(id.into(), &name, reachability);
1121        }
1122    }
1123
1124    fn handle_fetch_success(ctx: &mut NetworkCheckContext, ip: std::net::IpAddr) {
1125        match ctx.checker_state {
1126            NetworkCheckState::FetchHttp => match ip {
1127                IpAddr::V4(_) => {
1128                    ctx.discovered_state.ipv4.application.http_fetch_succeeded = true;
1129                }
1130                IpAddr::V6(_) => {
1131                    ctx.discovered_state.ipv6.application.http_fetch_succeeded = true;
1132                }
1133            },
1134            NetworkCheckState::PingGateway
1135            | NetworkCheckState::PingInternet
1136            | NetworkCheckState::Begin
1137            | NetworkCheckState::Idle
1138            | NetworkCheckState::ResolveDns => {
1139                panic!("continue check had an invalid state")
1140            }
1141        }
1142    }
1143
1144    fn handle_ping_success(ctx: &mut NetworkCheckContext, addr: &std::net::SocketAddr) {
1145        match ctx.checker_state {
1146            NetworkCheckState::PingGateway => match addr {
1147                std::net::SocketAddr::V4 { .. } => {
1148                    ctx.gateway_pingable.ipv4 = true;
1149                    ctx.discovered_state.ipv4.set_link_state(LinkState::Gateway);
1150                }
1151                std::net::SocketAddr::V6 { .. } => {
1152                    ctx.gateway_pingable.ipv6 = true;
1153                    ctx.discovered_state.ipv6.set_link_state(LinkState::Gateway);
1154                }
1155            },
1156            NetworkCheckState::PingInternet => match addr {
1157                std::net::SocketAddr::V4 { .. } => {
1158                    ctx.discovered_state.ipv4.set_link_state(LinkState::Internet)
1159                }
1160                std::net::SocketAddr::V6 { .. } => {
1161                    ctx.discovered_state.ipv6.set_link_state(LinkState::Internet)
1162                }
1163            },
1164            NetworkCheckState::FetchHttp
1165            | NetworkCheckState::Begin
1166            | NetworkCheckState::Idle
1167            | NetworkCheckState::ResolveDns => {
1168                panic!("continue check had an invalid state")
1169            }
1170        }
1171    }
1172}
1173
1174impl<Time: TimeProvider> NetworkChecker for Monitor<Time> {
1175    fn begin(
1176        &mut self,
1177        InterfaceView {
1178            properties:
1179                &fnet_interfaces_ext::Properties {
1180                    id,
1181                    ref name,
1182                    port_class,
1183                    online,
1184                    ref addresses,
1185                    has_default_ipv4_route,
1186                    has_default_ipv6_route,
1187                    port_identity_koid: _,
1188                },
1189            routes,
1190            neighbors,
1191        }: InterfaceView<'_>,
1192    ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
1193        let id = Id::from(id);
1194        // Check to see if the current interface view is already in the map. If its state is not
1195        // Idle then another network check for the interface is already processing. In this case,
1196        // drop the `begin` request and log it.
1197        // It is expected for this to occur when an interface is experiencing many events in a
1198        // short period of time, for example changing between online and offline multiple times
1199        // over the span of a few seconds. It is safe that this happens, as the system is
1200        // eventually consistent.
1201        let telemetry_context = TelemetryContext::new(
1202            port_class,
1203            &addresses,
1204            has_default_ipv4_route,
1205            has_default_ipv6_route,
1206        );
1207        let ctx = self
1208            .interface_context
1209            .entry(id)
1210            .or_insert_with(|| NetworkCheckContext::from(telemetry_context.clone()));
1211
1212        match ctx.checker_state {
1213            NetworkCheckState::Begin => {}
1214            NetworkCheckState::Idle => {
1215                let mut new_ctx = NetworkCheckContext::default();
1216                // Copy persistent context context between passes
1217                std::mem::swap(&mut new_ctx.persistent_context, &mut ctx.persistent_context);
1218                // The telemetry should be updated based on the Properties passed into `begin`.
1219                new_ctx.persistent_context.telemetry = telemetry_context;
1220                *ctx = new_ctx;
1221            }
1222            NetworkCheckState::PingGateway
1223            | NetworkCheckState::PingInternet
1224            | NetworkCheckState::FetchHttp
1225            | NetworkCheckState::ResolveDns => {
1226                // Update the Properties for the TelemetryContext so that the LinkProperties can
1227                // be reported properly.
1228                ctx.persistent_context.telemetry = telemetry_context;
1229                return Err(anyhow!("skipped, non-idle state found on Interface [{id}]"));
1230            }
1231        }
1232
1233        if !online {
1234            ctx.set_global_link_state(LinkState::Down);
1235            return self.update_state_from_context(id, name);
1236        }
1237
1238        ctx.set_global_link_state(LinkState::Up);
1239
1240        // TODO(https://fxbug.dev/42154208) Check if packet count has increased, and if so upgrade
1241        // the state to LinkLayerUp.
1242        let device_routes: Vec<_> = routes.device_routes(id).collect();
1243
1244        let neighbor_scan_health = scan_neighbor_health(neighbors, &device_routes);
1245
1246        let has_route = IpVersions {
1247            ipv4: device_routes
1248                .iter()
1249                .any(|route| matches!(route.destination.addr, fnet::IpAddress::Ipv4(_))),
1250            ipv6: device_routes
1251                .iter()
1252                .any(|route| matches!(route.destination.addr, fnet::IpAddress::Ipv6(_))),
1253        };
1254
1255        if neighbor_scan_health.ipv4 == NeighborHealthScanResult::NoneHealthy
1256            && neighbor_scan_health.ipv6 == NeighborHealthScanResult::NoneHealthy
1257        {
1258            if !has_route.ipv4 && !has_route.ipv6 {
1259                // Both protocols are `Up`, no need to perform any further calculations.
1260                return self.update_state_from_context(id, name);
1261            }
1262
1263            // When a router is not discoverable via ND, the internet should only be pinged
1264            // if the gateway ping succeeds.
1265            ctx.always_ping_internet = false;
1266        }
1267        if has_route.ipv4 || neighbor_scan_health.ipv4.is_healthy() {
1268            ctx.discovered_state.ipv4.set_link_state(LinkState::Local);
1269        }
1270        if has_route.ipv6 || neighbor_scan_health.ipv6.is_healthy() {
1271            ctx.discovered_state.ipv6.set_link_state(LinkState::Local);
1272        }
1273
1274        let gateway_ping_addrs = device_routes
1275            .iter()
1276            .filter_map(move |Route { destination, outbound_interface, next_hop }| {
1277                if *destination != UNSPECIFIED_V4 && *destination != UNSPECIFIED_V6 {
1278                    return None;
1279                }
1280                next_hop.and_then(|next_hop| {
1281                    let fnet_ext::IpAddress(next_hop) = next_hop.into();
1282                    match next_hop.into() {
1283                        std::net::IpAddr::V4(v4) => {
1284                            Some(std::net::SocketAddr::V4(std::net::SocketAddrV4::new(v4, 0)))
1285                        }
1286                        std::net::IpAddr::V6(v6) => match (*outbound_interface).try_into() {
1287                            Err(std::num::TryFromIntError { .. }) => {
1288                                error!("device id {} doesn't fit in u32", outbound_interface);
1289                                None
1290                            }
1291                            Ok(device_id) => {
1292                                if device_id == 0
1293                                    && net_types::ip::Ipv6Addr::from_bytes(v6.octets()).scope()
1294                                        != net_types::ip::Ipv6Scope::Global
1295                                {
1296                                    None
1297                                } else {
1298                                    Some(std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
1299                                        v6, 0, 0, device_id,
1300                                    )))
1301                                }
1302                            }
1303                        },
1304                    }
1305                })
1306            })
1307            .map(|next_hop| next_hop)
1308            .collect::<Vec<_>>();
1309
1310        // A router is determined to be discoverable if it is online (marked as healthy by ND).
1311        ctx.router_discoverable = IpVersions {
1312            ipv4: neighbor_scan_health.ipv4 == NeighborHealthScanResult::HealthyRouter,
1313            ipv6: neighbor_scan_health.ipv6 == NeighborHealthScanResult::HealthyRouter,
1314        };
1315        if gateway_ping_addrs.is_empty() {
1316            // When there are no gateway addresses to ping, the gateway is not pingable. The list
1317            // of Gateway addresses is obtained by filtering the default IPv4 and IPv6 routes.
1318
1319            // We use the discovery of an online router as a separate opportunity to calculate
1320            // internet reachability because of the potential for various network configurations.
1321            // One potential case involves having an AP operating in bridge mode, and having a
1322            // separate device host DHCP. In this situation, it's possible to have routes that can
1323            // be used to send pings to the internet that are not default routes. In another case,
1324            // a router may have a very specific target prefix that is routable. The device could
1325            // access a remote set of addresses through this local router and not view it as being
1326            // accessed through a default route.
1327            if neighbor_scan_health.ipv4 == NeighborHealthScanResult::HealthyRouter
1328                || neighbor_scan_health.ipv6 == NeighborHealthScanResult::HealthyRouter
1329            {
1330                // Setup to ping internet addresses, skipping over gateway pings.
1331                // Internet can be pinged when either an online router is discovered or the gateway
1332                // is pingable. In this case, the discovery of a router enables the internet ping.
1333                // TODO(https://fxbug.dev/42074958): Create an occurrence metric for this case
1334                ctx.initiate_ping(
1335                    id,
1336                    name,
1337                    &self.network_check_sender,
1338                    NetworkCheckState::PingInternet,
1339                    [
1340                        IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1341                        IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1342                    ]
1343                    .into_iter()
1344                    .map(|ip| std::net::SocketAddr::new(ip, 0))
1345                    .collect(),
1346                );
1347            } else {
1348                // The router is not online and the gateway cannot be pinged; therefore, the
1349                // internet pings can be skipped and the final reachability state can be
1350                // determined.
1351                return self.update_state_from_context(id, name);
1352            }
1353        } else {
1354            // Setup to ping gateway addresses.
1355            if neighbor_scan_health.ipv4.is_healthy_router() {
1356                ctx.discovered_state.ipv4.set_link_state(LinkState::Gateway);
1357            }
1358            if neighbor_scan_health.ipv6.is_healthy_router() {
1359                ctx.discovered_state.ipv6.set_link_state(LinkState::Gateway);
1360            }
1361            ctx.initiate_ping(
1362                id,
1363                name,
1364                &self.network_check_sender,
1365                NetworkCheckState::PingGateway,
1366                gateway_ping_addrs,
1367            );
1368        }
1369        Ok(NetworkCheckerOutcome::MustResume)
1370    }
1371
1372    fn resume(
1373        &mut self,
1374        cookie: NetworkCheckCookie,
1375        result: NetworkCheckResult,
1376    ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
1377        let ctx = self.interface_context.get_mut(&cookie.id).ok_or_else(|| {
1378            anyhow!("resume: interface id {} should already exist in map", cookie.id)
1379        })?;
1380        let interface_name = result.interface_name().to_string();
1381        match ctx.checker_state {
1382            NetworkCheckState::Begin | NetworkCheckState::Idle => {
1383                return Err(anyhow!(
1384                    "skipped, idle state found in resume for interface {}",
1385                    cookie.id
1386                ));
1387            }
1388            NetworkCheckState::PingGateway | NetworkCheckState::PingInternet => {
1389                let (parameters, result) = result.ping_result().ok_or_else(|| {
1390                    anyhow!("resume: mismatched state and result {interface_name} ({})", cookie.id)
1391                })?;
1392                ctx.pings_completed = ctx.pings_completed + 1;
1393
1394                // Grab `ping_is_ok` before `result` is moved into `telemetry_sender.send`.
1395                let ping_is_ok = result.is_ok();
1396
1397                if let Some(telemetry_sender) = &mut self.telemetry_sender {
1398                    let interface_identifiers =
1399                        ctx.persistent_context.telemetry.interface_identifiers.clone();
1400                    if let NetworkCheckState::PingInternet = ctx.checker_state {
1401                        telemetry_sender.send(TelemetryEvent::InternetPingResult {
1402                            interface_identifiers,
1403                            ping_parameters: parameters.clone(),
1404                            internet_ping_result: result,
1405                        });
1406                    } else {
1407                        telemetry_sender.send(TelemetryEvent::GatewayPingResult {
1408                            interface_identifiers,
1409                            ping_parameters: parameters.clone(),
1410                            gateway_ping_result: result,
1411                        });
1412                    }
1413                }
1414
1415                let PingParameters { interface_name, addr, .. } = parameters;
1416                if ping_is_ok {
1417                    let () = Self::handle_ping_success(ctx, &addr);
1418                }
1419
1420                if ctx.pings_completed == ctx.pings_expected {
1421                    if let NetworkCheckState::PingGateway = ctx.checker_state {
1422                        ctx.initiate_ping(
1423                            cookie.id,
1424                            &interface_name,
1425                            &self.network_check_sender,
1426                            NetworkCheckState::PingInternet,
1427                            [
1428                                IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1429                                IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1430                            ]
1431                            .into_iter()
1432                            .map(|ip| std::net::SocketAddr::new(ip, 0))
1433                            .collect(),
1434                        );
1435                    } else {
1436                        let parameters = ResolveDnsParameters {
1437                            interface_name: interface_name.to_string(),
1438                            domain: GSTATIC.into(),
1439                        };
1440                        ctx.checker_state = NetworkCheckState::ResolveDns;
1441
1442                        if self.time_provider.now() - ctx.persistent_context.resolved_time
1443                            < DNS_PROBE_PERIOD
1444                        {
1445                            debug!(
1446                                "Skipping ResolveDns since it has not yet been {} seconds",
1447                                DNS_PROBE_PERIOD.clone().into_seconds()
1448                            );
1449                            if let Some(ips) = ctx.persistent_context.resolved_addrs.get(GSTATIC) {
1450                                if !ips.v4.is_empty() {
1451                                    ctx.discovered_state.ipv4.application.dns_resolved = true;
1452                                }
1453                                if !ips.v6.is_empty() {
1454                                    ctx.discovered_state.ipv6.application.dns_resolved = true;
1455                                }
1456                            }
1457                            return self.resume(
1458                                cookie,
1459                                NetworkCheckResult::ResolveDns { parameters, ips: None },
1460                            );
1461                        }
1462
1463                        let action = NetworkCheckAction::ResolveDns(parameters);
1464                        match self
1465                            .network_check_sender
1466                            .unbounded_send((action, NetworkCheckCookie { id: cookie.id }))
1467                        {
1468                            Ok(()) => {}
1469                            Err(e) => {
1470                                debug!("unable to send network check internet msg: {e:?}")
1471                            }
1472                        }
1473                    }
1474                }
1475            }
1476            NetworkCheckState::ResolveDns => {
1477                let (ResolveDnsParameters { interface_name, domain }, ips) =
1478                    result.resolve_dns_result().ok_or_else(|| {
1479                        anyhow!(
1480                            "resume: mismatched state and result {interface_name} ({})",
1481                            cookie.id
1482                        )
1483                    })?;
1484
1485                if let Some(ips) = ips {
1486                    if !ips.v4.is_empty() {
1487                        ctx.discovered_state.ipv4.application.dns_resolved = true;
1488                    }
1489                    if !ips.v6.is_empty() {
1490                        ctx.discovered_state.ipv6.application.dns_resolved = true;
1491                    }
1492                    ctx.persistent_context.resolved_time = self.time_provider.now();
1493                    let _: Option<ResolvedIps> =
1494                        ctx.persistent_context.resolved_addrs.insert(domain.clone(), ips);
1495                }
1496
1497                ctx.checker_state = NetworkCheckState::FetchHttp;
1498                ctx.fetches_expected = 0;
1499
1500                let mut add_fetch = |ip: IpAddr| {
1501                    ctx.fetches_expected += 1;
1502                    let action = NetworkCheckAction::Fetch(FetchParameters {
1503                        interface_name: interface_name.clone(),
1504                        domain: domain.clone(),
1505                        ip,
1506                        path: GENERATE_204.into(),
1507                        expected_statuses: vec![204],
1508                    });
1509                    match self
1510                        .network_check_sender
1511                        .unbounded_send((action, NetworkCheckCookie { id: cookie.id }))
1512                    {
1513                        Ok(()) => {}
1514                        Err(e) => debug!("unable to send network check internet message: {e:?}"),
1515                    }
1516                };
1517
1518                if let Some(v4) =
1519                    ctx.persistent_context.resolved_addrs.get(&domain).and_then(|ips| ips.v4.get(0))
1520                {
1521                    add_fetch(IpAddr::V4(*v4));
1522                }
1523                if let Some(v6) =
1524                    ctx.persistent_context.resolved_addrs.get(&domain).and_then(|ips| ips.v6.get(0))
1525                {
1526                    add_fetch(IpAddr::V6(*v6));
1527                }
1528
1529                if ctx.fetches_expected == 0 {
1530                    return self.update_state_from_context(cookie.id, &interface_name);
1531                }
1532            }
1533            NetworkCheckState::FetchHttp => {
1534                let (parameters, result) = result.fetch_result().ok_or_else(|| {
1535                    anyhow!("resume: mismatched state and result {interface_name} ({})", cookie.id)
1536                })?;
1537                ctx.fetches_completed += 1;
1538
1539                // Grab `fetch_ok_status` before `result` is moved into `telemetry_sender.send`.
1540                let fetch_ok_status = result.as_ref().copied().ok();
1541
1542                if let Some(telemetry_sender) = &mut self.telemetry_sender {
1543                    telemetry_sender.send(TelemetryEvent::FetchResult {
1544                        interface_identifiers: ctx
1545                            .persistent_context
1546                            .telemetry
1547                            .interface_identifiers
1548                            .clone(),
1549                        fetch_parameters: parameters.clone(),
1550                        fetch_result: result,
1551                    });
1552                }
1553
1554                let FetchParameters { interface_name, ip, expected_statuses, .. } = parameters;
1555                if let Some(status) = fetch_ok_status {
1556                    if expected_statuses.contains(&status) {
1557                        let () = Self::handle_fetch_success(ctx, ip);
1558                    }
1559                }
1560
1561                if ctx.fetches_completed == ctx.fetches_expected {
1562                    return self.update_state_from_context(cookie.id, &interface_name);
1563                }
1564            }
1565        }
1566        Ok(NetworkCheckerOutcome::MustResume)
1567    }
1568}
1569
1570fn log_state(info: Option<&mut InspectInfo>, proto: Proto, state: State) {
1571    info.into_iter().for_each(|info| info.log_link_state(proto, state.link))
1572}
1573
1574#[derive(Default, PartialEq)]
1575enum NeighborHealthScanResult {
1576    // No healthy neighbors were discovered.
1577    #[default]
1578    NoneHealthy,
1579    // A healthy neighbor was discovered.
1580    HealthyNeighbor,
1581    // A healthy router was discovered. Takes precedence over
1582    // `HealthyNeighbor` since an healthy router implies
1583    // a healthy neighbor.
1584    HealthyRouter,
1585}
1586
1587impl NeighborHealthScanResult {
1588    // A neighbor was discovered. Update the state based on whether the neighbor
1589    // is a router.
1590    fn update_scan_result(&mut self, is_router: bool) {
1591        *self = match (&self, is_router) {
1592            // HealthyRouter should never degrade to HealthyNeighbor.
1593            (_, true) | (Self::HealthyRouter, _) => Self::HealthyRouter,
1594            _ => Self::HealthyNeighbor,
1595        }
1596    }
1597
1598    fn is_healthy(&self) -> bool {
1599        match self {
1600            Self::NoneHealthy => false,
1601            Self::HealthyNeighbor | Self::HealthyRouter => true,
1602        }
1603    }
1604
1605    fn is_healthy_router(&self) -> bool {
1606        match self {
1607            Self::NoneHealthy | Self::HealthyNeighbor => false,
1608            Self::HealthyRouter => true,
1609        }
1610    }
1611}
1612
1613// Determines whether any online neighbors or online gateways are discoverable via neighbor
1614// discovery. The definition of a Healthy neighbor correlates to a neighbor being online.
1615fn scan_neighbor_health(
1616    neighbors: Option<&InterfaceNeighborCache>,
1617    device_routes: &Vec<route_table::Route>,
1618) -> IpVersions<NeighborHealthScanResult> {
1619    match neighbors {
1620        None => Default::default(),
1621        Some(neighbors) => {
1622            let router_next_hops: HashSet<_> =
1623                device_routes.iter().filter_map(|Route { next_hop, .. }| *next_hop).collect();
1624            neighbors.iter_health().fold(
1625                Default::default(),
1626                |mut neighbor_health_scan, (neighbor, health)| {
1627                    match health {
1628                        // When we find an unhealthy or unknown neighbor, continue,
1629                        // keeping whether we've previously found a healthy neighbor.
1630                        neighbor_cache::NeighborHealth::Unhealthy { .. }
1631                        | neighbor_cache::NeighborHealth::Unknown => neighbor_health_scan,
1632                        // If there's a healthy router, then we're done. If the neighbor
1633                        // is not a router, then we know we have a healthy neighbor, but
1634                        // not a healthy router.
1635                        neighbor_cache::NeighborHealth::Healthy { .. } => {
1636                            let scan = match neighbor {
1637                                fnet::IpAddress::Ipv4(..) => &mut neighbor_health_scan.ipv4,
1638                                fnet::IpAddress::Ipv6(..) => &mut neighbor_health_scan.ipv6,
1639                            };
1640
1641                            scan.update_scan_result(router_next_hops.contains(neighbor));
1642                            neighbor_health_scan
1643                        }
1644                    }
1645                },
1646            )
1647        }
1648    }
1649}
1650
1651#[cfg(test)]
1652mod tests {
1653    use crate::fetch::FetchAddr;
1654
1655    use super::*;
1656    use crate::dig::Dig;
1657    use crate::fetch::Fetch;
1658    use crate::neighbor_cache::{NeighborHealth, NeighborState};
1659    use crate::ping::Ping;
1660    use async_trait::async_trait;
1661    use diagnostics_assertions::assert_data_tree;
1662    use fidl_fuchsia_net as fnet;
1663    use fidl_fuchsia_net_interfaces as fnet_interfaces;
1664    use fuchsia_async as fasync;
1665    use futures::StreamExt as _;
1666    use net_declare::{fidl_ip, fidl_subnet, std_ip, std_socket_addr};
1667    use net_types::ip;
1668    use std::pin::pin;
1669    use std::task::Poll;
1670    use test_case::test_case;
1671
1672    const ETHERNET_INTERFACE_NAME: &str = "eth1";
1673    const ID1: u64 = 1;
1674    const ID2: u64 = 2;
1675    // RFC5737§3 specifies the reserved IPv4 address prefix for tests and documentation.
1676    const IPV4_ADDR: fnet::IpAddress = fidl_ip!("192.168.0.1");
1677    // RFC-3849§4 specifies the global IPv6 unicast address prefix for tests and documentation.
1678    const IPV6_ADDR: fnet::IpAddress = fidl_ip!("2001:db8::");
1679
1680    // A trait for writing helper constructors.
1681    //
1682    // Note that this trait differs from `std::convert::From` only in name, but will almost always
1683    // contain shortcuts that would be too surprising for an actual `From` implementation.
1684    trait Construct<T> {
1685        fn construct(_: T) -> Self;
1686    }
1687
1688    impl<S: Into<State>> Construct<S> for StateEvent {
1689        fn construct(link: S) -> Self {
1690            Self { state: link.into(), time: fasync::MonotonicInstant::INFINITE }
1691        }
1692    }
1693
1694    impl Construct<(LinkState, bool, bool)> for StateEvent {
1695        fn construct((link, dns_resolved, http_fetch_succeeded): (LinkState, bool, bool)) -> Self {
1696            Self {
1697                state: State {
1698                    link,
1699                    application: ApplicationState { dns_resolved, http_fetch_succeeded },
1700                },
1701                time: fasync::MonotonicInstant::INFINITE,
1702            }
1703        }
1704    }
1705
1706    impl Construct<StateEvent> for IpVersions<StateEvent> {
1707        fn construct(state: StateEvent) -> Self {
1708            Self { ipv4: state, ipv6: state }
1709        }
1710    }
1711
1712    struct FakeTime {
1713        increment: zx::MonotonicDuration,
1714        time: zx::MonotonicInstant,
1715    }
1716
1717    impl TimeProvider for FakeTime {
1718        fn now(&mut self) -> zx::MonotonicInstant {
1719            let result = self.time;
1720            self.time += self.increment;
1721            result
1722        }
1723    }
1724
1725    #[fuchsia::test]
1726    async fn test_log_state_vals_inspect() {
1727        let inspector = Inspector::default();
1728        LinkState::log_state_vals_inspect(inspector.root(), "state_vals");
1729        assert_data_tree!(inspector, root: {
1730            state_vals: {
1731                "1": "None",
1732                "5": "Removed",
1733                "10": "Down",
1734                "15": "Up",
1735                "20": "Local",
1736                "25": "Gateway",
1737                "30": "Internet",
1738            }
1739        })
1740    }
1741
1742    #[fuchsia::test]
1743    fn test_display() {
1744        assert_eq!(Proto::IPv4.to_string(), "IPv4");
1745        assert_eq!(Proto::IPv6.to_string(), "IPv6");
1746
1747        let system_state = SystemState {
1748            id: 2,
1749            state: StateEvent {
1750                state: State {
1751                    link: LinkState::Local,
1752                    application: ApplicationState {
1753                        dns_resolved: false,
1754                        http_fetch_succeeded: false,
1755                    },
1756                },
1757                time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1758            },
1759        };
1760        assert_eq!(system_state.to_string(), "Local (dns: false, http: false) via Interface [2]");
1761
1762        let system_state_zero = SystemState {
1763            id: 0,
1764            state: StateEvent {
1765                state: State {
1766                    link: LinkState::Down,
1767                    application: ApplicationState {
1768                        dns_resolved: false,
1769                        http_fetch_succeeded: false,
1770                    },
1771                },
1772                time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1773            },
1774        };
1775        assert_eq!(
1776            system_state_zero.to_string(),
1777            "Down (dns: false, http: false) via Interface [0]"
1778        );
1779
1780        let system_state_internet = SystemState {
1781            id: 42,
1782            state: StateEvent {
1783                state: State {
1784                    link: LinkState::Internet,
1785                    application: ApplicationState {
1786                        dns_resolved: true,
1787                        http_fetch_succeeded: true,
1788                    },
1789                },
1790                time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1791            },
1792        };
1793        assert_eq!(
1794            system_state_internet.to_string(),
1795            "Internet (dns: true, http: true) via Interface [42]"
1796        );
1797
1798        let system_state_max_id = SystemState {
1799            id: u64::MAX,
1800            state: StateEvent {
1801                state: State {
1802                    link: LinkState::Gateway,
1803                    application: ApplicationState {
1804                        dns_resolved: true,
1805                        http_fetch_succeeded: false,
1806                    },
1807                },
1808                time: fasync::MonotonicInstant::from_nanos(1_000_000_000),
1809            },
1810        };
1811        assert_eq!(
1812            system_state_max_id.to_string(),
1813            "Gateway (dns: true, http: false) via Interface [18446744073709551615]"
1814        );
1815
1816        // Verify that no internal types or compiler artifacts leak into formatted strings.
1817        for formatted in [
1818            system_state.to_string(),
1819            system_state_zero.to_string(),
1820            system_state_internet.to_string(),
1821            system_state_max_id.to_string(),
1822        ] {
1823            assert!(!formatted.contains("PhantomData"));
1824            assert!(!formatted.contains("Instant"));
1825            assert!(!formatted.contains("MonotonicTimeline"));
1826        }
1827    }
1828
1829    #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("1.2.3.0:8080")];
1830        "gateway ping on ipv4")]
1831    #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("[123::]:0")];
1832        "gateway ping on ipv6")]
1833    #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("1.2.3.0:8080"),
1834        std_socket_addr!("[123::]:0")]; "gateway ping on ipv4/ipv6")]
1835    #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("8.8.8.8:0")];
1836        "internet ping on ipv4")]
1837    #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("[2001:4860:4860::8888]:0")];
1838        "internet ping on ipv6")]
1839    #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("8.8.8.8:0"),
1840        std_socket_addr!("[2001:4860:4860::8888]:0")]; "internet ping on ipv4/ipv6")]
1841    fn test_handle_ping_success(checker_state: NetworkCheckState, addrs: &[std::net::SocketAddr]) {
1842        let mut expected_state_v4: State = Default::default();
1843        let mut expected_state_v6: State = Default::default();
1844
1845        let mut ctx = NetworkCheckContext { checker_state, ..Default::default() };
1846        // Initial state.
1847        assert_eq!(ctx.discovered_state.ipv4, expected_state_v4);
1848        assert_eq!(ctx.discovered_state.ipv6, expected_state_v6);
1849
1850        let expected_state = match ctx.checker_state {
1851            NetworkCheckState::PingGateway => LinkState::Gateway.into(),
1852            NetworkCheckState::PingInternet => LinkState::Internet.into(),
1853            NetworkCheckState::ResolveDns => LinkState::Internet.into(),
1854            NetworkCheckState::FetchHttp => State {
1855                link: LinkState::Internet,
1856                application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
1857            },
1858            NetworkCheckState::Begin | NetworkCheckState::Idle => Default::default(),
1859        };
1860
1861        addrs.iter().for_each(|addr| {
1862            // Run the function under test for each address.
1863            let () = Monitor::<FakeTime>::handle_ping_success(&mut ctx, addr);
1864            // Update the expected values accordingly.
1865            match addr {
1866                std::net::SocketAddr::V4 { .. } => {
1867                    expected_state_v4 = expected_state;
1868                }
1869                std::net::SocketAddr::V6 { .. } => {
1870                    expected_state_v6 = expected_state;
1871                }
1872            }
1873        });
1874        // Final state.
1875        assert_eq!(ctx.discovered_state.ipv4, expected_state_v4);
1876        assert_eq!(ctx.discovered_state.ipv6, expected_state_v6);
1877    }
1878
1879    #[derive(Default, Clone)]
1880    struct FakePing {
1881        gateway_addrs: std::collections::HashSet<std::net::IpAddr>,
1882        gateway_response: bool,
1883        internet_response: bool,
1884    }
1885
1886    #[async_trait]
1887    impl Ping for FakePing {
1888        async fn ping(
1889            &self,
1890            _interface_name: &str,
1891            addr: std::net::SocketAddr,
1892        ) -> Result<(), crate::ping::PingError> {
1893            let Self { gateway_addrs, gateway_response, internet_response } = self;
1894            let ip = addr.ip();
1895            let success = if [
1896                IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1897                IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1898            ]
1899            .contains(&ip)
1900            {
1901                *internet_response
1902            } else if gateway_addrs.contains(&ip) {
1903                *gateway_response
1904            } else {
1905                false
1906            };
1907            if success { Ok(()) } else { Err(crate::ping::PingError::NoReply) }
1908        }
1909    }
1910
1911    #[derive(Default)]
1912    struct FakeDig {
1913        response: Option<ResolvedIps>,
1914    }
1915
1916    impl FakeDig {
1917        fn new(ips: Vec<std::net::IpAddr>) -> Self {
1918            let mut ips_out = ResolvedIps::default();
1919            for ip in ips {
1920                match ip {
1921                    IpAddr::V4(v4) => ips_out.v4.push(v4),
1922                    IpAddr::V6(v6) => ips_out.v6.push(v6),
1923                }
1924            }
1925            FakeDig { response: Some(ips_out) }
1926        }
1927    }
1928
1929    #[async_trait]
1930    impl Dig for FakeDig {
1931        async fn dig(&self, _interface_name: &str, _domain: &str) -> Option<ResolvedIps> {
1932            self.response.clone()
1933        }
1934    }
1935
1936    #[derive(Default)]
1937    struct FakeFetch {
1938        expected_url: Option<&'static str>,
1939        response: Option<Box<dyn Fn() -> Result<u16, fetch::FetchError> + Send + Sync>>,
1940    }
1941
1942    #[async_trait]
1943    impl Fetch for FakeFetch {
1944        async fn fetch<FA: FetchAddr + std::marker::Sync>(
1945            &self,
1946            _interface_name: &str,
1947            domain: &str,
1948            path: &str,
1949            _addr: &FA,
1950        ) -> Result<u16, fetch::FetchError> {
1951            if let Some(expected) = self.expected_url {
1952                assert_eq!(
1953                    format!("http://{domain}{path}"),
1954                    expected,
1955                    "Did not receive expected URL"
1956                );
1957            }
1958            if let Some(response) = &self.response {
1959                response()
1960            } else {
1961                Err(fetch::FetchError::ReadTcpStreamTimeout)
1962            }
1963        }
1964    }
1965
1966    struct NetworkCheckTestResponder {
1967        receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
1968    }
1969
1970    impl NetworkCheckTestResponder {
1971        fn new(
1972            receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
1973        ) -> Self {
1974            Self { receiver }
1975        }
1976
1977        async fn respond_to_messages<P: Ping, D: Dig, F: Fetch, Time: TimeProvider>(
1978            &mut self,
1979            monitor: &mut Monitor<Time>,
1980            p: P,
1981            d: D,
1982            f: F,
1983        ) {
1984            loop {
1985                if let Some((action, cookie)) = self.receiver.next().await {
1986                    match action {
1987                        NetworkCheckAction::Ping(parameters) => {
1988                            let result = p.ping(&parameters.interface_name, parameters.addr).await;
1989                            match monitor
1990                                .resume(cookie, NetworkCheckResult::Ping { parameters, result })
1991                            {
1992                                // Has reached final state.
1993                                Ok(NetworkCheckerOutcome::Complete) => return,
1994                                _ => {}
1995                            }
1996                        }
1997                        NetworkCheckAction::ResolveDns(parameters) => {
1998                            let ips = d.dig(&parameters.interface_name, &parameters.domain).await;
1999                            match monitor
2000                                .resume(cookie, NetworkCheckResult::ResolveDns { parameters, ips })
2001                            {
2002                                // Has reached final state.
2003                                Ok(NetworkCheckerOutcome::Complete) => return,
2004                                _ => {}
2005                            }
2006                        }
2007                        NetworkCheckAction::Fetch(parameters) => {
2008                            let result = f
2009                                .fetch(
2010                                    &parameters.interface_name,
2011                                    &parameters.domain,
2012                                    &parameters.path,
2013                                    &parameters.ip,
2014                                )
2015                                .await;
2016                            match monitor
2017                                .resume(cookie, NetworkCheckResult::Fetch { parameters, result })
2018                            {
2019                                // Has reached final state.
2020                                Ok(NetworkCheckerOutcome::Complete) => return,
2021                                _ => {}
2022                            }
2023                        }
2024                    }
2025                }
2026            }
2027        }
2028    }
2029
2030    fn run_network_check_partial_properties_repeated<P: Ping, D: Dig, F: Fetch>(
2031        exec: &mut fasync::TestExecutor,
2032        name: &str,
2033        interface_id: u64,
2034        routes: &RouteTable,
2035        mocks: Vec<(P, D, F)>,
2036        neighbors: Option<&InterfaceNeighborCache>,
2037        internet_ping_address: std::net::IpAddr,
2038        sleep_between: Option<zx::MonotonicDuration>,
2039    ) -> Vec<State> {
2040        let properties = &fnet_interfaces_ext::Properties {
2041            id: interface_id.try_into().expect("should be nonzero"),
2042            name: name.to_string(),
2043            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2044            online: true,
2045            addresses: Default::default(),
2046            has_default_ipv4_route: Default::default(),
2047            has_default_ipv6_route: Default::default(),
2048            port_identity_koid: Default::default(),
2049        };
2050
2051        let mock_count = mocks.len();
2052        match run_network_check_repeated(exec, properties, routes, neighbors, mocks, sleep_between)
2053        {
2054            Ok(Some(events)) => {
2055                // Implementation checks v4 and v6 connectivity concurrently, although these tests
2056                // only check for a single protocol at a time. The address being pinged determines
2057                // which protocol to use.
2058                events
2059                    .into_iter()
2060                    .map(|event| match internet_ping_address {
2061                        std::net::IpAddr::V4 { .. } => event.ipv4.state,
2062                        std::net::IpAddr::V6 { .. } => event.ipv6.state,
2063                    })
2064                    .collect()
2065            }
2066            Ok(None) => {
2067                error!("id for interface unexpectedly did not exist after network check");
2068                std::iter::repeat(LinkState::None.into()).take(mock_count).collect()
2069            }
2070            Err(e) => {
2071                error!("network check had an issue calculating state: {:?}", e);
2072                std::iter::repeat(LinkState::None.into()).take(mock_count).collect()
2073            }
2074        }
2075    }
2076
2077    fn run_network_check_partial_properties<P: Ping, D: Dig, F: Fetch>(
2078        exec: &mut fasync::TestExecutor,
2079        name: &str,
2080        interface_id: u64,
2081        routes: &RouteTable,
2082        pinger: P,
2083        digger: D,
2084        fetcher: F,
2085        neighbors: Option<&InterfaceNeighborCache>,
2086        internet_ping_address: std::net::IpAddr,
2087    ) -> State {
2088        run_network_check_partial_properties_repeated(
2089            exec,
2090            name,
2091            interface_id,
2092            routes,
2093            vec![(pinger, digger, fetcher)],
2094            neighbors,
2095            internet_ping_address,
2096            None,
2097        )
2098        .pop()
2099        .unwrap_or_else(|| {
2100            error!("network check returned no states");
2101            LinkState::None.into()
2102        })
2103    }
2104
2105    fn run_network_check_repeated<P: Ping, D: Dig, F: Fetch>(
2106        exec: &mut fasync::TestExecutor,
2107        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
2108        routes: &RouteTable,
2109        neighbors: Option<&InterfaceNeighborCache>,
2110        mocks: Vec<(P, D, F)>,
2111        sleep_between: Option<zx::MonotonicDuration>,
2112    ) -> Result<Option<Vec<IpVersions<StateEvent>>>, anyhow::Error> {
2113        let (sender, receiver) = mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
2114        let mut monitor = Monitor::new_with_time_provider(
2115            sender,
2116            FakeTime {
2117                increment: sleep_between.unwrap_or(zx::MonotonicDuration::from_nanos(10)),
2118                time: zx::MonotonicInstant::get(),
2119            },
2120        )
2121        .unwrap();
2122        let mut network_check_responder = NetworkCheckTestResponder::new(receiver);
2123
2124        let view = InterfaceView { properties, routes, neighbors };
2125        let network_check_fut = async {
2126            let mut states = Vec::new();
2127            for (pinger, digger, fetcher) in mocks {
2128                match monitor.begin(view) {
2129                    Ok(NetworkCheckerOutcome::Complete) => {}
2130                    Ok(NetworkCheckerOutcome::MustResume) => {
2131                        let () = network_check_responder
2132                            .respond_to_messages(&mut monitor, pinger, digger, fetcher)
2133                            .await;
2134                    }
2135                    Err(e) => {
2136                        error!("begin had an issue calculating state: {:?}", e)
2137                    }
2138                }
2139                states.push(monitor.state().get(properties.id.get()).map(Clone::clone));
2140            }
2141            states
2142        };
2143
2144        let mut network_check_fut = pin!(network_check_fut);
2145        match exec.run_until_stalled(&mut network_check_fut) {
2146            Poll::Ready(got) => Ok(got.into_iter().collect()),
2147            Poll::Pending => Err(anyhow::anyhow!("network_check blocked unexpectedly")),
2148        }
2149    }
2150
2151    fn run_network_check<P: Ping, D: Dig, F: Fetch>(
2152        exec: &mut fasync::TestExecutor,
2153        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
2154        routes: &RouteTable,
2155        neighbors: Option<&InterfaceNeighborCache>,
2156        pinger: P,
2157        digger: D,
2158        fetcher: F,
2159    ) -> Result<Option<IpVersions<StateEvent>>, anyhow::Error> {
2160        run_network_check_repeated(
2161            exec,
2162            properties,
2163            routes,
2164            neighbors,
2165            vec![(pinger, digger, fetcher)],
2166            None,
2167        )
2168        .map(|res| res.and_then(|mut v| v.pop()))
2169    }
2170
2171    #[test]
2172    fn test_network_check_ipv6_local_only() {
2173        let mut exec = fasync::TestExecutor::new_with_fake_time();
2174        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2175        let () = exec.set_fake_time(time.into());
2176
2177        // The next_hop of the default route must be the same as a known neighbor. This is used
2178        // to determine this neighbor as a valid gateway.
2179        let routes = testutil::build_route_table_from_flattened_routes([Route {
2180            destination: UNSPECIFIED_V6,
2181            outbound_interface: ID1,
2182            next_hop: Some(IPV6_ADDR),
2183        }]);
2184        let properties = &fnet_interfaces_ext::Properties {
2185            id: ID1.try_into().expect("should be nonzero"),
2186            name: ETHERNET_INTERFACE_NAME.to_string(),
2187            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2188            online: true,
2189            addresses: vec![],
2190            has_default_ipv4_route: false,
2191            has_default_ipv6_route: true,
2192            port_identity_koid: Default::default(),
2193        };
2194        let neighbors = InterfaceNeighborCache::default();
2195
2196        let got = run_network_check(
2197            &mut exec,
2198            properties,
2199            &routes,
2200            Some(&neighbors),
2201            FakePing::default(),
2202            FakeDig::default(),
2203            FakeFetch::default(),
2204        )
2205        .expect("run_network_check failed")
2206        .expect("interface state not found");
2207
2208        let want_ipv4 =
2209            StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2210        let want_ipv6 =
2211            StateEvent { state: State { link: LinkState::Local, ..Default::default() }, time };
2212        assert_eq!(got.ipv4, want_ipv4);
2213        assert_eq!(got.ipv6, want_ipv6);
2214    }
2215
2216    #[test]
2217    fn test_network_check_ipv6_local_only_not_default_route() {
2218        let mut exec = fasync::TestExecutor::new_with_fake_time();
2219        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2220        let () = exec.set_fake_time(time.into());
2221
2222        // The next_hop of the default route must be the same as a known neighbor. This is used
2223        // to determine this neighbor as a valid gateway.
2224        let routes = testutil::build_route_table_from_flattened_routes([Route {
2225            destination: fidl_subnet!("::/1"),
2226            outbound_interface: ID1,
2227            next_hop: Some(IPV6_ADDR),
2228        }]);
2229        let properties = &fnet_interfaces_ext::Properties {
2230            id: ID1.try_into().expect("should be nonzero"),
2231            name: ETHERNET_INTERFACE_NAME.to_string(),
2232            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2233            online: true,
2234            addresses: vec![],
2235            has_default_ipv4_route: false,
2236            has_default_ipv6_route: true,
2237            port_identity_koid: Default::default(),
2238        };
2239        let neighbors = InterfaceNeighborCache::default();
2240
2241        let got = run_network_check(
2242            &mut exec,
2243            properties,
2244            &routes,
2245            Some(&neighbors),
2246            FakePing::default(),
2247            FakeDig::default(),
2248            FakeFetch::default(),
2249        )
2250        .expect("run_network_check failed")
2251        .expect("interface state not found");
2252
2253        let want_ipv4 =
2254            StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2255        let want_ipv6 =
2256            StateEvent { state: State { link: LinkState::Local, ..Default::default() }, time };
2257        assert_eq!(got.ipv4, want_ipv4);
2258        assert_eq!(got.ipv6, want_ipv6);
2259    }
2260
2261    #[test]
2262    fn test_network_check_ipv6_gateway_only() {
2263        let mut exec = fasync::TestExecutor::new_with_fake_time();
2264        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2265        let () = exec.set_fake_time(time.into());
2266
2267        // The next_hop of the default route must be the same as a known neighbor. This is used
2268        // to determine this neighbor as a valid gateway.
2269        let routes = testutil::build_route_table_from_flattened_routes([Route {
2270            destination: UNSPECIFIED_V6,
2271            outbound_interface: ID1,
2272            next_hop: Some(IPV6_ADDR),
2273        }]);
2274        let properties = &fnet_interfaces_ext::Properties {
2275            id: ID1.try_into().expect("should be nonzero"),
2276            name: ETHERNET_INTERFACE_NAME.to_string(),
2277            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2278            online: true,
2279            addresses: vec![],
2280            has_default_ipv4_route: false,
2281            has_default_ipv6_route: true,
2282            port_identity_koid: Default::default(),
2283        };
2284        let neighbors = InterfaceNeighborCache {
2285            neighbors: [(
2286                IPV6_ADDR,
2287                NeighborState::new(NeighborHealth::Healthy {
2288                    last_observed: zx::MonotonicInstant::default(),
2289                }),
2290            )]
2291            .into_iter()
2292            .collect::<HashMap<fnet::IpAddress, NeighborState>>(),
2293        };
2294
2295        let got = run_network_check(
2296            &mut exec,
2297            properties,
2298            &routes,
2299            Some(&neighbors),
2300            FakePing::default(),
2301            FakeDig::default(),
2302            FakeFetch::default(),
2303        )
2304        .expect("run_network_check failed")
2305        .expect("interface state not found");
2306
2307        let want_ipv4 =
2308            StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2309        let want_ipv6 =
2310            StateEvent { state: State { link: LinkState::Gateway, ..Default::default() }, time };
2311        assert_eq!(got.ipv4, want_ipv4);
2312        assert_eq!(got.ipv6, want_ipv6);
2313    }
2314
2315    #[fuchsia::test]
2316    fn test_network_check_ipv4_and_ipv6_gateway() {
2317        let mut exec = fasync::TestExecutor::new_with_fake_time();
2318        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2319        let () = exec.set_fake_time(time.into());
2320
2321        // The next_hop of the default route must be the same as a known neighbor. This is used
2322        // to determine this neighbor as a valid gateway.
2323        let routes = testutil::build_route_table_from_flattened_routes([
2324            Route {
2325                destination: UNSPECIFIED_V4,
2326                outbound_interface: ID1,
2327                next_hop: Some(IPV4_ADDR),
2328            },
2329            Route {
2330                destination: UNSPECIFIED_V6,
2331                outbound_interface: ID1,
2332                next_hop: Some(IPV6_ADDR),
2333            },
2334        ]);
2335        let properties = &fnet_interfaces_ext::Properties {
2336            id: ID1.try_into().expect("should be nonzero"),
2337            name: ETHERNET_INTERFACE_NAME.to_string(),
2338            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2339            online: true,
2340            addresses: vec![],
2341            has_default_ipv4_route: true,
2342            has_default_ipv6_route: true,
2343            port_identity_koid: Default::default(),
2344        };
2345        let neighbors = InterfaceNeighborCache {
2346            neighbors: [
2347                (
2348                    IPV4_ADDR,
2349                    NeighborState::new(NeighborHealth::Healthy {
2350                        last_observed: zx::MonotonicInstant::default(),
2351                    }),
2352                ),
2353                (
2354                    IPV6_ADDR,
2355                    NeighborState::new(NeighborHealth::Healthy {
2356                        last_observed: zx::MonotonicInstant::default(),
2357                    }),
2358                ),
2359            ]
2360            .into_iter()
2361            .collect::<HashMap<fnet::IpAddress, NeighborState>>(),
2362        };
2363
2364        let got = run_network_check(
2365            &mut exec,
2366            properties,
2367            &routes,
2368            Some(&neighbors),
2369            FakePing::default(),
2370            FakeDig::default(),
2371            FakeFetch::default(),
2372        )
2373        .expect("run_network_check failed")
2374        .expect("interface state not found");
2375
2376        assert_eq!(
2377            got,
2378            IpVersions::construct(StateEvent {
2379                state: State { link: LinkState::Gateway, ..Default::default() },
2380                time
2381            })
2382        );
2383    }
2384
2385    #[test]
2386    fn test_network_check_ethernet_ipv4() {
2387        test_network_check_ethernet::<ip::Ipv4>(
2388            fidl_ip!("1.2.3.0"),
2389            fidl_ip!("1.2.3.4"),
2390            fidl_ip!("1.2.3.1"),
2391            fidl_ip!("2.2.3.0"),
2392            fidl_ip!("2.2.3.1"),
2393            UNSPECIFIED_V4,
2394            fidl_subnet!("0.0.0.0/1"),
2395            IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
2396            24,
2397        );
2398    }
2399
2400    #[test]
2401    fn test_network_check_ethernet_ipv6() {
2402        test_network_check_ethernet::<ip::Ipv6>(
2403            fidl_ip!("123::"),
2404            fidl_ip!("123::4"),
2405            fidl_ip!("123::1"),
2406            fidl_ip!("223::"),
2407            fidl_ip!("223::1"),
2408            UNSPECIFIED_V6,
2409            fidl_subnet!("::/1"),
2410            IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
2411            64,
2412        );
2413    }
2414
2415    fn test_network_check_ethernet<I: ip::Ip>(
2416        net1: fnet::IpAddress,
2417        _net1_addr: fnet::IpAddress,
2418        net1_gateway: fnet::IpAddress,
2419        net2: fnet::IpAddress,
2420        net2_gateway: fnet::IpAddress,
2421        unspecified_addr: fnet::Subnet,
2422        non_default_addr: fnet::Subnet,
2423        ping_internet_addr: std::net::IpAddr,
2424        prefix_len: u8,
2425    ) {
2426        let route_table = testutil::build_route_table_from_flattened_routes([
2427            Route {
2428                destination: unspecified_addr,
2429                outbound_interface: ID1,
2430                next_hop: Some(net1_gateway),
2431            },
2432            Route {
2433                destination: fnet::Subnet { addr: net1, prefix_len },
2434                outbound_interface: ID1,
2435                next_hop: None,
2436            },
2437        ]);
2438        let route_table_2 = testutil::build_route_table_from_flattened_routes([
2439            Route {
2440                destination: unspecified_addr,
2441                outbound_interface: ID1,
2442                next_hop: Some(net2_gateway),
2443            },
2444            Route {
2445                destination: fnet::Subnet { addr: net1, prefix_len },
2446                outbound_interface: ID1,
2447                next_hop: None,
2448            },
2449            Route {
2450                destination: fnet::Subnet { addr: net2, prefix_len },
2451                outbound_interface: ID1,
2452                next_hop: None,
2453            },
2454        ]);
2455        let route_table_3 = testutil::build_route_table_from_flattened_routes([
2456            Route {
2457                destination: unspecified_addr,
2458                outbound_interface: ID2,
2459                next_hop: Some(net1_gateway),
2460            },
2461            Route {
2462                destination: fnet::Subnet { addr: net1, prefix_len },
2463                outbound_interface: ID2,
2464                next_hop: None,
2465            },
2466        ]);
2467        let route_table_4 = testutil::build_route_table_from_flattened_routes([
2468            Route {
2469                destination: non_default_addr,
2470                outbound_interface: ID1,
2471                next_hop: Some(net1_gateway),
2472            },
2473            Route {
2474                destination: fnet::Subnet { addr: net1, prefix_len },
2475                outbound_interface: ID1,
2476                next_hop: None,
2477            },
2478        ]);
2479
2480        let fnet_ext::IpAddress(net1_gateway_ext) = net1_gateway.into();
2481        let mut exec = fasync::TestExecutor::new();
2482
2483        // TODO(fxrev.dev/120580): Extract test cases into variants/helper function
2484        assert_eq!(
2485            run_network_check_partial_properties(
2486                &mut exec,
2487                ETHERNET_INTERFACE_NAME,
2488                ID1,
2489                &route_table,
2490                FakePing {
2491                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2492                    gateway_response: true,
2493                    internet_response: true,
2494                },
2495                FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]),
2496                FakeFetch {
2497                    expected_url: Some("http://www.gstatic.com/generate_204"),
2498                    response: Some(Box::new(|| Ok(204))),
2499                },
2500                None,
2501                ping_internet_addr,
2502            ),
2503            State {
2504                link: LinkState::Internet,
2505                application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
2506            },
2507            "All is good. Can reach internet"
2508        );
2509
2510        assert_eq!(
2511            run_network_check_partial_properties(
2512                &mut exec,
2513                ETHERNET_INTERFACE_NAME,
2514                ID1,
2515                &route_table,
2516                FakePing {
2517                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2518                    gateway_response: true,
2519                    internet_response: true,
2520                },
2521                FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]),
2522                FakeFetch::default(),
2523                None,
2524                ping_internet_addr,
2525            ),
2526            State {
2527                link: LinkState::Internet,
2528                application: ApplicationState { dns_resolved: true, ..Default::default() },
2529            },
2530            "HTTP Fetch fails"
2531        );
2532
2533        assert_eq!(
2534            run_network_check_partial_properties(
2535                &mut exec,
2536                ETHERNET_INTERFACE_NAME,
2537                ID1,
2538                &route_table,
2539                FakePing {
2540                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2541                    gateway_response: true,
2542                    internet_response: true,
2543                },
2544                FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("1.2.4.0")]),
2545                FakeFetch::default(),
2546                None,
2547                ping_internet_addr,
2548            ),
2549            State {
2550                link: LinkState::Internet,
2551                application: ApplicationState {
2552                    dns_resolved: ping_internet_addr.is_ipv4(),
2553                    ..Default::default()
2554                },
2555            },
2556            "DNS Resolves only IPV4",
2557        );
2558
2559        assert_eq!(
2560            run_network_check_partial_properties(
2561                &mut exec,
2562                ETHERNET_INTERFACE_NAME,
2563                ID1,
2564                &route_table,
2565                FakePing {
2566                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2567                    gateway_response: true,
2568                    internet_response: true,
2569                },
2570                FakeDig::new(vec![std_ip!("123::"), std_ip!("124::")]),
2571                FakeFetch::default(),
2572                None,
2573                ping_internet_addr,
2574            ),
2575            State {
2576                link: LinkState::Internet,
2577                application: ApplicationState {
2578                    dns_resolved: ping_internet_addr.is_ipv6(),
2579                    ..Default::default()
2580                },
2581            },
2582            "DNS Resolves only IPV6",
2583        );
2584
2585        assert_eq!(
2586            run_network_check_partial_properties(
2587                &mut exec,
2588                ETHERNET_INTERFACE_NAME,
2589                ID1,
2590                &route_table,
2591                FakePing {
2592                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2593                    gateway_response: false,
2594                    internet_response: true,
2595                },
2596                FakeDig::default(),
2597                FakeFetch::default(),
2598                Some(&InterfaceNeighborCache {
2599                    neighbors: [(
2600                        net1_gateway,
2601                        NeighborState::new(NeighborHealth::Healthy {
2602                            last_observed: zx::MonotonicInstant::default(),
2603                        })
2604                    )]
2605                    .into_iter()
2606                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2607                }),
2608                ping_internet_addr,
2609            ),
2610            LinkState::Internet.into(),
2611            "Can reach internet, gateway responding via ARP/ND"
2612        );
2613
2614        assert_eq!(
2615            run_network_check_partial_properties(
2616                &mut exec,
2617                ETHERNET_INTERFACE_NAME,
2618                ID1,
2619                &route_table,
2620                FakePing {
2621                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2622                    gateway_response: false,
2623                    internet_response: true,
2624                },
2625                FakeDig::default(),
2626                FakeFetch::default(),
2627                Some(&InterfaceNeighborCache {
2628                    neighbors: [(
2629                        net1,
2630                        NeighborState::new(NeighborHealth::Healthy {
2631                            last_observed: zx::MonotonicInstant::default(),
2632                        })
2633                    )]
2634                    .into_iter()
2635                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2636                }),
2637                ping_internet_addr,
2638            ),
2639            LinkState::Internet.into(),
2640            "Gateway not responding via ping or ARP/ND. Can reach internet"
2641        );
2642
2643        assert_eq!(
2644            run_network_check_partial_properties(
2645                &mut exec,
2646                ETHERNET_INTERFACE_NAME,
2647                ID1,
2648                &route_table_4,
2649                FakePing {
2650                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2651                    gateway_response: true,
2652                    internet_response: true,
2653                },
2654                FakeDig::default(),
2655                FakeFetch::default(),
2656                Some(&InterfaceNeighborCache {
2657                    neighbors: [(
2658                        net1_gateway,
2659                        NeighborState::new(NeighborHealth::Healthy {
2660                            last_observed: zx::MonotonicInstant::default(),
2661                        })
2662                    )]
2663                    .into_iter()
2664                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2665                }),
2666                ping_internet_addr,
2667            ),
2668            LinkState::Internet.into(),
2669            "No default route, but healthy gateway with internet/gateway response"
2670        );
2671
2672        assert_eq!(
2673            run_network_check_partial_properties(
2674                &mut exec,
2675                ETHERNET_INTERFACE_NAME,
2676                ID1,
2677                &route_table,
2678                FakePing {
2679                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2680                    gateway_response: true,
2681                    internet_response: false,
2682                },
2683                FakeDig::default(),
2684                FakeFetch::default(),
2685                None,
2686                ping_internet_addr,
2687            ),
2688            LinkState::Gateway.into(),
2689            "Can reach gateway via ping"
2690        );
2691
2692        assert_eq!(
2693            run_network_check_partial_properties(
2694                &mut exec,
2695                ETHERNET_INTERFACE_NAME,
2696                ID1,
2697                &route_table,
2698                FakePing::default(),
2699                FakeDig::default(),
2700                FakeFetch::default(),
2701                Some(&InterfaceNeighborCache {
2702                    neighbors: [(
2703                        net1_gateway,
2704                        NeighborState::new(NeighborHealth::Healthy {
2705                            last_observed: zx::MonotonicInstant::default(),
2706                        })
2707                    )]
2708                    .into_iter()
2709                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2710                }),
2711                ping_internet_addr,
2712            ),
2713            LinkState::Gateway.into(),
2714            "Can reach gateway via ARP/ND"
2715        );
2716
2717        assert_eq!(
2718            run_network_check_partial_properties(
2719                &mut exec,
2720                ETHERNET_INTERFACE_NAME,
2721                ID1,
2722                &route_table,
2723                FakePing {
2724                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2725                    gateway_response: false,
2726                    internet_response: false,
2727                },
2728                FakeDig::default(),
2729                FakeFetch::default(),
2730                None,
2731                ping_internet_addr,
2732            ),
2733            LinkState::Local.into(),
2734            "Local only, Cannot reach gateway"
2735        );
2736
2737        assert_eq!(
2738            run_network_check_partial_properties(
2739                &mut exec,
2740                ETHERNET_INTERFACE_NAME,
2741                ID1,
2742                &route_table_2,
2743                FakePing::default(),
2744                FakeDig::default(),
2745                FakeFetch::default(),
2746                None,
2747                ping_internet_addr,
2748            ),
2749            LinkState::Local.into(),
2750            "No default route"
2751        );
2752
2753        assert_eq!(
2754            run_network_check_partial_properties(
2755                &mut exec,
2756                ETHERNET_INTERFACE_NAME,
2757                ID1,
2758                &route_table_4,
2759                FakePing {
2760                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2761                    gateway_response: true,
2762                    internet_response: false,
2763                },
2764                FakeDig::default(),
2765                FakeFetch::default(),
2766                None,
2767                ping_internet_addr,
2768            ),
2769            LinkState::Local.into(),
2770            "No default route, with only gateway response"
2771        );
2772
2773        assert_eq!(
2774            run_network_check_partial_properties(
2775                &mut exec,
2776                ETHERNET_INTERFACE_NAME,
2777                ID1,
2778                &route_table_2,
2779                FakePing::default(),
2780                FakeDig::default(),
2781                FakeFetch::default(),
2782                Some(&InterfaceNeighborCache {
2783                    neighbors: [(
2784                        net1,
2785                        NeighborState::new(NeighborHealth::Healthy {
2786                            last_observed: zx::MonotonicInstant::default(),
2787                        })
2788                    )]
2789                    .into_iter()
2790                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2791                }),
2792                ping_internet_addr,
2793            ),
2794            LinkState::Local.into(),
2795            "Local only, neighbors responsive with no default route"
2796        );
2797
2798        assert_eq!(
2799            run_network_check_partial_properties(
2800                &mut exec,
2801                ETHERNET_INTERFACE_NAME,
2802                ID1,
2803                &route_table,
2804                FakePing::default(),
2805                FakeDig::default(),
2806                FakeFetch::default(),
2807                Some(&InterfaceNeighborCache {
2808                    neighbors: [(
2809                        net1,
2810                        NeighborState::new(NeighborHealth::Healthy {
2811                            last_observed: zx::MonotonicInstant::default(),
2812                        })
2813                    )]
2814                    .into_iter()
2815                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2816                }),
2817                ping_internet_addr
2818            ),
2819            LinkState::Local.into(),
2820            "Local only, neighbors responsive with a default route"
2821        );
2822
2823        assert_eq!(
2824            run_network_check_partial_properties(
2825                &mut exec,
2826                ETHERNET_INTERFACE_NAME,
2827                ID1,
2828                &route_table_3,
2829                FakePing::default(),
2830                FakeDig::default(),
2831                FakeFetch::default(),
2832                Some(&InterfaceNeighborCache {
2833                    neighbors: [(
2834                        net1,
2835                        NeighborState::new(NeighborHealth::Healthy {
2836                            last_observed: zx::MonotonicInstant::default(),
2837                        })
2838                    )]
2839                    .into_iter()
2840                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2841                }),
2842                ping_internet_addr,
2843            ),
2844            LinkState::Local.into(),
2845            "Local only, neighbors responsive with no routes"
2846        );
2847
2848        assert_eq!(
2849            run_network_check_partial_properties(
2850                &mut exec,
2851                ETHERNET_INTERFACE_NAME,
2852                ID1,
2853                &route_table,
2854                FakePing::default(),
2855                FakeDig::default(),
2856                FakeFetch::default(),
2857                Some(&InterfaceNeighborCache {
2858                    neighbors: [
2859                        (
2860                            net1,
2861                            NeighborState::new(NeighborHealth::Healthy {
2862                                last_observed: zx::MonotonicInstant::default(),
2863                            })
2864                        ),
2865                        (
2866                            net1_gateway,
2867                            NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None })
2868                        )
2869                    ]
2870                    .into_iter()
2871                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2872                }),
2873                ping_internet_addr,
2874            ),
2875            LinkState::Local.into(),
2876            "Local only, gateway unhealthy with healthy neighbor"
2877        );
2878
2879        assert_eq!(
2880            run_network_check_partial_properties(
2881                &mut exec,
2882                ETHERNET_INTERFACE_NAME,
2883                ID1,
2884                &route_table_3,
2885                FakePing::default(),
2886                FakeDig::default(),
2887                FakeFetch::default(),
2888                Some(&InterfaceNeighborCache {
2889                    neighbors: [(
2890                        net1_gateway,
2891                        NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None })
2892                    )]
2893                    .into_iter()
2894                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2895                }),
2896                ping_internet_addr,
2897            ),
2898            LinkState::Up.into(),
2899            "No routes and unhealthy gateway"
2900        );
2901
2902        assert_eq!(
2903            run_network_check_partial_properties(
2904                &mut exec,
2905                ETHERNET_INTERFACE_NAME,
2906                ID1,
2907                &route_table_3,
2908                FakePing::default(),
2909                FakeDig::default(),
2910                FakeFetch::default(),
2911                None,
2912                ping_internet_addr,
2913            ),
2914            LinkState::Up.into(),
2915            "No routes",
2916        );
2917
2918        assert_eq!(
2919            run_network_check_partial_properties_repeated(
2920                &mut exec,
2921                ETHERNET_INTERFACE_NAME,
2922                ID1,
2923                &route_table,
2924                vec![
2925                    (
2926                        FakePing {
2927                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2928                            gateway_response: true,
2929                            internet_response: true,
2930                        },
2931                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
2932                        FakeFetch {
2933                            expected_url: Some("http://www.gstatic.com/generate_204"),
2934                            response: Some(Box::new(|| Ok(204))),
2935                        },
2936                    ),
2937                    (
2938                        FakePing {
2939                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2940                            gateway_response: true,
2941                            internet_response: true,
2942                        },
2943                        FakeDig { response: None }, // Then, use one that fails
2944                        FakeFetch {
2945                            expected_url: Some("http://www.gstatic.com/generate_204"),
2946                            response: Some(Box::new(|| Ok(204))),
2947                        },
2948                    ),
2949                ],
2950                None,
2951                ping_internet_addr,
2952                None,
2953            ),
2954            vec![
2955                State {
2956                    link: LinkState::Internet,
2957                    application: ApplicationState {
2958                        dns_resolved: true,
2959                        http_fetch_succeeded: true
2960                    }
2961                },
2962                State {
2963                    link: LinkState::Internet,
2964                    application: ApplicationState {
2965                        dns_resolved: true,
2966                        http_fetch_succeeded: true
2967                    }
2968                }
2969            ],
2970            "Fail DNS on second check; fetch succeeds; no pause"
2971        );
2972
2973        assert_eq!(
2974            run_network_check_partial_properties_repeated(
2975                &mut exec,
2976                ETHERNET_INTERFACE_NAME,
2977                ID1,
2978                &route_table,
2979                vec![
2980                    (
2981                        FakePing {
2982                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2983                            gateway_response: true,
2984                            internet_response: true,
2985                        },
2986                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
2987                        FakeFetch {
2988                            expected_url: Some("http://www.gstatic.com/generate_204"),
2989                            response: Some(Box::new(|| Ok(204))),
2990                        },
2991                    ),
2992                    (
2993                        FakePing {
2994                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2995                            gateway_response: true,
2996                            internet_response: true,
2997                        },
2998                        FakeDig { response: None }, // Then, use one that fails
2999                        FakeFetch {
3000                            expected_url: Some("http://www.gstatic.com/generate_204"),
3001                            response: Some(Box::new(|| Ok(204))),
3002                        },
3003                    ),
3004                ],
3005                None,
3006                ping_internet_addr,
3007                Some(DNS_PROBE_PERIOD),
3008            ),
3009            vec![
3010                State {
3011                    link: LinkState::Internet,
3012                    application: ApplicationState {
3013                        dns_resolved: true,
3014                        http_fetch_succeeded: true
3015                    }
3016                },
3017                State {
3018                    link: LinkState::Internet,
3019                    application: ApplicationState {
3020                        dns_resolved: false,
3021                        http_fetch_succeeded: true
3022                    }
3023                }
3024            ],
3025            "Fail DNS on second check; fetch succeeds"
3026        );
3027
3028        assert_eq!(
3029            run_network_check_partial_properties_repeated(
3030                &mut exec,
3031                ETHERNET_INTERFACE_NAME,
3032                ID1,
3033                &route_table,
3034                vec![
3035                    (
3036                        FakePing {
3037                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3038                            gateway_response: true,
3039                            internet_response: true,
3040                        },
3041                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
3042                        FakeFetch {
3043                            expected_url: Some("http://www.gstatic.com/generate_204"),
3044                            response: Some(Box::new(|| Err(
3045                                fetch::FetchError::ReadTcpStreamTimeout
3046                            ))),
3047                        },
3048                    ),
3049                    (
3050                        FakePing {
3051                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3052                            gateway_response: true,
3053                            internet_response: true,
3054                        },
3055                        FakeDig { response: None }, // Then, use one that fails
3056                        FakeFetch {
3057                            expected_url: Some("http://www.gstatic.com/generate_204"),
3058                            response: Some(Box::new(|| Err(
3059                                fetch::FetchError::ReadTcpStreamTimeout
3060                            ))),
3061                        },
3062                    ),
3063                ],
3064                None,
3065                ping_internet_addr,
3066                None,
3067            ),
3068            vec![
3069                State {
3070                    link: LinkState::Internet,
3071                    application: ApplicationState { dns_resolved: true, ..Default::default() }
3072                },
3073                State {
3074                    link: LinkState::Internet,
3075                    application: ApplicationState { dns_resolved: true, ..Default::default() }
3076                }
3077            ],
3078            "Fail DNS on second check; fetch fails; no pause"
3079        );
3080
3081        assert_eq!(
3082            run_network_check_partial_properties_repeated(
3083                &mut exec,
3084                ETHERNET_INTERFACE_NAME,
3085                ID1,
3086                &route_table,
3087                vec![
3088                    (
3089                        FakePing {
3090                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3091                            gateway_response: true,
3092                            internet_response: true,
3093                        },
3094                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
3095                        FakeFetch {
3096                            expected_url: Some("http://www.gstatic.com/generate_204"),
3097                            response: Some(Box::new(|| Err(
3098                                fetch::FetchError::ReadTcpStreamTimeout
3099                            ))),
3100                        },
3101                    ),
3102                    (
3103                        FakePing {
3104                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
3105                            gateway_response: true,
3106                            internet_response: true,
3107                        },
3108                        FakeDig { response: None }, // Then, use one that fails
3109                        FakeFetch {
3110                            expected_url: Some("http://www.gstatic.com/generate_204"),
3111                            response: Some(Box::new(|| Err(
3112                                fetch::FetchError::ReadTcpStreamTimeout
3113                            ))),
3114                        },
3115                    ),
3116                ],
3117                None,
3118                ping_internet_addr,
3119                Some(DNS_PROBE_PERIOD),
3120            ),
3121            vec![
3122                State {
3123                    link: LinkState::Internet,
3124                    application: ApplicationState { dns_resolved: true, ..Default::default() }
3125                },
3126                State {
3127                    link: LinkState::Internet,
3128                    application: ApplicationState { dns_resolved: false, ..Default::default() }
3129                }
3130            ],
3131            "Fail DNS on second check; fetch fails"
3132        );
3133    }
3134
3135    #[test]
3136    fn test_network_check_varying_properties() {
3137        let properties = fnet_interfaces_ext::Properties {
3138            id: ID1.try_into().expect("should be nonzero"),
3139            name: ETHERNET_INTERFACE_NAME.to_string(),
3140            port_class: fnet_interfaces_ext::PortClass::Ethernet,
3141            has_default_ipv4_route: true,
3142            has_default_ipv6_route: true,
3143            online: true,
3144            addresses: vec![
3145                fnet_interfaces_ext::Address {
3146                    addr: fidl_subnet!("1.2.3.0/24"),
3147                    valid_until: fnet_interfaces_ext::NoInterest,
3148                    preferred_lifetime_info: fnet_interfaces_ext::NoInterest,
3149                    assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
3150                },
3151                fnet_interfaces_ext::Address {
3152                    addr: fidl_subnet!("123::4/64"),
3153                    valid_until: fnet_interfaces_ext::NoInterest,
3154                    preferred_lifetime_info: fnet_interfaces_ext::NoInterest,
3155                    assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
3156                },
3157            ],
3158            port_identity_koid: Default::default(),
3159        };
3160        let local_routes = testutil::build_route_table_from_flattened_routes([
3161            Route {
3162                destination: fidl_subnet!("1.2.3.0/24"),
3163                outbound_interface: ID1,
3164                next_hop: None,
3165            },
3166            Route {
3167                destination: fidl_subnet!("123::/64"),
3168                outbound_interface: ID1,
3169                next_hop: None,
3170            },
3171        ]);
3172        let route_table = testutil::build_route_table_from_flattened_routes([
3173            Route {
3174                destination: fidl_subnet!("0.0.0.0/0"),
3175                outbound_interface: ID1,
3176                next_hop: Some(fidl_ip!("1.2.3.1")),
3177            },
3178            Route {
3179                destination: fidl_subnet!("::0/0"),
3180                outbound_interface: ID1,
3181                next_hop: Some(fidl_ip!("123::1")),
3182            },
3183        ]);
3184        let route_table2 = testutil::build_route_table_from_flattened_routes([
3185            Route {
3186                destination: fidl_subnet!("0.0.0.0/0"),
3187                outbound_interface: ID1,
3188                next_hop: Some(fidl_ip!("2.2.3.1")),
3189            },
3190            Route {
3191                destination: fidl_subnet!("::0/0"),
3192                outbound_interface: ID1,
3193                next_hop: Some(fidl_ip!("223::1")),
3194            },
3195        ]);
3196
3197        const NON_ETHERNET_INTERFACE_NAME: &str = "test01";
3198
3199        let mut exec = fasync::TestExecutor::new_with_fake_time();
3200        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
3201        let () = exec.set_fake_time(time.into());
3202
3203        let got = run_network_check(
3204            &mut exec,
3205            &fnet_interfaces_ext::Properties {
3206                id: ID1.try_into().expect("should be nonzero"),
3207                name: NON_ETHERNET_INTERFACE_NAME.to_string(),
3208                port_class: fnet_interfaces_ext::PortClass::Virtual,
3209                online: false,
3210                has_default_ipv4_route: false,
3211                has_default_ipv6_route: false,
3212                addresses: vec![],
3213                port_identity_koid: Default::default(),
3214            },
3215            &Default::default(),
3216            None,
3217            FakePing::default(),
3218            FakeDig::default(),
3219            FakeFetch::default(),
3220        )
3221        .expect(
3222            "error calling network check with non-ethernet interface, no addresses, interface down",
3223        );
3224        assert_eq!(
3225            got,
3226            Some(IpVersions::construct(StateEvent {
3227                state: State { link: LinkState::Down, ..Default::default() },
3228                time
3229            }))
3230        );
3231
3232        let got = run_network_check(
3233            &mut exec,
3234            &fnet_interfaces_ext::Properties { online: false, ..properties.clone() },
3235            &Default::default(),
3236            None,
3237            FakePing::default(),
3238            FakeDig::default(),
3239            FakeFetch::default(),
3240        )
3241        .expect("error calling network check, want Down state");
3242        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3243            state: State { link: LinkState::Down, ..Default::default() },
3244            time,
3245        }));
3246        assert_eq!(got, want);
3247
3248        let got = run_network_check(
3249            &mut exec,
3250            &fnet_interfaces_ext::Properties {
3251                has_default_ipv4_route: false,
3252                has_default_ipv6_route: false,
3253                ..properties.clone()
3254            },
3255            &local_routes,
3256            None,
3257            FakePing::default(),
3258            FakeDig::default(),
3259            FakeFetch::default(),
3260        )
3261        .expect("error calling network check, want Local state due to no default routes");
3262        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3263            state: State { link: LinkState::Local, ..Default::default() },
3264            time,
3265        }));
3266        assert_eq!(got, want);
3267
3268        let got = run_network_check(
3269            &mut exec,
3270            &properties,
3271            &route_table2,
3272            None,
3273            FakePing::default(),
3274            FakeDig::default(),
3275            FakeFetch::default(),
3276        )
3277        .expect("error calling network check, want Local state due to no matching default route");
3278        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3279            state: State { link: LinkState::Local, ..Default::default() },
3280            time,
3281        }));
3282        assert_eq!(got, want);
3283
3284        let got = run_network_check(
3285            &mut exec,
3286            &properties,
3287            &route_table,
3288            None,
3289            FakePing {
3290                gateway_addrs: [std_ip!("1.2.3.1"), std_ip!("123::1")].into_iter().collect(),
3291                gateway_response: true,
3292                internet_response: false,
3293            },
3294            FakeDig::default(),
3295            FakeFetch::default(),
3296        )
3297        .expect("error calling network check, want Gateway state");
3298        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3299            state: State { link: LinkState::Gateway, ..Default::default() },
3300            time,
3301        }));
3302        assert_eq!(got, want);
3303
3304        let got = run_network_check(
3305            &mut exec,
3306            &properties,
3307            &route_table,
3308            None,
3309            FakePing {
3310                gateway_addrs: [std_ip!("1.2.3.1"), std_ip!("123::1")].into_iter().collect(),
3311                gateway_response: true,
3312                internet_response: true,
3313            },
3314            FakeDig::default(),
3315            FakeFetch::default(),
3316        )
3317        .expect("error calling network check, want Internet state");
3318        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3319            state: State { link: LinkState::Internet, ..Default::default() },
3320            time,
3321        }));
3322        assert_eq!(got, want);
3323    }
3324
3325    fn update_delta(port: Delta<StateEvent>, system: Delta<SystemState>) -> StateDelta {
3326        StateDelta {
3327            port: IpVersions { ipv4: port.clone(), ipv6: port },
3328            system: IpVersions { ipv4: system.clone(), ipv6: system },
3329        }
3330    }
3331
3332    #[test]
3333    fn test_state_info_update() {
3334        let if1_local_event = StateEvent::construct(LinkState::Local);
3335        let if1_local = IpVersions::<StateEvent>::construct(if1_local_event);
3336        // Post-update the system state should be Local due to interface 1.
3337        let mut state = StateInfo::default();
3338        let want = update_delta(
3339            Delta { previous: None, current: if1_local_event },
3340            Delta { previous: None, current: SystemState { id: ID1, state: if1_local_event } },
3341        );
3342        assert_eq!(state.update(ID1, if1_local.clone()), want);
3343        let want_state = StateInfo {
3344            per_interface: std::iter::once((ID1, if1_local.clone())).collect::<HashMap<_, _>>(),
3345            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3346        };
3347        assert_eq!(state, want_state);
3348
3349        let if2_gateway_event = StateEvent::construct(LinkState::Gateway);
3350        let if2_gateway = IpVersions::<StateEvent>::construct(if2_gateway_event);
3351        // Pre-update, the system state is Local due to interface 1; post-update the system state
3352        // will be Gateway due to interface 2.
3353        let want = update_delta(
3354            Delta { previous: None, current: if2_gateway_event },
3355            Delta {
3356                previous: Some(SystemState { id: ID1, state: if1_local_event }),
3357                current: SystemState { id: ID2, state: if2_gateway_event },
3358            },
3359        );
3360        assert_eq!(state.update(ID2, if2_gateway.clone()), want);
3361        let want_state = StateInfo {
3362            per_interface: [(ID1, if1_local.clone()), (ID2, if2_gateway.clone())]
3363                .into_iter()
3364                .collect::<HashMap<_, _>>(),
3365            system: IpVersions { ipv4: Some(ID2), ipv6: Some(ID2) },
3366        };
3367        assert_eq!(state, want_state);
3368
3369        let if2_removed_event = StateEvent::construct(LinkState::Removed);
3370        let if2_removed = IpVersions::<StateEvent>::construct(if2_removed_event);
3371        // Pre-update, the system state is Gateway due to interface 2; post-update the system state
3372        // will be Local due to interface 1.
3373        let want = update_delta(
3374            Delta { previous: Some(if2_gateway_event), current: if2_removed_event },
3375            Delta {
3376                previous: Some(SystemState { id: ID2, state: if2_gateway_event }),
3377                current: SystemState { id: ID1, state: if1_local_event },
3378            },
3379        );
3380        assert_eq!(state.update(ID2, if2_removed.clone()), want);
3381        let want_state = StateInfo {
3382            per_interface: [(ID1, if1_local.clone()), (ID2, if2_removed.clone())]
3383                .into_iter()
3384                .collect::<HashMap<_, _>>(),
3385            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3386        };
3387        assert_eq!(state, want_state);
3388    }
3389
3390    // Regression test against https://fxbug.dev/439597080
3391    // Confirm that a new event with the same state as the current system state does not change
3392    // the id of the system state value.
3393    #[test]
3394    fn test_state_info_update_same_link_state() {
3395        let if_local_event = StateEvent::construct(LinkState::Local);
3396        let if_local = IpVersions::<StateEvent>::construct(if_local_event);
3397        // Post-update the system state should be Local due to interface 1.
3398        let mut state = StateInfo::default();
3399        let want = update_delta(
3400            Delta { previous: None, current: if_local_event },
3401            Delta { previous: None, current: SystemState { id: ID1, state: if_local_event } },
3402        );
3403        assert_eq!(state.update(ID1, if_local.clone()), want);
3404        let want_state = StateInfo {
3405            per_interface: std::iter::once((ID1, if_local.clone())).collect::<HashMap<_, _>>(),
3406            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3407        };
3408        assert_eq!(state, want_state);
3409
3410        // Post-update the system state should be the same due to the interface 2 having the
3411        // same state.
3412        let want = update_delta(
3413            Delta { previous: None, current: if_local_event },
3414            Delta {
3415                previous: Some(SystemState { id: ID1, state: if_local_event }),
3416                current: SystemState { id: ID1, state: if_local_event },
3417            },
3418        );
3419        assert_eq!(state.update(ID2, if_local.clone()), want);
3420        let want_state = StateInfo {
3421            per_interface: [(ID1, if_local.clone()), (ID2, if_local.clone())]
3422                .into_iter()
3423                .collect::<HashMap<_, _>>(),
3424            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3425        };
3426        assert_eq!(state, want_state);
3427
3428        // Post-update the system state should reflect interface 2 having the system state since
3429        // interface 1 is now at a strictly worse state.
3430        let if_removed_event = StateEvent::construct(LinkState::Removed);
3431        let if_removed = IpVersions::<StateEvent>::construct(if_removed_event);
3432        let want = update_delta(
3433            Delta { previous: Some(if_local_event), current: if_removed_event },
3434            Delta {
3435                previous: Some(SystemState { id: ID1, state: if_local_event }),
3436                current: SystemState { id: ID2, state: if_local_event },
3437            },
3438        );
3439        assert_eq!(state.update(ID1, if_removed.clone()), want);
3440        let want_state = StateInfo {
3441            per_interface: [(ID1, if_removed.clone()), (ID2, if_local.clone())]
3442                .into_iter()
3443                .collect::<HashMap<_, _>>(),
3444            system: IpVersions { ipv4: Some(ID2), ipv6: Some(ID2) },
3445        };
3446        assert_eq!(state, want_state);
3447    }
3448
3449    #[test_case(None::<LinkState>, None::<LinkState>, false, false, false, false;
3450        "no interfaces available")]
3451    #[test_case(Some(LinkState::Local), Some(LinkState::Local), false, false, false, false;
3452        "no interfaces with gateway or internet state")]
3453    #[test_case(Some(LinkState::Local), Some(LinkState::Gateway), false, false, false, true;
3454        "only one interface with gateway state or above")]
3455    #[test_case(Some(LinkState::Local), Some(LinkState::Internet), false, false, true, true;
3456        "only one interface with internet state")]
3457    #[test_case(Some(LinkState::Internet), Some(LinkState::Internet), false, false, true, true;
3458        "all interfaces with internet")]
3459    #[test_case(Some(LinkState::Internet), None::<LinkState>, false, false, true, true;
3460        "only one interface available, has internet state")]
3461    #[test_case(Some(LinkState::Local), Some((LinkState::Internet, true, false)), false, true, true, true;
3462        "only one interface with DNS resolved state")]
3463    #[test_case(Some((LinkState::Internet, true, false)), Some((LinkState::Internet, true, false)), false, true, true, true;
3464        "all interfaces with DNS resolved state")]
3465    #[test_case(Some((LinkState::Internet, true, false)), None::<LinkState>, false, true, true, true;
3466        "only one interface available, has DNS resolved state")]
3467    #[test_case(Some(LinkState::Local), Some((LinkState::Internet, true, true)), true, true, true, true;
3468        "only one interface with HTTP resolved state")]
3469    #[test_case(Some((LinkState::Internet, true, true)), Some((LinkState::Internet, true, true)), true, true, true, true;
3470        "all interfaces with HTTP resolved state")]
3471    #[test_case(Some((LinkState::Internet, true, true)), None::<LinkState>, true, true, true, true;
3472        "only one interface available, has HTTP resolved state")]
3473    #[test_case(Some((LinkState::Internet, false, true)), None::<LinkState>, true, false, true, true;
3474        "only one interface available, has HTTP resolved state, but no DNS")]
3475    fn test_system_has_state<S1, S2>(
3476        ipv4_state: Option<S1>,
3477        ipv6_state: Option<S2>,
3478        expect_http: bool,
3479        expect_dns: bool,
3480        expect_internet: bool,
3481        expect_gateway: bool,
3482    ) where
3483        StateEvent: Construct<S1>,
3484        StateEvent: Construct<S2>,
3485    {
3486        let if1 = ipv4_state
3487            .map(|state| IpVersions::<StateEvent>::construct(StateEvent::construct(state)));
3488        let if2 = ipv6_state
3489            .map(|state| IpVersions::<StateEvent>::construct(StateEvent::construct(state)));
3490
3491        let mut system_interfaces: HashMap<u64, IpVersions<StateEvent>> = HashMap::new();
3492
3493        let system_interface_ipv4 = if1.map(|interface| {
3494            let _ = system_interfaces.insert(ID1, interface);
3495            ID1
3496        });
3497
3498        let system_interface_ipv6 = if2.map(|interface| {
3499            let _ = system_interfaces.insert(ID2, interface);
3500            ID2
3501        });
3502
3503        let state = StateInfo {
3504            per_interface: system_interfaces,
3505            system: IpVersions { ipv4: system_interface_ipv4, ipv6: system_interface_ipv6 },
3506        };
3507
3508        assert_eq!(state.system_has_http(), expect_http);
3509        assert_eq!(state.system_has_dns(), expect_dns);
3510        assert_eq!(state.system_has_internet(), expect_internet);
3511        assert_eq!(state.system_has_gateway(), expect_gateway);
3512    }
3513
3514    #[test]
3515    fn test_resume_after_interface_removed() {
3516        use assert_matches::assert_matches;
3517
3518        let _exec = fasync::TestExecutor::new();
3519        let (sender, _receiver) = mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
3520        let mut monitor: Monitor<MonotonicInstant> = Monitor::new(sender).unwrap();
3521
3522        let properties = fnet_interfaces_ext::Properties {
3523            id: ID1.try_into().expect("should be nonzero"),
3524            name: ETHERNET_INTERFACE_NAME.to_string(),
3525            port_class: fnet_interfaces_ext::PortClass::Ethernet,
3526            online: false,
3527            addresses: vec![],
3528            has_default_ipv4_route: false,
3529            has_default_ipv6_route: false,
3530            port_identity_koid: Default::default(),
3531        };
3532
3533        // Insert a placeholder state so that the interface is tracked.
3534        let initial_state = IpVersions {
3535            ipv4: StateEvent {
3536                state: State { link: LinkState::None, ..Default::default() },
3537                time: fasync::MonotonicInstant::now(),
3538            },
3539            ipv6: StateEvent {
3540                state: State { link: LinkState::None, ..Default::default() },
3541                time: fasync::MonotonicInstant::now(),
3542            },
3543        };
3544        monitor.update_state(ID1, ETHERNET_INTERFACE_NAME, initial_state);
3545
3546        // Remove the interface. All future updates involving this interface should cause no
3547        // change to the interface's state.
3548        monitor.handle_interface_removed(properties.clone());
3549
3550        // Assert that the state is now `Removed`.
3551        let removed_state = monitor.state().get(ID1).unwrap();
3552        assert_eq!(removed_state.ipv4.state.link, LinkState::Removed);
3553        assert_eq!(removed_state.ipv6.state.link, LinkState::Removed);
3554
3555        // Start another iteration of the network check to ensure that any future state updates
3556        // do not affect the `Removed` state. In practice, the network check may be in-progress
3557        // when a removal event is received. That new state should not override the
3558        // `Removed` state.
3559        let routes = testutil::build_route_table_from_flattened_routes([]);
3560        let view = InterfaceView { properties: &properties, routes: &routes, neighbors: None };
3561        assert_matches!(monitor.begin(view), Ok(NetworkCheckerOutcome::Complete));
3562
3563        // Confirm that the LinkState discovered from the network check was `Down` and
3564        // not `Removed`.
3565        let interface_context = monitor.interface_context.get(&ID1).unwrap();
3566        assert_matches!(interface_context.discovered_state.ipv4.link, LinkState::Down);
3567        assert_matches!(interface_context.discovered_state.ipv6.link, LinkState::Down);
3568
3569        // Assert that the state is still `Removed`, and was not updated to `Down`
3570        // by the completed network check's result.
3571        let final_state = monitor.state().get(ID1).unwrap();
3572        assert_eq!(final_state.ipv4.state.link, LinkState::Removed);
3573        assert_eq!(final_state.ipv6.state.link, LinkState::Removed);
3574    }
3575
3576    #[test]
3577    fn test_ping_and_fetch_telemetry_events_sent() {
3578        let mut exec = fasync::TestExecutor::new_with_fake_time();
3579        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
3580        let () = exec.set_fake_time(time.into());
3581
3582        let (action_sender, action_receiver) = mpsc::unbounded();
3583        let mut monitor = Monitor::new_with_time_provider(
3584            action_sender,
3585            FakeTime {
3586                increment: zx::MonotonicDuration::from_nanos(10),
3587                time: zx::MonotonicInstant::get(),
3588            },
3589        )
3590        .unwrap();
3591
3592        let (telemetry_tx, mut telemetry_rx) = mpsc::channel(100);
3593        monitor.set_telemetry_sender(TelemetrySender::new(telemetry_tx));
3594
3595        let properties = &fnet_interfaces_ext::Properties {
3596            id: ID1.try_into().unwrap(),
3597            name: ETHERNET_INTERFACE_NAME.to_string(),
3598            port_class: fnet_interfaces_ext::PortClass::Ethernet,
3599            online: true,
3600            addresses: vec![],
3601            has_default_ipv4_route: true,
3602            has_default_ipv6_route: false,
3603            port_identity_koid: Default::default(),
3604        };
3605
3606        let net_gateway = fidl_ip!("192.168.0.254");
3607        let net_gateway_std = std_ip!("192.168.0.254");
3608
3609        // Needs to have at least a default route so it attempts internet test
3610        let routes = testutil::build_route_table_from_flattened_routes([Route {
3611            destination: UNSPECIFIED_V4,
3612            outbound_interface: ID1,
3613            next_hop: Some(net_gateway),
3614        }]);
3615
3616        // Setup neighbors for gateway caching
3617        let neighbors_map = [(
3618            net_gateway,
3619            NeighborState::new(NeighborHealth::Healthy {
3620                last_observed: zx::MonotonicInstant::default(),
3621            }),
3622        )]
3623        .into_iter()
3624        .collect::<HashMap<fnet::IpAddress, NeighborState>>();
3625        let neighbors = InterfaceNeighborCache { neighbors: neighbors_map };
3626
3627        let view = InterfaceView { properties, routes: &routes, neighbors: Some(&neighbors) };
3628
3629        let mut network_check_responder = NetworkCheckTestResponder::new(action_receiver);
3630
3631        let pinger = FakePing {
3632            gateway_addrs: vec![net_gateway_std].into_iter().collect(),
3633            gateway_response: true,
3634            internet_response: true,
3635        };
3636        let digger = FakeDig::new(vec![std_ip!("1.2.3.0")]);
3637        let fetcher = FakeFetch::default();
3638
3639        let network_check_fut = async {
3640            match monitor.begin(view) {
3641                Ok(NetworkCheckerOutcome::MustResume) => {
3642                    let () = network_check_responder
3643                        .respond_to_messages(&mut monitor, pinger, digger, fetcher)
3644                        .await;
3645                }
3646                _ => panic!("Expected MustResume"),
3647            }
3648        };
3649
3650        let mut network_check_fut = pin!(network_check_fut);
3651        match exec.run_until_stalled(&mut network_check_fut) {
3652            Poll::Ready(()) => {}
3653            Poll::Pending => panic!("network_check blocked unexpectedly"),
3654        }
3655
3656        let mut events = Vec::new();
3657        while let Poll::Ready(Some(event)) = exec.run_until_stalled(&mut telemetry_rx.next()) {
3658            events.push(event);
3659        }
3660
3661        let has_gateway =
3662            events.iter().any(|e| matches!(e, TelemetryEvent::GatewayPingResult { .. }));
3663        let has_internet =
3664            events.iter().any(|e| matches!(e, TelemetryEvent::InternetPingResult { .. }));
3665        let has_fetch = events.iter().any(|e| matches!(e, TelemetryEvent::FetchResult { .. }));
3666
3667        assert!(has_gateway, "Expected GatewayPingResult telemetry event");
3668        assert!(has_internet, "Expected InternetPingResult telemetry event");
3669        assert!(has_fetch, "Expected FetchResult telemetry event");
3670    }
3671}