Skip to main content

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