//! Finding out what the server has and why: looking routes up, and seeing //! what was refused. use std::net::Ipv4Addr; use std::time::Duration; use bgp_integration_tests::default_policy; 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 bgp_server::config::PeerConfig; use bgp_server::family::FamilySet; use bgp_server::policy::ImportPolicy; use eyre::Result; use serde_json::Value; use tokio::net::TcpListener; const TIMEOUT: Duration = Duration::from_secs(5); fn announcement(prefixes: &[&str], as_path: &[u32]) -> Message { Message::Update(UpdateMessage { withdrawn_routes: vec![], path_attributes: vec![ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(as_path.to_vec()), PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))), ], announced_routes: prefixes .iter() .map(|p| IpPrefix::try_from(*p).unwrap().into()) .collect(), }) } /// The routes a query returns, as "prefix from AS, best or not". async fn routes(client: &reqwest::Client, base: &str, query: &str) -> Result> { let reply = client .get(format!("{base}/api/routes/received?{query}")) .send() .await? .error_for_status()?; let page: Value = reply.json().await?; Ok(page["routes"] .as_array() .unwrap() .iter() .map(|r| { format!( "{} AS{}{}", r["prefix"].as_str().unwrap(), r["peer_asn"], if r["best"] == true { " best" } else { "" } ) }) .collect()) } /// The questions asked when something is wrong with a prefix: who sends it /// and which route won, what covers an address, what is inside a block. #[tokio::test] async fn test_routes_can_be_looked_up() -> Result<()> { let server = TestServer::start(65000).await?; let (base, _http) = server.serve_http().await?; let client = reqwest::Client::new(); let mut one = TestPeer::connect(server.addr).await?; one.handshake(65001, Ipv4Addr::new(10, 0, 0, 1), 90).await?; let mut two = TestPeer::connect(server.addr).await?; two.handshake(65002, Ipv4Addr::new(10, 0, 0, 2), 90).await?; one.send(&announcement( &["10.0.0.0/8", "10.1.0.0/16", "10.1.2.0/24", "192.0.2.0/24"], &[65001], )) .await?; // A longer path for one of the same prefixes: it loses. two.send(&announcement(&["10.1.0.0/16"], &[65002, 65003])) .await?; tokio::time::timeout(TIMEOUT, async { while server.rib.len() != 5 { tokio::time::sleep(Duration::from_millis(5)).await; } }) .await?; // One prefix: every route for it, the best first. assert_eq!( routes(&client, &base, "prefix=10.1.0.0/16").await?, ["10.1.0.0/16 AS65001 best", "10.1.0.0/16 AS65002"] ); assert!( routes(&client, &base, "prefix=10.2.0.0/16") .await? .is_empty() ); // An address: every prefix that covers it, the most specific first. assert_eq!( routes(&client, &base, "address=10.1.2.3").await?, [ "10.1.2.0/24 AS65001 best", "10.1.0.0/16 AS65001 best", "10.1.0.0/16 AS65002", "10.0.0.0/8 AS65001 best" ] ); assert_eq!( routes(&client, &base, "address=10.200.0.1").await?, ["10.0.0.0/8 AS65001 best"] ); // A block: itself and everything inside it. assert_eq!( routes(&client, &base, "within=10.1.0.0/16").await?, [ "10.1.0.0/16 AS65001 best", "10.1.0.0/16 AS65002", "10.1.2.0/24 AS65001 best" ] ); // Only what was chosen; and everything, in address order. assert_eq!( routes(&client, &base, "within=10.0.0.0/8&best=true").await?, [ "10.0.0.0/8 AS65001 best", "10.1.0.0/16 AS65001 best", "10.1.2.0/24 AS65001 best" ] ); assert_eq!(routes(&client, &base, "").await?.len(), 5); // A page holds whole prefixes: at most the limit, unless one prefix // alone has more. assert!((1..=2).contains(&routes(&client, &base, "limit=2").await?.len())); // One peer's routes, by its address; a peer that is not there has none. assert_eq!(routes(&client, &base, "peer=127.0.0.1").await?.len(), 5); assert!(routes(&client, &base, "peer=192.0.2.9").await?.is_empty()); // A query that makes no sense says so. for (query, complaint) in [ ("prefix=10.1.0.0", "is not a prefix"), ("address=example", "is not an IP address"), ("prefix=10.0.0.0/8&address=10.0.0.1", "at most one of"), ] { let reply = client .get(format!("{base}/api/routes/received?{query}")) .send() .await?; assert_eq!(reply.status(), 400, "{query}"); let text = reply.text().await?; assert!(text.contains(complaint), "{query}: {text}"); } Ok(()) } /// A refused route is not in the table, so it has to be findable somewhere /// else: which peer, which prefix, with what path, and why. And there is /// something to alert on. #[tokio::test] async fn test_refused_routes_can_be_seen_and_counted() -> Result<()> { let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; let endpoint = listener.local_addr()?; let server = TestServer::start_with(1299, |c| { c.accept_unconfigured_peers = false; c.peers = vec![PeerConfig { name: "refused-demo".into(), endpoint: endpoint.into(), remote_asn: 3333, hold_time_secs: 90, local_address: None, families: FamilySet::supported(), add_path: None, import: ImportPolicy { max_prefixes: Some(100), ..default_policy() }, role: None, require_role: false, }]; }) .await?; let (base, _http) = server.serve_http().await?; let client = reqwest::Client::new(); let get = |path: &str| { let request = client.get(format!("{base}{path}")); async move { Ok::<_, eyre::Report>(request.send().await?.error_for_status()?) } }; let refused: Value = get("/api/routes/rejected").await?.json().await?; assert_eq!(refused, serde_json::json!([]), "nothing refused yet"); let mut peer = TestPeer::connect(server.addr).await?; peer.handshake(3333, Ipv4Addr::new(10, 0, 0, 2), 90).await?; peer.send(&announcement(&["193.0.0.0/21", "10.0.0.0/8"], &[3333])) .await?; peer.send(&announcement( &["194.0.1.0/24", "194.0.2.0/24"], &[3333, 64512], )) .await?; peer.send(&announcement(&["193.0.9.0/25"], &[3333, 2121])) .await?; tokio::time::timeout(TIMEOUT, async { while server .registry .list() .first() .map(|s| s.counters.rejected()) != Some(4) { tokio::time::sleep(Duration::from_millis(5)).await; } }) .await?; let refused: Value = get("/api/routes/rejected").await?.json().await?; let record = &refused[0]; assert_eq!(record["peer"], "refused-demo"); assert_eq!(record["peer_addr"], "127.0.0.1"); assert_eq!(record["total"], 4); assert_eq!( record["by_reason"], serde_json::json!({"bogon AS in path": 2, "bogon prefix": 1, "prefix too specific": 1}) ); // The latest first, each with the path it came with. let recent: Vec<(String, String, String)> = record["recent"] .as_array() .unwrap() .iter() .map(|r| { ( r["prefix"].as_str().unwrap().to_string(), r["reason"].as_str().unwrap().to_string(), r["as_path"].to_string(), ) }) .collect(); let row = |p: &str, why: &str, path: &str| (p.to_string(), why.to_string(), path.to_string()); assert_eq!( recent, [ row("193.0.9.0/25", "prefix too specific", "[3333,2121]"), row("194.0.2.0/24", "bogon AS in path", "[3333,64512]"), row("194.0.1.0/24", "bogon AS in path", "[3333,64512]"), row("10.0.0.0/8", "bogon prefix", "[3333]"), ] ); // Asked for by peer, by name or by address. for peer in ["refused-demo", "127.0.0.1"] { let one: Value = get(&format!("/api/routes/rejected?peer={peer}")) .await? .json() .await?; assert_eq!(one.as_array().unwrap().len(), 1, "{peer}"); } let none: Value = get("/api/routes/rejected?peer=someone-else") .await? .json() .await?; assert_eq!(none, serde_json::json!([])); // The peer's line in the list of peers has the numbers too. let peers: Value = get("/api/peers").await?.json().await?; assert_eq!(peers[0]["routes"], 1); assert_eq!(peers[0]["rejected_routes"], 4); // What an alert would be made from. let metrics = get("/metricz").await?.text().await?; for line in [ r#"bgp_routes_rejected_total{peer="refused-demo",reason="bogon AS in path"} 2"#, r#"bgp_routes_rejected_total{peer="refused-demo",reason="bogon prefix"} 1"#, r#"bgp_peer_routes{peer="refused-demo"} 1"#, r#"bgp_peer_max_prefixes{peer="refused-demo"} 100"#, ] { assert!(metrics.contains(line), "{line} is not in:\n{metrics}"); } // The record outlives the session; the count of routes does not. drop(peer); tokio::time::timeout(TIMEOUT, async { while !server.registry.list().is_empty() { tokio::time::sleep(Duration::from_millis(5)).await; } }) .await?; let refused: Value = get("/api/routes/rejected").await?.json().await?; assert_eq!(refused[0]["total"], 4); let metrics = get("/metricz").await?.text().await?; assert!(!metrics.contains(r#"bgp_peer_routes{peer="refused-demo"}"#)); Ok(()) }