blob: 6a2f2896e144df25fb412c310ba7f42e3d37e463 [file] [log] [blame]
// Copyright 2018 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use core::{fmt::Debug, slice::Iter};
use net_types::{
ip::{Ip, IpAddress, Subnet},
SpecifiedAddr,
};
use thiserror::Error;
use crate::ip::*;
// TODO(joshlf):
// - How do we detect circular routes? Do we attempt to detect at rule
// installation time? At runtime? Using what algorithm?
/// The destination of an outbound IP packet.
///
/// Outbound IP packets are sent to a particular device (specified by the
/// `device` field). They are sent to a particular IP host on the local network
/// attached to that device, identified by `next_hop`. Note that `next_hop` is
/// not necessarily the destination IP address of the IP packet. In particular,
/// if the destination is not on the local network, the `next_hop` will be the
/// IP address of the next IP router on the way to the destination.
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub(crate) struct Destination<A: IpAddress, D> {
pub(crate) next_hop: SpecifiedAddr<A>,
pub(crate) device: D,
}
/// An error encountered when adding a forwarding entry.
#[derive(Error, Debug, PartialEq)]
pub enum AddRouteError {
/// Indicates that the route already exists.
#[error("Already exists")]
AlreadyExists,
/// Indicates the gateway is not a neighbor of the host.
#[error("Gateway is not a neighbor")]
GatewayNotNeighbor,
}
impl From<ExistsError> for AddRouteError {
fn from(ExistsError: ExistsError) -> AddRouteError {
AddRouteError::AlreadyExists
}
}
/// An IP forwarding table.
///
/// `ForwardingTable` maps destination subnets to the nearest IP hosts (on the
/// local network) able to route IP packets to those subnets.
// TODO(ghanan): Use metrics to determine active route?
pub struct ForwardingTable<I: Ip, D> {
/// All the routes available to forward a packet.
///
/// `table` may have redundant, but unique, paths to the same
/// destination.
///
/// The entries are sorted based on the subnet's prefix length and
/// local-ness; when there's a tie in prefix length, on-linkness breaks the
/// tie (with the on-link routes appearing before off-link ones).
table: Vec<Entry<I::Addr, D>>,
}
impl<I: Ip, D> Default for ForwardingTable<I, D> {
fn default() -> ForwardingTable<I, D> {
ForwardingTable { table: Vec::new() }
}
}
impl<I: Ip, D: Clone + Debug + PartialEq> ForwardingTable<I, D> {
/// Adds `entry` to the forwarding table if it does not already exist.
fn add_entry(&mut self, entry: Entry<I::Addr, D>) -> Result<(), ExistsError> {
let Self { table } = self;
if table.contains(&entry) {
// If we already have this exact route, don't add it again.
return Err(ExistsError);
}
// Insert the new entry after the last route to a more specific and/or
// local subnet to maintain the invariant that the table is sorted by
// subnet prefix length and local-ness.
let Entry { subnet, device: _, gateway: _ } = entry;
let prefix = subnet.prefix();
table.insert(
table.partition_point(|Entry { subnet, device: _, gateway }| {
let subnet_prefix = subnet.prefix();
subnet_prefix > prefix
|| (subnet_prefix == prefix
&& match gateway {
Some(SpecifiedAddr { .. }) => false,
// on-link routes have a higher preference.
None => true,
})
}),
entry,
);
Ok(())
}
// TODO(joshlf): Should `next_hop` actually be restricted even further,
// perhaps to unicast addresses?
/// Add a route to a destination subnet that requires going through a
/// gateway.
///
/// The egress device for the gateway is calculated before inserting the
/// route to the forwarding table. Note that the gateway must be a neighbor.
pub(crate) fn add_route(
&mut self,
subnet: Subnet<I::Addr>,
gateway: SpecifiedAddr<I::Addr>,
) -> Result<(), AddRouteError> {
debug!("adding route: {} -> {}", subnet, gateway);
let device = self.lookup(None, gateway).map_or(
Err(AddRouteError::GatewayNotNeighbor),
|Destination { next_hop, device }| {
// If the gateway is a neighbor, the next hop to the gateway
// should be the gateway itself.
if next_hop != gateway {
Err(AddRouteError::GatewayNotNeighbor)
} else {
Ok(device)
}
},
)?;
self.add_entry(Entry { subnet, device, gateway: Some(gateway) }).map_err(From::from)
}
/// Add a route to a destination subnet that lives on a link an interface is
/// attached to.
pub(crate) fn add_device_route(
&mut self,
subnet: Subnet<I::Addr>,
device: D,
) -> Result<(), ExistsError> {
debug!("adding device route: {} -> {:?}", subnet, device);
self.add_entry(Entry { subnet, device, gateway: None })
}
/// Delete all routes to a subnet, returning `Err` if no route was found to
/// be deleted.
///
/// Returns all the deleted entries on success.
///
/// Note, `del_route` will remove *all* routes to a `subnet`, including
/// routes that consider `subnet` on-link for some device and routes that
/// require packets destined to a node within `subnet` to be routed through
/// some next-hop node.
pub(crate) fn del_route(
&mut self,
subnet: Subnet<I::Addr>,
) -> Result<Vec<Entry<I::Addr, D>>, NotFoundError> {
debug!("deleting route: {}", subnet);
// Delete all routes to a subnet.
//
// TODO(https://github.com/rust-lang/rust/issues/43244): Use
// drain_filter to avoid extra allocation.
let Self { table } = self;
let owned_table = core::mem::replace(table, Vec::new());
let (owned_table, removed) =
owned_table.into_iter().partition(|entry| entry.subnet != subnet);
*table = owned_table;
if removed.is_empty() {
// If a path to `subnet` was not in our installed table, then it
// definitely won't be in our active routes cache.
return Err(NotFoundError);
}
Ok(removed)
}
/// Get an iterator over all of the forwarding entries ([`Entry`]) this
/// `ForwardingTable` knows about.
pub(crate) fn iter_table(&self) -> Iter<'_, Entry<I::Addr, D>> {
self.table.iter()
}
/// Look up an address in the table.
///
/// Look up an IP address in the table, returning a next hop IP address and
/// a device to send over. If `address` is link-local, then the returned
/// next hop will be `address`. Otherwise, it will be the link-local address
/// of an IP router capable of delivering packets to `address`.
///
/// If `device` is specified, the available routes are limited to those that
/// egress over the device.
///
/// If `address` matches an entry which maps to an IP address, `lookup` will
/// look that address up in the table as well, continuing until a link-local
/// address and device are found.
///
/// If multiple entries match `address` or any intermediate IP address, the
/// entry with the longest prefix will be chosen.
///
/// The unspecified address (0.0.0.0 in IPv4 and :: in IPv6) are not
/// routable and will return None even if they have been added to the table.
pub(crate) fn lookup(
&self,
local_device: Option<D>,
address: SpecifiedAddr<I::Addr>,
) -> Option<Destination<I::Addr, D>> {
let Self { table } = self;
// Get all potential routes we could take to reach `address`.
table.iter().find_map(|Entry { subnet, device, gateway }| {
(subnet.contains(&address) && local_device.as_ref().map_or(true, |d| d == device)).then(
|| {
let next_hop =
if let Some(next_hop) = gateway { next_hop.clone() } else { address };
Destination { next_hop, device: device.clone() }
},
)
})
}
/// Retains only the entries that pass the predicate.
pub(crate) fn retain<F: FnMut(&Entry<I::Addr, D>) -> bool>(&mut self, pred: F) {
self.table.retain(pred)
}
}
#[cfg(test)]
mod tests {
use fakealloc::collections::HashSet;
use specialize_ip_macro::ip_test;
use super::*;
use crate::{device::DeviceId, testutil::DummyEventDispatcherConfig};
impl<I: Ip, D: Clone + Debug + PartialEq> ForwardingTable<I, D> {
/// Print the table.
fn print_table(&self) {
trace!("Installed Routing table:");
if self.table.is_empty() {
trace!(" No Routes");
return;
}
for Entry { subnet, device, gateway } in self.iter_table() {
trace!(" {} -> via {:?} on device {:?}", subnet, gateway, device);
}
}
}
trait TestIpExt: crate::testutil::TestIpExt {
fn subnet(v: u8, neg_prefix: u8) -> Subnet<Self::Addr>;
fn next_hop_addr_sub(
v: u8,
neg_prefix: u8,
) -> (SpecifiedAddr<Self::Addr>, Subnet<Self::Addr>);
fn next_hop_addr() -> SpecifiedAddr<Self::Addr>;
}
impl TestIpExt for Ipv4 {
fn subnet(v: u8, neg_prefix: u8) -> Subnet<Ipv4Addr> {
Subnet::new(Ipv4Addr::new([v, 0, 0, 0]), 32 - neg_prefix).unwrap()
}
fn next_hop_addr_sub(v: u8, neg_prefix: u8) -> (SpecifiedAddr<Ipv4Addr>, Subnet<Ipv4Addr>) {
(SpecifiedAddr::new(Ipv4Addr::new([v, 0, 0, 1])).unwrap(), Ipv4::subnet(v, neg_prefix))
}
fn next_hop_addr() -> SpecifiedAddr<Ipv4Addr> {
SpecifiedAddr::new(Ipv4Addr::new([10, 0, 0, 1])).unwrap()
}
}
impl TestIpExt for Ipv6 {
fn subnet(v: u8, neg_prefix: u8) -> Subnet<Ipv6Addr> {
Subnet::new(
Ipv6Addr::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, v, 0, 0, 0]),
128 - neg_prefix,
)
.unwrap()
}
fn next_hop_addr_sub(v: u8, neg_prefix: u8) -> (SpecifiedAddr<Ipv6Addr>, Subnet<Ipv6Addr>) {
(
SpecifiedAddr::new(Ipv6Addr::from([
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, v, 0, 0, 1,
]))
.unwrap(),
Ipv6::subnet(v, neg_prefix),
)
}
fn next_hop_addr() -> SpecifiedAddr<Ipv6Addr> {
SpecifiedAddr::new(Ipv6Addr::from([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 10, 0, 0, 1]))
.unwrap()
}
}
fn simple_setup<I: Ip + TestIpExt>() -> (
ForwardingTable<I, DeviceId>,
DummyEventDispatcherConfig<I::Addr>,
SpecifiedAddr<I::Addr>,
Subnet<I::Addr>,
DeviceId,
) {
let mut table = ForwardingTable::<I, DeviceId>::default();
let config = I::DUMMY_CONFIG;
let subnet = config.subnet;
let device = DeviceId::new_ethernet(0);
let (next_hop, next_hop_subnet) = I::next_hop_addr_sub(1, 1);
// Should add the route successfully.
table.add_device_route(subnet, device).unwrap();
assert_eq!(
table.iter_table().collect::<Vec<_>>(),
&[&Entry { subnet, device, gateway: None }]
);
// Attempting to add the route again should fail.
assert_eq!(table.add_device_route(subnet, device).unwrap_err(), ExistsError);
assert_eq!(
table.iter_table().collect::<Vec<_>>(),
&[&Entry { subnet, device, gateway: None }]
);
// Add the route but as a next hop route.
table.add_device_route(next_hop_subnet, device).unwrap();
table.add_route(subnet, next_hop).unwrap();
assert_eq!(
table.iter_table().collect::<HashSet<_>>(),
HashSet::from([
&Entry { subnet, device, gateway: None },
&Entry { subnet: next_hop_subnet, device, gateway: None },
&Entry { subnet, device, gateway: Some(next_hop) },
])
);
// Attempting to add the route again should fail.
assert_eq!(table.add_route(subnet, next_hop).unwrap_err(), AddRouteError::AlreadyExists);
assert_eq!(
table.iter_table().collect::<HashSet<_>>(),
HashSet::from([
&Entry { subnet, device, gateway: None },
&Entry { subnet: next_hop_subnet, device, gateway: None },
&Entry { subnet, device, gateway: Some(next_hop) },
])
);
(table, config, next_hop, next_hop_subnet, device)
}
#[ip_test]
fn test_simple_add_del<I: Ip + TestIpExt>() {
let (mut table, config, next_hop, next_hop_subnet, device) = simple_setup::<I>();
assert_eq!(table.iter_table().count(), 3);
// Delete all routes to subnet.
assert_eq!(
table.del_route(config.subnet).unwrap().into_iter().collect::<HashSet<_>>(),
HashSet::from([
Entry { subnet: config.subnet, device, gateway: None },
Entry { subnet: config.subnet, device, gateway: Some(next_hop) }
])
);
assert_eq!(
table.iter_table().collect::<Vec<_>>(),
&[&Entry { subnet: next_hop_subnet, device, gateway: None }]
);
}
#[ip_test]
fn test_simple_lookup<I: Ip + TestIpExt>() {
let (mut table, config, next_hop, _next_hop_subnet, device) = simple_setup::<I>();
// Do lookup for our next hop (should be the device).
assert_eq!(table.lookup(None, next_hop), Some(Destination { next_hop, device }));
// Do lookup for some address within `subnet`.
assert_eq!(
table.lookup(None, config.local_ip),
Some(Destination { next_hop: config.local_ip, device })
);
assert_eq!(
table.lookup(None, config.remote_ip),
Some(Destination { next_hop: config.remote_ip, device })
);
// Delete routes to the subnet and make sure that we can no longer route
// to destinations in the subnet.
assert_eq!(
table.del_route(config.subnet).unwrap().into_iter().collect::<HashSet<_>>(),
HashSet::from([
Entry { subnet: config.subnet, device, gateway: None },
Entry { subnet: config.subnet, device, gateway: Some(next_hop) }
])
);
assert_eq!(table.lookup(None, next_hop), Some(Destination { next_hop, device }));
assert_eq!(table.lookup(None, config.local_ip), None);
assert_eq!(table.lookup(None, config.remote_ip), None);
// Make the subnet routable again but through a gateway.
table.add_route(config.subnet, next_hop).unwrap();
assert_eq!(table.lookup(None, next_hop), Some(Destination { next_hop, device }));
assert_eq!(table.lookup(None, config.local_ip), Some(Destination { next_hop, device }));
assert_eq!(table.lookup(None, config.remote_ip), Some(Destination { next_hop, device }));
}
#[ip_test]
fn test_default_route_ip<I: Ip + TestIpExt>() {
let mut table = ForwardingTable::<I, DeviceId>::default();
let device0 = DeviceId::new_ethernet(0);
let (addr1, sub1) = I::next_hop_addr_sub(1, 24);
let (addr2, _) = I::next_hop_addr_sub(2, 24);
let (addr3, _) = I::next_hop_addr_sub(3, 24);
// Add the following routes:
// sub1 -> device0
//
// Our expected forwarding table should look like:
// sub1 -> device0
table.add_device_route(sub1, device0).unwrap();
table.print_table();
assert_eq!(
table.lookup(None, addr1).unwrap(),
Destination { next_hop: addr1, device: device0 }
);
assert_eq!(table.lookup(None, addr2), None);
// Add a default route.
//
// Our expected forwarding table should look like:
// sub1 -> device0
// default -> addr1 w/ device0
let default_sub = Subnet::new(I::UNSPECIFIED_ADDRESS, 0).unwrap();
table.add_route(default_sub, addr1).unwrap();
assert_eq!(
table.lookup(None, addr1).unwrap(),
Destination { next_hop: addr1, device: device0 }
);
assert_eq!(
table.lookup(None, addr2).unwrap(),
Destination { next_hop: addr1, device: device0 }
);
assert_eq!(
table.lookup(None, addr3).unwrap(),
Destination { next_hop: addr1, device: device0 }
);
}
#[ip_test]
fn test_device_filter_with_varying_prefix_lengths<I: Ip + TestIpExt>() {
const MORE_SPECIFIC_SUB_DEVICE: u8 = 1;
const LESS_SPECIFIC_SUB_DEVICE: u8 = 2;
let mut table = ForwardingTable::<I, u8>::default();
let (next_hop, more_specific_sub) = I::next_hop_addr_sub(1, 1);
let less_specific_sub = {
let (addr, sub) = I::next_hop_addr_sub(1, 2);
assert_eq!(next_hop, addr);
sub
};
table.add_device_route(less_specific_sub, LESS_SPECIFIC_SUB_DEVICE).unwrap();
assert_eq!(
table.lookup(None, next_hop),
Some(Destination { next_hop, device: LESS_SPECIFIC_SUB_DEVICE }),
"matches route"
);
assert_eq!(
table.lookup(Some(LESS_SPECIFIC_SUB_DEVICE), next_hop),
Some(Destination { next_hop, device: LESS_SPECIFIC_SUB_DEVICE }),
"route matches specified device"
);
assert_eq!(
table.lookup(Some(MORE_SPECIFIC_SUB_DEVICE), next_hop),
None,
"no route with the specified device"
);
table.add_device_route(more_specific_sub, MORE_SPECIFIC_SUB_DEVICE).unwrap();
assert_eq!(
table.lookup(None, next_hop).unwrap(),
Destination { next_hop, device: MORE_SPECIFIC_SUB_DEVICE },
"matches most specific route"
);
assert_eq!(
table.lookup(Some(LESS_SPECIFIC_SUB_DEVICE), next_hop),
Some(Destination { next_hop, device: LESS_SPECIFIC_SUB_DEVICE }),
"matches less specific route with the specified device"
);
assert_eq!(
table.lookup(Some(MORE_SPECIFIC_SUB_DEVICE), next_hop).unwrap(),
Destination { next_hop, device: MORE_SPECIFIC_SUB_DEVICE },
"matches the most specific route with the specified device"
);
}
#[ip_test]
fn test_multiple_routes_to_subnet_through_different_devices<I: Ip + TestIpExt>() {
const DEVICE1: u8 = 1;
const DEVICE2: u8 = 2;
let mut table = ForwardingTable::<I, u8>::default();
let (next_hop, sub) = I::next_hop_addr_sub(1, 1);
table.add_device_route(sub, DEVICE1).unwrap();
table.add_device_route(sub, DEVICE2).unwrap();
let lookup = table.lookup(None, next_hop);
assert!(
[
Some(Destination { next_hop, device: DEVICE1 }),
Some(Destination { next_hop, device: DEVICE2 })
]
.contains(&lookup),
"lookup = {:?}",
lookup
);
assert_eq!(
table.lookup(Some(DEVICE1), next_hop),
Some(Destination { next_hop, device: DEVICE1 }),
);
assert_eq!(
table.lookup(Some(DEVICE2), next_hop),
Some(Destination { next_hop, device: DEVICE2 }),
);
}
}