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