1use 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
18const DEVICE_COUNTERS_UNHEALTHY_TIME: zx::MonotonicDuration =
21 zx::MonotonicDuration::from_minutes(2);
22
23const DEBUG_INFO_COOLDOWN: zx::MonotonicDuration = zx::MonotonicDuration::from_minutes(15);
26
27const 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 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 None | Some((true, _)) => (true, now),
103 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 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 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 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 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 GatewayHealth::Healthy
311 | GatewayHealth::RecentlyUnhealthy
314 | GatewayHealth::Unknown
317 => {
318 itertools::FoldWhile::Done(Some(true))
319 }
320 | 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 None => {
336 debug!(iface = interface; "no gateway in neighbors");
337 return None;
338 }
339 Some(true) => {
342 debug!(iface = interface; "neighbors are healthy");
343 health.set_healthy();
344 return None;
345 }
346 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 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 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 assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
610
611 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 assert_eq!(Watchdog::evaluate_interface_state(now, &mut state, view.view()).await, None);
697
698 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 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 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 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 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 assert_eq!(
794 GatewayHealth::from_neighbor_health(
795 &NeighborHealth::Unhealthy { last_healthy: None },
796 now
797 ),
798 GatewayHealth::NeverHealthy
799 );
800
801 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 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 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}