The API, the metrics, the status page and the documentation say a route is "rejected" (/api/routes/rejected, rejected_routes, bgp_routes_rejected_total, "Rejected routes"), and the reason type is Rejection. The code that keeps the record said "refused": RefusalLog, PeerRefusals, RefusedRoute, RefusalsView, RefusedRouteView, the refusals module. Those are RejectionLog, PeerRejections, RejectedRoute, RejectionsView, RejectedRouteView and rejections now, and the log line says "Rejected a route". Nothing the API or the metrics expose changes. Refusing stays what it was: a connection turned away at the door (server::Refusal), a kernel that would not take a route, an OPEN we would not have. The two aliases of SendReceive, AddPathSendRecv and OrfSendRecv, are gone; it is one type. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
276 lines
10 KiB
Rust
276 lines
10 KiB
Rust
//! 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::announcement::announcement;
|
|
use bgp_integration_tests::test_cache::TestCache;
|
|
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_packet::message::PathAttribute;
|
|
use bgp_rpki::{CacheState, RpkiConfig};
|
|
use bgp_server::policy::{ImportPolicy, PeerRole};
|
|
use eyre::Result;
|
|
use serde_json::Value;
|
|
|
|
const LOCAL_ASN: u32 = 1299;
|
|
const PEER_ASN: u32 = 3333;
|
|
const PEER_ID: Ipv4Addr = Ipv4Addr::new(10, 0, 0, 2);
|
|
|
|
/// 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<PeerRole>,
|
|
import: ImportPolicy,
|
|
) -> Result<ServerWithPeer> {
|
|
let cache = cache.address();
|
|
let setup = TestServer::with_peer(LOCAL_ASN, PEER_ASN, |c, peer| {
|
|
c.rpki = Some(RpkiConfig {
|
|
caches: vec![cache],
|
|
retry: Some(Duration::from_millis(100)),
|
|
..RpkiConfig::default()
|
|
});
|
|
peer.import = import;
|
|
peer.role = role;
|
|
})
|
|
.await?;
|
|
let rpki = setup.server.rpki.clone().expect("validation is configured");
|
|
wait_until("the server has the cache's data", || {
|
|
rpki.status().state == CacheState::Synchronised
|
|
})
|
|
.await?;
|
|
Ok(setup)
|
|
}
|
|
|
|
async fn established(server: &TestServer) -> Result<TestPeer> {
|
|
server.established(PEER_ASN, PEER_ID).await
|
|
}
|
|
|
|
/// Announces `prefixes` with the AS path `path`.
|
|
async fn announce(peer: &mut TestPeer, prefixes: &[&str], path: &[u32]) -> Result<()> {
|
|
peer.announce(announcement(prefixes, path)).await
|
|
}
|
|
|
|
/// AS 15169 buys from 3356 and says so; 3356 and the peer, 3333, have no
|
|
/// providers.
|
|
fn aspas() -> Vec<(u32, Vec<u32>)> {
|
|
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 ServerWithPeer { 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?;
|
|
server
|
|
.table_becomes(&["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::<Value, eyre::Report>(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 ServerWithPeer { 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 {
|
|
server.table_becomes(&["8.8.8.0/24"], 0).await
|
|
} else {
|
|
server.table_becomes(&[], 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 ServerWithPeer { 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?;
|
|
server.table_becomes(&["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 ServerWithPeer { 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?;
|
|
server.table_becomes(&["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);
|
|
server.table_becomes(&[], 2).await?;
|
|
let refused = &server.rejections.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]),
|
|
]);
|
|
server
|
|
.table_becomes(&["194.0.0.0/24", "8.8.8.0/24"], 0)
|
|
.await?;
|
|
let paths: BTreeSet<Vec<u32>> = 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]),
|
|
]);
|
|
server.table_becomes(&["194.0.0.0/24"], 1).await?;
|
|
cache.set_aspas(vec![]);
|
|
server
|
|
.table_becomes(&["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 ServerWithPeer { 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?;
|
|
server.table_becomes(&["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.rejections.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 ServerWithPeer { 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?;
|
|
server.table_becomes(&[], 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;
|
|
server.table_becomes(&[], 1).await?;
|
|
|
|
cache.set_aspas(vec![(15169, vec![3356]), (3356, vec![PEER_ASN])]);
|
|
server.table_becomes(&["8.8.8.0/24"], 0).await
|
|
}
|