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>
190 lines
6.4 KiB
Rust
190 lines
6.4 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);
|
|
// 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.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(())
|
|
}
|