summaryrefslogtreecommitdiff
path: root/src/iface/route.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/iface/route.rs')
-rw-r--r--src/iface/route.rs327
1 files changed, 327 insertions, 0 deletions
diff --git a/src/iface/route.rs b/src/iface/route.rs
new file mode 100644
index 0000000..123c695
--- /dev/null
+++ b/src/iface/route.rs
@@ -0,0 +1,327 @@
+use heapless::Vec;
+
+use crate::config::IFACE_MAX_ROUTE_COUNT;
+use crate::time::Instant;
+use crate::wire::{IpAddress, IpCidr};
+#[cfg(feature = "proto-ipv4")]
+use crate::wire::{Ipv4Address, Ipv4Cidr};
+#[cfg(feature = "proto-ipv6")]
+use crate::wire::{Ipv6Address, Ipv6Cidr};
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct RouteTableFull;
+
+impl core::fmt::Display for RouteTableFull {
+ fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
+ write!(f, "Route table full")
+ }
+}
+
+#[cfg(feature = "std")]
+impl std::error::Error for RouteTableFull {}
+
+/// A prefix of addresses that should be routed via a router
+#[derive(Debug, Clone, Copy)]
+#[cfg_attr(feature = "defmt", derive(defmt::Format))]
+pub struct Route {
+ pub cidr: IpCidr,
+ pub via_router: IpAddress,
+ /// `None` means "forever".
+ pub preferred_until: Option<Instant>,
+ /// `None` means "forever".
+ pub expires_at: Option<Instant>,
+}
+
+#[cfg(feature = "proto-ipv4")]
+const IPV4_DEFAULT: IpCidr = IpCidr::Ipv4(Ipv4Cidr::new(Ipv4Address::new(0, 0, 0, 0), 0));
+#[cfg(feature = "proto-ipv6")]
+const IPV6_DEFAULT: IpCidr =
+ IpCidr::Ipv6(Ipv6Cidr::new(Ipv6Address::new(0, 0, 0, 0, 0, 0, 0, 0), 0));
+
+impl Route {
+ /// Returns a route to 0.0.0.0/0 via the `gateway`, with no expiry.
+ #[cfg(feature = "proto-ipv4")]
+ pub fn new_ipv4_gateway(gateway: Ipv4Address) -> Route {
+ Route {
+ cidr: IPV4_DEFAULT,
+ via_router: gateway.into(),
+ preferred_until: None,
+ expires_at: None,
+ }
+ }
+
+ /// Returns a route to ::/0 via the `gateway`, with no expiry.
+ #[cfg(feature = "proto-ipv6")]
+ pub fn new_ipv6_gateway(gateway: Ipv6Address) -> Route {
+ Route {
+ cidr: IPV6_DEFAULT,
+ via_router: gateway.into(),
+ preferred_until: None,
+ expires_at: None,
+ }
+ }
+}
+
+/// A routing table.
+#[derive(Debug)]
+pub struct Routes {
+ storage: Vec<Route, IFACE_MAX_ROUTE_COUNT>,
+}
+
+impl Routes {
+ /// Creates a new empty routing table.
+ pub fn new() -> Self {
+ Self {
+ storage: Vec::new(),
+ }
+ }
+
+ /// Update the routes of this node.
+ pub fn update<F: FnOnce(&mut Vec<Route, IFACE_MAX_ROUTE_COUNT>)>(&mut self, f: F) {
+ f(&mut self.storage);
+ }
+
+ /// Add a default ipv4 gateway (ie. "ip route add 0.0.0.0/0 via `gateway`").
+ ///
+ /// On success, returns the previous default route, if any.
+ #[cfg(feature = "proto-ipv4")]
+ pub fn add_default_ipv4_route(
+ &mut self,
+ gateway: Ipv4Address,
+ ) -> Result<Option<Route>, RouteTableFull> {
+ let old = self.remove_default_ipv4_route();
+ self.storage
+ .push(Route::new_ipv4_gateway(gateway))
+ .map_err(|_| RouteTableFull)?;
+ Ok(old)
+ }
+
+ /// Add a default ipv6 gateway (ie. "ip -6 route add ::/0 via `gateway`").
+ ///
+ /// On success, returns the previous default route, if any.
+ #[cfg(feature = "proto-ipv6")]
+ pub fn add_default_ipv6_route(
+ &mut self,
+ gateway: Ipv6Address,
+ ) -> Result<Option<Route>, RouteTableFull> {
+ let old = self.remove_default_ipv6_route();
+ self.storage
+ .push(Route::new_ipv6_gateway(gateway))
+ .map_err(|_| RouteTableFull)?;
+ Ok(old)
+ }
+
+ /// Remove the default ipv4 gateway
+ ///
+ /// On success, returns the previous default route, if any.
+ #[cfg(feature = "proto-ipv4")]
+ pub fn remove_default_ipv4_route(&mut self) -> Option<Route> {
+ if let Some((i, _)) = self
+ .storage
+ .iter()
+ .enumerate()
+ .find(|(_, r)| r.cidr == IPV4_DEFAULT)
+ {
+ Some(self.storage.remove(i))
+ } else {
+ None
+ }
+ }
+
+ /// Remove the default ipv6 gateway
+ ///
+ /// On success, returns the previous default route, if any.
+ #[cfg(feature = "proto-ipv6")]
+ pub fn remove_default_ipv6_route(&mut self) -> Option<Route> {
+ if let Some((i, _)) = self
+ .storage
+ .iter()
+ .enumerate()
+ .find(|(_, r)| r.cidr == IPV6_DEFAULT)
+ {
+ Some(self.storage.remove(i))
+ } else {
+ None
+ }
+ }
+
+ pub(crate) fn lookup(&self, addr: &IpAddress, timestamp: Instant) -> Option<IpAddress> {
+ assert!(addr.is_unicast());
+
+ self.storage
+ .iter()
+ // Keep only matching routes
+ .filter(|route| {
+ if let Some(expires_at) = route.expires_at {
+ if timestamp > expires_at {
+ return false;
+ }
+ }
+ route.cidr.contains_addr(addr)
+ })
+ // pick the most specific one (highest prefix_len)
+ .max_by_key(|route| route.cidr.prefix_len())
+ .map(|route| route.via_router)
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ #[cfg(feature = "proto-ipv6")]
+ mod mock {
+ use super::super::*;
+ pub const ADDR_1A: Ipv6Address =
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 1]);
+ pub const ADDR_1B: Ipv6Address =
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 13]);
+ pub const ADDR_1C: Ipv6Address =
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 42]);
+ pub fn cidr_1() -> Ipv6Cidr {
+ Ipv6Cidr::new(
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0]),
+ 64,
+ )
+ }
+
+ pub const ADDR_2A: Ipv6Address =
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 51, 100, 0, 0, 0, 0, 0, 0, 0, 1]);
+ pub const ADDR_2B: Ipv6Address =
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 51, 100, 0, 0, 0, 0, 0, 0, 0, 21]);
+ pub fn cidr_2() -> Ipv6Cidr {
+ Ipv6Cidr::new(
+ Ipv6Address([0xfe, 0x80, 0, 0, 0, 0, 51, 100, 0, 0, 0, 0, 0, 0, 0, 0]),
+ 64,
+ )
+ }
+ }
+
+ #[cfg(all(feature = "proto-ipv4", not(feature = "proto-ipv6")))]
+ mod mock {
+ use super::super::*;
+ pub const ADDR_1A: Ipv4Address = Ipv4Address([192, 0, 2, 1]);
+ pub const ADDR_1B: Ipv4Address = Ipv4Address([192, 0, 2, 13]);
+ pub const ADDR_1C: Ipv4Address = Ipv4Address([192, 0, 2, 42]);
+ pub fn cidr_1() -> Ipv4Cidr {
+ Ipv4Cidr::new(Ipv4Address([192, 0, 2, 0]), 24)
+ }
+
+ pub const ADDR_2A: Ipv4Address = Ipv4Address([198, 51, 100, 1]);
+ pub const ADDR_2B: Ipv4Address = Ipv4Address([198, 51, 100, 21]);
+ pub fn cidr_2() -> Ipv4Cidr {
+ Ipv4Cidr::new(Ipv4Address([198, 51, 100, 0]), 24)
+ }
+ }
+
+ use self::mock::*;
+
+ #[test]
+ fn test_fill() {
+ let mut routes = Routes::new();
+
+ assert_eq!(
+ routes.lookup(&ADDR_1A.into(), Instant::from_millis(0)),
+ None
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1B.into(), Instant::from_millis(0)),
+ None
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1C.into(), Instant::from_millis(0)),
+ None
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2A.into(), Instant::from_millis(0)),
+ None
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2B.into(), Instant::from_millis(0)),
+ None
+ );
+
+ let route = Route {
+ cidr: cidr_1().into(),
+ via_router: ADDR_1A.into(),
+ preferred_until: None,
+ expires_at: None,
+ };
+ routes.update(|storage| {
+ storage.push(route).unwrap();
+ });
+
+ assert_eq!(
+ routes.lookup(&ADDR_1A.into(), Instant::from_millis(0)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1B.into(), Instant::from_millis(0)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1C.into(), Instant::from_millis(0)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2A.into(), Instant::from_millis(0)),
+ None
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2B.into(), Instant::from_millis(0)),
+ None
+ );
+
+ let route2 = Route {
+ cidr: cidr_2().into(),
+ via_router: ADDR_2A.into(),
+ preferred_until: Some(Instant::from_millis(10)),
+ expires_at: Some(Instant::from_millis(10)),
+ };
+ routes.update(|storage| {
+ storage.push(route2).unwrap();
+ });
+
+ assert_eq!(
+ routes.lookup(&ADDR_1A.into(), Instant::from_millis(0)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1B.into(), Instant::from_millis(0)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1C.into(), Instant::from_millis(0)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2A.into(), Instant::from_millis(0)),
+ Some(ADDR_2A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2B.into(), Instant::from_millis(0)),
+ Some(ADDR_2A.into())
+ );
+
+ assert_eq!(
+ routes.lookup(&ADDR_1A.into(), Instant::from_millis(10)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1B.into(), Instant::from_millis(10)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_1C.into(), Instant::from_millis(10)),
+ Some(ADDR_1A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2A.into(), Instant::from_millis(10)),
+ Some(ADDR_2A.into())
+ );
+ assert_eq!(
+ routes.lookup(&ADDR_2B.into(), Instant::from_millis(10)),
+ Some(ADDR_2A.into())
+ );
+ }
+}