Skip to main content

reachability_core/
lib.rs

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