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