Skip to main content

reachability_core/
neighbor_cache.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
5use log::error;
6use std::collections::HashMap;
7use {
8    fidl_fuchsia_net as fnet, fidl_fuchsia_net_neighbor as fnet_neighbor,
9    fidl_fuchsia_net_neighbor_ext as fnet_neighbor_ext,
10};
11
12use super::Id;
13
14#[derive(Debug, Clone)]
15#[cfg_attr(test, derive(PartialEq, Eq))]
16pub(crate) enum NeighborHealth {
17    Unknown,
18    Healthy { last_observed: zx::MonotonicInstant },
19    Unhealthy { last_healthy: Option<zx::MonotonicInstant> },
20}
21
22impl NeighborHealth {
23    fn last_healthy(&self) -> Option<zx::MonotonicInstant> {
24        match self {
25            NeighborHealth::Unknown => None,
26            NeighborHealth::Healthy { last_observed } => Some(*last_observed),
27            NeighborHealth::Unhealthy { last_healthy } => *last_healthy,
28        }
29    }
30
31    /// Transitions to a new [`NeighborHealth`] state given a new
32    /// [`fnet_neighbor::EntryState`].
33    ///
34    /// Entry states that do not explicitly encode healthy or unhealthy status
35    /// (`Delay`, `Probe`, `Static`, `Stale`) will maintain the state machine in
36    /// the current state.
37    ///
38    /// `Stale` does not change the current neighbor health because it can't
39    /// infer two-way reachability. There are two notable cases where a neighbor
40    /// could be in `Stale` repeatedly or for long periods:
41    /// - If interest in the neighbor goes away. The neighbor health state
42    ///   machine is only used for gateway health, and the reachability
43    ///   component generates traffic directed at gateways. So we can expect
44    ///   neighbors to not be parked in the stale state for very long.
45    /// - If the neighbor repeatedly changes its Mac address. This is taken to
46    ///   be a pathological corner case and probably causes the network to be
47    ///   unhealthy either way.
48    ///
49    /// A `Reachable` entry will always move to a `Healthy` state.
50    ///
51    /// An `Incomplete` or `Unreachable` entry will always move to an
52    /// `Unhealthy` state.
53    fn transition(&self, now: zx::MonotonicInstant, state: fnet_neighbor::EntryState) -> Self {
54        match state {
55            fnet_neighbor::EntryState::Incomplete | fnet_neighbor::EntryState::Unreachable => {
56                NeighborHealth::Unhealthy { last_healthy: self.last_healthy() }
57            }
58            fnet_neighbor::EntryState::Reachable => NeighborHealth::Healthy { last_observed: now },
59            fnet_neighbor::EntryState::Delay
60            | fnet_neighbor::EntryState::Probe
61            | fnet_neighbor::EntryState::Static
62            | fnet_neighbor::EntryState::Stale => self.clone(),
63        }
64    }
65}
66
67#[derive(Debug, Clone)]
68#[cfg_attr(test, derive(PartialEq, Eq))]
69pub(crate) struct NeighborState {
70    health: NeighborHealth,
71}
72
73impl NeighborState {
74    #[cfg(test)]
75    pub(crate) const fn new(health: NeighborHealth) -> Self {
76        Self { health }
77    }
78}
79
80#[derive(Debug, Default)]
81#[cfg_attr(test, derive(Eq, PartialEq))]
82pub struct InterfaceNeighborCache {
83    pub(crate) neighbors: HashMap<fnet::IpAddress, NeighborState>,
84}
85
86impl InterfaceNeighborCache {
87    pub(crate) fn iter_health(
88        &self,
89    ) -> impl Iterator<Item = (&'_ fnet::IpAddress, &'_ NeighborHealth)> {
90        let Self { neighbors } = self;
91        neighbors.iter().map(|(n, NeighborState { health })| (n, health))
92    }
93}
94
95#[cfg(test)]
96impl FromIterator<(fnet::IpAddress, NeighborState)> for InterfaceNeighborCache {
97    fn from_iter<T: IntoIterator<Item = (fnet::IpAddress, NeighborState)>>(iter: T) -> Self {
98        Self { neighbors: FromIterator::from_iter(iter) }
99    }
100}
101
102/// Provides a cache of known neighbors and keeps track of their health.
103#[derive(Debug, Default)]
104pub struct NeighborCache {
105    interfaces: HashMap<Id, InterfaceNeighborCache>,
106}
107
108impl NeighborCache {
109    pub fn process_neighbor_event(&mut self, e: fnet_neighbor::EntryIteratorItem) {
110        let Self { interfaces } = self;
111        enum Event {
112            Added,
113            Changed,
114            Removed,
115        }
116        let (event, entry) = match e {
117            fnet_neighbor::EntryIteratorItem::Existing(entry)
118            | fnet_neighbor::EntryIteratorItem::Added(entry) => (Event::Added, entry),
119            fnet_neighbor::EntryIteratorItem::Idle(fnet_neighbor::IdleEvent {}) => {
120                return;
121            }
122            fnet_neighbor::EntryIteratorItem::Changed(entry) => (Event::Changed, entry),
123            fnet_neighbor::EntryIteratorItem::Removed(entry) => (Event::Removed, entry),
124        };
125        let fnet_neighbor_ext::Entry { interface, neighbor, state, mac: _, updated_at } =
126            match fnet_neighbor_ext::Entry::try_from(entry) {
127                Ok(entry) => entry,
128                Err(e) => {
129                    error!(e:? = e; "invalid neighbor entry");
130                    return;
131                }
132            };
133        let updated_at = zx::MonotonicInstant::from_nanos(updated_at);
134
135        let InterfaceNeighborCache { neighbors } =
136            interfaces.entry(interface.get()).or_insert_with(Default::default);
137
138        match event {
139            Event::Added => match neighbors.entry(neighbor) {
140                std::collections::hash_map::Entry::Occupied(occupied) => {
141                    error!(entry:? = occupied; "received entry for already existing neighbor");
142                    return;
143                }
144                std::collections::hash_map::Entry::Vacant(vacant) => {
145                    let _: &mut _ = vacant.insert(NeighborState {
146                        health: NeighborHealth::Unknown.transition(updated_at, state),
147                    });
148                }
149            },
150            Event::Changed => {
151                let NeighborState { health } = match neighbors.get_mut(&neighbor) {
152                    Some(s) => s,
153                    None => {
154                        error!(neigh:? = neighbor; "got changed event for unseen neighbor");
155                        return;
156                    }
157                };
158                *health = health.transition(updated_at, state);
159            }
160            Event::Removed => match neighbors.remove(&neighbor) {
161                Some(NeighborState { .. }) => {
162                    if neighbors.is_empty() {
163                        // Clean up interface state when we see all neighbors
164                        // removed. Unwrap is valid because `neighbors` is
165                        // itself a borrow into the map's entry.
166                        InterfaceNeighborCache { .. } =
167                            interfaces.remove(&interface.get()).unwrap();
168                    }
169                }
170                None => {
171                    error!(neigh:? = neighbor; "got removed event for unseen neighbor");
172                }
173            },
174        }
175    }
176
177    pub fn get_interface_neighbors(&self, interface: Id) -> Option<&InterfaceNeighborCache> {
178        let Self { interfaces } = self;
179        interfaces.get(&interface)
180    }
181}
182
183#[cfg(test)]
184mod tests {
185    use super::*;
186    use net_declare::fidl_ip;
187
188    const IFACE1: Id = 1;
189    const IFACE2: Id = 2;
190    const NEIGH1: fnet::IpAddress = fidl_ip!("192.0.2.1");
191    const NEIGH2: fnet::IpAddress = fidl_ip!("2001:db8::1");
192
193    struct EventSource {
194        now: zx::MonotonicInstant,
195    }
196
197    impl EventSource {
198        fn new() -> Self {
199            Self { now: zx::MonotonicInstant::from_nanos(0) }
200        }
201
202        fn advance_secs(&mut self, secs: u64) {
203            self.now += zx::MonotonicDuration::from_seconds(secs.try_into().unwrap());
204        }
205
206        fn entry(
207            &self,
208            interface: Id,
209            neighbor: fnet::IpAddress,
210            state: fnet_neighbor::EntryState,
211        ) -> NeighborEntry {
212            NeighborEntry { interface, neighbor, state, updated_at: self.now }
213        }
214
215        fn reachable(&self, interface: Id, neighbor: fnet::IpAddress) -> NeighborEntry {
216            self.entry(interface, neighbor, fnet_neighbor::EntryState::Reachable)
217        }
218
219        fn probe(&self, interface: Id, neighbor: fnet::IpAddress) -> NeighborEntry {
220            self.entry(interface, neighbor, fnet_neighbor::EntryState::Probe)
221        }
222
223        fn stale(&self, interface: Id, neighbor: fnet::IpAddress) -> NeighborEntry {
224            self.entry(interface, neighbor, fnet_neighbor::EntryState::Stale)
225        }
226
227        fn delay(&self, interface: Id, neighbor: fnet::IpAddress) -> NeighborEntry {
228            self.entry(interface, neighbor, fnet_neighbor::EntryState::Delay)
229        }
230
231        fn incomplete(&self, interface: Id, neighbor: fnet::IpAddress) -> NeighborEntry {
232            self.entry(interface, neighbor, fnet_neighbor::EntryState::Incomplete)
233        }
234
235        fn unreachable(&self, interface: Id, neighbor: fnet::IpAddress) -> NeighborEntry {
236            self.entry(interface, neighbor, fnet_neighbor::EntryState::Unreachable)
237        }
238    }
239
240    struct NeighborEntry {
241        interface: Id,
242        neighbor: fnet::IpAddress,
243        state: fnet_neighbor::EntryState,
244        updated_at: zx::MonotonicInstant,
245    }
246
247    impl NeighborEntry {
248        fn into_entry(self) -> fnet_neighbor::Entry {
249            let Self { interface, neighbor, state, updated_at } = self;
250            fnet_neighbor::Entry {
251                interface: Some(interface),
252                neighbor: Some(neighbor),
253                state: Some(state),
254                updated_at: Some(updated_at.into_nanos()),
255                ..Default::default()
256            }
257        }
258
259        fn into_added(self) -> fnet_neighbor::EntryIteratorItem {
260            fnet_neighbor::EntryIteratorItem::Added(self.into_entry())
261        }
262
263        fn into_changed(self) -> fnet_neighbor::EntryIteratorItem {
264            fnet_neighbor::EntryIteratorItem::Changed(self.into_entry())
265        }
266
267        fn into_removed(self) -> fnet_neighbor::EntryIteratorItem {
268            fnet_neighbor::EntryIteratorItem::Removed(self.into_entry())
269        }
270    }
271
272    impl NeighborCache {
273        fn assert_neighbors(
274            &self,
275            interface: Id,
276            it: impl IntoIterator<Item = (fnet::IpAddress, NeighborHealth)>,
277        ) {
278            let InterfaceNeighborCache { neighbors } =
279                self.get_interface_neighbors(interface).unwrap();
280            let it = it
281                .into_iter()
282                .map(|(n, health)| (n, NeighborState { health }))
283                .collect::<HashMap<_, _>>();
284            assert_eq!(neighbors, &it);
285        }
286    }
287
288    #[fuchsia::test]
289    fn caches_healthy_neighbors_per_interface() {
290        let mut cache = NeighborCache::default();
291        let mut events = EventSource::new();
292        cache.process_neighbor_event(events.reachable(IFACE1, NEIGH1).into_added());
293        cache.assert_neighbors(
294            IFACE1,
295            [(NEIGH1, NeighborHealth::Healthy { last_observed: events.now })],
296        );
297
298        events.advance_secs(1);
299        cache.process_neighbor_event(events.reachable(IFACE2, NEIGH2).into_added());
300        cache.assert_neighbors(
301            IFACE2,
302            [(NEIGH2, NeighborHealth::Healthy { last_observed: events.now })],
303        );
304    }
305
306    #[fuchsia::test]
307    fn updates_healthy_state() {
308        let mut cache = NeighborCache::default();
309        let mut events = EventSource::new();
310        cache.process_neighbor_event(events.reachable(IFACE1, NEIGH1).into_added());
311        cache.assert_neighbors(
312            IFACE1,
313            [(NEIGH1, NeighborHealth::Healthy { last_observed: events.now })],
314        );
315
316        events.advance_secs(3);
317        cache.process_neighbor_event(events.reachable(IFACE1, NEIGH1).into_changed());
318        cache.assert_neighbors(
319            IFACE1,
320            [(NEIGH1, NeighborHealth::Healthy { last_observed: events.now })],
321        );
322    }
323
324    #[fuchsia::test]
325    fn probe_reachable_stale() {
326        let mut cache = NeighborCache::default();
327        let mut events = EventSource::new();
328
329        cache.process_neighbor_event(events.probe(IFACE1, NEIGH1).into_added());
330        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unknown)]);
331        events.advance_secs(1);
332
333        cache.process_neighbor_event(events.reachable(IFACE1, NEIGH1).into_changed());
334        cache.assert_neighbors(
335            IFACE1,
336            [(NEIGH1, NeighborHealth::Healthy { last_observed: events.now })],
337        );
338
339        let last_healthy = events.now;
340        events.advance_secs(1);
341        cache.process_neighbor_event(events.stale(IFACE1, NEIGH1).into_changed());
342        cache.assert_neighbors(
343            IFACE1,
344            [(NEIGH1, NeighborHealth::Healthy { last_observed: last_healthy })],
345        );
346    }
347
348    #[fuchsia::test]
349    fn stale_delay_reachable() {
350        let mut cache = NeighborCache::default();
351        let mut events = EventSource::new();
352
353        cache.process_neighbor_event(events.stale(IFACE1, NEIGH1).into_added());
354        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unknown)]);
355
356        events.advance_secs(1);
357        cache.process_neighbor_event(events.delay(IFACE1, NEIGH1).into_changed());
358        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unknown)]);
359
360        events.advance_secs(1);
361        cache.process_neighbor_event(events.reachable(IFACE1, NEIGH1).into_changed());
362        cache.assert_neighbors(
363            IFACE1,
364            [(NEIGH1, NeighborHealth::Healthy { last_observed: events.now })],
365        );
366    }
367
368    #[fuchsia::test]
369    fn reachable_unreachable() {
370        let mut cache = NeighborCache::default();
371        let mut events = EventSource::new();
372
373        cache.process_neighbor_event(events.reachable(IFACE1, NEIGH1).into_added());
374        cache.assert_neighbors(
375            IFACE1,
376            [(NEIGH1, NeighborHealth::Healthy { last_observed: events.now })],
377        );
378
379        let last_healthy = Some(events.now);
380        events.advance_secs(1);
381        cache.process_neighbor_event(events.unreachable(IFACE1, NEIGH1).into_changed());
382        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unhealthy { last_healthy })]);
383    }
384
385    #[fuchsia::test]
386    fn probe_incomplete() {
387        let mut cache = NeighborCache::default();
388        let mut events = EventSource::new();
389
390        cache.process_neighbor_event(events.probe(IFACE1, NEIGH1).into_added());
391        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unknown)]);
392
393        events.advance_secs(1);
394        cache.process_neighbor_event(events.incomplete(IFACE1, NEIGH1).into_changed());
395        cache
396            .assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unhealthy { last_healthy: None })]);
397    }
398
399    #[fuchsia::test]
400    fn stale_unreachable_probe_incomplete() {
401        let mut cache = NeighborCache::default();
402        let mut events = EventSource::new();
403
404        cache.process_neighbor_event(events.stale(IFACE1, NEIGH1).into_added());
405        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unknown)]);
406
407        events.advance_secs(1);
408        cache.process_neighbor_event(events.unreachable(IFACE1, NEIGH1).into_changed());
409        cache
410            .assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unhealthy { last_healthy: None })]);
411
412        events.advance_secs(1);
413        cache.process_neighbor_event(events.probe(IFACE1, NEIGH1).into_changed());
414        cache
415            .assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unhealthy { last_healthy: None })]);
416
417        events.advance_secs(1);
418        cache.process_neighbor_event(events.incomplete(IFACE1, NEIGH1).into_changed());
419        cache
420            .assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unhealthy { last_healthy: None })]);
421    }
422
423    #[fuchsia::test]
424    fn removing_last_neighbor_clears_interface_state() {
425        let mut cache = NeighborCache::default();
426        let events = EventSource::new();
427
428        cache.process_neighbor_event(events.probe(IFACE1, NEIGH1).into_added());
429        cache.assert_neighbors(IFACE1, [(NEIGH1, NeighborHealth::Unknown)]);
430
431        cache.process_neighbor_event(events.probe(IFACE1, NEIGH1).into_removed());
432        assert_eq!(cache.get_interface_neighbors(IFACE1), None);
433    }
434}