| 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()) |
| ); |
| } |
| } |