reachability/
eventloop.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//! The special-purpose event loop used by the reachability monitor.
6//!
7//! This event loop receives events from netstack. Thsose events are used by the reachability
8//! monitor to infer the connectivity state.
9
10use reachability_handler::ReachabilityState;
11
12use anyhow::{Context as _, anyhow};
13use fidl_fuchsia_net_interfaces_ext::{self as fnet_interfaces_ext, Update as _};
14use fuchsia_async::{self as fasync};
15use fuchsia_inspect::Inspector;
16use fuchsia_inspect::health::Reporter;
17use futures::channel::mpsc;
18use futures::prelude::*;
19use futures::select;
20use log::{debug, error, info, warn};
21use named_timer::NamedTimeoutExt;
22use reachability_core::dig::Dig;
23use reachability_core::fetch::Fetch;
24use reachability_core::ping::Ping;
25use reachability_core::route_table::RouteTable;
26use reachability_core::telemetry::{self, TelemetryEvent, TelemetrySender};
27use reachability_core::{
28    FIDL_TIMEOUT_ID, InterfaceView, Monitor, NeighborCache, NetworkCheckAction, NetworkCheckCookie,
29    NetworkCheckResult, NetworkChecker, NetworkCheckerOutcome, watchdog,
30};
31use reachability_handler::ReachabilityHandler;
32use std::collections::{HashMap, HashSet};
33use std::pin::pin;
34use {
35    fidl_fuchsia_hardware_network as fhardware_network, fidl_fuchsia_net_debug as fnet_debug,
36    fidl_fuchsia_net_interfaces as fnet_interfaces, fidl_fuchsia_net_neighbor as fnet_neighbor,
37    fidl_fuchsia_net_routes as fnet_routes, fidl_fuchsia_net_routes_ext as fnet_routes_ext,
38};
39
40const REPORT_PERIOD: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(60);
41const PROBE_PERIOD: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(60);
42const FIDL_TIMEOUT: zx::MonotonicDuration = zx::MonotonicDuration::from_seconds(90);
43
44struct SystemDispatcher;
45
46#[async_trait::async_trait]
47impl watchdog::SystemDispatcher for SystemDispatcher {
48    type DeviceDiagnostics = DeviceDiagnosticsProvider;
49
50    async fn log_debug_info(&self) -> Result<(), watchdog::Error> {
51        let diagnostics =
52            fuchsia_component::client::connect_to_protocol::<fnet_debug::DiagnosticsMarker>()
53                .map_err(|e| {
54                    error!(e:? = e; "failed to connect to protocol");
55                    watchdog::Error::NotSupported
56                })?;
57        diagnostics
58            .log_debug_info_to_syslog()
59            .map_err(watchdog::Error::Fidl)
60            .on_timeout_named(&FIDL_TIMEOUT_ID, FIDL_TIMEOUT, || Err(watchdog::Error::Timeout))
61            .await
62    }
63
64    fn get_device_diagnostics(
65        &self,
66        interface: u64,
67    ) -> Result<Self::DeviceDiagnostics, watchdog::Error> {
68        let interfaces_debug =
69            fuchsia_component::client::connect_to_protocol::<fnet_debug::InterfacesMarker>()
70                .map_err(|e| {
71                    error!(e:? = e; "failed to connect to protocol");
72                    watchdog::Error::NotSupported
73                })?;
74        let (port, server_end) = fidl::endpoints::create_proxy();
75        interfaces_debug.get_port(interface, server_end)?;
76
77        let (diagnostics, server_end) = fidl::endpoints::create_proxy();
78        port.get_diagnostics(server_end)?;
79
80        Ok(DeviceDiagnosticsProvider { interface, diagnostics, port })
81    }
82}
83
84struct DeviceDiagnosticsProvider {
85    interface: u64,
86    diagnostics: fhardware_network::DiagnosticsProxy,
87    port: fhardware_network::PortProxy,
88}
89
90#[async_trait::async_trait]
91impl watchdog::DeviceDiagnosticsProvider for DeviceDiagnosticsProvider {
92    async fn get_counters(&self) -> Result<watchdog::DeviceCounters, watchdog::Error> {
93        self.port
94            .get_counters()
95            .map_err(watchdog::Error::Fidl)
96            .and_then(
97                |fhardware_network::PortGetCountersResponse { rx_frames, tx_frames, .. }| match (
98                    rx_frames, tx_frames,
99                ) {
100                    (Some(rx_frames), Some(tx_frames)) => {
101                        futures::future::ok(watchdog::DeviceCounters { rx_frames, tx_frames })
102                    }
103                    (None, Some(_)) | (Some(_), None) | (None, None) => {
104                        error!(iface = self.interface; "missing information from port counters");
105                        futures::future::err(watchdog::Error::NotSupported)
106                    }
107                },
108            )
109            .on_timeout_named(&FIDL_TIMEOUT_ID, FIDL_TIMEOUT, || Err(watchdog::Error::Timeout))
110            .await
111    }
112
113    async fn log_debug_info(&self) -> Result<(), watchdog::Error> {
114        self.diagnostics
115            .log_debug_info_to_syslog()
116            .map_err(watchdog::Error::Fidl)
117            .on_timeout_named(&FIDL_TIMEOUT_ID, FIDL_TIMEOUT, || Err(watchdog::Error::Timeout))
118            .await
119    }
120}
121
122type Watchdog = watchdog::Watchdog<SystemDispatcher>;
123
124async fn handle_network_check_message<'a>(
125    netcheck_futures: &mut futures::stream::FuturesUnordered<
126        futures::future::BoxFuture<'a, (NetworkCheckCookie, NetworkCheckResult)>,
127    >,
128    msg: Option<(NetworkCheckAction, NetworkCheckCookie)>,
129) {
130    let (action, cookie) = msg
131        .context("network check receiver unexpectedly closed")
132        .unwrap_or_else(|err| exit_with_anyhow_error(err));
133    match action {
134        NetworkCheckAction::Ping(parameters) => {
135            netcheck_futures.push(Box::pin(async move {
136                let success = reachability_core::ping::Pinger
137                    .ping(&parameters.interface_name, parameters.addr)
138                    .await;
139                (cookie, NetworkCheckResult::Ping { parameters, success })
140            }));
141        }
142        NetworkCheckAction::ResolveDns(parameters) => {
143            netcheck_futures.push(Box::pin(async move {
144                let ips = reachability_core::dig::Digger::new()
145                    .dig(&parameters.interface_name, &parameters.domain)
146                    .await;
147                (cookie, NetworkCheckResult::ResolveDns { parameters, ips })
148            }));
149        }
150        NetworkCheckAction::Fetch(parameters) => {
151            netcheck_futures.push(Box::pin(async move {
152                let status = reachability_core::fetch::Fetcher
153                    .fetch(
154                        &parameters.interface_name,
155                        &parameters.domain,
156                        &parameters.path,
157                        &parameters.ip,
158                    )
159                    .await;
160                (cookie, NetworkCheckResult::Fetch { parameters, status })
161            }));
162        }
163    }
164}
165
166/// The event loop.
167pub struct EventLoop {
168    monitor: Monitor,
169    handler: ReachabilityHandler,
170    watchdog: Watchdog,
171    interface_properties: HashMap<
172        u64,
173        fnet_interfaces_ext::PropertiesAndState<(), fnet_interfaces_ext::DefaultInterest>,
174    >,
175    neighbor_cache: NeighborCache,
176    routes: RouteTable,
177    telemetry_sender: Option<TelemetrySender>,
178    network_check_receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
179    inspector: &'static Inspector,
180}
181
182impl EventLoop {
183    /// `new` returns an `EventLoop` instance.
184    pub fn new(
185        monitor: Monitor,
186        handler: ReachabilityHandler,
187        network_check_receiver: mpsc::UnboundedReceiver<(NetworkCheckAction, NetworkCheckCookie)>,
188        inspector: &'static Inspector,
189    ) -> Self {
190        fuchsia_inspect::component::health().set_starting_up();
191        EventLoop {
192            monitor,
193            handler,
194            watchdog: Watchdog::new(),
195            interface_properties: Default::default(),
196            neighbor_cache: Default::default(),
197            routes: Default::default(),
198            telemetry_sender: None,
199            network_check_receiver,
200            inspector,
201        }
202    }
203
204    /// `run` starts the event loop.
205    pub async fn run(&mut self) -> Result<(), anyhow::Error> {
206        use fuchsia_component::client::connect_to_protocol;
207
208        let cobalt_svc = fuchsia_component::client::connect_to_protocol::<
209            fidl_fuchsia_metrics::MetricEventLoggerFactoryMarker,
210        >()
211        .context("connect to metrics service")
212        .unwrap_or_else(|err| exit_with_anyhow_error(err));
213
214        let cobalt_proxy = match telemetry::create_metrics_logger(cobalt_svc).await {
215            Ok(proxy) => proxy,
216            Err(e) => {
217                warn!("Metrics logging is unavailable: {}", e);
218
219                // If it is not possible to acquire a metrics logging proxy, create a disconnected
220                // proxy and attempt to serve the policy API with metrics disabled.
221                let (proxy, _) = fidl::endpoints::create_proxy::<
222                    fidl_fuchsia_metrics::MetricEventLoggerMarker,
223                >();
224                proxy
225            }
226        };
227
228        let telemetry_inspect_node = self.inspector.root().create_child("telemetry");
229        let (telemetry_sender, telemetry_fut) =
230            telemetry::serve_telemetry(cobalt_proxy, telemetry_inspect_node);
231        let telemetry_fut = telemetry_fut.fuse();
232        self.telemetry_sender = Some(telemetry_sender.clone());
233        self.monitor.set_telemetry_sender(telemetry_sender);
234
235        let if_watcher_stream = {
236            let interface_state = connect_to_protocol::<fnet_interfaces::StateMarker>()
237                .context("network_manager failed to connect to interface state")
238                .unwrap_or_else(|err| exit_with_anyhow_error(err));
239            fnet_interfaces_ext::event_stream_from_state(
240                &interface_state,
241                fnet_interfaces_ext::IncludedAddresses::OnlyAssigned,
242            )
243            .context("get interface event stream")
244            .unwrap_or_else(|err| exit_with_anyhow_error(err))
245            .fuse()
246        };
247
248        let neigh_watcher_stream = {
249            let view = connect_to_protocol::<fnet_neighbor::ViewMarker>()
250                .context("failed to connect to neighbor view")
251                .unwrap_or_else(|err| exit_with_anyhow_error(err));
252            let (proxy, server_end) =
253                fidl::endpoints::create_proxy::<fnet_neighbor::EntryIteratorMarker>();
254            let () = view
255                .open_entry_iterator(server_end, &fnet_neighbor::EntryIteratorOptions::default())
256                .context("failed to open EntryIterator")
257                .unwrap_or_else(|err| exit_with_anyhow_error(err));
258            futures::stream::try_unfold(proxy, |proxy| {
259                proxy.get_next().map_ok(|e| {
260                    Some((
261                        futures::stream::iter(e.into_iter().map(Result::<_, fidl::Error>::Ok)),
262                        proxy,
263                    ))
264                })
265            })
266            .try_flatten()
267            .fuse()
268        };
269
270        let ipv4_route_event_stream = {
271            let state_v4 = connect_to_protocol::<fnet_routes::StateV4Marker>()
272                .context("failed to connect to fuchsia.net.routes/StateV4")
273                .unwrap_or_else(|err| exit_with_anyhow_error(err));
274            fnet_routes_ext::event_stream_from_state(&state_v4)
275                .context("failed to initialize a `WatcherV4` client")
276                .unwrap_or_else(|err| exit_with_anyhow_error(err))
277                .fuse()
278        };
279        let ipv6_route_event_stream = {
280            let state_v6 = connect_to_protocol::<fnet_routes::StateV6Marker>()
281                .context("failed to connect to fuchsia.net.routes/StateV6")
282                .unwrap_or_else(|err| exit_with_anyhow_error(err));
283            fnet_routes_ext::event_stream_from_state(&state_v6)
284                .context("failed to initialize a `WatcherV6` client")
285                .unwrap_or_else(|err| exit_with_anyhow_error(err))
286                .fuse()
287        };
288
289        let mut probe_futures = futures::stream::FuturesUnordered::new();
290        let mut netcheck_futures = futures::stream::FuturesUnordered::new();
291        let report_stream = fasync::Interval::new(REPORT_PERIOD).fuse();
292
293        let mut if_watcher_stream = pin!(if_watcher_stream);
294        let mut report_stream = pin!(report_stream);
295        let mut neigh_watcher_stream = pin!(neigh_watcher_stream);
296        let mut ipv4_route_event_stream = pin!(ipv4_route_event_stream);
297        let mut ipv6_route_event_stream = pin!(ipv6_route_event_stream);
298        let mut telemetry_fut = pin!(telemetry_fut);
299
300        // Establish the current routing table state.
301        let (v4_routes, v6_routes) = futures::join!(
302            fnet_routes_ext::collect_routes_until_idle::<_, HashSet<_>>(
303                ipv4_route_event_stream.by_ref(),
304            ),
305            fnet_routes_ext::collect_routes_until_idle::<_, HashSet<_>>(
306                ipv6_route_event_stream.by_ref(),
307            )
308        );
309        self.routes = RouteTable::new_with_existing_routes(
310            v4_routes
311                .context("get existing IPv4 routes")
312                .unwrap_or_else(|err| exit_with_anyhow_error(err)),
313            v6_routes
314                .context("get existing IPv6 routes")
315                .unwrap_or_else(|err| exit_with_anyhow_error(err)),
316        );
317
318        debug!("starting event loop");
319
320        fuchsia_inspect::component::health().set_ok();
321
322        loop {
323            select! {
324                if_watcher_res = if_watcher_stream.try_next() => {
325                    if let Some(id) = self.handle_interface_watcher_result(if_watcher_res).await {
326                        probe_futures.push(fasync::Interval::new(PROBE_PERIOD).map(move |()| id).into_future());
327                    }
328                },
329                neigh_res = neigh_watcher_stream.try_next() => {
330                    let event = neigh_res
331                        .unwrap_or_else(|err| exit_with_anyhow_error(
332                            anyhow!("neighbor watcher event stream fidl error: {err}")
333                        ))
334                        .unwrap_or_else(|| exit_with_anyhow_error(
335                            anyhow!("neighbor event stream ended")
336                        ));
337                    self.neighbor_cache.process_neighbor_event(event);
338                }
339                route_v4_res = ipv4_route_event_stream.try_next() => {
340                    let event = route_v4_res
341                        .unwrap_or_else(|err| exit_with_anyhow_error(
342                            anyhow!("ipv4 route watch error: {err}")
343                        ))
344                        .unwrap_or_else(|| exit_with_anyhow_error(
345                            anyhow!("ipv4 route event stream ended")
346                        ));
347                    self.handle_route_watcher_event(event);
348                }
349                route_v6_res = ipv6_route_event_stream.try_next() => {
350                    let event = route_v6_res
351                        .unwrap_or_else(|err| exit_with_anyhow_error(
352                            anyhow!("ipv6 route watch error: {err}")
353                        ))
354                        .unwrap_or_else(|| exit_with_anyhow_error(
355                            anyhow!("ipv6 route event stream ended")
356                        ));
357                    self.handle_route_watcher_event(event);
358                }
359                report = report_stream.next() => {
360                    let () = report
361                        .context("periodic timer for reporting unexpectedly ended")
362                        .unwrap_or_else(|err| exit_with_anyhow_error(err));
363                    let () = self.monitor.report_state();
364                },
365                probe = probe_futures.select_next_some() => {
366                    match probe {
367                        (Some(id), stream) => {
368                            if let Some(fnet_interfaces_ext::PropertiesAndState { properties, state: _ }) = self.interface_properties.get(&id) {
369                                let () = Self::begin_network_check(
370                                    &mut self.monitor,
371                                    &mut self.watchdog,
372                                    &mut self.handler,
373                                    properties,
374                                    &self.routes,
375                                    &self.neighbor_cache
376                                ).await;
377
378                                let () = probe_futures.push(stream.into_future());
379                            }
380                        }
381                        (None, _) => {
382                            exit_with_anyhow_error(anyhow!(
383                                "probe timer for probing reachability unexpectedly ended"
384                            ));
385                        }
386                    }
387                },
388                msg = self.network_check_receiver.next() => {
389                    handle_network_check_message(&mut netcheck_futures, msg).await;
390                },
391                netcheck_res = netcheck_futures.select_next_some() => {
392                    self.handle_netcheck_response(netcheck_res).await;
393                },
394                telemetry_res = telemetry_fut => exit_with_anyhow_error(
395                    anyhow!("unexpectedly stopped serving telemetry: {telemetry_res:?}")
396                ),
397            }
398        }
399    }
400
401    async fn handle_interface_watcher_result(
402        &mut self,
403        if_watcher_res: Result<
404            Option<fnet_interfaces_ext::EventWithInterest<fnet_interfaces_ext::DefaultInterest>>,
405            fidl::Error,
406        >,
407    ) -> Option<u64> {
408        let event = if_watcher_res
409            .unwrap_or_else(|err| {
410                exit_with_anyhow_error(anyhow!("interface watcher event stream fidl error: {err}"))
411            })
412            .unwrap_or_else(|| {
413                exit_with_anyhow_error(anyhow!("interface watcher stream unexpectedly ended"))
414            });
415
416        // Only proceed with tracking interfaces in `interface_properties` if
417        // the interface should be monitored.
418        let should_monitor_interface = match event.inner() {
419            fnet_interfaces::Event::Existing(properties)
420            | fnet_interfaces::Event::Added(properties) => Self::should_monitor_interface(
421                properties
422                    .port_class
423                    .clone()
424                    .expect("non-Changed Events should have port_class present"),
425            ),
426            // Changed and Removed events are preceded by a previous Existing
427            // or Added event that add the interface to `interface_properties`.
428            // When the interface does not exist in the map, don't process the
429            // Changed or Removed event.
430            fnet_interfaces::Event::Changed(fnet_interfaces::Properties { id, .. }) => self
431                .interface_properties
432                .contains_key(&id.expect("Changed events must include the interface id")),
433            fnet_interfaces::Event::Removed(id) => self.interface_properties.contains_key(id),
434            fnet_interfaces::Event::Idle(_) => true,
435        };
436        if !should_monitor_interface {
437            return None;
438        }
439
440        let discovered_id = self
441            .handle_interface_watcher_event(event)
442            .await
443            .unwrap_or_else(|err| exit_with_anyhow_error(err));
444        if let Some(id) = discovered_id {
445            if let Some(telemetry_sender) = &self.telemetry_sender {
446                let has_default_ipv4_route =
447                    self.interface_properties.values().any(|p| p.properties.has_default_ipv4_route);
448                let has_default_ipv6_route =
449                    self.interface_properties.values().any(|p| p.properties.has_default_ipv6_route);
450                telemetry_sender.send(TelemetryEvent::NetworkConfig {
451                    has_default_ipv4_route,
452                    has_default_ipv6_route,
453                });
454            }
455            return Some(id);
456        }
457        None
458    }
459
460    async fn handle_interface_watcher_event(
461        &mut self,
462        event: fnet_interfaces_ext::EventWithInterest<fnet_interfaces_ext::DefaultInterest>,
463    ) -> Result<Option<u64>, anyhow::Error> {
464        match self
465            .interface_properties
466            .update(event)
467            .context("failed to update interface properties map with watcher event")?
468        {
469            fnet_interfaces_ext::UpdateResult::Added { properties, state: _ }
470            | fnet_interfaces_ext::UpdateResult::Existing { properties, state: _ } => {
471                let id = properties.id;
472                debug!("setting timer for interface {}", id);
473
474                let () = Self::begin_network_check(
475                    &mut self.monitor,
476                    &mut self.watchdog,
477                    &mut self.handler,
478                    properties,
479                    &self.routes,
480                    &self.neighbor_cache,
481                )
482                .await;
483
484                return Ok(Some(id.get()));
485            }
486            fnet_interfaces_ext::UpdateResult::Changed {
487                previous:
488                    fnet_interfaces::Properties {
489                        online,
490                        addresses,
491                        has_default_ipv4_route,
492                        has_default_ipv6_route,
493                        ..
494                    },
495                current:
496                    properties @ fnet_interfaces_ext::Properties {
497                        addresses: current_addresses,
498                        id: _,
499                        name: _,
500                        port_class: _,
501                        online: _,
502                        has_default_ipv4_route: _,
503                        has_default_ipv6_route: _,
504                        port_identity_koid: _,
505                    },
506                state: _,
507            } => {
508                // NOTE: Filter changed events to only changes that might affect
509                // reachability. This acts as a guard against too many
510                // reachability checks in case of fuchsia.net.interfaces adding
511                // more fields.
512                if online.is_some()
513                    || has_default_ipv4_route.is_some()
514                    || has_default_ipv6_route.is_some()
515                    || addresses.is_some_and(|addresses| {
516                        let previous = addresses
517                            .iter()
518                            .filter_map(|fnet_interfaces::Address { addr, .. }| addr.as_ref());
519                        let current = current_addresses.iter().map(
520                            |fnet_interfaces_ext::Address {
521                                 addr,
522                                 valid_until: _,
523                                 preferred_lifetime_info: _,
524                                 assignment_state,
525                             }| {
526                                assert_eq!(
527                                    *assignment_state,
528                                    fnet_interfaces::AddressAssignmentState::Assigned
529                                );
530                                addr
531                            },
532                        );
533                        previous.ne(current)
534                    })
535                {
536                    let () = Self::begin_network_check(
537                        &mut self.monitor,
538                        &mut self.watchdog,
539                        &mut self.handler,
540                        properties,
541                        &self.routes,
542                        &self.neighbor_cache,
543                    )
544                    .await;
545                }
546            }
547            fnet_interfaces_ext::UpdateResult::Removed(
548                fnet_interfaces_ext::PropertiesAndState { properties, state: () },
549            ) => {
550                self.watchdog.handle_interface_removed(properties.id.get());
551                self.monitor.handle_interface_removed(properties);
552            }
553            fnet_interfaces_ext::UpdateResult::NoChange => {}
554        }
555        Ok(None)
556    }
557
558    /// Determine whether an interface should be monitored or not.
559    fn should_monitor_interface(port_class: fnet_interfaces::PortClass) -> bool {
560        match port_class {
561            fnet_interfaces::PortClass::Loopback(_)
562            | fnet_interfaces::PortClass::Blackhole(_)
563            | fnet_interfaces::PortClass::Device(fhardware_network::PortClass::Lowpan) => false,
564            fnet_interfaces::PortClass::Device(fhardware_network::PortClass::Virtual)
565            | fnet_interfaces::PortClass::Device(fhardware_network::PortClass::Ethernet)
566            | fnet_interfaces::PortClass::Device(fhardware_network::PortClass::WlanClient)
567            | fnet_interfaces::PortClass::Device(fhardware_network::PortClass::WlanAp)
568            | fnet_interfaces::PortClass::Device(fhardware_network::PortClass::Ppp)
569            | fnet_interfaces::PortClass::Device(fhardware_network::PortClass::Bridge) => true,
570            fnet_interfaces::PortClass::Device(
571                fhardware_network::PortClass::__SourceBreaking { unknown_ordinal },
572            ) => {
573                log::warn!("unknown fhardware_network::PortClass ordinal {unknown_ordinal:?}");
574                false
575            }
576            fnet_interfaces::PortClass::__SourceBreaking { unknown_ordinal } => {
577                log::warn!("unknown fnet_interfaces::PortClass ordinal {unknown_ordinal:?}");
578                false
579            }
580        }
581    }
582
583    /// Handles events observed by the route watchers by adding/removing routes
584    /// from the underlying `RouteTable`.
585    ///
586    /// # Panics
587    ///
588    /// Panics if the given event is unexpected (e.g. not an add or remove).
589    pub fn handle_route_watcher_event<I: net_types::ip::Ip>(
590        &mut self,
591        event: fnet_routes_ext::Event<I>,
592    ) {
593        match event {
594            fnet_routes_ext::Event::Added(route) => {
595                if !self.routes.add_route(route) {
596                    error!("Received add event for already existing route: {:?}", route)
597                }
598            }
599            fnet_routes_ext::Event::Removed(route) => {
600                if !self.routes.remove_route(&route) {
601                    error!("Received removed event for non-existing route: {:?}", route)
602                }
603            }
604            // Note that we don't expect to observe any existing events, because
605            // the route watchers were drained of existing events prior to
606            // starting the event loop.
607            fnet_routes_ext::Event::Existing(_)
608            | fnet_routes_ext::Event::Idle
609            | fnet_routes_ext::Event::Unknown => {
610                panic!("route watcher observed unexpected event: {:?}", event)
611            }
612        }
613    }
614
615    async fn begin_network_check(
616        monitor: &mut Monitor,
617        watchdog: &mut Watchdog,
618        handler: &mut ReachabilityHandler,
619        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
620        routes: &RouteTable,
621        neighbor_cache: &NeighborCache,
622    ) {
623        let view = InterfaceView {
624            properties,
625            routes,
626            neighbors: neighbor_cache.get_interface_neighbors(properties.id.get()),
627        };
628
629        // TODO(https://fxbug.dev/42074495): Move watchdog into its own future in the eventloop to prevent
630        // network check reliance on the watchdog completing.
631        let () = watchdog
632            .check_interface_state(zx::MonotonicInstant::get(), &SystemDispatcher {}, view)
633            .await;
634
635        let (system_internet, system_gateway, system_dns, system_http) = {
636            match monitor.begin(view) {
637                Ok(NetworkCheckerOutcome::MustResume) => return,
638                Ok(NetworkCheckerOutcome::Complete) => {
639                    let monitor_state = monitor.state();
640                    (
641                        monitor_state.system_has_internet(),
642                        monitor_state.system_has_gateway(),
643                        monitor_state.system_has_dns(),
644                        monitor_state.system_has_http(),
645                    )
646                }
647                Err(e) => {
648                    info!("begin network check error: {:?}", e);
649                    return;
650                }
651            }
652        };
653
654        handler
655            .replace_state(ReachabilityState {
656                internet_available: system_internet,
657                gateway_reachable: system_gateway,
658                dns_active: system_dns,
659                http_active: system_http,
660            })
661            .await;
662    }
663
664    // TODO(https://fxbug.dev/42076412): handle_netcheck_response and handle_network_check_message are missing
665    // tests because they reply on NetworkCheckCookie, which cannot be created in the event loop.
666    async fn handle_netcheck_response(
667        &mut self,
668        (cookie, result): (NetworkCheckCookie, NetworkCheckResult),
669    ) {
670        match self.monitor.resume(cookie, result) {
671            Ok(NetworkCheckerOutcome::MustResume) => {}
672            Ok(NetworkCheckerOutcome::Complete) => {
673                let (system_internet, system_gateway, system_dns, system_http) = {
674                    let monitor_state = self.monitor.state();
675                    (
676                        monitor_state.system_has_internet(),
677                        monitor_state.system_has_gateway(),
678                        monitor_state.system_has_dns(),
679                        monitor_state.system_has_http(),
680                    )
681                };
682
683                self.handler
684                    .replace_state(ReachabilityState {
685                        internet_available: system_internet,
686                        gateway_reachable: system_gateway,
687                        dns_active: system_dns,
688                        http_active: system_http,
689                    })
690                    .await;
691            }
692            Err(e) => error!("resume network check error: {:?}", e),
693        }
694    }
695}
696
697/// If we encounter an unrecoverable state, log an error and exit.
698//
699// TODO(https://fxbug.dev/42070352): add a test that works as intended.
700fn exit_with_anyhow_error(cause: anyhow::Error) -> ! {
701    error!(cause:%; "exiting due to error");
702    std::process::exit(1);
703}
704
705#[cfg(test)]
706mod tests {
707    use super::*;
708    #[allow(unused)]
709    use assert_matches::assert_matches as _;
710    use fidl_fuchsia_net::{IpAddress, Ipv4Address, Ipv6Address, Subnet};
711    use fidl_fuchsia_net_interfaces::{Address, AddressAssignmentState, Event, Properties};
712    use net_declare::{std_ip_v4, std_ip_v6};
713
714    fn create_eventloop() -> EventLoop {
715        let handler = ReachabilityHandler::new();
716        let inspector = fuchsia_inspect::component::inspector();
717        let (sender, receiver) =
718            futures::channel::mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
719        let mut monitor = Monitor::new(sender).expect("failed to create reachability monitor");
720        let () = monitor.set_inspector(inspector);
721
722        return EventLoop::new(monitor, handler, receiver, inspector);
723    }
724
725    #[fuchsia::test]
726    async fn test_handle_interface_watcher_result_ipv4() {
727        let mut event_loop = create_eventloop();
728
729        let v4_subnet = Subnet {
730            addr: IpAddress::Ipv4(Ipv4Address { addr: std_ip_v4!("192.0.2.1").octets() }),
731            prefix_len: 16,
732        };
733
734        let addr = Address {
735            addr: Some(v4_subnet),
736            assignment_state: Some(AddressAssignmentState::Assigned),
737            ..Default::default()
738        };
739
740        let mut props = Properties {
741            id: Some(12345),
742            addresses: Some(vec![addr]),
743            online: Some(true),
744            port_class: Some(fnet_interfaces::PortClass::Device(
745                fidl_fuchsia_hardware_network::PortClass::Ethernet,
746            )),
747            has_default_ipv4_route: Some(true),
748            has_default_ipv6_route: Some(true),
749            name: Some("IPv4 Reachability Test Interface".to_string()),
750            ..Default::default()
751        };
752
753        let event_res = Ok(Some(Event::Existing(props.clone()).into()));
754        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(12345));
755
756        props.id = Some(54321);
757        let event_res = Ok(Some(Event::Added(props.clone()).into()));
758        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(54321));
759    }
760
761    #[fuchsia::test]
762    async fn test_handle_interface_watcher_result_ipv6() {
763        let mut event_loop = create_eventloop();
764
765        let v6_subnet = Subnet {
766            addr: IpAddress::Ipv6(Ipv6Address { addr: std_ip_v6!("2001:db8::1").octets() }),
767            prefix_len: 16,
768        };
769
770        let addr = Address {
771            addr: Some(v6_subnet),
772            assignment_state: Some(AddressAssignmentState::Assigned),
773            ..Default::default()
774        };
775
776        let mut props = Properties {
777            id: Some(12345),
778            addresses: Some(vec![addr]),
779            online: Some(true),
780            port_class: Some(fnet_interfaces::PortClass::Device(
781                fidl_fuchsia_hardware_network::PortClass::Ethernet,
782            )),
783            has_default_ipv4_route: Some(true),
784            has_default_ipv6_route: Some(true),
785            name: Some("IPv6 Reachability Test Interface".to_string()),
786            ..Default::default()
787        };
788
789        let event_res = Ok(Some(Event::Existing(props.clone()).into()));
790        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(12345));
791
792        props.id = Some(54321);
793        let event_res = Ok(Some(Event::Added(props.clone()).into()));
794        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(54321));
795    }
796}