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