增加rtr一些必要的api接口
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
use std::fmt::Write;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use serde_json::{json, Value};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
|
||||
use rpki::rtr::cache::SerialResult;
|
||||
|
||||
@@ -79,17 +79,10 @@ fn version_report_stats_separate_snapshot_and_delta_payload_types() {
|
||||
Asn::from(64496u32),
|
||||
vec![1, 2, 3],
|
||||
));
|
||||
let aspa = Payload::Aspa(Aspa::new(
|
||||
Asn::from(64496u32),
|
||||
vec![Asn::from(64497u32)],
|
||||
));
|
||||
let aspa = Payload::Aspa(Aspa::new(Asn::from(64496u32), vec![Asn::from(64497u32)]));
|
||||
let snapshot = Snapshot::from_payloads(vec![vrp.clone(), router_key.clone(), aspa.clone()]);
|
||||
let mut deltas = VecDeque::new();
|
||||
deltas.push_back(Arc::new(Delta::new(
|
||||
101,
|
||||
vec![vrp, aspa],
|
||||
vec![router_key],
|
||||
)));
|
||||
deltas.push_back(Arc::new(Delta::new(101, vec![vrp, aspa], vec![router_key])));
|
||||
let cache = RtrCacheBuilder::new()
|
||||
.session_ids(SessionIds::from_array([40, 41, 42]))
|
||||
.serials([99, 100, 101])
|
||||
@@ -568,8 +561,11 @@ fn get_deltas_since_returns_up_to_date_when_client_serial_matches_current() {
|
||||
|
||||
let result = cache.get_deltas_since_for_version(2, 100);
|
||||
|
||||
let input =
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100);
|
||||
let input = get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
100,
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
|
||||
test_report(
|
||||
@@ -615,7 +611,11 @@ fn get_deltas_since_returns_reset_required_when_client_serial_is_too_old() {
|
||||
|
||||
let input = format!(
|
||||
"{}delta_window:\n{}",
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 99),
|
||||
get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
99
|
||||
),
|
||||
indent_block(&deltas_window_to_string(&deltas), 2),
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
@@ -680,7 +680,11 @@ fn get_deltas_since_returns_minimal_merged_delta() {
|
||||
|
||||
let input = format!(
|
||||
"{}delta_window:\n{}",
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 101),
|
||||
get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
101
|
||||
),
|
||||
indent_block(&deltas_window_to_string(&deltas), 2),
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
@@ -726,8 +730,11 @@ fn get_deltas_since_returns_reset_required_when_client_serial_is_in_future() {
|
||||
|
||||
let result = cache.get_deltas_since_for_version(2, 101);
|
||||
|
||||
let input =
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 101);
|
||||
let input = get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
101,
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
|
||||
test_report(
|
||||
@@ -888,8 +895,11 @@ fn get_deltas_since_returns_reset_required_when_client_serial_is_in_future_acros
|
||||
|
||||
let result = cache.get_deltas_since_for_version(2, 0);
|
||||
|
||||
let input =
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 0);
|
||||
let input = get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
0,
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
|
||||
test_report(
|
||||
@@ -1227,7 +1237,11 @@ fn get_deltas_since_cancels_announce_then_withdraw_for_same_prefix() {
|
||||
|
||||
let input = format!(
|
||||
"{}delta_window:\n{}",
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
|
||||
get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
100
|
||||
),
|
||||
indent_block(&deltas_window_to_string(&deltas), 2),
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
@@ -1281,7 +1295,11 @@ fn get_deltas_since_cancels_withdraw_then_announce_for_same_prefix() {
|
||||
|
||||
let input = format!(
|
||||
"{}delta_window:\n{}",
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
|
||||
get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
100
|
||||
),
|
||||
indent_block(&deltas_window_to_string(&deltas), 2),
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
@@ -1335,7 +1353,11 @@ fn get_deltas_since_merges_replacement_into_withdraw_and_announce() {
|
||||
|
||||
let input = format!(
|
||||
"{}delta_window:\n{}",
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
|
||||
get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
100
|
||||
),
|
||||
indent_block(&deltas_window_to_string(&deltas), 2),
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
@@ -1414,7 +1436,11 @@ fn get_deltas_since_merges_multiple_deltas_to_final_minimal_view() {
|
||||
|
||||
let input = format!(
|
||||
"{}delta_window:\n{}",
|
||||
get_deltas_since_input_to_string(cache.session_id_for_version(1), cache.serial_for_version(2), 100),
|
||||
get_deltas_since_input_to_string(
|
||||
cache.session_id_for_version(1),
|
||||
cache.serial_for_version(2),
|
||||
100
|
||||
),
|
||||
indent_block(&deltas_window_to_string(&deltas), 2),
|
||||
);
|
||||
let output = serial_result_detail_to_string(&result);
|
||||
@@ -1501,7 +1527,9 @@ fn get_deltas_since_merges_aspa_replacement_into_single_announcement() {
|
||||
.session_ids(SessionIds::from_array([42, 42, 42]))
|
||||
.serials(serials_all(102))
|
||||
.timing(Timing::default())
|
||||
.snapshots(snapshots_all(Snapshot::from_payloads(vec![Payload::Aspa(new.clone())])))
|
||||
.snapshots(snapshots_all(Snapshot::from_payloads(vec![Payload::Aspa(
|
||||
new.clone(),
|
||||
)])))
|
||||
.deltas_by_version(deltas_all(deltas))
|
||||
.build();
|
||||
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
use std::fs;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use tempfile::tempdir;
|
||||
use rpki::source::ccr::{
|
||||
ParsedAspa, ParsedCcrSnapshot, ParsedVrp, find_latest_ccr_file, load_ccr_snapshot_from_file,
|
||||
snapshot_to_payloads_with_options,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("data").join(name)
|
||||
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("data")
|
||||
.join(name)
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -17,8 +19,7 @@ fn ccr_loader_smoke() {
|
||||
// let path = "./mini_data/20260403T000001Z-mini-a.ccr";
|
||||
// let path = "20260403T000101Z-mini-b.ccr";
|
||||
let path = "20260403T000201Z-mini-c.ccr";
|
||||
let snapshot = load_ccr_snapshot_from_file(fixture_path(path))
|
||||
.expect("load CCR snapshot");
|
||||
let snapshot = load_ccr_snapshot_from_file(fixture_path(path)).expect("load CCR snapshot");
|
||||
|
||||
println!("content_type_oid: {}", snapshot.content_type_oid);
|
||||
println!("produced_at : {:?}", snapshot.produced_at);
|
||||
@@ -115,7 +116,15 @@ fn generated_mini_ccr_files_are_parseable() {
|
||||
for (name, expect_vrps, expect_vaps) in cases {
|
||||
let snapshot = load_ccr_snapshot_from_file(fixture_path(name))
|
||||
.unwrap_or_else(|e| panic!("failed to parse {}: {:?}", name, e));
|
||||
assert_eq!(snapshot.vrps.len(), expect_vrps, "vrp count mismatch for {name}");
|
||||
assert_eq!(snapshot.vaps.len(), expect_vaps, "vap count mismatch for {name}");
|
||||
assert_eq!(
|
||||
snapshot.vrps.len(),
|
||||
expect_vrps,
|
||||
"vrp count mismatch for {name}"
|
||||
);
|
||||
assert_eq!(
|
||||
snapshot.vaps.len(),
|
||||
expect_vaps,
|
||||
"vap count mismatch for {name}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
use rpki::data_model::crl::Asn1TimeEncoding;
|
||||
use rpki::data_model::crl::RpkixCrl;
|
||||
|
||||
#[test]
|
||||
fn decode_and_validate_crl_fixture() {
|
||||
@@ -42,9 +42,8 @@ fn crl_signature_verification_succeeds_with_issuer_cert() {
|
||||
|
||||
#[test]
|
||||
fn decode_crl_with_revoked_entries() {
|
||||
let der =
|
||||
std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl")
|
||||
.expect("read CRL fixture with revoked entries");
|
||||
let der = std::fs::read("tests/fixtures/0099DEAB073EFD74C250C0A382B25012B5082AEE.crl")
|
||||
.expect("read CRL fixture with revoked entries");
|
||||
|
||||
let crl = RpkixCrl::decode_der(&der).expect("decode CRL");
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
use std::net::Ipv4Addr;
|
||||
|
||||
use tokio::io::{duplex, AsyncWriteExt};
|
||||
use tokio::io::{AsyncWriteExt, duplex};
|
||||
|
||||
use rpki::data_model::resources::as_resources::Asn;
|
||||
use rpki::rtr::error_type::ErrorCode;
|
||||
use rpki::rtr::payload::{Aspa as PayloadAspa, Ski, Timing};
|
||||
use rpki::rtr::pdu::{
|
||||
Aspa, EndOfDataV1, ErrorReport, Flags, Header, IPv4Prefix, RouterKey, SerialNotify,
|
||||
END_OF_DATA_V1_LEN, MAX_PDU_LEN,
|
||||
Aspa, END_OF_DATA_V1_LEN, EndOfDataV1, ErrorReport, Flags, Header, IPv4Prefix, MAX_PDU_LEN,
|
||||
RouterKey, SerialNotify,
|
||||
};
|
||||
|
||||
const ERROR_REPORT_FIXED_PART_LEN: usize = 16;
|
||||
@@ -271,7 +271,9 @@ async fn end_of_data_v1_read_payload_rejects_invalid_timing() {
|
||||
client.write_all(&600u32.to_be_bytes()).await.unwrap();
|
||||
client.write_all(&600u32.to_be_bytes()).await.unwrap();
|
||||
|
||||
let err = EndOfDataV1::read_payload(header, &mut server).await.unwrap_err();
|
||||
let err = EndOfDataV1::read_payload(header, &mut server)
|
||||
.await
|
||||
.unwrap_err();
|
||||
assert_eq!(err.kind(), std::io::ErrorKind::InvalidData);
|
||||
assert!(err.to_string().contains("expire interval"));
|
||||
}
|
||||
|
||||
13
tests/test_rtr_admin.rs
Normal file
13
tests/test_rtr_admin.rs
Normal file
@@ -0,0 +1,13 @@
|
||||
use rpki::rtr::admin::tail_log_lines;
|
||||
|
||||
#[test]
|
||||
fn tail_log_lines_returns_requested_suffix() {
|
||||
let output = tail_log_lines(b"one\ntwo\nthree\nfour\n".to_vec(), 2);
|
||||
assert_eq!(output, b"three\nfour\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tail_log_lines_returns_whole_buffer_when_shorter() {
|
||||
let output = tail_log_lines(b"one\ntwo\n".to_vec(), 5);
|
||||
assert_eq!(output, b"one\ntwo\n");
|
||||
}
|
||||
87
tests/test_rtr_config.rs
Normal file
87
tests/test_rtr_config.rs
Normal file
@@ -0,0 +1,87 @@
|
||||
use rpki::rtr::config::{AppConfig, RuntimeConfigPatch, RuntimeTimingConfigPatch, parse_timezone};
|
||||
|
||||
#[test]
|
||||
fn parse_timezone_accepts_iana_names_and_common_aliases() {
|
||||
assert_eq!(
|
||||
parse_timezone("Asia/Shanghai", "TEST").unwrap(),
|
||||
chrono_tz::Asia::Shanghai
|
||||
);
|
||||
assert_eq!(
|
||||
parse_timezone("shanghai", "TEST").unwrap(),
|
||||
chrono_tz::Asia::Shanghai
|
||||
);
|
||||
assert_eq!(
|
||||
parse_timezone("Europe/London", "TEST").unwrap(),
|
||||
chrono_tz::Europe::London
|
||||
);
|
||||
assert_eq!(
|
||||
parse_timezone("America/New_York", "TEST").unwrap(),
|
||||
chrono_tz::America::New_York
|
||||
);
|
||||
assert_eq!(parse_timezone("UTC", "TEST").unwrap(), chrono_tz::UTC);
|
||||
assert_eq!(parse_timezone("Z", "TEST").unwrap(), chrono_tz::UTC);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_timezone_rejects_offset_and_invalid_values() {
|
||||
assert!(parse_timezone("+08:00", "TEST").is_err());
|
||||
assert!(parse_timezone("+0800", "TEST").is_err());
|
||||
assert!(parse_timezone("Mars/Base", "TEST").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_config_patch_validates_and_preserves_unspecified_values() {
|
||||
let current = AppConfig::default().runtime_config();
|
||||
let next = current
|
||||
.apply_patch(RuntimeConfigPatch {
|
||||
max_delta: Some(8),
|
||||
prune_delta_by_snapshot_size: Some(true),
|
||||
source_refresh_interval_seconds: Some(60),
|
||||
runtime_report_interval_seconds: Some(120),
|
||||
report_history_limit: Some(20),
|
||||
strict_ccr_validation: Some(true),
|
||||
timezone: Some("UTC".to_string()),
|
||||
timing: Some(RuntimeTimingConfigPatch {
|
||||
refresh: Some(1800),
|
||||
retry: None,
|
||||
expire: Some(7200),
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(next.max_delta, 8);
|
||||
assert!(next.prune_delta_by_snapshot_size);
|
||||
assert_eq!(next.source_refresh_interval_seconds, 60);
|
||||
assert_eq!(next.runtime_report_interval_seconds, 120);
|
||||
assert_eq!(next.report_history_limit, 20);
|
||||
assert!(next.strict_ccr_validation);
|
||||
assert_eq!(next.timezone, "UTC");
|
||||
assert_eq!(next.timing.refresh, 1800);
|
||||
assert_eq!(next.timing.retry, current.timing.retry);
|
||||
assert_eq!(next.timing.expire, 7200);
|
||||
|
||||
assert!(
|
||||
current
|
||||
.apply_patch(RuntimeConfigPatch {
|
||||
max_delta: Some(0),
|
||||
..RuntimeConfigPatch::default()
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
current
|
||||
.apply_patch(RuntimeConfigPatch {
|
||||
runtime_report_interval_seconds: Some(0),
|
||||
..RuntimeConfigPatch::default()
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
assert!(
|
||||
current
|
||||
.apply_patch(RuntimeConfigPatch {
|
||||
timezone: Some("+08:00".to_string()),
|
||||
..RuntimeConfigPatch::default()
|
||||
})
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
290
tests/test_rtr_report.rs
Normal file
290
tests/test_rtr_report.rs
Normal file
@@ -0,0 +1,290 @@
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
|
||||
use arc_swap::ArcSwap;
|
||||
use chrono::Utc;
|
||||
use rpki::rtr::cache::RtrCache;
|
||||
use rpki::rtr::report::{ReportConfiguration, ReportContext};
|
||||
use rpki::rtr::server::RtrService;
|
||||
use rpki::source::pipeline::{
|
||||
DataQualityReport, FileFingerprint, PayloadTypeCounts, SlurmRuleCounts, SourceFingerprint,
|
||||
SourceLoadReport,
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
fn test_configuration(report_history_limit: usize) -> ReportConfiguration {
|
||||
ReportConfiguration::new(
|
||||
300,
|
||||
300,
|
||||
report_history_limit,
|
||||
100,
|
||||
false,
|
||||
false,
|
||||
chrono_tz::Asia::Shanghai,
|
||||
(3600, 600, 7200),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_report_creates_parseable_json() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report_dir = temp.path().join("report");
|
||||
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
|
||||
let service = RtrService::new(shared_cache.clone());
|
||||
let notifier = service.notifier();
|
||||
let context = ReportContext::new(test_configuration(10));
|
||||
|
||||
context.write_or_warn(
|
||||
&report_dir,
|
||||
"test",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
);
|
||||
|
||||
let source_report = read_single_report(&report_dir, "rtr-source");
|
||||
assert_eq!(source_report["schema_version"], 2);
|
||||
assert_eq!(source_report["phase"], "test");
|
||||
assert_report_time_offset(&source_report["generated_at"]);
|
||||
assert_report_time_offset(&source_report["cache"]["created_at"]);
|
||||
assert_eq!(source_report["cache"]["availability"], "ready");
|
||||
assert_eq!(source_report["refresh"]["status"], "not_attempted");
|
||||
assert!(source_report["source"].is_null());
|
||||
assert!(source_report["data_quality"].is_null());
|
||||
|
||||
let runtime_report = read_single_report(&report_dir, "rtr-runtime");
|
||||
assert_report_time_offset(&runtime_report["service"]["started_at"]);
|
||||
assert_eq!(
|
||||
runtime_report["configuration"]["source_refresh_interval_seconds"],
|
||||
300
|
||||
);
|
||||
assert_eq!(runtime_report["configuration"]["report_history_limit"], 10);
|
||||
|
||||
let clients_report = read_single_report(&report_dir, "rtr-clients");
|
||||
assert_eq!(clients_report["service"]["max_connections"], 1024);
|
||||
assert_eq!(clients_report["service"]["active_connections"], 0);
|
||||
assert_eq!(
|
||||
clients_report["service"]["connections_by_transport"]["tcp"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
clients_report["service"]["connections_by_transport"]["tls"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
clients_report["service"]["connections_by_transport"]["ssh"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
source_report["cache"]["versions"].as_array().unwrap().len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
source_report["cache"]["versions"][2]["snapshot"]["total"],
|
||||
0
|
||||
);
|
||||
assert_eq!(
|
||||
source_report["cache"]["memory"]["delta_payload_counts"][2],
|
||||
0
|
||||
);
|
||||
assert!(source_report["source_fingerprint"].is_null());
|
||||
assert_no_temporary_reports(&report_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_failure_preserves_last_successful_source_data() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report_dir = temp.path().join("report");
|
||||
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
|
||||
let service = RtrService::new(shared_cache.clone());
|
||||
let notifier = service.notifier();
|
||||
let context = ReportContext::new(test_configuration(10));
|
||||
let source = SourceLoadReport {
|
||||
ccr_file: "data/example.ccr".to_string(),
|
||||
ccr_file_size_bytes: 123,
|
||||
ccr_modified_at: Some(Utc::now()),
|
||||
ccr_produced_at: Some("20260615000000Z".to_string()),
|
||||
slurm_enabled: true,
|
||||
slurm_file_count: 1,
|
||||
slurm_files: vec!["policy.slurm".to_string()],
|
||||
slurm_version: Some(2),
|
||||
};
|
||||
let quality = DataQualityReport {
|
||||
ccr_input: PayloadTypeCounts {
|
||||
total: 11,
|
||||
vrp: 10,
|
||||
router_key: 0,
|
||||
aspa: 1,
|
||||
},
|
||||
invalid: PayloadTypeCounts::default(),
|
||||
before_slurm: PayloadTypeCounts {
|
||||
total: 11,
|
||||
vrp: 10,
|
||||
router_key: 0,
|
||||
aspa: 1,
|
||||
},
|
||||
after_slurm: PayloadTypeCounts {
|
||||
total: 10,
|
||||
vrp: 9,
|
||||
router_key: 0,
|
||||
aspa: 1,
|
||||
},
|
||||
slurm_filters: SlurmRuleCounts {
|
||||
prefix: 1,
|
||||
router_key: 0,
|
||||
aspa: 0,
|
||||
},
|
||||
slurm_assertions: SlurmRuleCounts::default(),
|
||||
};
|
||||
context.record_source_fingerprint(SourceFingerprint {
|
||||
ccr: FileFingerprint {
|
||||
path: "data/example.ccr".to_string(),
|
||||
len: 123,
|
||||
modified_unix_secs: 1_781_404_800,
|
||||
},
|
||||
slurm_files: vec![FileFingerprint {
|
||||
path: "policy.slurm".to_string(),
|
||||
len: 42,
|
||||
modified_unix_secs: 1_781_408_400,
|
||||
}],
|
||||
});
|
||||
|
||||
context.record_refresh_success(Utc::now(), 12, true, source, quality);
|
||||
context.record_refresh_failure(Utc::now(), 5, &anyhow::anyhow!("source unavailable"));
|
||||
context.write_or_warn(
|
||||
&report_dir,
|
||||
"refresh_failed",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
);
|
||||
|
||||
let report = read_single_report(&report_dir, "rtr-source");
|
||||
assert_eq!(report["source"]["ccr_file"], "data/example.ccr");
|
||||
assert_eq!(
|
||||
report["source_fingerprint"]["ccr"]["path"],
|
||||
"data/example.ccr"
|
||||
);
|
||||
assert_eq!(report["source_fingerprint"]["ccr"]["len"], 123);
|
||||
assert_report_time_offset(&report["source_fingerprint"]["ccr"]["modified_at"]);
|
||||
assert_eq!(
|
||||
report["source_fingerprint"]["slurm_files"][0]["path"],
|
||||
"policy.slurm"
|
||||
);
|
||||
assert_report_time_offset(&report["source"]["ccr_modified_at"]);
|
||||
assert_eq!(report["data_quality"]["after_slurm"]["total"], 10);
|
||||
assert_eq!(report["refresh"]["status"], "failed");
|
||||
assert_eq!(report["refresh"]["consecutive_failures"], 1);
|
||||
assert_eq!(report["refresh"]["last_error"], "source unavailable");
|
||||
assert!(!report["refresh"]["last_success_at"].is_null());
|
||||
assert_report_time_offset(&report["refresh"]["last_success_at"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rolling_reports_keep_latest_files_per_category() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report_dir = temp.path().join("report");
|
||||
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
|
||||
let service = RtrService::new(shared_cache.clone());
|
||||
let notifier = service.notifier();
|
||||
let context = ReportContext::new(test_configuration(2));
|
||||
|
||||
for phase in ["one", "two", "three"] {
|
||||
context.write_or_warn(
|
||||
&report_dir,
|
||||
phase,
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 2);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 2);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 2);
|
||||
assert_no_temporary_reports(&report_dir);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn category_writes_only_create_requested_report_type() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let report_dir = temp.path().join("report");
|
||||
let shared_cache = Arc::new(ArcSwap::from_pointee(RtrCache::default()));
|
||||
let service = RtrService::new(shared_cache.clone());
|
||||
let notifier = service.notifier();
|
||||
let context = ReportContext::new(test_configuration(10));
|
||||
|
||||
context.write_source_or_warn(
|
||||
&report_dir,
|
||||
"source_only",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
);
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 0);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 0);
|
||||
|
||||
context.write_clients_or_warn(
|
||||
&report_dir,
|
||||
"clients_only",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
);
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 0);
|
||||
|
||||
context.write_runtime_or_warn(
|
||||
&report_dir,
|
||||
"runtime_only",
|
||||
&shared_cache,
|
||||
¬ifier,
|
||||
&service.stats(),
|
||||
);
|
||||
assert_eq!(report_files(&report_dir, "rtr-source").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-clients").len(), 1);
|
||||
assert_eq!(report_files(&report_dir, "rtr-runtime").len(), 1);
|
||||
}
|
||||
|
||||
fn assert_report_time_offset(value: &Value) {
|
||||
let value = value.as_str().expect("report time should be a string");
|
||||
assert!(
|
||||
value.ends_with("+08:00"),
|
||||
"report time should use +08:00 offset, got {value}"
|
||||
);
|
||||
}
|
||||
|
||||
fn read_single_report(report_dir: &Path, prefix: &str) -> Value {
|
||||
let files = report_files(report_dir, prefix);
|
||||
assert_eq!(files.len(), 1, "expected one {prefix} report");
|
||||
serde_json::from_slice(&fs::read(&files[0]).unwrap()).unwrap()
|
||||
}
|
||||
|
||||
fn report_files(report_dir: &Path, prefix: &str) -> Vec<PathBuf> {
|
||||
let start = format!("{prefix}-");
|
||||
let mut files = fs::read_dir(report_dir)
|
||||
.unwrap()
|
||||
.map(|entry| entry.unwrap().path())
|
||||
.filter(|path| {
|
||||
path.file_name()
|
||||
.and_then(|name| name.to_str())
|
||||
.is_some_and(|name| name.starts_with(&start) && name.ends_with(".json"))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
|
||||
fn assert_no_temporary_reports(report_dir: &Path) {
|
||||
let has_temporary = fs::read_dir(report_dir).unwrap().any(|entry| {
|
||||
entry
|
||||
.unwrap()
|
||||
.file_name()
|
||||
.to_str()
|
||||
.is_some_and(|name| name.ends_with(".tmp"))
|
||||
});
|
||||
assert!(!has_temporary);
|
||||
}
|
||||
@@ -16,8 +16,8 @@ use tokio_rustls::TlsConnector;
|
||||
use rpki::rtr::cache::{RtrCacheBuilder, SessionIds, SharedRtrCache};
|
||||
use rpki::rtr::payload::Timing;
|
||||
use rpki::rtr::pdu::{CacheResponse, EndOfDataV1, ResetQuery};
|
||||
use rpki::rtr::server::ssh::SshAuthMode;
|
||||
use rpki::rtr::server::RtrService;
|
||||
use rpki::rtr::server::ssh::SshAuthMode;
|
||||
use russh::client;
|
||||
use russh::keys;
|
||||
use russh::keys::ssh_key::LineEnding;
|
||||
|
||||
@@ -15,16 +15,16 @@ use tokio::io::{AsyncReadExt, AsyncWrite, AsyncWriteExt};
|
||||
use tokio::net::{TcpListener, TcpStream};
|
||||
use tokio::sync::{broadcast, watch};
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio::time::{timeout, Duration};
|
||||
use tokio::time::{Duration, timeout};
|
||||
use tokio_rustls::{TlsAcceptor, TlsConnector};
|
||||
|
||||
use common::test_helper::{
|
||||
dump_cache_reset, dump_cache_response, dump_eod_v1, dump_ipv4_prefix, dump_ipv6_prefix,
|
||||
RtrDebugDumper,
|
||||
RtrDebugDumper, dump_cache_reset, dump_cache_response, dump_eod_v1, dump_ipv4_prefix,
|
||||
dump_ipv6_prefix,
|
||||
};
|
||||
|
||||
use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
|
||||
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};
|
||||
@@ -32,10 +32,10 @@ use rpki::rtr::pdu::{
|
||||
Aspa as AspaPdu, CacheReset, CacheResponse, EndOfDataV1, ErrorReport, Header, IPv4Prefix,
|
||||
IPv6Prefix, ResetQuery, RouterKey as RouterKeyPdu, SerialNotify, SerialQuery,
|
||||
};
|
||||
use rpki::rtr::store::RtrStore;
|
||||
use rpki::rtr::server::connection::handle_tls_connection;
|
||||
use rpki::rtr::server::tls::load_rustls_server_config_with_options;
|
||||
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))
|
||||
@@ -119,11 +119,7 @@ async fn start_session_server_returning_result(
|
||||
|
||||
async fn start_tls_session_server(
|
||||
cache: SharedRtrCache,
|
||||
) -> (
|
||||
SocketAddr,
|
||||
watch::Sender<bool>,
|
||||
JoinHandle<()>,
|
||||
) {
|
||||
) -> (SocketAddr, watch::Sender<bool>, JoinHandle<()>) {
|
||||
start_tls_session_server_with_cert(cache, "server.crt", "server.key").await
|
||||
}
|
||||
|
||||
@@ -131,11 +127,7 @@ async fn start_tls_session_server_with_cert(
|
||||
cache: SharedRtrCache,
|
||||
cert_name: &str,
|
||||
key_name: &str,
|
||||
) -> (
|
||||
SocketAddr,
|
||||
watch::Sender<bool>,
|
||||
JoinHandle<()>,
|
||||
) {
|
||||
) -> (SocketAddr, watch::Sender<bool>, JoinHandle<()>) {
|
||||
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
|
||||
let addr = listener.local_addr().unwrap();
|
||||
|
||||
@@ -181,11 +173,8 @@ async fn shutdown_server(
|
||||
shutdown_io(&mut client, shutdown_tx, server_handle).await;
|
||||
}
|
||||
|
||||
async fn shutdown_io<S>(
|
||||
io: &mut S,
|
||||
shutdown_tx: watch::Sender<bool>,
|
||||
server_handle: JoinHandle<()>,
|
||||
) where
|
||||
async fn shutdown_io<S>(io: &mut S, shutdown_tx: watch::Sender<bool>, server_handle: JoinHandle<()>)
|
||||
where
|
||||
S: AsyncWrite + Unpin,
|
||||
{
|
||||
let _ = io.shutdown().await;
|
||||
@@ -419,8 +408,8 @@ async fn restart_restores_versioned_state_and_serves_queries() {
|
||||
};
|
||||
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,
|
||||
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(),
|
||||
@@ -464,7 +453,9 @@ async fn restart_restores_versioned_state_and_serves_queries() {
|
||||
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();
|
||||
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;
|
||||
|
||||
@@ -484,7 +475,10 @@ async fn restart_restores_versioned_state_and_serves_queries() {
|
||||
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();
|
||||
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);
|
||||
@@ -511,7 +505,10 @@ async fn serial_query_returns_end_of_data_when_up_to_date() {
|
||||
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();
|
||||
SerialQuery::new(1, 42, 100)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut dump = RtrDebugDumper::new();
|
||||
|
||||
@@ -554,7 +551,10 @@ async fn serial_query_returns_corrupt_data_when_session_id_mismatch() {
|
||||
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();
|
||||
SerialQuery::new(1, 999, 100)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut dump = RtrDebugDumper::new();
|
||||
|
||||
@@ -610,7 +610,10 @@ async fn serial_query_returns_deltas_when_incremental_update_available() {
|
||||
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();
|
||||
SerialQuery::new(1, 42, 100)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut dump = RtrDebugDumper::new();
|
||||
|
||||
@@ -823,7 +826,10 @@ async fn reset_query_returns_payloads_in_rtr_order() {
|
||||
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(),
|
||||
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());
|
||||
@@ -901,7 +907,10 @@ async fn serial_query_returns_announcements_before_withdrawals() {
|
||||
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();
|
||||
SerialQuery::new(1, 42, 100)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut dump = RtrDebugDumper::new();
|
||||
|
||||
@@ -1256,7 +1265,10 @@ async fn serial_query_returns_no_data_available_when_cache_is_unavailable() {
|
||||
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();
|
||||
SerialQuery::new(1, 42, 100)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let report = ErrorReport::read(&mut client).await.unwrap();
|
||||
assert_error_report_matches(
|
||||
@@ -1266,7 +1278,10 @@ async fn serial_query_returns_no_data_available_when_cache_is_unavailable() {
|
||||
SerialQuery::new(1, 42, 100).as_ref(),
|
||||
);
|
||||
|
||||
SerialQuery::new(1, 42, 100).write(&mut client).await.unwrap();
|
||||
SerialQuery::new(1, 42, 100)
|
||||
.write(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
let second = ErrorReport::read(&mut client).await.unwrap();
|
||||
assert_error_report_matches(
|
||||
&second,
|
||||
@@ -1327,7 +1342,11 @@ async fn first_pdu_with_invalid_length_returns_corrupt_data() {
|
||||
);
|
||||
|
||||
assert_error_report_matches(&report, 1, ErrorCode::CorruptData, &request);
|
||||
assert!(std::str::from_utf8(report.text()).unwrap().contains("invalid length"));
|
||||
assert!(
|
||||
std::str::from_utf8(report.text())
|
||||
.unwrap()
|
||||
.contains("invalid length")
|
||||
);
|
||||
|
||||
let read_res = timeout(Duration::from_secs(1), Header::read(&mut client))
|
||||
.await
|
||||
@@ -1442,9 +1461,11 @@ async fn established_session_invalid_header_returns_corrupt_data() {
|
||||
}),
|
||||
);
|
||||
assert_error_report_matches(&report, 1, ErrorCode::CorruptData, invalid_header.as_ref());
|
||||
assert!(std::str::from_utf8(report.text())
|
||||
.unwrap()
|
||||
.contains("invalid PDU length"));
|
||||
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());
|
||||
@@ -1506,7 +1527,12 @@ async fn established_session_unknown_pdu_returns_unsupported_pdu_type() {
|
||||
}),
|
||||
);
|
||||
|
||||
assert_error_report_matches(&report, 1, ErrorCode::UnsupportedPduType, unknown_pdu.as_ref());
|
||||
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());
|
||||
@@ -1525,11 +1551,12 @@ async fn established_session_unknown_pdu_returns_unsupported_pdu_type() {
|
||||
#[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 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]))
|
||||
@@ -1548,7 +1575,9 @@ async fn version_zero_does_not_send_router_key_or_aspa() {
|
||||
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();
|
||||
let eod = rpki::rtr::pdu::EndOfDataV0::read(&mut client)
|
||||
.await
|
||||
.unwrap();
|
||||
dump.push_value(
|
||||
eod.pdu(),
|
||||
json!({
|
||||
@@ -1561,7 +1590,10 @@ async fn version_zero_does_not_send_router_key_or_aspa() {
|
||||
);
|
||||
|
||||
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");
|
||||
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;
|
||||
@@ -1569,7 +1601,10 @@ async fn version_zero_does_not_send_router_key_or_aspa() {
|
||||
|
||||
#[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 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);
|
||||
@@ -1586,7 +1621,10 @@ async fn version_two_aspa_withdraw_has_empty_provider_list() {
|
||||
|
||||
let mut client = TcpStream::connect(addr).await.unwrap();
|
||||
let mut dump = RtrDebugDumper::new();
|
||||
SerialQuery::new(2, 42, 100).write(&mut client).await.unwrap();
|
||||
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));
|
||||
@@ -1618,15 +1656,13 @@ async fn version_two_aspa_withdraw_has_empty_provider_list() {
|
||||
#[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,
|
||||
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 snapshot =
|
||||
Snapshot::from_payloads(vec![Payload::RouterKey(router_key), Payload::Aspa(aspa)]);
|
||||
|
||||
let cache = RtrCacheBuilder::new()
|
||||
.session_ids(SessionIds::from_array([42, 42, 42]))
|
||||
@@ -1664,7 +1700,10 @@ async fn version_one_sends_router_key_but_not_aspa() {
|
||||
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");
|
||||
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;
|
||||
@@ -1793,9 +1832,10 @@ async fn tls_server_dns_name_san_strict_mode_rejects_ip_only_certificate() {
|
||||
)
|
||||
.unwrap_err();
|
||||
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("does not contain a subjectAltName dNSName entry"));
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("does not contain a subjectAltName dNSName entry")
|
||||
);
|
||||
|
||||
let _ = cache;
|
||||
}
|
||||
@@ -1825,7 +1865,10 @@ async fn tls_client_with_mismatched_san_ip_is_rejected() {
|
||||
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");
|
||||
assert!(
|
||||
read_res.is_err(),
|
||||
"server should close TLS session when client SAN IP mismatches"
|
||||
);
|
||||
dump.push_value(
|
||||
0,
|
||||
json!({
|
||||
@@ -1847,7 +1890,8 @@ async fn invalid_timing_prevents_end_of_data_response() {
|
||||
.build();
|
||||
|
||||
let server_cache = shared_cache(cache);
|
||||
let (addr, shutdown_tx, server_handle) = start_session_server_returning_result(server_cache).await;
|
||||
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();
|
||||
@@ -1859,7 +1903,10 @@ async fn invalid_timing_prevents_end_of_data_response() {
|
||||
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");
|
||||
assert!(
|
||||
read_res.is_err(),
|
||||
"server should close instead of sending invalid EndOfData"
|
||||
);
|
||||
|
||||
let _ = shutdown_tx.send(true);
|
||||
let join = timeout(Duration::from_secs(1), server_handle)
|
||||
@@ -1872,10 +1919,8 @@ async fn invalid_timing_prevents_end_of_data_response() {
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_aspa_prevents_snapshot_response() {
|
||||
let snapshot = Snapshot::from_payloads(vec![Payload::Aspa(Aspa::new(
|
||||
Asn::from(64496u32),
|
||||
vec![],
|
||||
))]);
|
||||
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))
|
||||
@@ -1884,7 +1929,8 @@ async fn invalid_aspa_prevents_snapshot_response() {
|
||||
.build();
|
||||
|
||||
let server_cache = shared_cache(cache);
|
||||
let (addr, shutdown_tx, server_handle) = start_session_server_returning_result(server_cache).await;
|
||||
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();
|
||||
@@ -1896,7 +1942,10 @@ async fn invalid_aspa_prevents_snapshot_response() {
|
||||
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");
|
||||
assert!(
|
||||
read_res.is_err(),
|
||||
"server should close instead of sending invalid ASPA"
|
||||
);
|
||||
|
||||
let _ = shutdown_tx.send(true);
|
||||
let join = timeout(Duration::from_secs(1), server_handle)
|
||||
@@ -1922,7 +1971,8 @@ async fn invalid_router_key_prevents_snapshot_response() {
|
||||
.build();
|
||||
|
||||
let server_cache = shared_cache(cache);
|
||||
let (addr, shutdown_tx, server_handle) = start_session_server_returning_result(server_cache).await;
|
||||
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();
|
||||
@@ -1934,7 +1984,10 @@ async fn invalid_router_key_prevents_snapshot_response() {
|
||||
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");
|
||||
assert!(
|
||||
read_res.is_err(),
|
||||
"server should close instead of sending invalid RouterKey"
|
||||
);
|
||||
|
||||
let _ = shutdown_tx.send(true);
|
||||
let join = timeout(Duration::from_secs(1), server_handle)
|
||||
|
||||
@@ -676,7 +676,10 @@ fn applies_filters_before_assertions_and_excludes_duplicates() {
|
||||
}}
|
||||
}}"#
|
||||
);
|
||||
log_slurm_input("applies_filters_before_assertions_and_excludes_duplicates", &json);
|
||||
log_slurm_input(
|
||||
"applies_filters_before_assertions_and_excludes_duplicates",
|
||||
&json,
|
||||
);
|
||||
let slurm = SlurmFile::from_slice(json.as_bytes()).unwrap();
|
||||
log_slurm_ok(
|
||||
"applies_filters_before_assertions_and_excludes_duplicates",
|
||||
@@ -788,9 +791,11 @@ fn rejects_hex_encoded_ski_and_aspa_customer_in_providers() {
|
||||
"rejects_hex_encoded_ski_and_aspa_customer_in_providers.invalid_aspa",
|
||||
&aspa_err,
|
||||
);
|
||||
assert!(aspa_err
|
||||
.to_string()
|
||||
.contains("providerAsns must not contain customerAsn"));
|
||||
assert!(
|
||||
aspa_err
|
||||
.to_string()
|
||||
.contains("providerAsns must not contain customerAsn")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -840,7 +845,10 @@ fn merges_multiple_slurm_files_without_conflict() {
|
||||
),
|
||||
])
|
||||
.unwrap();
|
||||
log_slurm_ok("merges_multiple_slurm_files_without_conflict.merged", &merged);
|
||||
log_slurm_ok(
|
||||
"merges_multiple_slurm_files_without_conflict.merged",
|
||||
&merged,
|
||||
);
|
||||
|
||||
assert_eq!(merged.version(), SlurmVersion::V2);
|
||||
assert_eq!(merged.locally_added_assertions().prefix_assertions.len(), 1);
|
||||
|
||||
109
tests/test_slurm_admin.rs
Normal file
109
tests/test_slurm_admin.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use std::fs;
|
||||
|
||||
use rpki::slurm::admin::SlurmAdmin;
|
||||
|
||||
fn valid_slurm() -> &'static str {
|
||||
r#"{
|
||||
"slurmVersion": 1,
|
||||
"validationOutputFilters": {
|
||||
"prefixFilters": [],
|
||||
"bgpsecFilters": []
|
||||
},
|
||||
"locallyAddedAssertions": {
|
||||
"prefixAssertions": [],
|
||||
"bgpsecAssertions": []
|
||||
}
|
||||
}"#
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slurm_admin_rejects_unsafe_file_names() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let admin = SlurmAdmin::new(temp.path());
|
||||
|
||||
for name in ["", "../x.slurm", "a/b.slurm", "a\\b.slurm", "a.json"] {
|
||||
assert!(
|
||||
admin
|
||||
.put_file(name, valid_slurm(), "create_or_update")
|
||||
.is_err(),
|
||||
"{name}"
|
||||
);
|
||||
}
|
||||
assert!(
|
||||
admin
|
||||
.put_file("policy.slurm", valid_slurm(), "create_or_update")
|
||||
.is_ok()
|
||||
);
|
||||
assert!(
|
||||
admin
|
||||
.put_file("policy.slurm.disabled", valid_slurm(), "create_or_update")
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn writes_backs_up_toggles_deletes_and_rolls_back() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let admin = SlurmAdmin::new(temp.path());
|
||||
|
||||
let create = admin
|
||||
.put_file("policy.slurm", valid_slurm(), "create_or_update")
|
||||
.unwrap();
|
||||
assert!(temp.path().join("policy.slurm").is_file());
|
||||
assert!(create.result.backup.is_none());
|
||||
|
||||
let update = admin
|
||||
.put_file("policy.slurm", valid_slurm(), "create_or_update")
|
||||
.unwrap();
|
||||
assert!(update.result.backup.is_some());
|
||||
|
||||
let disable = admin.disable_file("policy.slurm").unwrap();
|
||||
assert!(!temp.path().join("policy.slurm").exists());
|
||||
assert!(temp.path().join("policy.slurm.disabled").is_file());
|
||||
|
||||
disable.rollback().unwrap();
|
||||
assert!(temp.path().join("policy.slurm").is_file());
|
||||
assert!(!temp.path().join("policy.slurm.disabled").exists());
|
||||
|
||||
admin.disable_file("policy.slurm").unwrap();
|
||||
let enable = admin.enable_file("policy.slurm.disabled").unwrap();
|
||||
assert!(temp.path().join("policy.slurm").is_file());
|
||||
assert!(!temp.path().join("policy.slurm.disabled").exists());
|
||||
enable.rollback().unwrap();
|
||||
assert!(!temp.path().join("policy.slurm").exists());
|
||||
assert!(temp.path().join("policy.slurm.disabled").is_file());
|
||||
|
||||
admin.enable_file("policy.slurm.disabled").unwrap();
|
||||
let delete = admin.delete_file("policy.slurm").unwrap();
|
||||
assert!(!temp.path().join("policy.slurm").exists());
|
||||
assert!(delete.result.backup.is_some());
|
||||
delete.rollback().unwrap();
|
||||
assert!(temp.path().join("policy.slurm").is_file());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn lists_and_reads_slurm_files() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let admin = SlurmAdmin::new(temp.path());
|
||||
|
||||
admin
|
||||
.put_file("enabled.slurm", valid_slurm(), "create_or_update")
|
||||
.unwrap();
|
||||
admin
|
||||
.put_file("disabled.slurm.disabled", valid_slurm(), "create_or_update")
|
||||
.unwrap();
|
||||
fs::write(temp.path().join("ignore.txt"), "not slurm").unwrap();
|
||||
|
||||
let files = admin.list_files().unwrap();
|
||||
assert_eq!(files.len(), 2);
|
||||
assert_eq!(files[0].name, "disabled.slurm.disabled");
|
||||
assert!(!files[0].enabled);
|
||||
assert_eq!(files[1].name, "enabled.slurm");
|
||||
assert!(files[1].enabled);
|
||||
|
||||
let file = admin.read_file("enabled.slurm").unwrap();
|
||||
assert_eq!(file.name, "enabled.slurm");
|
||||
assert!(file.enabled);
|
||||
assert!(file.content.contains("\"slurmVersion\""));
|
||||
assert!(admin.read_file("missing.slurm").is_err());
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
mod common;
|
||||
|
||||
use std::net::Ipv6Addr;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
||||
use common::test_helper::{v4_origin, v6_origin};
|
||||
|
||||
use rpki::data_model::resources::as_resources::Asn;
|
||||
use rpki::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
|
||||
use rpki::rtr::cache::{CacheAvailability, Delta, Snapshot};
|
||||
use rpki::rtr::payload::Payload;
|
||||
use rpki::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski};
|
||||
use rpki::rtr::store::RtrStore;
|
||||
|
||||
#[test]
|
||||
@@ -107,6 +109,60 @@ fn store_db_versioned_state_persists_and_restores_all_versions() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_cache_state_persists_single_canonical_snapshot_for_all_versions() {
|
||||
let temp = tempfile::tempdir().unwrap();
|
||||
let store = RtrStore::open(temp.path()).unwrap();
|
||||
let source = mixed_snapshot();
|
||||
let snapshots = std::array::from_fn(|version| source.project_for_version(version as u8));
|
||||
let deltas: [Option<&Delta>; 3] = [None, None, None];
|
||||
let windows: [Option<(u32, u32)>; 3] = [None, None, None];
|
||||
let clear = [true, true, true];
|
||||
|
||||
store
|
||||
.save_cache_state_versioned(
|
||||
CacheAvailability::Ready,
|
||||
&snapshots,
|
||||
&[1, 2, 3],
|
||||
&[10, 11, 12],
|
||||
&deltas,
|
||||
&windows,
|
||||
&clear,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let restored_v0 = store.get_snapshot_for_version(0).unwrap().unwrap();
|
||||
assert_eq!(restored_v0.origins().len(), 1);
|
||||
assert_eq!(restored_v0.router_keys().len(), 0);
|
||||
assert_eq!(restored_v0.aspas().len(), 0);
|
||||
|
||||
let restored_v1 = store.get_snapshot_for_version(1).unwrap().unwrap();
|
||||
assert_eq!(restored_v1.origins().len(), 1);
|
||||
assert_eq!(restored_v1.router_keys().len(), 1);
|
||||
assert_eq!(restored_v1.aspas().len(), 0);
|
||||
|
||||
let restored_v2 = store.get_snapshot_for_version(2).unwrap().unwrap();
|
||||
assert_eq!(restored_v2.origins().len(), 1);
|
||||
assert_eq!(restored_v2.router_keys().len(), 1);
|
||||
assert_eq!(restored_v2.aspas().len(), 1);
|
||||
}
|
||||
|
||||
fn mixed_snapshot() -> Snapshot {
|
||||
Snapshot::from_payloads(vec![
|
||||
Payload::RouteOrigin(RouteOrigin::new(
|
||||
IPAddressPrefix::new(IPAddress::from_ipv4(Ipv4Addr::new(192, 0, 2, 0)), 24),
|
||||
24,
|
||||
Asn::from(64496),
|
||||
)),
|
||||
Payload::RouterKey(RouterKey::new(
|
||||
Ski::default(),
|
||||
Asn::from(64497),
|
||||
vec![1, 2, 3],
|
||||
)),
|
||||
Payload::Aspa(Aspa::new(Asn::from(64498), vec![Asn::from(64499)])),
|
||||
])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn store_db_versioned_delta_window_wraparound_is_isolated_by_version() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -371,7 +427,8 @@ fn store_db_versioned_load_delta_window_requires_complete_range() {
|
||||
.unwrap();
|
||||
|
||||
let err = store.load_delta_window_for_version(1, 10, 11).unwrap_err();
|
||||
assert!(err
|
||||
.to_string()
|
||||
.contains("delta window starts at 10, but first persisted delta is Some(11)"));
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("delta window starts at 10, but first persisted delta is Some(11)")
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user