Skip to main content

reachability_core/
lib.rs

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