Add an interoperability lab with FRR, BIRD and Routinator
scripts/interop/lab.sh runs the daemon against FRR, BIRD and Routinator, each in a network namespace on one exchange LAN, and checks what every side ends up with: sessions and routes over IPv4 and IPv6 with their attributes, four-octet AS numbers, ADD-PATH, RFC 9234 roles and Only to Customer, RTR version 2 and invalid routes, route refresh, session resets, killed peers, a cut wire, the daemon stopped and killed, a table of a million routes and a restart under it, role mismatch and the prefix limit. All of it passes against FRR 10.5.1, BIRD 2.18 and Routinator 0.15.2. scripts/interop/real-rpki.sh checks the RTR client against a Routinator with the real RPKI: 713,258 IPv4 and 187,479 IPv6 VRPs and 3,379 ASPAs, the same numbers on both sides. What the lab found, fixed here: - A daemon restarted with its routes in the kernel removed them all at once, its table being empty until the peers had sent theirs again, and then put them back: no forwarding meanwhile. Routes of an earlier run are now kept for `stale_seconds` (120 by default) and only those nobody announced again are removed. A restart under a million routes takes 1.1 s to be in step, with nothing removed. - FRR ends a session it loses to a collision with a state machine error without a subcode, which was shown as "error code 5 subcode 0". CI gets a job that runs the lab. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
1 parent
1de75fc610
commit
321af51e4b
16 files changed
+1024
-17
No files matched your search
+96
-9
@@ -142,6 +142,9 @@ struct Entry {
|
||||
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.
|
||||
@@ -209,18 +212,49 @@ impl Plan {
|
||||
self.installed.keys()
|
||||
}
|
||||
|
||||
/// Notes a route of ours that the kernel already has, from before this
|
||||
/// run. It stays only if the routing table turns out to have the same.
|
||||
pub fn adopt(&mut self, prefix: IpPrefix, target: Option<Target>) {
|
||||
/// 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) {
|
||||
@@ -246,6 +280,7 @@ impl Plan {
|
||||
let entry = self.installed.entry(prefix).or_insert(Entry {
|
||||
target: None,
|
||||
confirmed: true,
|
||||
kept: false,
|
||||
});
|
||||
entry.confirmed = true;
|
||||
if entry.target == Some(target) {
|
||||
@@ -279,7 +314,7 @@ impl Plan {
|
||||
let stale: Vec<IpPrefix> = self
|
||||
.installed
|
||||
.iter()
|
||||
.filter(|(_, entry)| !entry.confirmed)
|
||||
.filter(|(_, entry)| !entry.confirmed && !entry.kept)
|
||||
.map(|(prefix, _)| *prefix)
|
||||
.collect();
|
||||
stale
|
||||
@@ -493,10 +528,10 @@ mod tests {
|
||||
);
|
||||
let a = target("192.0.2.2", 2);
|
||||
// Left in the kernel by an earlier run.
|
||||
plan.adopt(kept, Some(a));
|
||||
plan.adopt(changed, Some(a));
|
||||
plan.adopt(stale, Some(a));
|
||||
plan.adopt(odd, None);
|
||||
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();
|
||||
@@ -520,6 +555,58 @@ mod tests {
|
||||
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();
|
||||
@@ -578,7 +665,7 @@ mod tests {
|
||||
let mut plan = Plan::default();
|
||||
for (prefix, gateway) in left_behind {
|
||||
kernel.insert(p(prefix), t(gateway));
|
||||
plan.adopt(p(prefix), Some(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();
|
||||
|
||||
Reference in new issue
Block a user