use crate::utils::truncate_name;
use anyhow::{anyhow, Result};
use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiTrie, RpkiViewsCollector};
use chrono::NaiveDate;
use ipnet::IpNet;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tabled::Tabled;
#[derive(Debug, Clone, Serialize, Deserialize, Tabled)]
pub struct RpkiRoaEntry {
pub prefix: String,
pub max_length: u8,
pub origin_asn: u32,
pub ta: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpkiAspaProvider {
pub asn: u32,
pub name: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpkiAspaEntry {
pub customer_asn: u32,
pub customer_name: Option<String>,
pub customer_country: Option<String>,
pub providers: Vec<RpkiAspaProvider>,
}
#[derive(Debug, Clone, Tabled)]
pub struct RpkiAspaTableEntry {
#[tabled(rename = "Customer ASN")]
pub customer_asn: String,
#[tabled(rename = "Customer Name")]
pub customer_name: String,
#[tabled(rename = "Country")]
pub customer_country: String,
#[tabled(rename = "Providers")]
pub providers: String,
}
const DEFAULT_NAME_MAX_WIDTH: usize = 20;
impl From<&RpkiAspaEntry> for RpkiAspaTableEntry {
fn from(entry: &RpkiAspaEntry) -> Self {
RpkiAspaTableEntry {
customer_asn: format!("AS{}", entry.customer_asn),
customer_name: entry
.customer_name
.as_ref()
.map(|n| truncate_name(n, DEFAULT_NAME_MAX_WIDTH))
.unwrap_or_else(|| "—".to_string()),
customer_country: entry
.customer_country
.clone()
.unwrap_or_else(|| "—".to_string()),
providers: entry
.providers
.iter()
.map(|p| p.asn.to_string())
.collect::<Vec<_>>()
.join(", "),
}
}
}
pub fn parse_rpkiviews_collector(collector: &str) -> Result<RpkiViewsCollector> {
match collector.to_lowercase().as_str() {
"soborost" | "soborostnet" => Ok(RpkiViewsCollector::SoborostNet),
"massars" | "massarsnet" => Ok(RpkiViewsCollector::MassarsNet),
"attn" | "attnjp" => Ok(RpkiViewsCollector::AttnJp),
"kerfuffle" | "kerfufflenet" => Ok(RpkiViewsCollector::KerfuffleNet),
_ => Err(anyhow!(
"Unknown RPKIviews collector: {}. Valid options: soborost, massars, attn, kerfuffle",
collector
)),
}
}
pub fn parse_historical_source(
source: &str,
collector: Option<&str>,
) -> Result<HistoricalRpkiSource> {
match source.to_lowercase().as_str() {
"ripe" => Ok(HistoricalRpkiSource::Ripe),
"rpkiviews" => {
let collector = collector.unwrap_or("soborost");
let rpkiviews_collector = parse_rpkiviews_collector(collector)?;
Ok(HistoricalRpkiSource::RpkiViews(rpkiviews_collector))
}
_ => Err(anyhow!(
"Unknown RPKI source: {}. Valid options: ripe, rpkiviews",
source
)),
}
}
pub fn load_current_rpki() -> Result<RpkiTrie> {
RpkiTrie::from_cloudflare().map_err(|e| anyhow!("Failed to load current RPKI data: {}", e))
}
pub fn load_historical_rpki(date: NaiveDate, source: HistoricalRpkiSource) -> Result<RpkiTrie> {
match source {
HistoricalRpkiSource::Ripe => RpkiTrie::from_ripe_historical(date)
.map_err(|e| anyhow!("Failed to load RIPE historical RPKI data: {}", e)),
HistoricalRpkiSource::RpkiViews(collector) => RpkiTrie::from_rpkiviews(collector, date)
.map_err(|e| anyhow!("Failed to load RPKIviews RPKI data: {}", e)),
}
}
pub fn load_rpki_data(
date: Option<NaiveDate>,
source: Option<&str>,
collector: Option<&str>,
) -> Result<RpkiTrie> {
match date {
None => load_current_rpki(),
Some(d) => {
let source_str = source.unwrap_or("ripe");
let historical_source = parse_historical_source(source_str, collector)?;
load_historical_rpki(d, historical_source)
}
}
}
pub fn get_roas(
trie: &RpkiTrie,
prefix_filter: Option<&str>,
asn_filter: Option<u32>,
) -> Result<Vec<RpkiRoaEntry>> {
let mut results: Vec<RpkiRoaEntry> = Vec::new();
if let Some(prefix_str) = prefix_filter {
let prefix = IpNet::from_str(prefix_str)
.map_err(|e| anyhow!("Invalid prefix '{}': {}", prefix_str, e))?;
let roas = trie.lookup_by_prefix(&prefix);
for roa in roas {
if let Some(asn) = asn_filter {
if roa.asn != asn {
continue;
}
}
results.push(RpkiRoaEntry {
prefix: roa.prefix.to_string(),
max_length: roa.max_length,
origin_asn: roa.asn,
ta: roa.rir.map(|r| format!("{:?}", r)).unwrap_or_default(),
});
}
} else {
for (prefix, roas) in trie.trie.iter() {
for roa in roas {
if let Some(asn) = asn_filter {
if roa.asn != asn {
continue;
}
}
results.push(RpkiRoaEntry {
prefix: prefix.to_string(),
max_length: roa.max_length,
origin_asn: roa.asn,
ta: roa.rir.map(|r| format!("{:?}", r)).unwrap_or_default(),
});
}
}
}
results.sort_by(|a, b| a.prefix.cmp(&b.prefix));
Ok(results)
}
pub fn get_aspas(
trie: &RpkiTrie,
customer_asn: Option<u32>,
provider_asn: Option<u32>,
) -> Result<Vec<RpkiAspaEntry>> {
let mut results: Vec<RpkiAspaEntry> = Vec::new();
for aspa in &trie.aspas {
if let Some(customer) = customer_asn {
if aspa.customer_asn != customer {
continue;
}
}
let filtered_providers: Vec<u32> = if let Some(prov_filter) = provider_asn {
aspa.providers
.iter()
.copied()
.filter(|&p| p == prov_filter)
.collect()
} else {
aspa.providers.clone()
};
if filtered_providers.is_empty() {
continue;
}
let mut sorted_providers = filtered_providers;
sorted_providers.sort();
let providers_with_names: Vec<RpkiAspaProvider> = sorted_providers
.into_iter()
.map(|asn| RpkiAspaProvider { asn, name: None })
.collect();
results.push(RpkiAspaEntry {
customer_asn: aspa.customer_asn,
customer_name: None,
customer_country: None,
providers: providers_with_names,
});
}
results.sort_by_key(|a| a.customer_asn);
Ok(results)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_rpkiviews_collector() {
assert!(matches!(
parse_rpkiviews_collector("soborost").unwrap(),
RpkiViewsCollector::SoborostNet
));
assert!(matches!(
parse_rpkiviews_collector("kerfuffle").unwrap(),
RpkiViewsCollector::KerfuffleNet
));
assert!(parse_rpkiviews_collector("invalid").is_err());
}
#[test]
fn test_parse_historical_source() {
assert!(matches!(
parse_historical_source("ripe", None).unwrap(),
HistoricalRpkiSource::Ripe
));
assert!(matches!(
parse_historical_source("rpkiviews", Some("soborost")).unwrap(),
HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::SoborostNet)
));
}
}