mod cloudflare;
mod ripe_historical;
pub(crate) mod rpki_client;
mod rpkispools;
mod rpkiviews;
use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
use ipnet::IpNet;
use ipnet_trie::IpnetTrie;
use crate::errors::{load_methods, modules};
use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
pub use ripe_historical::list_ripe_files;
use rpki_client::RpkiClientData;
pub use rpkispools::{
RpkiSpoolsCollector, RpkiSpoolsData, list_rpkispools_files, parse_ccr, parse_rpkispools_archive,
};
pub use rpkiviews::{RpkiViewsCollector, list_rpkiviews_files};
use serde::{Deserialize, Serialize};
use std::fmt::Display;
use std::str::FromStr;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Roa {
pub prefix: IpNet,
pub asn: u32,
pub max_length: u8,
pub rir: Option<Rir>,
pub not_before: Option<NaiveDateTime>,
pub not_after: Option<NaiveDateTime>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Aspa {
pub customer_asn: u32,
pub providers: Vec<u32>,
pub expires: Option<NaiveDateTime>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpkiFile {
pub url: String,
pub timestamp: DateTime<Utc>,
pub size: Option<u64>,
pub rir: Option<Rir>,
pub collector: Option<RpkiViewsCollector>,
}
#[derive(Debug, Clone, Default)]
pub enum HistoricalRpkiSource {
#[default]
Ripe,
RpkiViews(RpkiViewsCollector),
RpkiSpools(RpkiSpoolsCollector),
}
impl std::fmt::Display for HistoricalRpkiSource {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
HistoricalRpkiSource::Ripe => write!(f, "RIPE NCC"),
HistoricalRpkiSource::RpkiViews(collector) => write!(f, "RPKIviews ({})", collector),
HistoricalRpkiSource::RpkiSpools(collector) => {
write!(f, "RPKISPOOL ({})", collector)
}
}
}
}
#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Rir {
AFRINIC,
APNIC,
ARIN,
LACNIC,
RIPENCC,
}
impl FromStr for Rir {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"afrinic" => Ok(Rir::AFRINIC),
"apnic" => Ok(Rir::APNIC),
"arin" => Ok(Rir::ARIN),
"lacnic" => Ok(Rir::LACNIC),
"ripe" => Ok(Rir::RIPENCC),
_ => Err(format!("unknown RIR: {}", s)),
}
}
}
impl Display for Rir {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Rir::AFRINIC => write!(f, "AFRINIC"),
Rir::APNIC => write!(f, "APNIC"),
Rir::ARIN => write!(f, "ARIN"),
Rir::LACNIC => write!(f, "LACNIC"),
Rir::RIPENCC => write!(f, "RIPENCC"),
}
}
}
impl Rir {
pub fn to_ripe_ftp_root_url(&self) -> String {
match self {
Rir::AFRINIC => "https://ftp.ripe.net/rpki/afrinic.tal".to_string(),
Rir::APNIC => "https://ftp.ripe.net/rpki/apnic.tal".to_string(),
Rir::ARIN => "https://ftp.ripe.net/rpki/arin.tal".to_string(),
Rir::LACNIC => "https://ftp.ripe.net/rpki/lacnic.tal".to_string(),
Rir::RIPENCC => "https://ftp.ripe.net/rpki/ripencc.tal".to_string(),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RpkiValidation {
Valid,
Invalid,
Unknown,
}
impl Display for RpkiValidation {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RpkiValidation::Valid => write!(f, "valid"),
RpkiValidation::Invalid => write!(f, "invalid"),
RpkiValidation::Unknown => write!(f, "unknown"),
}
}
}
#[deprecated(since = "0.10.0", note = "Use Roa instead")]
pub type RoaEntry = Roa;
#[derive(Clone)]
pub struct RpkiTrie {
pub trie: IpnetTrie<Vec<Roa>>,
pub aspas: Vec<Aspa>,
date: Option<NaiveDate>,
}
impl Default for RpkiTrie {
fn default() -> Self {
Self {
trie: IpnetTrie::new(),
aspas: vec![],
date: None,
}
}
}
impl RpkiTrie {
pub fn new(date: Option<NaiveDate>) -> Self {
Self {
trie: IpnetTrie::new(),
aspas: vec![],
date,
}
}
pub fn insert_roa(&mut self, roa: Roa) -> bool {
match self.trie.exact_match_mut(roa.prefix) {
Some(existing_roas) => {
if !existing_roas.iter().any(|existing| {
existing.asn == roa.asn && existing.max_length == roa.max_length
}) {
existing_roas.push(roa);
}
false
}
None => {
self.trie.insert(roa.prefix, vec![roa]);
true
}
}
}
pub fn insert_roas(&mut self, roas: Vec<Roa>) {
for roa in roas {
self.insert_roa(roa);
}
}
pub(crate) fn from_rpki_client_data(
data: RpkiClientData,
date: Option<NaiveDate>,
) -> Result<Self> {
let mut trie = RpkiTrie::new(date);
trie.merge_rpki_client_data(data);
Ok(trie)
}
pub(crate) fn merge_rpki_client_data(&mut self, data: RpkiClientData) {
for roa in data.roas {
let prefix = match roa.prefix.parse::<IpNet>() {
Ok(p) => p,
Err(_) => continue,
};
let rir = Rir::from_str(&roa.ta).ok();
let not_after =
DateTime::from_timestamp(roa.expires as i64, 0).map(|dt| dt.naive_utc());
self.insert_roa(Roa {
prefix,
asn: roa.asn,
max_length: roa.max_length,
rir,
not_before: None,
not_after,
});
}
for aspa in data.aspas {
if !self
.aspas
.iter()
.any(|a| a.customer_asn == aspa.customer_asid)
{
let expires = DateTime::from_timestamp(aspa.expires, 0).map(|dt| dt.naive_utc());
self.aspas.push(Aspa {
customer_asn: aspa.customer_asid,
providers: aspa.providers,
expires,
});
}
}
}
pub fn lookup_by_prefix(&self, prefix: &IpNet) -> Vec<Roa> {
let mut all_matches = vec![];
for (p, roas) in self.trie.matches(prefix) {
if p.contains(prefix) {
for roa in roas {
if roa.max_length >= prefix.prefix_len() {
all_matches.push(roa.clone());
}
}
}
}
all_matches
}
fn lookup_covering_roas(&self, prefix: &IpNet) -> Vec<Roa> {
let mut all_matches = vec![];
for (p, roas) in self.trie.matches(prefix) {
if p.contains(prefix) {
for roa in roas {
all_matches.push(roa.clone());
}
}
}
all_matches
}
pub fn validate(&self, prefix: &IpNet, asn: u32) -> RpkiValidation {
let covering_roas = self.lookup_covering_roas(prefix);
if covering_roas.is_empty() {
return RpkiValidation::Unknown;
}
let matches = self.lookup_by_prefix(prefix);
for roa in matches {
if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
return RpkiValidation::Valid;
}
}
RpkiValidation::Invalid
}
pub fn validate_check_expiry(
&self,
prefix: &IpNet,
asn: u32,
check_time: Option<NaiveDateTime>,
) -> RpkiValidation {
let covering_roas = self.lookup_covering_roas(prefix);
if covering_roas.is_empty() {
return RpkiValidation::Unknown;
}
let check_time = check_time.unwrap_or_else(|| Utc::now().naive_utc());
let mut found_matching_asn = false;
let matches = self.lookup_by_prefix(prefix);
for roa in matches {
if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
found_matching_asn = true;
let is_valid_time = {
if let Some(not_before) = roa.not_before {
if check_time < not_before {
false } else {
true
}
} else {
true }
} && {
if let Some(not_after) = roa.not_after {
if check_time > not_after {
false } else {
true
}
} else {
true }
};
if is_valid_time {
return RpkiValidation::Valid;
}
}
}
if found_matching_asn {
return RpkiValidation::Unknown;
}
RpkiValidation::Invalid
}
pub fn reload(&mut self) -> Result<()> {
match self.date {
Some(date) => {
let trie = RpkiTrie::from_ripe_historical(date)?;
self.trie = trie.trie;
self.aspas = trie.aspas;
}
None => {
let trie = RpkiTrie::from_cloudflare()?;
self.trie = trie.trie;
self.aspas = trie.aspas;
}
}
Ok(())
}
}
impl LazyLoadable for RpkiTrie {
fn reload(&mut self) -> Result<()> {
self.reload()
}
fn is_loaded(&self) -> bool {
!self.trie.is_empty()
}
fn loading_status(&self) -> &'static str {
if self.is_loaded() {
"RPKI data loaded"
} else {
"RPKI data not loaded"
}
}
}
impl BgpkitCommons {
pub fn rpki_lookup_by_prefix(&self, prefix: &str) -> Result<Vec<Roa>> {
if self.rpki_trie.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::RPKI,
load_methods::LOAD_RPKI,
));
}
let prefix = prefix.parse()?;
Ok(self.rpki_trie.as_ref().unwrap().lookup_by_prefix(&prefix))
}
pub fn rpki_validate(&self, asn: u32, prefix: &str) -> Result<RpkiValidation> {
if self.rpki_trie.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::RPKI,
load_methods::LOAD_RPKI,
));
}
let prefix = prefix.parse()?;
Ok(self.rpki_trie.as_ref().unwrap().validate(&prefix, asn))
}
pub fn rpki_validate_check_expiry(
&self,
asn: u32,
prefix: &str,
check_time: Option<NaiveDateTime>,
) -> Result<RpkiValidation> {
if self.rpki_trie.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::RPKI,
load_methods::LOAD_RPKI,
));
}
let prefix = prefix.parse()?;
Ok(self
.rpki_trie
.as_ref()
.unwrap()
.validate_check_expiry(&prefix, asn, check_time))
}
pub fn rpki_lookup_aspa(&self, customer_asn: u32) -> Result<Option<Aspa>> {
if self.rpki_trie.is_none() {
return Err(BgpkitCommonsError::module_not_loaded(
modules::RPKI,
load_methods::LOAD_RPKI,
));
}
Ok(self
.rpki_trie
.as_ref()
.unwrap()
.aspas
.iter()
.find(|a| a.customer_asn == customer_asn)
.cloned())
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::DateTime;
#[test]
fn test_multiple_roas_same_prefix() {
let mut trie = RpkiTrie::new(None);
let roa1 = Roa {
prefix: "192.0.2.0/24".parse().unwrap(),
asn: 64496,
max_length: 24,
rir: Some(Rir::APNIC),
not_before: None,
not_after: None,
};
assert!(trie.insert_roa(roa1.clone()));
let roa2 = Roa {
prefix: "192.0.2.0/24".parse().unwrap(),
asn: 64497,
max_length: 24,
rir: Some(Rir::APNIC),
not_before: None,
not_after: None,
};
assert!(!trie.insert_roa(roa2.clone()));
let roa_dup = Roa {
prefix: "192.0.2.0/24".parse().unwrap(),
asn: 64496,
max_length: 24,
rir: Some(Rir::ARIN), not_before: None,
not_after: None,
};
assert!(!trie.insert_roa(roa_dup));
let roa3 = Roa {
prefix: "192.0.2.0/24".parse().unwrap(),
asn: 64496,
max_length: 28,
rir: Some(Rir::APNIC),
not_before: None,
not_after: None,
};
assert!(!trie.insert_roa(roa3.clone()));
let prefix: IpNet = "192.0.2.0/24".parse().unwrap();
let roas = trie.lookup_by_prefix(&prefix);
assert_eq!(roas.len(), 3);
assert_eq!(trie.validate(&prefix, 64496), RpkiValidation::Valid);
assert_eq!(trie.validate(&prefix, 64497), RpkiValidation::Valid);
assert_eq!(trie.validate(&prefix, 64498), RpkiValidation::Invalid);
let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
assert_eq!(
trie.validate(&unknown_prefix, 64496),
RpkiValidation::Unknown
);
}
#[test]
fn test_validate_check_expiry_with_time_constraints() {
let mut trie = RpkiTrie::new(None);
let past_time = DateTime::from_timestamp(1600000000, 0)
.map(|dt| dt.naive_utc())
.unwrap();
let current_time = DateTime::from_timestamp(1700000000, 0)
.map(|dt| dt.naive_utc())
.unwrap();
let future_time = DateTime::from_timestamp(1800000000, 0)
.map(|dt| dt.naive_utc())
.unwrap();
let roa_valid = Roa {
prefix: "192.0.2.0/24".parse().unwrap(),
asn: 64496,
max_length: 24,
rir: Some(Rir::APNIC),
not_before: Some(past_time),
not_after: Some(future_time),
};
trie.insert_roa(roa_valid);
let roa_expired = Roa {
prefix: "198.51.100.0/24".parse().unwrap(),
asn: 64497,
max_length: 24,
rir: Some(Rir::APNIC),
not_before: Some(past_time),
not_after: Some(past_time), };
trie.insert_roa(roa_expired);
let roa_future = Roa {
prefix: "203.0.113.0/24".parse().unwrap(),
asn: 64498,
max_length: 24,
rir: Some(Rir::APNIC),
not_before: Some(future_time), not_after: None,
};
trie.insert_roa(roa_future);
let prefix_valid: IpNet = "192.0.2.0/24".parse().unwrap();
assert_eq!(
trie.validate_check_expiry(&prefix_valid, 64496, Some(current_time)),
RpkiValidation::Valid
);
let prefix_expired: IpNet = "198.51.100.0/24".parse().unwrap();
assert_eq!(
trie.validate_check_expiry(&prefix_expired, 64497, Some(current_time)),
RpkiValidation::Unknown
);
let prefix_future: IpNet = "203.0.113.0/24".parse().unwrap();
assert_eq!(
trie.validate_check_expiry(&prefix_future, 64498, Some(current_time)),
RpkiValidation::Unknown
);
let far_future = DateTime::from_timestamp(1900000000, 0)
.map(|dt| dt.naive_utc())
.unwrap();
assert_eq!(
trie.validate_check_expiry(&prefix_future, 64498, Some(far_future)),
RpkiValidation::Valid
);
assert_eq!(
trie.validate_check_expiry(&prefix_valid, 64499, Some(current_time)),
RpkiValidation::Invalid
);
}
#[test]
#[ignore] fn test_load_from_ripe_historical() {
let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
let trie = RpkiTrie::from_ripe_historical(date).expect("Failed to load RIPE data");
let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
println!(
"Loaded {} ROAs from RIPE historical for {}",
total_roas, date
);
println!("Loaded {} ASPAs", trie.aspas.len());
assert!(total_roas > 0, "Should have loaded some ROAs");
}
#[test]
#[ignore] fn test_load_from_rpkiviews() {
let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
let trie = RpkiTrie::from_rpkiviews(RpkiViewsCollector::default(), date)
.expect("Failed to load RPKIviews data");
let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
println!("Loaded {} ROAs from RPKIviews for {}", total_roas, date);
println!("Loaded {} ASPAs", trie.aspas.len());
assert!(total_roas > 0, "Should have loaded some ROAs");
}
#[test]
#[ignore] fn test_rpkiviews_file_position() {
use crate::rpki::rpkiviews::list_files_in_tgz;
let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
.expect("Failed to list files");
assert!(!files.is_empty(), "Should have found some files");
let tgz_url = &files[0].url;
println!("Checking file positions in: {}", tgz_url);
let entries = list_files_in_tgz(tgz_url, Some(50)).expect("Failed to list tgz entries");
let json_position = entries
.iter()
.position(|e| e.path.ends_with("rpki-client.json"));
println!("First {} entries:", entries.len());
for (i, entry) in entries.iter().enumerate() {
println!(" [{}] {} ({} bytes)", i, entry.path, entry.size);
}
if let Some(pos) = json_position {
println!(
"\nrpki-client.json found at position {} (early in archive)",
pos
);
assert!(
pos < 50,
"rpki-client.json should appear early in the archive"
);
} else {
println!("\nrpki-client.json not in first 50 entries - may need to stream more");
}
}
#[test]
#[ignore] fn test_list_rpkiviews_files() {
let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
.expect("Failed to list files");
println!("Found {} files for {} from Kerfuffle", files.len(), date);
for file in files.iter().take(3) {
println!(
" {} ({} bytes, {})",
file.url,
file.size.unwrap_or(0),
file.timestamp
);
}
assert!(!files.is_empty(), "Should have found some files");
}
#[test]
fn test_validate_max_length_exceeded() {
let mut trie = RpkiTrie::new(None);
let roa = Roa {
prefix: "103.21.244.0/23".parse().unwrap(),
asn: 13335, max_length: 23,
rir: Some(Rir::APNIC),
not_before: None,
not_after: None,
};
trie.insert_roa(roa);
let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
assert_eq!(
trie.validate(&prefix_24, 13335),
RpkiValidation::Invalid,
"Prefix covered by ROA but max_length exceeded should be Invalid"
);
assert_eq!(
trie.validate(&prefix_24, 64496),
RpkiValidation::Invalid,
"Prefix covered by ROA with wrong ASN should be Invalid"
);
let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
assert_eq!(
trie.validate(&prefix_23, 13335),
RpkiValidation::Valid,
"Exact prefix match with correct ASN should be Valid"
);
let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
assert_eq!(
trie.validate(&unknown_prefix, 13335),
RpkiValidation::Unknown,
"Prefix not covered by any ROA should be Unknown"
);
}
#[test]
fn test_validate_check_expiry_max_length_exceeded() {
let mut trie = RpkiTrie::new(None);
let current_time = DateTime::from_timestamp(1700000000, 0)
.map(|dt| dt.naive_utc())
.unwrap();
let future_time = DateTime::from_timestamp(1800000000, 0)
.map(|dt| dt.naive_utc())
.unwrap();
let roa = Roa {
prefix: "103.21.244.0/23".parse().unwrap(),
asn: 13335,
max_length: 23,
rir: Some(Rir::APNIC),
not_before: Some(current_time),
not_after: Some(future_time),
};
trie.insert_roa(roa);
let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
assert_eq!(
trie.validate_check_expiry(&prefix_24, 13335, Some(current_time)),
RpkiValidation::Invalid,
"Prefix covered by ROA but max_length exceeded should be Invalid"
);
assert_eq!(
trie.validate_check_expiry(&prefix_24, 64496, Some(current_time)),
RpkiValidation::Invalid,
"Prefix covered by ROA with wrong ASN should be Invalid"
);
let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
assert_eq!(
trie.validate_check_expiry(&prefix_23, 13335, Some(current_time)),
RpkiValidation::Valid,
"Exact prefix match with correct ASN should be Valid"
);
let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
assert_eq!(
trie.validate_check_expiry(&unknown_prefix, 13335, Some(current_time)),
RpkiValidation::Unknown,
"Prefix not covered by any ROA should be Unknown"
);
}
#[test]
fn test_lookup_covering_roas() {
let mut trie = RpkiTrie::new(None);
let roa = Roa {
prefix: "103.21.244.0/23".parse().unwrap(),
asn: 13335,
max_length: 23,
rir: Some(Rir::APNIC),
not_before: None,
not_after: None,
};
trie.insert_roa(roa);
let roa2 = Roa {
prefix: "192.0.2.0/24".parse().unwrap(),
asn: 64496,
max_length: 24,
rir: Some(Rir::ARIN),
not_before: None,
not_after: None,
};
trie.insert_roa(roa2);
let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
let covering = trie.lookup_covering_roas(&prefix_24);
assert_eq!(covering.len(), 1, "Should find 1 covering ROA");
assert_eq!(covering[0].asn, 13335);
let matching = trie.lookup_by_prefix(&prefix_24);
assert!(
matching.is_empty(),
"lookup_by_prefix should filter by max_length"
);
let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
let covering_exact = trie.lookup_covering_roas(&prefix_23);
let matching_exact = trie.lookup_by_prefix(&prefix_23);
assert_eq!(covering_exact.len(), 1);
assert_eq!(matching_exact.len(), 1);
let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
assert!(trie.lookup_covering_roas(&unknown_prefix).is_empty());
assert!(trie.lookup_by_prefix(&unknown_prefix).is_empty());
}
}