//! AS path verification with ASPAs, end to end: a server that gets ASPAs //! from a cache, a peer that announces routes, and the ASPAs changing //! under both. use std::collections::BTreeSet; use std::net::Ipv4Addr; use std::time::Duration; use bgp_integration_tests::default_policy; use bgp_integration_tests::test_cache::TestCache; 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_rpki::{CacheState, RpkiConfig}; use bgp_server::config::PeerConfig; use bgp_server::family::FamilySet; use bgp_server::policy::{ImportPolicy, PeerRole}; use eyre::Result; use serde_json::Value; use tokio::net::TcpListener; const LOCAL_ASN: u32 = 1299; const PEER_ASN: u32 = 3333; const TIMEOUT: Duration = Duration::from_secs(5); fn prefix(s: &str) -> IpPrefix { IpPrefix::try_from(s).unwrap() } /// What a test holds on to: the server, and the listener its dialling /// goes to. struct Setup { server: TestServer, _listener: TcpListener, } /// A server that validates against `cache`, with one configured peer at /// the address the test connects from, which is `role` to it. Returns once /// the server has the cache's data. async fn server_with( cache: &TestCache, role: Option, import: ImportPolicy, ) -> Result { let listener = TcpListener::bind((Ipv4Addr::LOCALHOST, 0)).await?; let endpoint = listener.local_addr()?; let cache = cache.address(); let server = TestServer::start_with(LOCAL_ASN, |c| { c.accept_unconfigured_peers = false; c.rpki = Some(RpkiConfig { caches: vec![cache], retry: Some(Duration::from_millis(100)), ..RpkiConfig::default() }); c.peers = vec![PeerConfig { name: "r1".into(), endpoint: endpoint.into(), remote_asn: PEER_ASN, hold_time_secs: 90, local_address: None, families: FamilySet::supported(), add_path: None, import, role, require_role: false, }]; }) .await?; let rpki = server.rpki.clone().expect("validation is configured"); tokio::time::timeout(TIMEOUT, async { while rpki.status().state != CacheState::Synchronised { tokio::time::sleep(Duration::from_millis(5)).await; } }) .await?; Ok(Setup { server, _listener: listener, }) } async fn established(server: &TestServer) -> Result { let mut peer = TestPeer::connect(server.addr).await?; peer.handshake(PEER_ASN, Ipv4Addr::new(10, 0, 0, 2), 90) .await?; tokio::time::timeout(TIMEOUT, async { while server.registry.list().is_empty() { tokio::time::sleep(Duration::from_millis(5)).await; } }) .await?; Ok(peer) } /// Announces `prefixes` with the AS path `path`. async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> { peer.send(&Message::Update(UpdateMessage { withdrawn_routes: vec![], path_attributes: vec![ PathAttribute::Origin(OriginPathAttribute::IGP), AsPathAttribute::from_asns(path.to_vec()), PathAttribute::NextHop(NextHopPathAttribute(Ipv4Addr::new(192, 0, 2, 1))), ], announced_routes: prefixes.iter().map(|p| prefix(p).into()).collect(), })) .await } /// Waits until the table holds exactly `expected` and the peer has `held` /// routes held aside. async fn table_becomes(server: &TestServer, expected: &[&str], held: u64) -> Result<()> { let expected: BTreeSet = expected.iter().map(|p| p.to_string()).collect(); let table = || -> BTreeSet { server .received_routes() .iter() .map(|route| route.prefix.to_string()) .collect() }; let held_now = || { server .registry .list() .first() .map_or(0, |session| session.counters.held()) }; tokio::time::timeout(TIMEOUT, async { while table() != expected || held_now() != held { tokio::time::sleep(Duration::from_millis(5)).await; } }) .await .map_err(|_| { eyre::eyre!( "the table has {:?} with {} held; expected {expected:?} with {held} held", table(), held_now() ) }) } /// AS 15169 buys from 3356 and says so; 3356 and the peer, 3333, have no /// providers. fn aspas() -> Vec<(u32, Vec)> { vec![(15169, vec![3356]), (3356, vec![0]), (PEER_ASN, vec![0])] } /// A peer that hands us a route it got from one of its own peers is /// leaking it. The route is refused; routes whose paths are attested, or /// that nothing is known about, are taken. #[tokio::test] async fn test_a_leaked_route_is_refused_and_the_rest_are_taken() -> Result<()> { let cache = TestCache::start_with(vec![], aspas()).await; let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?; let (base, _http) = server.serve_http().await?; let mut peer = established(&server).await?; // The peer's own route; one of a customer of its that has no ASPA; // and 15169's route by way of 3356, which is not for 3333 to pass on // to a peer: 3356 is not its customer. announce(&mut peer, &["193.0.0.0/21"], &[PEER_ASN]).await?; announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?; announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?; table_becomes(&server, &["193.0.0.0/21", "194.0.0.0/24"], 1).await?; let client = reqwest::Client::new(); let get = |path: &str| { let request = client.get(format!("{base}{path}")); async move { Ok::(request.send().await?.error_for_status()?.json().await?) } }; let routes = get("/api/routes/received").await?["routes"].take(); let validity: Vec<(&str, &str)> = routes .as_array() .unwrap() .iter() .map(|r| (r["prefix"].as_str().unwrap(), r["aspa"].as_str().unwrap())) .collect(); assert_eq!( validity, [("193.0.0.0/21", "valid"), ("194.0.0.0/24", "unknown")] ); let refused = get("/api/routes/rejected").await?; assert_eq!( refused[0]["by_reason"], serde_json::json!({"ASPA invalid": 1}) ); let peers = get("/api/peers").await?; assert_eq!(peers[0]["role"], "peer"); assert_eq!(peers[0]["held_routes"], 1); let rpki = get("/api/rpki").await?; assert_eq!(rpki["aspas"], 3); assert_eq!(rpki["protocol_version"], 2); Ok(()) } /// What a path may look like depends on who it comes from. The same route /// that a peer may not pass on is in order from a provider, which may hand /// us anything it has. Where it is not said what the peer is, paths are /// not verified. #[tokio::test] async fn test_a_route_that_has_crossed_a_peering_is_taken_from_a_provider_only() -> Result<()> { for (role, taken) in [ (Some(PeerRole::Provider), true), (Some(PeerRole::Customer), false), (Some(PeerRole::Peer), false), (Some(PeerRole::RouteServerClient), false), (None, true), ] { let cache = TestCache::start_with(vec![], aspas()).await; let Setup { server, .. } = server_with(&cache, role, default_policy()).await?; let mut peer = established(&server).await?; announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?; let expected = if taken { table_becomes(&server, &["8.8.8.0/24"], 0).await } else { table_becomes(&server, &[], 1).await }; expected.map_err(|e| eyre::eyre!("from a peer that is {role:?}: {e}"))?; } Ok(()) } /// An Internet exchange's route server does not put its AS on the path, so /// the path need not begin with the route server's AS. A leak is refused /// across it all the same. #[tokio::test] async fn test_paths_from_a_route_server_do_not_begin_with_its_as() -> Result<()> { let cache = TestCache::start_with(vec![], aspas()).await; let import = ImportPolicy { accept_any_first_as: true, ..default_policy() }; let Setup { server, .. } = server_with(&cache, Some(PeerRole::RouteServer), import).await?; let mut peer = established(&server).await?; // 3356's customer route, as 3356 gave it to the route server. announce(&mut peer, &["8.8.8.0/24"], &[3356, 15169]).await?; // The same route leaked to the route server by another member. announce(&mut peer, &["8.8.4.0/24"], &[2121, 3356, 15169]).await?; table_becomes(&server, &["8.8.8.0/24"], 1).await } /// Validity changes with the ASPAs, and the routes follow: one that was /// taken goes when its path becomes invalid, and one that was refused is /// taken when it no longer is. The peer sends nothing more meanwhile. #[tokio::test] async fn test_routes_follow_the_aspas() -> Result<()> { let cache = TestCache::start_with(vec![], aspas()).await; let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?; let mut peer = established(&server).await?; announce(&mut peer, &["194.0.0.0/24"], &[PEER_ASN, 2121]).await?; announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?; table_becomes(&server, &["194.0.0.0/24"], 1).await?; // 2121 says that its only provider is 1101: its route by way of 3333 // is now invalid, and goes. let mut changed = aspas(); changed.push((2121, vec![1101])); cache.set_aspas(changed); table_becomes(&server, &[], 2).await?; let refused = &server.refusals.peers()[0].1; assert_eq!(refused.total(), 2); assert_eq!(refused.recent()[0].prefix, prefix("194.0.0.0/24")); assert_eq!(&refused.recent()[0].as_path[..], [PEER_ASN, 2121]); // 3356 becomes a customer of 3333, and 2121 too: both routes come // back, with the paths they were announced with. cache.set_aspas(vec![ (15169, vec![3356]), (3356, vec![PEER_ASN]), (PEER_ASN, vec![0]), (2121, vec![1101, PEER_ASN]), ]); table_becomes(&server, &["194.0.0.0/24", "8.8.8.0/24"], 0).await?; let paths: BTreeSet> = server .received_routes() .iter() .flat_map(|route| &route.attributes) .filter_map(|attribute| match attribute { PathAttribute::AsPath(path) => Some(path.segments[0].path.clone()), _ => None, }) .collect(); assert_eq!( paths, [vec![PEER_ASN, 2121], vec![PEER_ASN, 3356, 15169]].into() ); // The validator loses its ASPAs altogether: nothing is known about // any path, and nothing is refused. cache.set_aspas(vec![ (15169, vec![3356]), (3356, vec![0]), (PEER_ASN, vec![0]), ]); table_becomes(&server, &["194.0.0.0/24"], 1).await?; cache.set_aspas(vec![]); table_becomes(&server, &["194.0.0.0/24", "8.8.8.0/24"], 0).await } /// A peer can be exempt: its routes with invalid paths are taken, and /// shown for what they are. #[tokio::test] async fn test_a_peer_can_be_exempt_from_path_verification() -> Result<()> { let cache = TestCache::start_with(vec![], aspas()).await; let import = ImportPolicy { accept_aspa_invalid: true, ..default_policy() }; let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), import).await?; let (base, _http) = server.serve_http().await?; let mut peer = established(&server).await?; announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?; table_becomes(&server, &["8.8.8.0/24"], 0).await?; let routes: Value = reqwest::get(format!("{base}/api/routes/received")) .await? .json() .await?; assert_eq!(routes["routes"][0]["aspa"], "invalid"); assert!(server.refusals.peers().is_empty()); Ok(()) } /// A route can fail both checks. It is taken only when it passes both. #[tokio::test] async fn test_a_route_is_held_until_origin_and_path_are_both_in_order() -> Result<()> { let google = |asn| bgp_rpki::Vrp { prefix: prefix("8.8.8.0/24"), max_length: 24, asn, }; let cache = TestCache::start_with(vec![google(13335)], aspas()).await; let Setup { server, .. } = server_with(&cache, Some(PeerRole::Peer), default_policy()).await?; let mut peer = established(&server).await?; announce(&mut peer, &["8.8.8.0/24"], &[PEER_ASN, 3356, 15169]).await?; table_becomes(&server, &[], 1).await?; // The origin is put right; the path is still a leak. cache.set(vec![google(15169)]); tokio::time::sleep(Duration::from_millis(500)).await; table_becomes(&server, &[], 1).await?; cache.set_aspas(vec![(15169, vec![3356]), (3356, vec![PEER_ASN])]); table_becomes(&server, &["8.8.8.0/24"], 0).await }