The test files had grown their own copies of the same helpers: prefix() twelve times, established() seven, an UPDATE builder six, a seventeen field PeerConfig literal in fifteen places, a timeout constant in twenty one. Each is in crates/integration-tests/src once now: peer_config() and TestServer::with_peer for a configured peer, Announcement for building UPDATEs, established(), the waits (wait_until, wait_for, wait_for_routes, wait_for_sessions), get_json, and TIMEOUT. A new field on PeerConfig is one line to add, not fifteen. TestPeer::recv has a deadline, so a test that gets nothing fails instead of hanging; is_closed, which read one byte and took a KEEPALIVE for an open connection, is closed_within and closed_unanswered. Net 788 lines fewer; every test checks what it did. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
292 lines
11 KiB
Rust
292 lines
11 KiB
Rust
//! Route origin validation, end to end: a server that validates against a
|
|
//! cache, a peer that announces routes, and the validation data changing
|
|
//! under both.
|
|
|
|
use std::net::Ipv4Addr;
|
|
use std::time::Duration;
|
|
|
|
use bgp_integration_tests::announcement::{announcement, withdrawal};
|
|
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::{AsPathAttribute, PathAttribute};
|
|
use bgp_rpki::{CacheState, RpkiConfig, Vrp};
|
|
use bgp_server::policy::ImportPolicy;
|
|
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);
|
|
|
|
fn vrp(p: &str, max_length: u8, asn: u32) -> Vrp {
|
|
Vrp {
|
|
prefix: prefix(p),
|
|
max_length,
|
|
asn,
|
|
}
|
|
}
|
|
|
|
/// A server that validates against `cache`, with one configured peer at
|
|
/// the address the test connects from. Returns once the server has the
|
|
/// cache's data.
|
|
async fn server_with(
|
|
cache: &TestCache,
|
|
expire: Option<Duration>,
|
|
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],
|
|
// Data never expires sooner than a refresh and a retry take.
|
|
refresh: expire.map(|expire| expire / 2),
|
|
retry: Some(Duration::from_millis(100)),
|
|
expire,
|
|
});
|
|
peer.import = import;
|
|
})
|
|
.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` as originated by `origin`, reached through the
|
|
/// peer.
|
|
async fn announce(peer: &mut TestPeer, prefixes: &[&str], origin: u32) -> Result<()> {
|
|
let path = if origin == PEER_ASN {
|
|
vec![PEER_ASN]
|
|
} else {
|
|
vec![PEER_ASN, origin]
|
|
};
|
|
peer.announce(announcement(prefixes, &path)).await
|
|
}
|
|
|
|
/// A route whose origin is not the one the holder of the addresses
|
|
/// authorised is refused. One that is authorised is taken, and so is one
|
|
/// that nothing is known about.
|
|
#[tokio::test]
|
|
async fn test_an_invalid_route_is_refused_and_the_rest_are_taken() -> Result<()> {
|
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
|
let (base, _http) = server.serve_http().await?;
|
|
let mut peer = established(&server).await?;
|
|
|
|
announce(&mut peer, &["193.0.0.0/21", "194.0.0.0/24"], PEER_ASN).await?;
|
|
// Someone else's AS as the origin of a covered prefix; and the right
|
|
// AS, more specific than it may announce.
|
|
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
|
announce(&mut peer, &["193.0.6.0/25"], PEER_ASN).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?) }
|
|
};
|
|
// Each route says how it stands.
|
|
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["rpki"].as_str().unwrap()))
|
|
.collect();
|
|
assert_eq!(
|
|
validity,
|
|
[("193.0.0.0/21", "valid"), ("194.0.0.0/24", "not found")]
|
|
);
|
|
// The refused one is on record with its reason. The too-specific one
|
|
// was refused before validation came to it.
|
|
let refused = get("/api/routes/rejected").await?;
|
|
assert_eq!(
|
|
refused[0]["by_reason"],
|
|
serde_json::json!({"RPKI invalid": 1, "prefix too specific": 1})
|
|
);
|
|
let peers = get("/api/peers").await?;
|
|
assert_eq!(peers[0]["routes"], 2);
|
|
assert_eq!(peers[0]["held_routes"], 1);
|
|
|
|
let rpki = get("/api/rpki").await?;
|
|
assert_eq!(rpki["enabled"], true);
|
|
assert_eq!(rpki["state"], "synchronised");
|
|
assert_eq!(rpki["vrps_ipv4"], 1);
|
|
assert_eq!(rpki["cache"], cache.address().to_string());
|
|
Ok(())
|
|
}
|
|
|
|
/// Validity changes with the validation data, and the routes follow: one
|
|
/// that was taken goes when it becomes invalid, and one that was refused
|
|
/// as invalid is taken when it no longer is. The peer sends nothing more
|
|
/// meanwhile; it has said all it has to say.
|
|
#[tokio::test]
|
|
async fn test_routes_follow_the_validation_data() -> Result<()> {
|
|
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
|
let cache = TestCache::start(vec![ripe]).await;
|
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
|
let mut peer = established(&server).await?;
|
|
|
|
// Nothing is known about 8.8.8.0/24, so it is taken. 193.0.4.0/24 is
|
|
// covered, and not for this origin.
|
|
announce(&mut peer, &["8.8.8.0/24", "193.0.4.0/24"], 15169).await?;
|
|
server.table_becomes(&["8.8.8.0/24"], 1).await?;
|
|
|
|
// The holder of 8.8.8.0/24 authorises another AS: the route in the
|
|
// table is now invalid, and goes.
|
|
cache.set(vec![ripe, vrp("8.8.8.0/24", 24, 13335)]);
|
|
server.table_becomes(&[], 2).await?;
|
|
let refused = &server.refusals.peers()[0].1;
|
|
assert_eq!(refused.total(), 2);
|
|
assert_eq!(refused.recent()[0].prefix, prefix("8.8.8.0/24"));
|
|
assert_eq!(&refused.recent()[0].as_path[..], [PEER_ASN, 15169]);
|
|
|
|
// Both are authorised for the origin they have: both come back, with
|
|
// the attributes they were announced with.
|
|
cache.set(vec![
|
|
ripe,
|
|
vrp("8.8.8.0/24", 24, 15169),
|
|
vrp("193.0.4.0/24", 24, 15169),
|
|
]);
|
|
server
|
|
.table_becomes(&["8.8.8.0/24", "193.0.4.0/24"], 0)
|
|
.await?;
|
|
for route in server.received_routes() {
|
|
assert!(
|
|
route
|
|
.attributes
|
|
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN, 15169])),
|
|
"{route:?}"
|
|
);
|
|
}
|
|
assert_eq!(server.registry.list()[0].counters.routes(), 2);
|
|
Ok(())
|
|
}
|
|
|
|
/// A held route is the peer's to withdraw or replace like any other. One
|
|
/// it has withdrawn does not appear when it would have become valid.
|
|
#[tokio::test]
|
|
async fn test_a_held_route_the_peer_withdrew_does_not_come_back() -> Result<()> {
|
|
let ripe = vrp("193.0.0.0/21", 24, PEER_ASN);
|
|
let cache = TestCache::start(vec![ripe]).await;
|
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
|
let mut peer = established(&server).await?;
|
|
|
|
announce(&mut peer, &["193.0.4.0/24", "193.0.5.0/24"], 15169).await?;
|
|
server.table_becomes(&[], 2).await?;
|
|
|
|
// One is withdrawn, the other announced again with the right origin.
|
|
peer.announce(withdrawal([prefix("193.0.4.0/24").into()]))
|
|
.await?;
|
|
announce(&mut peer, &["193.0.5.0/24"], PEER_ASN).await?;
|
|
server.table_becomes(&["193.0.5.0/24"], 0).await?;
|
|
|
|
// Now the old origin would be valid for both. Neither old route is
|
|
// there to be brought back.
|
|
cache.set(vec![ripe, vrp("193.0.4.0/23", 24, 15169)]);
|
|
tokio::time::sleep(Duration::from_millis(500)).await;
|
|
server.table_becomes(&["193.0.5.0/24"], 0).await?;
|
|
assert!(
|
|
server.received_routes()[0]
|
|
.attributes
|
|
.contains(&AsPathAttribute::from_asns(vec![PEER_ASN])),
|
|
"the route the peer has now"
|
|
);
|
|
Ok(())
|
|
}
|
|
|
|
/// A peer can be exempt: its invalid routes are taken, and shown for what
|
|
/// they are.
|
|
#[tokio::test]
|
|
async fn test_a_peer_can_be_exempt_from_validation() -> Result<()> {
|
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
|
let import = ImportPolicy {
|
|
accept_rpki_invalid: true,
|
|
..default_policy()
|
|
};
|
|
let ServerWithPeer { server, .. } = server_with(&cache, None, import).await?;
|
|
let (base, _http) = server.serve_http().await?;
|
|
let mut peer = established(&server).await?;
|
|
|
|
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
|
server.table_becomes(&["193.0.4.0/24"], 0).await?;
|
|
let routes: Value = reqwest::get(format!("{base}/api/routes/received"))
|
|
.await?
|
|
.json()
|
|
.await?;
|
|
assert_eq!(routes["routes"][0]["rpki"], "invalid");
|
|
assert!(server.refusals.peers().is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// Validation data that no cache has confirmed for too long is dropped,
|
|
/// and with it every verdict: routes held as invalid are taken. Refusing
|
|
/// routes on the word of data that may be long out of date would be worse.
|
|
#[tokio::test]
|
|
async fn test_held_routes_are_taken_when_the_validation_data_expires() -> Result<()> {
|
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, PEER_ASN)]).await;
|
|
let ServerWithPeer { server, .. } =
|
|
server_with(&cache, Some(Duration::from_millis(1500)), default_policy()).await?;
|
|
let mut peer = established(&server).await?;
|
|
announce(&mut peer, &["193.0.4.0/24"], 15169).await?;
|
|
server.table_becomes(&[], 1).await?;
|
|
|
|
drop(cache);
|
|
server.table_becomes(&["193.0.4.0/24"], 0).await?;
|
|
assert!(server.rpki.as_ref().unwrap().current().vrps.is_empty());
|
|
Ok(())
|
|
}
|
|
|
|
/// A hijack that was possible: on a session with two-octet AS numbers a
|
|
/// route has two paths, AS_PATH and AS4_PATH, and a peer can make them
|
|
/// say different things. Here AS_PATH is the peer alone, and an AS4_PATH
|
|
/// too long to be used ends at the AS that holds the addresses. The route
|
|
/// is the peer's and invalid, when it arrives and when it is looked at
|
|
/// again; nothing may take the unused path's word for where it comes from.
|
|
#[tokio::test]
|
|
async fn test_an_unused_as4_path_does_not_make_a_route_valid() -> Result<()> {
|
|
use bgp_packet::message::AsPathSegment;
|
|
const HOLDER: u32 = 15169;
|
|
let cache = TestCache::start(vec![vrp("193.0.0.0/21", 24, HOLDER)]).await;
|
|
let ServerWithPeer { server, .. } = server_with(&cache, None, default_policy()).await?;
|
|
|
|
// No capabilities: AS numbers are two octets wide on this session.
|
|
let mut peer = server.established_with(PEER_ASN, PEER_ID, vec![]).await?;
|
|
assert!(!server.registry.list()[0].four_octet_asn);
|
|
|
|
let as4_path = PathAttribute::As4Path(AsPathAttribute {
|
|
segments: vec![AsPathSegment {
|
|
ordered: true,
|
|
path: vec![3356, HOLDER],
|
|
}],
|
|
});
|
|
peer.announce(announcement(&["193.0.4.0/24"], &[PEER_ASN]).with_attributes([as4_path]))
|
|
.await?;
|
|
server.table_becomes(&[], 1).await?;
|
|
|
|
// The data changes, and every held route is looked at again.
|
|
cache.set(vec![
|
|
vrp("193.0.0.0/21", 24, HOLDER),
|
|
vrp("198.51.100.0/24", 24, 64500),
|
|
]);
|
|
let rpki = server.rpki.clone().unwrap();
|
|
wait_until("the server has the new data", || {
|
|
rpki.current().vrps.len() == 2
|
|
})
|
|
.await?;
|
|
// Time enough for the look, and then some.
|
|
tokio::time::sleep(Duration::from_millis(600)).await;
|
|
server.table_becomes(&[], 1).await?;
|
|
Ok(())
|
|
}
|