rtr的输入换成ccr文件

This commit is contained in:
xiuting.xu
2026-03-30 10:03:43 +08:00
parent 7432fbcac4
commit 8d6173f605
15 changed files with 759 additions and 137 deletions

View File

@@ -63,7 +63,7 @@ cargo run --bin rtr_debug_client -- <addr> <version> [reset|serial <session_id>
- `version`: `1`
- `mode`: `reset`
- `timeout`: `30`
- `poll`: `60`
- `poll`: `600`
## TCP 示例
@@ -148,7 +148,7 @@ cargo run --bin rtr_debug_client -- \
等待下一个 PDU 的读取超时时间,单位秒。
- `--poll <secs>`
在尚未拿到 `EndOfData` timing hint 前,默认使用的自动轮询间隔。
在尚未拿到 `EndOfData` timing hint 前,默认使用的自动轮询间隔。默认值为 `600` 秒,对齐 draft 第 6 节的默认 Retry Interval。
- `--keep-after-error`
收到 `ErrorReport` 后不暂停自动轮询。

View File

@@ -21,7 +21,7 @@ use crate::pretty::{
use crate::protocol::{PduHeader, PduType, QueryMode};
const DEFAULT_READ_TIMEOUT_SECS: u64 = 30;
const DEFAULT_POLL_INTERVAL_SECS: u64 = 60;
const DEFAULT_POLL_INTERVAL_SECS: u64 = 600;
trait AsyncStream: AsyncRead + AsyncWrite + Unpin + Send {}
impl<T> AsyncStream for T where T: AsyncRead + AsyncWrite + Unpin + Send {}

View File

@@ -7,8 +7,8 @@ use anyhow::{anyhow, Result};
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::loader::{load_aspas_from_file, load_router_keys_from_file, load_vrps_from_file};
use rpki::rtr::payload::Timing;
use rpki::rtr::server::{RtrNotifier, RtrService, RtrServiceConfig, RunningRtrService};
use rpki::rtr::store::RtrStore;
@@ -20,14 +20,14 @@ struct AppConfig {
tls_addr: SocketAddr,
db_path: String,
vrp_file: String,
aspa_file: String,
router_key_file: String,
ccr_dir: String,
tls_cert_path: String,
tls_key_path: String,
tls_client_ca_path: String,
max_delta: u8,
prune_delta_by_snapshot_size: bool,
strict_ccr_validation: bool,
refresh_interval: Duration,
service_config: RtrServiceConfig,
@@ -41,14 +41,14 @@ impl Default for AppConfig {
tls_addr: "0.0.0.0:324".parse().expect("invalid default tls_addr"),
db_path: "./rtr-db".to_string(),
vrp_file: r"C:\Users\xuxiu\git_code\rpki\data\vrps.txt".to_string(),
aspa_file: "./data/aspas.txt".to_string(),
router_key_file: "./data/router-keys.txt".to_string(),
ccr_dir: "./data".to_string(),
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(),
max_delta: 100,
prune_delta_by_snapshot_size: false,
strict_ccr_validation: false,
refresh_interval: Duration::from_secs(300),
service_config: RtrServiceConfig {
@@ -82,14 +82,8 @@ impl AppConfig {
if let Some(value) = env_var("RPKI_RTR_DB_PATH")? {
config.db_path = value;
}
if let Some(value) = env_var("RPKI_RTR_VRP_FILE")? {
config.vrp_file = value;
}
if let Some(value) = env_var("RPKI_RTR_ASPA_FILE")? {
config.aspa_file = value;
}
if let Some(value) = env_var("RPKI_RTR_ROUTER_KEY_FILE")? {
config.router_key_file = value;
if let Some(value) = env_var("RPKI_RTR_CCR_DIR")? {
config.ccr_dir = value;
}
if let Some(value) = env_var("RPKI_RTR_TLS_CERT_PATH")? {
config.tls_cert_path = value;
@@ -105,6 +99,14 @@ impl AppConfig {
.parse()
.map_err(|err| anyhow!("invalid RPKI_RTR_MAX_DELTA '{}': {}", value, err))?;
}
if let Some(value) = env_var("RPKI_RTR_PRUNE_DELTA_BY_SNAPSHOT_SIZE")? {
config.prune_delta_by_snapshot_size =
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")?;
}
if let Some(value) = env_var("RPKI_RTR_REFRESH_INTERVAL_SECS")? {
let secs: u64 = value.parse().map_err(|err| {
anyhow!(
@@ -185,8 +187,9 @@ fn init_shared_cache(config: &AppConfig, store: &RtrStore) -> Result<SharedRtrCa
let initial_cache = RtrCache::default().init(
store,
config.max_delta,
config.prune_delta_by_snapshot_size,
Timing::default(),
|| load_payloads_from_files(&config.vrp_file, &config.aspa_file, &config.router_key_file),
|| load_payloads_from_latest_ccr(&config.ccr_dir, config.strict_ccr_validation),
)?;
let shared_cache: SharedRtrCache = Arc::new(RwLock::new(initial_cache));
@@ -229,9 +232,8 @@ fn spawn_refresh_task(
notifier: RtrNotifier,
) -> JoinHandle<()> {
let refresh_interval = config.refresh_interval;
let vrp_file = config.vrp_file.clone();
let aspa_file = config.aspa_file.clone();
let router_key_file = config.router_key_file.clone();
let ccr_dir = config.ccr_dir.clone();
let strict_ccr_validation = config.strict_ccr_validation;
tokio::spawn(async move {
let mut interval = tokio::time::interval(refresh_interval);
@@ -239,7 +241,7 @@ fn spawn_refresh_task(
loop {
interval.tick().await;
match load_payloads_from_files(&vrp_file, &aspa_file, &router_key_file) {
match load_payloads_from_latest_ccr(&ccr_dir, strict_ccr_validation) {
Ok(payloads) => {
let payload_count = payloads.len();
let updated = {
@@ -258,8 +260,8 @@ fn spawn_refresh_task(
let new_serial = cache.serial();
if new_serial != old_serial {
info!(
"RTR cache refresh applied: vrp_file={}, payload_count={}, old_serial={}, new_serial={}",
vrp_file,
"RTR cache refresh applied: ccr_dir={}, payload_count={}, old_serial={}, new_serial={}",
ccr_dir,
payload_count,
old_serial,
new_serial
@@ -267,8 +269,8 @@ fn spawn_refresh_task(
true
} else {
info!(
"RTR cache refresh found no change: vrp_file={}, payload_count={}, serial={}",
vrp_file,
"RTR cache refresh found no change: ccr_dir={}, payload_count={}, serial={}",
ccr_dir,
payload_count,
old_serial
);
@@ -288,7 +290,7 @@ fn spawn_refresh_task(
}
}
Err(err) => {
warn!("failed to reload VRPs from file {}: {:?}", vrp_file, err);
warn!("failed to reload CCR payloads from {}: {:?}", ccr_dir, err);
}
}
}
@@ -314,10 +316,9 @@ fn log_startup_config(config: &AppConfig) {
info!("tls_client_ca_path={}", config.tls_client_ca_path);
}
info!("vrp_file={}", config.vrp_file);
info!("aspa_file={}", config.aspa_file);
info!("router_key_file={}", config.router_key_file);
info!("ccr_dir={}", config.ccr_dir);
info!("max_delta={}", config.max_delta);
info!("strict_ccr_validation={}", config.strict_ccr_validation);
info!(
"refresh_interval_secs={}",
config.refresh_interval.as_secs()
@@ -372,13 +373,49 @@ fn parse_bool(value: &str, name: &str) -> Result<bool> {
}
}
fn load_payloads_from_files(
vrp_file: &str,
aspa_file: &str,
router_key_file: &str,
fn load_payloads_from_latest_ccr(
ccr_dir: &str,
strict_ccr_validation: bool,
) -> Result<Vec<rpki::rtr::payload::Payload>> {
let mut payloads = load_vrps_from_file(vrp_file)?;
payloads.extend(load_aspas_from_file(aspa_file)?);
payloads.extend(load_router_keys_from_file(router_key_file)?);
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()
}

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

@@ -55,6 +55,7 @@ pub struct RtrCache {
snapshot: Snapshot,
deltas: VecDeque<Arc<Delta>>,
max_delta: u8,
prune_delta_by_snapshot_size: bool,
timing: Timing,
last_update_begin: DualTime,
last_update_end: DualTime,
@@ -71,6 +72,7 @@ impl Default for RtrCache {
snapshot: Snapshot::empty(),
deltas: VecDeque::with_capacity(100),
max_delta: 100,
prune_delta_by_snapshot_size: false,
timing: Timing::default(),
last_update_begin: now.clone(),
last_update_end: now.clone(),
@@ -83,6 +85,7 @@ pub struct RtrCacheBuilder {
availability: Option<CacheAvailability>,
session_ids: Option<SessionIds>,
max_delta: Option<u8>,
prune_delta_by_snapshot_size: Option<bool>,
timing: Option<Timing>,
serial: Option<u32>,
snapshot: Option<Snapshot>,
@@ -96,6 +99,7 @@ impl RtrCacheBuilder {
availability: None,
session_ids: None,
max_delta: None,
prune_delta_by_snapshot_size: None,
timing: None,
serial: None,
snapshot: None,
@@ -119,6 +123,11 @@ impl RtrCacheBuilder {
self
}
pub fn prune_delta_by_snapshot_size(mut self, v: bool) -> Self {
self.prune_delta_by_snapshot_size = Some(v);
self
}
pub fn timing(mut self, v: Timing) -> Self {
self.timing = Some(v);
self
@@ -147,6 +156,7 @@ impl RtrCacheBuilder {
pub fn build(self) -> RtrCache {
let now = DualTime::now();
let max_delta = self.max_delta.unwrap_or(100);
let prune_delta_by_snapshot_size = self.prune_delta_by_snapshot_size.unwrap_or(false);
let timing = self.timing.unwrap_or_default();
let snapshot = self.snapshot.unwrap_or_else(Snapshot::empty);
let deltas = self
@@ -167,6 +177,7 @@ impl RtrCacheBuilder {
snapshot,
deltas,
max_delta,
prune_delta_by_snapshot_size,
timing,
last_update_begin: now.clone(),
last_update_end: now,
@@ -231,24 +242,41 @@ impl RtrCache {
}
fn push_delta(&mut self, delta: Arc<Delta>) {
let dropped_serial = if self.deltas.len() >= self.max_delta as usize {
self.deltas.front().map(|oldest| oldest.serial())
} else {
None
};
if self.deltas.len() >= self.max_delta as usize {
self.deltas.pop_front();
}
debug!(
"RTR cache pushing delta into window: delta_serial={}, announced={}, withdrawn={}, dropped_oldest_serial={:?}, window_size_before={}, max_delta={}",
delta.serial(),
delta.announced().len(),
delta.withdrawn().len(),
dropped_serial,
self.deltas.len(),
self.max_delta
);
self.deltas.push_back(delta);
let mut dropped_serials = Vec::new();
if self.prune_delta_by_snapshot_size {
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
{
if let Some(oldest) = self.deltas.pop_front() {
dropped_serials.push(oldest.serial());
cumulative_delta_wire_size =
estimate_delta_window_payload_wire_size(&self.deltas);
}
}
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
);
}
debug!(
"RTR cache pushing delta into window: delta_serial={}, announced={}, withdrawn={}, dropped_oldest_serials={:?}, window_size_after={}, max_delta={}, prune_delta_by_snapshot_size={}",
self.deltas.back().map(|d| d.serial()).unwrap_or(0),
self.deltas.back().map(|d| d.announced().len()).unwrap_or(0),
self.deltas.back().map(|d| d.withdrawn().len()).unwrap_or(0),
dropped_serials,
self.deltas.len(),
self.max_delta,
self.prune_delta_by_snapshot_size
);
}
fn replace_snapshot(&mut self, snapshot: Snapshot) {
@@ -353,8 +381,8 @@ impl RtrCache {
return Ok(None);
}
self.push_delta(delta.clone());
self.replace_snapshot(new_snapshot.clone());
self.push_delta(delta.clone());
self.last_update_end = DualTime::now();
let delta_window = self.delta_window();
info!(
@@ -574,6 +602,43 @@ impl RtrCache {
}
}
fn estimate_snapshot_payload_wire_size(snapshot: &Snapshot) -> usize {
snapshot
.payloads_for_rtr()
.iter()
.map(|payload| estimate_payload_wire_size(payload, true))
.sum()
}
fn estimate_delta_window_payload_wire_size(deltas: &VecDeque<Arc<Delta>>) -> usize {
deltas
.iter()
.map(|delta| estimate_delta_wire_size(delta))
.sum()
}
fn estimate_delta_wire_size(delta: &Delta) -> usize {
delta
.payloads_for_rtr()
.iter()
.map(|(announce, payload)| estimate_payload_wire_size(payload, *announce))
.sum()
}
fn estimate_payload_wire_size(payload: &Payload, announce: bool) -> usize {
match payload {
Payload::RouteOrigin(origin) => match origin.prefix().address {
crate::data_model::resources::ip_resources::IPAddress::V4(_) => 20,
crate::data_model::resources::ip_resources::IPAddress::V6(_) => 32,
},
Payload::RouterKey(key) => 8 + 20 + 4 + key.spki().len(),
Payload::Aspa(aspa) => {
let providers = if announce { aspa.provider_asns().len() } else { 0 };
8 + 4 + providers * 4
}
}
}
#[derive(Debug, Clone, Default)]
struct LogicalState {
before: Option<Payload>,

View File

@@ -14,10 +14,16 @@ impl RtrCache {
self,
store: &RtrStore,
max_delta: u8,
prune_delta_by_snapshot_size: bool,
timing: Timing,
file_loader: impl Fn() -> Result<Vec<Payload>>,
) -> Result<Self> {
if let Some(cache) = try_restore_from_store(store, max_delta, 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(),
@@ -80,6 +86,7 @@ impl RtrCache {
.availability(availability)
.session_ids(session_ids)
.max_delta(max_delta)
.prune_delta_by_snapshot_size(prune_delta_by_snapshot_size)
.timing(timing)
.serial(serial)
.snapshot(snapshot)
@@ -95,7 +102,12 @@ impl RtrCache {
}
}
fn try_restore_from_store(store: &RtrStore, max_delta: u8, timing: Timing) -> Result<Option<RtrCache>> {
fn try_restore_from_store(
store: &RtrStore,
max_delta: u8,
prune_delta_by_snapshot_size: bool,
timing: Timing,
) -> Result<Option<RtrCache>> {
let snapshot = store.get_snapshot()?;
let session_ids = store.get_session_ids()?;
let serial = store.get_serial()?;
@@ -143,6 +155,7 @@ fn try_restore_from_store(store: &RtrStore, max_delta: u8, timing: Timing) -> Re
.availability(availability)
.session_ids(session_ids)
.max_delta(max_delta)
.prune_delta_by_snapshot_size(prune_delta_by_snapshot_size)
.timing(timing)
.serial(serial)
.snapshot(snapshot)

361
src/rtr/ccr.rs Normal file
View File

@@ -0,0 +1,361 @@
use std::fs;
use std::net::{IpAddr, Ipv4Addr, Ipv6Addr};
use std::path::{Path, PathBuf};
use anyhow::{anyhow, Context, Result};
use der_parser::ber::{BerObject, BerObjectContent};
use der_parser::der::parse_der;
use crate::rtr::loader::{ParsedAspa, ParsedVrp, build_aspa, build_route_origin};
use crate::rtr::payload::Payload;
const VRPS_INDEX: usize = 3;
const VAPS_INDEX: usize = 4;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct ParsedCcrSnapshot {
pub content_type_oid: String,
pub produced_at: Option<String>,
pub vrps: Vec<ParsedVrp>,
pub vaps: Vec<ParsedAspa>,
}
#[derive(Clone, Debug, Default, Eq, PartialEq)]
pub struct CcrPayloadConversion {
pub payloads: Vec<Payload>,
pub invalid_vrps: Vec<String>,
pub invalid_vaps: Vec<String>,
}
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()))?;
parse_ccr_bytes(&bytes).with_context(|| format!("failed to parse CCR file: {}", path.display()))
}
pub fn load_ccr_payloads_from_file(path: impl AsRef<Path>) -> Result<Vec<Payload>> {
let snapshot = load_ccr_snapshot_from_file(path)?;
snapshot_to_payloads(&snapshot)
}
pub fn load_ccr_payloads_from_file_with_options(
path: impl AsRef<Path>,
strict: bool,
) -> Result<CcrPayloadConversion> {
let snapshot = load_ccr_snapshot_from_file(path)?;
snapshot_to_payloads_with_options(&snapshot, strict)
}
pub fn find_latest_ccr_file(dir: impl AsRef<Path>) -> Result<PathBuf> {
let dir = dir.as_ref();
let mut latest: Option<PathBuf> = None;
for entry in fs::read_dir(dir)
.with_context(|| format!("failed to read CCR directory: {}", dir.display()))?
{
let entry =
entry.with_context(|| format!("failed to iterate CCR directory: {}", dir.display()))?;
let path = entry.path();
if !path.is_file() {
continue;
}
if path.extension().and_then(|ext| ext.to_str()) != Some("ccr") {
continue;
}
if latest.as_ref().is_none_or(|current| {
file_name_key(&path) > file_name_key(current)
}) {
latest = Some(path);
}
}
latest.ok_or_else(|| anyhow!("no .ccr files found in {}", dir.display()))
}
pub fn snapshot_to_payloads(snapshot: &ParsedCcrSnapshot) -> Result<Vec<Payload>> {
Ok(snapshot_to_payloads_with_options(snapshot, true)?.payloads)
}
pub fn snapshot_to_payloads_with_options(
snapshot: &ParsedCcrSnapshot,
strict: bool,
) -> Result<CcrPayloadConversion> {
let mut payloads = Vec::with_capacity(snapshot.vrps.len() + snapshot.vaps.len());
let mut invalid_vrps = Vec::new();
let mut invalid_vaps = Vec::new();
for vrp in &snapshot.vrps {
match build_route_origin(vrp.clone()) {
Ok(origin) => payloads.push(Payload::RouteOrigin(origin)),
Err(err) => {
let msg = format!("invalid CCR VRP: {:?}: {}", vrp, err);
if strict {
return Err(anyhow!(msg));
}
invalid_vrps.push(msg);
}
}
}
for vap in &snapshot.vaps {
match build_aspa(vap.clone()) {
Ok(aspa) => payloads.push(Payload::Aspa(aspa)),
Err(err) => {
let msg = format!("invalid CCR VAP/ASPA: {:?}: {}", vap, err);
if strict {
return Err(anyhow!(msg));
}
invalid_vaps.push(msg);
}
}
}
Ok(CcrPayloadConversion {
payloads,
invalid_vrps,
invalid_vaps,
})
}
pub fn parse_ccr_bytes(bytes: &[u8]) -> Result<ParsedCcrSnapshot> {
let (rem, root) = parse_der(bytes).map_err(|err| anyhow!("failed to parse CCR DER: {err}"))?;
if !rem.is_empty() {
return Err(anyhow!("CCR DER has {} trailing bytes", rem.len()));
}
let root_items = sequence_items(&root)?;
if root_items.len() != 2 {
return Err(anyhow!("CCR root must contain exactly 2 items"));
}
let content_type_oid = match &root_items[0].content {
BerObjectContent::OID(oid) => oid.to_string(),
other => {
return Err(anyhow!(
"CCR root first element must be content type OID, got {other:?}"
));
}
};
let payload = decode_context_wrapped_sequence(&root_items[1])?;
let payload_items = sequence_items(&payload)?;
let produced_at = payload_items.get(1).and_then(|obj| match &obj.content {
BerObjectContent::GeneralizedTime(t) => Some(t.to_string()),
_ => None,
});
let vrps = if let Some(vrps_field) = payload_items.get(VRPS_INDEX) {
parse_vrps(vrps_field)?
} else {
Vec::new()
};
let vaps = if let Some(vaps_field) = payload_items.get(VAPS_INDEX) {
parse_vaps(vaps_field)?
} else {
Vec::new()
};
Ok(ParsedCcrSnapshot {
content_type_oid,
produced_at,
vrps,
vaps,
})
}
fn parse_vrps(field: &BerObject<'_>) -> Result<Vec<ParsedVrp>> {
let vrp_state = decode_context_wrapped_sequence(field)?;
let vrp_state_items = sequence_items(&vrp_state)?;
let roa_payload_sets = vrp_state_items
.first()
.ok_or_else(|| anyhow!("ROA payload state missing payload set list"))?;
let roa_payload_sets = sequence_items(roa_payload_sets)?;
let mut vrps = Vec::new();
for payload_set in roa_payload_sets {
let payload_set_items = sequence_items(payload_set)?;
if payload_set_items.len() != 2 {
return Err(anyhow!(
"ROAPayloadSet must contain 2 items, got {}",
payload_set_items.len()
));
}
let asn = as_u32(&payload_set_items[0], "ROAPayloadSet.asID")?;
let families = sequence_items(&payload_set_items[1])?;
for family in families {
let family_items = sequence_items(family)?;
if family_items.len() != 2 {
return Err(anyhow!(
"ROAIPAddressFamily must contain 2 items, got {}",
family_items.len()
));
}
let address_family = as_octets(&family_items[0], "ROAIPAddressFamily.addressFamily")?;
let addresses = sequence_items(&family_items[1])?;
for address in addresses {
let address_items = sequence_items(address)?;
let (prefix_addr, prefix_len, max_len) =
parse_roa_address(address_family, address_items)?;
vrps.push(ParsedVrp {
prefix_addr,
prefix_len,
max_len,
asn,
});
}
}
}
Ok(vrps)
}
fn parse_vaps(field: &BerObject<'_>) -> Result<Vec<ParsedAspa>> {
let vap_state = decode_context_wrapped_sequence(field)?;
let vap_state_items = sequence_items(&vap_state)?;
let aspa_payload_sets = vap_state_items
.first()
.ok_or_else(|| anyhow!("ASPA payload state missing payload set list"))?;
let aspa_payload_sets = sequence_items(aspa_payload_sets)?;
let mut vaps = Vec::new();
for payload_set in aspa_payload_sets {
let payload_set_items = sequence_items(payload_set)?;
if payload_set_items.len() != 2 {
return Err(anyhow!(
"ASPAPayloadSet must contain 2 items, got {}",
payload_set_items.len()
));
}
let customer_asn = as_u32(&payload_set_items[0], "ASPAPayloadSet.customerASID")?;
let provider_set = sequence_items(&payload_set_items[1])?;
let mut provider_asns = Vec::with_capacity(provider_set.len());
for provider in provider_set {
provider_asns.push(as_u32(provider, "ASPAPayloadSet.providerASID")?);
}
vaps.push(ParsedAspa {
customer_asn,
provider_asns,
});
}
Ok(vaps)
}
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"))?;
let (unused_bits, bit_string) = match &address.content {
BerObjectContent::BitString(unused_bits, bit_string) => (*unused_bits, bit_string),
other => {
return Err(anyhow!(
"ROAIPAddress.address must be BIT STRING, got {other:?}"
));
}
};
let prefix_len = (bit_string.data.len() * 8)
.checked_sub(usize::from(unused_bits))
.ok_or_else(|| anyhow!("invalid ROAIPAddress BIT STRING length"))?;
let prefix_len = u8::try_from(prefix_len)
.map_err(|_| anyhow!("prefix length {prefix_len} does not fit in u8"))?;
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"))?
}
None => prefix_len,
};
let prefix_addr = match address_family {
[0, 1] | [0, 1, ..] => {
let mut octets = [0u8; 4];
if bit_string.data.len() > octets.len() {
return Err(anyhow!(
"IPv4 ROAIPAddress too long: {} bytes",
bit_string.data.len()
));
}
octets[..bit_string.data.len()].copy_from_slice(bit_string.data);
IpAddr::V4(Ipv4Addr::from(octets))
}
[0, 2] | [0, 2, ..] => {
let mut octets = [0u8; 16];
if bit_string.data.len() > octets.len() {
return Err(anyhow!(
"IPv6 ROAIPAddress too long: {} bytes",
bit_string.data.len()
));
}
octets[..bit_string.data.len()].copy_from_slice(bit_string.data);
IpAddr::V6(Ipv6Addr::from(octets))
}
_ => {
return Err(anyhow!(
"unsupported ROA address family octets: {:?}",
address_family
));
}
};
Ok((prefix_addr, prefix_len, max_len))
}
fn sequence_items<'a>(obj: &'a BerObject<'a>) -> Result<&'a [BerObject<'a>]> {
match &obj.content {
BerObjectContent::Sequence(items) => Ok(items),
other => Err(anyhow!("expected SEQUENCE, got {other:?}")),
}
}
fn decode_context_wrapped_sequence<'a>(obj: &'a BerObject<'a>) -> Result<BerObject<'a>> {
match &obj.content {
BerObjectContent::Unknown(any) => {
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()
));
}
Ok(inner)
}
other => Err(anyhow!(
"expected context-specific wrapped field, got {other:?}"
)),
}
}
fn as_u32(obj: &BerObject<'_>, field_name: &str) -> Result<u32> {
obj.as_u32()
.map_err(|err| anyhow!("{field_name} must be INTEGER fitting in u32: {err}"))
}
fn as_octets<'a>(obj: &'a BerObject<'a>, field_name: &str) -> Result<&'a [u8]> {
match &obj.content {
BerObjectContent::OctetString(bytes) => Ok(bytes),
other => Err(anyhow!("{field_name} must be OCTET STRING, got {other:?}")),
}
}
fn file_name_key(path: &Path) -> String {
path.file_name()
.and_then(|name| name.to_str())
.map(|name| name.to_ascii_lowercase())
.unwrap_or_default()
}

View File

@@ -7,3 +7,4 @@ pub mod error_type;
pub mod state;
pub mod server;
pub mod loader;
pub mod ccr;

View File

@@ -7,7 +7,7 @@ use std::sync::{
use anyhow::{Context, Result, anyhow};
use tokio::net::TcpStream;
use tokio::sync::{broadcast, watch, OwnedSemaphorePermit};
use tracing::{error, info, warn};
use tracing::{info, warn};
use x509_parser::extensions::GeneralName;
use x509_parser::prelude::{FromDer, X509Certificate};
@@ -52,11 +52,7 @@ pub async fn handle_tcp_connection(
shutdown_rx: watch::Receiver<bool>,
) -> Result<()> {
let session = RtrSession::new(cache, stream, notify_rx, shutdown_rx);
if let Err(err) = session.run().await {
error!("RTR TCP session run failed for {}: {:?}", peer_addr, err);
return Err(err);
}
session.run().await?;
info!("RTR TCP session completed normally for {}", peer_addr);
Ok(())
@@ -81,16 +77,28 @@ pub async fn handle_tls_connection(
info!("RTR TLS client certificate validated for {}", peer_addr);
let session = RtrSession::new(cache, tls_stream, notify_rx, shutdown_rx);
if let Err(err) = session.run().await {
error!("RTR TLS session run failed for {}: {:?}", peer_addr, err);
return Err(err);
}
session.run().await?;
info!("RTR TLS session completed normally for {}", peer_addr);
Ok(())
}
pub fn is_expected_disconnect(err: &anyhow::Error) -> bool {
err.chain().any(|cause| {
if let Some(io_err) = cause.downcast_ref::<std::io::Error>() {
return matches!(
io_err.kind(),
std::io::ErrorKind::BrokenPipe
| std::io::ErrorKind::UnexpectedEof
| std::io::ErrorKind::ConnectionAborted
| std::io::ErrorKind::ConnectionReset
| std::io::ErrorKind::NotConnected
) || io_err.raw_os_error() == Some(10054);
}
false
})
}
fn verify_peer_certificate_ip(
tls_stream: &tokio_rustls::server::TlsStream<TcpStream>,
peer_ip: IpAddr,

View File

@@ -16,7 +16,12 @@ use rustls::ServerConfig;
use tokio_rustls::TlsAcceptor;
use crate::rtr::cache::SharedRtrCache;
use crate::rtr::server::connection::{ConnectionGuard, handle_tcp_connection, handle_tls_connection};
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::tls::load_rustls_server_config_with_options;
@@ -136,12 +141,21 @@ impl RtrServer {
if let Err(err) =
handle_tcp_connection(cache, stream, peer_addr, notify_rx, shutdown_rx).await
{
warn!(
"RTR TCP session closed with error: peer_addr={}, active_connections={}, err={:?}",
peer_addr,
guard.active_count(),
err
);
if is_expected_disconnect(&err) {
info!(
"RTR TCP session closed by peer: peer_addr={}, active_connections={}, err={}",
peer_addr,
guard.active_count(),
err
);
} else {
warn!(
"RTR TCP session closed with error: peer_addr={}, active_connections={}, err={}",
peer_addr,
guard.active_count(),
err
);
}
} else {
info!(
"RTR TCP session closed cleanly: peer_addr={}, active_connections={}",
@@ -250,12 +264,21 @@ impl RtrServer {
notify_rx,
shutdown_rx,
).await {
warn!(
"RTR TLS session closed with error: peer_addr={}, active_connections={}, err={:?}",
peer_addr,
guard.active_count(),
err
);
if is_expected_disconnect(&err) {
info!(
"RTR TLS session closed by peer: peer_addr={}, active_connections={}, err={}",
peer_addr,
guard.active_count(),
err
);
} else {
warn!(
"RTR TLS session closed with error: peer_addr={}, active_connections={}, err={}",
peer_addr,
guard.active_count(),
err
);
}
} else {
info!(
"RTR TLS session closed cleanly: peer_addr={}, active_connections={}",