rpki/tests/test_session.rs
2026-07-24 11:10:32 +08:00

2068 lines
71 KiB
Rust

mod common;
use std::collections::VecDeque;
use std::fs::File;
use std::io::BufReader;
use std::net::{Ipv4Addr, Ipv6Addr, SocketAddr};
use std::path::{Path, PathBuf};
use std::sync::Arc;
use arc_swap::ArcSwap;
use rustls::{ClientConfig, RootCertStore};
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use serde_json::json;
use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, watch};
use tokio::task::JoinHandle;
use tokio::time::{Duration, timeout};
use tokio_rustls::{TlsAcceptor, TlsConnector};
use common::test_helper::{
RtrDebugDumper, dump_cache_reset, dump_cache_response, dump_eod_v1, dump_ipv4_prefix,
dump_ipv6_prefix,
};
use rpki::data_model::resources::as_resources::Asn;
use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
use rpki::rtr::cache::{Delta, RtrCacheBuilder, SessionIds, SharedRtrCache, Snapshot};
use rpki::rtr::error_type::ErrorCode;
use rpki::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski, Timing};
use rpki::rtr::pdu::{
Aspa as AspaPdu, CacheReset, CacheResponse, EndOfDataV1, ErrorReport, Header, IPv4Prefix,
IPv6Prefix, ResetQuery, RouterKey as RouterKeyPdu, SerialNotify, SerialQuery,
};
use rpki::rtr::server::connection::handle_tls_connection;
use rpki::rtr::server::tls::load_rustls_server_config_with_options;
use rpki::rtr::server::{RtrServiceControl, RtrShutdownReason};
use rpki::rtr::session::RtrSession;
use rpki::rtr::store::RtrStore;
fn shared_cache(cache: rpki::rtr::cache::RtrCache) -> SharedRtrCache {
Arc::new(ArcSwap::from_pointee(cache))
}
async fn start_session_server(
cache: SharedRtrCache,
) -> (
SocketAddr,
broadcast::Sender<()>,
watch::Sender<RtrServiceControl>,
JoinHandle<()>,
) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (notify_tx, notify_rx) = broadcast::channel(16);
let (shutdown_tx, shutdown_rx) = watch::channel(RtrServiceControl::Running);
let handle = tokio::spawn(async move {
let Ok((stream, _)) = listener.accept().await else {
return;
};
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx);
let _ = session.run().await;
});
(addr, notify_tx, shutdown_tx, handle)
}
async fn start_session_server_with_transport_timeout(
cache: SharedRtrCache,
transport_timeout: Duration,
) -> (
SocketAddr,
broadcast::Sender<()>,
watch::Sender<RtrServiceControl>,
JoinHandle<()>,
) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (notify_tx, notify_rx) = broadcast::channel(16);
let (shutdown_tx, shutdown_rx) = watch::channel(RtrServiceControl::Running);
let handle = tokio::spawn(async move {
let Ok((stream, _)) = listener.accept().await else {
return;
};
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx)
.with_transport_timeout(transport_timeout);
let _ = session.run().await;
});
(addr, notify_tx, shutdown_tx, handle)
}
async fn start_session_server_returning_result(
cache: SharedRtrCache,
) -> (
SocketAddr,
watch::Sender<RtrServiceControl>,
JoinHandle<anyhow::Result<()>>,
) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (_notify_tx, notify_rx) = broadcast::channel(16);
let (shutdown_tx, shutdown_rx) = watch::channel(RtrServiceControl::Running);
let handle = tokio::spawn(async move {
let (stream, _) = listener.accept().await.unwrap();
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx);
session.run().await
});
(addr, shutdown_tx, handle)
}
async fn start_tls_session_server(
cache: SharedRtrCache,
) -> (SocketAddr, watch::Sender<RtrServiceControl>, JoinHandle<()>) {
start_tls_session_server_with_cert(cache, "server.crt", "server.key").await
}
async fn start_tls_session_server_with_cert(
cache: SharedRtrCache,
cert_name: &str,
key_name: &str,
) -> (SocketAddr, watch::Sender<RtrServiceControl>, JoinHandle<()>) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let addr = listener.local_addr().unwrap();
let (_notify_tx, notify_rx) = broadcast::channel(16);
let (shutdown_tx, shutdown_rx) = watch::channel(RtrServiceControl::Running);
let tls_config = Arc::new(
load_rustls_server_config_with_options(
fixture_path(cert_name),
fixture_path(key_name),
fixture_path("client-ca.crt"),
false,
)
.unwrap(),
);
let acceptor = TlsAcceptor::from(tls_config);
let handle = tokio::spawn(async move {
let Ok((stream, peer_addr)) = listener.accept().await else {
return;
};
let _ = handle_tls_connection(
cache,
stream,
peer_addr,
acceptor,
true,
notify_rx,
shutdown_rx,
None,
)
.await;
});
(addr, shutdown_tx, handle)
}
async fn shutdown_server(
mut client: TcpStream,
shutdown_tx: watch::Sender<RtrServiceControl>,
server_handle: JoinHandle<()>,
) {
shutdown_io(&mut client, shutdown_tx, server_handle).await;
}
async fn shutdown_io<S>(
io: &mut S,
shutdown_tx: watch::Sender<RtrServiceControl>,
server_handle: JoinHandle<()>,
) where
S: AsyncWrite + Unpin,
{
let _ = io.shutdown().await;
let _ = shutdown_tx.send(RtrServiceControl::Shutdown(RtrShutdownReason::CloseOnly));
match timeout(Duration::from_secs(1), server_handle).await {
Ok(join_res) => {
let _ = join_res;
}
Err(_) => {
panic!("server task did not exit within timeout");
}
}
}
fn fixture_path(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests")
.join("fixtures")
.join("tls")
.join(name)
}
fn load_pem_certs(path: &Path) -> Vec<CertificateDer<'static>> {
let file = File::open(path).unwrap();
let mut reader = BufReader::new(file);
rustls_pemfile::certs(&mut reader)
.collect::<Result<Vec<_>, _>>()
.unwrap()
}
fn load_pem_key(path: &Path) -> PrivateKeyDer<'static> {
let file = File::open(path).unwrap();
let mut reader = BufReader::new(file);
rustls_pemfile::private_key(&mut reader)
.unwrap()
.expect("missing PEM private key")
}
async fn connect_tls_client(
addr: SocketAddr,
cert_name: &str,
key_name: &str,
) -> tokio_rustls::client::TlsStream<TcpStream> {
connect_tls_client_with_server_name(
addr,
cert_name,
key_name,
ServerName::IpAddress(addr.ip().into()),
)
.await
}
async fn connect_tls_client_with_server_name(
addr: SocketAddr,
cert_name: &str,
key_name: &str,
server_name: ServerName<'static>,
) -> tokio_rustls::client::TlsStream<TcpStream> {
let mut roots = RootCertStore::empty();
for cert in load_pem_certs(&fixture_path("client-ca.crt")) {
roots.add(cert).unwrap();
}
let certs = load_pem_certs(&fixture_path(cert_name));
let key = load_pem_key(&fixture_path(key_name));
let client_config = ClientConfig::builder()
.with_root_certificates(roots)
.with_client_auth_cert(certs, key)
.unwrap();
let connector = TlsConnector::from(Arc::new(client_config));
let tcp = TcpStream::connect(addr).await.unwrap();
connector.connect(server_name, tcp).await.unwrap()
}
/// Dump Serial Notify in the same style as the other test helpers.
fn dump_serial_notify(notify: &SerialNotify) -> serde_json::Value {
json!({
"version": notify.version(),
"pdu": notify.pdu(),
"pdu_name": "Serial Notify",
"session_id": notify.session_id(),
"serial_number": notify.serial_number(),
})
}
fn assert_error_report_matches(
report: &ErrorReport,
version: u8,
code: ErrorCode,
offending_pdu: &[u8],
) {
assert_eq!(report.version(), version);
assert_eq!(report.error_code(), Ok(code));
assert_eq!(report.erroneous_pdu(), offending_pdu);
}
fn serials_all(serial: u32) -> [u32; 3] {
[serial; 3]
}
fn snapshots_all(snapshot: Snapshot) -> [Snapshot; 3] {
[snapshot.clone(), snapshot.clone(), snapshot]
}
fn deltas_all(deltas: VecDeque<Arc<Delta>>) -> [VecDeque<Arc<Delta>>; 3] {
[deltas.clone(), deltas.clone(), deltas]
}
async fn wait_for_store_serials(store: &RtrStore, expected: [u32; 3]) {
for _ in 0..100 {
let current = [
store.get_serial_for_version(0).unwrap(),
store.get_serial_for_version(1).unwrap(),
store.get_serial_for_version(2).unwrap(),
];
if current == [Some(expected[0]), Some(expected[1]), Some(expected[2])] {
return;
}
tokio::time::sleep(Duration::from_millis(20)).await;
}
panic!(
"timed out waiting store serials {:?}, current={:?}",
expected,
[
store.get_serial_for_version(0).unwrap(),
store.get_serial_for_version(1).unwrap(),
store.get_serial_for_version(2).unwrap(),
]
);
}
/// Reset Query returns a full snapshot and finishes with End of Data.
#[tokio::test]
async fn reset_query_returns_snapshot_and_end_of_data() {
let prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let origin = RouteOrigin::new(prefix, 24, 64496u32.into());
let snapshot = Snapshot::from_payloads(vec![Payload::RouteOrigin(origin)]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.pdu(), 3);
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let prefix = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(prefix.pdu(), dump_ipv4_prefix(&prefix));
assert_eq!(prefix.pdu(), 4);
assert_eq!(prefix.version(), 1);
assert!(prefix.flag().is_announce());
assert_eq!(prefix.prefix_len(), 24);
assert_eq!(prefix.max_len(), 24);
assert_eq!(prefix.prefix(), Ipv4Addr::new(192, 0, 2, 0));
assert_eq!(prefix.asn(), 64496u32.into());
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.pdu(), 7);
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
let timing = eod.timing();
assert_eq!(timing.refresh, 600);
assert_eq!(timing.retry, 600);
assert_eq!(timing.expire, 7200);
dump.print_pretty("reset_query_returns_snapshot_and_end_of_data");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn reset_query_uses_version_specific_session_id() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([40, 41, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(2).write(&mut client).await.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.version(), 2);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.version(), 2);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
dump.print_pretty("reset_query_uses_version_specific_session_id");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn version_two_cache_restart_shutdown_sends_error_code_12() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([40, 41, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(2).write(&mut client).await.unwrap();
let _response = CacheResponse::read(&mut client).await.unwrap();
let _eod = EndOfDataV1::read(&mut client).await.unwrap();
shutdown_tx
.send(RtrServiceControl::Shutdown(RtrShutdownReason::CacheRestart))
.unwrap();
let report = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(&report, 2, ErrorCode::CacheRestart, &[]);
assert!(String::from_utf8_lossy(report.text()).contains("restarting"));
timeout(Duration::from_secs(1), server_handle)
.await
.expect("server task did not exit within timeout")
.unwrap();
}
#[tokio::test]
async fn version_one_cache_restart_shutdown_does_not_send_v2_error_code() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([40, 41, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let _response = CacheResponse::read(&mut client).await.unwrap();
let _eod = EndOfDataV1::read(&mut client).await.unwrap();
shutdown_tx
.send(RtrServiceControl::Shutdown(RtrShutdownReason::CacheRestart))
.unwrap();
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.expect("timed out waiting for session close");
assert!(
read_res.is_err(),
"v1 session must not receive v2 error code 12"
);
timeout(Duration::from_secs(1), server_handle)
.await
.expect("server task did not exit within timeout")
.unwrap();
}
#[tokio::test]
async fn restart_restores_versioned_state_and_serves_queries() {
let dir = tempfile::tempdir().unwrap();
let store = RtrStore::open(dir.path()).unwrap();
let prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let origin = Payload::RouteOrigin(RouteOrigin::new(prefix, 24, 64496u32.into()));
let valid_spki = vec![
0x30, 0x13, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
];
let router_key = Payload::RouterKey(RouterKey::new(
Ski::default(),
Asn::from(64496u32),
valid_spki.clone(),
));
let aspa_old = Payload::Aspa(Aspa::new(Asn::from(64496u32), vec![Asn::from(64497u32)]));
let aspa_new = Payload::Aspa(Aspa::new(
Asn::from(64496u32),
vec![Asn::from(64497u32), Asn::from(64498u32)],
));
let initial_payloads = vec![origin.clone(), router_key.clone(), aspa_old];
let mut cache = rpki::rtr::cache::RtrCache::default()
.init(&store, 16, false, Timing::new(600, 600, 7200), || {
Ok(initial_payloads.clone())
})
.unwrap();
cache
.update(vec![origin.clone(), router_key.clone(), aspa_new], &store)
.unwrap();
wait_for_store_serials(&store, [1, 1, 2]).await;
let restored = rpki::rtr::cache::RtrCache::default()
.init(&store, 16, false, Timing::new(600, 600, 7200), || {
panic!("file loader should not be used when restoring from store")
})
.unwrap();
assert_eq!(restored.serial_for_version(0), 1);
assert_eq!(restored.serial_for_version(1), 1);
assert_eq!(restored.serial_for_version(2), 2);
let shared = shared_cache(restored);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(shared.clone()).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(0).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 0);
let expected_sid_v0 = shared.load_full().session_id_for_version(0);
assert_eq!(response.session_id(), expected_sid_v0);
let _v4 = IPv4Prefix::read(&mut client).await.unwrap();
let eod_v0 = rpki::rtr::pdu::EndOfDataV0::read(&mut client)
.await
.unwrap();
assert_eq!(eod_v0.serial_number(), 1);
shutdown_server(client, shutdown_tx, server_handle).await;
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(shared.clone()).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 1);
let expected_sid_v1 = shared.load_full().session_id_for_version(1);
assert_eq!(response.session_id(), expected_sid_v1);
let _v4 = IPv4Prefix::read(&mut client).await.unwrap();
let _rk = RouterKeyPdu::read(&mut client).await.unwrap();
let eod_v1 = EndOfDataV1::read(&mut client).await.unwrap();
assert_eq!(eod_v1.serial_number(), 1);
shutdown_server(client, shutdown_tx, server_handle).await;
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(shared.clone()).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let sid_v2 = shared.load_full().session_id_for_version(2);
SerialQuery::new(2, sid_v2, 1)
.write(&mut client)
.await
.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 2);
assert_eq!(response.session_id(), sid_v2);
let _aspa = AspaPdu::read(&mut client).await.unwrap();
let eod_v2 = EndOfDataV1::read(&mut client).await.unwrap();
assert_eq!(eod_v2.serial_number(), 2);
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Serial Query returns only End of Data when the client is up to date.
#[tokio::test]
async fn serial_query_returns_end_of_data_when_up_to_date() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing {
refresh: 600,
retry: 600,
expire: 7200,
})
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 42, 100)
.write(&mut client)
.await
.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.pdu(), 3);
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.pdu(), 7);
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
let timing = eod.timing();
assert_eq!(timing.refresh, 600);
assert_eq!(timing.retry, 600);
assert_eq!(timing.expire, 7200);
dump.print_pretty("serial_query_returns_end_of_data_when_up_to_date");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Established sessions reject mismatched Session IDs with Corrupt Data.
#[tokio::test]
async fn serial_query_returns_corrupt_data_when_session_id_mismatch() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing {
refresh: 600,
retry: 600,
expire: 7200,
})
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 999, 100)
.write(&mut client)
.await
.unwrap();
let mut dump = RtrDebugDumper::new();
let report = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&report,
1,
ErrorCode::CorruptData,
SerialQuery::new(1, 999, 100).as_ref(),
);
let read_res = Header::read(&mut client).await;
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_corrupt_session_id",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("serial_query_returns_corrupt_data_when_session_id_mismatch");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Serial Query returns Cache Response, delta payloads, and End of Data when incremental updates exist.
#[tokio::test]
async fn serial_query_returns_deltas_when_incremental_update_available() {
let prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let origin = RouteOrigin::new(prefix, 24, 64496u32.into());
let delta = Arc::new(Delta::new(101, vec![Payload::RouteOrigin(origin)], vec![]));
let mut deltas = VecDeque::new();
deltas.push_back(delta);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(101))
.timing(Timing {
refresh: 600,
retry: 600,
expire: 7200,
})
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 42, 100)
.write(&mut client)
.await
.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.pdu(), 3);
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let prefix = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(prefix.pdu(), dump_ipv4_prefix(&prefix));
assert_eq!(prefix.pdu(), 4);
assert_eq!(prefix.version(), 1);
assert!(prefix.flag().is_announce());
assert_eq!(prefix.prefix_len(), 24);
assert_eq!(prefix.max_len(), 24);
assert_eq!(prefix.prefix(), Ipv4Addr::new(192, 0, 2, 0));
assert_eq!(prefix.asn(), 64496u32.into());
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.pdu(), 7);
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 101);
let timing = eod.timing();
assert_eq!(timing.refresh, 600);
assert_eq!(timing.retry, 600);
assert_eq!(timing.expire, 7200);
dump.print_pretty("serial_query_returns_deltas_when_incremental_update_available");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn serial_query_returns_deltas_across_serial_wraparound() {
let first_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let first_origin = RouteOrigin::new(first_prefix, 24, 64496u32.into());
let second_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(198, 51, 100, 0)),
prefix_length: 24,
};
let second_origin = RouteOrigin::new(second_prefix, 24, 64497u32.into());
let d_max = Arc::new(Delta::new(
u32::MAX,
vec![Payload::RouteOrigin(first_origin.clone())],
vec![],
));
let d_zero = Arc::new(Delta::new(
0,
vec![Payload::RouteOrigin(second_origin.clone())],
vec![],
));
let mut deltas = VecDeque::new();
deltas.push_back(d_max);
deltas.push_back(d_zero);
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(first_origin),
Payload::RouteOrigin(second_origin),
]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(0))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 42, u32::MAX.wrapping_sub(1))
.write(&mut client)
.await
.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let first = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(first.pdu(), dump_ipv4_prefix(&first));
assert!(first.flag().is_announce());
assert_eq!(first.prefix(), Ipv4Addr::new(198, 51, 100, 0));
let second = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(second.pdu(), dump_ipv4_prefix(&second));
assert!(second.flag().is_announce());
assert_eq!(second.prefix(), Ipv4Addr::new(192, 0, 2, 0));
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 0);
dump.print_pretty("serial_query_returns_deltas_across_serial_wraparound");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn serial_query_returns_cache_reset_for_future_serial_across_wraparound() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(u32::MAX))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 42, 0).write(&mut client).await.unwrap();
let mut dump = RtrDebugDumper::new();
let reset = CacheReset::read(&mut client).await.unwrap();
dump.push_value(reset.pdu(), dump_cache_reset(reset.version(), reset.pdu()));
assert_eq!(reset.pdu(), CacheReset::PDU);
assert_eq!(reset.version(), 1);
dump.print_pretty("serial_query_returns_cache_reset_for_future_serial_across_wraparound");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Reset Query returns payloads in RTR ordering.
#[tokio::test]
async fn reset_query_returns_payloads_in_rtr_order() {
let v4_low_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let v4_low_origin = RouteOrigin::new(v4_low_prefix, 24, 64496u32.into());
let v4_high_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(198, 51, 100, 0)),
prefix_length: 24,
};
let v4_high_origin = RouteOrigin::new(v4_high_prefix, 24, 64497u32.into());
let v6_prefix = IPAddressPrefix {
address: IPAddress::from_ipv6(Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)),
prefix_length: 32,
};
let v6_origin = RouteOrigin::new(v6_prefix, 48, 64498u32.into());
let snapshot = Snapshot::from_payloads(vec![
Payload::RouteOrigin(v6_origin),
Payload::RouteOrigin(v4_low_origin),
Payload::RouteOrigin(v4_high_origin),
]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.pdu(), 3);
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let first = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(first.pdu(), dump_ipv4_prefix(&first));
assert_eq!(first.pdu(), 4);
assert_eq!(first.version(), 1);
assert!(first.flag().is_announce());
assert_eq!(first.prefix(), Ipv4Addr::new(198, 51, 100, 0));
assert_eq!(first.prefix_len(), 24);
assert_eq!(first.max_len(), 24);
assert_eq!(first.asn(), 64497u32.into());
let second = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(second.pdu(), dump_ipv4_prefix(&second));
assert_eq!(second.pdu(), 4);
assert_eq!(second.version(), 1);
assert!(second.flag().is_announce());
assert_eq!(second.prefix(), Ipv4Addr::new(192, 0, 2, 0));
assert_eq!(second.prefix_len(), 24);
assert_eq!(second.max_len(), 24);
assert_eq!(second.asn(), 64496u32.into());
assert!(u32::from(first.prefix()) > u32::from(second.prefix()));
let third = IPv6Prefix::read(&mut client).await.unwrap();
dump.push_value(third.pdu(), dump_ipv6_prefix(&third));
assert_eq!(third.pdu(), 6);
assert_eq!(third.version(), 1);
assert!(third.flag().is_announce());
assert_eq!(
third.prefix(),
Ipv6Addr::new(0x2001, 0xdb8, 0, 0, 0, 0, 0, 0)
);
assert_eq!(third.prefix_len(), 32);
assert_eq!(third.max_len(), 48);
assert_eq!(third.asn(), 64498u32.into());
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.pdu(), 7);
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
let timing = eod.timing();
assert_eq!(timing.refresh, 600);
assert_eq!(timing.retry, 600);
assert_eq!(timing.expire, 7200);
dump.print_pretty("reset_query_returns_payloads_in_rtr_order");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Serial Query returns announcements before withdrawals, with each side sorted.
#[tokio::test]
async fn serial_query_returns_announcements_before_withdrawals() {
let announced_low_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)),
prefix_length: 24,
};
let announced_low_origin = RouteOrigin::new(announced_low_prefix, 24, 64496u32.into());
let announced_high_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(198, 51, 100, 0)),
prefix_length: 24,
};
let announced_high_origin = RouteOrigin::new(announced_high_prefix, 24, 64497u32.into());
let withdrawn_low_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(10, 0, 0, 0)),
prefix_length: 24,
};
let withdrawn_low_origin = RouteOrigin::new(withdrawn_low_prefix, 24, 64500u32.into());
let withdrawn_high_prefix = IPAddressPrefix {
address: IPAddress::from_ipv4(Ipv4Addr::new(203, 0, 113, 0)),
prefix_length: 24,
};
let withdrawn_high_origin = RouteOrigin::new(withdrawn_high_prefix, 24, 64501u32.into());
let delta = Arc::new(Delta::new(
101,
vec![
Payload::RouteOrigin(announced_low_origin),
Payload::RouteOrigin(announced_high_origin),
],
vec![
Payload::RouteOrigin(withdrawn_high_origin),
Payload::RouteOrigin(withdrawn_low_origin),
],
));
let mut deltas = VecDeque::new();
deltas.push_back(delta);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(101))
.timing(Timing {
refresh: 600,
retry: 600,
expire: 7200,
})
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 42, 100)
.write(&mut client)
.await
.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.pdu(), 3);
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let first = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(first.pdu(), dump_ipv4_prefix(&first));
assert_eq!(first.pdu(), 4);
assert_eq!(first.version(), 1);
assert!(first.flag().is_announce());
assert_eq!(first.prefix(), Ipv4Addr::new(198, 51, 100, 0));
assert_eq!(first.prefix_len(), 24);
assert_eq!(first.max_len(), 24);
assert_eq!(first.asn(), 64497u32.into());
let second = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(second.pdu(), dump_ipv4_prefix(&second));
assert_eq!(second.pdu(), 4);
assert_eq!(second.version(), 1);
assert!(second.flag().is_announce());
assert_eq!(second.prefix(), Ipv4Addr::new(192, 0, 2, 0));
assert_eq!(second.prefix_len(), 24);
assert_eq!(second.max_len(), 24);
assert_eq!(second.asn(), 64496u32.into());
assert!(u32::from(first.prefix()) > u32::from(second.prefix()));
let third = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(third.pdu(), dump_ipv4_prefix(&third));
assert_eq!(third.pdu(), 4);
assert_eq!(third.version(), 1);
assert!(!third.flag().is_announce());
assert_eq!(third.prefix(), Ipv4Addr::new(10, 0, 0, 0));
assert_eq!(third.prefix_len(), 24);
assert_eq!(third.max_len(), 24);
assert_eq!(third.asn(), 64500u32.into());
let fourth = IPv4Prefix::read(&mut client).await.unwrap();
dump.push_value(fourth.pdu(), dump_ipv4_prefix(&fourth));
assert_eq!(fourth.pdu(), 4);
assert_eq!(fourth.version(), 1);
assert!(!fourth.flag().is_announce());
assert_eq!(fourth.prefix(), Ipv4Addr::new(203, 0, 113, 0));
assert_eq!(fourth.prefix_len(), 24);
assert_eq!(fourth.max_len(), 24);
assert_eq!(fourth.asn(), 64501u32.into());
assert!(u32::from(third.prefix()) < u32::from(fourth.prefix()));
assert!(first.flag().is_announce());
assert!(second.flag().is_announce());
assert!(!third.flag().is_announce());
assert!(!fourth.flag().is_announce());
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.pdu(), 7);
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 101);
let timing = eod.timing();
assert_eq!(timing.refresh, 600);
assert_eq!(timing.retry, 600);
assert_eq!(timing.expire, 7200);
dump.print_pretty("serial_query_returns_announcements_before_withdrawals");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Established sessions send Serial Notify after receiving cache update notifications.
#[tokio::test]
async fn established_session_sends_serial_notify() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
notify_tx.send(()).unwrap();
let notify = SerialNotify::read(&mut client).await.unwrap();
dump.push_value(notify.pdu(), dump_serial_notify(&notify));
assert_eq!(notify.pdu(), SerialNotify::PDU);
assert_eq!(notify.version(), 1);
assert_eq!(notify.session_id(), 42);
assert_eq!(notify.serial_number(), 100);
dump.print_pretty("established_session_sends_serial_notify");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// A too-high first-PDU protocol version returns Unsupported Protocol Version.
#[tokio::test]
async fn first_pdu_with_too_high_version_returns_unsupported_version_error() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(3).write(&mut client).await.unwrap();
let report = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&report,
2,
ErrorCode::UnsupportedProtocolVersion,
ResetQuery::new(3).as_ref(),
);
let read_res = Header::read(&mut client).await;
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_unsupported_protocol_version",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("first_pdu_with_too_high_version_returns_unsupported_version_error");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Established sessions reject later protocol version changes.
#[tokio::test]
async fn session_rejects_version_change_after_negotiation() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
ResetQuery::new(2).write(&mut client).await.unwrap();
let report = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&report,
1,
ErrorCode::UnexpectedProtocolVersion,
ResetQuery::new(2).as_ref(),
);
let read_res = Header::read(&mut client).await;
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_unexpected_protocol_version",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("session_rejects_version_change_after_negotiation");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Notifications before version negotiation do not send Serial Notify.
#[tokio::test]
async fn notify_is_not_sent_before_version_negotiation() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
notify_tx.send(()).unwrap();
let res = timeout(Duration::from_millis(100), SerialNotify::read(&mut client)).await;
assert!(
res.is_err(),
"serial notify should not be sent before version negotiation"
);
dump.push_value(
0,
json!({
"event": "notify_before_version_negotiation",
"result": "no_serial_notify_received_within_timeout",
"timeout_ms": 100
}),
);
dump.print_pretty("notify_is_not_sent_before_version_negotiation");
shutdown_server(client, shutdown_tx, server_handle).await;
}
/// Serial Notify is rate-limited to once per minute per session.
#[tokio::test]
async fn serial_notify_is_rate_limited_to_once_per_minute() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let mut dump = RtrDebugDumper::new();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.pdu(), 3);
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.pdu(), 7);
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
notify_tx.send(()).unwrap();
let first_notify = SerialNotify::read(&mut client).await.unwrap();
dump.push_value(first_notify.pdu(), dump_serial_notify(&first_notify));
assert_eq!(first_notify.pdu(), SerialNotify::PDU);
assert_eq!(first_notify.version(), 1);
assert_eq!(first_notify.session_id(), 42);
assert_eq!(first_notify.serial_number(), 100);
notify_tx.send(()).unwrap();
let second_res = timeout(Duration::from_millis(100), SerialNotify::read(&mut client)).await;
assert!(
second_res.is_err(),
"second serial notify should be rate-limited within one minute"
);
dump.push_value(
0,
json!({
"event": "second_notify_within_rate_limit_window",
"result": "no_second_serial_notify_received_within_timeout",
"timeout_ms": 100
}),
);
dump.print_pretty("serial_notify_is_rate_limited_to_once_per_minute");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn reset_query_returns_no_data_available_when_cache_is_unavailable() {
let cache = RtrCacheBuilder::new()
.availability(rpki::rtr::cache::CacheAvailability::NoDataAvailable)
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let report = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&report,
1,
ErrorCode::NoDataAvailable,
ResetQuery::new(1).as_ref(),
);
ResetQuery::new(1).write(&mut client).await.unwrap();
let second = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&second,
1,
ErrorCode::NoDataAvailable,
ResetQuery::new(1).as_ref(),
);
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn serial_query_returns_no_data_available_when_cache_is_unavailable() {
let cache = RtrCacheBuilder::new()
.availability(rpki::rtr::cache::CacheAvailability::NoDataAvailable)
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
SerialQuery::new(1, 42, 100)
.write(&mut client)
.await
.unwrap();
let report = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&report,
1,
ErrorCode::NoDataAvailable,
SerialQuery::new(1, 42, 100).as_ref(),
);
SerialQuery::new(1, 42, 100)
.write(&mut client)
.await
.unwrap();
let second = ErrorReport::read(&mut client).await.unwrap();
assert_error_report_matches(
&second,
1,
ErrorCode::NoDataAvailable,
SerialQuery::new(1, 42, 100).as_ref(),
);
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn first_pdu_with_invalid_length_returns_corrupt_data() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, _shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
let header = Header::new(1, ResetQuery::PDU, 0, 9);
let mut request = Vec::from(header.as_ref());
request.push(0);
timeout(Duration::from_secs(1), client.write_all(&request))
.await
.expect("write_all timed out")
.unwrap();
dump.push_value(
ResetQuery::PDU,
json!({
"event": "invalid_first_pdu_sent",
"raw_hex": common::test_helper::bytes_to_hex(&request),
"length": request.len(),
}),
);
let report = timeout(Duration::from_secs(1), ErrorReport::read(&mut client))
.await
.expect("timed out waiting for ErrorReport")
.unwrap();
dump.push_value(
ErrorReport::PDU,
json!({
"version": report.version(),
"pdu": ErrorReport::PDU,
"pdu_name": "Error Report",
"error_code": report.error_code().map(|code| code.as_u16()).unwrap_or_else(|code| code),
"erroneous_pdu_len": report.erroneous_pdu().len(),
"erroneous_pdu_hex": common::test_helper::bytes_to_hex(report.erroneous_pdu()),
"text": String::from_utf8_lossy(report.text()),
}),
);
assert_error_report_matches(&report, 1, ErrorCode::CorruptData, &request);
assert!(
std::str::from_utf8(report.text())
.unwrap()
.contains("invalid length")
);
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.expect("timed out waiting for connection close");
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_invalid_first_pdu",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("first_pdu_with_invalid_length_returns_corrupt_data");
drop(client);
server_handle.abort();
let _ = server_handle.await;
}
#[tokio::test]
async fn established_session_closes_after_receiving_error_report() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
let report = ErrorReport::new(1, ErrorCode::InternalError.as_u16(), [], b"peer error");
client.write_all(report.as_ref()).await.unwrap();
dump.push_value(
ErrorReport::PDU,
json!({
"event": "peer_error_report_sent",
"version": report.version(),
"error_code": report.error_code().map(|code| code.as_u16()).unwrap_or_else(|code| code),
"text": String::from_utf8_lossy(report.text()),
}),
);
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.unwrap();
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_receiving_peer_error_report",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("established_session_closes_after_receiving_error_report");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn established_session_invalid_header_returns_corrupt_data() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
let invalid_header = Header::new(1, SerialQuery::PDU, 42, 7);
client.write_all(invalid_header.as_ref()).await.unwrap();
dump.push_value(
SerialQuery::PDU,
json!({
"event": "invalid_header_sent_after_establishment",
"raw_hex": common::test_helper::bytes_to_hex(invalid_header.as_ref()),
"length": invalid_header.length(),
}),
);
let report = ErrorReport::read(&mut client).await.unwrap();
dump.push_value(
ErrorReport::PDU,
json!({
"version": report.version(),
"pdu": ErrorReport::PDU,
"pdu_name": "Error Report",
"error_code": report.error_code().map(|code| code.as_u16()).unwrap_or_else(|code| code),
"erroneous_pdu_len": report.erroneous_pdu().len(),
"erroneous_pdu_hex": common::test_helper::bytes_to_hex(report.erroneous_pdu()),
"text": String::from_utf8_lossy(report.text()),
}),
);
assert_error_report_matches(&report, 1, ErrorCode::CorruptData, invalid_header.as_ref());
assert!(
std::str::from_utf8(report.text())
.unwrap()
.contains("invalid PDU length")
);
let read_res = Header::read(&mut client).await;
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_invalid_established_header",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("established_session_invalid_header_returns_corrupt_data");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn established_session_unknown_pdu_returns_unsupported_pdu_type() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
let unknown_pdu = Header::new(1, 12, 0, 8);
client.write_all(unknown_pdu.as_ref()).await.unwrap();
dump.push_value(
12,
json!({
"event": "unknown_pdu_sent_after_establishment",
"raw_hex": common::test_helper::bytes_to_hex(unknown_pdu.as_ref()),
"length": unknown_pdu.length(),
}),
);
let report = ErrorReport::read(&mut client).await.unwrap();
dump.push_value(
ErrorReport::PDU,
json!({
"version": report.version(),
"pdu": ErrorReport::PDU,
"pdu_name": "Error Report",
"error_code": report.error_code().map(|code| code.as_u16()).unwrap_or_else(|code| code),
"erroneous_pdu_len": report.erroneous_pdu().len(),
"erroneous_pdu_hex": common::test_helper::bytes_to_hex(report.erroneous_pdu()),
"text": String::from_utf8_lossy(report.text()),
}),
);
assert_error_report_matches(
&report,
1,
ErrorCode::UnsupportedPduType,
unknown_pdu.as_ref(),
);
let read_res = Header::read(&mut client).await;
assert!(read_res.is_err());
dump.push_value(
0,
json!({
"event": "connection_closed_after_unknown_pdu",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("established_session_unknown_pdu_returns_unsupported_pdu_type");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn version_zero_does_not_send_router_key_or_aspa() {
let router_key = RouterKey::new(Ski::default(), Asn::from(64496u32), vec![1u8; 32]);
let aspa = Aspa::new(
Asn::from(64496u32),
vec![Asn::from(64497u32), Asn::from(64498u32)],
);
let snapshot =
Snapshot::from_payloads(vec![Payload::RouterKey(router_key), Payload::Aspa(aspa)]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(0).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let eod = rpki::rtr::pdu::EndOfDataV0::read(&mut client)
.await
.unwrap();
dump.push_value(
eod.pdu(),
json!({
"version": eod.version(),
"pdu": eod.pdu(),
"pdu_name": "End of Data",
"session_id": eod.session_id(),
"serial_number": eod.serial_number(),
}),
);
let res = timeout(Duration::from_millis(100), Header::read(&mut client)).await;
assert!(
res.is_err(),
"version 0 response should not contain RouterKey or ASPA PDUs"
);
dump.print_pretty("version_zero_does_not_send_router_key_or_aspa");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn version_two_aspa_withdraw_has_empty_provider_list() {
let aspa = Aspa::new(
Asn::from(64496u32),
vec![Asn::from(64497u32), Asn::from(64498u32)],
);
let delta = Arc::new(Delta::new(101, vec![], vec![Payload::Aspa(aspa)]));
let mut deltas = VecDeque::new();
deltas.push_back(delta);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(101))
.timing(Timing::new(600, 600, 7200))
.deltas_by_version(deltas_all(deltas))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
SerialQuery::new(2, 42, 100)
.write(&mut client)
.await
.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let header = Header::read(&mut client).await.unwrap();
assert_eq!(header.pdu(), AspaPdu::PDU);
assert_eq!(header.length(), 12);
let mut body = [0u8; 4];
client.read_exact(&mut body).await.unwrap();
assert_eq!(u32::from_be_bytes(body), 64496);
dump.push_value(
AspaPdu::PDU,
json!({
"version": header.version(),
"pdu": header.pdu(),
"length": header.length(),
"customer_asn": u32::from_be_bytes(body),
"withdraw": true,
}),
);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
dump.print_pretty("version_two_aspa_withdraw_has_empty_provider_list");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn version_one_sends_router_key_but_not_aspa() {
let valid_spki = vec![
0x30, 0x13, 0x30, 0x0d, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x01,
0x05, 0x00, 0x03, 0x02, 0x00, 0x00,
];
let router_key = RouterKey::new(Ski::default(), Asn::from(64496u32), valid_spki);
let aspa = Aspa::new(Asn::from(64496u32), vec![Asn::from(64497u32)]);
let snapshot =
Snapshot::from_payloads(vec![Payload::RouterKey(router_key), Payload::Aspa(aspa)]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) = start_session_server(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let header = Header::read(&mut client).await.unwrap();
assert_eq!(header.pdu(), RouterKeyPdu::PDU);
dump.push_value(
RouterKeyPdu::PDU,
json!({
"version": header.version(),
"pdu": header.pdu(),
"length": header.length(),
}),
);
let payload_len = header.length() as usize - 8;
let mut payload = vec![0u8; payload_len];
client.read_exact(&mut payload).await.unwrap();
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
let res = timeout(Duration::from_millis(100), Header::read(&mut client)).await;
assert!(
res.is_err(),
"version 1 response should not contain ASPA PDUs"
);
dump.print_pretty("version_one_sends_router_key_but_not_aspa");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn established_session_idle_does_not_trigger_transport_failed() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, _notify_tx, shutdown_tx, server_handle) =
start_session_server_with_transport_timeout(server_cache, Duration::from_millis(100)).await;
let mut client = TcpStream::connect(addr).await.unwrap();
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
let idle_res = timeout(Duration::from_millis(300), Header::read(&mut client)).await;
assert!(
idle_res.is_err(),
"established session should stay idle without sending TransportFailed"
);
dump.push_value(
0,
json!({
"event": "no_transport_failed_on_established_idle",
"result": "no_pdu_received_within_timeout",
"timeout_ms": 300
}),
);
dump.print_pretty("established_session_idle_does_not_trigger_transport_failed");
shutdown_server(client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn tls_client_with_matching_san_ip_is_accepted() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, shutdown_tx, server_handle) = start_tls_session_server(server_cache).await;
let mut client = connect_tls_client(addr, "client-good.crt", "client-good.key").await;
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
dump.print_pretty("tls_client_with_matching_san_ip_is_accepted");
shutdown_io(&mut client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn tls_client_accepts_server_certificate_with_dns_san() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, shutdown_tx, server_handle) =
start_tls_session_server_with_cert(server_cache, "server-dns.crt", "server-dns.key").await;
let mut client = connect_tls_client_with_server_name(
addr,
"client-good.crt",
"client-good.key",
ServerName::try_from("localhost").unwrap(),
)
.await;
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
dump.push_value(response.pdu(), dump_cache_response(&response));
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let eod = EndOfDataV1::read(&mut client).await.unwrap();
dump.push_value(eod.pdu(), dump_eod_v1(&eod));
assert_eq!(eod.version(), 1);
assert_eq!(eod.session_id(), 42);
assert_eq!(eod.serial_number(), 100);
dump.print_pretty("tls_client_accepts_server_certificate_with_dns_san");
shutdown_io(&mut client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn tls_server_dns_name_san_strict_mode_rejects_ip_only_certificate() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let err = load_rustls_server_config_with_options(
fixture_path("server.crt"),
fixture_path("server.key"),
fixture_path("client-ca.crt"),
true,
)
.unwrap_err();
assert!(
err.to_string()
.contains("does not contain a subjectAltName dNSName entry")
);
let _ = cache;
}
#[tokio::test]
async fn tls_client_with_mismatched_san_ip_is_rejected() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, shutdown_tx, server_handle) = start_tls_session_server(server_cache).await;
let mut client = connect_tls_client(addr, "client-bad.crt", "client-bad.key").await;
let mut dump = RtrDebugDumper::new();
ResetQuery::new(1).write(&mut client).await.unwrap();
dump.push_value(
ResetQuery::PDU,
json!({
"event": "tls_reset_query_sent_with_bad_client_cert",
"version": 1,
}),
);
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.expect("timed out waiting for TLS session close");
assert!(
read_res.is_err(),
"server should close TLS session when client SAN IP mismatches"
);
dump.push_value(
0,
json!({
"event": "tls_session_closed_after_san_ip_mismatch",
"result": "header_read_failed_as_expected"
}),
);
dump.print_pretty("tls_client_with_mismatched_san_ip_is_rejected");
shutdown_io(&mut client, shutdown_tx, server_handle).await;
}
#[tokio::test]
async fn invalid_timing_prevents_end_of_data_response() {
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 8000, 7200))
.build();
let server_cache = shared_cache(cache);
let (addr, shutdown_tx, server_handle) =
start_session_server_returning_result(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.expect("timed out waiting for server close");
assert!(
read_res.is_err(),
"server should close instead of sending invalid EndOfData"
);
let _ = shutdown_tx.send(RtrServiceControl::Shutdown(RtrShutdownReason::CloseOnly));
let join = timeout(Duration::from_secs(1), server_handle)
.await
.expect("server task did not exit within timeout")
.unwrap();
let err = join.expect_err("session should fail on invalid timing");
assert!(err.to_string().contains("retry interval"));
}
#[tokio::test]
async fn invalid_aspa_prevents_snapshot_response() {
let snapshot =
Snapshot::from_payloads(vec![Payload::Aspa(Aspa::new(Asn::from(64496u32), vec![]))]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
let (addr, shutdown_tx, server_handle) =
start_session_server_returning_result(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(2).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 2);
assert_eq!(response.session_id(), 42);
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.expect("timed out waiting for server close");
assert!(
read_res.is_err(),
"server should close instead of sending invalid ASPA"
);
let _ = shutdown_tx.send(RtrServiceControl::Shutdown(RtrShutdownReason::CloseOnly));
let join = timeout(Duration::from_secs(1), server_handle)
.await
.expect("server task did not exit within timeout")
.unwrap();
let err = join.expect_err("session should fail on invalid ASPA");
assert!(err.to_string().contains("ASPA announcement"));
}
#[tokio::test]
async fn invalid_router_key_prevents_snapshot_response() {
let snapshot = Snapshot::from_payloads(vec![Payload::RouterKey(RouterKey::new(
Ski::default(),
Asn::from(64496u32),
vec![0xde, 0xad, 0xbe, 0xef],
))]);
let cache = RtrCacheBuilder::new()
.session_ids(SessionIds::from_array([42, 42, 42]))
.serials(serials_all(100))
.timing(Timing::new(600, 600, 7200))
.snapshots(snapshots_all(snapshot))
.build();
let server_cache = shared_cache(cache);
let (addr, shutdown_tx, server_handle) =
start_session_server_returning_result(server_cache).await;
let mut client = TcpStream::connect(addr).await.unwrap();
ResetQuery::new(1).write(&mut client).await.unwrap();
let response = CacheResponse::read(&mut client).await.unwrap();
assert_eq!(response.version(), 1);
assert_eq!(response.session_id(), 42);
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
.await
.expect("timed out waiting for server close");
assert!(
read_res.is_err(),
"server should close instead of sending invalid RouterKey"
);
let _ = shutdown_tx.send(RtrServiceControl::Shutdown(RtrShutdownReason::CloseOnly));
let join = timeout(Duration::from_secs(1), server_handle)
.await
.expect("server task did not exit within timeout")
.unwrap();
let err = join.expect_err("session should fail on invalid RouterKey");
assert!(err.to_string().contains("RouterKey SPKI"));
}