A million IPv4 routes took 1.6 s to install (release build); they take 1.1 s now, and 0.5 s to remove instead of 1.15 s. Most of what is left is the kernel's own work on each route. - Nexthop objects (Linux 5.3 and later): a gateway is put in the kernel once and routes name it, so the kernel checks it once and not for every route. The daemon makes its own objects, uses nobody else's, is told when one of its own is removed and puts it back, and removes them when it stops, which takes all the routes that name them along at once. `without_nexthop_objects` turns this off; a kernel without them is noticed and served as before. - Writing to the kernel is a task of its own, fed through a short queue, so the next batch is worked out while the kernel is busy with the last. "In step" is said when the kernel has done the batch that completes the table, not when it was handed over. - What a next hop resolves to is kept, by next hop: it was looked up among the machine's addresses for every route. - A quicker hash for the plan's maps, and no lookups in the ones that are empty. Fixes a hole found on the way: when an interface went down and came back within the second it takes us to react, the kernel had dropped our routes and we did not put them back, since nothing about them had changed for us. After such a change the kernel is now asked what it actually has. scripts/in-namespace.sh runs one command in a scratch namespace; FERRIX_ROUTES sets the size of the timing test. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
179 lines
5.9 KiB
Rust
179 lines
5.9 KiB
Rust
//! The whole daemon against a real kernel: a peer announces routes over a
|
|
//! session, and `ip route` shows them.
|
|
//!
|
|
//! This changes the machine's interfaces and routes, so it is not run by
|
|
//! `cargo test`; `scripts/kernel-tests.sh` runs it in a network namespace
|
|
//! of its own.
|
|
#![cfg(target_os = "linux")]
|
|
|
|
use std::collections::BTreeSet;
|
|
use std::net::Ipv4Addr;
|
|
use std::process::Command;
|
|
use std::time::{Duration, Instant};
|
|
|
|
use bgp_fib::FibConfig;
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use bgp_packet::message::{
|
|
AsPathAttribute, Message, NextHopPathAttribute, OriginPathAttribute, PathAttribute,
|
|
UpdateMessage,
|
|
};
|
|
use bgp_primitives::ip_prefix::IpPrefix;
|
|
use eyre::Result;
|
|
use serde_json::Value;
|
|
|
|
const TIMEOUT: Duration = Duration::from_secs(10);
|
|
const TABLE: u32 = 1100;
|
|
const INTERFACE: &str = "ferrix100";
|
|
|
|
#[track_caller]
|
|
fn ip(arguments: &str) {
|
|
let output = Command::new("ip")
|
|
.args(arguments.split_whitespace())
|
|
.output()
|
|
.expect("the ip command is there");
|
|
assert!(
|
|
output.status.success(),
|
|
"ip {arguments}: {}",
|
|
String::from_utf8_lossy(&output.stderr)
|
|
);
|
|
}
|
|
|
|
fn kernel_routes() -> BTreeSet<String> {
|
|
let output = Command::new("ip")
|
|
.args(["route", "show", "table", &TABLE.to_string()])
|
|
.output()
|
|
.unwrap();
|
|
String::from_utf8_lossy(&output.stdout)
|
|
.lines()
|
|
.map(|line| line.split_whitespace().collect::<Vec<_>>().join(" "))
|
|
// The number of the nexthop object a route names is the kernel's
|
|
// to choose.
|
|
.map(|line| match line.split_once(" nhid ") {
|
|
Some((prefix, rest)) => {
|
|
let rest = rest.split_once(' ').map_or("", |(_, rest)| rest);
|
|
format!("{prefix} {rest}")
|
|
}
|
|
None => line,
|
|
})
|
|
.filter(|line| !line.is_empty())
|
|
.collect()
|
|
}
|
|
|
|
#[track_caller]
|
|
fn kernel_becomes(expected: &[&str]) {
|
|
let expected: BTreeSet<String> = expected.iter().map(|route| route.to_string()).collect();
|
|
let start = Instant::now();
|
|
while kernel_routes() != expected {
|
|
assert!(
|
|
start.elapsed() < TIMEOUT,
|
|
"the kernel has {:#?}; expected {expected:#?}",
|
|
kernel_routes()
|
|
);
|
|
std::thread::sleep(Duration::from_millis(20));
|
|
}
|
|
}
|
|
|
|
/// Removes the test's interface, and its routes with it, when dropped.
|
|
struct Interface;
|
|
|
|
impl Drop for Interface {
|
|
fn drop(&mut self) {
|
|
let _ = Command::new("ip").args(["link", "del", INTERFACE]).output();
|
|
let _ = Command::new("ip")
|
|
.args(["route", "flush", "table", &TABLE.to_string()])
|
|
.output();
|
|
}
|
|
}
|
|
|
|
async fn announce(peer: &mut TestPeer, prefixes: &[&str], next_hop: Ipv4Addr) -> Result<()> {
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![],
|
|
path_attributes: vec![
|
|
PathAttribute::Origin(OriginPathAttribute::IGP),
|
|
AsPathAttribute::from_asns(vec![65001]),
|
|
PathAttribute::NextHop(NextHopPathAttribute(next_hop)),
|
|
],
|
|
announced_routes: prefixes
|
|
.iter()
|
|
.map(|p| IpPrefix::try_from(*p).unwrap().into())
|
|
.collect(),
|
|
}))
|
|
.await
|
|
}
|
|
|
|
/// A peer's routes go from the session to the kernel, and out of it again
|
|
/// when they are withdrawn and when the session ends.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
#[ignore = "changes the machine's routes; run scripts/kernel-tests.sh"]
|
|
async fn test_routes_from_a_session_reach_the_kernel() -> Result<()> {
|
|
ip(&format!("link add {INTERFACE} type dummy"));
|
|
let _interface = Interface;
|
|
ip(&format!("addr add 10.100.0.1/24 dev {INTERFACE}"));
|
|
ip(&format!("link set {INTERFACE} up"));
|
|
|
|
let server = TestServer::start_with(65000, |config| {
|
|
config.kernel = Some(FibConfig {
|
|
table: TABLE,
|
|
..FibConfig::default()
|
|
});
|
|
})
|
|
.await?;
|
|
let (base, _http) = server.serve_http().await?;
|
|
let mut peer = TestPeer::connect(server.addr).await?;
|
|
peer.handshake(65001, Ipv4Addr::new(10, 0, 0, 2), 90)
|
|
.await?;
|
|
|
|
// The peer speaks from the loopback and names a router on the test's
|
|
// interface as next hop, as a route server would.
|
|
let neighbor = Ipv4Addr::new(10, 100, 0, 2);
|
|
announce(&mut peer, &["203.0.113.0/24", "198.18.0.0/15"], neighbor).await?;
|
|
// One by way of a router that is not a neighbor.
|
|
announce(&mut peer, &["192.88.99.0/24"], Ipv4Addr::new(172, 31, 0, 1)).await?;
|
|
kernel_becomes(&[
|
|
"203.0.113.0/24 via 10.100.0.2 dev ferrix100 proto bgp metric 20",
|
|
"198.18.0.0/15 via 10.100.0.2 dev ferrix100 proto bgp metric 20",
|
|
]);
|
|
|
|
let kernel: Value = reqwest::get(format!("{base}/api/kernel"))
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
assert_eq!(kernel["enabled"], true);
|
|
assert_eq!(kernel["table"], TABLE);
|
|
assert_eq!(kernel["installed"], 2);
|
|
assert_eq!(
|
|
kernel["unresolved"],
|
|
serde_json::json!({"next hop not directly connected": 1})
|
|
);
|
|
let shown = kernel["interfaces"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|interface| interface["name"] == INTERFACE)
|
|
.expect("the test's interface is listed");
|
|
assert_eq!(shown["up"], true);
|
|
// Its own address, beside the link-local one the kernel gave it.
|
|
assert!(
|
|
shown["addresses"]
|
|
.as_array()
|
|
.unwrap()
|
|
.contains(&serde_json::json!("10.100.0.1/24")),
|
|
"{shown}"
|
|
);
|
|
|
|
// One is withdrawn.
|
|
peer.send(&Message::Update(UpdateMessage {
|
|
withdrawn_routes: vec![IpPrefix::try_from("198.18.0.0/15").unwrap().into()],
|
|
path_attributes: vec![],
|
|
announced_routes: vec![],
|
|
}))
|
|
.await?;
|
|
kernel_becomes(&["203.0.113.0/24 via 10.100.0.2 dev ferrix100 proto bgp metric 20"]);
|
|
|
|
// The session ends: packets must not go on following its routes.
|
|
drop(peer);
|
|
kernel_becomes(&[]);
|
|
Ok(())
|
|
}
|