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