Where routes go into the kernel, a route the kernel could not be given is no longer taken into the table: its next hop is no neighbor on any interface, or its prefix is a network the machine is itself on. Such a route used to be able to win selection and then leave its prefix without a route in the kernel, though another peer had one that worked. It is held aside the way an RPKI-invalid route is, and by the same means: judged on arrival against the interfaces as the kernel last described them, looked for in the table when they change, and taken from what is held when its next hop can be reached. The fib publishes the interfaces for this; the next hop is resolved once for an UPDATE and not for each of its routes. The lab has a phase, nexthops: FRR's better route with a next hop that is nowhere is held and BIRD's is installed, and they change places as an address comes and goes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
899 lines
32 KiB
Rust
899 lines
32 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::interfaces::Interfaces;
|
|
|
|
// Asked for every route: a quick hash matters here.
|
|
type HashMap<K, V> = std::collections::HashMap<K, V, foldhash::fast::RandomState>;
|
|
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.
|
|
/// Finding the way to such a next hop through other routes is not
|
|
/// done.
|
|
NotConnected,
|
|
/// 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; 5] = [
|
|
Self::Missing,
|
|
Self::OtherFamily,
|
|
Self::Ourselves,
|
|
Self::NotConnected,
|
|
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::NotConnected => "next hop not directly connected",
|
|
Self::OwnNetwork => "inside a network of this machine",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// 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`.
|
|
///
|
|
/// Only a next hop that is a neighbor on one of our interfaces is
|
|
/// resolved. 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(
|
|
interfaces: &Interfaces,
|
|
prefix: &IpPrefix,
|
|
next_hop: Option<&NlriNextHop>,
|
|
peer: Option<Neighbor>,
|
|
) -> Result<Target, Unresolved> {
|
|
if interfaces.has_network_around(prefix) {
|
|
return Err(Unresolved::OwnNetwork);
|
|
}
|
|
resolve_next_hop(interfaces, prefix.address_family(), next_hop, peer)
|
|
}
|
|
|
|
/// 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(
|
|
interfaces: &Interfaces,
|
|
routes: IpVersion,
|
|
next_hop: Option<&NlriNextHop>,
|
|
peer: Option<Neighbor>,
|
|
) -> Result<Target, Unresolved> {
|
|
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);
|
|
}
|
|
interfaces
|
|
.connected(gateway)
|
|
.map(|connected| Target {
|
|
gateway,
|
|
interface: connected.interface,
|
|
})
|
|
.ok_or(Unresolved::NotConnected)
|
|
};
|
|
// 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::NotConnected)? {
|
|
// 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::NotConnected)?;
|
|
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)
|
|
}
|
|
NlriNextHop::Ipv6WithLl { global, link_local } => match direct(global.into()) {
|
|
Err(Unresolved::NotConnected) => 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),
|
|
};
|
|
for (reason, by) in [(before, -1isize), (reason, 1)] {
|
|
if let Some(reason) = reason {
|
|
let count = &mut self.unresolved_counts[reason as usize];
|
|
*count = count.wrapping_add_signed(by);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// How many prefixes have a best route that the kernel refused.
|
|
pub fn refused(&self) -> usize {
|
|
self.refused.len()
|
|
}
|
|
|
|
/// The prefixes of the routes the kernel has.
|
|
pub fn installed_prefixes(&self) -> impl Iterator<Item = &IpPrefix> {
|
|
self.installed.keys()
|
|
}
|
|
|
|
/// 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.unresolved.clear();
|
|
self.unresolved_counts = [0; Unresolved::ALL.len()];
|
|
self.refused.clear();
|
|
}
|
|
|
|
/// 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.unresolved.clear();
|
|
self.unresolved_counts = [0; Unresolved::ALL.len()];
|
|
self.refused.clear();
|
|
self.installed
|
|
.drain()
|
|
.map(|(prefix, _)| Op::Remove { prefix })
|
|
.collect()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use proptest::prelude::*;
|
|
|
|
use super::*;
|
|
use crate::netlink::{Address, Link, Message};
|
|
|
|
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() -> Interfaces {
|
|
let mut interfaces = Interfaces::default();
|
|
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"),
|
|
] {
|
|
interfaces.apply(&Message::Link {
|
|
present: true,
|
|
link: Link {
|
|
index,
|
|
name: format!("eth{index}"),
|
|
up: true,
|
|
},
|
|
});
|
|
interfaces.apply(&Message::Address {
|
|
present: true,
|
|
address: Address {
|
|
interface: index,
|
|
local: local.parse().unwrap(),
|
|
network: prefix(network),
|
|
},
|
|
});
|
|
}
|
|
interfaces
|
|
}
|
|
|
|
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::NotConnected),
|
|
(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::NotConnected)
|
|
);
|
|
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::NotConnected)
|
|
);
|
|
}
|
|
// 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))
|
|
);
|
|
}
|
|
|
|
#[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::NotConnected)),
|
|
Some(Op::Remove { prefix: p })
|
|
);
|
|
assert_eq!((plan.installed(), plan.unresolved()), (0, 1));
|
|
assert_eq!(
|
|
plan.unresolved_by_reason().collect::<Vec<_>>(),
|
|
[(Unresolved::NotConnected, 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::NotConnected);
|
|
// 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()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|