//! Blackhole requests (RFC 7999): refused by default, and with //! `accept_blackholes` taken for one address inside what the peer may //! announce, whatever their next hop. use std::collections::BTreeSet; use std::net::Ipv4Addr; use std::time::Duration; use bgp_integration_tests::announcement::announcement; use bgp_integration_tests::test_peer::TestPeer; use bgp_integration_tests::test_server::{ServerWithPeer, TestServer}; use bgp_integration_tests::{default_policy, prefix, wait_until}; use bgp_server::policy::{ImportPolicy, PrefixSet}; use eyre::Result; const LOCAL_ASN: u32 = 1299; const PEER_ASN: u32 = 3333; const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2); const BLACKHOLE: (u16, u16) = (0xFFFF, 0x029A); async fn server_with(import: ImportPolicy) -> Result { TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| { c.connect_retry = Duration::from_millis(50); peer.import = import; }) .await } /// Waits until the server has taken `taken` routes of the session's and /// refused `refused`, and returns the prefixes in its table. async fn table_after(server: &TestServer, taken: u64, refused: u64) -> Result> { let counters = server.registry.list()[0].counters.clone(); wait_until("the routes are dealt with", || { (counters.routes(), counters.rejected()) == (taken, refused) }) .await .map_err(|_| { eyre::eyre!( "the server took {} routes and refused {}; expected {taken} and {refused}", counters.routes(), counters.rejected() ) })?; Ok(server .received_routes() .iter() .map(|route| route.prefix.to_string()) .collect()) } /// The policy that lets the peer announce 193.0.0.0/21 and nothing /// longer than a /24 inside it, as a customer's would. fn customer_policy(accept_blackholes: bool) -> ImportPolicy { let mut prefixes = PrefixSet::default(); prefixes.allow(prefix("193.0.0.0/21"), 24); ImportPolicy { prefixes: Some(prefixes), accept_blackholes, ..default_policy() } } #[tokio::test] async fn test_blackhole_requests_are_refused_unless_accepted() -> Result<()> { let with_peer = server_with(customer_policy(false)).await?; let server = &with_peer.server; let mut peer: TestPeer = server.established(PEER_ASN, PEER_ID).await?; peer.announce(announcement(&["193.0.0.9/32"], &[PEER_ASN]).with_communities(&[BLACKHOLE])) .await?; peer.announce(announcement(&["193.0.0.0/24"], &[PEER_ASN])) .await?; assert_eq!(table_after(server, 1, 1).await?, set(&["193.0.0.0/24"])); let refused = server.rejections.peers(); assert_eq!(refused.len(), 1); assert_eq!( refused[0] .1 .by_reason() .iter() .map(|(reason, n)| (reason.as_str(), *n)) .collect::>(), [("blackhole request", 1)] ); Ok(()) } #[tokio::test] async fn test_a_blackhole_request_is_taken_for_one_address_in_the_peers_space() -> Result<()> { let with_peer = server_with(customer_policy(true)).await?; let server = &with_peer.server; let mut peer: TestPeer = server.established(PEER_ASN, PEER_ID).await?; // One address inside the peer's prefix, from a next hop that is no // neighbor and would not pass as an ordinary route's. peer.announce( announcement(&["193.0.0.9/32"], &[PEER_ASN]) .with_next_hop(Ipv4Addr::new(172, 31, 0, 9)) .with_communities(&[BLACKHOLE]), ) .await?; // More than one address: not a blackhole request that is taken. peer.announce(announcement(&["193.0.0.0/25"], &[PEER_ASN]).with_communities(&[BLACKHOLE])) .await?; // One address outside the peer's space. peer.announce(announcement(&["193.0.8.9/32"], &[PEER_ASN]).with_communities(&[BLACKHOLE])) .await?; // And an ordinary route, taken as ever. peer.announce(announcement(&["193.0.0.0/24"], &[PEER_ASN])) .await?; assert_eq!( table_after(server, 2, 2).await?, set(&["193.0.0.0/24", "193.0.0.9/32"]) ); let refused = server.rejections.peers(); let mut reasons: Vec<(&str, u64)> = refused[0] .1 .by_reason() .iter() .map(|(reason, n)| (reason.as_str(), *n)) .collect(); reasons.sort(); assert_eq!( reasons, [ ("blackhole for more than one address", 1), ("prefix not allowed", 1) ] ); // The route is in the table with its request on it, for the kernel // and for whoever it goes on to. let routes = server.received_routes(); let blackhole = routes .iter() .find(|route| route.prefix == prefix("193.0.0.9/32")) .expect("the blackhole route is in the table"); assert!(ImportPolicy::is_blackhole_request(&blackhole.attributes)); Ok(()) } fn set(prefixes: &[&str]) -> BTreeSet { prefixes.iter().map(|p| p.to_string()).collect() }