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::{anyhow, Context as _};
13use fidl_fuchsia_net_interfaces_ext::{self as fnet_interfaces_ext, Update as _};
14use fuchsia_async::{self as fasync};
15use fuchsia_inspect::health::Reporter;
16use fuchsia_inspect::Inspector;
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    watchdog, InterfaceView, Monitor, NeighborCache, NetworkCheckAction, NetworkCheckCookie,
29    NetworkCheckResult, NetworkChecker, NetworkCheckerOutcome, FIDL_TIMEOUT_ID,
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_fut => exit_with_anyhow_error(anyhow!("unexpectedly stopped serving telemetry")),
395            }
396        }
397    }
398
399    async fn handle_interface_watcher_result(
400        &mut self,
401        if_watcher_res: Result<
402            Option<fnet_interfaces_ext::EventWithInterest<fnet_interfaces_ext::DefaultInterest>>,
403            fidl::Error,
404        >,
405    ) -> Option<u64> {
406        let event = if_watcher_res
407            .unwrap_or_else(|err| {
408                exit_with_anyhow_error(anyhow!("interface watcher event stream fidl error: {err}"))
409            })
410            .unwrap_or_else(|| {
411                exit_with_anyhow_error(anyhow!("interface watcher stream unexpectedly ended"))
412            });
413        let discovered_id = self
414            .handle_interface_watcher_event(event)
415            .await
416            .unwrap_or_else(|err| exit_with_anyhow_error(err));
417        if let Some(id) = discovered_id {
418            if let Some(telemetry_sender) = &self.telemetry_sender {
419                let has_default_ipv4_route =
420                    self.interface_properties.values().any(|p| p.properties.has_default_ipv4_route);
421                let has_default_ipv6_route =
422                    self.interface_properties.values().any(|p| p.properties.has_default_ipv6_route);
423                telemetry_sender.send(TelemetryEvent::NetworkConfig {
424                    has_default_ipv4_route,
425                    has_default_ipv6_route,
426                });
427            }
428            return Some(id);
429        }
430        None
431    }
432
433    async fn handle_interface_watcher_event(
434        &mut self,
435        event: fnet_interfaces_ext::EventWithInterest<fnet_interfaces_ext::DefaultInterest>,
436    ) -> Result<Option<u64>, anyhow::Error> {
437        match self
438            .interface_properties
439            .update(event)
440            .context("failed to update interface properties map with watcher event")?
441        {
442            fnet_interfaces_ext::UpdateResult::Added { properties, state: _ }
443            | fnet_interfaces_ext::UpdateResult::Existing { properties, state: _ } => {
444                if !Self::should_monitor_interface(&properties) {
445                    return Ok(None);
446                }
447
448                let id = properties.id;
449                debug!("setting timer for interface {}", id);
450
451                let () = Self::begin_network_check(
452                    &mut self.monitor,
453                    &mut self.watchdog,
454                    &mut self.handler,
455                    properties,
456                    &self.routes,
457                    &self.neighbor_cache,
458                )
459                .await;
460
461                return Ok(Some(id.get()));
462            }
463            fnet_interfaces_ext::UpdateResult::Changed {
464                previous:
465                    fnet_interfaces::Properties {
466                        online,
467                        addresses,
468                        has_default_ipv4_route,
469                        has_default_ipv6_route,
470                        ..
471                    },
472                current:
473                    properties @ fnet_interfaces_ext::Properties {
474                        addresses: current_addresses,
475                        id: _,
476                        name: _,
477                        port_class: _,
478                        online: _,
479                        has_default_ipv4_route: _,
480                        has_default_ipv6_route: _,
481                    },
482                state: _,
483            } => {
484                // NOTE: Filter changed events to only changes that might affect
485                // reachability. This acts as a guard against too many
486                // reachability checks in case of fuchsia.net.interfaces adding
487                // more fields.
488                if online.is_some()
489                    || has_default_ipv4_route.is_some()
490                    || has_default_ipv6_route.is_some()
491                    || addresses.is_some_and(|addresses| {
492                        let previous = addresses
493                            .iter()
494                            .filter_map(|fnet_interfaces::Address { addr, .. }| addr.as_ref());
495                        let current = current_addresses.iter().map(
496                            |fnet_interfaces_ext::Address {
497                                 addr,
498                                 valid_until: _,
499                                 preferred_lifetime_info: _,
500                                 assignment_state,
501                             }| {
502                                assert_eq!(
503                                    *assignment_state,
504                                    fnet_interfaces::AddressAssignmentState::Assigned
505                                );
506                                addr
507                            },
508                        );
509                        previous.ne(current)
510                    })
511                {
512                    let () = Self::begin_network_check(
513                        &mut self.monitor,
514                        &mut self.watchdog,
515                        &mut self.handler,
516                        properties,
517                        &self.routes,
518                        &self.neighbor_cache,
519                    )
520                    .await;
521                }
522            }
523            fnet_interfaces_ext::UpdateResult::Removed(
524                fnet_interfaces_ext::PropertiesAndState { properties, state: () },
525            ) => {
526                self.watchdog.handle_interface_removed(properties.id.get());
527                self.monitor.handle_interface_removed(properties);
528            }
529            fnet_interfaces_ext::UpdateResult::NoChange => {}
530        }
531        Ok(None)
532    }
533
534    /// Determine whether an interface should be monitored or not.
535    fn should_monitor_interface(
536        &fnet_interfaces_ext::Properties { port_class, .. }: &fnet_interfaces_ext::Properties<
537            fnet_interfaces_ext::DefaultInterest,
538        >,
539    ) -> bool {
540        return match port_class {
541            fnet_interfaces_ext::PortClass::Loopback
542            | fnet_interfaces_ext::PortClass::Blackhole
543            | fnet_interfaces_ext::PortClass::Lowpan => false,
544            fnet_interfaces_ext::PortClass::Virtual
545            | fnet_interfaces_ext::PortClass::Ethernet
546            | fnet_interfaces_ext::PortClass::WlanClient
547            | fnet_interfaces_ext::PortClass::WlanAp
548            | fnet_interfaces_ext::PortClass::Ppp
549            | fnet_interfaces_ext::PortClass::Bridge => true,
550        };
551    }
552
553    /// Handles events observed by the route watchers by adding/removing routes
554    /// from the underlying `RouteTable`.
555    ///
556    /// # Panics
557    ///
558    /// Panics if the given event is unexpected (e.g. not an add or remove).
559    pub fn handle_route_watcher_event<I: net_types::ip::Ip>(
560        &mut self,
561        event: fnet_routes_ext::Event<I>,
562    ) {
563        match event {
564            fnet_routes_ext::Event::Added(route) => {
565                if !self.routes.add_route(route) {
566                    error!("Received add event for already existing route: {:?}", route)
567                }
568            }
569            fnet_routes_ext::Event::Removed(route) => {
570                if !self.routes.remove_route(&route) {
571                    error!("Received removed event for non-existing route: {:?}", route)
572                }
573            }
574            // Note that we don't expect to observe any existing events, because
575            // the route watchers were drained of existing events prior to
576            // starting the event loop.
577            fnet_routes_ext::Event::Existing(_)
578            | fnet_routes_ext::Event::Idle
579            | fnet_routes_ext::Event::Unknown => {
580                panic!("route watcher observed unexpected event: {:?}", event)
581            }
582        }
583    }
584
585    async fn begin_network_check(
586        monitor: &mut Monitor,
587        watchdog: &mut Watchdog,
588        handler: &mut ReachabilityHandler,
589        properties: &fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
590        routes: &RouteTable,
591        neighbor_cache: &NeighborCache,
592    ) {
593        let view = InterfaceView {
594            properties,
595            routes,
596            neighbors: neighbor_cache.get_interface_neighbors(properties.id.get()),
597        };
598
599        // TODO(https://fxbug.dev/42074495): Move watchdog into its own future in the eventloop to prevent
600        // network check reliance on the watchdog completing.
601        let () = watchdog
602            .check_interface_state(zx::MonotonicInstant::get(), &SystemDispatcher {}, view)
603            .await;
604
605        let (system_internet, system_gateway, system_dns, system_http) = {
606            match monitor.begin(view) {
607                Ok(NetworkCheckerOutcome::MustResume) => return,
608                Ok(NetworkCheckerOutcome::Complete) => {
609                    let monitor_state = monitor.state();
610                    (
611                        monitor_state.system_has_internet(),
612                        monitor_state.system_has_gateway(),
613                        monitor_state.system_has_dns(),
614                        monitor_state.system_has_http(),
615                    )
616                }
617                Err(e) => {
618                    info!("begin network check error: {:?}", e);
619                    return;
620                }
621            }
622        };
623
624        handler
625            .replace_state(ReachabilityState {
626                internet_available: system_internet,
627                gateway_reachable: system_gateway,
628                dns_active: system_dns,
629                http_active: system_http,
630            })
631            .await;
632    }
633
634    // TODO(https://fxbug.dev/42076412): handle_netcheck_response and handle_network_check_message are missing
635    // tests because they reply on NetworkCheckCookie, which cannot be created in the event loop.
636    async fn handle_netcheck_response(
637        &mut self,
638        (cookie, result): (NetworkCheckCookie, NetworkCheckResult),
639    ) {
640        match self.monitor.resume(cookie, result) {
641            Ok(NetworkCheckerOutcome::MustResume) => {}
642            Ok(NetworkCheckerOutcome::Complete) => {
643                let (system_internet, system_gateway, system_dns, system_http) = {
644                    let monitor_state = self.monitor.state();
645                    (
646                        monitor_state.system_has_internet(),
647                        monitor_state.system_has_gateway(),
648                        monitor_state.system_has_dns(),
649                        monitor_state.system_has_http(),
650                    )
651                };
652
653                self.handler
654                    .replace_state(ReachabilityState {
655                        internet_available: system_internet,
656                        gateway_reachable: system_gateway,
657                        dns_active: system_dns,
658                        http_active: system_http,
659                    })
660                    .await;
661            }
662            Err(e) => error!("resume network check error: {:?}", e),
663        }
664    }
665}
666
667/// If we encounter an unrecoverable state, log an error and exit.
668//
669// TODO(https://fxbug.dev/42070352): add a test that works as intended.
670fn exit_with_anyhow_error(cause: anyhow::Error) -> ! {
671    error!(cause:%; "exiting due to error");
672    std::process::exit(1);
673}
674
675#[cfg(test)]
676mod tests {
677    use super::*;
678    #[allow(unused)]
679    use assert_matches::assert_matches as _;
680    use fidl_fuchsia_net::{IpAddress, Ipv4Address, Ipv6Address, Subnet};
681    use fidl_fuchsia_net_interfaces::{Address, AddressAssignmentState, Event, Properties};
682    use net_declare::{std_ip_v4, std_ip_v6};
683
684    fn create_eventloop() -> EventLoop {
685        let handler = ReachabilityHandler::new();
686        let inspector = fuchsia_inspect::component::inspector();
687        let (sender, receiver) =
688            futures::channel::mpsc::unbounded::<(NetworkCheckAction, NetworkCheckCookie)>();
689        let mut monitor = Monitor::new(sender).expect("failed to create reachability monitor");
690        let () = monitor.set_inspector(inspector);
691
692        return EventLoop::new(monitor, handler, receiver, inspector);
693    }
694
695    #[fuchsia::test]
696    async fn test_handle_interface_watcher_result_ipv4() {
697        let mut event_loop = create_eventloop();
698
699        let v4_subnet = Subnet {
700            addr: IpAddress::Ipv4(Ipv4Address { addr: std_ip_v4!("192.0.2.1").octets() }),
701            prefix_len: 16,
702        };
703
704        let addr = Address {
705            addr: Some(v4_subnet),
706            assignment_state: Some(AddressAssignmentState::Assigned),
707            ..Default::default()
708        };
709
710        let mut props = Properties {
711            id: Some(12345),
712            addresses: Some(vec![addr]),
713            online: Some(true),
714            port_class: Some(fnet_interfaces::PortClass::Device(
715                fidl_fuchsia_hardware_network::PortClass::Ethernet,
716            )),
717            has_default_ipv4_route: Some(true),
718            has_default_ipv6_route: Some(true),
719            name: Some("IPv4 Reachability Test Interface".to_string()),
720            ..Default::default()
721        };
722
723        let event_res = Ok(Some(Event::Existing(props.clone()).into()));
724        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(12345));
725
726        props.id = Some(54321);
727        let event_res = Ok(Some(Event::Added(props.clone()).into()));
728        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(54321));
729    }
730
731    #[fuchsia::test]
732    async fn test_handle_interface_watcher_result_ipv6() {
733        let mut event_loop = create_eventloop();
734
735        let v6_subnet = Subnet {
736            addr: IpAddress::Ipv6(Ipv6Address { addr: std_ip_v6!("2001:db8::1").octets() }),
737            prefix_len: 16,
738        };
739
740        let addr = Address {
741            addr: Some(v6_subnet),
742            assignment_state: Some(AddressAssignmentState::Assigned),
743            ..Default::default()
744        };
745
746        let mut props = Properties {
747            id: Some(12345),
748            addresses: Some(vec![addr]),
749            online: Some(true),
750            port_class: Some(fnet_interfaces::PortClass::Device(
751                fidl_fuchsia_hardware_network::PortClass::Ethernet,
752            )),
753            has_default_ipv4_route: Some(true),
754            has_default_ipv6_route: Some(true),
755            name: Some("IPv6 Reachability Test Interface".to_string()),
756            ..Default::default()
757        };
758
759        let event_res = Ok(Some(Event::Existing(props.clone()).into()));
760        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(12345));
761
762        props.id = Some(54321);
763        let event_res = Ok(Some(Event::Added(props.clone()).into()));
764        assert_eq!(event_loop.handle_interface_watcher_result(event_res).await, Some(54321));
765    }
766}