smoltcp/iface/
route.rs

1use heapless::Vec;
2
3use crate::config::IFACE_MAX_ROUTE_COUNT;
4use crate::time::Instant;
5use crate::wire::{IpAddress, IpCidr};
6#[cfg(feature = "proto-ipv4")]
7use crate::wire::{Ipv4Address, Ipv4Cidr};
8#[cfg(feature = "proto-ipv6")]
9use crate::wire::{Ipv6Address, Ipv6Cidr};
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12#[cfg_attr(feature = "defmt", derive(defmt::Format))]
13pub struct RouteTableFull;
14
15impl core::fmt::Display for RouteTableFull {
16    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
17        write!(f, "Route table full")
18    }
19}
20
21#[cfg(feature = "std")]
22impl std::error::Error for RouteTableFull {}
23
24/// A prefix of addresses that should be routed via a router
25#[derive(Debug, Clone, Copy)]
26#[cfg_attr(feature = "defmt", derive(defmt::Format))]
27pub struct Route {
28    pub cidr: IpCidr,
29    pub via_router: IpAddress,
30    /// `None` means "forever".
31    pub preferred_until: Option<Instant>,
32    /// `None` means "forever".
33    pub expires_at: Option<Instant>,
34}
35
36#[cfg(feature = "proto-ipv4")]
37const IPV4_DEFAULT: IpCidr = IpCidr::Ipv4(Ipv4Cidr::new(Ipv4Address::new(0, 0, 0, 0), 0));
38#[cfg(feature = "proto-ipv6")]
39const IPV6_DEFAULT: IpCidr =
40    IpCidr::Ipv6(Ipv6Cidr::new(Ipv6Address::new(0, 0, 0, 0, 0, 0, 0, 0), 0));
41
42impl Route {
43    /// Returns a route to 0.0.0.0/0 via the `gateway`, with no expiry.
44    #[cfg(feature = "proto-ipv4")]
45    pub fn new_ipv4_gateway(gateway: Ipv4Address) -> Route {
46        Route {
47            cidr: IPV4_DEFAULT,
48            via_router: gateway.into(),
49            preferred_until: None,
50            expires_at: None,
51        }
52    }
53
54    /// Returns a route to ::/0 via the `gateway`, with no expiry.
55    #[cfg(feature = "proto-ipv6")]
56    pub fn new_ipv6_gateway(gateway: Ipv6Address) -> Route {
57        Route {
58            cidr: IPV6_DEFAULT,
59            via_router: gateway.into(),
60            preferred_until: None,
61            expires_at: None,
62        }
63    }
64}
65
66/// A routing table.
67#[derive(Debug)]
68pub struct Routes {
69    storage: Vec<Route, IFACE_MAX_ROUTE_COUNT>,
70}
71
72impl Routes {
73    /// Creates a new empty routing table.
74    pub fn new() -> Self {
75        Self {
76            storage: Vec::new(),
77        }
78    }
79
80    /// Update the routes of this node.
81    pub fn update<F: FnOnce(&mut Vec<Route, IFACE_MAX_ROUTE_COUNT>)>(&mut self, f: F) {
82        f(&mut self.storage);
83    }
84
85    /// Add a default ipv4 gateway (ie. "ip route add 0.0.0.0/0 via `gateway`").
86    ///
87    /// On success, returns the previous default route, if any.
88    #[cfg(feature = "proto-ipv4")]
89    pub fn add_default_ipv4_route(
90        &mut self,
91        gateway: Ipv4Address,
92    ) -> Result<Option<Route>, RouteTableFull> {
93        let old = self.remove_default_ipv4_route();
94        self.storage
95            .push(Route::new_ipv4_gateway(gateway))
96            .map_err(|_| RouteTableFull)?;
97        Ok(old)
98    }
99
100    /// Add a default ipv6 gateway (ie. "ip -6 route add ::/0 via `gateway`").
101    ///
102    /// On success, returns the previous default route, if any.
103    #[cfg(feature = "proto-ipv6")]
104    pub fn add_default_ipv6_route(
105        &mut self,
106        gateway: Ipv6Address,
107    ) -> Result<Option<Route>, RouteTableFull> {
108        let old = self.remove_default_ipv6_route();
109        self.storage
110            .push(Route::new_ipv6_gateway(gateway))
111            .map_err(|_| RouteTableFull)?;
112        Ok(old)
113    }
114
115    /// Remove the default ipv4 gateway
116    ///
117    /// On success, returns the previous default route, if any.
118    #[cfg(feature = "proto-ipv4")]
119    pub fn remove_default_ipv4_route(&mut self) -> Option<Route> {
120        if let Some((i, _)) = self
121            .storage
122            .iter()
123            .enumerate()
124            .find(|(_, r)| r.cidr == IPV4_DEFAULT)
125        {
126            Some(self.storage.remove(i))
127        } else {
128            None
129        }
130    }
131
132    /// Remove the default ipv6 gateway
133    ///
134    /// On success, returns the previous default route, if any.
135    #[cfg(feature = "proto-ipv6")]
136    pub fn remove_default_ipv6_route(&mut self) -> Option<Route> {
137        if let Some((i, _)) = self
138            .storage
139            .iter()
140            .enumerate()
141            .find(|(_, r)| r.cidr == IPV6_DEFAULT)
142        {
143            Some(self.storage.remove(i))
144        } else {
145            None
146        }
147    }
148
149    pub(crate) fn lookup(&self, addr: &IpAddress, timestamp: Instant) -> Option<IpAddress> {
150        assert!(addr.is_unicast());
151
152        self.storage
153            .iter()
154            // Keep only matching routes
155            .filter(|route| {
156                if let Some(expires_at) = route.expires_at {
157                    if timestamp > expires_at {
158                        return false;
159                    }
160                }
161                route.cidr.contains_addr(addr)
162            })
163            // pick the most specific one (highest prefix_len)
164            .max_by_key(|route| route.cidr.prefix_len())
165            .map(|route| route.via_router)
166    }
167}
168
169#[cfg(test)]
170mod test {
171    use super::*;
172    #[cfg(feature = "proto-ipv6")]
173    mod mock {
174        use super::super::*;
175        pub const ADDR_1A: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 2, 0, 0, 0, 1);
176        pub const ADDR_1B: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 2, 0, 0, 0, 13);
177        pub const ADDR_1C: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 2, 0, 0, 0, 42);
178        pub fn cidr_1() -> Ipv6Cidr {
179            Ipv6Cidr::new(Ipv6Address::new(0xfe80, 0, 0, 2, 0, 0, 0, 0), 64)
180        }
181
182        pub const ADDR_2A: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 0x3364, 0, 0, 0, 1);
183        pub const ADDR_2B: Ipv6Address = Ipv6Address::new(0xfe80, 0, 0, 0x3364, 0, 0, 0, 21);
184        pub fn cidr_2() -> Ipv6Cidr {
185            Ipv6Cidr::new(Ipv6Address::new(0xfe80, 0, 0, 0x3364, 0, 0, 0, 0), 64)
186        }
187    }
188
189    #[cfg(all(feature = "proto-ipv4", not(feature = "proto-ipv6")))]
190    mod mock {
191        use super::super::*;
192        pub const ADDR_1A: Ipv4Address = Ipv4Address::new(192, 0, 2, 1);
193        pub const ADDR_1B: Ipv4Address = Ipv4Address::new(192, 0, 2, 13);
194        pub const ADDR_1C: Ipv4Address = Ipv4Address::new(192, 0, 2, 42);
195        pub fn cidr_1() -> Ipv4Cidr {
196            Ipv4Cidr::new(Ipv4Address::new(192, 0, 2, 0), 24)
197        }
198
199        pub const ADDR_2A: Ipv4Address = Ipv4Address::new(198, 51, 100, 1);
200        pub const ADDR_2B: Ipv4Address = Ipv4Address::new(198, 51, 100, 21);
201        pub fn cidr_2() -> Ipv4Cidr {
202            Ipv4Cidr::new(Ipv4Address::new(198, 51, 100, 0), 24)
203        }
204    }
205
206    use self::mock::*;
207
208    #[test]
209    fn test_fill() {
210        let mut routes = Routes::new();
211
212        assert_eq!(
213            routes.lookup(&ADDR_1A.into(), Instant::from_millis(0)),
214            None
215        );
216        assert_eq!(
217            routes.lookup(&ADDR_1B.into(), Instant::from_millis(0)),
218            None
219        );
220        assert_eq!(
221            routes.lookup(&ADDR_1C.into(), Instant::from_millis(0)),
222            None
223        );
224        assert_eq!(
225            routes.lookup(&ADDR_2A.into(), Instant::from_millis(0)),
226            None
227        );
228        assert_eq!(
229            routes.lookup(&ADDR_2B.into(), Instant::from_millis(0)),
230            None
231        );
232
233        let route = Route {
234            cidr: cidr_1().into(),
235            via_router: ADDR_1A.into(),
236            preferred_until: None,
237            expires_at: None,
238        };
239        routes.update(|storage| {
240            storage.push(route).unwrap();
241        });
242
243        assert_eq!(
244            routes.lookup(&ADDR_1A.into(), Instant::from_millis(0)),
245            Some(ADDR_1A.into())
246        );
247        assert_eq!(
248            routes.lookup(&ADDR_1B.into(), Instant::from_millis(0)),
249            Some(ADDR_1A.into())
250        );
251        assert_eq!(
252            routes.lookup(&ADDR_1C.into(), Instant::from_millis(0)),
253            Some(ADDR_1A.into())
254        );
255        assert_eq!(
256            routes.lookup(&ADDR_2A.into(), Instant::from_millis(0)),
257            None
258        );
259        assert_eq!(
260            routes.lookup(&ADDR_2B.into(), Instant::from_millis(0)),
261            None
262        );
263
264        let route2 = Route {
265            cidr: cidr_2().into(),
266            via_router: ADDR_2A.into(),
267            preferred_until: Some(Instant::from_millis(10)),
268            expires_at: Some(Instant::from_millis(10)),
269        };
270        routes.update(|storage| {
271            storage.push(route2).unwrap();
272        });
273
274        assert_eq!(
275            routes.lookup(&ADDR_1A.into(), Instant::from_millis(0)),
276            Some(ADDR_1A.into())
277        );
278        assert_eq!(
279            routes.lookup(&ADDR_1B.into(), Instant::from_millis(0)),
280            Some(ADDR_1A.into())
281        );
282        assert_eq!(
283            routes.lookup(&ADDR_1C.into(), Instant::from_millis(0)),
284            Some(ADDR_1A.into())
285        );
286        assert_eq!(
287            routes.lookup(&ADDR_2A.into(), Instant::from_millis(0)),
288            Some(ADDR_2A.into())
289        );
290        assert_eq!(
291            routes.lookup(&ADDR_2B.into(), Instant::from_millis(0)),
292            Some(ADDR_2A.into())
293        );
294
295        assert_eq!(
296            routes.lookup(&ADDR_1A.into(), Instant::from_millis(10)),
297            Some(ADDR_1A.into())
298        );
299        assert_eq!(
300            routes.lookup(&ADDR_1B.into(), Instant::from_millis(10)),
301            Some(ADDR_1A.into())
302        );
303        assert_eq!(
304            routes.lookup(&ADDR_1C.into(), Instant::from_millis(10)),
305            Some(ADDR_1A.into())
306        );
307        assert_eq!(
308            routes.lookup(&ADDR_2A.into(), Instant::from_millis(10)),
309            Some(ADDR_2A.into())
310        );
311        assert_eq!(
312            routes.lookup(&ADDR_2B.into(), Instant::from_millis(10)),
313            Some(ADDR_2A.into())
314        );
315    }
316}