Before this change, Ferrix only installed a route if its BGP next hop was
directly connected: an address in the subnet of an interface that is up.
A route with any other next hop was held at import and never installed.
This made routes from iBGP neighbors unusable unless the neighbor set
itself as the next hop, and it is why the route reflector rewrites the
next hop on every reflected route.
Ferrix now resolves a next hop that is not directly connected through the
kernel's own routes, as other BGP implementations do:
* The route is looked up in one kernel table, the "resolution table"
(`kernel { resolution_table: N }`, default 254 = main). The longest
prefix that contains the next hop wins; of several routes for that
prefix, the one with the lowest metric.
* Because the Linux kernel does not resolve gateways recursively, the BGP
route is installed with the gateway and interface of the kernel route
that covers the next hop. A kernel route without a gateway (a route
onto a point-to-point or tunnel interface) makes the next hop itself
the gateway on that interface. A kernel route that names a nexthop
object is resolved through the object.
* Routes with protocol `bgp` never resolve a next hop, whether Ferrix's
own or another daemon's. A next hop that is reachable only through a
BGP route depends on another BGP next hop, and such chains are not
predictable.
* The default route does not resolve a next hop unless
`kernel { resolve_via_default_route: true }` is set. On a router the
default route points at a transit provider, and a next hop that is
reachable only through it is not usable.
* Blackhole, unreachable and prohibit routes make the next hop
unresolvable, as does a route whose interface is down.
The same resolution is used by the import policy and the revalidator, so
a route whose next hop cannot be resolved is still held rather than
selected (RFC 4271 section 9.1.2.1), and is taken when a kernel route
makes its next hop resolvable.
Implementation:
* `bgp_fib::routes::KernelRoutes` holds the routes of the resolution
table and answers longest-prefix lookups.
* `bgp_fib::topology::Topology` bundles the interfaces, the kernel routes
and the kernel's nexthop objects into one value. The kernel task
publishes it through `Fib::topology()` (formerly `Fib::interfaces()`),
and `plan::resolve` takes it in place of `Interfaces`.
* The kernel task learns the resolution table with a dump of that table
alone, using NETLINK_GET_STRICT_CHK (Linux 4.20 and later; older
kernels get a dump of every table, filtered in user space). It asks
again, debounced, whenever a dedicated netlink socket that is a member
of the route multicast groups receives anything. That socket is given a
64 KiB receive buffer on purpose: the kernel reports Ferrix's own route
installs on it too, up to a million at a time, and an ENOBUFS overflow
is treated as the same hint as a message. Its contents are never
parsed. An earlier attempt to receive route notifications on the
socket that writes routes failed, because the kernel only excludes the
requesting socket from a notification when NLM_F_ECHO was requested.
* `Unresolved::NotConnected` is replaced by `NoRoute`, `OnlyDefault` and
`Discarded`, which appear as the reasons in `/api/kernel` and on the
status page.
Configuration: two new `kernel` fields, `resolution_table` and
`resolve_via_default_route`, documented in the configuration reference
and in the kernel guide, whose "Next Hop Resolution" section is rewritten.
Existing configurations behave as before except that routes with far
next hops may now be installed where a static or connected route covers
the next hop.
Testing: unit tests for `KernelRoutes`, `Topology` and resolution
through static, tunnel, nexthop-object, blackhole and default routes; a
kernel test (`a_far_next_hop_goes_the_way_the_kernels_routes_say`) that
checks the installed routes follow static, tunnel and nexthop-object
routes as they are added, replaced and removed; the existing kernel
tests, the interoperability lab with FRR and BIRD, and the workspace
tests, all passing. Installing 200,000 routes still runs at about
186,000 routes per second.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1145 lines
41 KiB
Rust
1145 lines
41 KiB
Rust
//! What should be in the kernel's table, what is, and what to do about the
|
|
//! difference. No I/O here: this is told what the routing table's best
|
|
//! routes are and what the kernel said, and answers with the changes to
|
|
//! ask the kernel for.
|
|
|
|
use std::net::{IpAddr, Ipv6Addr};
|
|
|
|
use bgp_packet::message::NlriNextHop;
|
|
use bgp_primitives::constants::IpVersion;
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
|
|
use crate::netlink::KIND_UNICAST;
|
|
use crate::topology::Topology;
|
|
|
|
// Asked for every route: a quick hash matters here.
|
|
pub(crate) type HashMap<K, V> = std::collections::HashMap<K, V, foldhash::fast::RandomState>;
|
|
pub(crate) type HashSet<K> = std::collections::HashSet<K, foldhash::fast::RandomState>;
|
|
|
|
/// Where the kernel is to send packets for a prefix.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct Target {
|
|
pub gateway: IpAddr,
|
|
/// The index of the interface the gateway is on.
|
|
pub interface: u32,
|
|
}
|
|
|
|
/// Why a route's next hop is not something the kernel can be given.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum Unresolved {
|
|
/// The route has no next hop.
|
|
Missing,
|
|
/// An IPv6 route with an IPv4 next hop.
|
|
OtherFamily,
|
|
/// The next hop is one of this machine's own addresses.
|
|
Ourselves,
|
|
/// The next hop is not in the network of any interface that is up,
|
|
/// and none of the kernel's routes that count says the way to it
|
|
/// (see `crate::routes`).
|
|
NoRoute,
|
|
/// The only route to the next hop is the default route, which is not
|
|
/// taken as the way to a next hop unless asked for
|
|
/// (`Topology::via_default`).
|
|
OnlyDefault,
|
|
/// The route to the next hop is one that discards packets: a
|
|
/// blackhole, unreachable or prohibit route.
|
|
Discarded,
|
|
/// The route is for a network this machine is on itself, or for part
|
|
/// of one. The kernel reaches that directly; a route from a peer that
|
|
/// said otherwise would send the neighbors' traffic, and the sessions
|
|
/// with them, wherever the peer liked.
|
|
OwnNetwork,
|
|
}
|
|
|
|
impl Unresolved {
|
|
pub const ALL: [Self; 7] = [
|
|
Self::Missing,
|
|
Self::OtherFamily,
|
|
Self::Ourselves,
|
|
Self::NoRoute,
|
|
Self::OnlyDefault,
|
|
Self::Discarded,
|
|
Self::OwnNetwork,
|
|
];
|
|
|
|
pub fn as_str(self) -> &'static str {
|
|
match self {
|
|
Self::Missing => "no next hop",
|
|
Self::OtherFamily => "IPv4 next hop for an IPv6 route",
|
|
Self::Ourselves => "next hop is this machine",
|
|
Self::NoRoute => "no route to the next hop",
|
|
Self::OnlyDefault => "next hop reachable only by the default route",
|
|
Self::Discarded => "the route to the next hop discards packets",
|
|
Self::OwnNetwork => "inside a network of this machine",
|
|
}
|
|
}
|
|
}
|
|
|
|
// A reason's place in `ALL` is its number as declared, which is what
|
|
// [`Plan`] counts them by.
|
|
const _: () = {
|
|
let mut at = 0;
|
|
while at < Unresolved::ALL.len() {
|
|
assert!(Unresolved::ALL[at] as usize == at);
|
|
at += 1;
|
|
}
|
|
};
|
|
|
|
/// The peer a route was learned from, as far as its next hop is concerned.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct Neighbor {
|
|
pub address: IpAddr,
|
|
/// The interface the session with the peer is on, where the peer's
|
|
/// address is a link-local one and says nothing without it; zero
|
|
/// otherwise.
|
|
pub interface: u32,
|
|
}
|
|
|
|
impl From<IpAddr> for Neighbor {
|
|
fn from(address: IpAddr) -> Self {
|
|
Self {
|
|
address,
|
|
interface: 0,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Works out where the kernel is to send packets for a route to `prefix`
|
|
/// with the next hop `next_hop`, learned from `peer`.
|
|
///
|
|
/// A next hop that is a neighbor on one of our interfaces is where the
|
|
/// packets go. One that is not is looked up in the kernel's own routes,
|
|
/// and the packets go where the route that covers it sends them: to its
|
|
/// gateway, or onto its interface. A link-local next hop means something
|
|
/// only on one link, and that is taken to be the link the peer itself is
|
|
/// on.
|
|
pub fn resolve(
|
|
topology: &Topology,
|
|
prefix: &IpPrefix,
|
|
next_hop: Option<&NlriNextHop>,
|
|
peer: Option<Neighbor>,
|
|
) -> Result<Target, Unresolved> {
|
|
if topology.interfaces.has_network_around(prefix) {
|
|
return Err(Unresolved::OwnNetwork);
|
|
}
|
|
resolve_next_hop(topology, prefix.address_family(), next_hop, peer)
|
|
}
|
|
|
|
/// The way to `gateway`, which is not a neighbor, by the kernel's routes
|
|
/// that count: where the route that covers it sends packets. One step
|
|
/// only: the kernel sees to it that a route's own gateway is a neighbor.
|
|
fn through_routes(
|
|
topology: &Topology,
|
|
routes: IpVersion,
|
|
gateway: IpAddr,
|
|
) -> Result<Target, Unresolved> {
|
|
let route = topology.routes.lookup(gateway).ok_or(Unresolved::NoRoute)?;
|
|
if route.prefix.length() == 0 && !topology.via_default {
|
|
return Err(Unresolved::OnlyDefault);
|
|
}
|
|
if route.kind != KIND_UNICAST {
|
|
return Err(Unresolved::Discarded);
|
|
}
|
|
let (hop, interface) = match route.nexthop {
|
|
Some(id) => {
|
|
// A group of nexthops has no gateway or interface of its own;
|
|
// which of its members packets take is the kernel's to say,
|
|
// and nothing to give it as a route.
|
|
let nexthop = topology.nexthops.get(&id).ok_or(Unresolved::NoRoute)?;
|
|
(
|
|
nexthop.gateway,
|
|
nexthop.interface.ok_or(Unresolved::NoRoute)?,
|
|
)
|
|
}
|
|
None => (route.gateway, route.interface.ok_or(Unresolved::NoRoute)?),
|
|
};
|
|
if !topology
|
|
.interfaces
|
|
.link(interface)
|
|
.is_some_and(|link| link.up)
|
|
{
|
|
return Err(Unresolved::NoRoute);
|
|
}
|
|
// A route without a gateway puts the destination on its interface:
|
|
// the next hop itself is the neighbor there.
|
|
let gateway = hop.unwrap_or(gateway);
|
|
// An IPv4 route may go by an IPv6 gateway (RFC 8950, and the kernel
|
|
// allows it); an IPv6 route by an IPv4 gateway is nothing.
|
|
if routes == IpVersion::Ipv6 && gateway.is_ipv4() {
|
|
return Err(Unresolved::NoRoute);
|
|
}
|
|
Ok(Target { gateway, interface })
|
|
}
|
|
|
|
/// The part of [`resolve`] that is the same for every route of IP version
|
|
/// `routes` with one next hop from one peer: where the next hop is. For
|
|
/// those who ask it of many such routes, and ask of each only whether it
|
|
/// is for one of the machine's own networks.
|
|
pub fn resolve_next_hop(
|
|
topology: &Topology,
|
|
routes: IpVersion,
|
|
next_hop: Option<&NlriNextHop>,
|
|
peer: Option<Neighbor>,
|
|
) -> Result<Target, Unresolved> {
|
|
let interfaces = &topology.interfaces;
|
|
let next_hop = next_hop.ok_or(Unresolved::Missing)?;
|
|
let version = match next_hop {
|
|
NlriNextHop::Ipv4(_) => IpVersion::Ipv4,
|
|
NlriNextHop::Ipv6(_) | NlriNextHop::Ipv6WithLl { .. } => IpVersion::Ipv6,
|
|
};
|
|
// An IPv4 route may have an IPv6 next hop: that is how routes go over
|
|
// a link with only IPv6 addresses (RFC 8950), and the kernel takes
|
|
// such a route. The other way about means nothing.
|
|
if version != routes && version == IpVersion::Ipv4 {
|
|
return Err(Unresolved::OtherFamily);
|
|
}
|
|
let direct = |gateway: IpAddr| {
|
|
if interfaces.is_local(gateway) {
|
|
return Err(Unresolved::Ourselves);
|
|
}
|
|
match interfaces.connected(gateway) {
|
|
Some(connected) => Ok(Target {
|
|
gateway,
|
|
interface: connected.interface,
|
|
}),
|
|
None => through_routes(topology, routes, gateway),
|
|
}
|
|
};
|
|
// The interface the peer is a neighbor on, for a link-local gateway.
|
|
let on_peers_link = |gateway: Ipv6Addr| {
|
|
let interface = match peer.ok_or(Unresolved::NoRoute)? {
|
|
// The session is on a link-local address itself, and so says
|
|
// which link: every interface has fe80::/64, and looking the
|
|
// peer up there would find any of them.
|
|
Neighbor { interface, .. } if interface != 0 => interfaces
|
|
.link(interface)
|
|
.filter(|link| link.up)
|
|
.map(|link| link.index),
|
|
Neighbor { address, .. } => interfaces
|
|
.connected(address)
|
|
.map(|connected| connected.interface),
|
|
}
|
|
.ok_or(Unresolved::NoRoute)?;
|
|
if interfaces
|
|
.addresses_of(interface)
|
|
.any(|ours| ours.local == IpAddr::V6(gateway))
|
|
{
|
|
return Err(Unresolved::Ourselves);
|
|
}
|
|
Ok(Target {
|
|
gateway: gateway.into(),
|
|
interface,
|
|
})
|
|
};
|
|
match *next_hop {
|
|
NlriNextHop::Ipv4(gateway) => direct(gateway.into()),
|
|
NlriNextHop::Ipv6(gateway) if gateway.is_unicast_link_local() => on_peers_link(gateway),
|
|
NlriNextHop::Ipv6(gateway) => direct(gateway.into()),
|
|
// The global address if it is a neighbor; else the link-local one,
|
|
// which is there for exactly that case (RFC 2545 §3).
|
|
// Some put the link-local address, or nothing, where the global
|
|
// one goes, having no global one to give.
|
|
NlriNextHop::Ipv6WithLl { global, link_local }
|
|
if global.is_unspecified() || global.is_unicast_link_local() =>
|
|
{
|
|
on_peers_link(link_local)
|
|
}
|
|
// Where there is no way to the global one, the link-local one is
|
|
// tried; a way that is there and is bad (ourselves, a blackhole)
|
|
// is an answer as it stands.
|
|
NlriNextHop::Ipv6WithLl { global, link_local } => match direct(global.into()) {
|
|
Err(Unresolved::NoRoute | Unresolved::OnlyDefault) => on_peers_link(link_local),
|
|
resolved => resolved,
|
|
},
|
|
}
|
|
}
|
|
|
|
/// A change to ask the kernel for.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum Op {
|
|
/// Put this route there, in place of ours for the prefix if there is
|
|
/// one.
|
|
Install {
|
|
prefix: IpPrefix,
|
|
target: Target,
|
|
},
|
|
Remove {
|
|
prefix: IpPrefix,
|
|
},
|
|
}
|
|
|
|
impl Op {
|
|
pub fn prefix(&self) -> &IpPrefix {
|
|
match self {
|
|
Self::Install { prefix, .. } | Self::Remove { prefix } => prefix,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct Entry {
|
|
/// What the kernel has; `None` for a route found there at startup that
|
|
/// is not of a shape we make.
|
|
target: Option<Target>,
|
|
/// Whether the prefix was heard of since the last reset.
|
|
confirmed: bool,
|
|
/// Whether the route is one an earlier run left that is being kept
|
|
/// for now though not confirmed: see [`Plan::adopt`].
|
|
kept: bool,
|
|
}
|
|
|
|
/// The routes of ours in the kernel, kept in step with the best routes.
|
|
///
|
|
/// It follows what a reader of the routing table is given: the whole
|
|
/// table between a reset and its completion, and changes after. Whatever
|
|
/// is in the kernel and was not confirmed by the end of a whole table is
|
|
/// removed: that is how routes left by an earlier run, or made stale by a
|
|
/// change of interfaces, are cleared out.
|
|
#[derive(Default)]
|
|
pub struct Plan {
|
|
/// What the kernel has been asked to have, and has not refused.
|
|
installed: HashMap<IpPrefix, Entry>,
|
|
/// Prefixes whose best route is not installed, and why.
|
|
unresolved: HashMap<IpPrefix, Unresolved>,
|
|
/// How many of them for each reason, in the order of
|
|
/// [`Unresolved::ALL`].
|
|
unresolved_counts: [usize; Unresolved::ALL.len()],
|
|
/// Prefixes whose route the kernel refused.
|
|
refused: HashSet<IpPrefix>,
|
|
}
|
|
|
|
impl Plan {
|
|
/// How many routes of ours the kernel has.
|
|
pub fn installed(&self) -> usize {
|
|
self.installed.len()
|
|
}
|
|
|
|
/// How many prefixes have a best route that could not be given to the
|
|
/// kernel for want of a usable next hop.
|
|
pub fn unresolved(&self) -> usize {
|
|
self.unresolved.len()
|
|
}
|
|
|
|
/// The same, by reason, leaving out the reasons there are none of.
|
|
pub fn unresolved_by_reason(&self) -> impl Iterator<Item = (Unresolved, usize)> {
|
|
Unresolved::ALL
|
|
.into_iter()
|
|
.zip(self.unresolved_counts)
|
|
.filter(|(_, count)| *count > 0)
|
|
}
|
|
|
|
/// Notes why the best route of `prefix` is not installed, or with
|
|
/// `None` that this is no longer so.
|
|
fn set_unresolved(&mut self, prefix: IpPrefix, reason: Option<Unresolved>) {
|
|
let before = match reason {
|
|
Some(reason) => self.unresolved.insert(prefix, reason),
|
|
None => self.unresolved.remove(&prefix),
|
|
};
|
|
if let Some(before) = before {
|
|
self.unresolved_counts[before as usize] -= 1;
|
|
}
|
|
if let Some(reason) = reason {
|
|
self.unresolved_counts[reason as usize] += 1;
|
|
}
|
|
}
|
|
|
|
/// Forgets which prefixes are unresolved or refused, and why.
|
|
fn forget_unresolved_and_refused(&mut self) {
|
|
self.unresolved.clear();
|
|
self.unresolved_counts = [0; Unresolved::ALL.len()];
|
|
self.refused.clear();
|
|
}
|
|
|
|
/// How many prefixes have a best route that the kernel refused.
|
|
pub fn refused(&self) -> usize {
|
|
self.refused.len()
|
|
}
|
|
|
|
/// Notes a route of ours that the kernel already has. It stays only if
|
|
/// the routing table turns out to have the same.
|
|
///
|
|
/// With `keep` it is not removed for being absent from a whole table,
|
|
/// only by [`Plan::release`]. That is for the routes an earlier run
|
|
/// left: when the daemon starts, its table is empty until its peers
|
|
/// have sent theirs again, and to remove everything meanwhile would
|
|
/// stop the machine forwarding for as long as that takes.
|
|
pub fn adopt(&mut self, prefix: IpPrefix, target: Option<Target>, keep: bool) {
|
|
self.installed.insert(
|
|
prefix,
|
|
Entry {
|
|
target,
|
|
confirmed: false,
|
|
kept: keep,
|
|
},
|
|
);
|
|
}
|
|
|
|
/// How many routes are being kept from an earlier run without the
|
|
/// routing table having confirmed them.
|
|
pub fn kept(&self) -> usize {
|
|
self.installed
|
|
.values()
|
|
.filter(|entry| entry.kept && !entry.confirmed)
|
|
.count()
|
|
}
|
|
|
|
/// The time for keeping unconfirmed routes of an earlier run is over:
|
|
/// those the routing table still does not have go.
|
|
pub fn release(&mut self) -> Vec<Op> {
|
|
let mut stale = Vec::new();
|
|
self.installed.retain(|prefix, entry| {
|
|
let goes = entry.kept && !entry.confirmed;
|
|
entry.kept = false;
|
|
if goes {
|
|
stale.push(Op::Remove { prefix: *prefix });
|
|
}
|
|
!goes
|
|
});
|
|
stale
|
|
}
|
|
|
|
/// The whole table follows: nothing is known to be wanted until it is
|
|
/// heard of again.
|
|
pub fn reset(&mut self) {
|
|
for entry in self.installed.values_mut() {
|
|
entry.confirmed = false;
|
|
}
|
|
self.forget_unresolved_and_refused();
|
|
}
|
|
|
|
/// The best route for `prefix` is now one that resolves as `target`.
|
|
pub fn best(&mut self, prefix: IpPrefix, target: Result<Target, Unresolved>) -> Option<Op> {
|
|
// Nearly always there is nothing to forget.
|
|
if !self.refused.is_empty() {
|
|
self.refused.remove(&prefix);
|
|
}
|
|
match target {
|
|
Ok(target) => {
|
|
if !self.unresolved.is_empty() {
|
|
self.set_unresolved(prefix, None);
|
|
}
|
|
let entry = self.installed.entry(prefix).or_insert(Entry {
|
|
target: None,
|
|
confirmed: true,
|
|
kept: false,
|
|
});
|
|
entry.confirmed = true;
|
|
if entry.target == Some(target) {
|
|
return None;
|
|
}
|
|
entry.target = Some(target);
|
|
Some(Op::Install { prefix, target })
|
|
}
|
|
Err(reason) => {
|
|
self.set_unresolved(prefix, Some(reason));
|
|
// Packets must not go on following a route that is no
|
|
// longer the best.
|
|
self.installed
|
|
.remove(&prefix)
|
|
.map(|_| Op::Remove { prefix })
|
|
}
|
|
}
|
|
}
|
|
|
|
/// `prefix` has no route any more.
|
|
pub fn withdrawn(&mut self, prefix: IpPrefix) -> Option<Op> {
|
|
self.set_unresolved(prefix, None);
|
|
self.refused.remove(&prefix);
|
|
self.installed
|
|
.remove(&prefix)
|
|
.map(|_| Op::Remove { prefix })
|
|
}
|
|
|
|
/// The whole table has been given: what was not in it goes.
|
|
pub fn synced(&mut self) -> Vec<Op> {
|
|
let stale: Vec<IpPrefix> = self
|
|
.installed
|
|
.iter()
|
|
.filter(|(_, entry)| !entry.confirmed && !entry.kept)
|
|
.map(|(prefix, _)| *prefix)
|
|
.collect();
|
|
stale
|
|
.into_iter()
|
|
.map(|prefix| {
|
|
self.installed.remove(&prefix);
|
|
Op::Remove { prefix }
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// The kernel refused `op`. Returns what to ask of it instead, if
|
|
/// anything.
|
|
pub fn refused_by_kernel(&mut self, op: &Op) -> Option<Op> {
|
|
match op {
|
|
Op::Install { prefix, target } => {
|
|
// If the plan has moved on since, this is old news.
|
|
if self.installed.get(prefix)?.target != Some(*target) {
|
|
return None;
|
|
}
|
|
self.installed.remove(prefix);
|
|
self.refused.insert(*prefix);
|
|
// What the kernel refused to replace, it still has.
|
|
Some(Op::Remove { prefix: *prefix })
|
|
}
|
|
// It was not there to remove: as wanted.
|
|
Op::Remove { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// Everything goes: the changes that empty the kernel of our routes.
|
|
pub fn clear(&mut self) -> Vec<Op> {
|
|
self.forget_unresolved_and_refused();
|
|
self.installed
|
|
.drain()
|
|
.map(|(prefix, _)| Op::Remove { prefix })
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use proptest::prelude::*;
|
|
|
|
use super::*;
|
|
use crate::netlink::{Address, Link, Message, Nexthop, PROTOCOL_BGP, Route, TABLE_MAIN};
|
|
|
|
fn prefix(s: &str) -> IpPrefix {
|
|
IpPrefix::try_from(s).unwrap()
|
|
}
|
|
|
|
fn target(gateway: &str, interface: u32) -> Target {
|
|
Target {
|
|
gateway: gateway.parse().unwrap(),
|
|
interface,
|
|
}
|
|
}
|
|
|
|
fn interfaces() -> Topology {
|
|
let mut topology = Topology::new(TABLE_MAIN, false);
|
|
for (index, local, network) in [
|
|
(2, "192.0.2.1", "192.0.2.0/24"),
|
|
(2, "2001:db8::1", "2001:db8::/64"),
|
|
(3, "198.51.100.1", "198.51.100.0/24"),
|
|
] {
|
|
topology.apply(&Message::Link {
|
|
present: true,
|
|
link: Link {
|
|
index,
|
|
name: format!("eth{index}"),
|
|
up: true,
|
|
},
|
|
});
|
|
topology.apply(&Message::Address {
|
|
present: true,
|
|
address: Address {
|
|
interface: index,
|
|
local: local.parse().unwrap(),
|
|
network: prefix(network),
|
|
},
|
|
});
|
|
}
|
|
topology
|
|
}
|
|
|
|
/// A route of the kernel's main table, as a static route is.
|
|
fn kernel_route(prefix_text: &str, gateway: Option<&str>, interface: Option<u32>) -> Route {
|
|
Route {
|
|
prefix: prefix(prefix_text),
|
|
table: TABLE_MAIN,
|
|
protocol: 4,
|
|
metric: 0,
|
|
gateway: gateway.map(|g| g.parse().unwrap()),
|
|
interface,
|
|
nexthop: None,
|
|
kind: KIND_UNICAST,
|
|
}
|
|
}
|
|
|
|
fn with_route(topology: &mut Topology, route: Route) {
|
|
assert!(topology.apply(&Message::Route {
|
|
present: true,
|
|
route,
|
|
}));
|
|
}
|
|
|
|
fn neighbor(address: &str) -> Neighbor {
|
|
address.parse::<IpAddr>().unwrap().into()
|
|
}
|
|
|
|
#[test]
|
|
fn a_next_hop_resolves_if_it_is_a_neighbor() {
|
|
let interfaces = interfaces();
|
|
let v4 = prefix("203.0.113.0/24");
|
|
let v6 = prefix("2001:db8:100::/48");
|
|
let peer = Some(neighbor("192.0.2.2"));
|
|
let v4_hop = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap());
|
|
let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap());
|
|
|
|
assert_eq!(
|
|
resolve(&interfaces, &v4, Some(&v4_hop("192.0.2.2")), peer),
|
|
Ok(target("192.0.2.2", 2))
|
|
);
|
|
// A neighbor other than the peer: a route server's routes.
|
|
assert_eq!(
|
|
resolve(&interfaces, &v4, Some(&v4_hop("198.51.100.9")), peer),
|
|
Ok(target("198.51.100.9", 3))
|
|
);
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&v6_hop("2001:db8::2")), peer),
|
|
Ok(target("2001:db8::2", 2))
|
|
);
|
|
for (hop, why) in [
|
|
(Some(v4_hop("10.9.9.9")), Unresolved::NoRoute),
|
|
(Some(v4_hop("192.0.2.1")), Unresolved::Ourselves),
|
|
(None, Unresolved::Missing),
|
|
] {
|
|
assert_eq!(resolve(&interfaces, &v4, hop.as_ref(), peer), Err(why));
|
|
}
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&v4_hop("192.0.2.2")), peer),
|
|
Err(Unresolved::OtherFamily)
|
|
);
|
|
// A route for part of a network we are on is not for the kernel,
|
|
// however good its next hop; one for more than the network is.
|
|
for ours in [
|
|
"192.0.2.0/24",
|
|
"192.0.2.128/25",
|
|
"192.0.2.7/32",
|
|
"2001:db8::/64",
|
|
] {
|
|
let ours = prefix(ours);
|
|
let hop = match ours.address_family() {
|
|
IpVersion::Ipv4 => v4_hop("192.0.2.2"),
|
|
IpVersion::Ipv6 => v6_hop("2001:db8::2"),
|
|
};
|
|
assert_eq!(
|
|
resolve(&interfaces, &ours, Some(&hop), peer),
|
|
Err(Unresolved::OwnNetwork),
|
|
"{ours}"
|
|
);
|
|
}
|
|
assert_eq!(
|
|
resolve(
|
|
&interfaces,
|
|
&prefix("192.0.0.0/22"),
|
|
Some(&v4_hop("192.0.2.2")),
|
|
peer
|
|
),
|
|
Ok(target("192.0.2.2", 2))
|
|
);
|
|
// An IPv4 route by way of an IPv6 neighbor: a link with only IPv6
|
|
// addresses (RFC 8950).
|
|
assert_eq!(
|
|
resolve(&interfaces, &v4, Some(&v6_hop("2001:db8::2")), peer),
|
|
Ok(target("2001:db8::2", 2))
|
|
);
|
|
assert_eq!(
|
|
resolve(
|
|
&interfaces,
|
|
&v4,
|
|
Some(&NlriNextHop::Ipv6WithLl {
|
|
global: "2001:db8:ffff::2".parse().unwrap(),
|
|
link_local: "fe80::2".parse().unwrap(),
|
|
}),
|
|
Some(neighbor("2001:db8::2")),
|
|
),
|
|
Ok(target("fe80::2", 2))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn a_link_local_next_hop_is_on_the_peers_link() {
|
|
let interfaces = interfaces();
|
|
let v6 = prefix("2001:db8:100::/48");
|
|
let peer = Some(neighbor("2001:db8::2"));
|
|
let link_local: Ipv6Addr = "fe80::2".parse().unwrap();
|
|
let both = |global: &str| NlriNextHop::Ipv6WithLl {
|
|
global: global.parse().unwrap(),
|
|
link_local,
|
|
};
|
|
// The global address is a neighbor: it is used.
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&both("2001:db8::2")), peer),
|
|
Ok(target("2001:db8::2", 2))
|
|
);
|
|
// It is not: the link-local one, on the interface the peer is on.
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&both("2001:db8:ffff::2")), peer),
|
|
Ok(target("fe80::2", 2))
|
|
);
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), peer),
|
|
Ok(target("fe80::2", 2))
|
|
);
|
|
// A peer that is not a neighbor says nothing about the link.
|
|
let far = Some(neighbor("2001:db8:ffff::9"));
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&NlriNextHop::Ipv6(link_local)), far),
|
|
Err(Unresolved::NoRoute)
|
|
);
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&both("2001:db8::1")), peer),
|
|
Err(Unresolved::Ourselves)
|
|
);
|
|
}
|
|
|
|
/// A session on link-local addresses: which link is known from the
|
|
/// session and from nothing else, every interface having fe80::/64.
|
|
#[test]
|
|
fn a_link_local_peer_is_on_the_interface_of_its_session() {
|
|
let mut interfaces = interfaces();
|
|
for (index, local) in [(2, "fe80::a"), (3, "fe80::b")] {
|
|
interfaces.apply(&Message::Address {
|
|
present: true,
|
|
address: Address {
|
|
interface: index,
|
|
local: local.parse().unwrap(),
|
|
network: prefix("fe80::/64"),
|
|
},
|
|
});
|
|
}
|
|
let v4 = prefix("203.0.113.0/24");
|
|
let v6 = prefix("2001:db8:100::/48");
|
|
let link_local: Ipv6Addr = "fe80::2".parse().unwrap();
|
|
let on = |interface| {
|
|
Some(Neighbor {
|
|
address: link_local.into(),
|
|
interface,
|
|
})
|
|
};
|
|
// However the peer says it: the address alone, or twice, or after
|
|
// nothing where a global one would go.
|
|
for next_hop in [
|
|
NlriNextHop::Ipv6(link_local),
|
|
NlriNextHop::Ipv6WithLl {
|
|
global: link_local,
|
|
link_local,
|
|
},
|
|
NlriNextHop::Ipv6WithLl {
|
|
global: Ipv6Addr::UNSPECIFIED,
|
|
link_local,
|
|
},
|
|
] {
|
|
for interface in [2, 3] {
|
|
for route in [&v4, &v6] {
|
|
assert_eq!(
|
|
resolve(&interfaces, route, Some(&next_hop), on(interface)),
|
|
Ok(target("fe80::2", interface)),
|
|
"{next_hop:?} on {interface}"
|
|
);
|
|
}
|
|
}
|
|
// An interface that is not there, or is down.
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&next_hop), on(9)),
|
|
Err(Unresolved::NoRoute)
|
|
);
|
|
}
|
|
// Our own address on that link, which on another is a neighbor's.
|
|
let ours = NlriNextHop::Ipv6("fe80::a".parse().unwrap());
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&ours), on(2)),
|
|
Err(Unresolved::Ourselves)
|
|
);
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&ours), on(3)),
|
|
Ok(target("fe80::a", 3))
|
|
);
|
|
// A global next hop on such a session is looked up as any other.
|
|
let global = NlriNextHop::Ipv6("2001:db8::2".parse().unwrap());
|
|
assert_eq!(
|
|
resolve(&interfaces, &v6, Some(&global), on(3)),
|
|
Ok(target("2001:db8::2", 2))
|
|
);
|
|
}
|
|
|
|
/// A next hop that is no neighbor is reached the way the kernel's
|
|
/// own routes say, and the route goes into the kernel with that way's
|
|
/// gateway and interface.
|
|
#[test]
|
|
fn a_far_next_hop_resolves_by_the_kernels_routes() {
|
|
let mut topology = interfaces();
|
|
let v4 = prefix("203.0.113.0/24");
|
|
let v6 = prefix("2001:db8:100::/48");
|
|
let peer = Some(neighbor("192.0.2.2"));
|
|
let v4_hop = |s: &str| NlriNextHop::Ipv4(s.parse().unwrap());
|
|
let v6_hop = |s: &str| NlriNextHop::Ipv6(s.parse().unwrap());
|
|
let far = v4_hop("10.9.9.9");
|
|
|
|
// Nothing says the way.
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Err(Unresolved::NoRoute)
|
|
);
|
|
// The default route does not count unless asked for.
|
|
with_route(
|
|
&mut topology,
|
|
kernel_route("0.0.0.0/0", Some("192.0.2.254"), Some(2)),
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Err(Unresolved::OnlyDefault)
|
|
);
|
|
topology.via_default = true;
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Ok(target("192.0.2.254", 2))
|
|
);
|
|
topology.via_default = false;
|
|
// A static route with a gateway: the gateway is where packets go.
|
|
with_route(
|
|
&mut topology,
|
|
kernel_route("10.9.0.0/16", Some("198.51.100.9"), Some(3)),
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Ok(target("198.51.100.9", 3))
|
|
);
|
|
// A more specific route onto an interface, as a tunnel's is: the
|
|
// next hop itself is the neighbor there.
|
|
topology.apply(&Message::Link {
|
|
present: true,
|
|
link: Link {
|
|
index: 7,
|
|
name: "wg0".into(),
|
|
up: true,
|
|
},
|
|
});
|
|
with_route(&mut topology, kernel_route("10.9.9.0/24", None, Some(7)));
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Ok(target("10.9.9.9", 7))
|
|
);
|
|
// The tunnel goes down: the kernel keeps such routes, we do not
|
|
// use them.
|
|
topology.apply(&Message::Link {
|
|
present: true,
|
|
link: Link {
|
|
index: 7,
|
|
name: "wg0".into(),
|
|
up: false,
|
|
},
|
|
});
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Err(Unresolved::NoRoute)
|
|
);
|
|
// A route that names a nexthop object goes where the object says.
|
|
with_route(
|
|
&mut topology,
|
|
Route {
|
|
nexthop: Some(77),
|
|
..kernel_route("10.9.9.9/32", None, None)
|
|
},
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Err(Unresolved::NoRoute),
|
|
"an object the kernel has not told of"
|
|
);
|
|
topology.apply(&Message::Nexthop {
|
|
present: true,
|
|
nexthop: Nexthop {
|
|
id: 77,
|
|
protocol: 4,
|
|
gateway: Some("192.0.2.77".parse().unwrap()),
|
|
interface: Some(2),
|
|
},
|
|
});
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Ok(target("192.0.2.77", 2))
|
|
);
|
|
// A route that discards packets is a way to nowhere.
|
|
with_route(
|
|
&mut topology,
|
|
Route {
|
|
kind: 6, // blackhole
|
|
..kernel_route("10.9.9.9/32", None, None)
|
|
},
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&far), peer),
|
|
Err(Unresolved::Discarded)
|
|
);
|
|
// Routes of BGP's never say the way, be the prefix ever so long.
|
|
assert!(!topology.apply(&Message::Route {
|
|
present: true,
|
|
route: Route {
|
|
protocol: PROTOCOL_BGP,
|
|
metric: 20,
|
|
..kernel_route("10.9.9.9/32", Some("198.51.100.9"), Some(3))
|
|
},
|
|
}));
|
|
// An IPv6 next hop by an IPv4 gateway is no way for an IPv6 route,
|
|
// but is one for an IPv4 route (RFC 8950 the other way about is
|
|
// what the kernel allows).
|
|
with_route(
|
|
&mut topology,
|
|
kernel_route("2001:db8:9::/48", Some("198.51.100.9"), Some(3)),
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer),
|
|
Err(Unresolved::NoRoute)
|
|
);
|
|
with_route(
|
|
&mut topology,
|
|
kernel_route("2001:db8:9::/48", Some("2001:db8::9"), Some(2)),
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v6, Some(&v6_hop("2001:db8:9::9")), peer),
|
|
Ok(target("2001:db8::9", 2))
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&v6_hop("2001:db8:9::9")), peer),
|
|
Ok(target("2001:db8::9", 2))
|
|
);
|
|
// A neighbor is a neighbor, whatever the routes say.
|
|
with_route(
|
|
&mut topology,
|
|
kernel_route("192.0.2.2/32", Some("198.51.100.9"), Some(3)),
|
|
);
|
|
assert_eq!(
|
|
resolve(&topology, &v4, Some(&v4_hop("192.0.2.2")), peer),
|
|
Ok(target("192.0.2.2", 2))
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn the_kernel_is_asked_only_for_what_changes() {
|
|
let mut plan = Plan::default();
|
|
let p = prefix("203.0.113.0/24");
|
|
let (a, b) = (target("192.0.2.2", 2), target("192.0.2.3", 2));
|
|
|
|
assert_eq!(
|
|
plan.best(p, Ok(a)),
|
|
Some(Op::Install {
|
|
prefix: p,
|
|
target: a
|
|
})
|
|
);
|
|
// The best route changes to one with the same next hop: nothing
|
|
// for the kernel.
|
|
assert_eq!(plan.best(p, Ok(a)), None);
|
|
assert_eq!(
|
|
plan.best(p, Ok(b)),
|
|
Some(Op::Install {
|
|
prefix: p,
|
|
target: b
|
|
})
|
|
);
|
|
assert_eq!(plan.installed(), 1);
|
|
// A best route that cannot be installed takes the old one out.
|
|
assert_eq!(
|
|
plan.best(p, Err(Unresolved::NoRoute)),
|
|
Some(Op::Remove { prefix: p })
|
|
);
|
|
assert_eq!((plan.installed(), plan.unresolved()), (0, 1));
|
|
assert_eq!(
|
|
plan.unresolved_by_reason().collect::<Vec<_>>(),
|
|
[(Unresolved::NoRoute, 1)]
|
|
);
|
|
assert_eq!(plan.withdrawn(p), None);
|
|
assert_eq!(plan.unresolved_by_reason().count(), 0);
|
|
assert_eq!(plan.unresolved(), 0);
|
|
|
|
plan.best(p, Ok(a));
|
|
assert_eq!(plan.withdrawn(p), Some(Op::Remove { prefix: p }));
|
|
assert_eq!(plan.withdrawn(p), None);
|
|
}
|
|
|
|
#[test]
|
|
fn what_a_whole_table_does_not_confirm_is_removed() {
|
|
let mut plan = Plan::default();
|
|
let (kept, changed, stale, odd) = (
|
|
prefix("203.0.113.0/24"),
|
|
prefix("198.18.0.0/15"),
|
|
prefix("192.88.99.0/24"),
|
|
prefix("2001:db8:5::/48"),
|
|
);
|
|
let a = target("192.0.2.2", 2);
|
|
// Left in the kernel by an earlier run.
|
|
plan.adopt(kept, Some(a), false);
|
|
plan.adopt(changed, Some(a), false);
|
|
plan.adopt(stale, Some(a), false);
|
|
plan.adopt(odd, None, false);
|
|
assert_eq!(plan.installed(), 4);
|
|
|
|
plan.reset();
|
|
// The same as the kernel has: nothing to do.
|
|
assert_eq!(plan.best(kept, Ok(a)), None);
|
|
let b = target("192.0.2.3", 2);
|
|
assert_eq!(
|
|
plan.best(changed, Ok(b)),
|
|
Some(Op::Install {
|
|
prefix: changed,
|
|
target: b
|
|
})
|
|
);
|
|
let mut removed = plan.synced();
|
|
removed.sort_by_key(|op| *op.prefix());
|
|
let mut expected = vec![Op::Remove { prefix: stale }, Op::Remove { prefix: odd }];
|
|
expected.sort_by_key(|op| *op.prefix());
|
|
assert_eq!(removed, expected);
|
|
assert_eq!(plan.installed(), 2);
|
|
// A second completion finds nothing more.
|
|
assert_eq!(plan.synced(), []);
|
|
}
|
|
|
|
/// When the daemon starts its table is empty, and stays so until its
|
|
/// peers have sent theirs. What an earlier run left in the kernel is
|
|
/// kept meanwhile.
|
|
#[test]
|
|
fn routes_of_an_earlier_run_are_kept_until_released() {
|
|
let mut plan = Plan::default();
|
|
let (still_right, moved, gone) = (
|
|
prefix("203.0.113.0/24"),
|
|
prefix("198.18.0.0/15"),
|
|
prefix("192.88.99.0/24"),
|
|
);
|
|
let (a, b) = (target("192.0.2.2", 2), target("192.0.2.3", 2));
|
|
for p in [still_right, moved, gone] {
|
|
plan.adopt(p, Some(a), true);
|
|
}
|
|
// The first whole table is the empty one: nothing goes.
|
|
plan.reset();
|
|
assert_eq!(plan.synced(), []);
|
|
assert_eq!((plan.installed(), plan.kept()), (3, 3));
|
|
|
|
// The peers come back.
|
|
assert_eq!(plan.best(still_right, Ok(a)), None);
|
|
assert_eq!(
|
|
plan.best(moved, Ok(b)),
|
|
Some(Op::Install {
|
|
prefix: moved,
|
|
target: b
|
|
})
|
|
);
|
|
assert_eq!(plan.kept(), 1);
|
|
// Time is up: what nobody announced again goes, and only that.
|
|
assert_eq!(plan.release(), [Op::Remove { prefix: gone }]);
|
|
assert_eq!((plan.installed(), plan.kept()), (2, 0));
|
|
assert_eq!(plan.release(), []);
|
|
|
|
// From then on a whole table decides as always.
|
|
plan.reset();
|
|
plan.best(moved, Ok(b));
|
|
assert_eq!(
|
|
plan.synced(),
|
|
[Op::Remove {
|
|
prefix: still_right
|
|
}]
|
|
);
|
|
|
|
// A route that is withdrawn while kept goes at once.
|
|
let mut plan = Plan::default();
|
|
plan.adopt(gone, Some(a), true);
|
|
plan.best(gone, Ok(a));
|
|
assert_eq!(plan.withdrawn(gone), Some(Op::Remove { prefix: gone }));
|
|
}
|
|
|
|
#[test]
|
|
fn a_route_the_kernel_refuses_is_not_counted_as_there() {
|
|
let mut plan = Plan::default();
|
|
let p = prefix("203.0.113.0/24");
|
|
let (a, b) = (target("192.0.2.2", 2), target("192.0.2.3", 2));
|
|
let install = plan.best(p, Ok(a)).unwrap();
|
|
// What it refused to put there may have left an older one of ours.
|
|
assert_eq!(
|
|
plan.refused_by_kernel(&install),
|
|
Some(Op::Remove { prefix: p })
|
|
);
|
|
assert_eq!((plan.installed(), plan.refused()), (0, 1));
|
|
// Refusing to remove what is not there is as wanted.
|
|
assert_eq!(plan.refused_by_kernel(&Op::Remove { prefix: p }), None);
|
|
// The next best route is tried afresh.
|
|
assert!(plan.best(p, Ok(b)).is_some());
|
|
assert_eq!((plan.installed(), plan.refused()), (1, 0));
|
|
// News of a refusal of what has since been replaced is ignored.
|
|
assert_eq!(plan.refused_by_kernel(&install), None);
|
|
assert_eq!(plan.installed(), 1);
|
|
|
|
let mut cleared = plan.clear();
|
|
assert_eq!(cleared.pop(), Some(Op::Remove { prefix: p }));
|
|
assert_eq!(plan.installed(), 0);
|
|
}
|
|
|
|
/// What happens to a table of best routes.
|
|
#[derive(Debug, Clone)]
|
|
enum Step {
|
|
Best(u8, Option<u8>),
|
|
Withdrawn(u8),
|
|
/// A reader that starts over: the whole table again.
|
|
Resync,
|
|
}
|
|
|
|
fn any_step() -> impl Strategy<Value = Step> {
|
|
prop_oneof![
|
|
4 => (0u8..12, proptest::option::weighted(0.8, 0u8..3)).prop_map(|(p, t)| Step::Best(p, t)),
|
|
2 => (0u8..12).prop_map(Step::Withdrawn),
|
|
1 => Just(Step::Resync),
|
|
]
|
|
}
|
|
|
|
proptest! {
|
|
/// Whatever happens to the best routes, and whatever an earlier
|
|
/// run left behind, a kernel that does as it is asked ends up with
|
|
/// exactly the best routes that resolve.
|
|
#[test]
|
|
fn the_kernel_ends_up_with_the_best_routes_that_resolve(
|
|
left_behind in proptest::collection::vec((0u8..16, 0u8..3), 0..6),
|
|
steps in proptest::collection::vec(any_step(), 0..60),
|
|
) {
|
|
let p = |n: u8| IpPrefix::new(IpVersion::Ipv4, [10, n, 0, 0], 16).unwrap();
|
|
let t = |n: u8| target(&format!("192.0.2.{}", n + 2), 2);
|
|
let mut kernel: HashMap<IpPrefix, Target> = HashMap::default();
|
|
let mut plan = Plan::default();
|
|
for (prefix, gateway) in left_behind {
|
|
kernel.insert(p(prefix), t(gateway));
|
|
plan.adopt(p(prefix), Some(t(gateway)), false);
|
|
}
|
|
// The best routes: the prefix's next hop, if it resolves.
|
|
let mut table: HashMap<u8, Option<u8>> = HashMap::default();
|
|
let apply = |kernel: &mut HashMap<IpPrefix, Target>, op: Option<Op>| match op {
|
|
Some(Op::Install { prefix, target }) => { kernel.insert(prefix, target); }
|
|
Some(Op::Remove { prefix }) => { kernel.remove(&prefix); }
|
|
None => {}
|
|
};
|
|
let resolved = |gateway: Option<u8>| gateway.map(t).ok_or(Unresolved::NoRoute);
|
|
// A new reader is first given the whole table.
|
|
let steps = std::iter::once(Step::Resync).chain(steps);
|
|
for step in steps {
|
|
match step {
|
|
Step::Best(prefix, gateway) => {
|
|
table.insert(prefix, gateway);
|
|
apply(&mut kernel, plan.best(p(prefix), resolved(gateway)));
|
|
}
|
|
Step::Withdrawn(prefix) => {
|
|
table.remove(&prefix);
|
|
apply(&mut kernel, plan.withdrawn(p(prefix)));
|
|
}
|
|
Step::Resync => {
|
|
plan.reset();
|
|
for (prefix, gateway) in &table {
|
|
apply(&mut kernel, plan.best(p(*prefix), resolved(*gateway)));
|
|
}
|
|
for op in plan.synced() {
|
|
apply(&mut kernel, Some(op));
|
|
}
|
|
}
|
|
}
|
|
let wanted: HashMap<IpPrefix, Target> = table
|
|
.iter()
|
|
.filter_map(|(prefix, gateway)| Some((p(*prefix), t((*gateway)?))))
|
|
.collect();
|
|
prop_assert_eq!(&kernel, &wanted);
|
|
prop_assert_eq!(plan.installed(), wanted.len());
|
|
prop_assert_eq!(plan.unresolved(), table.len() - wanted.len());
|
|
prop_assert_eq!(
|
|
plan.unresolved_by_reason().map(|(_, count)| count).sum::<usize>(),
|
|
plan.unresolved()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|