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