bgpkit_commons/rpki/mod.rs
1//! RPKI (Resource Public Key Infrastructure) validation and data structures.
2//!
3//! This module provides functionality for loading and validating RPKI data from multiple sources,
4//! including real-time data from Cloudflare and historical data from RIPE NCC, RPKIviews, or RPKISPOOL.
5//!
6//! # Overview
7//!
8//! RPKI is a cryptographic framework used to secure internet routing by providing a way to
9//! validate the authenticity of BGP route announcements. This module implements RPKI validation
10//! using Route Origin Authorizations (ROAs) that specify which Autonomous Systems (ASes) are
11//! authorized to originate specific IP prefixes.
12//!
13//! # Data Sources
14//!
15//! ## Real-time Data (Cloudflare)
16//! - **Source**: [Cloudflare RPKI Portal](https://rpki.cloudflare.com/rpki.json)
17//! - **Format**: JSON with ROAs, ASPAs, and BGPsec keys
18//! - **Update Frequency**: Real-time
19//! - **Features**: Includes expiry timestamps for temporal validation
20//! - **Conditional loading**: [`RpkiTrie::from_cloudflare_conditional`] supports
21//! `ETag`/`Last-Modified` validators so frequent pollers receive a cheap
22//! `304 Not Modified` instead of re-downloading the full payload; responses
23//! are gzip-compressed on the wire
24//!
25//! ## Historical Data (RIPE NCC)
26//! - **Source**: [RIPE NCC FTP archives](https://ftp.ripe.net/rpki/)
27//! - **Format**: JSON files (output.json.xz) with ROAs, ASPAs
28//! - **Use Case**: Historical analysis and research
29//! - **Date Range**: Configurable historical date
30//! - **TAL Sources**:
31//! - AFRINIC: <https://ftp.ripe.net/rpki/afrinic.tal/>
32//! - APNIC: <https://ftp.ripe.net/rpki/apnic.tal/>
33//! - ARIN: <https://ftp.ripe.net/rpki/arin.tal/>
34//! - LACNIC: <https://ftp.ripe.net/rpki/lacnic.tal/>
35//! - RIPE NCC: <https://ftp.ripe.net/rpki/ripencc.tal/>
36//!
37//! ## Historical Data (RPKIviews)
38//! - **Source**: [RPKIviews](https://rpkiviews.org/)
39//! - **Format**: Compressed tarballs (.tgz) containing rpki-client.json
40//! - **Use Case**: Historical analysis from multiple vantage points
41//! - **Default Collector**: SobornostNet (josephine.sobornost.net)
42//! - **Collectors**:
43//! - Josephine: A2B Internet (AS51088), Amsterdam, Netherlands
44//! - Amber: Massar (AS57777), Lugano, Switzerland
45//! - Dango: Internet Initiative Japan (AS2497), Tokyo, Japan
46//! - Kerfuffle: Kerfuffle, LLC (AS35008), Fremont, California, United States
47//!
48//! ## Historical Data (RPKISPOOL)
49//! - **Format**: `.tar.zst` archives containing CCR (Canonical Cache Representation) files
50//! - **Use Case**: Efficient historical ROA/ASPA snapshots from collector-specific mirrors
51//! - **Collectors**: SobornostNet, AttnJp, and KerfuffleNet
52//!
53//! # Core Data Structures
54//!
55//! ## RpkiTrie
56//! The main data structure that stores RPKI data in a trie for efficient prefix lookups:
57//! - **Trie**: `IpnetTrie<Vec<Roa>>` - Maps IP prefixes to lists of ROA entries
58//! - **ASPAs**: `Vec<Aspa>` - AS Provider Authorization records
59//! - **Date**: `Option<NaiveDate>` - Optional date for historical data
60//!
61//! ## Roa
62//! Represents a Route Origin Authorization with the following fields:
63//! - `prefix: IpNet` - The IP prefix (e.g., 192.0.2.0/24)
64//! - `asn: u32` - The authorized ASN (e.g., 64496)
65//! - `max_length: u8` - Maximum allowed prefix length for more specifics
66//! - `rir: Option<Rir>` - Regional Internet Registry that issued the ROA
67//! - `not_before: Option<NaiveDateTime>` - ROA validity start time
68//! - `not_after: Option<NaiveDateTime>` - ROA validity end time (from expires field)
69//!
70//! ## Aspa
71//! Represents an AS Provider Authorization with the following fields:
72//! - `customer_asn: u32` - The customer AS number
73//! - `providers: Vec<u32>` - List of provider AS numbers
74//! - `expires: Option<NaiveDateTime>` - When this ASPA expires
75//!
76//! ## Validation Results
77//! RPKI validation returns one of three states:
78//! - **Valid**: The prefix-ASN pair is explicitly authorized by a valid ROA
79//! - **Invalid**: The prefix has ROAs but none authorize the given ASN
80//! - **Unknown**: No ROAs exist for the prefix, or all ROAs are outside their validity period
81//!
82//! # Usage Examples
83//!
84//! ## Loading Real-time Data (Cloudflare)
85//! ```rust,no_run
86//! use bgpkit_commons::BgpkitCommons;
87//!
88//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
89//! let mut commons = BgpkitCommons::new();
90//!
91//! // Load current RPKI data from Cloudflare
92//! commons.load_rpki(None)?;
93//!
94//! // Validate a prefix-ASN pair (standard validation)
95//! let result = commons.rpki_validate(64496, "192.0.2.0/24")?;
96//! match result {
97//! bgpkit_commons::rpki::RpkiValidation::Valid => println!("Route is RPKI valid"),
98//! bgpkit_commons::rpki::RpkiValidation::Invalid => println!("Route is RPKI invalid"),
99//! bgpkit_commons::rpki::RpkiValidation::Unknown => println!("No RPKI data for this prefix"),
100//! }
101//! # Ok(())
102//! # }
103//! ```
104//!
105//! ## Loading Historical Data with Source Selection
106//! ```rust,no_run
107//! use bgpkit_commons::BgpkitCommons;
108//! use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector, RpkiSpoolsCollector};
109//! use chrono::NaiveDate;
110//!
111//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
112//! let mut commons = BgpkitCommons::new();
113//! let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
114//!
115//! // Load from RIPE NCC
116//! commons.load_rpki_historical(date, HistoricalRpkiSource::Ripe)?;
117//!
118//! // Or load from RPKIviews (uses the SobornostNet collector by default)
119//! let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::default());
120//! commons.load_rpki_historical(date, source)?;
121//!
122//! // Or load from RPKISPOOL (CCR format, uses SobornostNet by default)
123//! let source = HistoricalRpkiSource::RpkiSpools(RpkiSpoolsCollector::default());
124//! commons.load_rpki_historical(date, source)?;
125//! # Ok(())
126//! # }
127//! ```
128//!
129//! ## Listing Available Files
130//! ```rust,no_run
131//! use bgpkit_commons::BgpkitCommons;
132//! use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
133//! use chrono::NaiveDate;
134//!
135//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
136//! let commons = BgpkitCommons::new();
137//! let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
138//!
139//! // List available files from RPKIviews (multiple snapshots per day)
140//! let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::default());
141//! let rpkiviews_files = commons.list_rpki_files(date, source)?;
142//! for file in &rpkiviews_files {
143//! println!("RPKIviews file: {} (timestamp: {})", file.url, file.timestamp);
144//! }
145//! # Ok(())
146//! # }
147//! ```
148
149mod cloudflare;
150mod ripe_historical;
151pub(crate) mod rpki_client;
152mod rpkispools;
153mod rpkiviews;
154
155use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
156use ipnet::IpNet;
157use ipnet_trie::IpnetTrie;
158
159use crate::errors::{load_methods, modules};
160use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
161pub use cloudflare::RpkiLoad;
162pub use ripe_historical::list_ripe_files;
163use rpki_client::RpkiClientData;
164pub use rpkispools::{
165 RpkiSpoolsCollector, RpkiSpoolsData, list_rpkispools_files, parse_ccr, parse_rpkispools_archive,
166};
167pub use rpkiviews::{RpkiViewsCollector, list_rpkiviews_files};
168use serde::{Deserialize, Serialize};
169use std::fmt::Display;
170use std::str::FromStr;
171
172// ============================================================================
173// Public Data Structures
174// ============================================================================
175
176/// A validated Route Origin Authorization (ROA).
177///
178/// ROAs authorize specific Autonomous Systems to originate specific IP prefixes.
179#[derive(Clone, Debug, Serialize, Deserialize)]
180pub struct Roa {
181 /// The IP prefix (e.g., 192.0.2.0/24 or 2001:db8::/32)
182 pub prefix: IpNet,
183 /// The AS number authorized to originate this prefix
184 pub asn: u32,
185 /// Maximum prefix length allowed for announcements
186 pub max_length: u8,
187 /// Regional Internet Registry that issued this ROA
188 pub rir: Option<Rir>,
189 /// ROA validity start time (if available)
190 pub not_before: Option<NaiveDateTime>,
191 /// ROA validity end time (from expires field)
192 pub not_after: Option<NaiveDateTime>,
193}
194
195/// A validated AS Provider Authorization (ASPA).
196///
197/// ASPAs specify which ASes are authorized providers for a customer AS.
198#[derive(Clone, Debug, Serialize, Deserialize)]
199pub struct Aspa {
200 /// The customer AS number
201 pub customer_asn: u32,
202 /// List of provider AS numbers
203 pub providers: Vec<u32>,
204 /// When this ASPA expires
205 pub expires: Option<NaiveDateTime>,
206}
207
208/// Information about an available RPKI data file.
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct RpkiFile {
211 /// Full URL to download the file
212 pub url: String,
213 /// Timestamp when the file was created
214 pub timestamp: DateTime<Utc>,
215 /// Size of the file in bytes (if available)
216 pub size: Option<u64>,
217 /// RIR that this file is for (for RIPE files)
218 pub rir: Option<Rir>,
219 /// Collector that provides this file
220 pub collector: Option<RpkiCollector>,
221}
222
223/// Identifies the collector that produced or hosts an RPKI data file.
224#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
225pub enum RpkiCollector {
226 /// RPKIviews collector
227 RpkiViews(RpkiViewsCollector),
228 /// RPKISPOOL collector
229 RpkiSpools(RpkiSpoolsCollector),
230}
231
232/// Historical RPKI data source.
233///
234/// Used to specify which data source to use when loading historical RPKI data.
235#[derive(Debug, Clone, Default)]
236pub enum HistoricalRpkiSource {
237 /// RIPE NCC historical archives (data from all 5 RIRs)
238 #[default]
239 Ripe,
240 /// RPKIviews collector (tgz archives with rpki-client JSON)
241 RpkiViews(RpkiViewsCollector),
242 /// RPKISPOOL collector (tar.zst archives with CCR files)
243 RpkiSpools(RpkiSpoolsCollector),
244}
245
246impl std::fmt::Display for HistoricalRpkiSource {
247 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
248 match self {
249 HistoricalRpkiSource::Ripe => write!(f, "RIPE NCC"),
250 HistoricalRpkiSource::RpkiViews(collector) => write!(f, "RPKIviews ({})", collector),
251 HistoricalRpkiSource::RpkiSpools(collector) => {
252 write!(f, "RPKISPOOL ({})", collector)
253 }
254 }
255 }
256}
257
258/// Regional Internet Registry (RIR).
259#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
260pub enum Rir {
261 AFRINIC,
262 APNIC,
263 ARIN,
264 LACNIC,
265 RIPENCC,
266}
267
268impl FromStr for Rir {
269 type Err = String;
270
271 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
272 match s.to_lowercase().as_str() {
273 "afrinic" => Ok(Rir::AFRINIC),
274 "apnic" => Ok(Rir::APNIC),
275 "arin" => Ok(Rir::ARIN),
276 "lacnic" => Ok(Rir::LACNIC),
277 "ripe" => Ok(Rir::RIPENCC),
278 _ => Err(format!("unknown RIR: {}", s)),
279 }
280 }
281}
282
283impl Display for Rir {
284 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
285 match self {
286 Rir::AFRINIC => write!(f, "AFRINIC"),
287 Rir::APNIC => write!(f, "APNIC"),
288 Rir::ARIN => write!(f, "ARIN"),
289 Rir::LACNIC => write!(f, "LACNIC"),
290 Rir::RIPENCC => write!(f, "RIPENCC"),
291 }
292 }
293}
294
295impl Rir {
296 pub fn to_ripe_ftp_root_url(&self) -> String {
297 match self {
298 Rir::AFRINIC => "https://ftp.ripe.net/rpki/afrinic.tal".to_string(),
299 Rir::APNIC => "https://ftp.ripe.net/rpki/apnic.tal".to_string(),
300 Rir::ARIN => "https://ftp.ripe.net/rpki/arin.tal".to_string(),
301 Rir::LACNIC => "https://ftp.ripe.net/rpki/lacnic.tal".to_string(),
302 Rir::RIPENCC => "https://ftp.ripe.net/rpki/ripencc.tal".to_string(),
303 }
304 }
305}
306
307/// RPKI validation result.
308#[derive(Clone, Debug, PartialEq, Eq)]
309pub enum RpkiValidation {
310 /// The prefix-ASN pair is explicitly authorized by a valid ROA
311 Valid,
312 /// The prefix has ROAs but none authorize the given ASN
313 Invalid,
314 /// No ROAs exist for the prefix, or all ROAs are outside their validity period
315 Unknown,
316}
317
318impl Display for RpkiValidation {
319 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320 match self {
321 RpkiValidation::Valid => write!(f, "valid"),
322 RpkiValidation::Invalid => write!(f, "invalid"),
323 RpkiValidation::Unknown => write!(f, "unknown"),
324 }
325 }
326}
327
328// ============================================================================
329// Backwards Compatibility Type Aliases
330// ============================================================================
331
332/// Type alias for backwards compatibility. Use [`Roa`] instead.
333/// Deprecated since 0.10.0. This alias will be removed in version 0.12.0.
334#[deprecated(since = "0.10.0", note = "Use Roa instead")]
335pub type RoaEntry = Roa;
336
337// ============================================================================
338// RpkiTrie Implementation
339// ============================================================================
340
341/// The main RPKI data structure storing ROAs and ASPAs.
342#[derive(Clone)]
343pub struct RpkiTrie {
344 /// Trie mapping IP prefixes to ROA entries
345 pub trie: IpnetTrie<Vec<Roa>>,
346 /// AS Provider Authorizations
347 pub aspas: Vec<Aspa>,
348 /// Date for historical data (None for real-time)
349 date: Option<NaiveDate>,
350}
351
352impl Default for RpkiTrie {
353 fn default() -> Self {
354 Self {
355 trie: IpnetTrie::new(),
356 aspas: vec![],
357 date: None,
358 }
359 }
360}
361
362impl RpkiTrie {
363 /// Create a new empty RpkiTrie.
364 pub fn new(date: Option<NaiveDate>) -> Self {
365 Self {
366 trie: IpnetTrie::new(),
367 aspas: vec![],
368 date,
369 }
370 }
371
372 /// Insert a ROA. Returns true if this is a new prefix, false if added to existing prefix.
373 /// Duplicates are avoided - ROAs with same (prefix, asn, max_length) are considered identical.
374 pub fn insert_roa(&mut self, roa: Roa) -> bool {
375 match self.trie.exact_match_mut(roa.prefix) {
376 Some(existing_roas) => {
377 // Check if this ROA already exists (same prefix, asn, max_length)
378 if !existing_roas.iter().any(|existing| {
379 existing.asn == roa.asn && existing.max_length == roa.max_length
380 }) {
381 existing_roas.push(roa);
382 }
383 false
384 }
385 None => {
386 self.trie.insert(roa.prefix, vec![roa]);
387 true
388 }
389 }
390 }
391
392 /// Insert multiple ROAs.
393 pub fn insert_roas(&mut self, roas: Vec<Roa>) {
394 for roa in roas {
395 self.insert_roa(roa);
396 }
397 }
398
399 /// Convert rpki-client data into an RpkiTrie.
400 ///
401 /// This is a shared conversion function used by all data sources
402 /// (Cloudflare, RIPE, RPKIviews) since they all produce the same
403 /// rpki-client JSON format.
404 pub(crate) fn from_rpki_client_data(
405 data: RpkiClientData,
406 date: Option<NaiveDate>,
407 ) -> Result<Self> {
408 let mut trie = RpkiTrie::new(date);
409 trie.merge_rpki_client_data(data);
410 Ok(trie)
411 }
412
413 /// Merge rpki-client data into this trie.
414 ///
415 /// This converts ROAs and ASPAs from rpki-client format and inserts them,
416 /// avoiding duplicates for ASPAs based on customer_asn.
417 pub(crate) fn merge_rpki_client_data(&mut self, data: RpkiClientData) {
418 // Convert and insert ROAs
419 for roa in data.roas {
420 let prefix = match roa.prefix.parse::<IpNet>() {
421 Ok(p) => p,
422 Err(_) => continue,
423 };
424 let rir = Rir::from_str(&roa.ta).ok();
425 let not_after =
426 DateTime::from_timestamp(roa.expires as i64, 0).map(|dt| dt.naive_utc());
427
428 self.insert_roa(Roa {
429 prefix,
430 asn: roa.asn,
431 max_length: roa.max_length,
432 rir,
433 not_before: None,
434 not_after,
435 });
436 }
437
438 // Convert and merge ASPAs (avoiding duplicates based on customer_asn)
439 for aspa in data.aspas {
440 if !self
441 .aspas
442 .iter()
443 .any(|a| a.customer_asn == aspa.customer_asid)
444 {
445 let expires = DateTime::from_timestamp(aspa.expires, 0).map(|dt| dt.naive_utc());
446 self.aspas.push(Aspa {
447 customer_asn: aspa.customer_asid,
448 providers: aspa.providers,
449 expires,
450 });
451 }
452 }
453 }
454
455 /// Lookup all ROAs that authorize a given prefix (matching ASN and max_length).
456 pub fn lookup_by_prefix(&self, prefix: &IpNet) -> Vec<Roa> {
457 let mut all_matches = vec![];
458 for (p, roas) in self.trie.matches(prefix) {
459 if p.contains(prefix) {
460 for roa in roas {
461 if roa.max_length >= prefix.prefix_len() {
462 all_matches.push(roa.clone());
463 }
464 }
465 }
466 }
467 all_matches
468 }
469
470 /// Lookup all ROAs that cover a given prefix, regardless of max_length.
471 ///
472 /// This returns all ROAs whose prefix contains the given prefix,
473 /// without filtering by max_length. Used to determine if a prefix
474 /// is covered by RPKI data at all.
475 fn lookup_covering_roas(&self, prefix: &IpNet) -> Vec<Roa> {
476 let mut all_matches = vec![];
477 for (p, roas) in self.trie.matches(prefix) {
478 if p.contains(prefix) {
479 for roa in roas {
480 all_matches.push(roa.clone());
481 }
482 }
483 }
484 all_matches
485 }
486
487 /// Validate a prefix with an ASN.
488 ///
489 /// Return values:
490 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid
491 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid
492 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI
493 pub fn validate(&self, prefix: &IpNet, asn: u32) -> RpkiValidation {
494 // First check if there are ANY covering ROAs (regardless of max_length)
495 let covering_roas = self.lookup_covering_roas(prefix);
496 if covering_roas.is_empty() {
497 return RpkiValidation::Unknown;
498 }
499
500 // Now check for valid matches (matching ASN and max_length)
501 let matches = self.lookup_by_prefix(prefix);
502 for roa in matches {
503 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
504 return RpkiValidation::Valid;
505 }
506 }
507 // There are covering ROAs but none authorize this prefix/ASN
508 RpkiValidation::Invalid
509 }
510
511 /// Validate a prefix with an ASN, checking expiry dates.
512 ///
513 /// Return values:
514 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid and not expired
515 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid (wrong ASN or max_length exceeded)
516 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI or all matching ROAs are outside their valid time range
517 pub fn validate_check_expiry(
518 &self,
519 prefix: &IpNet,
520 asn: u32,
521 check_time: Option<NaiveDateTime>,
522 ) -> RpkiValidation {
523 // First check if there are ANY covering ROAs (regardless of max_length)
524 let covering_roas = self.lookup_covering_roas(prefix);
525 if covering_roas.is_empty() {
526 return RpkiValidation::Unknown;
527 }
528
529 let check_time = check_time.unwrap_or_else(|| Utc::now().naive_utc());
530
531 let mut found_matching_asn = false;
532
533 // Check for valid matches (matching ASN and max_length)
534 let matches = self.lookup_by_prefix(prefix);
535 for roa in matches {
536 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
537 found_matching_asn = true;
538
539 // Check if ROA is within valid time period
540 let is_valid_time = {
541 if let Some(not_before) = roa.not_before {
542 if check_time < not_before {
543 false // ROA not yet valid
544 } else {
545 true
546 }
547 } else {
548 true // no not_before constraint
549 }
550 } && {
551 if let Some(not_after) = roa.not_after {
552 if check_time > not_after {
553 false // ROA expired
554 } else {
555 true
556 }
557 } else {
558 true // no not_after constraint
559 }
560 };
561
562 if is_valid_time {
563 return RpkiValidation::Valid;
564 }
565 }
566 }
567
568 // If we found matching ASN but all ROAs are outside valid time range, return Unknown
569 if found_matching_asn {
570 return RpkiValidation::Unknown;
571 }
572
573 // There are covering ROAs but none authorize this prefix/ASN
574 RpkiValidation::Invalid
575 }
576
577 /// Reload the RPKI data from its original source.
578 pub fn reload(&mut self) -> Result<()> {
579 match self.date {
580 Some(date) => {
581 let trie = RpkiTrie::from_ripe_historical(date)?;
582 self.trie = trie.trie;
583 self.aspas = trie.aspas;
584 }
585 None => {
586 let trie = RpkiTrie::from_cloudflare()?;
587 self.trie = trie.trie;
588 self.aspas = trie.aspas;
589 }
590 }
591
592 Ok(())
593 }
594}
595
596impl LazyLoadable for RpkiTrie {
597 fn reload(&mut self) -> Result<()> {
598 self.reload()
599 }
600
601 fn is_loaded(&self) -> bool {
602 !self.trie.is_empty()
603 }
604
605 fn loading_status(&self) -> &'static str {
606 if self.is_loaded() {
607 "RPKI data loaded"
608 } else {
609 "RPKI data not loaded"
610 }
611 }
612}
613
614// ============================================================================
615// BgpkitCommons Integration
616// ============================================================================
617
618impl BgpkitCommons {
619 pub fn rpki_lookup_by_prefix(&self, prefix: &str) -> Result<Vec<Roa>> {
620 if self.rpki_trie.is_none() {
621 return Err(BgpkitCommonsError::module_not_loaded(
622 modules::RPKI,
623 load_methods::LOAD_RPKI,
624 ));
625 }
626
627 let prefix = prefix.parse()?;
628
629 Ok(self.rpki_trie.as_ref().unwrap().lookup_by_prefix(&prefix))
630 }
631
632 pub fn rpki_validate(&self, asn: u32, prefix: &str) -> Result<RpkiValidation> {
633 if self.rpki_trie.is_none() {
634 return Err(BgpkitCommonsError::module_not_loaded(
635 modules::RPKI,
636 load_methods::LOAD_RPKI,
637 ));
638 }
639 let prefix = prefix.parse()?;
640 Ok(self.rpki_trie.as_ref().unwrap().validate(&prefix, asn))
641 }
642
643 pub fn rpki_validate_check_expiry(
644 &self,
645 asn: u32,
646 prefix: &str,
647 check_time: Option<NaiveDateTime>,
648 ) -> Result<RpkiValidation> {
649 if self.rpki_trie.is_none() {
650 return Err(BgpkitCommonsError::module_not_loaded(
651 modules::RPKI,
652 load_methods::LOAD_RPKI,
653 ));
654 }
655 let prefix = prefix.parse()?;
656 Ok(self
657 .rpki_trie
658 .as_ref()
659 .unwrap()
660 .validate_check_expiry(&prefix, asn, check_time))
661 }
662
663 /// Look up ASPA records for a given customer ASN.
664 ///
665 /// Returns the ASPA record if one exists for the given customer ASN,
666 /// or `None` if no ASPA is registered.
667 pub fn rpki_lookup_aspa(&self, customer_asn: u32) -> Result<Option<Aspa>> {
668 if self.rpki_trie.is_none() {
669 return Err(BgpkitCommonsError::module_not_loaded(
670 modules::RPKI,
671 load_methods::LOAD_RPKI,
672 ));
673 }
674 Ok(self
675 .rpki_trie
676 .as_ref()
677 .unwrap()
678 .aspas
679 .iter()
680 .find(|a| a.customer_asn == customer_asn)
681 .cloned())
682 }
683}
684
685// ============================================================================
686// Tests
687// ============================================================================
688
689#[cfg(test)]
690mod tests {
691 use super::*;
692 use chrono::DateTime;
693
694 #[test]
695 fn test_multiple_roas_same_prefix() {
696 let mut trie = RpkiTrie::new(None);
697
698 // Insert first ROA
699 let roa1 = Roa {
700 prefix: "192.0.2.0/24".parse().unwrap(),
701 asn: 64496,
702 max_length: 24,
703 rir: Some(Rir::APNIC),
704 not_before: None,
705 not_after: None,
706 };
707 assert!(trie.insert_roa(roa1.clone()));
708
709 // Insert second ROA with different ASN for same prefix
710 let roa2 = Roa {
711 prefix: "192.0.2.0/24".parse().unwrap(),
712 asn: 64497,
713 max_length: 24,
714 rir: Some(Rir::APNIC),
715 not_before: None,
716 not_after: None,
717 };
718 assert!(!trie.insert_roa(roa2.clone()));
719
720 // Insert duplicate ROA (same prefix, asn, max_length) - should be ignored
721 let roa_dup = Roa {
722 prefix: "192.0.2.0/24".parse().unwrap(),
723 asn: 64496,
724 max_length: 24,
725 rir: Some(Rir::ARIN), // Different RIR shouldn't matter
726 not_before: None,
727 not_after: None,
728 };
729 assert!(!trie.insert_roa(roa_dup));
730
731 // Insert ROA with different max_length - should be added
732 let roa3 = Roa {
733 prefix: "192.0.2.0/24".parse().unwrap(),
734 asn: 64496,
735 max_length: 28,
736 rir: Some(Rir::APNIC),
737 not_before: None,
738 not_after: None,
739 };
740 assert!(!trie.insert_roa(roa3.clone()));
741
742 // Lookup should return 3 ROAs (roa1, roa2, roa3)
743 let prefix: IpNet = "192.0.2.0/24".parse().unwrap();
744 let roas = trie.lookup_by_prefix(&prefix);
745 assert_eq!(roas.len(), 3);
746
747 // Validate AS 64496 - should be valid
748 assert_eq!(trie.validate(&prefix, 64496), RpkiValidation::Valid);
749
750 // Validate AS 64497 - should be valid
751 assert_eq!(trie.validate(&prefix, 64497), RpkiValidation::Valid);
752
753 // Validate AS 64498 - should be invalid (prefix has ROAs but not for this ASN)
754 assert_eq!(trie.validate(&prefix, 64498), RpkiValidation::Invalid);
755
756 // Validate unknown prefix - should be unknown
757 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
758 assert_eq!(
759 trie.validate(&unknown_prefix, 64496),
760 RpkiValidation::Unknown
761 );
762 }
763
764 #[test]
765 fn test_validate_check_expiry_with_time_constraints() {
766 let mut trie = RpkiTrie::new(None);
767
768 // Time references
769 let past_time = DateTime::from_timestamp(1600000000, 0)
770 .map(|dt| dt.naive_utc())
771 .unwrap();
772 let current_time = DateTime::from_timestamp(1700000000, 0)
773 .map(|dt| dt.naive_utc())
774 .unwrap();
775 let future_time = DateTime::from_timestamp(1800000000, 0)
776 .map(|dt| dt.naive_utc())
777 .unwrap();
778
779 // Insert ROA that's currently valid (not_before in past, not_after in future)
780 let roa_valid = Roa {
781 prefix: "192.0.2.0/24".parse().unwrap(),
782 asn: 64496,
783 max_length: 24,
784 rir: Some(Rir::APNIC),
785 not_before: Some(past_time),
786 not_after: Some(future_time),
787 };
788 trie.insert_roa(roa_valid);
789
790 // Insert ROA that's expired
791 let roa_expired = Roa {
792 prefix: "198.51.100.0/24".parse().unwrap(),
793 asn: 64497,
794 max_length: 24,
795 rir: Some(Rir::APNIC),
796 not_before: Some(past_time),
797 not_after: Some(past_time), // Expired in the past
798 };
799 trie.insert_roa(roa_expired);
800
801 // Insert ROA that's not yet valid
802 let roa_future = Roa {
803 prefix: "203.0.113.0/24".parse().unwrap(),
804 asn: 64498,
805 max_length: 24,
806 rir: Some(Rir::APNIC),
807 not_before: Some(future_time), // Not valid yet
808 not_after: None,
809 };
810 trie.insert_roa(roa_future);
811
812 // Test valid ROA at current time
813 let prefix_valid: IpNet = "192.0.2.0/24".parse().unwrap();
814 assert_eq!(
815 trie.validate_check_expiry(&prefix_valid, 64496, Some(current_time)),
816 RpkiValidation::Valid
817 );
818
819 // Test expired ROA at current time - should return Unknown (was valid but expired)
820 let prefix_expired: IpNet = "198.51.100.0/24".parse().unwrap();
821 assert_eq!(
822 trie.validate_check_expiry(&prefix_expired, 64497, Some(current_time)),
823 RpkiValidation::Unknown
824 );
825
826 // Test not-yet-valid ROA at current time - should return Unknown
827 let prefix_future: IpNet = "203.0.113.0/24".parse().unwrap();
828 assert_eq!(
829 trie.validate_check_expiry(&prefix_future, 64498, Some(current_time)),
830 RpkiValidation::Unknown
831 );
832
833 // Test not-yet-valid ROA at future time - should return Valid
834 let far_future = DateTime::from_timestamp(1900000000, 0)
835 .map(|dt| dt.naive_utc())
836 .unwrap();
837 assert_eq!(
838 trie.validate_check_expiry(&prefix_future, 64498, Some(far_future)),
839 RpkiValidation::Valid
840 );
841
842 // Test wrong ASN - should return Invalid
843 assert_eq!(
844 trie.validate_check_expiry(&prefix_valid, 64499, Some(current_time)),
845 RpkiValidation::Invalid
846 );
847 }
848
849 #[test]
850 #[ignore] // Requires network access
851 fn test_load_from_ripe_historical() {
852 // Use a recent date that should have data available
853 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
854 let trie = RpkiTrie::from_ripe_historical(date).expect("Failed to load RIPE data");
855
856 let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
857 println!(
858 "Loaded {} ROAs from RIPE historical for {}",
859 total_roas, date
860 );
861 println!("Loaded {} ASPAs", trie.aspas.len());
862
863 assert!(total_roas > 0, "Should have loaded some ROAs");
864 }
865
866 #[test]
867 #[ignore] // Requires network access
868 fn test_load_from_rpkiviews() {
869 // Note: This test streams from a remote tgz file but stops early
870 // once rpki-client.json is found (typically at position 3-4 in the archive).
871 // Due to streaming optimization, this typically completes in ~8 seconds.
872 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
873 let trie = RpkiTrie::from_rpkiviews(RpkiViewsCollector::default(), date)
874 .expect("Failed to load RPKIviews data");
875
876 let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
877 println!("Loaded {} ROAs from RPKIviews for {}", total_roas, date);
878 println!("Loaded {} ASPAs", trie.aspas.len());
879
880 assert!(total_roas > 0, "Should have loaded some ROAs");
881 }
882
883 #[test]
884 #[ignore] // Requires network access
885 fn test_rpkiviews_file_position() {
886 // Verify that rpki-client.json appears early in the archive
887 // This confirms our early-termination optimization works
888 use crate::rpki::rpkiviews::list_files_in_tgz;
889
890 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
891 let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
892 .expect("Failed to list files");
893
894 assert!(!files.is_empty(), "Should have found some files");
895
896 let tgz_url = &files[0].url;
897 println!("Checking file positions in: {}", tgz_url);
898
899 // List first 50 entries to see where rpki-client.json appears
900 let entries = list_files_in_tgz(tgz_url, Some(50)).expect("Failed to list tgz entries");
901
902 let json_position = entries
903 .iter()
904 .position(|e| e.path.ends_with("rpki-client.json"));
905
906 println!("First {} entries:", entries.len());
907 for (i, entry) in entries.iter().enumerate() {
908 println!(" [{}] {} ({} bytes)", i, entry.path, entry.size);
909 }
910
911 if let Some(pos) = json_position {
912 println!(
913 "\nrpki-client.json found at position {} (early in archive)",
914 pos
915 );
916 assert!(
917 pos < 50,
918 "rpki-client.json should appear early in the archive"
919 );
920 } else {
921 println!("\nrpki-client.json not in first 50 entries - may need to stream more");
922 }
923 }
924
925 #[test]
926 #[ignore] // Requires network access
927 fn test_list_rpkiviews_files() {
928 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
929 let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
930 .expect("Failed to list files");
931
932 println!("Found {} files for {} from Kerfuffle", files.len(), date);
933 for file in files.iter().take(3) {
934 println!(
935 " {} ({} bytes, {})",
936 file.url,
937 file.size.unwrap_or(0),
938 file.timestamp
939 );
940 }
941
942 assert!(!files.is_empty(), "Should have found some files");
943 }
944
945 #[test]
946 fn test_validate_max_length_exceeded() {
947 // Test the bug where a prefix covered by an ROA but with max_length exceeded
948 // should return Invalid, not Unknown
949 let mut trie = RpkiTrie::new(None);
950
951 // Insert ROA for /23 with max_length 23 (no more specific allowed)
952 let roa = Roa {
953 prefix: "103.21.244.0/23".parse().unwrap(),
954 asn: 13335, // Cloudflare
955 max_length: 23,
956 rir: Some(Rir::APNIC),
957 not_before: None,
958 not_after: None,
959 };
960 trie.insert_roa(roa);
961
962 // /24 is covered by /23 but max_length is 23, so this should be Invalid
963 let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
964
965 // Test with correct ASN - should be Invalid (covered by RPKI but not authorized due to max_length)
966 assert_eq!(
967 trie.validate(&prefix_24, 13335),
968 RpkiValidation::Invalid,
969 "Prefix covered by ROA but max_length exceeded should be Invalid"
970 );
971
972 // Test with wrong ASN - should also be Invalid
973 assert_eq!(
974 trie.validate(&prefix_24, 64496),
975 RpkiValidation::Invalid,
976 "Prefix covered by ROA with wrong ASN should be Invalid"
977 );
978
979 // The /23 itself with correct ASN should be Valid
980 let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
981 assert_eq!(
982 trie.validate(&prefix_23, 13335),
983 RpkiValidation::Valid,
984 "Exact prefix match with correct ASN should be Valid"
985 );
986
987 // Completely unrelated prefix should be Unknown
988 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
989 assert_eq!(
990 trie.validate(&unknown_prefix, 13335),
991 RpkiValidation::Unknown,
992 "Prefix not covered by any ROA should be Unknown"
993 );
994 }
995
996 #[test]
997 fn test_validate_check_expiry_max_length_exceeded() {
998 // Same test but for validate_check_expiry
999 let mut trie = RpkiTrie::new(None);
1000
1001 let current_time = DateTime::from_timestamp(1700000000, 0)
1002 .map(|dt| dt.naive_utc())
1003 .unwrap();
1004 let future_time = DateTime::from_timestamp(1800000000, 0)
1005 .map(|dt| dt.naive_utc())
1006 .unwrap();
1007
1008 // Insert ROA for /23 with max_length 23
1009 let roa = Roa {
1010 prefix: "103.21.244.0/23".parse().unwrap(),
1011 asn: 13335,
1012 max_length: 23,
1013 rir: Some(Rir::APNIC),
1014 not_before: Some(current_time),
1015 not_after: Some(future_time),
1016 };
1017 trie.insert_roa(roa);
1018
1019 // /24 is covered by /23 but max_length is 23, so this should be Invalid
1020 let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
1021
1022 // Test with correct ASN - should be Invalid
1023 assert_eq!(
1024 trie.validate_check_expiry(&prefix_24, 13335, Some(current_time)),
1025 RpkiValidation::Invalid,
1026 "Prefix covered by ROA but max_length exceeded should be Invalid"
1027 );
1028
1029 // Test with wrong ASN - should also be Invalid
1030 assert_eq!(
1031 trie.validate_check_expiry(&prefix_24, 64496, Some(current_time)),
1032 RpkiValidation::Invalid,
1033 "Prefix covered by ROA with wrong ASN should be Invalid"
1034 );
1035
1036 // The /23 itself with correct ASN should be Valid
1037 let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
1038 assert_eq!(
1039 trie.validate_check_expiry(&prefix_23, 13335, Some(current_time)),
1040 RpkiValidation::Valid,
1041 "Exact prefix match with correct ASN should be Valid"
1042 );
1043
1044 // Completely unrelated prefix should be Unknown
1045 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
1046 assert_eq!(
1047 trie.validate_check_expiry(&unknown_prefix, 13335, Some(current_time)),
1048 RpkiValidation::Unknown,
1049 "Prefix not covered by any ROA should be Unknown"
1050 );
1051 }
1052
1053 #[test]
1054 fn test_lookup_covering_roas() {
1055 // Test the helper method that finds all covering ROAs
1056 let mut trie = RpkiTrie::new(None);
1057
1058 // Insert ROA for /23 with max_length 23
1059 let roa = Roa {
1060 prefix: "103.21.244.0/23".parse().unwrap(),
1061 asn: 13335,
1062 max_length: 23,
1063 rir: Some(Rir::APNIC),
1064 not_before: None,
1065 not_after: None,
1066 };
1067 trie.insert_roa(roa);
1068
1069 // Insert another ROA for a different prefix
1070 let roa2 = Roa {
1071 prefix: "192.0.2.0/24".parse().unwrap(),
1072 asn: 64496,
1073 max_length: 24,
1074 rir: Some(Rir::ARIN),
1075 not_before: None,
1076 not_after: None,
1077 };
1078 trie.insert_roa(roa2);
1079
1080 // lookup_covering_roas should find the /23 ROA for the /24 prefix
1081 let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
1082 let covering = trie.lookup_covering_roas(&prefix_24);
1083 assert_eq!(covering.len(), 1, "Should find 1 covering ROA");
1084 assert_eq!(covering[0].asn, 13335);
1085
1086 // lookup_by_prefix should return empty (max_length filter)
1087 let matching = trie.lookup_by_prefix(&prefix_24);
1088 assert!(
1089 matching.is_empty(),
1090 "lookup_by_prefix should filter by max_length"
1091 );
1092
1093 // For the exact /23 prefix, both should return the ROA
1094 let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
1095 let covering_exact = trie.lookup_covering_roas(&prefix_23);
1096 let matching_exact = trie.lookup_by_prefix(&prefix_23);
1097 assert_eq!(covering_exact.len(), 1);
1098 assert_eq!(matching_exact.len(), 1);
1099
1100 // Unrelated prefix should find nothing
1101 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
1102 assert!(trie.lookup_covering_roas(&unknown_prefix).is_empty());
1103 assert!(trie.lookup_by_prefix(&unknown_prefix).is_empty());
1104 }
1105}