ndn_discovery/
neighbor.rs

1//! Neighbor table — engine-owned state for all discovered peers.
2//!
3//! The table is owned by the engine (via [`DiscoveryContext`]) rather than by
4//! any individual protocol implementation.  This means the table survives
5//! protocol swaps at runtime and can be shared across multiple simultaneous
6//! protocols (e.g. EtherND + SWIM running together).
7
8use std::collections::HashMap;
9use std::sync::{Arc, Mutex};
10use std::time::Instant;
11
12use ndn_packet::Name;
13use ndn_transport::FaceId;
14use tracing::{debug, info};
15
16use crate::MacAddr;
17
18/// Lifecycle state of a neighbor peer.
19#[derive(Clone, Debug)]
20pub enum NeighborState {
21    /// Actively probing — waiting for a hello response.
22    Probing {
23        /// Number of consecutive unanswered probe attempts.
24        attempts: u8,
25        /// When the last probe was sent.
26        last_probe: Instant,
27    },
28    /// Link confirmed reachable.
29    Established {
30        /// Timestamp of last successful exchange.
31        last_seen: Instant,
32    },
33    /// Missed several hellos; link may be degrading.
34    Stale {
35        /// Number of consecutive missed hellos.
36        miss_count: u8,
37        /// Timestamp of last successful exchange before failures began.
38        last_seen: Instant,
39    },
40    /// Peer is considered unreachable; entry pending removal.
41    Absent,
42}
43
44/// A discovered neighbor and its per-link face bindings.
45#[derive(Clone, Debug)]
46pub struct NeighborEntry {
47    /// NDN node name (e.g. `/ndn/site/host`).
48    pub node_name: Name,
49    /// Current reachability state.
50    pub state: NeighborState,
51    /// Per-link face bindings: `(face_id, source_mac, interface_name)`.
52    ///
53    /// A peer may be reachable over multiple interfaces simultaneously
54    /// (e.g. Ethernet + Wi-Fi).  Each gets its own unicast face.
55    pub faces: Vec<(FaceId, MacAddr, String)>,
56    /// Estimated round-trip time in microseconds (EWMA, `None` until measured).
57    pub rtt_us: Option<u32>,
58    /// Nonce sent in the most recent outstanding hello Interest, used for
59    /// replay detection and RTT measurement.
60    pub pending_nonce: Option<u32>,
61}
62
63impl NeighborEntry {
64    pub fn new(node_name: Name) -> Self {
65        Self {
66            node_name,
67            state: NeighborState::Probing {
68                attempts: 0,
69                last_probe: Instant::now(),
70            },
71            faces: Vec::new(),
72            rtt_us: None,
73            pending_nonce: None,
74        }
75    }
76
77    /// Return whether this neighbor has any live unicast faces.
78    pub fn is_reachable(&self) -> bool {
79        matches!(self.state, NeighborState::Established { .. }) && !self.faces.is_empty()
80    }
81
82    /// Find the face for the given MAC + interface, if one already exists.
83    pub fn face_for(&self, mac: &MacAddr, iface: &str) -> Option<FaceId> {
84        self.faces
85            .iter()
86            .find(|(_, m, i)| m == mac && i == iface)
87            .map(|(id, _, _)| *id)
88    }
89}
90
91/// Mutation applied to the neighbor table via [`DiscoveryContext::update_neighbor`].
92pub enum NeighborUpdate {
93    /// Insert or replace a full entry.
94    Upsert(NeighborEntry),
95    /// Transition an existing entry to a new state.
96    SetState { name: Name, state: NeighborState },
97    /// Add a face binding to an existing entry.
98    AddFace {
99        name: Name,
100        face_id: FaceId,
101        mac: MacAddr,
102        iface: String,
103    },
104    /// Remove a face binding.
105    RemoveFace { name: Name, face_id: FaceId },
106    /// Record a measured RTT for an entry.
107    UpdateRtt { name: Name, rtt_us: u32 },
108    /// Remove the entry entirely.
109    Remove(Name),
110}
111
112fn state_label(s: &NeighborState) -> &'static str {
113    match s {
114        NeighborState::Probing { .. } => "Probing",
115        NeighborState::Established { .. } => "Established",
116        NeighborState::Stale { .. } => "Stale",
117        NeighborState::Absent => "Absent",
118    }
119}
120
121/// Engine-owned, lock-protected neighbor table.
122///
123/// Wrapped in `Arc<NeighborTable>` so both the engine and the
124/// `DiscoveryContext` implementation can hold a reference.
125pub struct NeighborTable {
126    inner: Mutex<HashMap<Name, NeighborEntry>>,
127}
128
129impl NeighborTable {
130    pub fn new() -> Arc<Self> {
131        Arc::new(Self {
132            inner: Mutex::new(HashMap::new()),
133        })
134    }
135
136    /// Apply a [`NeighborUpdate`].
137    pub fn apply(&self, update: NeighborUpdate) {
138        let mut map = self.inner.lock().unwrap();
139        match update {
140            NeighborUpdate::Upsert(entry) => {
141                let is_new = !map.contains_key(&entry.node_name);
142                let label = state_label(&entry.state);
143                let name = entry.node_name.clone();
144                map.insert(name.clone(), entry);
145                if is_new {
146                    debug!(peer = %name, state = label, "neighbor added to table");
147                }
148            }
149            NeighborUpdate::SetState { name, state } => {
150                if let Some(entry) = map.get_mut(&name) {
151                    let from = state_label(&entry.state);
152                    let to = state_label(&state);
153                    if from != to {
154                        // State-variant transitions are meaningful lifecycle events;
155                        // same-variant updates (e.g. refreshing last_seen) are silent.
156                        if matches!(state, NeighborState::Established { .. }) {
157                            info!(peer = %name, %from, %to, "neighbor established");
158                        } else if matches!(state, NeighborState::Stale { .. }) {
159                            info!(peer = %name, %from, %to, "neighbor went stale");
160                        } else {
161                            debug!(peer = %name, %from, %to, "neighbor state →");
162                        }
163                    }
164                    entry.state = state;
165                }
166            }
167            NeighborUpdate::AddFace {
168                name,
169                face_id,
170                mac,
171                iface,
172            } => {
173                if let Some(entry) = map.get_mut(&name) {
174                    // Avoid duplicates.
175                    if entry.face_for(&mac, &iface).is_none() {
176                        entry.faces.push((face_id, mac, iface));
177                    }
178                }
179            }
180            NeighborUpdate::RemoveFace { name, face_id } => {
181                if let Some(entry) = map.get_mut(&name) {
182                    entry.faces.retain(|(id, _, _)| *id != face_id);
183                }
184            }
185            NeighborUpdate::UpdateRtt { name, rtt_us } => {
186                if let Some(entry) = map.get_mut(&name) {
187                    // EWMA with α = 0.125 (same as TCP RTO estimation).
188                    entry.rtt_us = Some(match entry.rtt_us {
189                        None => rtt_us,
190                        Some(prev) => (7 * prev + rtt_us) / 8,
191                    });
192                }
193            }
194            NeighborUpdate::Remove(name) => {
195                if map.remove(&name).is_some() {
196                    info!(peer = %name, "neighbor removed from table");
197                }
198            }
199        }
200    }
201
202    /// Snapshot a single entry by name.
203    pub fn get(&self, name: &Name) -> Option<NeighborEntry> {
204        self.inner.lock().unwrap().get(name).cloned()
205    }
206
207    /// Snapshot all entries.
208    pub fn all(&self) -> Vec<NeighborEntry> {
209        self.inner.lock().unwrap().values().cloned().collect()
210    }
211
212    /// Find a face for the given MAC + interface across all entries.
213    pub fn face_for_peer(&self, mac: &MacAddr, iface: &str) -> Option<FaceId> {
214        let map = self.inner.lock().unwrap();
215        for entry in map.values() {
216            if let Some(id) = entry.face_for(mac, iface) {
217                return Some(id);
218            }
219        }
220        None
221    }
222}
223
224impl Default for NeighborTable {
225    fn default() -> Self {
226        Self {
227            inner: Mutex::new(HashMap::new()),
228        }
229    }
230}
231
232impl crate::NeighborTableView for NeighborTable {
233    fn get(&self, name: &Name) -> Option<NeighborEntry> {
234        NeighborTable::get(self, name)
235    }
236    fn all(&self) -> Vec<NeighborEntry> {
237        NeighborTable::all(self)
238    }
239    fn face_for_peer(&self, mac: &crate::MacAddr, iface: &str) -> Option<FaceId> {
240        NeighborTable::face_for_peer(self, mac, iface)
241    }
242}
243
244#[cfg(test)]
245mod tests {
246    use super::*;
247    use std::str::FromStr;
248
249    fn name(s: &str) -> Name {
250        Name::from_str(s).unwrap()
251    }
252
253    #[test]
254    fn upsert_and_get() {
255        let table = NeighborTable::new();
256        let n = name("/ndn/test/node");
257        table.apply(NeighborUpdate::Upsert(NeighborEntry::new(n.clone())));
258        assert!(table.get(&n).is_some());
259    }
260
261    #[test]
262    fn remove_entry() {
263        let table = NeighborTable::new();
264        let n = name("/ndn/test/node");
265        table.apply(NeighborUpdate::Upsert(NeighborEntry::new(n.clone())));
266        table.apply(NeighborUpdate::Remove(n.clone()));
267        assert!(table.get(&n).is_none());
268    }
269
270    #[test]
271    fn rtt_ewma() {
272        let table = NeighborTable::new();
273        let n = name("/ndn/test/node");
274        table.apply(NeighborUpdate::Upsert(NeighborEntry::new(n.clone())));
275        table.apply(NeighborUpdate::UpdateRtt {
276            name: n.clone(),
277            rtt_us: 1000,
278        });
279        let e = table.get(&n).unwrap();
280        assert_eq!(e.rtt_us, Some(1000)); // first sample stored as-is
281
282        table.apply(NeighborUpdate::UpdateRtt {
283            name: n.clone(),
284            rtt_us: 2000,
285        });
286        let e = table.get(&n).unwrap();
287        // EWMA: (7*1000 + 2000) / 8 = 1125
288        assert_eq!(e.rtt_us, Some(1125));
289    }
290
291    #[test]
292    fn add_face_deduplicates() {
293        let table = NeighborTable::new();
294        let n = name("/ndn/test/node");
295        let mac = MacAddr::new([0xaa, 0xbb, 0xcc, 0x00, 0x00, 0x01]);
296        table.apply(NeighborUpdate::Upsert(NeighborEntry::new(n.clone())));
297        table.apply(NeighborUpdate::AddFace {
298            name: n.clone(),
299            face_id: FaceId(1),
300            mac,
301            iface: "eth0".into(),
302        });
303        table.apply(NeighborUpdate::AddFace {
304            name: n.clone(),
305            face_id: FaceId(1),
306            mac,
307            iface: "eth0".into(),
308        });
309        let e = table.get(&n).unwrap();
310        assert_eq!(e.faces.len(), 1);
311    }
312
313    #[test]
314    fn face_for_peer_lookup() {
315        let table = NeighborTable::new();
316        let n = name("/ndn/test/node");
317        let mac = MacAddr::new([0xde, 0xad, 0xbe, 0xef, 0x00, 0x01]);
318        table.apply(NeighborUpdate::Upsert(NeighborEntry::new(n.clone())));
319        table.apply(NeighborUpdate::AddFace {
320            name: n.clone(),
321            face_id: FaceId(7),
322            mac,
323            iface: "eth0".into(),
324        });
325        assert_eq!(table.face_for_peer(&mac, "eth0"), Some(FaceId(7)));
326        assert_eq!(table.face_for_peer(&mac, "eth1"), None);
327    }
328}