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                    port_identity_koid: _,
1152                },
1153            routes,
1154            neighbors,
1155        }: InterfaceView<'_>,
1156    ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
1157        let id = Id::from(id);
1158        // Check to see if the current interface view is already in the map. If its state is not
1159        // Idle then another network check for the interface is already processing. In this case,
1160        // drop the `begin` request and log it.
1161        // It is expected for this to occur when an interface is experiencing many events in a
1162        // short period of time, for example changing between online and offline multiple times
1163        // over the span of a few seconds. It is safe that this happens, as the system is
1164        // eventually consistent.
1165        let telemetry_context = TelemetryContext::new(
1166            port_class,
1167            &addresses,
1168            has_default_ipv4_route,
1169            has_default_ipv6_route,
1170        );
1171        let ctx = self
1172            .interface_context
1173            .entry(id)
1174            .or_insert_with(|| NetworkCheckContext::from(telemetry_context.clone()));
1175
1176        match ctx.checker_state {
1177            NetworkCheckState::Begin => {}
1178            NetworkCheckState::Idle => {
1179                let mut new_ctx = NetworkCheckContext::default();
1180                // Copy persistent context context between passes
1181                std::mem::swap(&mut new_ctx.persistent_context, &mut ctx.persistent_context);
1182                // The telemetry should be updated based on the Properties passed into `begin`.
1183                new_ctx.persistent_context.telemetry = telemetry_context;
1184                *ctx = new_ctx;
1185            }
1186            NetworkCheckState::PingGateway
1187            | NetworkCheckState::PingInternet
1188            | NetworkCheckState::FetchHttp
1189            | NetworkCheckState::ResolveDns => {
1190                // Update the Properties for the TelemetryContext so that the LinkProperties can
1191                // be reported properly.
1192                ctx.persistent_context.telemetry = telemetry_context;
1193                return Err(anyhow!("skipped, non-idle state found on interface {id}"));
1194            }
1195        }
1196
1197        if !online {
1198            ctx.set_global_link_state(LinkState::Down);
1199            return self.update_state_from_context(id, name);
1200        }
1201
1202        ctx.set_global_link_state(LinkState::Up);
1203
1204        // TODO(https://fxbug.dev/42154208) Check if packet count has increased, and if so upgrade
1205        // the state to LinkLayerUp.
1206        let device_routes: Vec<_> = routes.device_routes(id).collect();
1207
1208        let neighbor_scan_health = scan_neighbor_health(neighbors, &device_routes);
1209
1210        let has_route = IpVersions {
1211            ipv4: device_routes
1212                .iter()
1213                .any(|route| matches!(route.destination.addr, fnet::IpAddress::Ipv4(_))),
1214            ipv6: device_routes
1215                .iter()
1216                .any(|route| matches!(route.destination.addr, fnet::IpAddress::Ipv6(_))),
1217        };
1218
1219        if neighbor_scan_health.ipv4 == NeighborHealthScanResult::NoneHealthy
1220            && neighbor_scan_health.ipv6 == NeighborHealthScanResult::NoneHealthy
1221        {
1222            if !has_route.ipv4 && !has_route.ipv6 {
1223                // Both protocols are `Up`, no need to perform any further calculations.
1224                return self.update_state_from_context(id, name);
1225            }
1226
1227            // When a router is not discoverable via ND, the internet should only be pinged
1228            // if the gateway ping succeeds.
1229            ctx.always_ping_internet = false;
1230        }
1231        if has_route.ipv4 || neighbor_scan_health.ipv4.is_healthy() {
1232            ctx.discovered_state.ipv4.set_link_state(LinkState::Local);
1233        }
1234        if has_route.ipv6 || neighbor_scan_health.ipv6.is_healthy() {
1235            ctx.discovered_state.ipv6.set_link_state(LinkState::Local);
1236        }
1237
1238        let gateway_ping_addrs = device_routes
1239            .iter()
1240            .filter_map(move |Route { destination, outbound_interface, next_hop }| {
1241                if *destination != UNSPECIFIED_V4 && *destination != UNSPECIFIED_V6 {
1242                    return None;
1243                }
1244                next_hop.and_then(|next_hop| {
1245                    let fnet_ext::IpAddress(next_hop) = next_hop.into();
1246                    match next_hop.into() {
1247                        std::net::IpAddr::V4(v4) => {
1248                            Some(std::net::SocketAddr::V4(std::net::SocketAddrV4::new(v4, 0)))
1249                        }
1250                        std::net::IpAddr::V6(v6) => match (*outbound_interface).try_into() {
1251                            Err(std::num::TryFromIntError { .. }) => {
1252                                error!("device id {} doesn't fit in u32", outbound_interface);
1253                                None
1254                            }
1255                            Ok(device_id) => {
1256                                if device_id == 0
1257                                    && net_types::ip::Ipv6Addr::from_bytes(v6.octets()).scope()
1258                                        != net_types::ip::Ipv6Scope::Global
1259                                {
1260                                    None
1261                                } else {
1262                                    Some(std::net::SocketAddr::V6(std::net::SocketAddrV6::new(
1263                                        v6, 0, 0, device_id,
1264                                    )))
1265                                }
1266                            }
1267                        },
1268                    }
1269                })
1270            })
1271            .map(|next_hop| next_hop)
1272            .collect::<Vec<_>>();
1273
1274        // A router is determined to be discoverable if it is online (marked as healthy by ND).
1275        ctx.router_discoverable = IpVersions {
1276            ipv4: neighbor_scan_health.ipv4 == NeighborHealthScanResult::HealthyRouter,
1277            ipv6: neighbor_scan_health.ipv6 == NeighborHealthScanResult::HealthyRouter,
1278        };
1279        if gateway_ping_addrs.is_empty() {
1280            // When there are no gateway addresses to ping, the gateway is not pingable. The list
1281            // of Gateway addresses is obtained by filtering the default IPv4 and IPv6 routes.
1282
1283            // We use the discovery of an online router as a separate opportunity to calculate
1284            // internet reachability because of the potential for various network configurations.
1285            // One potential case involves having an AP operating in bridge mode, and having a
1286            // separate device host DHCP. In this situation, it's possible to have routes that can
1287            // be used to send pings to the internet that are not default routes. In another case,
1288            // a router may have a very specific target prefix that is routable. The device could
1289            // access a remote set of addresses through this local router and not view it as being
1290            // accessed through a default route.
1291            if neighbor_scan_health.ipv4 == NeighborHealthScanResult::HealthyRouter
1292                || neighbor_scan_health.ipv6 == NeighborHealthScanResult::HealthyRouter
1293            {
1294                // Setup to ping internet addresses, skipping over gateway pings.
1295                // Internet can be pinged when either an online router is discovered or the gateway
1296                // is pingable. In this case, the discovery of a router enables the internet ping.
1297                // TODO(https://fxbug.dev/42074958): Create an occurrence metric for this case
1298                ctx.initiate_ping(
1299                    id,
1300                    name,
1301                    &self.network_check_sender,
1302                    NetworkCheckState::PingInternet,
1303                    [
1304                        IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1305                        IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1306                    ]
1307                    .into_iter()
1308                    .map(|ip| std::net::SocketAddr::new(ip, 0))
1309                    .collect(),
1310                );
1311            } else {
1312                // The router is not online and the gateway cannot be pinged; therefore, the
1313                // internet pings can be skipped and the final reachability state can be
1314                // determined.
1315                return self.update_state_from_context(id, name);
1316            }
1317        } else {
1318            // Setup to ping gateway addresses.
1319            if neighbor_scan_health.ipv4.is_healthy_router() {
1320                ctx.discovered_state.ipv4.set_link_state(LinkState::Gateway);
1321            }
1322            if neighbor_scan_health.ipv6.is_healthy_router() {
1323                ctx.discovered_state.ipv6.set_link_state(LinkState::Gateway);
1324            }
1325            ctx.initiate_ping(
1326                id,
1327                name,
1328                &self.network_check_sender,
1329                NetworkCheckState::PingGateway,
1330                gateway_ping_addrs,
1331            );
1332        }
1333        Ok(NetworkCheckerOutcome::MustResume)
1334    }
1335
1336    fn resume(
1337        &mut self,
1338        cookie: NetworkCheckCookie,
1339        result: NetworkCheckResult,
1340    ) -> Result<NetworkCheckerOutcome, anyhow::Error> {
1341        let ctx = self.interface_context.get_mut(&cookie.id).ok_or_else(|| {
1342            anyhow!("resume: interface id {} should already exist in map", cookie.id)
1343        })?;
1344        let interface_name = result.interface_name().to_string();
1345        match ctx.checker_state {
1346            NetworkCheckState::Begin | NetworkCheckState::Idle => {
1347                return Err(anyhow!(
1348                    "skipped, idle state found in resume for interface {}",
1349                    cookie.id
1350                ));
1351            }
1352            NetworkCheckState::PingGateway | NetworkCheckState::PingInternet => {
1353                let (PingParameters { interface_name, addr }, success) =
1354                    result.ping_result().ok_or_else(|| {
1355                        anyhow!(
1356                            "resume: mismatched state and result {interface_name} ({})",
1357                            cookie.id
1358                        )
1359                    })?;
1360                ctx.pings_completed = ctx.pings_completed + 1;
1361
1362                if success {
1363                    let () = Self::handle_ping_success(ctx, &addr);
1364                }
1365
1366                if ctx.pings_completed == ctx.pings_expected {
1367                    if let NetworkCheckState::PingGateway = ctx.checker_state {
1368                        ctx.initiate_ping(
1369                            cookie.id,
1370                            &interface_name,
1371                            &self.network_check_sender,
1372                            NetworkCheckState::PingInternet,
1373                            [
1374                                IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1375                                IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
1376                            ]
1377                            .into_iter()
1378                            .map(|ip| std::net::SocketAddr::new(ip, 0))
1379                            .collect(),
1380                        );
1381                    } else {
1382                        let parameters = ResolveDnsParameters {
1383                            interface_name: interface_name.to_string(),
1384                            domain: GSTATIC.into(),
1385                        };
1386                        ctx.checker_state = NetworkCheckState::ResolveDns;
1387
1388                        if self.time_provider.now() - ctx.persistent_context.resolved_time
1389                            < DNS_PROBE_PERIOD
1390                        {
1391                            debug!(
1392                                "Skipping ResolveDns since it has not yet been {} seconds",
1393                                DNS_PROBE_PERIOD.clone().into_seconds()
1394                            );
1395                            if let Some(ips) = ctx.persistent_context.resolved_addrs.get(GSTATIC) {
1396                                if !ips.v4.is_empty() {
1397                                    ctx.discovered_state.ipv4.application.dns_resolved = true;
1398                                }
1399                                if !ips.v6.is_empty() {
1400                                    ctx.discovered_state.ipv6.application.dns_resolved = true;
1401                                }
1402                            }
1403                            return self.resume(
1404                                cookie,
1405                                NetworkCheckResult::ResolveDns { parameters, ips: None },
1406                            );
1407                        }
1408
1409                        let action = NetworkCheckAction::ResolveDns(parameters);
1410                        match self
1411                            .network_check_sender
1412                            .unbounded_send((action, NetworkCheckCookie { id: cookie.id }))
1413                        {
1414                            Ok(()) => {}
1415                            Err(e) => {
1416                                debug!("unable to send network check internet msg: {e:?}")
1417                            }
1418                        }
1419                    }
1420                }
1421            }
1422            NetworkCheckState::ResolveDns => {
1423                let (ResolveDnsParameters { interface_name, domain }, ips) =
1424                    result.resolve_dns_result().ok_or_else(|| {
1425                        anyhow!(
1426                            "resume: mismatched state and result {interface_name} ({})",
1427                            cookie.id
1428                        )
1429                    })?;
1430
1431                if let Some(ips) = ips {
1432                    if !ips.v4.is_empty() {
1433                        ctx.discovered_state.ipv4.application.dns_resolved = true;
1434                    }
1435                    if !ips.v6.is_empty() {
1436                        ctx.discovered_state.ipv6.application.dns_resolved = true;
1437                    }
1438                    ctx.persistent_context.resolved_time = self.time_provider.now();
1439                    let _: Option<ResolvedIps> =
1440                        ctx.persistent_context.resolved_addrs.insert(domain.clone(), ips);
1441                }
1442
1443                ctx.checker_state = NetworkCheckState::FetchHttp;
1444                ctx.fetches_expected = 0;
1445
1446                let mut add_fetch = |ip: IpAddr| {
1447                    ctx.fetches_expected += 1;
1448                    let action = NetworkCheckAction::Fetch(FetchParameters {
1449                        interface_name: interface_name.clone(),
1450                        domain: domain.clone(),
1451                        ip,
1452                        path: GENERATE_204.into(),
1453                        expected_statuses: vec![204],
1454                    });
1455                    match self
1456                        .network_check_sender
1457                        .unbounded_send((action, NetworkCheckCookie { id: cookie.id }))
1458                    {
1459                        Ok(()) => {}
1460                        Err(e) => debug!("unable to send network check internet message: {e:?}"),
1461                    }
1462                };
1463
1464                if let Some(v4) =
1465                    ctx.persistent_context.resolved_addrs.get(&domain).and_then(|ips| ips.v4.get(0))
1466                {
1467                    add_fetch(IpAddr::V4(*v4));
1468                }
1469                if let Some(v6) =
1470                    ctx.persistent_context.resolved_addrs.get(&domain).and_then(|ips| ips.v6.get(0))
1471                {
1472                    add_fetch(IpAddr::V6(*v6));
1473                }
1474
1475                if ctx.fetches_expected == 0 {
1476                    return self.update_state_from_context(cookie.id, &interface_name);
1477                }
1478            }
1479            NetworkCheckState::FetchHttp => {
1480                let (FetchParameters { interface_name, ip, expected_statuses, .. }, status) =
1481                    result.fetch_result().ok_or_else(|| {
1482                        anyhow!(
1483                            "resume: mismatched state and result {interface_name} ({})",
1484                            cookie.id
1485                        )
1486                    })?;
1487                ctx.fetches_completed += 1;
1488
1489                if let Some(status) = status {
1490                    if expected_statuses.contains(&status) {
1491                        let () = Self::handle_fetch_success(ctx, ip);
1492                    }
1493                }
1494
1495                if ctx.fetches_completed == ctx.fetches_expected {
1496                    return self.update_state_from_context(cookie.id, &interface_name);
1497                }
1498            }
1499        }
1500        Ok(NetworkCheckerOutcome::MustResume)
1501    }
1502}
1503
1504fn log_state(info: Option<&mut InspectInfo>, proto: Proto, state: State) {
1505    info.into_iter().for_each(|info| info.log_link_state(proto, state.link))
1506}
1507
1508#[derive(Default, PartialEq)]
1509enum NeighborHealthScanResult {
1510    // No healthy neighbors were discovered.
1511    #[default]
1512    NoneHealthy,
1513    // A healthy neighbor was discovered.
1514    HealthyNeighbor,
1515    // A healthy router was discovered. Takes precedence over
1516    // `HealthyNeighbor` since an healthy router implies
1517    // a healthy neighbor.
1518    HealthyRouter,
1519}
1520
1521impl NeighborHealthScanResult {
1522    // A neighbor was discovered. Update the state based on whether the neighbor
1523    // is a router.
1524    fn update_scan_result(&mut self, is_router: bool) {
1525        *self = match (&self, is_router) {
1526            // HealthyRouter should never degrade to HealthyNeighbor.
1527            (_, true) | (Self::HealthyRouter, _) => Self::HealthyRouter,
1528            _ => Self::HealthyNeighbor,
1529        }
1530    }
1531
1532    fn is_healthy(&self) -> bool {
1533        match self {
1534            Self::NoneHealthy => false,
1535            Self::HealthyNeighbor | Self::HealthyRouter => true,
1536        }
1537    }
1538
1539    fn is_healthy_router(&self) -> bool {
1540        match self {
1541            Self::NoneHealthy | Self::HealthyNeighbor => false,
1542            Self::HealthyRouter => true,
1543        }
1544    }
1545}
1546
1547// Determines whether any online neighbors or online gateways are discoverable via neighbor
1548// discovery. The definition of a Healthy neighbor correlates to a neighbor being online.
1549fn scan_neighbor_health(
1550    neighbors: Option<&InterfaceNeighborCache>,
1551    device_routes: &Vec<route_table::Route>,
1552) -> IpVersions<NeighborHealthScanResult> {
1553    match neighbors {
1554        None => Default::default(),
1555        Some(neighbors) => {
1556            neighbors.iter_health().fold(
1557                Default::default(),
1558                |mut neighbor_health_scan, (neighbor, health)| {
1559                    let is_router = device_routes.iter().any(
1560                        |Route { destination: _, outbound_interface: _, next_hop }| {
1561                            next_hop.map(|next_hop| *neighbor == next_hop).unwrap_or(false)
1562                        },
1563                    );
1564                    match health {
1565                        // When we find an unhealthy or unknown neighbor, continue,
1566                        // keeping whether we've previously found a healthy neighbor.
1567                        neighbor_cache::NeighborHealth::Unhealthy { .. }
1568                        | neighbor_cache::NeighborHealth::Unknown => neighbor_health_scan,
1569                        // If there's a healthy router, then we're done. If the neighbor
1570                        // is not a router, then we know we have a healthy neighbor, but
1571                        // not a healthy router.
1572                        neighbor_cache::NeighborHealth::Healthy { .. } => {
1573                            let scan = match neighbor {
1574                                fnet::IpAddress::Ipv4(..) => &mut neighbor_health_scan.ipv4,
1575                                fnet::IpAddress::Ipv6(..) => &mut neighbor_health_scan.ipv6,
1576                            };
1577
1578                            scan.update_scan_result(is_router);
1579                            neighbor_health_scan
1580                        }
1581                    }
1582                },
1583            )
1584        }
1585    }
1586}
1587
1588#[cfg(test)]
1589mod tests {
1590    use crate::fetch::FetchAddr;
1591
1592    use super::*;
1593    use crate::dig::Dig;
1594    use crate::fetch::Fetch;
1595    use crate::neighbor_cache::{NeighborHealth, NeighborState};
1596    use crate::ping::Ping;
1597    use async_trait::async_trait;
1598    use diagnostics_assertions::assert_data_tree;
1599    use futures::StreamExt as _;
1600    use net_declare::{fidl_ip, fidl_subnet, std_ip, std_socket_addr};
1601    use net_types::ip;
1602    use std::pin::pin;
1603    use std::task::Poll;
1604    use test_case::test_case;
1605    use {
1606        fidl_fuchsia_net as fnet, fidl_fuchsia_net_interfaces as fnet_interfaces,
1607        fuchsia_async as fasync,
1608    };
1609
1610    const ETHERNET_INTERFACE_NAME: &str = "eth1";
1611    const ID1: u64 = 1;
1612    const ID2: u64 = 2;
1613    // RFC5737§3 specifies the reserved IPv4 address prefix for tests and documentation.
1614    const IPV4_ADDR: fnet::IpAddress = fidl_ip!("192.168.0.1");
1615    // RFC-3849§4 specifies the global IPv6 unicast address prefix for tests and documentation.
1616    const IPV6_ADDR: fnet::IpAddress = fidl_ip!("2001:db8::");
1617
1618    // A trait for writing helper constructors.
1619    //
1620    // Note that this trait differs from `std::convert::From` only in name, but will almost always
1621    // contain shortcuts that would be too surprising for an actual `From` implementation.
1622    trait Construct<T> {
1623        fn construct(_: T) -> Self;
1624    }
1625
1626    impl<S: Into<State>> Construct<S> for StateEvent {
1627        fn construct(link: S) -> Self {
1628            Self { state: link.into(), time: fasync::MonotonicInstant::INFINITE }
1629        }
1630    }
1631
1632    impl Construct<(LinkState, bool, bool)> for StateEvent {
1633        fn construct((link, dns_resolved, http_fetch_succeeded): (LinkState, bool, bool)) -> Self {
1634            Self {
1635                state: State {
1636                    link,
1637                    application: ApplicationState { dns_resolved, http_fetch_succeeded },
1638                },
1639                time: fasync::MonotonicInstant::INFINITE,
1640            }
1641        }
1642    }
1643
1644    impl Construct<StateEvent> for IpVersions<StateEvent> {
1645        fn construct(state: StateEvent) -> Self {
1646            Self { ipv4: state, ipv6: state }
1647        }
1648    }
1649
1650    struct FakeTime {
1651        increment: zx::MonotonicDuration,
1652        time: zx::MonotonicInstant,
1653    }
1654
1655    impl TimeProvider for FakeTime {
1656        fn now(&mut self) -> zx::MonotonicInstant {
1657            let result = self.time;
1658            self.time += self.increment;
1659            result
1660        }
1661    }
1662
1663    #[fuchsia::test]
1664    async fn test_log_state_vals_inspect() {
1665        let inspector = Inspector::default();
1666        LinkState::log_state_vals_inspect(inspector.root(), "state_vals");
1667        assert_data_tree!(inspector, root: {
1668            state_vals: {
1669                "1": "None",
1670                "5": "Removed",
1671                "10": "Down",
1672                "15": "Up",
1673                "20": "Local",
1674                "25": "Gateway",
1675                "30": "Internet",
1676            }
1677        })
1678    }
1679
1680    #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("1.2.3.0:8080")];
1681        "gateway ping on ipv4")]
1682    #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("[123::]:0")];
1683        "gateway ping on ipv6")]
1684    #[test_case(NetworkCheckState::PingGateway, &[std_socket_addr!("1.2.3.0:8080"),
1685        std_socket_addr!("[123::]:0")]; "gateway ping on ipv4/ipv6")]
1686    #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("8.8.8.8:0")];
1687        "internet ping on ipv4")]
1688    #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("[2001:4860:4860::8888]:0")];
1689        "internet ping on ipv6")]
1690    #[test_case(NetworkCheckState::PingInternet, &[std_socket_addr!("8.8.8.8:0"),
1691        std_socket_addr!("[2001:4860:4860::8888]:0")]; "internet ping on ipv4/ipv6")]
1692    fn test_handle_ping_success(checker_state: NetworkCheckState, addrs: &[std::net::SocketAddr]) {
1693        let mut expected_state_v4: State = Default::default();
1694        let mut expected_state_v6: State = Default::default();
1695
1696        let mut ctx = NetworkCheckContext { checker_state, ..Default::default() };
1697        // Initial state.
1698        assert_eq!(ctx.discovered_state.ipv4, expected_state_v4);
1699        assert_eq!(ctx.discovered_state.ipv6, expected_state_v6);
1700
1701        let expected_state = match ctx.checker_state {
1702            NetworkCheckState::PingGateway => LinkState::Gateway.into(),
1703            NetworkCheckState::PingInternet => LinkState::Internet.into(),
1704            NetworkCheckState::ResolveDns => LinkState::Internet.into(),
1705            NetworkCheckState::FetchHttp => State {
1706                link: LinkState::Internet,
1707                application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
1708            },
1709            NetworkCheckState::Begin | NetworkCheckState::Idle => Default::default(),
1710        };
1711
1712        addrs.iter().for_each(|addr| {
1713            // Run the function under test for each address.
1714            let () = Monitor::<FakeTime>::handle_ping_success(&mut ctx, addr);
1715            // Update the expected values accordingly.
1716            match addr {
1717                std::net::SocketAddr::V4 { .. } => {
1718                    expected_state_v4 = expected_state;
1719                }
1720                std::net::SocketAddr::V6 { .. } => {
1721                    expected_state_v6 = expected_state;
1722                }
1723            }
1724        });
1725        // Final state.
1726        assert_eq!(ctx.discovered_state.ipv4, expected_state_v4);
1727        assert_eq!(ctx.discovered_state.ipv6, expected_state_v6);
1728    }
1729
1730    #[derive(Default, Clone)]
1731    struct FakePing {
1732        gateway_addrs: std::collections::HashSet<std::net::IpAddr>,
1733        gateway_response: bool,
1734        internet_response: bool,
1735    }
1736
1737    #[async_trait]
1738    impl Ping for FakePing {
1739        async fn ping(&self, _interface_name: &str, addr: std::net::SocketAddr) -> bool {
1740            let Self { gateway_addrs, gateway_response, internet_response } = self;
1741            let ip = addr.ip();
1742            if [IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS, IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS]
1743                .contains(&ip)
1744            {
1745                *internet_response
1746            } else if gateway_addrs.contains(&ip) {
1747                *gateway_response
1748            } else {
1749                false
1750            }
1751        }
1752    }
1753
1754    #[derive(Default)]
1755    struct FakeDig {
1756        response: Option<ResolvedIps>,
1757    }
1758
1759    impl FakeDig {
1760        fn new(ips: Vec<std::net::IpAddr>) -> Self {
1761            let mut ips_out = ResolvedIps::default();
1762            for ip in ips {
1763                match ip {
1764                    IpAddr::V4(v4) => ips_out.v4.push(v4),
1765                    IpAddr::V6(v6) => ips_out.v6.push(v6),
1766                }
1767            }
1768            FakeDig { response: Some(ips_out) }
1769        }
1770    }
1771
1772    #[async_trait]
1773    impl Dig for FakeDig {
1774        async fn dig(&self, _interface_name: &str, _domain: &str) -> Option<ResolvedIps> {
1775            self.response.clone()
1776        }
1777    }
1778
1779    #[derive(Default, Copy, Clone)]
1780    struct FakeFetch {
1781        expected_url: Option<&'static str>,
1782        response: Option<u16>,
1783    }
1784
1785    #[async_trait]
1786    impl Fetch for FakeFetch {
1787        async fn fetch<FA: FetchAddr + std::marker::Sync>(
1788            &self,
1789            _interface_name: &str,
1790            domain: &str,
1791            path: &str,
1792            _addr: &FA,
1793        ) -> Option<u16> {
1794            if let Some(expected) = self.expected_url {
1795                assert_eq!(
1796                    format!("http://{domain}{path}"),
1797                    expected,
1798                    "Did not receive expected URL"
1799                );
1800            }
1801            self.response
1802        }
1803    }
1804
1805    struct NetworkCheckTestResponder {
1806        receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
1807    }
1808
1809    impl NetworkCheckTestResponder {
1810        fn new(
1811            receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
1812        ) -> Self {
1813            Self { receiver }
1814        }
1815
1816        async fn respond_to_messages<P: Ping, D: Dig, F: Fetch, Time: TimeProvider>(
1817            &mut self,
1818            monitor: &mut Monitor<Time>,
1819            p: P,
1820            d: D,
1821            f: F,
1822        ) {
1823            loop {
1824                if let Some((action, cookie)) = self.receiver.next().await {
1825                    match action {
1826                        NetworkCheckAction::Ping(parameters) => {
1827                            let success = p.ping(&parameters.interface_name, parameters.addr).await;
1828                            match monitor
1829                                .resume(cookie, NetworkCheckResult::Ping { parameters, success })
1830                            {
1831                                // Has reached final state.
1832                                Ok(NetworkCheckerOutcome::Complete) => return,
1833                                _ => {}
1834                            }
1835                        }
1836                        NetworkCheckAction::ResolveDns(parameters) => {
1837                            let ips = d.dig(&parameters.interface_name, &parameters.domain).await;
1838                            match monitor
1839                                .resume(cookie, NetworkCheckResult::ResolveDns { parameters, ips })
1840                            {
1841                                // Has reached final state.
1842                                Ok(NetworkCheckerOutcome::Complete) => return,
1843                                _ => {}
1844                            }
1845                        }
1846                        NetworkCheckAction::Fetch(parameters) => {
1847                            let status = f
1848                                .fetch(
1849                                    &parameters.interface_name,
1850                                    &parameters.domain,
1851                                    &parameters.path,
1852                                    &parameters.ip,
1853                                )
1854                                .await;
1855                            match monitor
1856                                .resume(cookie, NetworkCheckResult::Fetch { parameters, status })
1857                            {
1858                                // Has reached final state.
1859                                Ok(NetworkCheckerOutcome::Complete) => return,
1860                                _ => {}
1861                            }
1862                        }
1863                    }
1864                }
1865            }
1866        }
1867    }
1868
1869    fn run_network_check_partial_properties_repeated<P: Ping, D: Dig, F: Fetch>(
1870        exec: &mut fasync::TestExecutor,
1871        name: &str,
1872        interface_id: u64,
1873        routes: &RouteTable,
1874        mocks: Vec<(P, D, F)>,
1875        neighbors: Option<&InterfaceNeighborCache>,
1876        internet_ping_address: std::net::IpAddr,
1877        sleep_between: Option<zx::MonotonicDuration>,
1878    ) -> Vec<State> {
1879        let properties = &fnet_interfaces_ext::Properties {
1880            id: interface_id.try_into().expect("should be nonzero"),
1881            name: name.to_string(),
1882            port_class: fnet_interfaces_ext::PortClass::Ethernet,
1883            online: true,
1884            addresses: Default::default(),
1885            has_default_ipv4_route: Default::default(),
1886            has_default_ipv6_route: Default::default(),
1887            port_identity_koid: Default::default(),
1888        };
1889
1890        let mock_count = mocks.len();
1891        match run_network_check_repeated(exec, properties, routes, neighbors, mocks, sleep_between)
1892        {
1893            Ok(Some(events)) => {
1894                // Implementation checks v4 and v6 connectivity concurrently, although these tests
1895                // only check for a single protocol at a time. The address being pinged determines
1896                // which protocol to use.
1897                events
1898                    .into_iter()
1899                    .map(|event| match internet_ping_address {
1900                        std::net::IpAddr::V4 { .. } => event.ipv4.state,
1901                        std::net::IpAddr::V6 { .. } => event.ipv6.state,
1902                    })
1903                    .collect()
1904            }
1905            Ok(None) => {
1906                error!("id for interface unexpectedly did not exist after network check");
1907                std::iter::repeat(LinkState::None.into()).take(mock_count).collect()
1908            }
1909            Err(e) => {
1910                error!("network check had an issue calculating state: {:?}", e);
1911                std::iter::repeat(LinkState::None.into()).take(mock_count).collect()
1912            }
1913        }
1914    }
1915
1916    fn run_network_check_partial_properties<P: Ping, D: Dig, F: Fetch>(
1917        exec: &mut fasync::TestExecutor,
1918        name: &str,
1919        interface_id: u64,
1920        routes: &RouteTable,
1921        pinger: P,
1922        digger: D,
1923        fetcher: F,
1924        neighbors: Option<&InterfaceNeighborCache>,
1925        internet_ping_address: std::net::IpAddr,
1926    ) -> State {
1927        run_network_check_partial_properties_repeated(
1928            exec,
1929            name,
1930            interface_id,
1931            routes,
1932            vec![(pinger, digger, fetcher)],
1933            neighbors,
1934            internet_ping_address,
1935            None,
1936        )
1937        .pop()
1938        .unwrap_or_else(|| {
1939            error!("network check returned no states");
1940            LinkState::None.into()
1941        })
1942    }
1943
1944    fn run_network_check_repeated<P: Ping, D: Dig, F: Fetch>(
1945        exec: &mut fasync::TestExecutor,
1946        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
1947        routes: &RouteTable,
1948        neighbors: Option<&InterfaceNeighborCache>,
1949        mocks: Vec<(P, D, F)>,
1950        sleep_between: Option<zx::MonotonicDuration>,
1951    ) -> Result<Option<Vec<IpVersions<StateEvent>>>, anyhow::Error> {
1952        let (sender, receiver) = mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
1953        let mut monitor = Monitor::new_with_time_provider(
1954            sender,
1955            FakeTime {
1956                increment: sleep_between.unwrap_or(zx::MonotonicDuration::from_nanos(10)),
1957                time: zx::MonotonicInstant::get(),
1958            },
1959        )
1960        .unwrap();
1961        let mut network_check_responder = NetworkCheckTestResponder::new(receiver);
1962
1963        let view = InterfaceView { properties, routes, neighbors };
1964        let network_check_fut = async {
1965            let mut states = Vec::new();
1966            for (pinger, digger, fetcher) in mocks {
1967                match monitor.begin(view) {
1968                    Ok(NetworkCheckerOutcome::Complete) => {}
1969                    Ok(NetworkCheckerOutcome::MustResume) => {
1970                        let () = network_check_responder
1971                            .respond_to_messages(&mut monitor, pinger, digger, fetcher)
1972                            .await;
1973                    }
1974                    Err(e) => {
1975                        error!("begin had an issue calculating state: {:?}", e)
1976                    }
1977                }
1978                states.push(monitor.state().get(properties.id.get()).map(Clone::clone));
1979            }
1980            states
1981        };
1982
1983        let mut network_check_fut = pin!(network_check_fut);
1984        match exec.run_until_stalled(&mut network_check_fut) {
1985            Poll::Ready(got) => Ok(got.into_iter().collect()),
1986            Poll::Pending => Err(anyhow::anyhow!("network_check blocked unexpectedly")),
1987        }
1988    }
1989
1990    fn run_network_check<P: Ping, D: Dig, F: Fetch>(
1991        exec: &mut fasync::TestExecutor,
1992        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
1993        routes: &RouteTable,
1994        neighbors: Option<&InterfaceNeighborCache>,
1995        pinger: P,
1996        digger: D,
1997        fetcher: F,
1998    ) -> Result<Option<IpVersions<StateEvent>>, anyhow::Error> {
1999        run_network_check_repeated(
2000            exec,
2001            properties,
2002            routes,
2003            neighbors,
2004            vec![(pinger, digger, fetcher)],
2005            None,
2006        )
2007        .map(|res| res.and_then(|mut v| v.pop()))
2008    }
2009
2010    #[test]
2011    fn test_network_check_ipv6_local_only() {
2012        let mut exec = fasync::TestExecutor::new_with_fake_time();
2013        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2014        let () = exec.set_fake_time(time.into());
2015
2016        // The next_hop of the default route must be the same as a known neighbor. This is used
2017        // to determine this neighbor as a valid gateway.
2018        let routes = testutil::build_route_table_from_flattened_routes([Route {
2019            destination: UNSPECIFIED_V6,
2020            outbound_interface: ID1,
2021            next_hop: Some(IPV6_ADDR),
2022        }]);
2023        let properties = &fnet_interfaces_ext::Properties {
2024            id: ID1.try_into().expect("should be nonzero"),
2025            name: ETHERNET_INTERFACE_NAME.to_string(),
2026            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2027            online: true,
2028            addresses: vec![],
2029            has_default_ipv4_route: false,
2030            has_default_ipv6_route: true,
2031            port_identity_koid: Default::default(),
2032        };
2033        let neighbors = InterfaceNeighborCache::default();
2034
2035        let got = run_network_check(
2036            &mut exec,
2037            properties,
2038            &routes,
2039            Some(&neighbors),
2040            FakePing::default(),
2041            FakeDig::default(),
2042            FakeFetch::default(),
2043        )
2044        .expect("run_network_check failed")
2045        .expect("interface state not found");
2046
2047        let want_ipv4 =
2048            StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2049        let want_ipv6 =
2050            StateEvent { state: State { link: LinkState::Local, ..Default::default() }, time };
2051        assert_eq!(got.ipv4, want_ipv4);
2052        assert_eq!(got.ipv6, want_ipv6);
2053    }
2054
2055    #[test]
2056    fn test_network_check_ipv6_local_only_not_default_route() {
2057        let mut exec = fasync::TestExecutor::new_with_fake_time();
2058        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2059        let () = exec.set_fake_time(time.into());
2060
2061        // The next_hop of the default route must be the same as a known neighbor. This is used
2062        // to determine this neighbor as a valid gateway.
2063        let routes = testutil::build_route_table_from_flattened_routes([Route {
2064            destination: fidl_subnet!("::/1"),
2065            outbound_interface: ID1,
2066            next_hop: Some(IPV6_ADDR),
2067        }]);
2068        let properties = &fnet_interfaces_ext::Properties {
2069            id: ID1.try_into().expect("should be nonzero"),
2070            name: ETHERNET_INTERFACE_NAME.to_string(),
2071            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2072            online: true,
2073            addresses: vec![],
2074            has_default_ipv4_route: false,
2075            has_default_ipv6_route: true,
2076            port_identity_koid: Default::default(),
2077        };
2078        let neighbors = InterfaceNeighborCache::default();
2079
2080        let got = run_network_check(
2081            &mut exec,
2082            properties,
2083            &routes,
2084            Some(&neighbors),
2085            FakePing::default(),
2086            FakeDig::default(),
2087            FakeFetch::default(),
2088        )
2089        .expect("run_network_check failed")
2090        .expect("interface state not found");
2091
2092        let want_ipv4 =
2093            StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2094        let want_ipv6 =
2095            StateEvent { state: State { link: LinkState::Local, ..Default::default() }, time };
2096        assert_eq!(got.ipv4, want_ipv4);
2097        assert_eq!(got.ipv6, want_ipv6);
2098    }
2099
2100    #[test]
2101    fn test_network_check_ipv6_gateway_only() {
2102        let mut exec = fasync::TestExecutor::new_with_fake_time();
2103        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2104        let () = exec.set_fake_time(time.into());
2105
2106        // The next_hop of the default route must be the same as a known neighbor. This is used
2107        // to determine this neighbor as a valid gateway.
2108        let routes = testutil::build_route_table_from_flattened_routes([Route {
2109            destination: UNSPECIFIED_V6,
2110            outbound_interface: ID1,
2111            next_hop: Some(IPV6_ADDR),
2112        }]);
2113        let properties = &fnet_interfaces_ext::Properties {
2114            id: ID1.try_into().expect("should be nonzero"),
2115            name: ETHERNET_INTERFACE_NAME.to_string(),
2116            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2117            online: true,
2118            addresses: vec![],
2119            has_default_ipv4_route: false,
2120            has_default_ipv6_route: true,
2121            port_identity_koid: Default::default(),
2122        };
2123        let neighbors = InterfaceNeighborCache {
2124            neighbors: [(
2125                IPV6_ADDR,
2126                NeighborState::new(NeighborHealth::Healthy {
2127                    last_observed: zx::MonotonicInstant::default(),
2128                }),
2129            )]
2130            .into_iter()
2131            .collect::<HashMap<fnet::IpAddress, NeighborState>>(),
2132        };
2133
2134        let got = run_network_check(
2135            &mut exec,
2136            properties,
2137            &routes,
2138            Some(&neighbors),
2139            FakePing::default(),
2140            FakeDig::default(),
2141            FakeFetch::default(),
2142        )
2143        .expect("run_network_check failed")
2144        .expect("interface state not found");
2145
2146        let want_ipv4 =
2147            StateEvent { state: State { link: LinkState::Up, ..Default::default() }, time };
2148        let want_ipv6 =
2149            StateEvent { state: State { link: LinkState::Gateway, ..Default::default() }, time };
2150        assert_eq!(got.ipv4, want_ipv4);
2151        assert_eq!(got.ipv6, want_ipv6);
2152    }
2153
2154    #[fuchsia::test]
2155    fn test_network_check_ipv4_and_ipv6_gateway() {
2156        let mut exec = fasync::TestExecutor::new_with_fake_time();
2157        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
2158        let () = exec.set_fake_time(time.into());
2159
2160        // The next_hop of the default route must be the same as a known neighbor. This is used
2161        // to determine this neighbor as a valid gateway.
2162        let routes = testutil::build_route_table_from_flattened_routes([
2163            Route {
2164                destination: UNSPECIFIED_V4,
2165                outbound_interface: ID1,
2166                next_hop: Some(IPV4_ADDR),
2167            },
2168            Route {
2169                destination: UNSPECIFIED_V6,
2170                outbound_interface: ID1,
2171                next_hop: Some(IPV6_ADDR),
2172            },
2173        ]);
2174        let properties = &fnet_interfaces_ext::Properties {
2175            id: ID1.try_into().expect("should be nonzero"),
2176            name: ETHERNET_INTERFACE_NAME.to_string(),
2177            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2178            online: true,
2179            addresses: vec![],
2180            has_default_ipv4_route: true,
2181            has_default_ipv6_route: true,
2182            port_identity_koid: Default::default(),
2183        };
2184        let neighbors = InterfaceNeighborCache {
2185            neighbors: [
2186                (
2187                    IPV4_ADDR,
2188                    NeighborState::new(NeighborHealth::Healthy {
2189                        last_observed: zx::MonotonicInstant::default(),
2190                    }),
2191                ),
2192                (
2193                    IPV6_ADDR,
2194                    NeighborState::new(NeighborHealth::Healthy {
2195                        last_observed: zx::MonotonicInstant::default(),
2196                    }),
2197                ),
2198            ]
2199            .into_iter()
2200            .collect::<HashMap<fnet::IpAddress, NeighborState>>(),
2201        };
2202
2203        let got = run_network_check(
2204            &mut exec,
2205            properties,
2206            &routes,
2207            Some(&neighbors),
2208            FakePing::default(),
2209            FakeDig::default(),
2210            FakeFetch::default(),
2211        )
2212        .expect("run_network_check failed")
2213        .expect("interface state not found");
2214
2215        assert_eq!(
2216            got,
2217            IpVersions::construct(StateEvent {
2218                state: State { link: LinkState::Gateway, ..Default::default() },
2219                time
2220            })
2221        );
2222    }
2223
2224    #[test]
2225    fn test_network_check_ethernet_ipv4() {
2226        test_network_check_ethernet::<ip::Ipv4>(
2227            fidl_ip!("1.2.3.0"),
2228            fidl_ip!("1.2.3.4"),
2229            fidl_ip!("1.2.3.1"),
2230            fidl_ip!("2.2.3.0"),
2231            fidl_ip!("2.2.3.1"),
2232            UNSPECIFIED_V4,
2233            fidl_subnet!("0.0.0.0/1"),
2234            IPV4_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
2235            24,
2236        );
2237    }
2238
2239    #[test]
2240    fn test_network_check_ethernet_ipv6() {
2241        test_network_check_ethernet::<ip::Ipv6>(
2242            fidl_ip!("123::"),
2243            fidl_ip!("123::4"),
2244            fidl_ip!("123::1"),
2245            fidl_ip!("223::"),
2246            fidl_ip!("223::1"),
2247            UNSPECIFIED_V6,
2248            fidl_subnet!("::/1"),
2249            IPV6_INTERNET_CONNECTIVITY_CHECK_ADDRESS,
2250            64,
2251        );
2252    }
2253
2254    fn test_network_check_ethernet<I: ip::Ip>(
2255        net1: fnet::IpAddress,
2256        _net1_addr: fnet::IpAddress,
2257        net1_gateway: fnet::IpAddress,
2258        net2: fnet::IpAddress,
2259        net2_gateway: fnet::IpAddress,
2260        unspecified_addr: fnet::Subnet,
2261        non_default_addr: fnet::Subnet,
2262        ping_internet_addr: std::net::IpAddr,
2263        prefix_len: u8,
2264    ) {
2265        let route_table = testutil::build_route_table_from_flattened_routes([
2266            Route {
2267                destination: unspecified_addr,
2268                outbound_interface: ID1,
2269                next_hop: Some(net1_gateway),
2270            },
2271            Route {
2272                destination: fnet::Subnet { addr: net1, prefix_len },
2273                outbound_interface: ID1,
2274                next_hop: None,
2275            },
2276        ]);
2277        let route_table_2 = testutil::build_route_table_from_flattened_routes([
2278            Route {
2279                destination: unspecified_addr,
2280                outbound_interface: ID1,
2281                next_hop: Some(net2_gateway),
2282            },
2283            Route {
2284                destination: fnet::Subnet { addr: net1, prefix_len },
2285                outbound_interface: ID1,
2286                next_hop: None,
2287            },
2288            Route {
2289                destination: fnet::Subnet { addr: net2, prefix_len },
2290                outbound_interface: ID1,
2291                next_hop: None,
2292            },
2293        ]);
2294        let route_table_3 = testutil::build_route_table_from_flattened_routes([
2295            Route {
2296                destination: unspecified_addr,
2297                outbound_interface: ID2,
2298                next_hop: Some(net1_gateway),
2299            },
2300            Route {
2301                destination: fnet::Subnet { addr: net1, prefix_len },
2302                outbound_interface: ID2,
2303                next_hop: None,
2304            },
2305        ]);
2306        let route_table_4 = testutil::build_route_table_from_flattened_routes([
2307            Route {
2308                destination: non_default_addr,
2309                outbound_interface: ID1,
2310                next_hop: Some(net1_gateway),
2311            },
2312            Route {
2313                destination: fnet::Subnet { addr: net1, prefix_len },
2314                outbound_interface: ID1,
2315                next_hop: None,
2316            },
2317        ]);
2318
2319        let fnet_ext::IpAddress(net1_gateway_ext) = net1_gateway.into();
2320        let mut exec = fasync::TestExecutor::new();
2321
2322        // TODO(fxrev.dev/120580): Extract test cases into variants/helper function
2323        assert_eq!(
2324            run_network_check_partial_properties(
2325                &mut exec,
2326                ETHERNET_INTERFACE_NAME,
2327                ID1,
2328                &route_table,
2329                FakePing {
2330                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2331                    gateway_response: true,
2332                    internet_response: true,
2333                },
2334                FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]),
2335                FakeFetch {
2336                    expected_url: Some("http://www.gstatic.com/generate_204"),
2337                    response: Some(204)
2338                },
2339                None,
2340                ping_internet_addr,
2341            ),
2342            State {
2343                link: LinkState::Internet,
2344                application: ApplicationState { dns_resolved: true, http_fetch_succeeded: true },
2345            },
2346            "All is good. Can reach internet"
2347        );
2348
2349        assert_eq!(
2350            run_network_check_partial_properties(
2351                &mut exec,
2352                ETHERNET_INTERFACE_NAME,
2353                ID1,
2354                &route_table,
2355                FakePing {
2356                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2357                    gateway_response: true,
2358                    internet_response: true,
2359                },
2360                FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]),
2361                FakeFetch::default(),
2362                None,
2363                ping_internet_addr,
2364            ),
2365            State {
2366                link: LinkState::Internet,
2367                application: ApplicationState { dns_resolved: true, ..Default::default() },
2368            },
2369            "HTTP Fetch fails"
2370        );
2371
2372        assert_eq!(
2373            run_network_check_partial_properties(
2374                &mut exec,
2375                ETHERNET_INTERFACE_NAME,
2376                ID1,
2377                &route_table,
2378                FakePing {
2379                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2380                    gateway_response: true,
2381                    internet_response: true,
2382                },
2383                FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("1.2.4.0")]),
2384                FakeFetch::default(),
2385                None,
2386                ping_internet_addr,
2387            ),
2388            State {
2389                link: LinkState::Internet,
2390                application: ApplicationState {
2391                    dns_resolved: ping_internet_addr.is_ipv4(),
2392                    ..Default::default()
2393                },
2394            },
2395            "DNS Resolves only IPV4",
2396        );
2397
2398        assert_eq!(
2399            run_network_check_partial_properties(
2400                &mut exec,
2401                ETHERNET_INTERFACE_NAME,
2402                ID1,
2403                &route_table,
2404                FakePing {
2405                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2406                    gateway_response: true,
2407                    internet_response: true,
2408                },
2409                FakeDig::new(vec![std_ip!("123::"), std_ip!("124::")]),
2410                FakeFetch::default(),
2411                None,
2412                ping_internet_addr,
2413            ),
2414            State {
2415                link: LinkState::Internet,
2416                application: ApplicationState {
2417                    dns_resolved: ping_internet_addr.is_ipv6(),
2418                    ..Default::default()
2419                },
2420            },
2421            "DNS Resolves only IPV6",
2422        );
2423
2424        assert_eq!(
2425            run_network_check_partial_properties(
2426                &mut exec,
2427                ETHERNET_INTERFACE_NAME,
2428                ID1,
2429                &route_table,
2430                FakePing {
2431                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2432                    gateway_response: false,
2433                    internet_response: true,
2434                },
2435                FakeDig::default(),
2436                FakeFetch::default(),
2437                Some(&InterfaceNeighborCache {
2438                    neighbors: [(
2439                        net1_gateway,
2440                        NeighborState::new(NeighborHealth::Healthy {
2441                            last_observed: zx::MonotonicInstant::default(),
2442                        })
2443                    )]
2444                    .into_iter()
2445                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2446                }),
2447                ping_internet_addr,
2448            ),
2449            LinkState::Internet.into(),
2450            "Can reach internet, gateway responding via ARP/ND"
2451        );
2452
2453        assert_eq!(
2454            run_network_check_partial_properties(
2455                &mut exec,
2456                ETHERNET_INTERFACE_NAME,
2457                ID1,
2458                &route_table,
2459                FakePing {
2460                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2461                    gateway_response: false,
2462                    internet_response: true,
2463                },
2464                FakeDig::default(),
2465                FakeFetch::default(),
2466                Some(&InterfaceNeighborCache {
2467                    neighbors: [(
2468                        net1,
2469                        NeighborState::new(NeighborHealth::Healthy {
2470                            last_observed: zx::MonotonicInstant::default(),
2471                        })
2472                    )]
2473                    .into_iter()
2474                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2475                }),
2476                ping_internet_addr,
2477            ),
2478            LinkState::Internet.into(),
2479            "Gateway not responding via ping or ARP/ND. Can reach internet"
2480        );
2481
2482        assert_eq!(
2483            run_network_check_partial_properties(
2484                &mut exec,
2485                ETHERNET_INTERFACE_NAME,
2486                ID1,
2487                &route_table_4,
2488                FakePing {
2489                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2490                    gateway_response: true,
2491                    internet_response: true,
2492                },
2493                FakeDig::default(),
2494                FakeFetch::default(),
2495                Some(&InterfaceNeighborCache {
2496                    neighbors: [(
2497                        net1_gateway,
2498                        NeighborState::new(NeighborHealth::Healthy {
2499                            last_observed: zx::MonotonicInstant::default(),
2500                        })
2501                    )]
2502                    .into_iter()
2503                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2504                }),
2505                ping_internet_addr,
2506            ),
2507            LinkState::Internet.into(),
2508            "No default route, but healthy gateway with internet/gateway response"
2509        );
2510
2511        assert_eq!(
2512            run_network_check_partial_properties(
2513                &mut exec,
2514                ETHERNET_INTERFACE_NAME,
2515                ID1,
2516                &route_table,
2517                FakePing {
2518                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2519                    gateway_response: true,
2520                    internet_response: false,
2521                },
2522                FakeDig::default(),
2523                FakeFetch::default(),
2524                None,
2525                ping_internet_addr,
2526            ),
2527            LinkState::Gateway.into(),
2528            "Can reach gateway via ping"
2529        );
2530
2531        assert_eq!(
2532            run_network_check_partial_properties(
2533                &mut exec,
2534                ETHERNET_INTERFACE_NAME,
2535                ID1,
2536                &route_table,
2537                FakePing::default(),
2538                FakeDig::default(),
2539                FakeFetch::default(),
2540                Some(&InterfaceNeighborCache {
2541                    neighbors: [(
2542                        net1_gateway,
2543                        NeighborState::new(NeighborHealth::Healthy {
2544                            last_observed: zx::MonotonicInstant::default(),
2545                        })
2546                    )]
2547                    .into_iter()
2548                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2549                }),
2550                ping_internet_addr,
2551            ),
2552            LinkState::Gateway.into(),
2553            "Can reach gateway via ARP/ND"
2554        );
2555
2556        assert_eq!(
2557            run_network_check_partial_properties(
2558                &mut exec,
2559                ETHERNET_INTERFACE_NAME,
2560                ID1,
2561                &route_table,
2562                FakePing {
2563                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2564                    gateway_response: false,
2565                    internet_response: false,
2566                },
2567                FakeDig::default(),
2568                FakeFetch::default(),
2569                None,
2570                ping_internet_addr,
2571            ),
2572            LinkState::Local.into(),
2573            "Local only, Cannot reach gateway"
2574        );
2575
2576        assert_eq!(
2577            run_network_check_partial_properties(
2578                &mut exec,
2579                ETHERNET_INTERFACE_NAME,
2580                ID1,
2581                &route_table_2,
2582                FakePing::default(),
2583                FakeDig::default(),
2584                FakeFetch::default(),
2585                None,
2586                ping_internet_addr,
2587            ),
2588            LinkState::Local.into(),
2589            "No default route"
2590        );
2591
2592        assert_eq!(
2593            run_network_check_partial_properties(
2594                &mut exec,
2595                ETHERNET_INTERFACE_NAME,
2596                ID1,
2597                &route_table_4,
2598                FakePing {
2599                    gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2600                    gateway_response: true,
2601                    internet_response: false,
2602                },
2603                FakeDig::default(),
2604                FakeFetch::default(),
2605                None,
2606                ping_internet_addr,
2607            ),
2608            LinkState::Local.into(),
2609            "No default route, with only gateway response"
2610        );
2611
2612        assert_eq!(
2613            run_network_check_partial_properties(
2614                &mut exec,
2615                ETHERNET_INTERFACE_NAME,
2616                ID1,
2617                &route_table_2,
2618                FakePing::default(),
2619                FakeDig::default(),
2620                FakeFetch::default(),
2621                Some(&InterfaceNeighborCache {
2622                    neighbors: [(
2623                        net1,
2624                        NeighborState::new(NeighborHealth::Healthy {
2625                            last_observed: zx::MonotonicInstant::default(),
2626                        })
2627                    )]
2628                    .into_iter()
2629                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2630                }),
2631                ping_internet_addr,
2632            ),
2633            LinkState::Local.into(),
2634            "Local only, neighbors responsive with no default route"
2635        );
2636
2637        assert_eq!(
2638            run_network_check_partial_properties(
2639                &mut exec,
2640                ETHERNET_INTERFACE_NAME,
2641                ID1,
2642                &route_table,
2643                FakePing::default(),
2644                FakeDig::default(),
2645                FakeFetch::default(),
2646                Some(&InterfaceNeighborCache {
2647                    neighbors: [(
2648                        net1,
2649                        NeighborState::new(NeighborHealth::Healthy {
2650                            last_observed: zx::MonotonicInstant::default(),
2651                        })
2652                    )]
2653                    .into_iter()
2654                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2655                }),
2656                ping_internet_addr
2657            ),
2658            LinkState::Local.into(),
2659            "Local only, neighbors responsive with a default route"
2660        );
2661
2662        assert_eq!(
2663            run_network_check_partial_properties(
2664                &mut exec,
2665                ETHERNET_INTERFACE_NAME,
2666                ID1,
2667                &route_table_3,
2668                FakePing::default(),
2669                FakeDig::default(),
2670                FakeFetch::default(),
2671                Some(&InterfaceNeighborCache {
2672                    neighbors: [(
2673                        net1,
2674                        NeighborState::new(NeighborHealth::Healthy {
2675                            last_observed: zx::MonotonicInstant::default(),
2676                        })
2677                    )]
2678                    .into_iter()
2679                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2680                }),
2681                ping_internet_addr,
2682            ),
2683            LinkState::Local.into(),
2684            "Local only, neighbors responsive with no routes"
2685        );
2686
2687        assert_eq!(
2688            run_network_check_partial_properties(
2689                &mut exec,
2690                ETHERNET_INTERFACE_NAME,
2691                ID1,
2692                &route_table,
2693                FakePing::default(),
2694                FakeDig::default(),
2695                FakeFetch::default(),
2696                Some(&InterfaceNeighborCache {
2697                    neighbors: [
2698                        (
2699                            net1,
2700                            NeighborState::new(NeighborHealth::Healthy {
2701                                last_observed: zx::MonotonicInstant::default(),
2702                            })
2703                        ),
2704                        (
2705                            net1_gateway,
2706                            NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None })
2707                        )
2708                    ]
2709                    .into_iter()
2710                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2711                }),
2712                ping_internet_addr,
2713            ),
2714            LinkState::Local.into(),
2715            "Local only, gateway unhealthy with healthy neighbor"
2716        );
2717
2718        assert_eq!(
2719            run_network_check_partial_properties(
2720                &mut exec,
2721                ETHERNET_INTERFACE_NAME,
2722                ID1,
2723                &route_table_3,
2724                FakePing::default(),
2725                FakeDig::default(),
2726                FakeFetch::default(),
2727                Some(&InterfaceNeighborCache {
2728                    neighbors: [(
2729                        net1_gateway,
2730                        NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None })
2731                    )]
2732                    .into_iter()
2733                    .collect::<HashMap<fnet::IpAddress, NeighborState>>()
2734                }),
2735                ping_internet_addr,
2736            ),
2737            LinkState::Up.into(),
2738            "No routes and unhealthy gateway"
2739        );
2740
2741        assert_eq!(
2742            run_network_check_partial_properties(
2743                &mut exec,
2744                ETHERNET_INTERFACE_NAME,
2745                ID1,
2746                &route_table_3,
2747                FakePing::default(),
2748                FakeDig::default(),
2749                FakeFetch::default(),
2750                None,
2751                ping_internet_addr,
2752            ),
2753            LinkState::Up.into(),
2754            "No routes",
2755        );
2756
2757        assert_eq!(
2758            run_network_check_partial_properties_repeated(
2759                &mut exec,
2760                ETHERNET_INTERFACE_NAME,
2761                ID1,
2762                &route_table,
2763                vec![
2764                    (
2765                        FakePing {
2766                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2767                            gateway_response: true,
2768                            internet_response: true,
2769                        },
2770                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
2771                        FakeFetch {
2772                            expected_url: Some("http://www.gstatic.com/generate_204"),
2773                            response: Some(204)
2774                        },
2775                    ),
2776                    (
2777                        FakePing {
2778                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2779                            gateway_response: true,
2780                            internet_response: true,
2781                        },
2782                        FakeDig { response: None }, // Then, use one that fails
2783                        FakeFetch {
2784                            expected_url: Some("http://www.gstatic.com/generate_204"),
2785                            response: Some(204)
2786                        },
2787                    ),
2788                ],
2789                None,
2790                ping_internet_addr,
2791                None,
2792            ),
2793            vec![
2794                State {
2795                    link: LinkState::Internet,
2796                    application: ApplicationState {
2797                        dns_resolved: true,
2798                        http_fetch_succeeded: true
2799                    }
2800                },
2801                State {
2802                    link: LinkState::Internet,
2803                    application: ApplicationState {
2804                        dns_resolved: true,
2805                        http_fetch_succeeded: true
2806                    }
2807                }
2808            ],
2809            "Fail DNS on second check; fetch succeeds; no pause"
2810        );
2811
2812        assert_eq!(
2813            run_network_check_partial_properties_repeated(
2814                &mut exec,
2815                ETHERNET_INTERFACE_NAME,
2816                ID1,
2817                &route_table,
2818                vec![
2819                    (
2820                        FakePing {
2821                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2822                            gateway_response: true,
2823                            internet_response: true,
2824                        },
2825                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
2826                        FakeFetch {
2827                            expected_url: Some("http://www.gstatic.com/generate_204"),
2828                            response: Some(204)
2829                        },
2830                    ),
2831                    (
2832                        FakePing {
2833                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2834                            gateway_response: true,
2835                            internet_response: true,
2836                        },
2837                        FakeDig { response: None }, // Then, use one that fails
2838                        FakeFetch {
2839                            expected_url: Some("http://www.gstatic.com/generate_204"),
2840                            response: Some(204)
2841                        },
2842                    ),
2843                ],
2844                None,
2845                ping_internet_addr,
2846                Some(DNS_PROBE_PERIOD),
2847            ),
2848            vec![
2849                State {
2850                    link: LinkState::Internet,
2851                    application: ApplicationState {
2852                        dns_resolved: true,
2853                        http_fetch_succeeded: true
2854                    }
2855                },
2856                State {
2857                    link: LinkState::Internet,
2858                    application: ApplicationState {
2859                        dns_resolved: false,
2860                        http_fetch_succeeded: true
2861                    }
2862                }
2863            ],
2864            "Fail DNS on second check; fetch succeeds"
2865        );
2866
2867        assert_eq!(
2868            run_network_check_partial_properties_repeated(
2869                &mut exec,
2870                ETHERNET_INTERFACE_NAME,
2871                ID1,
2872                &route_table,
2873                vec![
2874                    (
2875                        FakePing {
2876                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2877                            gateway_response: true,
2878                            internet_response: true,
2879                        },
2880                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
2881                        FakeFetch {
2882                            expected_url: Some("http://www.gstatic.com/generate_204"),
2883                            response: None
2884                        },
2885                    ),
2886                    (
2887                        FakePing {
2888                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2889                            gateway_response: true,
2890                            internet_response: true,
2891                        },
2892                        FakeDig { response: None }, // Then, use one that fails
2893                        FakeFetch {
2894                            expected_url: Some("http://www.gstatic.com/generate_204"),
2895                            response: None,
2896                        },
2897                    ),
2898                ],
2899                None,
2900                ping_internet_addr,
2901                None,
2902            ),
2903            vec![
2904                State {
2905                    link: LinkState::Internet,
2906                    application: ApplicationState { dns_resolved: true, ..Default::default() }
2907                },
2908                State {
2909                    link: LinkState::Internet,
2910                    application: ApplicationState { dns_resolved: true, ..Default::default() }
2911                }
2912            ],
2913            "Fail DNS on second check; fetch fails; no pause"
2914        );
2915
2916        assert_eq!(
2917            run_network_check_partial_properties_repeated(
2918                &mut exec,
2919                ETHERNET_INTERFACE_NAME,
2920                ID1,
2921                &route_table,
2922                vec![
2923                    (
2924                        FakePing {
2925                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2926                            gateway_response: true,
2927                            internet_response: true,
2928                        },
2929                        FakeDig::new(vec![std_ip!("1.2.3.0"), std_ip!("123::")]), // First, use a good digger
2930                        FakeFetch {
2931                            expected_url: Some("http://www.gstatic.com/generate_204"),
2932                            response: None
2933                        },
2934                    ),
2935                    (
2936                        FakePing {
2937                            gateway_addrs: std::iter::once(net1_gateway_ext).collect(),
2938                            gateway_response: true,
2939                            internet_response: true,
2940                        },
2941                        FakeDig { response: None }, // Then, use one that fails
2942                        FakeFetch {
2943                            expected_url: Some("http://www.gstatic.com/generate_204"),
2944                            response: None
2945                        },
2946                    ),
2947                ],
2948                None,
2949                ping_internet_addr,
2950                Some(DNS_PROBE_PERIOD),
2951            ),
2952            vec![
2953                State {
2954                    link: LinkState::Internet,
2955                    application: ApplicationState { dns_resolved: true, ..Default::default() }
2956                },
2957                State {
2958                    link: LinkState::Internet,
2959                    application: ApplicationState { dns_resolved: false, ..Default::default() }
2960                }
2961            ],
2962            "Fail DNS on second check; fetch fails"
2963        );
2964    }
2965
2966    #[test]
2967    fn test_network_check_varying_properties() {
2968        let properties = fnet_interfaces_ext::Properties {
2969            id: ID1.try_into().expect("should be nonzero"),
2970            name: ETHERNET_INTERFACE_NAME.to_string(),
2971            port_class: fnet_interfaces_ext::PortClass::Ethernet,
2972            has_default_ipv4_route: true,
2973            has_default_ipv6_route: true,
2974            online: true,
2975            addresses: vec![
2976                fnet_interfaces_ext::Address {
2977                    addr: fidl_subnet!("1.2.3.0/24"),
2978                    valid_until: fnet_interfaces_ext::NoInterest,
2979                    preferred_lifetime_info: fnet_interfaces_ext::NoInterest,
2980                    assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
2981                },
2982                fnet_interfaces_ext::Address {
2983                    addr: fidl_subnet!("123::4/64"),
2984                    valid_until: fnet_interfaces_ext::NoInterest,
2985                    preferred_lifetime_info: fnet_interfaces_ext::NoInterest,
2986                    assignment_state: fnet_interfaces::AddressAssignmentState::Assigned,
2987                },
2988            ],
2989            port_identity_koid: Default::default(),
2990        };
2991        let local_routes = testutil::build_route_table_from_flattened_routes([
2992            Route {
2993                destination: fidl_subnet!("1.2.3.0/24"),
2994                outbound_interface: ID1,
2995                next_hop: None,
2996            },
2997            Route {
2998                destination: fidl_subnet!("123::/64"),
2999                outbound_interface: ID1,
3000                next_hop: None,
3001            },
3002        ]);
3003        let route_table = testutil::build_route_table_from_flattened_routes([
3004            Route {
3005                destination: fidl_subnet!("0.0.0.0/0"),
3006                outbound_interface: ID1,
3007                next_hop: Some(fidl_ip!("1.2.3.1")),
3008            },
3009            Route {
3010                destination: fidl_subnet!("::0/0"),
3011                outbound_interface: ID1,
3012                next_hop: Some(fidl_ip!("123::1")),
3013            },
3014        ]);
3015        let route_table2 = testutil::build_route_table_from_flattened_routes([
3016            Route {
3017                destination: fidl_subnet!("0.0.0.0/0"),
3018                outbound_interface: ID1,
3019                next_hop: Some(fidl_ip!("2.2.3.1")),
3020            },
3021            Route {
3022                destination: fidl_subnet!("::0/0"),
3023                outbound_interface: ID1,
3024                next_hop: Some(fidl_ip!("223::1")),
3025            },
3026        ]);
3027
3028        const NON_ETHERNET_INTERFACE_NAME: &str = "test01";
3029
3030        let mut exec = fasync::TestExecutor::new_with_fake_time();
3031        let time = fasync::MonotonicInstant::from_nanos(1_000_000_000);
3032        let () = exec.set_fake_time(time.into());
3033
3034        let got = run_network_check(
3035            &mut exec,
3036            &fnet_interfaces_ext::Properties {
3037                id: ID1.try_into().expect("should be nonzero"),
3038                name: NON_ETHERNET_INTERFACE_NAME.to_string(),
3039                port_class: fnet_interfaces_ext::PortClass::Virtual,
3040                online: false,
3041                has_default_ipv4_route: false,
3042                has_default_ipv6_route: false,
3043                addresses: vec![],
3044                port_identity_koid: Default::default(),
3045            },
3046            &Default::default(),
3047            None,
3048            FakePing::default(),
3049            FakeDig::default(),
3050            FakeFetch::default(),
3051        )
3052        .expect(
3053            "error calling network check with non-ethernet interface, no addresses, interface down",
3054        );
3055        assert_eq!(
3056            got,
3057            Some(IpVersions::construct(StateEvent {
3058                state: State { link: LinkState::Down, ..Default::default() },
3059                time
3060            }))
3061        );
3062
3063        let got = run_network_check(
3064            &mut exec,
3065            &fnet_interfaces_ext::Properties { online: false, ..properties.clone() },
3066            &Default::default(),
3067            None,
3068            FakePing::default(),
3069            FakeDig::default(),
3070            FakeFetch::default(),
3071        )
3072        .expect("error calling network check, want Down state");
3073        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3074            state: State { link: LinkState::Down, ..Default::default() },
3075            time,
3076        }));
3077        assert_eq!(got, want);
3078
3079        let got = run_network_check(
3080            &mut exec,
3081            &fnet_interfaces_ext::Properties {
3082                has_default_ipv4_route: false,
3083                has_default_ipv6_route: false,
3084                ..properties.clone()
3085            },
3086            &local_routes,
3087            None,
3088            FakePing::default(),
3089            FakeDig::default(),
3090            FakeFetch::default(),
3091        )
3092        .expect("error calling network check, want Local state due to no default routes");
3093        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3094            state: State { link: LinkState::Local, ..Default::default() },
3095            time,
3096        }));
3097        assert_eq!(got, want);
3098
3099        let got = run_network_check(
3100            &mut exec,
3101            &properties,
3102            &route_table2,
3103            None,
3104            FakePing::default(),
3105            FakeDig::default(),
3106            FakeFetch::default(),
3107        )
3108        .expect("error calling network check, want Local state due to no matching default route");
3109        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3110            state: State { link: LinkState::Local, ..Default::default() },
3111            time,
3112        }));
3113        assert_eq!(got, want);
3114
3115        let got = run_network_check(
3116            &mut exec,
3117            &properties,
3118            &route_table,
3119            None,
3120            FakePing {
3121                gateway_addrs: [std_ip!("1.2.3.1"), std_ip!("123::1")].into_iter().collect(),
3122                gateway_response: true,
3123                internet_response: false,
3124            },
3125            FakeDig::default(),
3126            FakeFetch::default(),
3127        )
3128        .expect("error calling network check, want Gateway state");
3129        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3130            state: State { link: LinkState::Gateway, ..Default::default() },
3131            time,
3132        }));
3133        assert_eq!(got, want);
3134
3135        let got = run_network_check(
3136            &mut exec,
3137            &properties,
3138            &route_table,
3139            None,
3140            FakePing {
3141                gateway_addrs: [std_ip!("1.2.3.1"), std_ip!("123::1")].into_iter().collect(),
3142                gateway_response: true,
3143                internet_response: true,
3144            },
3145            FakeDig::default(),
3146            FakeFetch::default(),
3147        )
3148        .expect("error calling network check, want Internet state");
3149        let want = Some(IpVersions::<StateEvent>::construct(StateEvent {
3150            state: State { link: LinkState::Internet, ..Default::default() },
3151            time,
3152        }));
3153        assert_eq!(got, want);
3154    }
3155
3156    fn update_delta(port: Delta<StateEvent>, system: Delta<SystemState>) -> StateDelta {
3157        StateDelta {
3158            port: IpVersions { ipv4: port.clone(), ipv6: port },
3159            system: IpVersions { ipv4: system.clone(), ipv6: system },
3160        }
3161    }
3162
3163    #[test]
3164    fn test_state_info_update() {
3165        let if1_local_event = StateEvent::construct(LinkState::Local);
3166        let if1_local = IpVersions::<StateEvent>::construct(if1_local_event);
3167        // Post-update the system state should be Local due to interface 1.
3168        let mut state = StateInfo::default();
3169        let want = update_delta(
3170            Delta { previous: None, current: if1_local_event },
3171            Delta { previous: None, current: SystemState { id: ID1, state: if1_local_event } },
3172        );
3173        assert_eq!(state.update(ID1, if1_local.clone()), want);
3174        let want_state = StateInfo {
3175            per_interface: std::iter::once((ID1, if1_local.clone())).collect::<HashMap<_, _>>(),
3176            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3177        };
3178        assert_eq!(state, want_state);
3179
3180        let if2_gateway_event = StateEvent::construct(LinkState::Gateway);
3181        let if2_gateway = IpVersions::<StateEvent>::construct(if2_gateway_event);
3182        // Pre-update, the system state is Local due to interface 1; post-update the system state
3183        // will be Gateway due to interface 2.
3184        let want = update_delta(
3185            Delta { previous: None, current: if2_gateway_event },
3186            Delta {
3187                previous: Some(SystemState { id: ID1, state: if1_local_event }),
3188                current: SystemState { id: ID2, state: if2_gateway_event },
3189            },
3190        );
3191        assert_eq!(state.update(ID2, if2_gateway.clone()), want);
3192        let want_state = StateInfo {
3193            per_interface: [(ID1, if1_local.clone()), (ID2, if2_gateway.clone())]
3194                .into_iter()
3195                .collect::<HashMap<_, _>>(),
3196            system: IpVersions { ipv4: Some(ID2), ipv6: Some(ID2) },
3197        };
3198        assert_eq!(state, want_state);
3199
3200        let if2_removed_event = StateEvent::construct(LinkState::Removed);
3201        let if2_removed = IpVersions::<StateEvent>::construct(if2_removed_event);
3202        // Pre-update, the system state is Gateway due to interface 2; post-update the system state
3203        // will be Local due to interface 1.
3204        let want = update_delta(
3205            Delta { previous: Some(if2_gateway_event), current: if2_removed_event },
3206            Delta {
3207                previous: Some(SystemState { id: ID2, state: if2_gateway_event }),
3208                current: SystemState { id: ID1, state: if1_local_event },
3209            },
3210        );
3211        assert_eq!(state.update(ID2, if2_removed.clone()), want);
3212        let want_state = StateInfo {
3213            per_interface: [(ID1, if1_local.clone()), (ID2, if2_removed.clone())]
3214                .into_iter()
3215                .collect::<HashMap<_, _>>(),
3216            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3217        };
3218        assert_eq!(state, want_state);
3219    }
3220
3221    // Regression test against https://fxbug.dev/439597080
3222    // Confirm that a new event with the same state as the current system state does not change
3223    // the id of the system state value.
3224    #[test]
3225    fn test_state_info_update_same_link_state() {
3226        let if_local_event = StateEvent::construct(LinkState::Local);
3227        let if_local = IpVersions::<StateEvent>::construct(if_local_event);
3228        // Post-update the system state should be Local due to interface 1.
3229        let mut state = StateInfo::default();
3230        let want = update_delta(
3231            Delta { previous: None, current: if_local_event },
3232            Delta { previous: None, current: SystemState { id: ID1, state: if_local_event } },
3233        );
3234        assert_eq!(state.update(ID1, if_local.clone()), want);
3235        let want_state = StateInfo {
3236            per_interface: std::iter::once((ID1, if_local.clone())).collect::<HashMap<_, _>>(),
3237            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3238        };
3239        assert_eq!(state, want_state);
3240
3241        // Post-update the system state should be the same due to the interface 2 having the
3242        // same state.
3243        let want = update_delta(
3244            Delta { previous: None, current: if_local_event },
3245            Delta {
3246                previous: Some(SystemState { id: ID1, state: if_local_event }),
3247                current: SystemState { id: ID1, state: if_local_event },
3248            },
3249        );
3250        assert_eq!(state.update(ID2, if_local.clone()), want);
3251        let want_state = StateInfo {
3252            per_interface: [(ID1, if_local.clone()), (ID2, if_local.clone())]
3253                .into_iter()
3254                .collect::<HashMap<_, _>>(),
3255            system: IpVersions { ipv4: Some(ID1), ipv6: Some(ID1) },
3256        };
3257        assert_eq!(state, want_state);
3258
3259        // Post-update the system state should reflect interface 2 having the system state since
3260        // interface 1 is now at a strictly worse state.
3261        let if_removed_event = StateEvent::construct(LinkState::Removed);
3262        let if_removed = IpVersions::<StateEvent>::construct(if_removed_event);
3263        let want = update_delta(
3264            Delta { previous: Some(if_local_event), current: if_removed_event },
3265            Delta {
3266                previous: Some(SystemState { id: ID1, state: if_local_event }),
3267                current: SystemState { id: ID2, state: if_local_event },
3268            },
3269        );
3270        assert_eq!(state.update(ID1, if_removed.clone()), want);
3271        let want_state = StateInfo {
3272            per_interface: [(ID1, if_removed.clone()), (ID2, if_local.clone())]
3273                .into_iter()
3274                .collect::<HashMap<_, _>>(),
3275            system: IpVersions { ipv4: Some(ID2), ipv6: Some(ID2) },
3276        };
3277        assert_eq!(state, want_state);
3278    }
3279
3280    #[test_case(None::<LinkState>, None::<LinkState>, false, false, false, false;
3281        "no interfaces available")]
3282    #[test_case(Some(LinkState::Local), Some(LinkState::Local), false, false, false, false;
3283        "no interfaces with gateway or internet state")]
3284    #[test_case(Some(LinkState::Local), Some(LinkState::Gateway), false, false, false, true;
3285        "only one interface with gateway state or above")]
3286    #[test_case(Some(LinkState::Local), Some(LinkState::Internet), false, false, true, true;
3287        "only one interface with internet state")]
3288    #[test_case(Some(LinkState::Internet), Some(LinkState::Internet), false, false, true, true;
3289        "all interfaces with internet")]
3290    #[test_case(Some(LinkState::Internet), None::<LinkState>, false, false, true, true;
3291        "only one interface available, has internet state")]
3292    #[test_case(Some(LinkState::Local), Some((LinkState::Internet, true, false)), false, true, true, true;
3293        "only one interface with DNS resolved state")]
3294    #[test_case(Some((LinkState::Internet, true, false)), Some((LinkState::Internet, true, false)), false, true, true, true;
3295        "all interfaces with DNS resolved state")]
3296    #[test_case(Some((LinkState::Internet, true, false)), None::<LinkState>, false, true, true, true;
3297        "only one interface available, has DNS resolved state")]
3298    #[test_case(Some(LinkState::Local), Some((LinkState::Internet, true, true)), true, true, true, true;
3299        "only one interface with HTTP resolved state")]
3300    #[test_case(Some((LinkState::Internet, true, true)), Some((LinkState::Internet, true, true)), true, true, true, true;
3301        "all interfaces with HTTP resolved state")]
3302    #[test_case(Some((LinkState::Internet, true, true)), None::<LinkState>, true, true, true, true;
3303        "only one interface available, has HTTP resolved state")]
3304    #[test_case(Some((LinkState::Internet, false, true)), None::<LinkState>, true, false, true, true;
3305        "only one interface available, has HTTP resolved state, but no DNS")]
3306    fn test_system_has_state<S1, S2>(
3307        ipv4_state: Option<S1>,
3308        ipv6_state: Option<S2>,
3309        expect_http: bool,
3310        expect_dns: bool,
3311        expect_internet: bool,
3312        expect_gateway: bool,
3313    ) where
3314        StateEvent: Construct<S1>,
3315        StateEvent: Construct<S2>,
3316    {
3317        let if1 = ipv4_state
3318            .map(|state| IpVersions::<StateEvent>::construct(StateEvent::construct(state)));
3319        let if2 = ipv6_state
3320            .map(|state| IpVersions::<StateEvent>::construct(StateEvent::construct(state)));
3321
3322        let mut system_interfaces: HashMap<u64, IpVersions<StateEvent>> = HashMap::new();
3323
3324        let system_interface_ipv4 = if1.map(|interface| {
3325            let _ = system_interfaces.insert(ID1, interface);
3326            ID1
3327        });
3328
3329        let system_interface_ipv6 = if2.map(|interface| {
3330            let _ = system_interfaces.insert(ID2, interface);
3331            ID2
3332        });
3333
3334        let state = StateInfo {
3335            per_interface: system_interfaces,
3336            system: IpVersions { ipv4: system_interface_ipv4, ipv6: system_interface_ipv6 },
3337        };
3338
3339        assert_eq!(state.system_has_http(), expect_http);
3340        assert_eq!(state.system_has_dns(), expect_dns);
3341        assert_eq!(state.system_has_internet(), expect_internet);
3342        assert_eq!(state.system_has_gateway(), expect_gateway);
3343    }
3344
3345    #[test]
3346    fn test_resume_after_interface_removed() {
3347        use assert_matches::assert_matches;
3348
3349        let _exec = fasync::TestExecutor::new();
3350        let (sender, _receiver) = mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
3351        let mut monitor: Monitor<MonotonicInstant> = Monitor::new(sender).unwrap();
3352
3353        let properties = fnet_interfaces_ext::Properties {
3354            id: ID1.try_into().expect("should be nonzero"),
3355            name: ETHERNET_INTERFACE_NAME.to_string(),
3356            port_class: fnet_interfaces_ext::PortClass::Ethernet,
3357            online: false,
3358            addresses: vec![],
3359            has_default_ipv4_route: false,
3360            has_default_ipv6_route: false,
3361            port_identity_koid: Default::default(),
3362        };
3363
3364        // Insert a placeholder state so that the interface is tracked.
3365        let initial_state = IpVersions {
3366            ipv4: StateEvent {
3367                state: State { link: LinkState::None, ..Default::default() },
3368                time: fasync::MonotonicInstant::now(),
3369            },
3370            ipv6: StateEvent {
3371                state: State { link: LinkState::None, ..Default::default() },
3372                time: fasync::MonotonicInstant::now(),
3373            },
3374        };
3375        monitor.update_state(ID1, ETHERNET_INTERFACE_NAME, initial_state);
3376
3377        // Remove the interface. All future updates involving this interface should cause no
3378        // change to the interface's state.
3379        monitor.handle_interface_removed(properties.clone());
3380
3381        // Assert that the state is now `Removed`.
3382        let removed_state = monitor.state().get(ID1).unwrap();
3383        assert_eq!(removed_state.ipv4.state.link, LinkState::Removed);
3384        assert_eq!(removed_state.ipv6.state.link, LinkState::Removed);
3385
3386        // Start another iteration of the network check to ensure that any future state updates
3387        // do not affect the `Removed` state. In practice, the network check may be in-progress
3388        // when a removal event is received. That new state should not override the
3389        // `Removed` state.
3390        let routes = testutil::build_route_table_from_flattened_routes([]);
3391        let view = InterfaceView { properties: &properties, routes: &routes, neighbors: None };
3392        assert_matches!(monitor.begin(view), Ok(NetworkCheckerOutcome::Complete));
3393
3394        // Confirm that the LinkState discovered from the network check was `Down` and
3395        // not `Removed`.
3396        let interface_context = monitor.interface_context.get(&ID1).unwrap();
3397        assert_matches!(interface_context.discovered_state.ipv4.link, LinkState::Down);
3398        assert_matches!(interface_context.discovered_state.ipv6.link, LinkState::Down);
3399
3400        // Assert that the state is still `Removed`, and was not updated to `Down`
3401        // by the completed network check's result.
3402        let final_state = monitor.state().get(ID1).unwrap();
3403        assert_eq!(final_state.ipv4.state.link, LinkState::Removed);
3404        assert_eq!(final_state.ipv6.state.link, LinkState::Removed);
3405    }
3406}