pamoja_routing/router.rs
1//! The routing table and the per-packet forwarding decision.
2
3/// A learned route to a destination: the neighbour to send through, and the cost.
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
5pub struct Route {
6 dst: u32,
7 next_hop: u32,
8 cost: u16,
9}
10
11impl Route {
12 /// Returns the destination node this route reaches.
13 ///
14 /// # Returns
15 ///
16 /// The destination address.
17 pub fn dst(&self) -> u32 {
18 self.dst
19 }
20
21 /// Returns the neighbour to send through to reach the destination.
22 ///
23 /// # Returns
24 ///
25 /// The next-hop address.
26 pub fn next_hop(&self) -> u32 {
27 self.next_hop
28 }
29
30 /// Returns the cost of this route, in whatever metric the caller reports (hop count,
31 /// summed link cost, or another).
32 ///
33 /// # Returns
34 ///
35 /// The route cost; lower is better.
36 pub fn cost(&self) -> u16 {
37 self.cost
38 }
39}
40
41/// What to do with a packet bound for a given destination.
42#[derive(Clone, Copy, Debug, PartialEq, Eq)]
43pub enum Forward {
44 /// The packet is for this node; hand it to the application.
45 Deliver,
46 /// A route is known; unicast the packet to this next hop.
47 Relay(u32),
48 /// No route is known; fall back to flooding the packet.
49 Flood,
50}
51
52/// A fixed-size routing table for one node.
53///
54/// The table holds up to `N` routes, learned from the traffic the node hears. It keeps the
55/// cheapest route it knows to each destination, and when full it gives up the most
56/// expensive route to make room for a cheaper one, so its limited memory holds the routes
57/// most worth keeping.
58///
59/// # Examples
60///
61/// ```
62/// use pamoja_routing::{Forward, Router};
63///
64/// let mut router: Router<8> = Router::new(0x0A);
65/// router.observe(0x0B, 0x0C, 3); // reach 0x0B via 0x0C, cost 3
66/// assert_eq!(router.next_hop(0x0B), Some(0x0C));
67/// assert_eq!(router.forward(0x0A), Forward::Deliver); // a packet for us
68/// ```
69#[derive(Clone, Copy, Debug)]
70pub struct Router<const N: usize> {
71 me: u32,
72 routes: [Option<Route>; N],
73}
74
75impl<const N: usize> Router<N> {
76 /// Creates an empty router for the node at `me`.
77 ///
78 /// # Arguments
79 ///
80 /// * `me` - this node's address.
81 ///
82 /// # Returns
83 ///
84 /// A router holding no routes.
85 pub const fn new(me: u32) -> Self {
86 Router {
87 me,
88 routes: [None; N],
89 }
90 }
91
92 /// Returns this node's address.
93 ///
94 /// # Returns
95 ///
96 /// The address the router was created with.
97 pub fn address(&self) -> u32 {
98 self.me
99 }
100
101 /// Learns the way to a node from a packet heard from it.
102 ///
103 /// A packet that originated at `origin` and reached this node via the neighbour `via`
104 /// proves `via` is a way back to `origin` at the reported `cost`. The router adopts the
105 /// route if it is cheaper than what it knows, or if it refreshes the cost of the route
106 /// it is already using, and ignores a route to itself.
107 ///
108 /// # Arguments
109 ///
110 /// * `origin` - the node the packet came from, the destination this route reaches.
111 /// * `via` - the neighbour the packet arrived through, the next hop for this route.
112 /// * `cost` - the cost the packet reports for reaching `origin` through `via`.
113 ///
114 /// # Returns
115 ///
116 /// `true` if the table changed (a route was added, redirected, or recosted), `false`
117 /// if the observation taught it nothing new.
118 pub fn observe(&mut self, origin: u32, via: u32, cost: u16) -> bool {
119 observe_into(&mut self.routes, self.me, origin, via, cost)
120 }
121
122 /// Returns the next hop to reach a destination, if a route is known.
123 ///
124 /// # Arguments
125 ///
126 /// * `dst` - the destination to reach.
127 ///
128 /// # Returns
129 ///
130 /// The next-hop address, or [`None`] if no route is known.
131 pub fn next_hop(&self, dst: u32) -> Option<u32> {
132 self.route(dst).map(|route| route.next_hop)
133 }
134
135 /// Returns the cost of the known route to a destination, if any.
136 ///
137 /// # Arguments
138 ///
139 /// * `dst` - the destination to reach.
140 ///
141 /// # Returns
142 ///
143 /// The route cost, or [`None`] if no route is known.
144 pub fn cost(&self, dst: u32) -> Option<u16> {
145 self.route(dst).map(|route| route.cost)
146 }
147
148 /// Returns the known route to a destination, if any.
149 ///
150 /// # Arguments
151 ///
152 /// * `dst` - the destination to reach.
153 ///
154 /// # Returns
155 ///
156 /// The [`Route`], or [`None`] if no route is known.
157 pub fn route(&self, dst: u32) -> Option<Route> {
158 route_in(&self.routes, dst)
159 }
160
161 /// Decides what to do with a packet bound for a destination.
162 ///
163 /// # Arguments
164 ///
165 /// * `dst` - the packet's destination.
166 ///
167 /// # Returns
168 ///
169 /// [`Forward::Deliver`] if the packet is for this node, [`Forward::Relay`] with the
170 /// next hop if a route is known, or [`Forward::Flood`] otherwise.
171 pub fn forward(&self, dst: u32) -> Forward {
172 forward_in(&self.routes, self.me, dst)
173 }
174
175 /// Forgets the route to a destination, if one is held.
176 ///
177 /// # Arguments
178 ///
179 /// * `dst` - the destination whose route to drop.
180 pub fn forget(&mut self, dst: u32) {
181 forget_in(&mut self.routes, dst)
182 }
183
184 /// Returns how many routes the table currently holds.
185 ///
186 /// # Returns
187 ///
188 /// The number of routes.
189 pub fn len(&self) -> usize {
190 self.routes.iter().filter(|slot| slot.is_some()).count()
191 }
192
193 /// Reports whether the table holds no routes.
194 ///
195 /// # Returns
196 ///
197 /// `true` if no routes are held.
198 pub fn is_empty(&self) -> bool {
199 self.routes.iter().all(Option::is_none)
200 }
201}
202
203// Learns a route into a slot slice. Split out so the fixed-size and runtime-sized tables
204// share one implementation rather than two that can drift.
205fn observe_into(routes: &mut [Option<Route>], me: u32, origin: u32, via: u32, cost: u16) -> bool {
206 if origin == me {
207 return false;
208 }
209 if let Some(index) = index_of_in(routes, origin) {
210 let route = routes[index]
211 .as_mut()
212 .expect("index_of_in points at a route");
213 if cost < route.cost || via == route.next_hop {
214 let changed = route.next_hop != via || route.cost != cost;
215 route.next_hop = via;
216 route.cost = cost;
217 return changed;
218 }
219 return false;
220 }
221
222 let new = Route {
223 dst: origin,
224 next_hop: via,
225 cost,
226 };
227 if let Some(empty) = routes.iter().position(Option::is_none) {
228 routes[empty] = Some(new);
229 return true;
230 }
231
232 // The table is full; replace the costliest route if this one is cheaper. A capacity of
233 // zero leaves nothing to replace, so the observation is dropped.
234 if let Some((worst, worst_cost)) = routes
235 .iter()
236 .enumerate()
237 .filter_map(|(i, slot)| slot.as_ref().map(|route| (i, route.cost)))
238 .max_by_key(|&(_, cost)| cost)
239 {
240 if cost < worst_cost {
241 routes[worst] = Some(new);
242 return true;
243 }
244 }
245 false
246}
247
248// The slot index of the route to `dst`, if one is held.
249fn index_of_in(routes: &[Option<Route>], dst: u32) -> Option<usize> {
250 routes
251 .iter()
252 .position(|slot| slot.as_ref().is_some_and(|route| route.dst == dst))
253}
254
255// The route to `dst`, if one is held.
256fn route_in(routes: &[Option<Route>], dst: u32) -> Option<Route> {
257 index_of_in(routes, dst).map(|index| routes[index].expect("index_of_in points at a route"))
258}
259
260// Decides what to do with a packet bound for `dst`.
261fn forward_in(routes: &[Option<Route>], me: u32, dst: u32) -> Forward {
262 if dst == me {
263 return Forward::Deliver;
264 }
265 match route_in(routes, dst) {
266 Some(route) => Forward::Relay(route.next_hop),
267 None => Forward::Flood,
268 }
269}
270
271// Drops the route to `dst`, if one is held.
272fn forget_in(routes: &mut [Option<Route>], dst: u32) {
273 if let Some(index) = index_of_in(routes, dst) {
274 routes[index] = None;
275 }
276}
277
278/// A routing table whose size is chosen when it is built, rather than at compile time.
279///
280/// [`Router`] fixes its capacity in the type, which suits a microcontroller that knows its
281/// own limits. A gateway, or any caller reaching this through a language binding, does not
282/// know the size until it runs, and a const generic cannot cross a foreign function
283/// boundary at all. This is the same table with its slots on the heap, so both share one
284/// implementation and answer identically.
285///
286/// Requires the `alloc` feature.
287///
288/// # Examples
289///
290/// ```
291/// use pamoja_routing::{DynamicRouter, Forward};
292///
293/// // A gateway sizes its table for the mesh it is actually serving.
294/// let mut router = DynamicRouter::new(0x01, 512);
295/// router.observe(0x09, 0x05, 2);
296/// assert_eq!(router.forward(0x09), Forward::Relay(0x05));
297/// assert_eq!(router.capacity(), 512);
298/// ```
299#[cfg(any(feature = "alloc", test))]
300#[derive(Clone, Debug)]
301pub struct DynamicRouter {
302 me: u32,
303 routes: alloc::vec::Vec<Option<Route>>,
304}
305
306#[cfg(any(feature = "alloc", test))]
307impl DynamicRouter {
308 /// Creates an empty router holding up to `capacity` routes.
309 ///
310 /// # Arguments
311 ///
312 /// * `me` - this node's address.
313 /// * `capacity` - how many routes to make room for. A capacity of zero is allowed and
314 /// makes every unknown destination flood, which is the behaviour with no table.
315 ///
316 /// # Returns
317 ///
318 /// A router holding no routes.
319 pub fn new(me: u32, capacity: usize) -> Self {
320 DynamicRouter {
321 me,
322 routes: alloc::vec![None; capacity],
323 }
324 }
325
326 /// Returns this node's address.
327 ///
328 /// # Returns
329 ///
330 /// The address the router was created with.
331 pub fn address(&self) -> u32 {
332 self.me
333 }
334
335 /// Returns how many routes this table can hold.
336 ///
337 /// # Returns
338 ///
339 /// The capacity it was created with.
340 pub fn capacity(&self) -> usize {
341 self.routes.len()
342 }
343
344 /// Learns the way to a node from a packet heard from it.
345 ///
346 /// # Arguments
347 ///
348 /// * `origin` - the node the packet came from, the destination this route reaches.
349 /// * `via` - the neighbour the packet arrived through, the next hop for this route.
350 /// * `cost` - the cost the packet reports for reaching `origin` through `via`.
351 ///
352 /// # Returns
353 ///
354 /// `true` if the table changed, `false` if the observation taught it nothing new.
355 pub fn observe(&mut self, origin: u32, via: u32, cost: u16) -> bool {
356 observe_into(&mut self.routes, self.me, origin, via, cost)
357 }
358
359 /// Returns the next hop to reach a destination, if a route is known.
360 ///
361 /// # Arguments
362 ///
363 /// * `dst` - the destination to reach.
364 ///
365 /// # Returns
366 ///
367 /// The next-hop address, or [`None`] if no route is known.
368 pub fn next_hop(&self, dst: u32) -> Option<u32> {
369 route_in(&self.routes, dst).map(|route| route.next_hop)
370 }
371
372 /// Returns the cost of the known route to a destination, if any.
373 ///
374 /// # Arguments
375 ///
376 /// * `dst` - the destination to reach.
377 ///
378 /// # Returns
379 ///
380 /// The route cost, or [`None`] if no route is known.
381 pub fn cost(&self, dst: u32) -> Option<u16> {
382 route_in(&self.routes, dst).map(|route| route.cost)
383 }
384
385 /// Returns the known route to a destination, if any.
386 ///
387 /// # Arguments
388 ///
389 /// * `dst` - the destination to reach.
390 ///
391 /// # Returns
392 ///
393 /// The [`Route`], or [`None`] if no route is known.
394 pub fn route(&self, dst: u32) -> Option<Route> {
395 route_in(&self.routes, dst)
396 }
397
398 /// Decides what to do with a packet bound for a destination.
399 ///
400 /// # Arguments
401 ///
402 /// * `dst` - the packet's destination.
403 ///
404 /// # Returns
405 ///
406 /// [`Forward::Deliver`] if the packet is for this node, [`Forward::Relay`] with the
407 /// next hop if a route is known, or [`Forward::Flood`] otherwise.
408 pub fn forward(&self, dst: u32) -> Forward {
409 forward_in(&self.routes, self.me, dst)
410 }
411
412 /// Forgets the route to a destination, if one is held.
413 ///
414 /// # Arguments
415 ///
416 /// * `dst` - the destination whose route to drop.
417 pub fn forget(&mut self, dst: u32) {
418 forget_in(&mut self.routes, dst)
419 }
420
421 /// Returns how many routes the table currently holds.
422 ///
423 /// # Returns
424 ///
425 /// The number of routes.
426 pub fn len(&self) -> usize {
427 self.routes.iter().filter(|slot| slot.is_some()).count()
428 }
429
430 /// Reports whether the table holds no routes.
431 ///
432 /// # Returns
433 ///
434 /// `true` if no routes are held.
435 pub fn is_empty(&self) -> bool {
436 self.routes.iter().all(Option::is_none)
437 }
438}
439
440#[cfg(test)]
441mod tests {
442 use super::*;
443
444 #[test]
445 fn a_learned_route_is_used() {
446 let mut router: Router<8> = Router::new(1);
447 assert!(router.observe(9, 5, 2));
448 assert_eq!(router.next_hop(9), Some(5));
449 assert_eq!(router.cost(9), Some(2));
450 assert_eq!(router.forward(9), Forward::Relay(5));
451 }
452
453 #[test]
454 fn a_packet_for_this_node_is_delivered() {
455 let router: Router<8> = Router::new(1);
456 assert_eq!(router.forward(1), Forward::Deliver);
457 }
458
459 #[test]
460 fn an_unknown_destination_floods() {
461 let router: Router<8> = Router::new(1);
462 assert_eq!(router.forward(42), Forward::Flood);
463 }
464
465 #[test]
466 fn a_cheaper_route_replaces_a_costlier_one() {
467 let mut router: Router<8> = Router::new(1);
468 router.observe(9, 5, 4);
469 assert!(router.observe(9, 7, 1));
470 assert_eq!(router.next_hop(9), Some(7));
471 assert_eq!(router.cost(9), Some(1));
472 }
473
474 #[test]
475 fn a_costlier_route_is_ignored() {
476 let mut router: Router<8> = Router::new(1);
477 router.observe(9, 7, 1);
478 assert!(!router.observe(9, 5, 4));
479 assert_eq!(router.next_hop(9), Some(7));
480 }
481
482 #[test]
483 fn the_current_next_hop_can_refresh_its_cost() {
484 let mut router: Router<8> = Router::new(1);
485 router.observe(9, 7, 1);
486 // The same neighbour now reports a higher cost; we trust our current path.
487 assert!(router.observe(9, 7, 3));
488 assert_eq!(router.cost(9), Some(3));
489 }
490
491 #[test]
492 fn we_never_route_to_ourselves() {
493 let mut router: Router<8> = Router::new(1);
494 assert!(!router.observe(1, 5, 1));
495 assert_eq!(router.route(1), None);
496 }
497
498 #[test]
499 fn a_full_table_evicts_its_costliest_route_for_a_cheaper_one() {
500 let mut router: Router<2> = Router::new(1);
501 router.observe(10, 2, 5);
502 router.observe(11, 3, 8); // the costliest
503 assert_eq!(router.len(), 2);
504
505 // A cheaper route than the costliest evicts it.
506 assert!(router.observe(12, 4, 2));
507 assert_eq!(router.next_hop(11), None); // evicted
508 assert_eq!(router.next_hop(10), Some(2)); // kept
509 assert_eq!(router.next_hop(12), Some(4)); // added
510 }
511
512 #[test]
513 fn a_full_table_keeps_its_routes_against_a_costlier_one() {
514 let mut router: Router<2> = Router::new(1);
515 router.observe(10, 2, 5);
516 router.observe(11, 3, 8);
517 // A new route costlier than everything held is not worth a slot.
518 assert!(!router.observe(12, 4, 9));
519 assert_eq!(router.next_hop(12), None);
520 assert_eq!(router.len(), 2);
521 }
522
523 #[test]
524 fn forgetting_a_route_drops_it() {
525 let mut router: Router<8> = Router::new(1);
526 router.observe(9, 5, 2);
527 router.forget(9);
528 assert_eq!(router.route(9), None);
529 assert!(router.is_empty());
530 }
531
532 #[test]
533 fn an_empty_router_reports_empty() {
534 let router: Router<8> = Router::new(1);
535 assert!(router.is_empty());
536 assert_eq!(router.len(), 0);
537 }
538
539 #[test]
540 fn a_zero_capacity_router_never_learns_but_does_not_panic() {
541 let mut router: Router<0> = Router::new(1);
542 assert!(!router.observe(9, 5, 2));
543 assert_eq!(router.next_hop(9), None);
544 assert_eq!(router.forward(9), Forward::Flood);
545 assert!(router.is_empty());
546 }
547
548 #[test]
549 fn a_runtime_sized_table_decides_the_same_way() {
550 let mut fixed: Router<8> = Router::new(1);
551 let mut dynamic = DynamicRouter::new(1, 8);
552 for (origin, via, cost) in [(9u32, 5u32, 4u16), (9, 7, 1), (10, 5, 3), (1, 2, 1)] {
553 assert_eq!(
554 fixed.observe(origin, via, cost),
555 dynamic.observe(origin, via, cost),
556 "the two tables learn identically"
557 );
558 }
559 for dst in [1u32, 9, 10, 42] {
560 assert_eq!(fixed.forward(dst), dynamic.forward(dst));
561 assert_eq!(fixed.route(dst), dynamic.route(dst));
562 }
563 assert_eq!(fixed.len(), dynamic.len());
564
565 fixed.forget(9);
566 dynamic.forget(9);
567 assert_eq!(fixed.forward(9), dynamic.forward(9));
568 assert_eq!(fixed.len(), dynamic.len());
569 }
570
571 #[test]
572 fn a_runtime_sized_table_fills_to_the_size_it_was_given() {
573 let mut router = DynamicRouter::new(1, 3);
574 assert_eq!(router.capacity(), 3);
575 assert!(router.is_empty());
576 for node in 0..10u32 {
577 router.observe(node + 0x100, 0x05, 4);
578 }
579 assert_eq!(router.len(), 3, "it holds no more than it was sized for");
580 }
581
582 #[test]
583 fn a_table_with_no_room_floods_everything() {
584 let mut router = DynamicRouter::new(1, 0);
585 assert!(!router.observe(9, 5, 2), "there is nowhere to put a route");
586 assert_eq!(router.forward(9), Forward::Flood);
587 assert_eq!(
588 router.forward(1),
589 Forward::Deliver,
590 "a local packet still arrives"
591 );
592 }
593}