A review of the wire codec and the routing table, with the fixes. Bugs in the codec: - A NOTIFICATION with a code or subcode we have no name for (subcode 0, "unspecific", is common) failed to decode and was answered as a bad message. Every pair now reads, as BgpError::Other. - Optional parameters in the extended form of RFC 9072 were read and written with one-octet parameter lengths; they are two. - An ORF capability listing more than one address family made the whole OPEN malformed. - Bytes after an OPEN's optional parameters were accepted. - Extended communities were split into fields by rules that were wrong for the common types. They are kept as their eight octets. - Several length fields were computed in u16 and could wrap on encode. Simplifications in the codec: - Lengths are filled in after the value is written, for messages, attributes, capabilities and the UPDATE's two fields. The parallel wire_len functions, which had to be kept in step by hand, are gone. Message::to_wire no longer encodes into a second buffer. - Type codes for messages, attributes and capabilities are each defined once. The strum discriminant enums and their hand-written conversions are removed. - One function splits an attribute off a buffer; the UPDATE decoder, the plain attribute parser and the table's key reader all use it. - The UPDATE decoder's 185-line loop is an AttributeReader with one method that decides what becomes of an attribute. Duplicate detection is a bitset, not a HashSet allocated per UPDATE. - ParserContext::four_octet_asn is a bool. The "context not set" error path, which treated a programming error as a malformed route, is gone. - Codec functions take a context only if they read it, and return a Result only if they can fail. - MP_REACH_NLRI and MP_UNREACH_NLRI hold an AfiSafi. - AddressFamilyId is IpVersion, which is what it was. - Deserialize is no longer derived on codec types: nothing used it, and it bypassed IpPrefix's invariants. IpPrefix::to_wire, which was broken and unused, is removed. Bugs in the table: - The index of a row's best route was sixteen bits. A peer using ADD-PATH could send more routes for a prefix than that counts, and the index wrapped. It is 32 bits, at no cost in row size. - Shards and attribute-store stripes were picked from the top bits of the hash, which are the bits hashbrown keeps as the tag it compares first. With 64 shards each shard's table had 2 of 128 tag values. They are now picked from bits the table does not use. - remove_peer held the peer list's lock for the whole scan of the table, so no session could establish while a peer was removed. - When more prefixes went at once than were left, the forced trim dropped a fixed few entries, reset every reader, and re-ran the tidy on every later write. It drops the older half. - Readers were woken by every change, including a repeated announcement and changes their journal does not record. - Reader::poll with a budget of zero never made progress. Simplifications in the table: - The two journals of a shard are one Journals value written through three calls; the five places that kept them in step by hand are gone. Whether an entry is still live is decided in one place. - The shortcuts of route selection moved to select.rs, next to the invariant they depend on, which is now written down. - announce and withdraw take routes with or without path identifiers; the _paths variants are removed. - The selection key is read off the wire form only; the second implementation over parsed attributes is removed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
760 lines
26 KiB
Rust
760 lines
26 KiB
Rust
use std::collections::{BTreeMap, HashMap};
|
|
use std::net::{IpAddr, Ipv4Addr};
|
|
use std::sync::Arc;
|
|
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, LocalPrefPathAttribute, MultiExitDiscPathAttribute, NextHopPathAttribute,
|
|
OriginPathAttribute, PathAttribute,
|
|
};
|
|
use bgp_primitives::ip_prefix::{IpPrefix, Nlri};
|
|
use proptest::prelude::*;
|
|
|
|
use super::*;
|
|
|
|
pub(super) fn prefix(s: &str) -> IpPrefix {
|
|
IpPrefix::try_from(s).unwrap()
|
|
}
|
|
|
|
/// What a test says about a route; everything selection looks at.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(super) struct Route {
|
|
pub(super) local_pref: Option<u32>,
|
|
pub(super) path: Vec<u32>,
|
|
pub(super) origin: u8,
|
|
pub(super) med: Option<u32>,
|
|
}
|
|
|
|
impl Route {
|
|
pub(super) fn via(path: &[u32]) -> Self {
|
|
Self {
|
|
local_pref: None,
|
|
path: path.to_vec(),
|
|
origin: 0,
|
|
med: None,
|
|
}
|
|
}
|
|
|
|
pub(super) fn attributes(&self) -> Vec<PathAttribute> {
|
|
let mut attributes = vec![
|
|
PathAttribute::Origin(match self.origin {
|
|
0 => OriginPathAttribute::IGP,
|
|
1 => OriginPathAttribute::EGP,
|
|
_ => OriginPathAttribute::INCOMPLETE,
|
|
}),
|
|
AsPathAttribute::from_asns(self.path.clone()),
|
|
PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))),
|
|
];
|
|
attributes.extend(
|
|
self.med
|
|
.map(|med| PathAttribute::MultiExitDisc(MultiExitDiscPathAttribute(med))),
|
|
);
|
|
attributes.extend(
|
|
self.local_pref
|
|
.map(|lp| PathAttribute::LocalPref(LocalPrefPathAttribute(lp))),
|
|
);
|
|
attributes
|
|
}
|
|
}
|
|
|
|
pub(super) fn external(number: u8) -> PeerInfo {
|
|
PeerInfo {
|
|
asn: 65000 + u32::from(number),
|
|
router_id: Ipv4Addr::new(10, 0, 0, number),
|
|
address: IpAddr::from(Ipv4Addr::new(192, 0, 2, number)),
|
|
ibgp: false,
|
|
}
|
|
}
|
|
|
|
pub(super) fn internal(number: u8) -> PeerInfo {
|
|
PeerInfo {
|
|
asn: 64999,
|
|
ibgp: true,
|
|
..external(number)
|
|
}
|
|
}
|
|
|
|
/// A table and the peers added to it, for tests that speak of peers by the
|
|
/// order they were added in.
|
|
pub(super) struct Fixture {
|
|
pub(super) table: Arc<RibTable>,
|
|
pub(super) peers: Vec<PeerIndex>,
|
|
}
|
|
|
|
impl Fixture {
|
|
pub(super) fn new(peers: &[PeerInfo]) -> Self {
|
|
// More than one shard, so that a test's prefixes are spread.
|
|
Self::on(RibTable::new(4), peers)
|
|
}
|
|
|
|
pub(super) fn on(table: RibTable, peers: &[PeerInfo]) -> Self {
|
|
let table = Arc::new(table);
|
|
let peers = peers.iter().map(|p| table.add_peer(*p).unwrap()).collect();
|
|
Self { table, peers }
|
|
}
|
|
|
|
pub(super) fn announce(&self, peer: usize, prefix: &IpPrefix, route: &Route) -> Outcome {
|
|
let attrs = self
|
|
.table
|
|
.attrs()
|
|
.intern(&route.attributes(), None)
|
|
.unwrap();
|
|
self.table.announce(self.peers[peer], &attrs, &[*prefix])
|
|
}
|
|
|
|
pub(super) fn withdraw(&self, peer: usize, prefix: &IpPrefix) -> Outcome {
|
|
self.table.withdraw(self.peers[peer], &[*prefix])
|
|
}
|
|
|
|
/// As `announce`, from a peer that sends several routes for a prefix
|
|
/// and calls this one `path_id`.
|
|
pub(super) fn announce_path(
|
|
&self,
|
|
peer: usize,
|
|
prefix: &IpPrefix,
|
|
path_id: u32,
|
|
route: &Route,
|
|
) -> Outcome {
|
|
let attrs = self
|
|
.table
|
|
.attrs()
|
|
.intern(&route.attributes(), None)
|
|
.unwrap();
|
|
let nlri = Nlri {
|
|
prefix: *prefix,
|
|
path_id,
|
|
};
|
|
self.table.announce(self.peers[peer], &attrs, &[nlri])
|
|
}
|
|
|
|
pub(super) fn withdraw_path(&self, peer: usize, prefix: &IpPrefix, path_id: u32) -> Outcome {
|
|
let nlri = Nlri {
|
|
prefix: *prefix,
|
|
path_id,
|
|
};
|
|
self.table.withdraw(self.peers[peer], &[nlri])
|
|
}
|
|
|
|
/// The best route for `prefix`: which peer (by order added) it is from
|
|
/// and that peer's identifier for it.
|
|
pub(super) fn best_path(&self, prefix: &IpPrefix) -> Option<(usize, u32)> {
|
|
let best = self.table.routes(prefix).into_iter().find(|r| r.best)?;
|
|
let peer = self.peers.iter().position(|p| *p == best.peer)?;
|
|
Some((peer, best.path_id))
|
|
}
|
|
|
|
/// The routes held for `prefix`, as (peer, path identifier), in order.
|
|
pub(super) fn held(&self, prefix: &IpPrefix) -> Vec<(usize, u32)> {
|
|
let mut held: Vec<(usize, u32)> = self
|
|
.table
|
|
.routes(prefix)
|
|
.iter()
|
|
.map(|r| {
|
|
let peer = self.peers.iter().position(|p| *p == r.peer).unwrap();
|
|
(peer, r.path_id)
|
|
})
|
|
.collect();
|
|
held.sort_unstable();
|
|
held
|
|
}
|
|
|
|
/// Which peer (by order added) the best route for `prefix` is from.
|
|
fn best(&self, prefix: &IpPrefix) -> Option<usize> {
|
|
let (peer, _) = self.table.best(prefix)?;
|
|
self.peers.iter().position(|p| *p == peer)
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn each_step_of_selection_decides_when_the_ones_before_tie() {
|
|
let p = prefix("10.0.0.0/8");
|
|
// Peer 0 has the lower identifier, so it wins whenever nothing else
|
|
// decides; each case makes peer 1's route better in one step.
|
|
let cases: [(&str, [PeerInfo; 2], Route, Route); 6] = [
|
|
(
|
|
"higher LOCAL_PREF, despite a longer path",
|
|
[internal(1), internal(2)],
|
|
Route {
|
|
local_pref: Some(100),
|
|
..Route::via(&[1])
|
|
},
|
|
Route {
|
|
local_pref: Some(200),
|
|
..Route::via(&[1, 2, 3])
|
|
},
|
|
),
|
|
(
|
|
"shorter AS path",
|
|
[external(1), external(2)],
|
|
Route::via(&[1, 2, 3]),
|
|
Route::via(&[4, 5]),
|
|
),
|
|
(
|
|
"lower ORIGIN",
|
|
[external(1), external(2)],
|
|
Route {
|
|
origin: 2,
|
|
..Route::via(&[1, 2])
|
|
},
|
|
Route {
|
|
origin: 0,
|
|
..Route::via(&[3, 4])
|
|
},
|
|
),
|
|
(
|
|
"lower MED from the same neighbouring AS",
|
|
[external(1), external(2)],
|
|
Route {
|
|
med: Some(20),
|
|
..Route::via(&[7, 2])
|
|
},
|
|
Route {
|
|
med: Some(10),
|
|
..Route::via(&[7, 3])
|
|
},
|
|
),
|
|
(
|
|
"no MED counts as the lowest",
|
|
[external(1), external(2)],
|
|
Route {
|
|
med: Some(1),
|
|
..Route::via(&[7, 2])
|
|
},
|
|
Route::via(&[7, 3]),
|
|
),
|
|
(
|
|
"external over internal",
|
|
[internal(1), external(2)],
|
|
Route::via(&[7, 2]),
|
|
Route::via(&[8, 3]),
|
|
),
|
|
];
|
|
for (case, peers, first, second) in cases {
|
|
// In both orders of arrival: the choice must not depend on it.
|
|
for order in [[0, 1], [1, 0]] {
|
|
let fixture = Fixture::new(&peers);
|
|
for peer in order {
|
|
fixture.announce(peer, &p, [&first, &second][peer]);
|
|
}
|
|
assert_eq!(fixture.best(&p), Some(1), "{case}, arriving {order:?}");
|
|
}
|
|
}
|
|
|
|
// And when nothing decides: the lowest identifier.
|
|
let fixture = Fixture::new(&[external(2), external(1)]);
|
|
fixture.announce(0, &p, &Route::via(&[1, 2]));
|
|
fixture.announce(1, &p, &Route::via(&[3, 4]));
|
|
assert_eq!(fixture.best(&p), Some(1));
|
|
}
|
|
|
|
/// MEDs are compared only within a neighbouring AS. Taken pair by pair
|
|
/// that is not an order: A beats B, B beats C, C beats A. Whatever the
|
|
/// order of arrival, the answer must be the same.
|
|
#[test]
|
|
fn med_is_compared_only_within_a_neighbouring_as_and_never_goes_in_circles() {
|
|
let p = prefix("10.0.0.0/8");
|
|
let routes = [
|
|
// From AS 1, MED 10, and the highest identifier.
|
|
Route {
|
|
med: Some(10),
|
|
..Route::via(&[1, 9])
|
|
},
|
|
// From AS 2: its MED is not comparable with the others'.
|
|
Route {
|
|
med: Some(50),
|
|
..Route::via(&[2, 9])
|
|
},
|
|
// From AS 1, MED 20: loses to the first on MED.
|
|
Route {
|
|
med: Some(20),
|
|
..Route::via(&[1, 8])
|
|
},
|
|
];
|
|
// Identifiers: the third route's peer lowest, then the second's, then
|
|
// the first's. Without MED the third would win; it is knocked out by
|
|
// the first, and of the two left the second has the lower identifier.
|
|
let peers = [external(3), external(2), external(1)];
|
|
for order in [
|
|
[0, 1, 2],
|
|
[0, 2, 1],
|
|
[1, 0, 2],
|
|
[1, 2, 0],
|
|
[2, 0, 1],
|
|
[2, 1, 0],
|
|
] {
|
|
let fixture = Fixture::new(&peers);
|
|
for peer in order {
|
|
fixture.announce(peer, &p, &routes[peer]);
|
|
}
|
|
assert_eq!(fixture.best(&p), Some(1), "arriving {order:?}");
|
|
}
|
|
|
|
// Take away the route that knocked the third out, and the third wins.
|
|
let fixture = Fixture::new(&peers);
|
|
for (peer, route) in routes.iter().enumerate() {
|
|
fixture.announce(peer, &p, route);
|
|
}
|
|
let outcome = fixture.withdraw(0, &p);
|
|
assert_eq!(outcome.best_changed, 1);
|
|
assert_eq!(fixture.best(&p), Some(2));
|
|
}
|
|
|
|
/// A route that is not the best can still decide which is: by its MED it
|
|
/// keeps another route of its neighbouring AS out. When it gets worse or
|
|
/// goes, that other route is back in the running. The shortcuts must not
|
|
/// skip this.
|
|
#[test]
|
|
fn a_route_that_was_keeping_another_out_matters_when_it_changes() {
|
|
let p = prefix("10.0.0.0/8");
|
|
// Peer 0: from AS 1 with the lowest identifier, but MED 1.
|
|
// Peer 1: from AS 2. Best while peer 0's route is kept out.
|
|
// Peer 2: from AS 1 with MED 0, which keeps peer 0's route out.
|
|
let peers = [external(1), external(2), external(3)];
|
|
let held_out = Route {
|
|
med: Some(1),
|
|
..Route::via(&[1, 9])
|
|
};
|
|
let best = Route::via(&[2, 9]);
|
|
let holding_out = Route {
|
|
med: Some(0),
|
|
..Route::via(&[1, 8])
|
|
};
|
|
let set_up = || {
|
|
let fixture = Fixture::new(&peers);
|
|
fixture.announce(0, &p, &held_out);
|
|
fixture.announce(1, &p, &best);
|
|
fixture.announce(2, &p, &holding_out);
|
|
assert_eq!(fixture.best(&p), Some(1));
|
|
fixture
|
|
};
|
|
|
|
// It is replaced by a route with a longer path.
|
|
let fixture = set_up();
|
|
let outcome = fixture.announce(2, &p, &Route::via(&[1, 8, 7]));
|
|
assert_eq!(fixture.best(&p), Some(0));
|
|
assert_eq!(outcome.best_changed, 1);
|
|
|
|
// It is withdrawn.
|
|
let fixture = set_up();
|
|
let outcome = fixture.withdraw(2, &p);
|
|
assert_eq!(fixture.best(&p), Some(0));
|
|
assert_eq!(outcome.best_changed, 1);
|
|
|
|
// Its peer goes away.
|
|
let fixture = set_up();
|
|
let outcome = fixture.table.remove_peer(fixture.peers[2]);
|
|
assert_eq!(fixture.best(&p), Some(0));
|
|
assert_eq!(outcome.best_changed, 1);
|
|
|
|
// Its MED gets worse than the one it was keeping out.
|
|
let fixture = set_up();
|
|
fixture.announce(
|
|
2,
|
|
&p,
|
|
&Route {
|
|
med: Some(5),
|
|
..Route::via(&[1, 8])
|
|
},
|
|
);
|
|
assert_eq!(fixture.best(&p), Some(0));
|
|
}
|
|
|
|
#[test]
|
|
fn what_a_change_did_is_reported() {
|
|
let fixture = Fixture::new(&[external(1), external(2)]);
|
|
let p = prefix("10.0.0.0/8");
|
|
let long = Route::via(&[1, 2, 3]);
|
|
let short = Route::via(&[4]);
|
|
|
|
let first = fixture.announce(0, &p, &long);
|
|
assert_eq!((first.added, first.best_changed), (1, 1));
|
|
let worse = fixture.announce(1, &p, &Route::via(&[1, 2, 3, 4]));
|
|
assert_eq!((worse.added, worse.best_changed), (1, 0));
|
|
// The same route again changes nothing.
|
|
let again = fixture.announce(0, &p, &long);
|
|
assert_eq!((again.replaced, again.best_changed), (1, 0));
|
|
// The best route replaced by a better one is still a change of best.
|
|
let better = fixture.announce(0, &p, &short);
|
|
assert_eq!((better.replaced, better.best_changed), (1, 1));
|
|
assert_eq!((fixture.table.len(), fixture.table.prefix_count()), (2, 1));
|
|
|
|
assert_eq!(fixture.withdraw(1, &p).best_changed, 0);
|
|
let last = fixture.withdraw(0, &p);
|
|
assert_eq!((last.removed, last.best_changed), (1, 1));
|
|
assert_eq!(fixture.withdraw(0, &p), Outcome::default(), "nothing there");
|
|
assert!(fixture.table.is_empty());
|
|
assert_eq!(fixture.best(&p), None);
|
|
}
|
|
|
|
#[test]
|
|
fn removing_a_peer_takes_its_routes_and_frees_its_number() {
|
|
let fixture = Fixture::new(&[external(1), external(2)]);
|
|
let prefixes: Vec<IpPrefix> = (0..200u32)
|
|
.map(|i| {
|
|
IpPrefix::new(
|
|
bgp_primitives::constants::IpVersion::Ipv4,
|
|
(i << 8).to_be_bytes(),
|
|
24,
|
|
)
|
|
.unwrap()
|
|
})
|
|
.collect();
|
|
let attrs = |route: &Route| {
|
|
fixture
|
|
.table
|
|
.attrs()
|
|
.intern(&route.attributes(), None)
|
|
.unwrap()
|
|
};
|
|
// Peer 0 is best everywhere; peer 1 has only the first hundred.
|
|
fixture
|
|
.table
|
|
.announce(fixture.peers[0], &attrs(&Route::via(&[1])), &prefixes);
|
|
fixture.table.announce(
|
|
fixture.peers[1],
|
|
&attrs(&Route::via(&[2, 3])),
|
|
&prefixes[..100],
|
|
);
|
|
assert_eq!(fixture.table.len(), 300);
|
|
|
|
let outcome = fixture.table.remove_peer(fixture.peers[0]);
|
|
assert_eq!(outcome.removed, 200);
|
|
assert_eq!(outcome.best_changed, 200);
|
|
assert_eq!(
|
|
(fixture.table.len(), fixture.table.prefix_count()),
|
|
(100, 100)
|
|
);
|
|
assert_eq!(fixture.best(&prefixes[0]), Some(1));
|
|
assert_eq!(fixture.best(&prefixes[150]), None);
|
|
|
|
// Its routes are gone, so its attribute set can be dropped.
|
|
assert_eq!(fixture.table.attrs().sweep(), 1);
|
|
// And its number is given to the next peer.
|
|
assert_eq!(fixture.table.add_peer(external(3)), Ok(fixture.peers[0]));
|
|
}
|
|
|
|
#[test]
|
|
fn readers_see_every_route_and_which_is_best() {
|
|
let fixture = Fixture::new(&[external(1), external(2)]);
|
|
let (a, b) = (prefix("10.0.0.0/8"), prefix("2001:db8::/32"));
|
|
fixture.announce(0, &a, &Route::via(&[1, 2]));
|
|
fixture.announce(1, &a, &Route::via(&[3]));
|
|
fixture.announce(0, &b, &Route::via(&[1, 2]));
|
|
|
|
let mut best = Vec::new();
|
|
fixture
|
|
.table
|
|
.for_each_best(|prefix, peer, _| best.push((*prefix, peer)));
|
|
best.sort_by_key(|(prefix, _)| prefix.to_string());
|
|
assert_eq!(best, [(a, fixture.peers[1]), (b, fixture.peers[0])]);
|
|
|
|
let mut all = 0;
|
|
let mut marked_best = 0;
|
|
fixture.table.for_each_route(|route| {
|
|
all += 1;
|
|
marked_best += usize::from(route.best);
|
|
});
|
|
assert_eq!((all, marked_best), (3, 2));
|
|
|
|
let routes = fixture.table.routes(&a);
|
|
assert_eq!(routes.len(), 2);
|
|
assert!(
|
|
routes
|
|
.iter()
|
|
.any(|route| route.peer == fixture.peers[1] && route.best)
|
|
);
|
|
// Routes with the same attributes share one set.
|
|
let shared = &fixture.table.routes(&b)[0].attrs;
|
|
assert!(routes.iter().any(|route| Arc::ptr_eq(&route.attrs, shared)));
|
|
}
|
|
|
|
/// With ADD-PATH a peer sends several routes for one prefix and tells them
|
|
/// apart by a path identifier. Each is a route of its own: kept, replaced
|
|
/// and withdrawn by prefix and identifier together.
|
|
#[test]
|
|
fn a_peer_can_have_several_routes_for_a_prefix() {
|
|
let fixture = Fixture::new(&[external(1), external(2)]);
|
|
let p = prefix("10.0.0.0/8");
|
|
|
|
fixture.announce_path(0, &p, 1, &Route::via(&[1, 2, 3]));
|
|
let second = fixture.announce_path(0, &p, 2, &Route::via(&[1, 4]));
|
|
assert_eq!(
|
|
(second.added, second.replaced, second.best_changed),
|
|
(1, 0, 1)
|
|
);
|
|
fixture.announce(1, &p, &Route::via(&[2, 5, 6, 7]));
|
|
assert_eq!(fixture.held(&p), [(0, 1), (0, 2), (1, 0)]);
|
|
assert_eq!(fixture.best_path(&p), Some((0, 2)), "the shorter path");
|
|
assert_eq!((fixture.table.len(), fixture.table.prefix_count()), (3, 1));
|
|
|
|
// Announcing a path again replaces that path and no other.
|
|
let replaced = fixture.announce_path(0, &p, 1, &Route::via(&[1]));
|
|
assert_eq!((replaced.added, replaced.replaced), (0, 1));
|
|
assert_eq!(fixture.held(&p), [(0, 1), (0, 2), (1, 0)]);
|
|
assert_eq!(fixture.best_path(&p), Some((0, 1)));
|
|
|
|
// Withdrawing one leaves the peer's other.
|
|
assert_eq!(fixture.withdraw_path(0, &p, 1).removed, 1);
|
|
assert_eq!(fixture.held(&p), [(0, 2), (1, 0)]);
|
|
assert_eq!(fixture.best_path(&p), Some((0, 2)));
|
|
// A path that is not there is not withdrawn: not the same prefix
|
|
// under another identifier, and not the peer's plain route.
|
|
assert_eq!(fixture.withdraw_path(0, &p, 9), Outcome::default());
|
|
assert_eq!(fixture.withdraw(0, &p), Outcome::default());
|
|
|
|
// When the session ends, all of the peer's routes go.
|
|
fixture.announce_path(0, &p, 3, &Route::via(&[1, 8, 9]));
|
|
let outcome = fixture.table.remove_peer(fixture.peers[0]);
|
|
assert_eq!((outcome.removed, outcome.best_changed), (2, 1));
|
|
assert_eq!(fixture.held(&p), [(1, 0)]);
|
|
}
|
|
|
|
/// Two routes from one peer that nothing else tells apart: the lower path
|
|
/// identifier is chosen, whichever arrived first.
|
|
#[test]
|
|
fn equal_routes_from_one_peer_are_chosen_by_path_identifier() {
|
|
let p = prefix("10.0.0.0/8");
|
|
for order in [[5, 3], [3, 5]] {
|
|
let fixture = Fixture::new(&[external(1)]);
|
|
for path_id in order {
|
|
fixture.announce_path(0, &p, path_id, &Route::via(&[1, path_id]));
|
|
}
|
|
assert_eq!(fixture.best_path(&p), Some((0, 3)), "arriving {order:?}");
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// The table against a model
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Route selection written the plain way: all routes of a prefix, step by
|
|
/// step, with no shortcuts. The table takes shortcuts and must agree.
|
|
/// Routes are known by peer and path identifier.
|
|
fn chosen_plainly(
|
|
routes: &BTreeMap<(usize, u32), Route>,
|
|
peers: &[PeerInfo],
|
|
) -> Option<(usize, u32)> {
|
|
let local_pref = |peer: usize, route: &Route| {
|
|
if peers[peer].ibgp {
|
|
route.local_pref.unwrap_or(DEFAULT_LOCAL_PREF)
|
|
} else {
|
|
DEFAULT_LOCAL_PREF
|
|
}
|
|
};
|
|
let mut left: Vec<((usize, u32), &Route)> =
|
|
routes.iter().map(|(k, route)| (*k, route)).collect();
|
|
let most = left.iter().map(|(k, r)| local_pref(k.0, r)).max()?;
|
|
left.retain(|(k, r)| local_pref(k.0, r) == most);
|
|
let shortest = left.iter().map(|(_, r)| r.path.len()).min()?;
|
|
left.retain(|(_, r)| r.path.len() == shortest);
|
|
let lowest = left.iter().map(|(_, r)| r.origin).min()?;
|
|
left.retain(|(_, r)| r.origin == lowest);
|
|
|
|
let neighbor = |route: &Route| route.path.first().copied().unwrap_or(0);
|
|
let med = |route: &Route| route.med.unwrap_or(0);
|
|
let mut lowest_med: HashMap<u32, u32> = HashMap::new();
|
|
for (_, route) in &left {
|
|
let entry = lowest_med.entry(neighbor(route)).or_insert(u32::MAX);
|
|
*entry = (*entry).min(med(route));
|
|
}
|
|
left.retain(|(_, r)| med(r) == lowest_med[&neighbor(r)]);
|
|
|
|
left.into_iter()
|
|
.map(|(key, _)| key)
|
|
.min_by_key(|&(peer, path_id)| {
|
|
(
|
|
peers[peer].ibgp,
|
|
peers[peer].router_id,
|
|
peers[peer].address,
|
|
peer,
|
|
path_id,
|
|
)
|
|
})
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
enum Op {
|
|
Announce {
|
|
peer: usize,
|
|
prefix: usize,
|
|
path_id: u32,
|
|
route: Route,
|
|
},
|
|
Withdraw {
|
|
peer: usize,
|
|
prefix: usize,
|
|
path_id: u32,
|
|
},
|
|
/// The session goes down and comes straight back.
|
|
Flap { peer: usize },
|
|
}
|
|
|
|
const PEERS: usize = 6;
|
|
// Few prefixes, so that each gathers routes from most peers.
|
|
const PREFIXES: usize = 2;
|
|
// Most routes have the identifier zero, as without ADD-PATH; some peers
|
|
// send a second and third route.
|
|
const PATH_IDS: u32 = 3;
|
|
|
|
pub(super) fn route() -> impl Strategy<Value = Route> {
|
|
// Few values of everything, so that ties at every step are common:
|
|
// ties are where the shortcuts could be wrong.
|
|
(
|
|
proptest::option::of(100u32..102),
|
|
proptest::collection::vec(1u32..3, 1..3),
|
|
0u8..2,
|
|
proptest::option::of(0u32..3),
|
|
)
|
|
.prop_map(|(local_pref, path, origin, med)| Route {
|
|
local_pref,
|
|
path,
|
|
origin,
|
|
med,
|
|
})
|
|
}
|
|
|
|
fn path_id() -> impl Strategy<Value = u32> {
|
|
prop_oneof![3 => Just(0), 2 => 1..PATH_IDS]
|
|
}
|
|
|
|
fn op() -> impl Strategy<Value = Op> {
|
|
prop_oneof![
|
|
6 => (0..PEERS, 0..PREFIXES, path_id(), route()).prop_map(
|
|
|(peer, prefix, path_id, route)| Op::Announce { peer, prefix, path_id, route }
|
|
),
|
|
3 => (0..PEERS, 0..PREFIXES, path_id())
|
|
.prop_map(|(peer, prefix, path_id)| Op::Withdraw { peer, prefix, path_id }),
|
|
1 => (0..PEERS).prop_map(|peer| Op::Flap { peer }),
|
|
]
|
|
}
|
|
|
|
proptest! {
|
|
#![proptest_config(ProptestConfig::with_cases(2000))]
|
|
|
|
/// After any sequence of changes the table holds exactly the routes it
|
|
/// should, has chosen for every prefix what plain selection chooses,
|
|
/// and has reported a change of best route exactly when there was one.
|
|
#[test]
|
|
fn the_table_agrees_with_plain_selection(ops in proptest::collection::vec(op(), 1..80)) {
|
|
// Internal and external peers, and identifiers that are not in the
|
|
// order the peers are added in.
|
|
let infos: Vec<PeerInfo> = (0..PEERS)
|
|
.map(|i| {
|
|
let number = (PEERS - i) as u8;
|
|
if i % 3 == 0 { internal(number) } else { external(number) }
|
|
})
|
|
.collect();
|
|
let mut fixture = Fixture::new(&infos);
|
|
let prefixes: Vec<IpPrefix> = (0..PREFIXES)
|
|
.map(|i| prefix(&format!("10.{i}.0.0/16")))
|
|
.collect();
|
|
let mut model: Vec<BTreeMap<(usize, u32), Route>> = vec![BTreeMap::new(); PREFIXES];
|
|
|
|
for op in ops {
|
|
let before: Vec<Option<((usize, u32), Route)>> = model
|
|
.iter()
|
|
.map(|routes| chosen_plainly(routes, &infos).map(|key| (key, routes[&key].clone())))
|
|
.collect();
|
|
let outcome = match &op {
|
|
Op::Announce { peer, prefix, path_id, route } => {
|
|
model[*prefix].insert((*peer, *path_id), route.clone());
|
|
fixture.announce_path(*peer, &prefixes[*prefix], *path_id, route)
|
|
}
|
|
Op::Withdraw { peer, prefix, path_id } => {
|
|
model[*prefix].remove(&(*peer, *path_id));
|
|
fixture.withdraw_path(*peer, &prefixes[*prefix], *path_id)
|
|
}
|
|
Op::Flap { peer } => {
|
|
for routes in &mut model {
|
|
routes.retain(|(from, _), _| from != peer);
|
|
}
|
|
let outcome = fixture.table.remove_peer(fixture.peers[*peer]);
|
|
fixture.peers[*peer] = fixture.table.add_peer(infos[*peer]).unwrap();
|
|
outcome
|
|
}
|
|
};
|
|
|
|
let mut best_changes = 0;
|
|
for (i, routes) in model.iter().enumerate() {
|
|
let expected = chosen_plainly(routes, &infos);
|
|
prop_assert_eq!(fixture.best_path(&prefixes[i]), expected, "after {:?}", &op);
|
|
let now = expected.map(|key| (key, routes[&key].clone()));
|
|
best_changes += usize::from(now != before[i]);
|
|
prop_assert_eq!(
|
|
fixture.held(&prefixes[i]),
|
|
routes.keys().copied().collect::<Vec<_>>()
|
|
);
|
|
}
|
|
prop_assert_eq!(outcome.best_changed, best_changes, "after {:?}", &op);
|
|
prop_assert_eq!(
|
|
fixture.table.len(),
|
|
model.iter().map(BTreeMap::len).sum::<usize>()
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Shards and lock stripes are picked by bits of the hash that the hash
|
|
/// table inside does not use, so that keys which share a shard still
|
|
/// differ where that table looks.
|
|
#[test]
|
|
fn keys_of_one_shard_differ_in_the_bits_their_table_uses() {
|
|
let hasher = RandomState::default();
|
|
let shards = 64;
|
|
let mut counts = vec![0usize; shards];
|
|
let mut tags = vec![std::collections::HashSet::new(); shards];
|
|
for n in 0..64_000u32 {
|
|
let hash = hasher.hash_one(n);
|
|
let shard = spread(hash, shards);
|
|
counts[shard] += 1;
|
|
// The seven bits hashbrown keeps as a tag.
|
|
tags[shard].insert(hash >> 57);
|
|
}
|
|
for shard in 0..shards {
|
|
assert!(
|
|
(700..1300).contains(&counts[shard]),
|
|
"shard {shard} has {} of 64000 keys",
|
|
counts[shard]
|
|
);
|
|
assert_eq!(tags[shard].len(), 128, "shard {shard}");
|
|
}
|
|
assert_eq!(spread(u64::MAX, 3), 2);
|
|
assert_eq!(spread(0, 3), 0);
|
|
}
|
|
|
|
/// A peer that sends several routes for a prefix can send more than fit a
|
|
/// sixteen-bit index. The best route was kept as one, and wrapped.
|
|
#[test]
|
|
fn a_row_can_hold_more_routes_than_a_sixteen_bit_index_counts() {
|
|
let store = AttrStore::new();
|
|
let worse = store
|
|
.intern(&Route::via(&[1, 2]).attributes(), None)
|
|
.unwrap();
|
|
let better = store.intern(&Route::via(&[1]).attributes(), None).unwrap();
|
|
let info = external(1);
|
|
let peers = [Some(info)];
|
|
let peer = PeerIndex(0);
|
|
let path =
|
|
|attrs: &Arc<AttrSet>| Path::new(select::preference(attrs.key(), &info), peer, attrs);
|
|
let seqs = journal::Journals::new(journal::Limits::default()).appeared(prefix("10.0.0.0/8"));
|
|
|
|
let mut row = Row::new(prefix("10.0.0.0/8"), path(&worse), 1, seqs);
|
|
let mut scratch = select::Scratch::default();
|
|
let count = usize::from(u16::MAX) + 10;
|
|
for path_id in 2..=count as u32 {
|
|
row.push(path(&worse), path_id);
|
|
}
|
|
// Equal routes from one peer: the lowest identifier is the best.
|
|
assert!(!select::after_add(&mut row, &peers, &mut scratch));
|
|
assert_eq!(row.best(), 0);
|
|
|
|
row.push(path(&better), count as u32 + 1);
|
|
assert!(select::after_add(&mut row, &peers, &mut scratch));
|
|
assert_eq!(row.best(), count);
|
|
assert_eq!(row.route(row.best()).path_id, count as u32 + 1);
|
|
|
|
// With the better one gone, the choice among all the rest is the
|
|
// lowest identifier again.
|
|
assert!(select::after_remove(&mut row, count, &peers, &mut scratch));
|
|
assert_eq!(row.route(row.best()).path_id, 1);
|
|
}
|