//! 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 { 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::>().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 = 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(()) }