Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,20 @@ All notable changes to this project will be documented in this file.

## Unreleased

### Feature flags

* Added `bcder` and `zstd` as optional dependencies under the `rpki` feature flag

### New features

* Added RPKISPOOL data source support ([draft-snijders-rpkispool-format](https://datatracker.ietf.org/doc/draft-snijders-rpkispool-format/))
- Parses CCR ([draft-ietf-sidrops-rpki-ccr](https://datatracker.ietf.org/doc/draft-ietf-sidrops-rpki-ccr/)) files from RPKISPOOL `.tar.zst` archives
- Uses `bcder` for DER parsing instead of processing ~942K individual `.roa` files
- Added `RpkiSpoolsCollector` enum with three mirrors: `SobornostNet`, `AttnJp`, `KerfuffleNet`
- Public API: `parse_ccr()`, `parse_rpkispools_archive()`, `RpkiTrie::from_rpkispools()`
- Integrated into `BgpkitCommons::load_rpki_historical()`, `load_rpki_from_files()`, `list_rpki_files()`
- Added `rpkispools` example

### Bug fixes

* Fixed typo in `RpkiViewsCollector` enum variant: `SoborostNet` renamed to `SobornostNet`
Expand Down
8 changes: 7 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ regex = { version = "1", optional = true }
serde_json = { version = "1", optional = true }
tracing = { version = "0.1", optional = true }
tar = { version = "0.4", optional = true }
zstd = { version = "0.13", optional = true }
bcder = { version = "0.7", optional = true }

[dev-dependencies]
tracing-subscriber = "0.3"
Expand All @@ -42,7 +44,7 @@ as2rel = ["oneio", "serde_json", "tracing"]
bogons = ["oneio", "ipnet", "regex", "chrono"]
countries = ["oneio"]
mrt_collectors = ["oneio", "chrono"]
rpki = ["oneio", "ipnet", "ipnet-trie", "chrono", "tracing", "tar", "serde_json"]
rpki = ["oneio", "ipnet", "ipnet-trie", "chrono", "tracing", "tar", "serde_json", "zstd", "bcder"]

# Convenience feature to enable all modules
all = ["asinfo", "as2rel", "bogons", "countries", "mrt_collectors", "rpki"]
Expand All @@ -64,6 +66,10 @@ required-features = ["rpki"]
name = "rpki_historical"
required-features = ["rpki"]

[[example]]
name = "rpkispools"
required-features = ["rpki"]

[lints.clippy]
uninlined_format_args = "allow"
collapsible_if = "allow"
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ graph TD
M3 -->|bogons_match| A3[IANA registries]
M4 -->|country_by_code| A4[GeoNames]
M5 -->|mrt_collectors_all| A5[RouteViews / RIPE RIS]
M6 -->|rpki_validate| A6[Cloudflare / RIPE NCC / RPKIviews]
M6 -->|rpki_validate| A6[Cloudflare / RIPE NCC / RPKIviews / RPKISPOOL]
```

Each module is gated by a feature flag. The `all` feature (default) enables everything.
Expand All @@ -42,7 +42,7 @@ Data is fetched on the first `load_xxx()` call and kept in memory until `reload(
| [`bogons`] | `bogons` | IANA special registries | `bogons_match`, `bogons_match_prefix`, `bogons_match_asn` |
| [`countries`] | `countries` | GeoNames | `country_by_code`, `country_by_code3`, `country_by_name` |
| [`mrt_collectors`] | `mrt_collectors` | RouteViews, RIPE RIS | `mrt_collectors_all`, `mrt_collector_peers_all` |
| [`rpki`] | `rpki` | Cloudflare, RIPE NCC, RPKIviews | `rpki_validate`, `rpki_validate_check_expiry`, `rpki_lookup_by_prefix` |
| [`rpki`] | `rpki` | Cloudflare, RIPE NCC, RPKIviews, RPKISPOOL | `rpki_validate`, `rpki_validate_check_expiry`, `rpki_lookup_by_prefix` |

## Quick Start

Expand Down Expand Up @@ -99,10 +99,17 @@ commons.load_rpki_historical(date, HistoricalRpkiSource::Ripe).unwrap();
// Or from an RPKIviews collector
let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::SobornostNet);
commons.load_rpki_historical(date, source).unwrap();

// Or from RPKISPOOL (CCR format, parses faster)
use bgpkit_commons::rpki::RpkiSpoolsCollector;
let source = HistoricalRpkiSource::RpkiSpools(RpkiSpoolsCollector::default());
commons.load_rpki_historical(date, source).unwrap();
```

Available RPKIviews collectors: `SobornostNet` (default), `MassarsNet`, `AttnJp`, `KerfuffleNet`.

Available RPKISPOOL collectors: `SobornostNet` (default), `AttnJp`, `KerfuffleNet`.

### AS Information with Builder

```rust
Expand Down
60 changes: 60 additions & 0 deletions examples/rpkispools.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//! Example demonstrating RPKI data loading from RPKISPOOL archives (CCR format)
//!
//! Run with: cargo run --example rpkispools --features rpki

use bgpkit_commons::BgpkitCommons;
use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiSpoolsCollector};
use chrono::NaiveDate;

fn main() {
tracing_subscriber::fmt::init();

let date = NaiveDate::from_ymd_opt(2026, 1, 1).unwrap();
let mut commons = BgpkitCommons::new();

println!("Loading RPKISPOOL data for {} ...", date);
let source = HistoricalRpkiSource::RpkiSpools(RpkiSpoolsCollector::default());
commons
.load_rpki_historical(date, source)
.expect("failed to load RPKISPOOL data");

let prefix = "1.1.1.0/24";
println!("\nROAs covering {}:", prefix);
match commons.rpki_lookup_by_prefix(prefix) {
Ok(roas) => {
for roa in &roas {
println!(
" prefix={} AS{} max_length={}",
roa.prefix, roa.asn, roa.max_length,
);
}
if roas.is_empty() {
println!(" (none)");
}
}
Err(e) => println!(" Error: {}", e),
}

// Validate Cloudflare's origin for 1.1.1.0/24
let asn = 13335;
println!(
"\nValidation for {} origin AS{}: {:?}",
prefix,
asn,
commons.rpki_validate(asn, prefix).unwrap()
);

// Look up ASPA for AS400644
let customer_asn = 400644;
println!("\nASPA for AS{}:", customer_asn);
match commons.rpki_lookup_aspa(customer_asn) {
Ok(Some(aspa)) => {
println!(
" customer AS{} -> providers: {:?}",
aspa.customer_asn, aspa.providers
);
}
Ok(None) => println!(" (no ASPA found)"),
Err(e) => println!(" Error: {}", e),
}
}
16 changes: 14 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,10 @@
//!
//! ### [`rpki`] — RPKI Validation
//!
//! Feature: `rpki` | Sources: Cloudflare (real-time), RIPE NCC historical, RPKIviews historical
//! Feature: `rpki` | Sources: Cloudflare (real-time), RIPE NCC historical, RPKIviews historical, RPKISPOOL historical
//!
//! - Load: `load_rpki(optional_date)`, `load_rpki_historical(date, source)`, `load_rpki_from_files(urls, source, date)`
//! - Access: `rpki_validate(asn, prefix)`, `rpki_validate_check_expiry(asn, prefix, timestamp)`, `rpki_lookup_by_prefix(prefix)`
//! - Access: `rpki_validate(asn, prefix)`, `rpki_validate_check_expiry(asn, prefix, timestamp)`, `rpki_lookup_by_prefix(prefix)`, `rpki_lookup_aspa(customer_asn)`
//! - Route Origin Authorization (ROA) and ASPA validation, supports real-time and historical sources
//!
//! ## Examples
Expand Down Expand Up @@ -389,6 +389,9 @@ impl BgpkitCommons {
rpki::HistoricalRpkiSource::RpkiViews(collector) => {
self.rpki_trie = Some(rpki::RpkiTrie::from_rpkiviews(collector, date)?);
}
rpki::HistoricalRpkiSource::RpkiSpools(collector) => {
self.rpki_trie = Some(rpki::RpkiTrie::from_rpkispools(collector, date)?);
}
}
Ok(())
}
Expand Down Expand Up @@ -432,6 +435,12 @@ impl BgpkitCommons {
rpki::HistoricalRpkiSource::RpkiViews(_) => {
self.rpki_trie = Some(rpki::RpkiTrie::from_rpkiviews_files(urls, date)?);
}
rpki::HistoricalRpkiSource::RpkiSpools(_) => {
// For RPKISPOOL, each URL is a tar.zst archive; load the first one
if let Some(url) = urls.first() {
self.rpki_trie = Some(rpki::RpkiTrie::from_rpkispools_url(url, date)?);
}
}
}
Ok(())
}
Expand Down Expand Up @@ -466,6 +475,9 @@ impl BgpkitCommons {
rpki::HistoricalRpkiSource::RpkiViews(collector) => {
rpki::list_rpkiviews_files(collector, date)
}
rpki::HistoricalRpkiSource::RpkiSpools(collector) => {
rpki::list_rpkispools_files(collector, date)
}
}
}

Expand Down
32 changes: 31 additions & 1 deletion src/rpki/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@
mod cloudflare;
mod ripe_historical;
pub(crate) mod rpki_client;
mod rpkispools;
mod rpkiviews;

use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
Expand All @@ -146,6 +147,9 @@ 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;
Expand Down Expand Up @@ -210,15 +214,20 @@ pub enum HistoricalRpkiSource {
/// RIPE NCC historical archives (data from all 5 RIRs)
#[default]
Ripe,
/// RPKIviews collector
/// RPKIviews collector (tgz archives with rpki-client JSON)
RpkiViews(RpkiViewsCollector),
/// RPKISPOOL collector (tar.zst archives with CCR files)
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)
}
}
}
}
Expand Down Expand Up @@ -627,6 +636,27 @@ impl BgpkitCommons {
.unwrap()
.validate_check_expiry(&prefix, asn, check_time))
}

/// Look up ASPA records for a given customer ASN.
///
/// Returns the ASPA record if one exists for the given customer ASN,
/// or `None` if no ASPA is registered.
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())
}
}

// ============================================================================
Expand Down
Loading
Loading