The test files had grown their own copies of the same helpers: prefix() twelve times, established() seven, an UPDATE builder six, a seventeen field PeerConfig literal in fifteen places, a timeout constant in twenty one. Each is in crates/integration-tests/src once now: peer_config() and TestServer::with_peer for a configured peer, Announcement for building UPDATEs, established(), the waits (wait_until, wait_for, wait_for_routes, wait_for_sessions), get_json, and TIMEOUT. A new field on PeerConfig is one line to add, not fifteen. TestPeer::recv has a deadline, so a test that gets nothing fails instead of hanging; is_closed, which read one byte and took a KEEPALIVE for an open connection, is closed_within and closed_unanswered. Net 788 lines fewer; every test checks what it did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
173 lines
5.9 KiB
Rust
173 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::announcement::{announcement, withdrawal};
|
|
use bgp_integration_tests::prefix;
|
|
use bgp_integration_tests::test_peer::TestPeer;
|
|
use bgp_integration_tests::test_server::TestServer;
|
|
use eyre::Result;
|
|
use serde_json::Value;
|
|
|
|
/// The kernel is given more time than the loopback tests allow.
|
|
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.announce(announcement(prefixes, &[65001]).with_next_hop(next_hop))
|
|
.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);
|
|
// The route by way of a router that is no neighbor is not in the
|
|
// table for the kernel to be asked for: it is held, in case the
|
|
// router becomes one.
|
|
assert_eq!(kernel["unresolved"], serde_json::json!({}));
|
|
let session = server.registry.list().pop().expect("the session");
|
|
assert_eq!(session.counters.held(), 1);
|
|
assert_eq!(server.rib.len(), 2);
|
|
let refused: Value = reqwest::get(format!("{base}/api/routes/rejected"))
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
assert_eq!(
|
|
refused[0]["by_reason"],
|
|
serde_json::json!({"next hop unreachable": 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.announce(withdrawal([prefix("198.18.0.0/15").into()]))
|
|
.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(())
|
|
}
|