ndn_engine/
fib.rs

1use ndn_packet::Name;
2use ndn_store::NameTrie;
3use ndn_transport::FaceId;
4use std::sync::Arc;
5
6/// A single FIB nexthop: a face with an associated routing cost.
7#[derive(Clone, Debug)]
8pub struct FibNexthop {
9    pub face_id: FaceId,
10    pub cost: u32,
11}
12
13/// A FIB entry at a name prefix: one or more nexthops.
14#[derive(Clone, Debug)]
15pub struct FibEntry {
16    pub nexthops: Vec<FibNexthop>,
17}
18
19impl FibEntry {
20    pub fn nexthops_excluding(&self, exclude: FaceId) -> Vec<FibNexthop> {
21        self.nexthops
22            .iter()
23            .filter(|n| n.face_id != exclude)
24            .cloned()
25            .collect()
26    }
27}
28
29/// The Forwarding Information Base.
30///
31/// A name trie mapping prefixes to `FibEntry` values. Concurrent longest-prefix
32/// match with per-node `RwLock` (via `NameTrie`).
33pub struct Fib {
34    trie: NameTrie<Arc<FibEntry>>,
35}
36
37impl Fib {
38    pub fn new() -> Self {
39        Self {
40            trie: NameTrie::new(),
41        }
42    }
43
44    /// Longest-prefix match lookup.
45    pub fn lpm(&self, name: &Name) -> Option<Arc<FibEntry>> {
46        self.trie.lpm(name)
47    }
48
49    /// Register a nexthop for `prefix`. Replaces any existing entry.
50    pub fn add_nexthop(&self, prefix: &Name, face_id: FaceId, cost: u32) {
51        let existing = self.trie.get(prefix);
52        let mut nexthops = existing.map(|e| e.nexthops.clone()).unwrap_or_default();
53        // Remove any existing entry for this face, then add the new one.
54        nexthops.retain(|n| n.face_id != face_id);
55        nexthops.push(FibNexthop { face_id, cost });
56        self.trie.insert(prefix, Arc::new(FibEntry { nexthops }));
57    }
58
59    /// Return all FIB entries as `(prefix_uri, [(face_id, cost)])` tuples.
60    pub fn dump(&self) -> Vec<(Name, Arc<FibEntry>)> {
61        self.trie.dump()
62    }
63
64    /// Remove the entry for `prefix` entirely, regardless of nexthops.
65    pub fn remove_prefix(&self, prefix: &Name) {
66        self.trie.remove(prefix);
67    }
68
69    /// Atomically replace all nexthops for `prefix`.
70    ///
71    /// Used by the RIB to apply computed nexthops. If `nexthops` is empty the
72    /// entry is removed.
73    pub fn set_nexthops(&self, prefix: &Name, nexthops: Vec<FibNexthop>) {
74        if nexthops.is_empty() {
75            self.trie.remove(prefix);
76        } else {
77            self.trie.insert(prefix, Arc::new(FibEntry { nexthops }));
78        }
79    }
80
81    /// Remove all nexthops pointing to `face_id` across all prefixes.
82    ///
83    /// Called when a face is closed to prevent stale routes from accumulating.
84    pub fn remove_face(&self, face_id: FaceId) {
85        let entries = self.trie.dump();
86        for (prefix, entry) in entries {
87            if entry.nexthops.iter().any(|n| n.face_id == face_id) {
88                self.remove_nexthop(&prefix, face_id);
89            }
90        }
91    }
92
93    /// Remove the nexthop for `face_id` from `prefix`.
94    pub fn remove_nexthop(&self, prefix: &Name, face_id: FaceId) {
95        let Some(existing) = self.trie.get(prefix) else {
96            return;
97        };
98        let nexthops: Vec<_> = existing
99            .nexthops
100            .iter()
101            .filter(|n| n.face_id != face_id)
102            .cloned()
103            .collect();
104        if nexthops.is_empty() {
105            self.trie.remove(prefix);
106        } else {
107            self.trie.insert(prefix, Arc::new(FibEntry { nexthops }));
108        }
109    }
110}
111
112impl Default for Fib {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use bytes::Bytes;
122    use ndn_packet::NameComponent;
123
124    fn name1(s: &'static str) -> Name {
125        Name::from_components([NameComponent::generic(Bytes::from_static(s.as_bytes()))])
126    }
127
128    fn name2(a: &'static str, b: &'static str) -> Name {
129        Name::from_components([
130            NameComponent::generic(Bytes::from_static(a.as_bytes())),
131            NameComponent::generic(Bytes::from_static(b.as_bytes())),
132        ])
133    }
134
135    #[test]
136    fn lpm_empty_returns_none() {
137        let fib = Fib::new();
138        assert!(fib.lpm(&name1("a")).is_none());
139    }
140
141    #[test]
142    fn add_nexthop_and_lpm() {
143        let fib = Fib::new();
144        fib.add_nexthop(&name1("a"), FaceId(1), 10);
145        let entry = fib.lpm(&name1("a")).unwrap();
146        assert_eq!(entry.nexthops.len(), 1);
147        assert_eq!(entry.nexthops[0].face_id, FaceId(1));
148        assert_eq!(entry.nexthops[0].cost, 10);
149    }
150
151    #[test]
152    fn lpm_returns_longest_prefix() {
153        let fib = Fib::new();
154        fib.add_nexthop(&Name::root(), FaceId(1), 10);
155        fib.add_nexthop(&name1("a"), FaceId(2), 10);
156        // "a/b" should match "a" (longer than root)
157        let entry = fib.lpm(&name2("a", "b")).unwrap();
158        assert_eq!(entry.nexthops[0].face_id, FaceId(2));
159    }
160
161    #[test]
162    fn add_nexthop_updates_cost() {
163        let fib = Fib::new();
164        fib.add_nexthop(&name1("a"), FaceId(1), 10);
165        fib.add_nexthop(&name1("a"), FaceId(1), 20);
166        let entry = fib.lpm(&name1("a")).unwrap();
167        assert_eq!(entry.nexthops.len(), 1);
168        assert_eq!(entry.nexthops[0].cost, 20);
169    }
170
171    #[test]
172    fn add_multiple_nexthops() {
173        let fib = Fib::new();
174        fib.add_nexthop(&name1("a"), FaceId(1), 10);
175        fib.add_nexthop(&name1("a"), FaceId(2), 20);
176        let entry = fib.lpm(&name1("a")).unwrap();
177        assert_eq!(entry.nexthops.len(), 2);
178    }
179
180    #[test]
181    fn remove_nexthop_removes_face() {
182        let fib = Fib::new();
183        fib.add_nexthop(&name1("a"), FaceId(1), 10);
184        fib.add_nexthop(&name1("a"), FaceId(2), 20);
185        fib.remove_nexthop(&name1("a"), FaceId(1));
186        let entry = fib.lpm(&name1("a")).unwrap();
187        assert_eq!(entry.nexthops.len(), 1);
188        assert_eq!(entry.nexthops[0].face_id, FaceId(2));
189    }
190
191    #[test]
192    fn remove_last_nexthop_deletes_entry() {
193        let fib = Fib::new();
194        fib.add_nexthop(&name1("a"), FaceId(1), 10);
195        fib.remove_nexthop(&name1("a"), FaceId(1));
196        assert!(fib.lpm(&name1("a")).is_none());
197    }
198
199    #[test]
200    fn remove_nonexistent_is_noop() {
201        let fib = Fib::new();
202        fib.add_nexthop(&name1("a"), FaceId(1), 10);
203        fib.remove_nexthop(&name1("a"), FaceId(99));
204        let entry = fib.lpm(&name1("a")).unwrap();
205        assert_eq!(entry.nexthops.len(), 1);
206    }
207
208    #[test]
209    fn remove_face_cleans_all_prefixes() {
210        let fib = Fib::new();
211        fib.add_nexthop(&name1("a"), FaceId(1), 10);
212        fib.add_nexthop(&name1("a"), FaceId(2), 20);
213        fib.add_nexthop(&name1("b"), FaceId(1), 5);
214        fib.add_nexthop(&name2("c", "d"), FaceId(1), 0);
215
216        fib.remove_face(FaceId(1));
217
218        // /a still has face 2
219        let entry = fib.lpm(&name1("a")).unwrap();
220        assert_eq!(entry.nexthops.len(), 1);
221        assert_eq!(entry.nexthops[0].face_id, FaceId(2));
222        // /b was the only nexthop for face 1 → entry removed
223        assert!(fib.lpm(&name1("b")).is_none());
224        // /c/d was the only nexthop for face 1 → entry removed
225        assert!(fib.lpm(&name2("c", "d")).is_none());
226    }
227
228    #[test]
229    fn nexthops_excluding_filters_in_face() {
230        let entry = FibEntry {
231            nexthops: vec![
232                FibNexthop {
233                    face_id: FaceId(1),
234                    cost: 0,
235                },
236                FibNexthop {
237                    face_id: FaceId(2),
238                    cost: 0,
239                },
240            ],
241        };
242        let filtered = entry.nexthops_excluding(FaceId(1));
243        assert_eq!(filtered.len(), 1);
244        assert_eq!(filtered[0].face_id, FaceId(2));
245    }
246}