Skip to main content

reachability_core/
watchdog.rs

1// Copyright 2022 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//! Provides an interface health watchdog.
6//!
7//! The watchdog uses gateway neighbor reachability information and interface
8//! counters to evaluate interface health and triggers debug information dumps
9//! on the system logs when it finds unhealthy interfaces.
10
11use crate::neighbor_cache::NeighborHealth;
12use crate::{Id as InterfaceId, InterfaceView};
13use fidl_fuchsia_net_interfaces_ext as fnet_interfaces_ext;
14use itertools::Itertools as _;
15use log::{debug, error, info, warn};
16use std::collections::{HashMap, HashSet};
17
18/// The minimum amount of time for a device counter to be stuck in the same
19/// value for the device to be considered unhealthy.
20const DEVICE_COUNTERS_UNHEALTHY_TIME: zx::MonotonicDuration =
21    zx::MonotonicDuration::from_minutes(2);
22
23/// The minimum amount of time to wait before generating a new request for debug
24/// information.
25const DEBUG_INFO_COOLDOWN: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(15);
26
27/// The minimum amount of time for a neighbor in unhealthy state to trigger
28/// actions.
29const NEIGHBOR_UNHEALTHY_TIME: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(1);
30
31#[derive(Debug, thiserror::Error)]
32#[cfg_attr(test, derive(Clone))]
33pub enum Error {
34    #[error("Operation timed out")]
35    Timeout,
36    #[error("FIDL error {0}")]
37    Fidl(#[from] fidl::Error),
38    #[error("Unsupported operation")]
39    NotSupported,
40}
41
42#[derive(Debug)]
43#[cfg_attr(test, derive(Copy, Clone))]
44pub struct DeviceCounters {
45    pub rx_frames: u64,
46    pub tx_frames: u64,
47}
48
49#[derive(Debug)]
50#[cfg_attr(test, derive(Eq, PartialEq))]
51struct TimestampedCounter {
52    value: u64,
53    at: zx::MonotonicInstant,
54}
55
56impl TimestampedCounter {
57    fn update(&mut self, new_value: u64, new_at: zx::MonotonicInstant) -> bool {
58        let Self { value, at } = self;
59        if new_value != *value {
60            *at = new_at;
61            *value = new_value;
62            true
63        } else {
64            false
65        }
66    }
67}
68
69#[async_trait::async_trait]
70pub trait DeviceDiagnosticsProvider {
71    async fn get_counters(&self) -> Result<DeviceCounters, Error>;
72
73    async fn log_debug_info(&self) -> Result<(), Error>;
74}
75
76#[derive(Debug)]
77#[cfg_attr(test, derive(Eq, PartialEq))]
78enum HealthStatus {
79    Unhealthy { last_action: zx::MonotonicInstant },
80    Healthy { last_action: Option<zx::MonotonicInstant> },
81}
82
83impl HealthStatus {
84    /// Sets the status to unhealthy at time `now`.
85    ///
86    /// Return `true` if a debug info action should be triggered respecting
87    /// cooldown.
88    fn set_unhealthy_and_check_for_debug_info_cooldown(
89        &mut self,
90        now: zx::MonotonicInstant,
91    ) -> bool {
92        let last_action = match self {
93            HealthStatus::Unhealthy { last_action } => Some(*last_action),
94            HealthStatus::Healthy { last_action } => *last_action,
95        };
96
97        let (trigger_debug_info, last_action) = match last_action
98            .map(|last_action| (now - last_action >= DEBUG_INFO_COOLDOWN, last_action))
99        {
100            // Either we haven't yet triggered a debug info action or we've
101            // passed the cooldown period.
102            None | Some((true, _)) => (true, now),
103            // We're still in the cooldown period from the last triggered
104            // action.
105            Some((false, last_action)) => (false, last_action),
106        };
107
108        *self = HealthStatus::Unhealthy { last_action: last_action };
109
110        return trigger_debug_info;
111    }
112
113    /// Sets the system health status to healthy.
114    fn set_healthy(&mut self) {
115        match self {
116            HealthStatus::Unhealthy { last_action } => {
117                *self = HealthStatus::Healthy { last_action: Some(*last_action) };
118            }
119            HealthStatus::Healthy { last_action: _ } => {}
120        }
121    }
122}
123
124#[derive(Debug)]
125struct InterfaceDiagnosticsState<D> {
126    diagnostics: D,
127    rx: TimestampedCounter,
128    tx: TimestampedCounter,
129    updated_at: zx::MonotonicInstant,
130    health: HealthStatus,
131}
132
133#[derive(Debug)]
134struct InterfaceState<D> {
135    diagnostics_state: Option<InterfaceDiagnosticsState<D>>,
136}
137
138pub struct Watchdog<S: SystemDispatcher> {
139    interfaces: HashMap<InterfaceId, InterfaceState<S::DeviceDiagnostics>>,
140    system_health_status: HealthStatus,
141    _marker: std::marker::PhantomData<S>,
142}
143
144#[async_trait::async_trait]
145pub trait SystemDispatcher {
146    type DeviceDiagnostics: DeviceDiagnosticsProvider;
147
148    async fn log_debug_info(&self) -> Result<(), Error>;
149
150    fn get_device_diagnostics(
151        &self,
152        interface: InterfaceId,
153    ) -> Result<Self::DeviceDiagnostics, Error>;
154}
155
156#[derive(Debug, Eq, PartialEq, Clone, Copy)]
157enum ActionReason {
158    CantFetchCounters,
159    DeviceRxStall,
160    DeviceTxStall,
161}
162
163#[derive(Debug, Eq, PartialEq)]
164struct Action {
165    trigger_stack_diagnosis: bool,
166    trigger_device_diagnosis: bool,
167    reason: ActionReason,
168}
169
170impl<S> Watchdog<S>
171where
172    S: SystemDispatcher,
173{
174    pub fn new() -> Self {
175        Self {
176            interfaces: HashMap::new(),
177            system_health_status: HealthStatus::Healthy { last_action: None },
178            _marker: std::marker::PhantomData,
179        }
180    }
181
182    async fn initialize_interface_state(
183        now: zx::MonotonicInstant,
184        sys: &S,
185        interface: InterfaceId,
186    ) -> Option<InterfaceDiagnosticsState<S::DeviceDiagnostics>> {
187        // Get a diagnostics handle and read the initial counters.
188        let diagnostics = match sys.get_device_diagnostics(interface) {
189            Ok(d) => d,
190            Err(e) => {
191                warn!(
192                    err:? = e,
193                    iface = interface;
194                    "failed to read diagnostics state, assuming unsupported interface"
195                );
196                return None;
197            }
198        };
199        let DeviceCounters { rx_frames, tx_frames } = match diagnostics.get_counters().await {
200            Ok(c) => c,
201            Err(e) => {
202                warn!(
203                    err:? = e,
204                    iface = interface;
205                    "failed to read device counters, assuming unsupported interface"
206                );
207                return None;
208            }
209        };
210        Some(InterfaceDiagnosticsState {
211            diagnostics,
212            rx: TimestampedCounter { value: rx_frames, at: now },
213            tx: TimestampedCounter { value: tx_frames, at: now },
214            updated_at: now,
215            health: HealthStatus::Healthy { last_action: None },
216        })
217    }
218
219    pub async fn check_interface_state(
220        &mut self,
221        now: zx::MonotonicInstant,
222        sys: &S,
223        view: InterfaceView<'_>,
224    ) {
225        debug!(view:? = view; "poll interface state");
226        let Self { interfaces, system_health_status, _marker: _ } = self;
227
228        let interface = view.properties.id;
229
230        let InterfaceState { diagnostics_state } = match interfaces.entry(interface.get()) {
231            std::collections::hash_map::Entry::Occupied(entry) => entry.into_mut(),
232            std::collections::hash_map::Entry::Vacant(vacant) => vacant.insert(InterfaceState {
233                diagnostics_state: Self::initialize_interface_state(now, sys, interface.get())
234                    .await,
235            }),
236        };
237
238        let diagnostics_state = if let Some(d) = diagnostics_state.as_mut() {
239            d
240        } else {
241            // Do nothing for unsupported interfaces, we can't get counters or
242            // trigger debug info on them.
243            return;
244        };
245
246        if let Some(action) = Self::evaluate_interface_state(now, diagnostics_state, view).await {
247            info!(
248                action:? = action,
249                iface = interface;
250                "bad state detected, action requested"
251            );
252            let Action { trigger_stack_diagnosis, trigger_device_diagnosis, reason: _ } = action;
253            if trigger_device_diagnosis {
254                diagnostics_state.diagnostics.log_debug_info().await.unwrap_or_else(
255                    |e| error!(err:? = e, iface = interface; "failed to request device debug info"),
256                );
257            }
258            if trigger_stack_diagnosis {
259                if system_health_status.set_unhealthy_and_check_for_debug_info_cooldown(now) {
260                    sys.log_debug_info().await.unwrap_or_else(
261                        |e| error!(err:? = e; "failed to request system debug info"),
262                    );
263                }
264            }
265        }
266    }
267
268    /// Evaluates the given interface state, returning an optional debugging
269    /// action to be triggered.
270    ///
271    /// Interfaces are evaluated at two levels. First, all the gateways are
272    /// evaluated against the neighbor table. Second, if all gateways are
273    /// unhealthy, the device counters are polled until a stall is observed. If
274    /// an Rx or Tx stall is seen, a debug action will be requested.
275    ///
276    /// If there's a timeout attempting to fetch interface counters, a debug
277    /// request may also be issued.
278    async fn evaluate_interface_state(
279        now: zx::MonotonicInstant,
280        diag_state: &mut InterfaceDiagnosticsState<S::DeviceDiagnostics>,
281        InterfaceView {
282            properties: fnet_interfaces_ext::Properties { id: interface, .. },
283            routes,
284            neighbors,
285        }: InterfaceView<'_>,
286    ) -> Option<Action> {
287        let InterfaceDiagnosticsState { diagnostics, rx, tx, updated_at, health } = diag_state;
288        let interface = *interface;
289
290        debug!(iface = interface; "evaluate interface state");
291
292        let mut neighbors = neighbors.as_ref()?.iter_health();
293        let router_next_hops: HashSet<_> =
294            routes.device_routes(interface.get()).filter_map(|route| route.next_hop).collect();
295        let found_healthy_gateway = neighbors
296            .fold_while(None, |found_healthy_gateway, (neighbor, health)| {
297                if !router_next_hops.contains(neighbor) {
298                    return itertools::FoldWhile::Continue(found_healthy_gateway);
299                }
300                let gateway_health = GatewayHealth::from_neighbor_health(health, now);
301                debug!(
302                    iface = interface,
303                    neighbor:? = fidl_fuchsia_net_ext::IpAddress::from(neighbor.clone()),
304                    health:? = gateway_health;
305                    "router check"
306                );
307                match gateway_health {
308                    // When we find a healthy neighbor, immediately break the
309                    // fold.
310                    GatewayHealth::Healthy
311                    // A gateway that hasn't been unhealthy for a long time may
312                    // only be going through a temporary outage.
313                    | GatewayHealth::RecentlyUnhealthy
314                    // Unknown gateway state is assumed to be healthy. Expected
315                    // to shift once neighbor table fills up.
316                    | GatewayHealth::Unknown
317                    => {
318                        itertools::FoldWhile::Done(Some(true))
319                    }
320                    // A gateway that was never healthy is considered a
321                    // misconfiguration and should not trip the watchdog.
322                    // Skip it entirely so it's not considered for the search.
323                    | GatewayHealth::NeverHealthy => {
324                        itertools::FoldWhile::Continue(found_healthy_gateway)
325                    }
326                    GatewayHealth::Unhealthy => itertools::FoldWhile::Continue(Some(false)),
327                }
328            })
329            .into_inner();
330
331        match found_healthy_gateway {
332            // If there are no gateways, there's not much we can do. Assume that
333            // either the interface is not configured for upstream connectivity
334            // or we're going through a link flap event.
335            None => {
336                debug!(iface = interface; "no gateway in neighbors");
337                return None;
338            }
339            // If there's at least one healthy gateway, there's no action to be
340            // taken, but we can mark the interface as healthy.
341            Some(true) => {
342                debug!(iface = interface; "neighbors are healthy");
343                health.set_healthy();
344                return None;
345            }
346            // If we found at least one gateway and they're all unhealthy,
347            // proceed to check device counters.
348            Some(false) => (),
349        }
350
351        let counters = match diagnostics.get_counters().await {
352            Ok(counters) => counters,
353            Err(Error::Timeout) => {
354                return Some(Action {
355                    trigger_stack_diagnosis: false,
356                    trigger_device_diagnosis: true,
357                    reason: ActionReason::CantFetchCounters,
358                });
359            }
360            Err(Error::Fidl(e)) => {
361                if !e.is_closed() {
362                    error!(
363                        e:? = e,
364                        iface = interface;
365                        "failed to read counters for interface, no action will be taken"
366                    );
367                }
368                return None;
369            }
370            Err(Error::NotSupported) => {
371                error!(
372                    iface = interface;
373                    "failed to read counters for interface, no action will be taken"
374                );
375                return None;
376            }
377        };
378        let DeviceCounters { rx_frames, tx_frames } = counters;
379        if !rx.update(rx_frames, now) {
380            warn!(
381                rx:? = rx,
382                now = now.into_nanos(),
383                iface = interface;
384                "failed to observe rx traffic since last check"
385            );
386        }
387        if !tx.update(tx_frames, now) {
388            warn!(
389                tx:? = tx,
390                now = now.into_nanos(),
391                iface = interface;
392                "failed to observe tx traffic since last check"
393            );
394        }
395        *updated_at = now;
396        if let Some(reason) = [(rx, ActionReason::DeviceRxStall), (tx, ActionReason::DeviceTxStall)]
397            .iter()
398            .find_map(|(TimestampedCounter { value: _, at }, reason)| {
399                (now - *at >= DEVICE_COUNTERS_UNHEALTHY_TIME).then_some(*reason)
400            })
401        {
402            let action = health.set_unhealthy_and_check_for_debug_info_cooldown(now).then_some({
403                Action { trigger_stack_diagnosis: true, trigger_device_diagnosis: true, reason }
404            });
405
406            return action;
407        }
408
409        info!(
410            iface = interface,
411            rx = rx_frames,
412            tx = tx_frames;
413            "gateways are unhealthy, but counters are healthy."
414        );
415
416        // Counters are not stalled, mark the interface as healthy.
417        health.set_healthy();
418
419        None
420    }
421
422    pub fn handle_interface_removed(&mut self, interface: InterfaceId) {
423        let Self { interfaces, system_health_status: _, _marker: _ } = self;
424        match interfaces.remove(&interface) {
425            Some(InterfaceState { .. }) => (),
426            None => error!(iface = interface; "attempted to remove unknown interface"),
427        }
428    }
429}
430
431#[derive(Debug, PartialEq, Eq)]
432enum GatewayHealth {
433    Unknown,
434    Healthy,
435    RecentlyUnhealthy,
436    Unhealthy,
437    NeverHealthy,
438}
439
440impl GatewayHealth {
441    /// Checks if a gateway with reported `health` should be considered healthy.
442    fn from_neighbor_health(health: &NeighborHealth, now: zx::MonotonicInstant) -> Self {
443        match health {
444            NeighborHealth::Unknown => Self::Unknown,
445            NeighborHealth::Healthy { last_observed: _ } => Self::Healthy,
446            NeighborHealth::Unhealthy { last_healthy: None } => Self::NeverHealthy,
447            NeighborHealth::Unhealthy { last_healthy: Some(last_healthy) } => {
448                if now - *last_healthy < NEIGHBOR_UNHEALTHY_TIME {
449                    Self::RecentlyUnhealthy
450                } else {
451                    Self::Unhealthy
452                }
453            }
454        }
455    }
456}
457
458#[cfg(test)]
459mod tests {
460    use super::*;
461
462    use crate::route_table::Route;
463    use assert_matches::assert_matches;
464    use fidl_fuchsia_net as fnet;
465    use fuchsia_sync::Mutex;
466    use futures::FutureExt as _;
467    use net_declare::{fidl_ip, fidl_subnet};
468    use std::sync::Arc;
469
470    use crate::neighbor_cache::NeighborState;
471    use crate::route_table::RouteTable;
472    use crate::testutil;
473
474    #[test]
475    fn health_status_healthy() {
476        let now = SOME_TIME;
477        let mut status = HealthStatus::Healthy { last_action: None };
478        assert!(status.set_unhealthy_and_check_for_debug_info_cooldown(now));
479        assert_eq!(status, HealthStatus::Unhealthy { last_action: now });
480
481        status = HealthStatus::Healthy { last_action: Some(now) };
482        let later = now + zx::MonotonicDuration::from_seconds(1);
483        assert!(!status.set_unhealthy_and_check_for_debug_info_cooldown(later));
484        assert_eq!(status, HealthStatus::Unhealthy { last_action: now });
485
486        status = HealthStatus::Healthy { last_action: Some(now) };
487        let later = now + DEBUG_INFO_COOLDOWN;
488        assert!(status.set_unhealthy_and_check_for_debug_info_cooldown(later));
489        assert_eq!(status, HealthStatus::Unhealthy { last_action: later });
490    }
491
492    #[test]
493    fn health_status_unhealthy() {
494        let now = SOME_TIME;
495        let mut status = HealthStatus::Unhealthy { last_action: now };
496        let later = now + zx::MonotonicDuration::from_seconds(1);
497        assert!(!status.set_unhealthy_and_check_for_debug_info_cooldown(later));
498        assert_eq!(status, HealthStatus::Unhealthy { last_action: now });
499
500        let later = now + DEBUG_INFO_COOLDOWN;
501        assert!(status.set_unhealthy_and_check_for_debug_info_cooldown(later));
502        assert_eq!(status, HealthStatus::Unhealthy { last_action: later });
503    }
504
505    #[test]
506    fn timestamped_counter() {
507        let now = SOME_TIME;
508        let mut counter = TimestampedCounter { value: 1, at: now };
509
510        let later = now + zx::MonotonicDuration::from_seconds(1);
511        assert!(!counter.update(1, later));
512        assert_eq!(counter, TimestampedCounter { value: 1, at: now });
513
514        assert!(counter.update(2, later));
515        assert_eq!(counter, TimestampedCounter { value: 2, at: later });
516    }
517
518    #[fuchsia::test]
519    async fn initialize_interface_state() {
520        let now = SOME_TIME;
521
522        let sys = MockSystem::default();
523        assert_matches!(Watchdog::initialize_interface_state(now, &sys, IFACE1).await, None);
524
525        let counters = DeviceCounters { rx_frames: 1, tx_frames: 2 };
526        sys.insert_interface_diagnostics(IFACE1);
527        sys.increment_counters(IFACE1, counters.clone());
528
529        let InterfaceDiagnosticsState { diagnostics: _, rx, tx, updated_at, health } =
530            Watchdog::initialize_interface_state(now, &sys, IFACE1)
531                .await
532                .expect("failed to init interface");
533        assert_eq!(rx, TimestampedCounter { value: counters.rx_frames, at: now });
534        assert_eq!(tx, TimestampedCounter { value: counters.tx_frames, at: now });
535        assert_eq!(updated_at, now);
536        assert_eq!(health, HealthStatus::Healthy { last_action: None });
537    }
538
539    #[fuchsia::test]
540    async fn no_action_if_no_neighbors() {
541        let sys = MockSystem::default();
542        let now = SOME_TIME;
543        let mut state = sys.new_diagnostics_state(now, IFACE1);
544        let view = MockInterfaceView::new(IFACE1, None, None);
545        assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
546        assert_eq!(
547            Watchdog::evaluate_interface_state(
548                now,
549                &mut state,
550                InterfaceView { neighbors: None, ..view.view() }
551            )
552            .await,
553            None
554        );
555    }
556
557    #[fuchsia::test]
558    async fn no_action_if_unreachable_neighbor_isnt_gateway() {
559        let sys = MockSystem::default();
560        let now = SOME_TIME;
561        let mut state = sys.new_diagnostics_state(now, IFACE1);
562        let view = MockInterfaceView::new(IFACE1, None, [(NEIGH_V4, UNHEALTHY_NEIGHBOR)]);
563        assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
564    }
565
566    #[fuchsia::test]
567    async fn poll_counters_if_neighbor_is_gateway() {
568        let sys = MockSystem::default();
569        let now = SOME_TIME;
570        let mut state = sys.new_diagnostics_state(now, IFACE1);
571        let view = MockInterfaceView::new(
572            IFACE1,
573            [Route {
574                destination: SUBNET_V4,
575                outbound_interface: IFACE1,
576                next_hop: Some(NEIGH_V4),
577            }],
578            [(NEIGH_V4, UNHEALTHY_NEIGHBOR)],
579        );
580        sys.set_counters_return_timeout(IFACE1);
581        assert_eq!(
582            Watchdog::evaluate_interface_state(now, &mut state, view.view()).await,
583            Some(Action {
584                trigger_stack_diagnosis: false,
585                trigger_device_diagnosis: true,
586                reason: ActionReason::CantFetchCounters
587            })
588        );
589    }
590
591    #[fuchsia::test]
592    async fn ignore_never_healthy_neighbors() {
593        const NEVER_HEALTHY_NEIGHBOR: NeighborState =
594            NeighborState::new(NeighborHealth::Unhealthy { last_healthy: None });
595
596        let sys = MockSystem::default();
597        let now = SOME_TIME;
598        let mut state = sys.new_diagnostics_state(now, IFACE1);
599        let view = MockInterfaceView::new(
600            IFACE1,
601            [Route {
602                destination: SUBNET_V6,
603                outbound_interface: IFACE1,
604                next_hop: Some(NEIGH_V6),
605            }],
606            [(NEIGH_V6, NEVER_HEALTHY_NEIGHBOR)],
607        );
608        // Only never healthy neighbor doesn't trigger actions.
609        assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
610
611        // Once we have another eligible unhealthy gateway an action is
612        // triggered.
613        let view = MockInterfaceView::new(
614            IFACE1,
615            [
616                Route {
617                    destination: SUBNET_V4,
618                    outbound_interface: IFACE1,
619                    next_hop: Some(NEIGH_V4),
620                },
621                Route {
622                    destination: SUBNET_V6,
623                    outbound_interface: IFACE1,
624                    next_hop: Some(NEIGH_V6),
625                },
626            ],
627            [(NEIGH_V4, UNHEALTHY_NEIGHBOR), (NEIGH_V6, NEVER_HEALTHY_NEIGHBOR)],
628        );
629        sys.set_counters_return_timeout(IFACE1);
630        assert_eq!(
631            Watchdog::evaluate_interface_state(now, &mut state, view.view()).await,
632            Some(Action {
633                trigger_stack_diagnosis: false,
634                trigger_device_diagnosis: true,
635                reason: ActionReason::CantFetchCounters
636            })
637        );
638    }
639
640    #[fuchsia::test]
641    async fn no_action_if_one_gateway_is_healthy() {
642        let sys = MockSystem::default();
643        let now = SOME_TIME;
644        let mut state = sys.new_diagnostics_state(now, IFACE1);
645        let view = MockInterfaceView::new(
646            IFACE1,
647            [
648                Route {
649                    destination: SUBNET_V4,
650                    outbound_interface: IFACE1,
651                    next_hop: Some(NEIGH_V4),
652                },
653                Route {
654                    destination: SUBNET_V6,
655                    outbound_interface: IFACE1,
656                    next_hop: Some(NEIGH_V6),
657                },
658            ],
659            [(NEIGH_V4, UNHEALTHY_NEIGHBOR), (NEIGH_V6, HEALTHY_NEIGHBOR)],
660        );
661        assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
662    }
663
664    #[fuchsia::test]
665    async fn actions_from_counters() {
666        let sys = MockSystem::default();
667        let now = SOME_TIME;
668        let mut state = sys.new_diagnostics_state(now, IFACE1);
669        let view = MockInterfaceView::new(
670            IFACE1,
671            [Route {
672                destination: SUBNET_V4,
673                outbound_interface: IFACE1,
674                next_hop: Some(NEIGH_V4),
675            }],
676            [(NEIGH_V4, UNHEALTHY_NEIGHBOR)],
677        );
678        let now = now + DEVICE_COUNTERS_UNHEALTHY_TIME;
679        sys.increment_counters(IFACE1, DeviceCounters { rx_frames: 10, tx_frames: 10 });
680        assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
681
682        let now = now + DEVICE_COUNTERS_UNHEALTHY_TIME;
683        sys.increment_counters(IFACE1, DeviceCounters { rx_frames: 0, tx_frames: 10 });
684        assert_eq!(
685            Watchdog::evaluate_interface_state(now, &mut state, view.view()).await,
686            Some(Action {
687                trigger_stack_diagnosis: true,
688                trigger_device_diagnosis: true,
689                reason: ActionReason::DeviceRxStall
690            })
691        );
692        sys.increment_counters(IFACE1, DeviceCounters { rx_frames: 10, tx_frames: 0 });
693
694        let now = now + DEBUG_INFO_COOLDOWN - zx::MonotonicDuration::from_seconds(1);
695        // Don't trigger again because of cooldown.
696        assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
697
698        // Now detect a tx stall.
699        sys.increment_counters(IFACE1, DeviceCounters { rx_frames: 10, tx_frames: 0 });
700        let now = now + zx::MonotonicDuration::from_seconds(1);
701        assert_eq!(
702            Watchdog::evaluate_interface_state(now, &mut state, view.view()).await,
703            Some(Action {
704                trigger_stack_diagnosis: true,
705                trigger_device_diagnosis: true,
706                reason: ActionReason::DeviceTxStall
707            })
708        );
709        assert_eq!(state.health, HealthStatus::Unhealthy { last_action: now });
710
711        let later = now + zx::MonotonicDuration::from_seconds(1);
712
713        // If the gateway disappears, no action is taken but we maintain the
714        // unhealthy state.
715        let view = MockInterfaceView::new(IFACE1, None, [(NEIGH_V4, HEALTHY_NEIGHBOR)]);
716        assert_eq!(Watchdog::evaluate_interface_state(later, &mut state, view.view()).await, None);
717        assert_eq!(state.health, HealthStatus::Unhealthy { last_action: now });
718
719        // Finally, if the gateway becomes healthy, the system should go back to
720        // healthy state.
721        let later = later + zx::MonotonicDuration::from_seconds(1);
722        let view = MockInterfaceView::new(
723            IFACE1,
724            [Route {
725                destination: SUBNET_V4,
726                outbound_interface: IFACE1,
727                next_hop: Some(NEIGH_V4),
728            }],
729            [(NEIGH_V4, HEALTHY_NEIGHBOR)],
730        );
731        assert_eq!(Watchdog::evaluate_interface_state(later, &mut state, view.view()).await, None);
732        assert_eq!(state.health, HealthStatus::Healthy { last_action: Some(now) });
733    }
734
735    #[fuchsia::test]
736    async fn triggers_diagnostics_requests() {
737        let sys = MockSystem::default();
738        sys.insert_interface_diagnostics(IFACE1);
739        let now = SOME_TIME;
740        let view = MockInterfaceView::new(
741            IFACE1,
742            [Route {
743                destination: SUBNET_V4,
744                outbound_interface: IFACE1,
745                next_hop: Some(NEIGH_V4),
746            }],
747            [(NEIGH_V4, UNHEALTHY_NEIGHBOR)],
748        );
749
750        let mut watchdog = Watchdog::new();
751        watchdog.check_interface_state(now, &sys, view.view()).await;
752        assert!(!sys.take_interface_debug_requested(IFACE1));
753        assert!(!sys.take_system_debug_requested());
754
755        let now = now + DEVICE_COUNTERS_UNHEALTHY_TIME;
756        watchdog.check_interface_state(now, &sys, view.view()).await;
757        assert!(sys.take_interface_debug_requested(IFACE1));
758        assert!(sys.take_system_debug_requested());
759
760        // Still unhealthy, but cooling down on debug requests.
761        let now = now + DEBUG_INFO_COOLDOWN / 2;
762        watchdog.check_interface_state(now, &sys, view.view()).await;
763        assert!(!sys.take_interface_debug_requested(IFACE1));
764        assert!(!sys.take_system_debug_requested());
765
766        let now = now + DEBUG_INFO_COOLDOWN;
767        watchdog.check_interface_state(now, &sys, view.view()).await;
768        assert!(sys.take_interface_debug_requested(IFACE1));
769        assert!(sys.take_system_debug_requested());
770    }
771
772    #[fuchsia::test]
773    fn gateway_health() {
774        let now = SOME_TIME;
775
776        // Healthy neighbor is never considered unhealthy.
777        assert_eq!(
778            GatewayHealth::from_neighbor_health(
779                &NeighborHealth::Healthy { last_observed: now },
780                now
781            ),
782            GatewayHealth::Healthy
783        );
784        assert_eq!(
785            GatewayHealth::from_neighbor_health(
786                &NeighborHealth::Healthy { last_observed: now },
787                now + zx::MonotonicDuration::from_minutes(60),
788            ),
789            GatewayHealth::Healthy
790        );
791
792        // Neighbor is unhealthy has never been healthy.
793        assert_eq!(
794            GatewayHealth::from_neighbor_health(
795                &NeighborHealth::Unhealthy { last_healthy: None },
796                now
797            ),
798            GatewayHealth::NeverHealthy
799        );
800
801        // Unhealthy neighbor is only considered unhealthy gateway after some
802        // time.
803        assert_eq!(
804            GatewayHealth::from_neighbor_health(
805                &NeighborHealth::Unhealthy { last_healthy: Some(now) },
806                now
807            ),
808            GatewayHealth::RecentlyUnhealthy
809        );
810        assert_eq!(
811            GatewayHealth::from_neighbor_health(
812                &NeighborHealth::Unhealthy { last_healthy: Some(now) },
813                now + NEIGHBOR_UNHEALTHY_TIME
814            ),
815            GatewayHealth::Unhealthy
816        );
817    }
818
819    const ZERO_TIME: zx::MonotonicInstant = zx::MonotonicInstant::from_nanos(0);
820    const SOME_TIME: zx::MonotonicInstant =
821        zx::MonotonicInstant::from_nanos(NEIGHBOR_UNHEALTHY_TIME.into_nanos());
822    const UNHEALTHY_NEIGHBOR: NeighborState =
823        NeighborState::new(NeighborHealth::Unhealthy { last_healthy: Some(ZERO_TIME) });
824    const HEALTHY_NEIGHBOR: NeighborState =
825        NeighborState::new(NeighborHealth::Healthy { last_observed: ZERO_TIME });
826
827    const IFACE1: InterfaceId = 1;
828    const NEIGH_V4: fnet::IpAddress = fidl_ip!("192.0.2.1");
829    const NEIGH_V6: fnet::IpAddress = fidl_ip!("2001:db8::1");
830    // Arbitrary subnet values with which to create routes.
831    const SUBNET_V4: fnet::Subnet = fidl_subnet!("0.0.0.0/0");
832    const SUBNET_V6: fnet::Subnet = fidl_subnet!("::0/0");
833
834    struct MockInterfaceView {
835        properties: fnet_interfaces_ext::Properties<fnet_interfaces_ext::DefaultInterest>,
836        routes: RouteTable,
837        neighbors: crate::InterfaceNeighborCache,
838    }
839
840    impl MockInterfaceView {
841        fn new<
842            R: IntoIterator<Item = Route>,
843            N: IntoIterator<Item = (fnet::IpAddress, NeighborState)>,
844        >(
845            id: InterfaceId,
846            routes: R,
847            neighbors: N,
848        ) -> Self {
849            Self {
850                properties: fnet_interfaces_ext::Properties {
851                    id: id.try_into().expect("should be nonzero"),
852                    name: "foo".to_owned(),
853                    port_class: fnet_interfaces_ext::PortClass::Loopback,
854                    online: true,
855                    addresses: vec![],
856                    has_default_ipv4_route: true,
857                    has_default_ipv6_route: true,
858                    port_identity_koid: None,
859                },
860                routes: testutil::build_route_table_from_flattened_routes(routes),
861                neighbors: neighbors.into_iter().collect(),
862            }
863        }
864
865        fn view(&self) -> InterfaceView<'_> {
866            let Self { properties, routes, neighbors } = self;
867            InterfaceView { properties, routes: &routes, neighbors: Some(neighbors) }
868        }
869    }
870
871    #[derive(Debug)]
872    struct MockCounterState {
873        counters_result: Option<Result<DeviceCounters, Error>>,
874        debug_requested: bool,
875    }
876
877    type MockState = Arc<Mutex<HashMap<InterfaceId, MockCounterState>>>;
878
879    type Watchdog = super::Watchdog<MockSystem>;
880
881    #[derive(Default)]
882    struct MockSystem {
883        inner: MockState,
884        debug_info_requested: std::sync::atomic::AtomicBool,
885    }
886
887    #[async_trait::async_trait]
888    impl SystemDispatcher for MockSystem {
889        type DeviceDiagnostics = MockDiagnostics;
890
891        async fn log_debug_info(&self) -> Result<(), Error> {
892            let Self { inner: _, debug_info_requested } = self;
893            debug_info_requested.store(true, std::sync::atomic::Ordering::SeqCst);
894            Ok(())
895        }
896
897        fn get_device_diagnostics(
898            &self,
899            interface: InterfaceId,
900        ) -> Result<Self::DeviceDiagnostics, Error> {
901            let Self { inner, debug_info_requested: _ } = self;
902            Ok(MockDiagnostics { inner: inner.clone(), interface })
903        }
904    }
905
906    impl MockSystem {
907        fn insert_interface_diagnostics(&self, interface: InterfaceId) {
908            let counters = DeviceCounters { rx_frames: 0, tx_frames: 0 };
909            assert_matches!(
910                self.inner.lock().insert(
911                    interface,
912                    MockCounterState {
913                        counters_result: Some(Ok(counters)),
914                        debug_requested: false
915                    }
916                ),
917                None
918            );
919        }
920
921        fn new_diagnostics_state(
922            &self,
923            now: zx::MonotonicInstant,
924            interface: InterfaceId,
925        ) -> InterfaceDiagnosticsState<MockDiagnostics> {
926            self.insert_interface_diagnostics(interface);
927            let state = Watchdog::initialize_interface_state(now, self, interface)
928                .now_or_never()
929                .expect("future should be ready")
930                .expect("failed to initialize interface state");
931
932            // Remove the initial counters to force tests that use this function
933            // to explicitly set any counter values they may wish to use.
934            self.inner.lock().get_mut(&interface).unwrap().counters_result = None;
935
936            state
937        }
938
939        fn set_counters_return_timeout(&self, interface: InterfaceId) {
940            self.inner.lock().get_mut(&interface).unwrap().counters_result =
941                Some(Err(Error::Timeout));
942        }
943
944        fn increment_counters(
945            &self,
946            interface: InterfaceId,
947            DeviceCounters { rx_frames: rx, tx_frames: tx }: DeviceCounters,
948        ) {
949            let mut state = self.inner.lock();
950            let MockCounterState { counters_result, debug_requested: _ } =
951                state.get_mut(&interface).unwrap();
952            *counters_result = Some(Ok(match counters_result {
953                Some(Ok(DeviceCounters { rx_frames, tx_frames })) => {
954                    DeviceCounters { rx_frames: *rx_frames + rx, tx_frames: *tx_frames + tx }
955                }
956                None | Some(Err(_)) => DeviceCounters { rx_frames: rx, tx_frames: tx },
957            }));
958        }
959
960        fn take_interface_debug_requested(&self, interface: InterfaceId) -> bool {
961            let mut state = self.inner.lock();
962            if let Some(MockCounterState { counters_result: _, debug_requested }) =
963                state.get_mut(&interface)
964            {
965                std::mem::replace(debug_requested, false)
966            } else {
967                false
968            }
969        }
970
971        fn take_system_debug_requested(&self) -> bool {
972            self.debug_info_requested.swap(false, std::sync::atomic::Ordering::SeqCst)
973        }
974    }
975
976    #[derive(Debug)]
977    struct MockDiagnostics {
978        inner: MockState,
979        interface: InterfaceId,
980    }
981
982    #[async_trait::async_trait]
983    impl DeviceDiagnosticsProvider for MockDiagnostics {
984        async fn get_counters(&self) -> Result<DeviceCounters, Error> {
985            let Self { inner, interface } = self;
986            let state = inner.lock();
987            state.get(interface).ok_or_else(|| Error::Fidl(fidl::Error::Invalid)).and_then(
988                |MockCounterState { counters_result, debug_requested: _ }| {
989                    counters_result.clone().expect("called get_counters on uninitialized mock")
990                },
991            )
992        }
993
994        async fn log_debug_info(&self) -> Result<(), Error> {
995            let Self { inner, interface } = self;
996            let mut state = inner.lock();
997            let MockCounterState { counters_result: _, debug_requested } =
998                state.get_mut(interface).unwrap();
999            *debug_requested = true;
1000            Ok(())
1001        }
1002    }
1003}