//! End-to-end tests of route selection: routes for one prefix from several //! peers, which of them the server chooses, and how that is shown. use std::net::Ipv4Addr; use bgp_integration_tests::announcement::{Announcement, withdrawal as withdrawal_of}; use bgp_integration_tests::test_peer::TestPeer; use bgp_integration_tests::test_server::{RouteChange, TestServer}; use bgp_integration_tests::{TIMEOUT, get_json, prefix}; use bgp_packet::message::{PathAttribute, UpdateMessage}; use bgp_primitives::ip_prefix::{IpPrefix, Nlri}; use eyre::Result; use serde_json::Value; const LOCAL_ASN: u32 = 65000; /// A session from the peer numbered `number`, in AS 65000 + `number`. async fn peer(server: &TestServer, number: u8) -> Result { server .established( LOCAL_ASN + u32::from(number), Ipv4Addr::new(10, 0, 0, number), ) .await } fn announcement(prefixes: &[IpPrefix], as_path: &[u32]) -> Announcement { Announcement::of(Nlri::plain(prefixes.iter().copied()), as_path) } fn withdrawal(prefixes: &[IpPrefix]) -> UpdateMessage { withdrawal_of(Nlri::plain(prefixes.iter().copied())) } /// The AS path of the route the server holds as best for `prefix`. fn best_path(server: &TestServer, prefix: &IpPrefix) -> Option> { let best = server .received_routes() .into_iter() .find(|route| route.prefix == *prefix && route.best)?; best.attributes .iter() .find_map(|attribute| match attribute { PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()), _ => None, }) } /// Two peers offer the same prefix. The route with the shorter AS path is /// chosen; when it goes, the other is. #[tokio::test] async fn test_the_best_route_is_chosen_and_replaced_when_it_goes() -> Result<()> { let server = TestServer::start(LOCAL_ASN).await?; let (base, _http) = server.serve_http().await?; let client = reqwest::Client::new(); let mut changes = server.route_changes(); let p = prefix("203.0.113.0/24"); let mut far = peer(&server, 1).await?; far.announce(announcement(&[p], &[65001, 64999, 64998])) .await?; server.wait_for_routes(1).await?; assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998])); let mut near = peer(&server, 2).await?; near.announce(announcement(&[p], &[65002, 64998])).await?; server.wait_for_routes(2).await?; assert_eq!(best_path(&server, &p), Some(vec![65002, 64998])); // Both routes are kept and shown; one is marked. let status = get_json(&client, &format!("{base}/api/status")).await?; assert_eq!(status["received_routes"], 2); assert_eq!(status["received_prefixes"], 1); let routes = get_json(&client, &format!("{base}/api/routes/received")).await?["routes"].take(); let best: Vec<&Value> = routes .as_array() .unwrap() .iter() .filter(|route| route["best"] == true) .collect(); assert_eq!(routes.as_array().unwrap().len(), 2); assert_eq!(best.len(), 1); assert_eq!(best[0]["peer_asn"], 65002); // Listed from the best down, each with its place in selection. let ranks: Vec<(u64, u64)> = routes .as_array() .unwrap() .iter() .map(|route| { ( route["rank"].as_u64().unwrap(), route["peer_asn"].as_u64().unwrap(), ) }) .collect(); assert_eq!(ranks, vec![(1, 65002), (2, 65001)]); // The session with the best route ends: the other route takes over. drop(near); server.wait_for_routes(1).await?; assert_eq!(best_path(&server, &p), Some(vec![65001, 64999, 64998])); far.announce(withdrawal(&[p])).await?; server.wait_for_routes(0).await?; assert_eq!(best_path(&server, &p), None); // A follower of the best routes is told of each change: the first // route, the better one, back to the first, none. let mut seen = Vec::new(); while seen.last() != Some(&RouteChange::Withdrawn { prefix: p }) { seen.push(tokio::time::timeout(TIMEOUT, changes.recv()).await??); } assert!( seen.iter() .all(|change| matches!(change, RouteChange::Announced { prefix, .. } | RouteChange::Withdrawn { prefix } if *prefix == p)), "{seen:?}" ); Ok(()) } /// The event stream reports changes of best route, and only those. #[tokio::test] async fn test_changes_of_best_route_are_on_the_event_stream() -> Result<()> { let server = TestServer::start(LOCAL_ASN).await?; let (base, _http) = server.serve_http().await?; let p = prefix("203.0.113.0/24"); // A route that is there before the client connects is not an event. let mut first = peer(&server, 1).await?; first .announce(announcement(&[prefix("198.51.100.0/24")], &[65001])) .await?; server.wait_for_routes(1).await?; let mut stream = reqwest::Client::new() .get(format!("{base}/api/events")) .send() .await? .error_for_status()?; first.announce(announcement(&[p], &[65001, 64998])).await?; server.wait_for_routes(2).await?; first.announce(withdrawal(&[p])).await?; server.wait_for_routes(1).await?; let mut text = String::new(); let mut events: Vec = Vec::new(); tokio::time::timeout(TIMEOUT, async { while !events.iter().any(|event| event["type"] == "best_withdrawn") { let chunk = stream .chunk() .await? .ok_or_else(|| eyre::eyre!("stream ended"))?; text.push_str(std::str::from_utf8(&chunk)?); events = text .lines() .filter_map(|line| line.strip_prefix("data:")) .filter_map(|data| serde_json::from_str(data.trim()).ok()) .collect(); } Ok::<_, eyre::Error>(()) }) .await??; let about_routes: Vec<&Value> = events .iter() .filter(|event| { event["type"] .as_str() .is_some_and(|t| t.starts_with("best_")) }) .collect(); // The announcement and the withdrawal may have been read as one change // or as two; either way nothing is said of the route that was there // from the start, and the last word is that the prefix is gone. assert!(about_routes.len() <= 2, "{about_routes:?}"); for event in &about_routes { let named = event["prefix"] .as_str() .or(event["route"]["prefix"].as_str()); assert_eq!(named, Some("203.0.113.0/24"), "{event}"); } assert_eq!(about_routes.last().unwrap()["type"], "best_withdrawn"); Ok(()) } /// The table can be far too large to send whole: the API gives a part of /// it, and says how much there is. #[tokio::test] async fn test_received_routes_are_returned_up_to_a_limit() -> Result<()> { let server = TestServer::start(LOCAL_ASN).await?; let (base, _http) = server.serve_http().await?; let client = reqwest::Client::new(); let prefixes: Vec = (0..40).map(|n| prefix(&format!("10.{n}.0.0/16"))).collect(); let mut peer = peer(&server, 1).await?; peer.announce(announcement(&prefixes, &[65001])).await?; server.wait_for_routes(40).await?; let count = |page: Value| page["routes"].as_array().unwrap().len(); let url = format!("{base}/api/routes/received"); assert_eq!(count(get_json(&client, &url).await?), 40); assert_eq!( count(get_json(&client, &format!("{url}?limit=7")).await?), 7 ); let status = get_json(&client, &format!("{base}/api/status")).await?; assert_eq!(status["received_routes"], 40); Ok(()) }