reachability_core/
lib.rs

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