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