新增slurm功能

This commit is contained in:
xiuting.xu
2026-04-01 16:24:01 +08:00
parent 8d6173f605
commit cd0330e8ae
49 changed files with 2738 additions and 1052 deletions

View File

@@ -7,18 +7,16 @@ use rustls::{ClientConfig as RustlsClientConfig, RootCertStore};
use rustls_pki_types::{CertificateDer, PrivateKeyDer, ServerName};
use tokio::io::{self as tokio_io, AsyncBufReadExt, AsyncRead, AsyncWrite, BufReader, WriteHalf};
use tokio::net::TcpStream;
use tokio::time::{timeout, Duration, Instant};
use tokio::time::{Duration, Instant, timeout};
use tokio_rustls::TlsConnector;
mod wire;
mod pretty;
mod protocol;
mod wire;
use crate::wire::{read_pdu, send_reset_query, send_serial_query};
use crate::pretty::{
parse_end_of_data_info, parse_serial_notify_serial, print_pdu, print_raw_pdu,
};
use crate::pretty::{parse_end_of_data_info, parse_serial_notify_serial, print_pdu, print_raw_pdu};
use crate::protocol::{PduHeader, PduType, QueryMode};
use crate::wire::{read_pdu, send_reset_query, send_serial_query};
const DEFAULT_READ_TIMEOUT_SECS: u64 = 30;
const DEFAULT_POLL_INTERVAL_SECS: u64 = 600;
@@ -38,7 +36,10 @@ async fn main() -> io::Result<()> {
println!("transport: {}", config.transport.describe());
println!("version : {}", config.version);
println!("timeout : {}s", config.read_timeout_secs);
println!("poll : {}s (default before EndOfData refresh is known)", config.default_poll_secs);
println!(
"poll : {}s (default before EndOfData refresh is known)",
config.default_poll_secs
);
println!("keep-after-error: {}", config.keep_after_error);
match &config.mode {
QueryMode::Reset => {
@@ -72,11 +73,7 @@ async fn main() -> io::Result<()> {
}
Err(err) => {
let delay = state.reconnect_delay_secs();
eprintln!(
"connect failed: {}. retry after {}s",
err,
delay
);
eprintln!("connect failed: {}. retry after {}s", err, delay);
tokio::time::sleep(Duration::from_secs(delay)).await;
}
}
@@ -171,10 +168,7 @@ async fn main() -> io::Result<()> {
if reconnect {
let delay = state.reconnect_delay_secs();
state.current_session_id = None;
println!(
"[reconnect] transport disconnected, retry after {}s",
delay
);
println!("[reconnect] transport disconnected, retry after {}s", delay);
tokio::time::sleep(Duration::from_secs(delay)).await;
}
}
@@ -189,8 +183,7 @@ async fn send_resume_query(
(Some(session_id), Some(serial)) => {
println!(
"reconnected, send Serial Query with session_id={}, serial={}",
session_id,
serial
session_id, serial
);
send_serial_query(writer, state.version, session_id, serial).await?;
}
@@ -294,26 +287,20 @@ async fn handle_incoming_pdu(
println!();
println!(
"[notify] received Serial Notify: session_id={}, notify_serial={:?}",
notify_session_id,
notify_serial
notify_session_id, notify_serial
);
match (state.session_id, state.serial, notify_serial) {
(Some(current_session_id), Some(current_serial), Some(_new_serial))
if current_session_id == notify_session_id =>
{
println!(
"received Serial Notify for current session {}, send Serial Query with serial {}",
current_session_id, current_serial
);
send_serial_query(
writer,
state.version,
current_session_id,
current_serial,
)
.await?;
}
if current_session_id == notify_session_id =>
{
println!(
"received Serial Notify for current session {}, send Serial Query with serial {}",
current_session_id, current_serial
);
send_serial_query(writer, state.version, current_session_id, current_serial)
.await?;
}
_ => {
println!(
@@ -366,10 +353,7 @@ async fn handle_incoming_pdu(
Ok(())
}
async fn handle_poll_tick(
writer: &mut ClientWriter,
state: &mut ClientState,
) -> io::Result<()> {
async fn handle_poll_tick(writer: &mut ClientWriter, state: &mut ClientState) -> io::Result<()> {
println!();
println!(
"[auto-poll] timer fired (interval={}s)",
@@ -422,23 +406,21 @@ async fn handle_console_command(
state.schedule_next_poll();
}
["serial"] => {
match (state.session_id, state.serial) {
(Some(session_id), Some(serial)) => {
println!(
"manual command: send Serial Query with current state: session_id={}, serial={}",
session_id, serial
);
send_serial_query(writer, state.version, session_id, serial).await?;
state.schedule_next_poll();
}
_ => {
println!(
"manual command failed: current session_id/serial not available, use `reset` or `serial <session_id> <serial>`"
);
}
["serial"] => match (state.session_id, state.serial) {
(Some(session_id), Some(serial)) => {
println!(
"manual command: send Serial Query with current state: session_id={}, serial={}",
session_id, serial
);
send_serial_query(writer, state.version, session_id, serial).await?;
state.schedule_next_poll();
}
}
_ => {
println!(
"manual command failed: current session_id/serial not available, use `reset` or `serial <session_id> <serial>`"
);
}
},
["serial", session_id, serial] => {
let session_id = match session_id.parse::<u16>() {
@@ -493,7 +475,10 @@ async fn handle_console_command(
"current effective poll interval: {}s",
state.effective_poll_secs()
);
println!("poll interval source : {}", state.poll_interval_source());
println!(
"poll interval source : {}",
state.poll_interval_source()
);
println!("stored refresh hint : {:?}", state.refresh);
println!("default poll interval : {}s", state.default_poll_secs);
println!("last_error_code : {:?}", state.last_error_code);
@@ -626,17 +611,20 @@ impl ClientState {
fn effective_poll_secs(&self) -> u64 {
if self.should_prefer_retry_poll() {
self.retry
.map(|v| v as u64)
.unwrap_or_else(|| self.refresh.map(|v| v as u64).unwrap_or(self.default_poll_secs))
self.retry.map(|v| v as u64).unwrap_or_else(|| {
self.refresh
.map(|v| v as u64)
.unwrap_or(self.default_poll_secs)
})
} else {
self.refresh.map(|v| v as u64).unwrap_or(self.default_poll_secs)
self.refresh
.map(|v| v as u64)
.unwrap_or(self.default_poll_secs)
}
}
fn schedule_next_poll(&mut self) {
self.next_poll_deadline =
Instant::now() + Duration::from_secs(self.effective_poll_secs());
self.next_poll_deadline = Instant::now() + Duration::from_secs(self.effective_poll_secs());
}
fn pause_auto_poll(&mut self) {
@@ -728,7 +716,10 @@ impl Config {
}
"--server-name" => {
let name = args.next().ok_or_else(|| {
io::Error::new(io::ErrorKind::InvalidInput, "--server-name requires a value")
io::Error::new(
io::ErrorKind::InvalidInput,
"--server-name requires a value",
)
})?;
ensure_tls_config(&mut transport)?.server_name = Some(name);
}
@@ -805,10 +796,7 @@ impl Config {
let serial = positional
.next()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
"serial mode requires serial",
)
io::Error::new(io::ErrorKind::InvalidInput, "serial mode requires serial")
})?
.parse::<u32>()
.map_err(|e| {
@@ -949,9 +937,15 @@ async fn connect_tls_stream(addr: &str, tls: &TlsConfig) -> io::Result<DynStream
format!("invalid TLS server name '{}': {}", server_name_str, err),
)
})?;
let tls_stream = connector.connect(server_name, stream).await.map_err(|err| {
io::Error::new(io::ErrorKind::ConnectionAborted, format!("TLS handshake failed: {}", err))
})?;
let tls_stream = connector
.connect(server_name, stream)
.await
.map_err(|err| {
io::Error::new(
io::ErrorKind::ConnectionAborted,
format!("TLS handshake failed: {}", err),
)
})?;
Ok(Box::new(tls_stream))
}
@@ -966,7 +960,10 @@ fn build_tls_connector(tls: &TlsConfig) -> io::Result<TlsConnector> {
if added == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("no valid CA certificates found in {}", ca_cert_path.display()),
format!(
"no valid CA certificates found in {}",
ca_cert_path.display()
),
));
}

View File

@@ -1,9 +1,8 @@
use std::net::{Ipv4Addr, Ipv6Addr};
use crate::protocol::{
flag_meaning, hex_bytes, PduHeader, PduType, ASPA_FIXED_BODY_LEN,
END_OF_DATA_V0_BODY_LEN, END_OF_DATA_V1_BODY_LEN, IPV4_PREFIX_BODY_LEN,
IPV6_PREFIX_BODY_LEN, ROUTER_KEY_FIXED_BODY_LEN,
ASPA_FIXED_BODY_LEN, END_OF_DATA_V0_BODY_LEN, END_OF_DATA_V1_BODY_LEN, IPV4_PREFIX_BODY_LEN,
IPV6_PREFIX_BODY_LEN, PduHeader, PduType, ROUTER_KEY_FIXED_BODY_LEN, flag_meaning, hex_bytes,
};
pub fn print_pdu(header: &PduHeader, body: &[u8]) {
@@ -143,8 +142,7 @@ fn print_error_report(header: &PduHeader, body: &[u8]) {
return;
}
let encapsulated_len =
u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
let encapsulated_len = u32::from_be_bytes([body[0], body[1], body[2], body[3]]) as usize;
if body.len() < 4 + encapsulated_len + 4 {
println!("invalid ErrorReport: truncated encapsulated PDU");

View File

@@ -2,9 +2,7 @@ use std::io;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use crate::protocol::{
PduHeader, PduType, RawPdu, HEADER_LEN, MAX_PDU_LEN, SERIAL_QUERY_LEN,
};
use crate::protocol::{HEADER_LEN, MAX_PDU_LEN, PduHeader, PduType, RawPdu, SERIAL_QUERY_LEN};
pub async fn send_reset_query<S>(stream: &mut S, version: u8) -> io::Result<()>
where
@@ -56,10 +54,7 @@ where
if header.length < HEADER_LEN as u32 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"invalid PDU length {} < {}",
header.length, HEADER_LEN
),
format!("invalid PDU length {} < {}", header.length, HEADER_LEN),
));
}
@@ -78,4 +73,4 @@ where
stream.read_exact(&mut body).await?;
Ok(RawPdu { header, body })
}
}

View File

@@ -0,0 +1,168 @@
use std::env;
use std::fs::File;
use std::path::PathBuf;
use anyhow::{Context, Result, anyhow};
use serde::Serialize;
use rpki::rtr::payload::Payload;
use rpki::slurm::file::SlurmFile;
use rpki::source::ccr::{
find_latest_ccr_file, load_ccr_snapshot_from_file, snapshot_to_payloads_with_options,
};
#[derive(Debug)]
struct Cli {
ccr_path: PathBuf,
slurm_path: PathBuf,
strict_ccr: bool,
dump_payloads: bool,
}
#[derive(Debug, Serialize)]
struct Output {
ccr_path: String,
slurm_path: String,
produced_at: Option<String>,
slurm_version: u32,
input_payload_count: usize,
input_vrp_count: usize,
input_vap_count: usize,
output_payload_count: usize,
output_vrp_count: usize,
output_vap_count: usize,
invalid_vrps: Vec<String>,
invalid_vaps: Vec<String>,
sample_output_aspa_customers: Vec<u32>,
payloads: Option<Vec<Payload>>,
}
fn main() -> Result<()> {
let cli = parse_args(env::args().skip(1))?;
let snapshot = load_ccr_snapshot_from_file(&cli.ccr_path)
.with_context(|| format!("failed to load CCR snapshot: {}", cli.ccr_path.display()))?;
let slurm = load_slurm(&cli.slurm_path)?;
let conversion = snapshot_to_payloads_with_options(&snapshot, cli.strict_ccr)?;
let payloads = slurm.apply(&conversion.payloads);
let (input_vrp_count, input_vap_count) = count_vrps_and_vaps(&conversion.payloads);
let (output_vrp_count, output_vap_count) = count_vrps_and_vaps(&payloads);
let output = Output {
ccr_path: cli.ccr_path.display().to_string(),
slurm_path: cli.slurm_path.display().to_string(),
produced_at: snapshot.produced_at.clone(),
slurm_version: slurm.version().as_u32(),
input_payload_count: conversion.payloads.len(),
input_vrp_count,
input_vap_count,
output_payload_count: payloads.len(),
output_vrp_count,
output_vap_count,
invalid_vrps: conversion.invalid_vrps,
invalid_vaps: conversion.invalid_vaps,
sample_output_aspa_customers: sample_aspa_customers(&payloads, 8),
payloads: cli.dump_payloads.then_some(payloads),
};
println!("{}", serde_json::to_string_pretty(&output)?);
Ok(())
}
fn load_slurm(path: &PathBuf) -> Result<SlurmFile> {
let file = File::open(path)
.with_context(|| format!("failed to open SLURM file: {}", path.display()))?;
SlurmFile::from_reader(file)
.with_context(|| format!("failed to parse SLURM file: {}", path.display()))
}
fn parse_args(args: impl Iterator<Item = String>) -> Result<Cli> {
let mut strict_ccr = false;
let mut dump_payloads = false;
let mut positionals = Vec::new();
for arg in args {
match arg.as_str() {
"--strict-ccr" => strict_ccr = true,
"--dump-payloads" => dump_payloads = true,
"-h" | "--help" => {
print_help();
std::process::exit(0);
}
_ if arg.starts_with('-') => {
return Err(anyhow!("unknown option: {}", arg));
}
_ => positionals.push(arg),
}
}
if positionals.is_empty() {
return Ok(Cli {
ccr_path: find_latest_ccr_file("data")
.context("failed to find latest .ccr in ./data for default run")?,
slurm_path: PathBuf::from("data/example.slurm"),
strict_ccr,
dump_payloads,
});
}
if positionals.len() != 2 {
print_help();
return Err(anyhow!(
"expected: slurm_apply_client <snapshot.ccr> <policy.slurm>"
));
}
Ok(Cli {
ccr_path: PathBuf::from(&positionals[0]),
slurm_path: PathBuf::from(&positionals[1]),
strict_ccr,
dump_payloads,
})
}
fn print_help() {
eprintln!(
"Usage: cargo run --bin slurm_apply_client -- [--strict-ccr] [--dump-payloads] <snapshot.ccr> <policy.slurm>"
);
eprintln!();
eprintln!("Reads a CCR snapshot, converts it into payloads, applies SLURM, and prints JSON.");
eprintln!(
"When no arguments are provided, it defaults to the latest .ccr under ./data and ./data/example.slurm."
);
eprintln!("Use --dump-payloads to include the full payload list in the JSON output.");
}
fn count_vrps_and_vaps(payloads: &[Payload]) -> (usize, usize) {
let mut vrps = 0;
let mut vaps = 0;
for payload in payloads {
match payload {
Payload::RouteOrigin(_) => vrps += 1,
Payload::Aspa(_) => vaps += 1,
Payload::RouterKey(_) => {}
}
}
(vrps, vaps)
}
fn sample_aspa_customers(payloads: &[Payload], limit: usize) -> Vec<u32> {
let mut customers = Vec::new();
for payload in payloads {
if let Payload::Aspa(aspa) = payload {
let customer = aspa.customer_asn().into_u32();
if !customers.contains(&customer) {
customers.push(customer);
if customers.len() == limit {
break;
}
}
}
}
customers
}

View File

@@ -1,3 +1,4 @@
pub mod data_model;
mod slurm;
pub mod slurm;
pub mod rtr;
pub mod source;

View File

@@ -3,15 +3,15 @@ use std::net::SocketAddr;
use std::sync::{Arc, RwLock};
use std::time::Duration;
use anyhow::{anyhow, Result};
use anyhow::{Result, anyhow};
use tokio::task::JoinHandle;
use tracing::{info, warn};
use rpki::rtr::ccr::{find_latest_ccr_file, load_ccr_payloads_from_file_with_options, load_ccr_snapshot_from_file};
use rpki::rtr::cache::{RtrCache, SharedRtrCache};
use rpki::rtr::payload::Timing;
use rpki::rtr::server::{RtrNotifier, RtrService, RtrServiceConfig, RunningRtrService};
use rpki::rtr::store::RtrStore;
use rpki::source::pipeline::{PayloadLoadConfig, load_payloads_from_latest_sources};
#[derive(Debug, Clone)]
struct AppConfig {
@@ -21,6 +21,7 @@ struct AppConfig {
db_path: String,
ccr_dir: String,
slurm_dir: Option<String>,
tls_cert_path: String,
tls_key_path: String,
tls_client_ca_path: String,
@@ -42,6 +43,7 @@ impl Default for AppConfig {
db_path: "./rtr-db".to_string(),
ccr_dir: "./data".to_string(),
slurm_dir: None,
tls_cert_path: "./certs/server.crt".to_string(),
tls_key_path: "./certs/server.key".to_string(),
tls_client_ca_path: "./certs/client-ca.crt".to_string(),
@@ -85,6 +87,14 @@ impl AppConfig {
if let Some(value) = env_var("RPKI_RTR_CCR_DIR")? {
config.ccr_dir = value;
}
if let Some(value) = env_var("RPKI_RTR_SLURM_DIR")? {
let value = value.trim();
config.slurm_dir = if value.is_empty() {
None
} else {
Some(value.to_string())
};
}
if let Some(value) = env_var("RPKI_RTR_TLS_CERT_PATH")? {
config.tls_cert_path = value;
}
@@ -104,8 +114,7 @@ impl AppConfig {
parse_bool(&value, "RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")?;
}
if let Some(value) = env_var("RPKI_RTR_STRICT_CCR_VALIDATION")? {
config.strict_ccr_validation =
parse_bool(&value, "RPKI_RTR_STRICT_CCR_VALIDATION")?;
config.strict_ccr_validation = parse_bool(&value, "RPKI_RTR_STRICT_CCR_VALIDATION")?;
}
if let Some(value) = env_var("RPKI_RTR_REFRESH_INTERVAL_SECS")? {
let secs: u64 = value.parse().map_err(|err| {
@@ -118,9 +127,9 @@ impl AppConfig {
config.refresh_interval = Duration::from_secs(secs);
}
if let Some(value) = env_var("RPKI_RTR_MAX_CONNECTIONS")? {
config.service_config.max_connections = value.parse().map_err(|err| {
anyhow!("invalid RPKI_RTR_MAX_CONNECTIONS '{}': {}", value, err)
})?;
config.service_config.max_connections = value
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_MAX_CONNECTIONS '{}': {}", value, err))?;
}
if let Some(value) = env_var("RPKI_RTR_NOTIFY_QUEUE_SIZE")? {
config.service_config.notify_queue_size = value.parse().map_err(|err| {
@@ -184,12 +193,17 @@ fn open_store(config: &AppConfig) -> Result<RtrStore> {
}
fn init_shared_cache(config: &AppConfig, store: &RtrStore) -> Result<SharedRtrCache> {
let payload_load_config = PayloadLoadConfig {
ccr_dir: config.ccr_dir.clone(),
slurm_dir: config.slurm_dir.clone(),
strict_ccr_validation: config.strict_ccr_validation,
};
let initial_cache = RtrCache::default().init(
store,
config.max_delta,
config.prune_delta_by_snapshot_size,
Timing::default(),
|| load_payloads_from_latest_ccr(&config.ccr_dir, config.strict_ccr_validation),
|| load_payloads_from_latest_sources(&payload_load_config),
)?;
let shared_cache: SharedRtrCache = Arc::new(RwLock::new(initial_cache));
@@ -232,8 +246,11 @@ fn spawn_refresh_task(
notifier: RtrNotifier,
) -> JoinHandle<()> {
let refresh_interval = config.refresh_interval;
let ccr_dir = config.ccr_dir.clone();
let strict_ccr_validation = config.strict_ccr_validation;
let payload_load_config = PayloadLoadConfig {
ccr_dir: config.ccr_dir.clone(),
slurm_dir: config.slurm_dir.clone(),
strict_ccr_validation: config.strict_ccr_validation,
};
tokio::spawn(async move {
let mut interval = tokio::time::interval(refresh_interval);
@@ -241,7 +258,7 @@ fn spawn_refresh_task(
loop {
interval.tick().await;
match load_payloads_from_latest_ccr(&ccr_dir, strict_ccr_validation) {
match load_payloads_from_latest_sources(&payload_load_config) {
Ok(payloads) => {
let payload_count = payloads.len();
let updated = {
@@ -261,7 +278,7 @@ fn spawn_refresh_task(
if new_serial != old_serial {
info!(
"RTR cache refresh applied: ccr_dir={}, payload_count={}, old_serial={}, new_serial={}",
ccr_dir,
payload_load_config.ccr_dir,
payload_count,
old_serial,
new_serial
@@ -270,9 +287,7 @@ fn spawn_refresh_task(
} else {
info!(
"RTR cache refresh found no change: ccr_dir={}, payload_count={}, serial={}",
ccr_dir,
payload_count,
old_serial
payload_load_config.ccr_dir, payload_count, old_serial
);
false
}
@@ -290,7 +305,10 @@ fn spawn_refresh_task(
}
}
Err(err) => {
warn!("failed to reload CCR payloads from {}: {:?}", ccr_dir, err);
warn!(
"failed to reload CCR/SLURM payloads from {}: {:?}",
payload_load_config.ccr_dir, err
);
}
}
}
@@ -317,16 +335,17 @@ fn log_startup_config(config: &AppConfig) {
}
info!("ccr_dir={}", config.ccr_dir);
info!(
"slurm_dir={}",
config.slurm_dir.as_deref().unwrap_or("disabled")
);
info!("max_delta={}", config.max_delta);
info!("strict_ccr_validation={}", config.strict_ccr_validation);
info!(
"refresh_interval_secs={}",
config.refresh_interval.as_secs()
);
info!(
"max_connections={}",
config.service_config.max_connections
);
info!("max_connections={}", config.service_config.max_connections);
info!(
"notify_queue_size={}",
config.service_config.notify_queue_size
@@ -372,50 +391,3 @@ fn parse_bool(value: &str, name: &str) -> Result<bool> {
_ => Err(anyhow!("invalid {} '{}': expected boolean", name, value)),
}
}
fn load_payloads_from_latest_ccr(
ccr_dir: &str,
strict_ccr_validation: bool,
) -> Result<Vec<rpki::rtr::payload::Payload>> {
let latest = find_latest_ccr_file(ccr_dir)?;
let snapshot = load_ccr_snapshot_from_file(&latest)?;
let vrp_count = snapshot.vrps.len();
let vap_count = snapshot.vaps.len();
let produced_at = snapshot.produced_at.clone();
let conversion = load_ccr_payloads_from_file_with_options(&latest, strict_ccr_validation)?;
let payloads = conversion.payloads;
if !conversion.invalid_vrps.is_empty() {
warn!(
"CCR load skipped invalid VRPs: file={}, skipped={}, samples={:?}",
latest.display(),
conversion.invalid_vrps.len(),
sample_messages(&conversion.invalid_vrps)
);
}
if !conversion.invalid_vaps.is_empty() {
warn!(
"CCR load skipped invalid VAPs/ASPAs: file={}, skipped={}, samples={:?}",
latest.display(),
conversion.invalid_vaps.len(),
sample_messages(&conversion.invalid_vaps)
);
}
info!(
"loaded latest CCR snapshot: file={}, produced_at={:?}, vrp_count={}, vap_count={}, payload_count={}, strict_ccr_validation={}",
latest.display(),
produced_at,
vrp_count,
vap_count,
payloads.len(),
strict_ccr_validation
);
Ok(payloads)
}
fn sample_messages(messages: &[String]) -> Vec<&str> {
messages.iter().take(3).map(String::as_str).collect()
}

53
src/rtr/cache/core.rs vendored
View File

@@ -1,14 +1,14 @@
use std::collections::{BTreeMap, VecDeque};
use std::cmp::Ordering;
use std::sync::Arc;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::cmp::Ordering;
use std::collections::{BTreeMap, VecDeque};
use std::sync::Arc;
use tracing::{debug, info, warn};
use crate::rtr::payload::{Payload, Timing};
use super::model::{Delta, DualTime, Snapshot};
use super::ordering::{change_key, ChangeKey};
use super::ordering::{ChangeKey, change_key};
const SERIAL_HALF_RANGE: u32 = 1 << 31;
@@ -166,9 +166,7 @@ impl RtrCacheBuilder {
let serial = self.serial.unwrap_or(0);
let created_at = self.created_at.unwrap_or_else(|| now.clone());
let availability = self.availability.unwrap_or(CacheAvailability::Ready);
let session_ids = self
.session_ids
.unwrap_or_else(SessionIds::random_distinct);
let session_ids = self.session_ids.unwrap_or_else(SessionIds::random_distinct);
RtrCache {
availability,
@@ -235,8 +233,7 @@ impl RtrCache {
self.serial = self.serial.wrapping_add(1);
debug!(
"RTR cache advanced serial: old_serial={}, new_serial={}",
old,
self.serial
old, self.serial
);
self.serial
}
@@ -251,9 +248,7 @@ impl RtrCache {
let snapshot_wire_size = estimate_snapshot_payload_wire_size(&self.snapshot);
let mut cumulative_delta_wire_size =
estimate_delta_window_payload_wire_size(&self.deltas);
while !self.deltas.is_empty()
&& cumulative_delta_wire_size >= snapshot_wire_size
{
while !self.deltas.is_empty() && cumulative_delta_wire_size >= snapshot_wire_size {
if let Some(oldest) = self.deltas.pop_front() {
dropped_serials.push(oldest.serial());
cumulative_delta_wire_size =
@@ -262,9 +257,7 @@ impl RtrCache {
}
debug!(
"RTR cache delta-size pruning evaluated: snapshot_wire_size={}, cumulative_delta_wire_size={}, dropped_serials={:?}",
snapshot_wire_size,
cumulative_delta_wire_size,
dropped_serials
snapshot_wire_size, cumulative_delta_wire_size, dropped_serials
);
}
debug!(
@@ -292,7 +285,10 @@ impl RtrCache {
}
}
pub(super) fn apply_update(&mut self, new_payloads: Vec<Payload>) -> Result<Option<AppliedUpdate>> {
pub(super) fn apply_update(
&mut self,
new_payloads: Vec<Payload>,
) -> Result<Option<AppliedUpdate>> {
self.last_update_begin = DualTime::now();
info!(
"RTR cache applying update: availability={:?}, current_serial={}, incoming_payloads={}",
@@ -319,14 +315,15 @@ impl RtrCache {
self.last_update_end = DualTime::now();
if !changed {
debug!("RTR cache update produced empty snapshot but cache was already unavailable; no state change");
debug!(
"RTR cache update produced empty snapshot but cache was already unavailable; no state change"
);
return Ok(None);
}
info!(
"RTR cache update cleared usable data and marked cache unavailable: serial={}, session_ids={:?}",
self.serial,
self.session_ids
self.serial, self.session_ids
);
return Ok(Some(AppliedUpdate {
@@ -349,8 +346,7 @@ impl RtrCache {
self.last_update_end = DualTime::now();
debug!(
"RTR cache update detected identical snapshot content: serial={}, session_ids={:?}",
self.serial,
self.session_ids
self.serial, self.session_ids
);
return Ok(None);
}
@@ -455,8 +451,7 @@ impl RtrCache {
if client_serial == self.serial {
debug!(
"RTR cache delta query is already up to date: client_serial={}, cache_serial={}",
client_serial,
self.serial
client_serial, self.serial
);
return SerialResult::UpToDate;
}
@@ -467,8 +462,7 @@ impl RtrCache {
) {
warn!(
"RTR cache delta query requires reset due to invalid/newer client serial: client_serial={}, cache_serial={}",
client_serial,
self.serial
client_serial, self.serial
);
return SerialResult::ResetRequired;
}
@@ -489,8 +483,7 @@ impl RtrCache {
if deltas.is_empty() {
debug!(
"RTR cache delta query resolved to no deltas: client_serial={}, cache_serial={}",
client_serial,
self.serial
client_serial, self.serial
);
return SerialResult::UpToDate;
}
@@ -633,7 +626,11 @@ fn estimate_payload_wire_size(payload: &Payload, announce: bool) -> usize {
},
Payload::RouterKey(key) => 8 + 20 + 4 + key.spki().len(),
Payload::Aspa(aspa) => {
let providers = if announce { aspa.provider_asns().len() } else { 0 };
let providers = if announce {
aspa.provider_asns().len()
} else {
0
};
8 + 4 + providers * 4
}
}

View File

@@ -195,7 +195,12 @@ impl Snapshot {
}
if !self.same_aspas(new_snapshot) {
diff_aspas(&self.aspas, &new_snapshot.aspas, &mut announced, &mut withdrawn);
diff_aspas(
&self.aspas,
&new_snapshot.aspas,
&mut announced,
&mut withdrawn,
);
}
(announced, withdrawn)
@@ -206,9 +211,8 @@ impl Snapshot {
}
pub fn payloads(&self) -> Vec<Payload> {
let mut v = Vec::with_capacity(
self.origins.len() + self.router_keys.len() + self.aspas.len(),
);
let mut v =
Vec::with_capacity(self.origins.len() + self.router_keys.len() + self.aspas.len());
v.extend(self.origins.iter().cloned().map(Payload::RouteOrigin));
v.extend(self.router_keys.iter().cloned().map(Payload::RouterKey));
@@ -268,9 +272,7 @@ impl Snapshot {
}
pub fn is_empty(&self) -> bool {
self.origins.is_empty()
&& self.router_keys.is_empty()
&& self.aspas.is_empty()
self.origins.is_empty() && self.router_keys.is_empty() && self.aspas.is_empty()
}
}

View File

@@ -64,8 +64,18 @@ enum PayloadPduType {
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd)]
pub(crate) enum RouteOriginKey {
V4 { addr: u32, plen: u8, mlen: u8, asn: u32 },
V6 { addr: u128, plen: u8, mlen: u8, asn: u32 },
V4 {
addr: u32,
plen: u8,
mlen: u8,
asn: u32,
},
V6 {
addr: u128,
plen: u8,
mlen: u8,
asn: u32,
},
}
pub(crate) fn change_key(payload: &Payload) -> ChangeKey {
@@ -287,7 +297,11 @@ fn payload_brief(payload: &Payload) -> String {
match payload {
Payload::RouteOrigin(origin) => format!(
"{} prefix {:?}/{} max={} asn={}",
if route_origin_is_ipv4(origin) { "IPv4" } else { "IPv6" },
if route_origin_is_ipv4(origin) {
"IPv4"
} else {
"IPv6"
},
origin.prefix().address,
origin.prefix().prefix_length,
origin.max_length(),

View File

@@ -18,12 +18,9 @@ impl RtrCache {
timing: Timing,
file_loader: impl Fn() -> Result<Vec<Payload>>,
) -> Result<Self> {
if let Some(cache) = try_restore_from_store(
store,
max_delta,
prune_delta_by_snapshot_size,
timing,
)? {
if let Some(cache) =
try_restore_from_store(store, max_delta, prune_delta_by_snapshot_size, timing)?
{
tracing::info!(
"RTR cache restored from store: availability={:?}, session_ids={:?}, serial={}, snapshot(route_origins={}, router_keys={}, aspas={})",
cache.availability(),

View File

@@ -19,7 +19,6 @@ pub enum ErrorCode {
}
impl ErrorCode {
#[inline]
pub fn as_u16(self) -> u16 {
self as u16
@@ -27,41 +26,29 @@ impl ErrorCode {
pub fn description(self) -> &'static str {
match self {
ErrorCode::CorruptData =>
"Corrupt Data",
ErrorCode::CorruptData => "Corrupt Data",
ErrorCode::InternalError =>
"Internal Error",
ErrorCode::InternalError => "Internal Error",
ErrorCode::NoDataAvailable =>
"No Data Available",
ErrorCode::NoDataAvailable => "No Data Available",
ErrorCode::InvalidRequest =>
"Invalid Request",
ErrorCode::InvalidRequest => "Invalid Request",
ErrorCode::UnsupportedProtocolVersion =>
"Unsupported Protocol Version",
ErrorCode::UnsupportedProtocolVersion => "Unsupported Protocol Version",
ErrorCode::UnsupportedPduType =>
"Unsupported PDU Type",
ErrorCode::UnsupportedPduType => "Unsupported PDU Type",
ErrorCode::WithdrawalOfUnknownRecord =>
"Withdrawal of Unknown Record",
ErrorCode::WithdrawalOfUnknownRecord => "Withdrawal of Unknown Record",
ErrorCode::DuplicateAnnouncement =>
"Duplicate Announcement Received",
ErrorCode::DuplicateAnnouncement => "Duplicate Announcement Received",
ErrorCode::UnexpectedProtocolVersion =>
"Unexpected Protocol Version",
ErrorCode::UnexpectedProtocolVersion => "Unexpected Protocol Version",
ErrorCode::AspaProviderListError =>
"ASPA Provider List Error",
ErrorCode::AspaProviderListError => "ASPA Provider List Error",
ErrorCode::TransportFailed =>
"Transport Failed",
ErrorCode::TransportFailed => "Transport Failed",
ErrorCode::OrderingError =>
"Ordering Error",
ErrorCode::OrderingError => "Ordering Error",
}
}
}
@@ -90,9 +77,6 @@ impl TryFrom<u16> for ErrorCode {
impl fmt::Display for ErrorCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})",
self.description(),
*self as u16
)
write!(f, "{} ({})", self.description(), *self as u16)
}
}

View File

@@ -3,7 +3,7 @@ use std::net::IpAddr;
use std::path::Path;
use std::str::FromStr;
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use crate::data_model::resources::as_resources::Asn;
use crate::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
@@ -131,10 +131,9 @@ pub fn parse_vrp_line(line: &str) -> Result<ParsedVrp> {
}
let prefix_part = parts[0];
let max_len = u8::from_str(parts[1])
.with_context(|| format!("invalid max_len: {}", parts[1]))?;
let asn = u32::from_str(parts[2])
.with_context(|| format!("invalid asn: {}", parts[2]))?;
let max_len =
u8::from_str(parts[1]).with_context(|| format!("invalid max_len: {}", parts[1]))?;
let asn = u32::from_str(parts[2]).with_context(|| format!("invalid asn: {}", parts[2]))?;
let (addr_str, prefix_len_str) = prefix_part
.split_once('/')
@@ -164,14 +163,13 @@ pub fn parse_aspa_line(line: &str) -> Result<ParsedAspa> {
));
}
let customer_asn = u32::from_str(parts[0])
.with_context(|| format!("invalid customer_asn: {}", parts[0]))?;
let customer_asn =
u32::from_str(parts[0]).with_context(|| format!("invalid customer_asn: {}", parts[0]))?;
let provider_asns = parts[1]
.split_whitespace()
.map(|provider| {
u32::from_str(provider)
.with_context(|| format!("invalid provider_asn: {}", provider))
u32::from_str(provider).with_context(|| format!("invalid provider_asn: {}", provider))
})
.collect::<Result<Vec<_>>>()?;
@@ -186,23 +184,18 @@ pub fn parse_aspa_line(line: &str) -> Result<ParsedAspa> {
pub fn parse_router_key_line(line: &str) -> Result<ParsedRouterKey> {
let parts: Vec<_> = line.split(',').map(|s| s.trim()).collect();
if parts.len() != 3 {
return Err(anyhow!(
"expected format: <ski_hex>,<asn>,<spki_hex>"
));
return Err(anyhow!("expected format: <ski_hex>,<asn>,<spki_hex>"));
}
let ski_vec = decode_hex(parts[0])
.with_context(|| format!("invalid SKI hex: {}", parts[0]))?;
let ski_vec = decode_hex(parts[0]).with_context(|| format!("invalid SKI hex: {}", parts[0]))?;
if ski_vec.len() != 20 {
return Err(anyhow!("SKI must be exactly 20 bytes"));
}
let mut ski = [0u8; 20];
ski.copy_from_slice(&ski_vec);
let asn = u32::from_str(parts[1])
.with_context(|| format!("invalid asn: {}", parts[1]))?;
let spki = decode_hex(parts[2])
.with_context(|| format!("invalid SPKI hex: {}", parts[2]))?;
let asn = u32::from_str(parts[1]).with_context(|| format!("invalid asn: {}", parts[1]))?;
let spki = decode_hex(parts[2]).with_context(|| format!("invalid SPKI hex: {}", parts[2]))?;
validate_router_key(asn, &spki)?;
@@ -254,13 +247,9 @@ fn validate_aspa(customer_asn: u32, provider_asns: &[u32]) -> Result<()> {
}
fn validate_router_key(asn: u32, spki: &[u8]) -> Result<()> {
crate::rtr::payload::RouterKey::new(
Ski::default(),
Asn::from(asn),
spki.to_vec(),
)
.validate()
.map_err(|err| anyhow!(err.to_string()))?;
crate::rtr::payload::RouterKey::new(Ski::default(), Asn::from(asn), spki.to_vec())
.validate()
.map_err(|err| anyhow!(err.to_string()))?;
Ok(())
}
@@ -309,4 +298,3 @@ fn decode_hex(input: &str) -> Result<Vec<u8>> {
})
.collect()
}

View File

@@ -1,10 +1,9 @@
pub mod pdu;
pub mod cache;
pub mod payload;
pub mod store;
pub mod session;
pub mod error_type;
pub mod state;
pub mod server;
pub mod loader;
pub mod ccr;
pub mod payload;
pub mod pdu;
pub mod server;
pub mod session;
pub mod state;
pub mod store;

View File

@@ -1,13 +1,12 @@
use crate::data_model::resources::as_resources::Asn;
use crate::data_model::resources::ip_resources::IPAddressPrefix;
use serde::{Deserialize, Serialize};
use std::fmt::Debug;
use std::io;
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::data_model::resources::as_resources::Asn;
use crate::data_model::resources::ip_resources::IPAddressPrefix;
use x509_parser::prelude::FromDer;
use x509_parser::x509::SubjectPublicKeyInfo;
#[derive(Clone, Copy, Debug, Eq, PartialEq, Ord, PartialOrd)]
enum PayloadPduType {
Ipv4Prefix = 4,
@@ -16,7 +15,9 @@ enum PayloadPduType {
Aspa = 11,
}
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Ord, PartialOrd, Serialize, Deserialize,
)]
pub struct Ski([u8; 20]);
impl AsRef<[u8]> for Ski {
@@ -60,7 +61,6 @@ impl RouteOrigin {
}
}
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Serialize, Deserialize)]
pub struct RouterKey {
subject_key_identifier: Ski,
@@ -104,8 +104,8 @@ impl RouterKey {
));
}
let (rem, _) = SubjectPublicKeyInfo::from_der(&self.subject_public_key_info)
.map_err(|err| {
let (rem, _) =
SubjectPublicKeyInfo::from_der(&self.subject_public_key_info).map_err(|err| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("RouterKey SPKI is not valid DER: {err}"),
@@ -115,10 +115,7 @@ impl RouterKey {
if !rem.is_empty() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"RouterKey SPKI DER has trailing bytes: {}",
rem.len()
),
format!("RouterKey SPKI DER has trailing bytes: {}", rem.len()),
));
}
@@ -177,7 +174,6 @@ impl Aspa {
}
}
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))]
pub enum Payload {
@@ -191,7 +187,6 @@ pub enum Payload {
Aspa(Aspa),
}
// Timing
#[derive(Clone, Copy, Debug)]
pub struct Timing {
@@ -202,7 +197,7 @@ pub struct Timing {
pub retry: u32,
/// The number of secionds before data expires if not refreshed.
pub expire: u32
pub expire: u32,
}
impl Timing {
@@ -214,7 +209,11 @@ impl Timing {
pub const MAX_EXPIRE: u32 = 172_800;
pub const fn new(refresh: u32, retry: u32, expire: u32) -> Self {
Self { refresh, retry, expire }
Self {
refresh,
retry,
expire,
}
}
pub fn validate(self) -> Result<(), io::Error> {
@@ -223,7 +222,9 @@ impl Timing {
io::ErrorKind::InvalidData,
format!(
"refresh interval {} out of range {}..={}",
self.refresh, Self::MIN_REFRESH, Self::MAX_REFRESH
self.refresh,
Self::MIN_REFRESH,
Self::MAX_REFRESH
),
));
}
@@ -233,7 +234,9 @@ impl Timing {
io::ErrorKind::InvalidData,
format!(
"retry interval {} out of range {}..={}",
self.retry, Self::MIN_RETRY, Self::MAX_RETRY
self.retry,
Self::MIN_RETRY,
Self::MAX_RETRY
),
));
}
@@ -243,7 +246,9 @@ impl Timing {
io::ErrorKind::InvalidData,
format!(
"expire interval {} out of range {}..={}",
self.expire, Self::MIN_EXPIRE, Self::MAX_EXPIRE
self.expire,
Self::MIN_EXPIRE,
Self::MAX_EXPIRE
),
));
}
@@ -282,7 +287,6 @@ impl Timing {
pub fn expire(self) -> Duration {
Duration::from_secs(u64::from(self.expire))
}
}
impl Default for Timing {

View File

@@ -1,16 +1,16 @@
use std::{cmp, mem};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use crate::data_model::resources::as_resources::Asn;
use crate::rtr::error_type::ErrorCode;
use crate::rtr::payload::{Ski, Timing};
use std::io;
use tokio::io::{AsyncWrite};
use anyhow::Result;
use std::io;
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use std::{cmp, mem};
use tokio::io::AsyncWrite;
use std::slice;
use anyhow::bail;
use serde::Serialize;
use std::slice;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWriteExt};
pub const HEADER_LEN: usize = 8;
@@ -27,10 +27,7 @@ macro_rules! common {
#[allow(dead_code)]
impl $type {
/// Writes a value to a writer.
pub async fn write<A: AsyncWrite + Unpin>(
&self,
a: &mut A
) -> Result<(), io::Error> {
pub async fn write<A: AsyncWrite + Unpin>(&self, a: &mut A) -> Result<(), io::Error> {
a.write_all(self.as_ref()).await
}
}
@@ -38,10 +35,7 @@ macro_rules! common {
impl AsRef<[u8]> for $type {
fn as_ref(&self) -> &[u8] {
unsafe {
slice::from_raw_parts(
self as *const Self as *const u8,
mem::size_of::<Self>()
)
slice::from_raw_parts(self as *const Self as *const u8, mem::size_of::<Self>())
}
}
}
@@ -49,14 +43,11 @@ macro_rules! common {
impl AsMut<[u8]> for $type {
fn as_mut(&mut self) -> &mut [u8] {
unsafe {
slice::from_raw_parts_mut(
self as *mut Self as *mut u8,
mem::size_of::<Self>()
)
slice::from_raw_parts_mut(self as *mut Self as *mut u8, mem::size_of::<Self>())
}
}
}
}
};
}
macro_rules! concrete {
@@ -94,28 +85,20 @@ macro_rules! concrete {
///
/// If a value with a different PDU type is received, returns an
/// error.
pub async fn read<Sock: AsyncRead + Unpin>(
sock: &mut Sock
) -> Result<Self, io::Error> {
pub async fn read<Sock: AsyncRead + Unpin>(sock: &mut Sock) -> Result<Self, io::Error> {
let mut res = Self::default();
sock.read_exact(res.header.as_mut()).await?;
if res.header.pdu() != Self::PDU {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
concat!(
"PDU type mismatch when expecting ",
stringify!($type)
)
))
concat!("PDU type mismatch when expecting ", stringify!($type)),
));
}
if res.header.length() as usize != res.as_ref().len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
concat!(
"invalid length for ",
stringify!($type)
)
))
concat!("invalid length for ", stringify!($type)),
));
}
sock.read_exact(&mut res.as_mut()[Header::LEN..]).await?;
Ok(res)
@@ -126,32 +109,26 @@ macro_rules! concrete {
/// If a different PDU type is received, returns the header as
/// the error case of the ok case.
pub async fn try_read<Sock: AsyncRead + Unpin>(
sock: &mut Sock
sock: &mut Sock,
) -> Result<Result<Self, Header>, io::Error> {
let mut res = Self::default();
sock.read_exact(res.header.as_mut()).await?;
if res.header.pdu() == ErrorReport::PDU {
// Since we should drop the session after an error, we
// can safely ignore all the rest of the error for now.
return Ok(Err(res.header))
return Ok(Err(res.header));
}
if res.header.pdu() != Self::PDU {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
concat!(
"PDU type mismatch when expecting ",
stringify!($type)
)
))
concat!("PDU type mismatch when expecting ", stringify!($type)),
));
}
if res.header.length() as usize != res.as_ref().len() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
concat!(
"invalid length for ",
stringify!($type)
)
))
concat!("invalid length for ", stringify!($type)),
));
}
sock.read_exact(&mut res.as_mut()[Header::LEN..]).await?;
Ok(Ok(res))
@@ -163,17 +140,14 @@ macro_rules! concrete {
/// `header`, the function reads the rest of the PUD from the
/// reader and returns the complete value.
pub async fn read_payload<Sock: AsyncRead + Unpin>(
header: Header, sock: &mut Sock
header: Header,
sock: &mut Sock,
) -> Result<Self, io::Error> {
if header.length() as usize != mem::size_of::<Self>() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
concat!(
"invalid length for ",
stringify!($type),
" PDU"
)
))
concat!("invalid length for ", stringify!($type), " PDU"),
));
}
let mut res = Self::default();
sock.read_exact(&mut res.as_mut()[Header::LEN..]).await?;
@@ -181,10 +155,9 @@ macro_rules! concrete {
Ok(res)
}
}
}
};
}
// 所有PDU公共头部信息
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
@@ -196,7 +169,6 @@ pub struct Header {
}
impl Header {
const LEN: usize = mem::size_of::<Self>();
pub fn new(version: u8, pdu: u8, session: u16, length: u32) -> Self {
Header {
@@ -208,7 +180,7 @@ impl Header {
}
pub async fn read_raw<S: AsyncRead + Unpin>(
sock: &mut S
sock: &mut S,
) -> Result<[u8; HEADER_LEN], io::Error> {
let mut buf = [0u8; HEADER_LEN];
sock.read_exact(&mut buf).await?;
@@ -229,10 +201,7 @@ impl Header {
}
if length > MAX_PDU_LEN {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"PDU too large",
));
return Err(io::Error::new(io::ErrorKind::InvalidData, "PDU too large"));
}
Ok(Self {
@@ -247,13 +216,21 @@ impl Header {
Self::from_raw(Self::read_raw(sock).await?)
}
pub fn version(self) -> u8{self.version}
pub fn version(self) -> u8 {
self.version
}
pub fn pdu(self) -> u8{self.pdu}
pub fn pdu(self) -> u8 {
self.pdu
}
pub fn session_id(self) -> u16{u16::from_be(self.session_id)}
pub fn session_id(self) -> u16 {
u16::from_be(self.session_id)
}
pub fn length(self) -> u32{u32::from_be(self.length)}
pub fn length(self) -> u32 {
u32::from_be(self.length)
}
pub fn pdu_len(self) -> Result<usize, io::Error> {
usize::try_from(self.length()).map_err(|_| {
@@ -268,7 +245,6 @@ impl Header {
debug_assert_eq!(self.pdu(), ErrorReport::PDU);
self.session_id()
}
}
common!(Header);
@@ -304,12 +280,10 @@ impl HeaderWithFlags {
let pdu = buf[1];
let flags = buf[2];
let zero = buf[3];
let length = u32::from_be_bytes([
buf[4], buf[5], buf[6], buf[7],
]);
let length = u32::from_be_bytes([buf[4], buf[5], buf[6], buf[7]]);
// 3. 基础合法性校验
if length < HEADER_LEN as u32{
if length < HEADER_LEN as u32 {
bail!("Invalid PDU length");
}
@@ -327,18 +301,27 @@ impl HeaderWithFlags {
})
}
pub fn version(self) -> u8{self.version}
pub fn version(self) -> u8 {
self.version
}
pub fn pdu(self) -> u8{self.pdu}
pub fn pdu(self) -> u8 {
self.pdu
}
pub fn flags(self) -> Flags{Flags(self.flags)}
pub fn flags(self) -> Flags {
Flags(self.flags)
}
pub fn zero(self) -> u8 { self.zero }
pub fn zero(self) -> u8 {
self.zero
}
pub fn length(self) -> u32{u32::from_be(self.length)}
pub fn length(self) -> u32 {
u32::from_be(self.length)
}
}
// Serial Notify
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
@@ -351,20 +334,18 @@ impl SerialNotify {
pub const PDU: u8 = 0;
pub fn new(version: u8, session_id: u16, serial_number: u32) -> Self {
SerialNotify{
SerialNotify {
header: Header::new(version, Self::PDU, session_id, Self::size()),
serial_number: serial_number.to_be()
serial_number: serial_number.to_be(),
}
}
pub fn serial_number(self) -> u32 {
u32::from_be(self.serial_number)
}
}
concrete!(SerialNotify);
// Serial Query
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
@@ -377,9 +358,9 @@ impl SerialQuery {
pub const PDU: u8 = 1;
pub fn new(version: u8, session_id: u16, serial_number: u32) -> Self {
SerialQuery{
SerialQuery {
header: Header::new(version, Self::PDU, session_id, Self::size()),
serial_number: serial_number.to_be()
serial_number: serial_number.to_be(),
}
}
@@ -390,12 +371,11 @@ impl SerialQuery {
concrete!(SerialQuery);
// Reset Query
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct ResetQuery {
header: Header
header: Header,
}
impl ResetQuery {
@@ -410,7 +390,6 @@ impl ResetQuery {
concrete!(ResetQuery);
// Cache Response
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
@@ -430,7 +409,6 @@ impl CacheResponse {
concrete!(CacheResponse);
// Flags
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct Flags(u8);
@@ -464,7 +442,7 @@ pub struct IPv4Prefix {
max_len: u8,
zero: u8,
prefix: u32,
asn: u32
asn: u32,
}
impl IPv4Prefix {
@@ -475,7 +453,7 @@ impl IPv4Prefix {
prefix_len: u8,
max_len: u8,
prefix: Ipv4Addr,
asn: Asn
asn: Asn,
) -> Self {
IPv4Prefix {
header: Header::new(version, Self::PDU, ZERO_16, IPV4_PREFIX_LEN),
@@ -488,12 +466,22 @@ impl IPv4Prefix {
}
}
pub fn flag(self) -> Flags{self.flags}
pub fn flag(self) -> Flags {
self.flags
}
pub fn prefix_len(self) -> u8{self.prefix_len}
pub fn max_len(self) -> u8{self.max_len}
pub fn prefix(self) -> Ipv4Addr{u32::from_be(self.prefix).into()}
pub fn asn(self) -> Asn{u32::from_be(self.asn).into()}
pub fn prefix_len(self) -> u8 {
self.prefix_len
}
pub fn max_len(self) -> u8 {
self.max_len
}
pub fn prefix(self) -> Ipv4Addr {
u32::from_be(self.prefix).into()
}
pub fn asn(self) -> Asn {
u32::from_be(self.asn).into()
}
}
concrete!(IPv4Prefix);
@@ -509,7 +497,7 @@ pub struct IPv6Prefix {
max_len: u8,
zero: u8,
prefix: u128,
asn: u32
asn: u32,
}
impl IPv6Prefix {
@@ -520,7 +508,7 @@ impl IPv6Prefix {
prefix_len: u8,
max_len: u8,
prefix: Ipv6Addr,
asn: Asn
asn: Asn,
) -> Self {
IPv6Prefix {
header: Header::new(version, Self::PDU, ZERO_16, IPV6_PREFIX_LEN),
@@ -533,17 +521,26 @@ impl IPv6Prefix {
}
}
pub fn flag(self) -> Flags{self.flags}
pub fn flag(self) -> Flags {
self.flags
}
pub fn prefix_len(self) -> u8{self.prefix_len}
pub fn max_len(self) -> u8{self.max_len}
pub fn prefix(self) -> Ipv6Addr{u128::from_be(self.prefix).into()}
pub fn asn(self) -> Asn{u32::from_be(self.asn).into()}
pub fn prefix_len(self) -> u8 {
self.prefix_len
}
pub fn max_len(self) -> u8 {
self.max_len
}
pub fn prefix(self) -> Ipv6Addr {
u128::from_be(self.prefix).into()
}
pub fn asn(self) -> Asn {
u32::from_be(self.asn).into()
}
}
concrete!(IPv6Prefix);
// End of Data
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
pub enum EndOfData {
@@ -559,14 +556,20 @@ impl EndOfData {
timing: Timing,
) -> Result<Self, io::Error> {
if version == 0 {
Ok(EndOfData::V0(EndOfDataV0::new(version, session_id, serial_number)))
}
else {
Ok(EndOfData::V1(EndOfDataV1::new(version, session_id, serial_number, timing)?))
Ok(EndOfData::V0(EndOfDataV0::new(
version,
session_id,
serial_number,
)))
} else {
Ok(EndOfData::V1(EndOfDataV1::new(
version,
session_id,
serial_number,
timing,
)?))
}
}
}
#[repr(C, packed)]
@@ -587,11 +590,12 @@ impl EndOfDataV0 {
}
}
pub fn serial_number(self) -> u32{u32::from_be(self.serial_number)}
pub fn serial_number(self) -> u32 {
u32::from_be(self.serial_number)
}
}
concrete!(EndOfDataV0);
#[repr(C, packed)]
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct EndOfDataV1 {
@@ -601,7 +605,6 @@ pub struct EndOfDataV1 {
refresh_interval: u32,
retry_interval: u32,
expire_interval: u32,
}
impl EndOfDataV1 {
@@ -640,9 +643,11 @@ impl EndOfDataV1 {
})
}
pub fn serial_number(self) -> u32{u32::from_be(self.serial_number)}
pub fn serial_number(self) -> u32 {
u32::from_be(self.serial_number)
}
pub fn timing(self) -> Timing{
pub fn timing(self) -> Timing {
Timing {
refresh: u32::from_be(self.refresh_interval),
retry: u32::from_be(self.retry_interval),
@@ -654,22 +659,20 @@ impl EndOfDataV1 {
self.timing().validate()
}
pub async fn read<Sock: AsyncRead + Unpin>(
sock: &mut Sock
) -> Result<Self, io::Error> {
pub async fn read<Sock: AsyncRead + Unpin>(sock: &mut Sock) -> Result<Self, io::Error> {
let mut res = Self::default();
sock.read_exact(res.header.as_mut()).await?;
if res.header.pdu() != Self::PDU {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"PDU type mismatch when expecting EndOfDataV1",
))
));
}
if res.header.length() as usize != mem::size_of::<Self>() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid length for EndOfDataV1",
))
));
}
sock.read_exact(&mut res.as_mut()[Header::LEN..]).await?;
res.validate()?;
@@ -677,13 +680,14 @@ impl EndOfDataV1 {
}
pub async fn read_payload<Sock: AsyncRead + Unpin>(
header: Header, sock: &mut Sock
header: Header,
sock: &mut Sock,
) -> Result<Self, io::Error> {
if header.length() as usize != mem::size_of::<Self>() {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid length for EndOfDataV1 PDU",
))
));
}
let mut res = Self::default();
sock.read_exact(&mut res.as_mut()[Header::LEN..]).await?;
@@ -704,23 +708,21 @@ pub struct CacheReset {
impl CacheReset {
pub const PDU: u8 = 8;
pub fn new(version: u8) -> Self{
pub fn new(version: u8) -> Self {
CacheReset {
header: Header::new(version, Self::PDU, ZERO_16, HEADER_LEN as u32)
header: Header::new(version, Self::PDU, ZERO_16, HEADER_LEN as u32),
}
}
}
concrete!(CacheReset);
// Error Report
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct ErrorReport {
octets: Vec<u8>,
}
impl ErrorReport {
/// The PDU type of an error PDU.
pub const PDU: u8 = 10;
@@ -741,27 +743,19 @@ impl ErrorReport {
let text_len = cmp::min(text.len(), text_room);
let size = Self::FIXED_PART_LEN + pdu_len + text_len;
let header = Header::new(
version, 10, error_code, u32::try_from(size).unwrap()
);
let header = Header::new(version, 10, error_code, u32::try_from(size).unwrap());
let mut octets = Vec::with_capacity(size);
octets.extend_from_slice(header.as_ref());
octets.extend_from_slice(
u32::try_from(pdu_len).unwrap().to_be_bytes().as_ref()
);
octets.extend_from_slice(u32::try_from(pdu_len).unwrap().to_be_bytes().as_ref());
octets.extend_from_slice(&pdu[..pdu_len]);
octets.extend_from_slice(
u32::try_from(text_len).unwrap().to_be_bytes().as_ref()
);
octets.extend_from_slice(u32::try_from(text_len).unwrap().to_be_bytes().as_ref());
octets.extend_from_slice(&text[..text_len]);
ErrorReport { octets }
}
pub async fn read<Sock: AsyncRead + Unpin>(
sock: &mut Sock
) -> Result<Self, io::Error> {
pub async fn read<Sock: AsyncRead + Unpin>(sock: &mut Sock) -> Result<Self, io::Error> {
let header = Header::read(sock).await?;
if header.pdu() != Self::PDU {
return Err(io::Error::new(
@@ -787,7 +781,8 @@ impl ErrorReport {
let mut octets = Vec::with_capacity(total_len);
octets.extend_from_slice(header.as_ref());
octets.resize(total_len, 0);
sock.read_exact(&mut octets[mem::size_of::<Header>()..]).await?;
sock.read_exact(&mut octets[mem::size_of::<Header>()..])
.await?;
let res = ErrorReport { octets };
res.validate()?;
@@ -813,7 +808,8 @@ impl ErrorReport {
/// Skips over the payload of the error PDU.
pub async fn skip_payload<Sock: AsyncRead + Unpin>(
header: Header, sock: &mut Sock
header: Header,
sock: &mut Sock,
) -> Result<(), io::Error> {
let Some(mut remaining) = header.pdu_len()?.checked_sub(mem::size_of::<Header>()) else {
return Err(io::Error::new(
@@ -840,9 +836,7 @@ impl ErrorReport {
}
/// Writes the PUD to a writer.
pub async fn write<A: AsyncWrite + Unpin>(
&self, a: &mut A
) -> Result<(), io::Error> {
pub async fn write<A: AsyncWrite + Unpin>(&self, a: &mut A) -> Result<(), io::Error> {
a.write_all(self.as_ref()).await
}
@@ -860,7 +854,7 @@ impl ErrorReport {
u32::from_be_bytes(
self.octets[Header::LEN..Header::LEN + 4]
.try_into()
.unwrap()
.unwrap(),
) as usize
}
@@ -876,11 +870,7 @@ impl ErrorReport {
fn text_len(&self) -> usize {
let offset = self.text_len_offset();
u32::from_be_bytes(
self.octets[offset..offset + 4]
.try_into()
.unwrap()
) as usize
u32::from_be_bytes(self.octets[offset..offset + 4].try_into().unwrap()) as usize
}
fn text_range(&self) -> std::ops::Range<usize> {
@@ -916,7 +906,10 @@ impl ErrorReport {
let pdu_len = self.erroneous_pdu_len();
let text_len_offset = Header::LEN + 4 + pdu_len;
let Some(text_len_end) = text_len_offset.checked_add(4) else {
return Err(io::Error::new(io::ErrorKind::InvalidData, "ErrorReport length overflow"));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"ErrorReport length overflow",
));
};
if text_len_end > self.octets.len() {
return Err(io::Error::new(
@@ -928,10 +921,13 @@ impl ErrorReport {
let text_len = u32::from_be_bytes(
self.octets[text_len_offset..text_len_end]
.try_into()
.unwrap()
.unwrap(),
) as usize;
let Some(text_end) = text_len_end.checked_add(text_len) else {
return Err(io::Error::new(io::ErrorKind::InvalidData, "ErrorReport text overflow"));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"ErrorReport text overflow",
));
};
if text_end != self.octets.len() {
return Err(io::Error::new(
@@ -951,7 +947,6 @@ impl ErrorReport {
}
}
// TODO: 补全
/// Router Key
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)]
@@ -966,13 +961,10 @@ pub struct RouterKey {
}
impl RouterKey {
pub const PDU: u8 = 9;
const BASE_LEN: usize = HEADER_LEN + 20 + 4;
pub async fn read<Sock: AsyncRead + Unpin>(
sock: &mut Sock
) -> Result<Self, io::Error> {
pub async fn read<Sock: AsyncRead + Unpin>(sock: &mut Sock) -> Result<Self, io::Error> {
let header = HeaderWithFlags::read(sock)
.await
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))?;
@@ -1022,25 +1014,19 @@ impl RouterKey {
Ok(res)
}
pub async fn write<A: AsyncWrite + Unpin>(
&self,
w: &mut A,
) -> Result<(), io::Error> {
pub async fn write<A: AsyncWrite + Unpin>(&self, w: &mut A) -> Result<(), io::Error> {
let length = Self::BASE_LEN + self.subject_public_key_info.len();
let header = HeaderWithFlags::new(
self.header.version(),
Self::PDU,
self.flags,
length as u32,
);
let header =
HeaderWithFlags::new(self.header.version(), Self::PDU, self.flags, length as u32);
w.write_all(&[
header.version(),
header.pdu(),
header.flags().into_u8(),
ZERO_8,
]).await?;
])
.await?;
w.write_all(&(length as u32).to_be_bytes()).await?;
w.write_all(self.ski.as_ref()).await?;
@@ -1120,24 +1106,20 @@ impl RouterKey {
}
}
// ASPA
#[derive(Clone, Debug, Default, Eq, Hash, PartialEq, Serialize)]
pub struct Aspa{
pub struct Aspa {
header: HeaderWithFlags,
customer_asn: u32,
provider_asns: Vec<u32>
provider_asns: Vec<u32>,
}
impl Aspa {
pub const PDU: u8 = 11;
const BASE_LEN: usize = HEADER_LEN + 4;
pub async fn read<Sock: AsyncRead + Unpin>(
sock: &mut Sock
) -> Result<Self, io::Error> {
pub async fn read<Sock: AsyncRead + Unpin>(sock: &mut Sock) -> Result<Self, io::Error> {
let header = HeaderWithFlags::read(sock)
.await
.map_err(|err| io::Error::new(io::ErrorKind::InvalidData, err.to_string()))?;
@@ -1192,11 +1174,7 @@ impl Aspa {
Ok(res)
}
pub async fn write<A: AsyncWrite + Unpin>(
&self,
w: &mut A,
) -> Result<(), io::Error> {
pub async fn write<A: AsyncWrite + Unpin>(&self, w: &mut A) -> Result<(), io::Error> {
let length = Self::BASE_LEN + (self.provider_asns.len() * 4);
let header = HeaderWithFlags::new(
@@ -1211,7 +1189,8 @@ impl Aspa {
header.pdu(),
header.flags().into_u8(),
ZERO_8,
]).await?;
])
.await?;
w.write_all(&(length as u32).to_be_bytes()).await?;
w.write_all(&self.customer_asn.to_be_bytes()).await?;
@@ -1222,12 +1201,7 @@ impl Aspa {
Ok(())
}
pub fn new(
version: u8,
flags: Flags,
customer_asn: u32,
provider_asns: Vec<u32>,
) -> Self {
pub fn new(version: u8, flags: Flags, customer_asn: u32, provider_asns: Vec<u32>) -> Self {
let length = Self::BASE_LEN + (provider_asns.len() * 4);
Self {
@@ -1306,7 +1280,6 @@ impl Aspa {
}
}
//--- AsRef and AsMut
impl AsRef<[u8]> for ErrorReport {
fn as_ref(&self) -> &[u8] {
@@ -1319,4 +1292,3 @@ impl AsMut<[u8]> for ErrorReport {
self.octets.as_mut()
}
}

View File

@@ -6,7 +6,7 @@ use std::sync::{
use anyhow::{Context, Result, anyhow};
use tokio::net::TcpStream;
use tokio::sync::{broadcast, watch, OwnedSemaphorePermit};
use tokio::sync::{OwnedSemaphorePermit, broadcast, watch};
use tracing::{info, warn};
use x509_parser::extensions::GeneralName;
use x509_parser::prelude::{FromDer, X509Certificate};
@@ -22,10 +22,7 @@ pub struct ConnectionGuard {
}
impl ConnectionGuard {
pub fn new(
active_connections: Arc<AtomicUsize>,
permit: OwnedSemaphorePermit,
) -> Self {
pub fn new(active_connections: Arc<AtomicUsize>, permit: OwnedSemaphorePermit) -> Self {
active_connections.fetch_add(1, Ordering::Relaxed);
Self {
active_connections,
@@ -72,8 +69,12 @@ pub async fn handle_tls_connection(
.await
.with_context(|| format!("TLS handshake failed for {}", peer_addr))?;
info!("RTR TLS handshake completed for {}", peer_addr);
verify_peer_certificate_ip(&tls_stream, peer_addr.ip())
.with_context(|| format!("TLS client certificate SAN IP validation failed for {}", peer_addr))?;
verify_peer_certificate_ip(&tls_stream, peer_addr.ip()).with_context(|| {
format!(
"TLS client certificate SAN IP validation failed for {}",
peer_addr
)
})?;
info!("RTR TLS client certificate validated for {}", peer_addr);
let session = RtrSession::new(cache, tls_stream, notify_rx, shutdown_rx);
@@ -122,16 +123,16 @@ fn verify_peer_certificate_ip(
GeneralName::IPAddress(bytes) => {
let bytes = *bytes;
match (peer_ip, bytes.len()) {
(IpAddr::V4(ip), 4) => <[u8; 4]>::try_from(bytes)
.map(IpAddr::from)
.map(|cert_ip| cert_ip == IpAddr::V4(ip))
.unwrap_or(false),
(IpAddr::V6(ip), 16) => <[u8; 16]>::try_from(bytes)
.map(IpAddr::from)
.map(|cert_ip| cert_ip == IpAddr::V6(ip))
.unwrap_or(false),
_ => false,
}
(IpAddr::V4(ip), 4) => <[u8; 4]>::try_from(bytes)
.map(IpAddr::from)
.map(|cert_ip| cert_ip == IpAddr::V4(ip))
.unwrap_or(false),
(IpAddr::V6(ip), 16) => <[u8; 16]>::try_from(bytes)
.map(IpAddr::from)
.map(|cert_ip| cert_ip == IpAddr::V6(ip))
.unwrap_or(false),
_ => false,
}
}
_ => false,
});

View File

@@ -1,28 +1,22 @@
use std::net::SocketAddr;
use std::path::Path;
use std::sync::{
Arc,
atomic::AtomicUsize,
};
use std::sync::{Arc, atomic::AtomicUsize};
use std::time::Duration;
use anyhow::{Context, Result};
use socket2::{SockRef, TcpKeepalive};
use tokio::net::TcpListener;
use tokio::sync::{broadcast, watch, Semaphore};
use tokio::sync::{Semaphore, broadcast, watch};
use tracing::{info, warn};
use rustls::ServerConfig;
use tokio_rustls::TlsAcceptor;
use crate::rtr::cache::SharedRtrCache;
use crate::rtr::server::connection::{
ConnectionGuard,
handle_tcp_connection,
handle_tls_connection,
is_expected_disconnect,
};
use crate::rtr::server::config::RtrServiceConfig;
use crate::rtr::server::connection::{
ConnectionGuard, handle_tcp_connection, handle_tls_connection, is_expected_disconnect,
};
use crate::rtr::server::tls::load_rustls_server_config_with_options;
pub struct RtrServer {
@@ -65,7 +59,8 @@ impl RtrServer {
}
pub fn active_connections(&self) -> usize {
self.active_connections.load(std::sync::atomic::Ordering::Relaxed)
self.active_connections
.load(std::sync::atomic::Ordering::Relaxed)
}
pub async fn run_tcp(self) -> Result<()> {
@@ -293,10 +288,7 @@ impl RtrServer {
}
}
fn apply_keepalive(
stream: &tokio::net::TcpStream,
keepalive: Option<Duration>,
) -> Result<()> {
fn apply_keepalive(stream: &tokio::net::TcpStream, keepalive: Option<Duration>) -> Result<()> {
let Some(keepalive) = keepalive else {
return Ok(());
};

View File

@@ -9,4 +9,4 @@ pub use config::RtrServiceConfig;
pub use listener::RtrServer;
pub use notifier::RtrNotifier;
pub use service::{RtrService, RunningRtrService};
pub use tls::load_rustls_server_config;
pub use tls::load_rustls_server_config;

View File

@@ -13,4 +13,4 @@ impl RtrNotifier {
pub fn notify_cache_updated(&self) {
let _ = self.tx.send(());
}
}
}

View File

@@ -5,7 +5,7 @@ use std::sync::{
atomic::{AtomicUsize, Ordering},
};
use tokio::sync::{broadcast, watch, Semaphore};
use tokio::sync::{Semaphore, broadcast, watch};
use tokio::task::JoinHandle;
use tracing::{error, warn};
@@ -114,7 +114,10 @@ impl RtrService {
let server = self.tls_server(bind_addr);
tokio::spawn(async move {
if let Err(err) = server.run_tls_from_pem(cert_path, key_path, client_ca_path).await {
if let Err(err) = server
.run_tls_from_pem(cert_path, key_path, client_ca_path)
.await
{
error!("RTR TLS server {} exited with error: {:?}", bind_addr, err);
}
})
@@ -129,7 +132,8 @@ impl RtrService {
client_ca_path: impl AsRef<Path>,
) -> RunningRtrService {
let tcp_handle = self.spawn_tcp(tcp_bind_addr);
let tls_handle = self.spawn_tls_from_pem(tls_bind_addr, cert_path, key_path, client_ca_path);
let tls_handle =
self.spawn_tls_from_pem(tls_bind_addr, cert_path, key_path, client_ca_path);
RunningRtrService {
shutdown_tx: self.shutdown_tx.clone(),

View File

@@ -3,7 +3,7 @@ use std::io::BufReader;
use std::path::{Path, PathBuf};
use std::sync::Arc;
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use rustls::server::WebPkiClientVerifier;
use rustls::{RootCertStore, ServerConfig};
use rustls_pki_types::{CertificateDer, PrivateKeyDer};
@@ -36,8 +36,12 @@ pub fn load_rustls_server_config_with_options(
let key = load_private_key(&key_path)
.with_context(|| format!("failed to load private key from {}", key_path.display()))?;
let client_ca_certs = load_certs(&client_ca_path)
.with_context(|| format!("failed to load client CA certs from {}", client_ca_path.display()))?;
let client_ca_certs = load_certs(&client_ca_path).with_context(|| {
format!(
"failed to load client CA certs from {}",
client_ca_path.display()
)
})?;
let mut client_roots = RootCertStore::empty();
let (added, _) = client_roots.add_parsable_certificates(client_ca_certs);
if added == 0 {
@@ -100,8 +104,7 @@ fn load_certs(path: &Path) -> Result<Vec<CertificateDer<'static>>> {
let file = File::open(path)?;
let mut reader = BufReader::new(file);
let certs = rustls_pemfile::certs(&mut reader)
.collect::<std::result::Result<Vec<_>, _>>()?;
let certs = rustls_pemfile::certs(&mut reader).collect::<std::result::Result<Vec<_>, _>>()?;
if certs.is_empty() {
return Err(anyhow!("no certificates found in {}", path.display()));

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::time::{Duration, Instant};
use anyhow::{anyhow, bail, Result};
use anyhow::{Result, anyhow, bail};
use tokio::io;
use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
use tokio::sync::{broadcast, watch};
@@ -14,13 +14,11 @@ use crate::rtr::cache::{
validate_payloads_for_rtr,
};
use crate::rtr::error_type::ErrorCode;
use crate::rtr::pdu::{
Aspa as AspaPdu,
CacheReset, CacheResponse, EndOfData, ErrorReport, Flags, Header, IPv4Prefix, IPv6Prefix,
ResetQuery, RouterKey as RouterKeyPdu, SerialNotify, SerialQuery,
HEADER_LEN,
};
use crate::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey};
use crate::rtr::pdu::{
Aspa as AspaPdu, CacheReset, CacheResponse, EndOfData, ErrorReport, Flags, HEADER_LEN, Header,
IPv4Prefix, IPv6Prefix, ResetQuery, RouterKey as RouterKeyPdu, SerialNotify, SerialQuery,
};
const SUPPORTED_MAX_VERSION: u8 = 2;
const SUPPORTED_MIN_VERSION: u8 = 0;
@@ -83,10 +81,7 @@ where
}
async fn run_inner(&mut self) -> Result<()> {
info!(
"RTR session started: {}",
self.session_summary()
);
info!("RTR session started: {}", self.session_summary());
loop {
let transport_timeout = self.transport_timeout();
tokio::select! {
@@ -265,7 +260,10 @@ where
self.session_summary()
);
} else {
debug!("RTR session transport shutdown completed: {}", self.session_summary());
debug!(
"RTR session transport shutdown completed: {}",
self.session_summary()
);
}
}
@@ -351,7 +349,7 @@ where
offending_pdu,
msg.as_bytes(),
)
.await
.await
}
async fn send_unexpected_version(
@@ -362,8 +360,7 @@ where
) -> io::Result<()> {
let msg = format!(
"unexpected protocol version {}, established version is {}",
received_version,
established_version
received_version, established_version
);
self.send_error(
@@ -372,7 +369,7 @@ where
offending_pdu,
msg.as_bytes(),
)
.await
.await
}
async fn send_corrupt_session_id(
@@ -393,7 +390,7 @@ where
offending_pdu,
msg.as_bytes(),
)
.await
.await
}
async fn send_corrupt_data(
@@ -402,13 +399,8 @@ where
offending_pdu: &[u8],
detail: &[u8],
) -> io::Result<()> {
self.send_error(
version,
ErrorCode::CorruptData,
offending_pdu,
detail,
)
.await
self.send_error(version, ErrorCode::CorruptData, offending_pdu, detail)
.await
}
async fn send_no_data_available(
@@ -458,7 +450,8 @@ where
self.state = SessionState::Closed;
bail!(
"router version {} higher than cache max {}",
version, SUPPORTED_MAX_VERSION
version,
SUPPORTED_MAX_VERSION
);
}
self.negotiate_version(version).await?;
@@ -495,14 +488,16 @@ where
self.state = SessionState::Closed;
bail!(
"router version {} higher than cache max {}",
version, SUPPORTED_MAX_VERSION
version,
SUPPORTED_MAX_VERSION
);
}
self.negotiate_version(version).await?;
let session_id = query.session_id();
let serial = query.serial_number();
self.handle_serial(version, session_id, serial, query.as_ref()).await?;
self.handle_serial(version, session_id, serial, query.as_ref())
.await?;
self.state = SessionState::Established;
info!(
"RTR session established after Serial Query: negotiated_version={}, client_session_id={}, client_serial={}, {}",
@@ -613,7 +608,10 @@ where
.cache
.read()
.map_err(|_| anyhow!("cache read lock poisoned"))?;
(cache.is_data_available(), cache.session_id_for_version(version))
(
cache.is_data_available(),
cache.session_id_for_version(version),
)
};
if !data_available {
@@ -723,7 +721,10 @@ where
let now = Instant::now();
if let Some(last) = self.last_notify_at {
if now.duration_since(last) < NOTIFY_MIN_INTERVAL {
debug!("RTR session notify skipped due to rate limit: {}", self.session_summary());
debug!(
"RTR session notify skipped due to rate limit: {}",
self.session_summary()
);
return Ok(());
}
}
@@ -824,8 +825,7 @@ where
let version = self.version()?;
debug!(
"RTR session writing Cache Response: version={}, session_id={}",
version,
session_id
version, session_id
);
CacheResponse::new(version, session_id)
.write(&mut self.stream)
@@ -835,10 +835,7 @@ where
async fn write_cache_reset(&mut self) -> Result<()> {
let version = self.version()?;
info!(
"RTR session writing Cache Reset: version={}",
version
);
info!("RTR session writing Cache Reset: version={}", version);
CacheReset::new(version).write(&mut self.stream).await?;
Ok(())
}
@@ -880,8 +877,7 @@ where
// References:
// https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-8210bis-25#section-11.4
// https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-8210bis-25#section-12
validate_payloads_for_rtr(payloads, announce)
.map_err(|err| anyhow!(err.to_string()))?;
validate_payloads_for_rtr(payloads, announce).map_err(|err| anyhow!(err.to_string()))?;
let (route_origins, router_keys, aspas) = count_payloads(payloads);
debug!(
"RTR session sending snapshot payloads: announce={}, total={}, route_origins={}, router_keys={}, aspas={}",
@@ -906,8 +902,7 @@ where
// References:
// https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-8210bis-25#section-11.4
// https://datatracker.ietf.org/doc/html/draft-ietf-sidrops-8210bis-25#section-12
validate_payload_updates_for_rtr(&updates)
.map_err(|err| anyhow!(err.to_string()))?;
validate_payload_updates_for_rtr(&updates).map_err(|err| anyhow!(err.to_string()))?;
let (announced, withdrawn, route_origins, router_keys, aspas) =
count_payload_updates(&updates);
debug!(
@@ -1010,8 +1005,7 @@ where
});
let providers = if announce {
aspa
.provider_asns()
aspa.provider_asns()
.iter()
.map(|asn| asn.into_u32())
.collect::<Vec<_>>()
@@ -1019,18 +1013,12 @@ where
Vec::new()
};
let pdu = AspaPdu::new(
version,
flags,
aspa.customer_asn().into_u32(),
providers,
);
let pdu = AspaPdu::new(version, flags, aspa.customer_asn().into_u32(), providers);
pdu.write(&mut self.stream).await?;
Ok(())
}
async fn send_error(
&mut self,
version: u8,
@@ -1052,11 +1040,7 @@ where
.await
}
async fn handle_pdu_read_error(
&mut self,
header: Header,
err: io::Error,
) -> Result<()> {
async fn handle_pdu_read_error(&mut self, header: Header, err: io::Error) -> Result<()> {
warn!(
"RTR session failed to read established-session PDU payload: pdu={}, version={}, err={}",
header.pdu(),
@@ -1076,11 +1060,7 @@ where
Ok(())
}
async fn handle_first_pdu_read_error(
&mut self,
header: Header,
err: io::Error,
) -> Result<()> {
async fn handle_first_pdu_read_error(&mut self, header: Header, err: io::Error) -> Result<()> {
warn!(
"RTR session failed to read first PDU payload: pdu={}, version={}, err={}",
header.pdu(),
@@ -1089,13 +1069,12 @@ where
);
if err.kind() == io::ErrorKind::InvalidData {
let offending = self.read_full_pdu_bytes(header).await?;
let err_version = if (SUPPORTED_MIN_VERSION..=SUPPORTED_MAX_VERSION)
.contains(&header.version())
{
header.version()
} else {
SUPPORTED_MAX_VERSION
};
let err_version =
if (SUPPORTED_MIN_VERSION..=SUPPORTED_MAX_VERSION).contains(&header.version()) {
header.version()
} else {
SUPPORTED_MAX_VERSION
};
let detail = err.to_string();
let _ = self
@@ -1114,13 +1093,14 @@ where
) -> Result<()> {
warn!(
"RTR session handling invalid header bytes: raw_header={:02X?}, err={}",
raw_header,
err
raw_header, err
);
if err.kind() == io::ErrorKind::InvalidData {
let version = match self.version {
Some(version) => version,
None if (SUPPORTED_MIN_VERSION..=SUPPORTED_MAX_VERSION).contains(&raw_header[0]) => {
None if (SUPPORTED_MIN_VERSION..=SUPPORTED_MAX_VERSION)
.contains(&raw_header[0]) =>
{
raw_header[0]
}
None => SUPPORTED_MAX_VERSION,
@@ -1139,10 +1119,7 @@ where
async fn handle_transport_timeout(&mut self, offending_pdu: &[u8]) -> Result<()> {
let version = self.version.unwrap_or(SUPPORTED_MAX_VERSION);
let timeout = self.transport_timeout();
let detail = format!(
"transport stalled for longer than {:?}",
timeout
);
let detail = format!("transport stalled for longer than {:?}", timeout);
warn!(
"RTR session transport timeout: version={}, offending_pdu_len={}, timeout={:?}",
version,
@@ -1177,7 +1154,8 @@ where
bytes.resize(total_len, 0);
timeout(
self.transport_timeout(),
self.stream.read_exact(&mut bytes[HEADER_LEN..HEADER_LEN + payload_len]),
self.stream
.read_exact(&mut bytes[HEADER_LEN..HEADER_LEN + payload_len]),
)
.await
.map_err(|_| io::Error::new(io::ErrorKind::TimedOut, "transport read timed out"))??;

View File

@@ -12,7 +12,7 @@ impl State {
pub fn session_ids(self) -> SessionIds {
self.session_ids
}
pub fn serial(self) -> u32 {
self.serial
}

View File

@@ -1,6 +1,6 @@
use anyhow::{Result, anyhow};
use rocksdb::{ColumnFamilyDescriptor, DB, Direction, IteratorMode, Options, WriteBatch};
use anyhow::{anyhow, Result};
use serde::{de::DeserializeOwned, Serialize};
use serde::{Serialize, de::DeserializeOwned};
use std::path::Path;
use std::sync::Arc;
use tokio::task;
@@ -66,7 +66,10 @@ impl RtrStore {
/// Common serialize/put.
fn put_cf<T: Serialize>(&self, cf: &str, key: &[u8], value: &T) -> Result<()> {
let cf_handle = self.db.cf_handle(cf).ok_or_else(|| anyhow!("CF not found"))?;
let cf_handle = self
.db
.cf_handle(cf)
.ok_or_else(|| anyhow!("CF not found"))?;
let data = serde_json::to_vec(value)?;
self.db.put_cf(cf_handle, key, data)?;
Ok(())
@@ -74,7 +77,10 @@ impl RtrStore {
/// Common get/deserialize.
fn get_cf<T: DeserializeOwned>(&self, cf: &str, key: &[u8]) -> Result<Option<T>> {
let cf_handle = self.db.cf_handle(cf).ok_or_else(|| anyhow!("CF not found"))?;
let cf_handle = self
.db
.cf_handle(cf)
.ok_or_else(|| anyhow!("CF not found"))?;
if let Some(value) = self.db.get_cf(cf_handle, key)? {
let obj = serde_json::from_slice(&value)?;
Ok(Some(obj))
@@ -85,7 +91,10 @@ impl RtrStore {
/// Common delete.
fn delete_cf(&self, cf: &str, key: &[u8]) -> Result<()> {
let cf_handle = self.db.cf_handle(cf).ok_or_else(|| anyhow!("CF not found"))?;
let cf_handle = self
.db
.cf_handle(cf)
.ok_or_else(|| anyhow!("CF not found"))?;
self.db.delete_cf(cf_handle, key)?;
Ok(())
}
@@ -137,10 +146,12 @@ impl RtrStore {
pub fn set_delta_window(&self, min_serial: u32, max_serial: u32) -> Result<()> {
debug!(
"RTR store persisting delta window metadata: min_serial={}, max_serial={}",
min_serial,
max_serial
min_serial, max_serial
);
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
let mut batch = WriteBatch::default();
batch.put_cf(meta_cf, META_DELTA_MIN, serde_json::to_vec(&min_serial)?);
batch.put_cf(meta_cf, META_DELTA_MAX, serde_json::to_vec(&max_serial)?);
@@ -150,7 +161,10 @@ impl RtrStore {
pub fn clear_delta_window(&self) -> Result<()> {
debug!("RTR store clearing delta window metadata");
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
let mut batch = WriteBatch::default();
batch.delete_cf(meta_cf, META_DELTA_MIN);
batch.delete_cf(meta_cf, META_DELTA_MAX);
@@ -166,8 +180,7 @@ impl RtrStore {
(Some(min), Some(max)) => {
debug!(
"RTR store loaded delta window metadata: min_serial={}, max_serial={}",
min,
max
min, max
);
Ok(Some((min, max)))
}
@@ -189,7 +202,10 @@ impl RtrStore {
// ===============================
pub fn save_snapshot(&self, snapshot: &Snapshot) -> Result<()> {
let cf_handle = self.db.cf_handle(CF_SNAPSHOT).ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let cf_handle = self
.db
.cf_handle(CF_SNAPSHOT)
.ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let mut batch = WriteBatch::default();
let data = serde_json::to_vec(snapshot)?;
batch.put_cf(cf_handle, b"current", data);
@@ -206,8 +222,14 @@ impl RtrStore {
}
pub fn save_snapshot_and_state(&self, snapshot: &Snapshot, state: &State) -> Result<()> {
let snapshot_cf = self.db.cf_handle(CF_SNAPSHOT).ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let snapshot_cf = self
.db
.cf_handle(CF_SNAPSHOT)
.ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
let mut batch = WriteBatch::default();
batch.put_cf(snapshot_cf, b"current", serde_json::to_vec(snapshot)?);
@@ -234,8 +256,14 @@ impl RtrStore {
serial: u32,
) -> Result<()> {
let mut batch = WriteBatch::default();
let snapshot_cf = self.db.cf_handle(CF_SNAPSHOT).ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let snapshot_cf = self
.db
.cf_handle(CF_SNAPSHOT)
.ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
batch.put_cf(snapshot_cf, b"current", serde_json::to_vec(snapshot)?);
batch.put_cf(meta_cf, META_SESSION_IDS, serde_json::to_vec(session_ids)?);
@@ -266,15 +294,28 @@ impl RtrStore {
snapshot.router_keys().len(),
snapshot.aspas().len()
);
let snapshot_cf = self.db.cf_handle(CF_SNAPSHOT).ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let delta_cf = self.db.cf_handle(CF_DELTA).ok_or_else(|| anyhow!("CF_DELTA not found"))?;
let snapshot_cf = self
.db
.cf_handle(CF_SNAPSHOT)
.ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
let delta_cf = self
.db
.cf_handle(CF_DELTA)
.ok_or_else(|| anyhow!("CF_DELTA not found"))?;
let mut batch = WriteBatch::default();
batch.put_cf(snapshot_cf, b"current", serde_json::to_vec(snapshot)?);
batch.put_cf(meta_cf, META_SESSION_IDS, serde_json::to_vec(session_ids)?);
batch.put_cf(meta_cf, META_SERIAL, serde_json::to_vec(&serial)?);
batch.put_cf(meta_cf, META_AVAILABILITY, serde_json::to_vec(&availability)?);
batch.put_cf(
meta_cf,
META_AVAILABILITY,
serde_json::to_vec(&availability)?,
);
if let Some(delta) = delta {
debug!(
@@ -283,7 +324,11 @@ impl RtrStore {
delta.announced().len(),
delta.withdrawn().len()
);
batch.put_cf(delta_cf, delta_key(delta.serial()), serde_json::to_vec(delta)?);
batch.put_cf(
delta_cf,
delta_key(delta.serial()),
serde_json::to_vec(delta)?,
);
}
if clear_delta_window {
@@ -318,8 +363,7 @@ impl RtrStore {
} else {
debug!(
"RTR store found no stale delta records outside window [{}, {}]",
min_serial,
max_serial
min_serial, max_serial
);
}
for key in stale_keys {
@@ -334,8 +378,14 @@ impl RtrStore {
pub fn save_snapshot_and_serial(&self, snapshot: &Snapshot, serial: u32) -> Result<()> {
let mut batch = WriteBatch::default();
let snapshot_cf = self.db.cf_handle(CF_SNAPSHOT).ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let snapshot_cf = self
.db
.cf_handle(CF_SNAPSHOT)
.ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
batch.put_cf(snapshot_cf, b"current", serde_json::to_vec(snapshot)?);
batch.put_cf(meta_cf, META_SERIAL, serde_json::to_vec(&serial)?);
self.db.write(batch)?;
@@ -352,8 +402,14 @@ impl RtrStore {
task::spawn_blocking(move || {
let mut batch = WriteBatch::default();
let snapshot_cf = self.db.cf_handle(CF_SNAPSHOT).ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self.db.cf_handle(CF_META).ok_or_else(|| anyhow!("CF_META not found"))?;
let snapshot_cf = self
.db
.cf_handle(CF_SNAPSHOT)
.ok_or_else(|| anyhow!("CF_SNAPSHOT not found"))?;
let meta_cf = self
.db
.cf_handle(CF_META)
.ok_or_else(|| anyhow!("CF_META not found"))?;
batch.put_cf(snapshot_cf, b"current", snapshot_bytes);
batch.put_cf(meta_cf, META_SERIAL, serial_bytes);
self.db.write(batch)?;
@@ -370,7 +426,9 @@ impl RtrStore {
match (snapshot, state) {
(Some(snap), Some(state)) => Ok(Some((snap, state))),
(None, None) => Ok(None),
_ => Err(anyhow!("Inconsistent DB state: snapshot and state mismatch")),
_ => Err(anyhow!(
"Inconsistent DB state: snapshot and state mismatch"
)),
}
}
@@ -380,7 +438,9 @@ impl RtrStore {
match (snapshot, serial) {
(Some(snap), Some(serial)) => Ok(Some((snap, serial))),
(None, None) => Ok(None),
_ => Err(anyhow!("Inconsistent DB state: snapshot and serial mismatch")),
_ => Err(anyhow!(
"Inconsistent DB state: snapshot and serial mismatch"
)),
}
}
@@ -413,8 +473,8 @@ impl RtrStore {
for item in iter {
let (key, value) = item.map_err(|e| anyhow!("rocksdb iterator error: {}", e))?;
let parsed = delta_key_serial(key.as_ref())
.ok_or_else(|| anyhow!("Invalid delta key"))?;
let parsed =
delta_key_serial(key.as_ref()).ok_or_else(|| anyhow!("Invalid delta key"))?;
if parsed <= serial {
continue;
@@ -430,8 +490,7 @@ impl RtrStore {
pub fn load_delta_window(&self, min_serial: u32, max_serial: u32) -> Result<Vec<Delta>> {
info!(
"RTR store loading persisted delta window: min_serial={}, max_serial={}",
min_serial,
max_serial
min_serial, max_serial
);
let cf_handle = self
.db
@@ -442,8 +501,8 @@ impl RtrStore {
for item in iter {
let (key, value) = item.map_err(|e| anyhow!("rocksdb iterator error: {}", e))?;
let parsed = delta_key_serial(key.as_ref())
.ok_or_else(|| anyhow!("Invalid delta key"))?;
let parsed =
delta_key_serial(key.as_ref()).ok_or_else(|| anyhow!("Invalid delta key"))?;
// Restore by the persisted window bounds instead of load_deltas_since().
// The latter follows lexicographic key order and is not safe across serial
@@ -493,7 +552,11 @@ impl RtrStore {
Ok(keys)
}
fn list_delta_keys_outside_window(&self, min_serial: u32, max_serial: u32) -> Result<Vec<Vec<u8>>> {
fn list_delta_keys_outside_window(
&self,
min_serial: u32,
max_serial: u32,
) -> Result<Vec<Vec<u8>>> {
let cf_handle = self
.db
.cf_handle(CF_DELTA)
@@ -503,8 +566,8 @@ impl RtrStore {
for item in iter {
let (key, _value) = item.map_err(|e| anyhow!("rocksdb iterator error: {}", e))?;
let serial = delta_key_serial(key.as_ref())
.ok_or_else(|| anyhow!("Invalid delta key"))?;
let serial =
delta_key_serial(key.as_ref()).ok_or_else(|| anyhow!("Invalid delta key"))?;
if !serial_in_window(serial, min_serial, max_serial) {
keys.push(key.to_vec());
}
@@ -522,8 +585,7 @@ fn validate_delta_window(deltas: &[Delta], min_serial: u32, max_serial: u32) ->
if deltas.is_empty() {
warn!(
"RTR store delta window validation failed: no persisted deltas for window [{}, {}]",
min_serial,
max_serial
min_serial, max_serial
);
return Err(anyhow!(
"delta window [{}, {}] has no persisted deltas",

251
src/slurm/file.rs Normal file
View File

@@ -0,0 +1,251 @@
use std::collections::BTreeSet;
use std::io;
use crate::rtr::payload::Payload;
use crate::slurm::policy::{LocallyAddedAssertions, ValidationOutputFilters, prefix_encompasses};
#[derive(Debug, thiserror::Error)]
pub enum SlurmError {
#[error("failed to parse SLURM JSON: {0}")]
Parse(#[from] serde_json::Error),
#[error("invalid SLURM file: {0}")]
Invalid(String),
#[error("I/O error while reading SLURM file: {0}")]
Io(#[from] io::Error),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlurmVersion {
V1,
V2,
}
impl SlurmVersion {
pub const V1_U32: u32 = 1;
pub const V2_U32: u32 = 2;
pub fn as_u32(self) -> u32 {
match self {
Self::V1 => Self::V1_U32,
Self::V2 => Self::V2_U32,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlurmFile {
version: SlurmVersion,
validation_output_filters: ValidationOutputFilters,
locally_added_assertions: LocallyAddedAssertions,
}
impl SlurmFile {
pub fn new(
version: SlurmVersion,
validation_output_filters: ValidationOutputFilters,
locally_added_assertions: LocallyAddedAssertions,
) -> Result<Self, SlurmError> {
let slurm = Self {
version,
validation_output_filters,
locally_added_assertions,
};
slurm.validate()?;
Ok(slurm)
}
pub fn version(&self) -> SlurmVersion {
self.version
}
pub fn validation_output_filters(&self) -> &ValidationOutputFilters {
&self.validation_output_filters
}
pub fn locally_added_assertions(&self) -> &LocallyAddedAssertions {
&self.locally_added_assertions
}
pub fn apply(&self, payloads: &[Payload]) -> Vec<Payload> {
let mut seen = BTreeSet::new();
let mut result = Vec::new();
for payload in payloads {
if self.validation_output_filters.matches(payload) {
continue;
}
if seen.insert(payload.clone()) {
result.push(payload.clone());
}
}
for assertion in self.locally_added_assertions.to_payloads() {
if seen.insert(assertion.clone()) {
result.push(assertion);
}
}
result
}
pub fn merge_named(files: Vec<(String, SlurmFile)>) -> Result<Self, SlurmError> {
if files.is_empty() {
return Err(SlurmError::Invalid(
"SLURM directory does not contain any .slurm files".to_string(),
));
}
validate_cross_file_conflicts(&files)?;
let mut version = SlurmVersion::V1;
let mut merged_filters = ValidationOutputFilters {
prefix_filters: Vec::new(),
bgpsec_filters: Vec::new(),
aspa_filters: Vec::new(),
};
let mut merged_assertions = LocallyAddedAssertions {
prefix_assertions: Vec::new(),
bgpsec_assertions: Vec::new(),
aspa_assertions: Vec::new(),
};
for (_, file) in files {
if file.version == SlurmVersion::V2 {
version = SlurmVersion::V2;
}
merged_filters.extend_from(file.validation_output_filters());
merged_assertions.extend_from(file.locally_added_assertions());
}
Self::new(version, merged_filters, merged_assertions)
}
fn validate(&self) -> Result<(), SlurmError> {
self.validation_output_filters.validate(self.version)?;
self.locally_added_assertions.validate(self.version)?;
Ok(())
}
}
fn validate_cross_file_conflicts(files: &[(String, SlurmFile)]) -> Result<(), SlurmError> {
for i in 0..files.len() {
for j in (i + 1)..files.len() {
let (name_a, file_a) = &files[i];
let (name_b, file_b) = &files[j];
if prefix_spaces_overlap(file_a, file_b) {
return Err(SlurmError::Invalid(format!(
"conflicting SLURM files: '{}' and '{}' have overlapping prefix spaces",
name_a, name_b
)));
}
if let Some(asn) = bgpsec_asn_overlap(file_a, file_b) {
return Err(SlurmError::Invalid(format!(
"conflicting SLURM files: '{}' and '{}' both constrain BGPsec ASN {}",
name_a, name_b, asn
)));
}
if let Some(customer_asn) = aspa_customer_overlap(file_a, file_b) {
return Err(SlurmError::Invalid(format!(
"conflicting SLURM files: '{}' and '{}' both constrain ASPA customerAsn {}",
name_a, name_b, customer_asn
)));
}
}
}
Ok(())
}
fn prefix_spaces_overlap(lhs: &SlurmFile, rhs: &SlurmFile) -> bool {
let mut lhs_prefixes = lhs
.validation_output_filters()
.prefix_filters
.iter()
.filter_map(|f| f.prefix.as_ref())
.chain(
lhs.locally_added_assertions()
.prefix_assertions
.iter()
.map(|a| &a.prefix),
);
let rhs_prefixes = rhs
.validation_output_filters()
.prefix_filters
.iter()
.filter_map(|f| f.prefix.as_ref())
.chain(
rhs.locally_added_assertions()
.prefix_assertions
.iter()
.map(|a| &a.prefix),
)
.collect::<Vec<_>>();
lhs_prefixes.any(|left| {
rhs_prefixes
.iter()
.any(|right| prefix_encompasses(left, right) || prefix_encompasses(right, left))
})
}
fn bgpsec_asn_overlap(lhs: &SlurmFile, rhs: &SlurmFile) -> Option<u32> {
let lhs_asns = lhs
.validation_output_filters()
.bgpsec_filters
.iter()
.filter_map(|f| f.asn)
.chain(
lhs.locally_added_assertions()
.bgpsec_assertions
.iter()
.map(|a| a.asn),
)
.collect::<BTreeSet<_>>();
rhs.validation_output_filters()
.bgpsec_filters
.iter()
.filter_map(|f| f.asn)
.chain(
rhs.locally_added_assertions()
.bgpsec_assertions
.iter()
.map(|a| a.asn),
)
.find_map(|asn| lhs_asns.contains(&asn).then_some(asn.into_u32()))
}
fn aspa_customer_overlap(lhs: &SlurmFile, rhs: &SlurmFile) -> Option<u32> {
let lhs_customers = lhs
.validation_output_filters()
.aspa_filters
.iter()
.map(|f| f.customer_asn)
.chain(
lhs.locally_added_assertions()
.aspa_assertions
.iter()
.map(|a| a.customer_asn),
)
.collect::<BTreeSet<_>>();
rhs.validation_output_filters()
.aspa_filters
.iter()
.map(|f| f.customer_asn)
.chain(
rhs.locally_added_assertions()
.aspa_assertions
.iter()
.map(|a| a.customer_asn),
)
.find_map(|asn| lhs_customers.contains(&asn).then_some(asn.into_u32()))
}

View File

@@ -1 +1,3 @@
mod slurm;
pub mod file;
pub mod policy;
mod serde;

409
src/slurm/policy.rs Normal file
View File

@@ -0,0 +1,409 @@
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::str::FromStr;
use crate::data_model::resources::as_resources::Asn;
use crate::data_model::resources::ip_resources::{IPAddress, IPAddressPrefix};
use crate::rtr::payload::{Aspa, Payload, RouteOrigin, RouterKey, Ski};
use crate::slurm::file::{SlurmError, SlurmVersion};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationOutputFilters {
pub prefix_filters: Vec<PrefixFilter>,
pub bgpsec_filters: Vec<BgpsecFilter>,
pub aspa_filters: Vec<AspaFilter>,
}
impl ValidationOutputFilters {
pub(crate) fn extend_from(&mut self, other: &Self) {
self.prefix_filters
.extend(other.prefix_filters.iter().cloned());
self.bgpsec_filters
.extend(other.bgpsec_filters.iter().cloned());
self.aspa_filters.extend(other.aspa_filters.iter().cloned());
}
pub(crate) fn validate(&self, version: SlurmVersion) -> Result<(), SlurmError> {
if version == SlurmVersion::V1 && !self.aspa_filters.is_empty() {
return Err(SlurmError::Invalid(
"slurmVersion 1 must not contain aspaFilters".to_string(),
));
}
for filter in &self.prefix_filters {
filter.validate()?;
}
for filter in &self.bgpsec_filters {
filter.validate()?;
}
for filter in &self.aspa_filters {
filter.validate()?;
}
Ok(())
}
pub(crate) fn matches(&self, payload: &Payload) -> bool {
match payload {
Payload::RouteOrigin(route_origin) => self
.prefix_filters
.iter()
.any(|filter| filter.matches(route_origin)),
Payload::RouterKey(router_key) => self
.bgpsec_filters
.iter()
.any(|filter| filter.matches(router_key)),
Payload::Aspa(aspa) => self.aspa_filters.iter().any(|filter| filter.matches(aspa)),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocallyAddedAssertions {
pub prefix_assertions: Vec<PrefixAssertion>,
pub bgpsec_assertions: Vec<BgpsecAssertion>,
pub aspa_assertions: Vec<AspaAssertion>,
}
impl LocallyAddedAssertions {
pub(crate) fn extend_from(&mut self, other: &Self) {
self.prefix_assertions
.extend(other.prefix_assertions.iter().cloned());
self.bgpsec_assertions
.extend(other.bgpsec_assertions.iter().cloned());
self.aspa_assertions
.extend(other.aspa_assertions.iter().cloned());
}
pub(crate) fn validate(&self, version: SlurmVersion) -> Result<(), SlurmError> {
if version == SlurmVersion::V1 && !self.aspa_assertions.is_empty() {
return Err(SlurmError::Invalid(
"slurmVersion 1 must not contain aspaAssertions".to_string(),
));
}
for assertion in &self.prefix_assertions {
assertion.validate()?;
}
for assertion in &self.bgpsec_assertions {
assertion.validate()?;
}
for assertion in &self.aspa_assertions {
assertion.validate()?;
}
Ok(())
}
pub(crate) fn to_payloads(&self) -> Vec<Payload> {
let mut payloads = Vec::with_capacity(
self.prefix_assertions.len()
+ self.bgpsec_assertions.len()
+ self.aspa_assertions.len(),
);
payloads.extend(
self.prefix_assertions
.iter()
.cloned()
.map(|assertion| Payload::RouteOrigin(assertion.into_route_origin())),
);
payloads.extend(
self.bgpsec_assertions
.iter()
.cloned()
.map(|assertion| Payload::RouterKey(assertion.into_router_key())),
);
payloads.extend(
self.aspa_assertions
.iter()
.cloned()
.map(|assertion| Payload::Aspa(assertion.into_aspa())),
);
payloads
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefixFilter {
pub prefix: Option<IPAddressPrefix>,
pub asn: Option<Asn>,
pub comment: Option<String>,
}
impl PrefixFilter {
fn validate(&self) -> Result<(), SlurmError> {
if self.prefix.is_none() && self.asn.is_none() {
return Err(SlurmError::Invalid(
"prefixFilter must contain at least one of prefix or asn".to_string(),
));
}
Ok(())
}
fn matches(&self, route_origin: &RouteOrigin) -> bool {
let prefix_match = self
.prefix
.is_none_or(|filter_prefix| prefix_encompasses(&filter_prefix, route_origin.prefix()));
let asn_match = self.asn.is_none_or(|asn| asn == route_origin.asn());
prefix_match && asn_match
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BgpsecFilter {
pub asn: Option<Asn>,
pub ski: Option<Ski>,
pub comment: Option<String>,
}
impl BgpsecFilter {
fn validate(&self) -> Result<(), SlurmError> {
if self.asn.is_none() && self.ski.is_none() {
return Err(SlurmError::Invalid(
"bgpsecFilter must contain at least one of asn or SKI".to_string(),
));
}
Ok(())
}
fn matches(&self, router_key: &RouterKey) -> bool {
let asn_match = self.asn.is_none_or(|asn| asn == router_key.asn());
let ski_match = self.ski.is_none_or(|ski| ski == router_key.ski());
asn_match && ski_match
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AspaFilter {
pub customer_asn: Asn,
pub comment: Option<String>,
}
impl AspaFilter {
fn validate(&self) -> Result<(), SlurmError> {
if self.customer_asn.into_u32() == 0 {
return Err(SlurmError::Invalid(
"aspaFilter customerAsn must not be AS0".to_string(),
));
}
Ok(())
}
fn matches(&self, aspa: &Aspa) -> bool {
self.customer_asn == aspa.customer_asn()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefixAssertion {
pub prefix: IPAddressPrefix,
pub asn: Asn,
pub max_prefix_length: Option<u8>,
pub comment: Option<String>,
}
impl PrefixAssertion {
fn validate(&self) -> Result<(), SlurmError> {
if self.asn.into_u32() == 0 {
return Err(SlurmError::Invalid(
"prefixAssertion asn must not be AS0".to_string(),
));
}
if let Some(max_prefix_length) = self.max_prefix_length {
let address_bits = prefix_address_bits(&self.prefix);
if max_prefix_length < self.prefix.prefix_length() {
return Err(SlurmError::Invalid(format!(
"prefixAssertion maxPrefixLength {} must be >= prefix length {}",
max_prefix_length,
self.prefix.prefix_length()
)));
}
if max_prefix_length > address_bits {
return Err(SlurmError::Invalid(format!(
"prefixAssertion maxPrefixLength {} exceeds address size {}",
max_prefix_length, address_bits
)));
}
}
Ok(())
}
fn into_route_origin(self) -> RouteOrigin {
RouteOrigin::new(
self.prefix,
self.max_prefix_length
.unwrap_or(self.prefix.prefix_length()),
self.asn,
)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BgpsecAssertion {
pub asn: Asn,
pub ski: Ski,
pub router_public_key: Vec<u8>,
pub comment: Option<String>,
}
impl BgpsecAssertion {
fn validate(&self) -> Result<(), SlurmError> {
RouterKey::new(self.ski, self.asn, self.router_public_key.clone())
.validate()
.map_err(|err| SlurmError::Invalid(err.to_string()))
}
fn into_router_key(self) -> RouterKey {
RouterKey::new(self.ski, self.asn, self.router_public_key)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AspaAssertion {
pub customer_asn: Asn,
pub provider_asns: Vec<Asn>,
pub comment: Option<String>,
}
impl AspaAssertion {
fn validate(&self) -> Result<(), SlurmError> {
let providers = self
.provider_asns
.iter()
.map(|asn| asn.into_u32())
.collect::<Vec<_>>();
if providers.windows(2).any(|window| window[0] >= window[1]) {
return Err(SlurmError::Invalid(
"aspaAssertion providerAsns must be strictly increasing".to_string(),
));
}
let aspa = Aspa::new(self.customer_asn, self.provider_asns.clone());
aspa.validate_announcement()
.map_err(|err| SlurmError::Invalid(err.to_string()))?;
Ok(())
}
fn into_aspa(self) -> Aspa {
Aspa::new(self.customer_asn, self.provider_asns)
}
}
pub(crate) fn parse_ip_prefix(input: &str) -> Result<IPAddressPrefix, SlurmError> {
let (addr, prefix_length) = input
.split_once('/')
.ok_or_else(|| SlurmError::Invalid(format!("invalid prefix '{}'", input)))?;
let address = IpAddr::from_str(addr.trim())
.map_err(|err| SlurmError::Invalid(format!("invalid IP address '{}': {}", addr, err)))?;
let prefix_length = prefix_length.trim().parse::<u8>().map_err(|err| {
SlurmError::Invalid(format!(
"invalid prefix length '{}': {}",
prefix_length, err
))
})?;
match address {
IpAddr::V4(addr) => {
if prefix_length > 32 {
return Err(SlurmError::Invalid(format!(
"IPv4 prefix length {} exceeds 32",
prefix_length
)));
}
if !is_canonical_v4(addr, prefix_length) {
return Err(SlurmError::Invalid(format!(
"IPv4 prefix '{}' is not canonical",
input
)));
}
Ok(IPAddressPrefix::new(
IPAddress::from_ipv4(addr),
prefix_length,
))
}
IpAddr::V6(addr) => {
if prefix_length > 128 {
return Err(SlurmError::Invalid(format!(
"IPv6 prefix length {} exceeds 128",
prefix_length
)));
}
if !is_canonical_v6(addr, prefix_length) {
return Err(SlurmError::Invalid(format!(
"IPv6 prefix '{}' is not canonical",
input
)));
}
Ok(IPAddressPrefix::new(
IPAddress::from_ipv6(addr),
prefix_length,
))
}
}
}
pub(crate) fn prefix_address_bits(prefix: &IPAddressPrefix) -> u8 {
match prefix.address() {
IPAddress::V4(_) => 32,
IPAddress::V6(_) => 128,
}
}
pub(crate) fn prefix_encompasses(filter: &IPAddressPrefix, other: &IPAddressPrefix) -> bool {
if filter.afi() != other.afi() {
return false;
}
if filter.prefix_length() > other.prefix_length() {
return false;
}
match (filter.address(), other.address()) {
(IPAddress::V4(lhs), IPAddress::V4(rhs)) => {
prefix_match_v4(lhs, rhs, filter.prefix_length())
}
(IPAddress::V6(lhs), IPAddress::V6(rhs)) => {
prefix_match_v6(lhs, rhs, filter.prefix_length())
}
_ => false,
}
}
fn prefix_match_v4(lhs: Ipv4Addr, rhs: Ipv4Addr, prefix_length: u8) -> bool {
let mask = if prefix_length == 0 {
0
} else {
u32::MAX << (32 - prefix_length)
};
(u32::from(lhs) & mask) == (u32::from(rhs) & mask)
}
fn prefix_match_v6(lhs: Ipv6Addr, rhs: Ipv6Addr, prefix_length: u8) -> bool {
let mask = if prefix_length == 0 {
0
} else {
u128::MAX << (128 - prefix_length)
};
(u128::from(lhs) & mask) == (u128::from(rhs) & mask)
}
fn is_canonical_v4(addr: Ipv4Addr, prefix_length: u8) -> bool {
let mask = if prefix_length == 0 {
0
} else {
u32::MAX << (32 - prefix_length)
};
(u32::from(addr) & !mask) == 0
}
fn is_canonical_v6(addr: Ipv6Addr, prefix_length: u8) -> bool {
let mask = if prefix_length == 0 {
0
} else {
u128::MAX << (128 - prefix_length)
};
(u128::from(addr) & !mask) == 0
}

313
src/slurm/serde.rs Normal file
View File

@@ -0,0 +1,313 @@
use std::io;
use base64::Engine;
use base64::engine::general_purpose::STANDARD_NO_PAD;
use serde::Deserialize;
use crate::data_model::resources::as_resources::Asn;
use crate::rtr::payload::Ski;
use crate::slurm::file::{SlurmError, SlurmFile, SlurmVersion};
use crate::slurm::policy::{
AspaAssertion, AspaFilter, BgpsecAssertion, BgpsecFilter, LocallyAddedAssertions,
PrefixAssertion, PrefixFilter, ValidationOutputFilters, parse_ip_prefix,
};
impl SlurmFile {
pub fn from_slice(input: &[u8]) -> Result<Self, SlurmError> {
let version = serde_json::from_slice::<SlurmVersionMarker>(input)?.slurm_version;
match version {
SlurmVersion::V1_U32 => {
let raw = serde_json::from_slice::<RawSlurmFileV1>(input)?;
Self::from_raw_v1(raw)
}
SlurmVersion::V2_U32 => {
let raw = serde_json::from_slice::<RawSlurmFileV2>(input)?;
Self::from_raw_v2(raw)
}
other => Err(SlurmError::Invalid(format!(
"unsupported slurmVersion {}, expected 1 or 2",
other
))),
}
}
pub fn from_reader(mut reader: impl io::Read) -> Result<Self, SlurmError> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
Self::from_slice(&bytes)
}
fn from_raw_v1(raw: RawSlurmFileV1) -> Result<Self, SlurmError> {
Self::new(
SlurmVersion::V1,
ValidationOutputFilters {
prefix_filters: raw.validation_output_filters.prefix_filters,
bgpsec_filters: raw.validation_output_filters.bgpsec_filters,
aspa_filters: Vec::new(),
},
LocallyAddedAssertions {
prefix_assertions: raw.locally_added_assertions.prefix_assertions,
bgpsec_assertions: raw.locally_added_assertions.bgpsec_assertions,
aspa_assertions: Vec::new(),
},
)
}
fn from_raw_v2(raw: RawSlurmFileV2) -> Result<Self, SlurmError> {
Self::new(
SlurmVersion::V2,
ValidationOutputFilters {
prefix_filters: raw.validation_output_filters.prefix_filters,
bgpsec_filters: raw.validation_output_filters.bgpsec_filters,
aspa_filters: raw.validation_output_filters.aspa_filters,
},
LocallyAddedAssertions {
prefix_assertions: raw.locally_added_assertions.prefix_assertions,
bgpsec_assertions: raw.locally_added_assertions.bgpsec_assertions,
aspa_assertions: raw.locally_added_assertions.aspa_assertions,
},
)
}
}
#[derive(Deserialize)]
struct SlurmVersionMarker {
#[serde(rename = "slurmVersion")]
slurm_version: u32,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawSlurmFileV1 {
#[serde(rename = "slurmVersion")]
_slurm_version: u32,
#[serde(rename = "validationOutputFilters")]
validation_output_filters: RawValidationOutputFiltersV1,
#[serde(rename = "locallyAddedAssertions")]
locally_added_assertions: RawLocallyAddedAssertionsV1,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawSlurmFileV2 {
#[serde(rename = "slurmVersion")]
_slurm_version: u32,
#[serde(rename = "validationOutputFilters")]
validation_output_filters: RawValidationOutputFiltersV2,
#[serde(rename = "locallyAddedAssertions")]
locally_added_assertions: RawLocallyAddedAssertionsV2,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawValidationOutputFiltersV1 {
#[serde(rename = "prefixFilters")]
prefix_filters: Vec<PrefixFilter>,
#[serde(rename = "bgpsecFilters")]
bgpsec_filters: Vec<BgpsecFilter>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawValidationOutputFiltersV2 {
#[serde(rename = "prefixFilters")]
prefix_filters: Vec<PrefixFilter>,
#[serde(rename = "bgpsecFilters")]
bgpsec_filters: Vec<BgpsecFilter>,
#[serde(rename = "aspaFilters")]
aspa_filters: Vec<AspaFilter>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLocallyAddedAssertionsV1 {
#[serde(rename = "prefixAssertions")]
prefix_assertions: Vec<PrefixAssertion>,
#[serde(rename = "bgpsecAssertions")]
bgpsec_assertions: Vec<BgpsecAssertion>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawLocallyAddedAssertionsV2 {
#[serde(rename = "prefixAssertions")]
prefix_assertions: Vec<PrefixAssertion>,
#[serde(rename = "bgpsecAssertions")]
bgpsec_assertions: Vec<BgpsecAssertion>,
#[serde(rename = "aspaAssertions")]
aspa_assertions: Vec<AspaAssertion>,
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPrefixFilter {
prefix: Option<String>,
asn: Option<u32>,
comment: Option<String>,
}
impl<'de> Deserialize<'de> for PrefixFilter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawPrefixFilter::deserialize(deserializer)?;
Ok(Self {
prefix: raw
.prefix
.map(|prefix| parse_ip_prefix(&prefix))
.transpose()
.map_err(serde::de::Error::custom)?,
asn: raw.asn.map(Asn::from),
comment: raw.comment,
})
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawBgpsecFilter {
asn: Option<u32>,
#[serde(rename = "SKI")]
ski: Option<String>,
comment: Option<String>,
}
impl<'de> Deserialize<'de> for BgpsecFilter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawBgpsecFilter::deserialize(deserializer)?;
Ok(Self {
asn: raw.asn.map(Asn::from),
ski: raw
.ski
.map(|ski| decode_ski(&ski))
.transpose()
.map_err(serde::de::Error::custom)?,
comment: raw.comment,
})
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawAspaFilter {
#[serde(rename = "customerAsn")]
customer_asn: u32,
comment: Option<String>,
}
impl<'de> Deserialize<'de> for AspaFilter {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawAspaFilter::deserialize(deserializer)?;
Ok(Self {
customer_asn: Asn::from(raw.customer_asn),
comment: raw.comment,
})
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawPrefixAssertion {
prefix: String,
asn: u32,
#[serde(rename = "maxPrefixLength")]
max_prefix_length: Option<u8>,
comment: Option<String>,
}
impl<'de> Deserialize<'de> for PrefixAssertion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawPrefixAssertion::deserialize(deserializer)?;
Ok(Self {
prefix: parse_ip_prefix(&raw.prefix).map_err(serde::de::Error::custom)?,
asn: Asn::from(raw.asn),
max_prefix_length: raw.max_prefix_length,
comment: raw.comment,
})
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawBgpsecAssertion {
asn: u32,
#[serde(rename = "SKI")]
ski: String,
#[serde(rename = "routerPublicKey")]
router_public_key: String,
comment: Option<String>,
}
impl<'de> Deserialize<'de> for BgpsecAssertion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawBgpsecAssertion::deserialize(deserializer)?;
Ok(Self {
asn: Asn::from(raw.asn),
ski: decode_ski(&raw.ski).map_err(serde::de::Error::custom)?,
router_public_key: decode_router_public_key(&raw.router_public_key)
.map_err(serde::de::Error::custom)?,
comment: raw.comment,
})
}
}
#[derive(Deserialize)]
#[serde(deny_unknown_fields)]
struct RawAspaAssertion {
#[serde(rename = "customerAsn")]
customer_asn: u32,
#[serde(rename = "providerAsns")]
provider_asns: Vec<u32>,
comment: Option<String>,
}
impl<'de> Deserialize<'de> for AspaAssertion {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw = RawAspaAssertion::deserialize(deserializer)?;
Ok(Self {
customer_asn: Asn::from(raw.customer_asn),
provider_asns: raw.provider_asns.into_iter().map(Asn::from).collect(),
comment: raw.comment,
})
}
}
fn decode_ski(input: &str) -> Result<Ski, SlurmError> {
let bytes = hex::decode(input)
.map_err(|err| SlurmError::Invalid(format!("invalid SKI '{}': {}", input, err)))?;
if bytes.len() != 20 {
return Err(SlurmError::Invalid(format!(
"SKI must be exactly 20 bytes, got {}",
bytes.len()
)));
}
let mut ski = [0u8; 20];
ski.copy_from_slice(&bytes);
Ok(Ski::from_bytes(ski))
}
fn decode_router_public_key(input: &str) -> Result<Vec<u8>, SlurmError> {
STANDARD_NO_PAD.decode(input).map_err(|err| {
SlurmError::Invalid(format!(
"invalid routerPublicKey base64 '{}': {}",
input, err
))
})
}

View File

@@ -1,80 +0,0 @@
use std::io;
use crate::data_model::resources::as_resources::Asn;
#[derive(Debug, thiserror::Error)]
pub enum SlurmError {
#[error("Read slurm from reader error")]
SlurmFromReader(),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlurmFile {
pub version: u32,
pub validation_output_filters: ValidationOutputFilters,
pub locally_added_assertions: LocallyAddedAssertions,
}
impl SlurmFile {
pub fn new(filters: ValidationOutputFilters,
assertions: LocallyAddedAssertions,) -> Self {
let version = 1;
SlurmFile {
version,
validation_output_filters: filters,
locally_added_assertions: assertions,
}
}
// pub fn from_reader(reader: impl io::Read)-> Result<Self, SlurmError> {
//
// }
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ValidationOutputFilters {
pub prefix_filters: Vec<PrefixFilter>,
pub bgpset_filters: Vec<BgpsecFilter>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Comment(String);
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefixFilter {
pub prefix: String,
pub asn: Asn,
pub comment: Option<Comment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BgpsecFilter {
pub asn: Asn,
pub ski: u8,
pub comment: Option<Comment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LocallyAddedAssertions {
pub prefix_assertions: Vec<PrefixAssertion>,
pub bgpsec_assertions: Vec<BgpsecAssertion>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PrefixAssertion {
pub prefix: String,
pub asn: Asn,
pub max_prefix_length: u8,
pub comment: Option<Comment>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BgpsecAssertion {
pub asn: Asn,
pub ski: u8,
pub router_public_key: u8,
pub comment: Option<Comment>,
}

View File

@@ -2,7 +2,7 @@ use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use anyhow::{Context, Result, anyhow};
use der_parser::ber::{BerObject, BerObjectContent};
use der_parser::der::parse_der;
@@ -29,8 +29,8 @@ pub struct CcrPayloadConversion {
pub fn load_ccr_snapshot_from_file(path: impl AsRef<Path>) -> Result<ParsedCcrSnapshot> {
let path = path.as_ref();
let bytes = fs::read(path)
.with_context(|| format!("failed to read CCR file: {}", path.display()))?;
let bytes =
fs::read(path).with_context(|| format!("failed to read CCR file: {}", path.display()))?;
parse_ccr_bytes(&bytes).with_context(|| format!("failed to parse CCR file: {}", path.display()))
}
@@ -64,9 +64,10 @@ pub fn find_latest_ccr_file(dir: impl AsRef<Path>) -> Result<PathBuf> {
continue;
}
if latest.as_ref().is_none_or(|current| {
file_name_key(&path) > file_name_key(current)
}) {
if latest
.as_ref()
.is_none_or(|current| file_name_key(&path) > file_name_key(current))
{
latest = Some(path);
}
}
@@ -250,10 +251,7 @@ fn parse_vaps(field: &BerObject<'_>) -> Result<Vec<ParsedAspa>> {
Ok(vaps)
}
fn parse_roa_address(
address_family: &[u8],
items: &[BerObject<'_>],
) -> Result<(IpAddr, u8, u8)> {
fn parse_roa_address(address_family: &[u8], items: &[BerObject<'_>]) -> Result<(IpAddr, u8, u8)> {
let address = items
.first()
.ok_or_else(|| anyhow!("ROAIPAddress missing address field"))?;
@@ -275,8 +273,7 @@ fn parse_roa_address(
let max_len = match items.get(1) {
Some(value) => {
let max_len = as_u32(value, "ROAIPAddress.maxLength")?;
u8::try_from(max_len)
.map_err(|_| anyhow!("maxLength {max_len} does not fit in u8"))?
u8::try_from(max_len).map_err(|_| anyhow!("maxLength {max_len} does not fit in u8"))?
}
None => prefix_len,
};
@@ -328,10 +325,7 @@ fn decode_context_wrapped_sequence<'a>(obj: &'a BerObject<'a>) -> Result<BerObje
let (rem, inner) = parse_der(any.data)
.map_err(|err| anyhow!("failed to parse encapsulated DER: {err}"))?;
if !rem.is_empty() {
return Err(anyhow!(
"encapsulated DER has {} trailing bytes",
rem.len()
));
return Err(anyhow!("encapsulated DER has {} trailing bytes", rem.len()));
}
Ok(inner)
}

2
src/source/mod.rs Normal file
View File

@@ -0,0 +1,2 @@
pub mod ccr;
pub mod pipeline;

138
src/source/pipeline.rs Normal file
View File

@@ -0,0 +1,138 @@
use anyhow::{Result, anyhow};
use std::path::PathBuf;
use tracing::{info, warn};
use crate::rtr::payload::Payload;
use crate::slurm::file::SlurmFile;
use crate::source::ccr::{
find_latest_ccr_file, load_ccr_payloads_from_file_with_options, load_ccr_snapshot_from_file,
};
#[derive(Debug, Clone)]
pub struct PayloadLoadConfig {
pub ccr_dir: String,
pub slurm_dir: Option<String>,
pub strict_ccr_validation: bool,
}
pub fn load_payloads_from_latest_sources(config: &PayloadLoadConfig) -> Result<Vec<Payload>> {
let payloads = load_payloads_from_latest_ccr(&config.ccr_dir, config.strict_ccr_validation)?;
match config.slurm_dir.as_deref() {
Some(dir) => apply_slurm_to_payloads_from_dir(dir, payloads),
None => Ok(payloads),
}
}
fn load_payloads_from_latest_ccr(
ccr_dir: &str,
strict_ccr_validation: bool,
) -> Result<Vec<Payload>> {
let latest = find_latest_ccr_file(ccr_dir)?;
let snapshot = load_ccr_snapshot_from_file(&latest)?;
let vrp_count = snapshot.vrps.len();
let vap_count = snapshot.vaps.len();
let produced_at = snapshot.produced_at.clone();
let conversion = load_ccr_payloads_from_file_with_options(&latest, strict_ccr_validation)?;
let payloads = conversion.payloads;
if !conversion.invalid_vrps.is_empty() {
warn!(
"CCR load skipped invalid VRPs: file={}, skipped={}, samples={:?}",
latest.display(),
conversion.invalid_vrps.len(),
sample_messages(&conversion.invalid_vrps)
);
}
if !conversion.invalid_vaps.is_empty() {
warn!(
"CCR load skipped invalid VAPs/ASPAs: file={}, skipped={}, samples={:?}",
latest.display(),
conversion.invalid_vaps.len(),
sample_messages(&conversion.invalid_vaps)
);
}
info!(
"loaded latest CCR snapshot: file={}, produced_at={:?}, vrp_count={}, vap_count={}, payload_count={}, strict_ccr_validation={}",
latest.display(),
produced_at,
vrp_count,
vap_count,
payloads.len(),
strict_ccr_validation
);
Ok(payloads)
}
fn apply_slurm_to_payloads_from_dir(
slurm_dir: &str,
payloads: Vec<Payload>,
) -> Result<Vec<Payload>> {
let files = read_slurm_files(slurm_dir)?;
let file_count = files.len();
let file_names = files
.iter()
.map(|(name, _)| name.clone())
.collect::<Vec<_>>();
let slurm = SlurmFile::merge_named(files)
.map_err(|err| anyhow!("failed to merge SLURM files from '{}': {}", slurm_dir, err))?;
let input_count = payloads.len();
let filtered = slurm.apply(&payloads);
let output_count = filtered.len();
info!(
"applied SLURM policy set: slurm_dir={}, file_count={}, files={:?}, merged_slurm_version={}, input_payload_count={}, output_payload_count={}",
slurm_dir,
file_count,
file_names,
slurm.version().as_u32(),
input_count,
output_count
);
Ok(filtered)
}
fn read_slurm_files(slurm_dir: &str) -> Result<Vec<(String, SlurmFile)>> {
let mut paths = std::fs::read_dir(slurm_dir)
.map_err(|err| anyhow!("failed to read SLURM directory '{}': {}", slurm_dir, err))?
.filter_map(|entry| entry.ok())
.map(|entry| entry.path())
.filter(|path| path.is_file())
.filter(|path| path.extension().and_then(|ext| ext.to_str()) == Some("slurm"))
.collect::<Vec<PathBuf>>();
paths.sort_by_key(|path| {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_ascii_lowercase())
.unwrap_or_default()
});
if paths.is_empty() {
return Err(anyhow!(
"SLURM directory '{}' does not contain .slurm files",
slurm_dir
));
}
paths
.into_iter()
.map(|path| {
let name = path.to_string_lossy().to_string();
let file = std::fs::File::open(&path)
.map_err(|err| anyhow!("failed to open SLURM file '{}': {}", name, err))?;
let slurm = SlurmFile::from_reader(file)
.map_err(|err| anyhow!("failed to parse SLURM file '{}': {}", name, err))?;
Ok((name, slurm))
})
.collect()
}
fn sample_messages(messages: &[String]) -> Vec<&str> {
messages.iter().take(3).map(String::as_str).collect()
}