Skip to main content

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}