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 rpkiviews;
140
141use chrono::{DateTime, NaiveDate, NaiveDateTime, Utc};
142use ipnet::IpNet;
143use ipnet_trie::IpnetTrie;
144
145use crate::errors::{load_methods, modules};
146use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
147pub use ripe_historical::list_ripe_files;
148use rpki_client::RpkiClientData;
149pub use rpkiviews::{RpkiViewsCollector, list_rpkiviews_files};
150use serde::{Deserialize, Serialize};
151use std::fmt::Display;
152use std::str::FromStr;
153
154// ============================================================================
155// Public Data Structures
156// ============================================================================
157
158/// A validated Route Origin Authorization (ROA).
159///
160/// ROAs authorize specific Autonomous Systems to originate specific IP prefixes.
161#[derive(Clone, Debug, Serialize, Deserialize)]
162pub struct Roa {
163 /// The IP prefix (e.g., 192.0.2.0/24 or 2001:db8::/32)
164 pub prefix: IpNet,
165 /// The AS number authorized to originate this prefix
166 pub asn: u32,
167 /// Maximum prefix length allowed for announcements
168 pub max_length: u8,
169 /// Regional Internet Registry that issued this ROA
170 pub rir: Option<Rir>,
171 /// ROA validity start time (if available)
172 pub not_before: Option<NaiveDateTime>,
173 /// ROA validity end time (from expires field)
174 pub not_after: Option<NaiveDateTime>,
175}
176
177/// A validated AS Provider Authorization (ASPA).
178///
179/// ASPAs specify which ASes are authorized providers for a customer AS.
180#[derive(Clone, Debug, Serialize, Deserialize)]
181pub struct Aspa {
182 /// The customer AS number
183 pub customer_asn: u32,
184 /// List of provider AS numbers
185 pub providers: Vec<u32>,
186 /// When this ASPA expires
187 pub expires: Option<NaiveDateTime>,
188}
189
190/// Information about an available RPKI data file.
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct RpkiFile {
193 /// Full URL to download the file
194 pub url: String,
195 /// Timestamp when the file was created
196 pub timestamp: DateTime<Utc>,
197 /// Size of the file in bytes (if available)
198 pub size: Option<u64>,
199 /// RIR that this file is for (for RIPE files)
200 pub rir: Option<Rir>,
201 /// Collector that provides this file (for RPKIviews files)
202 pub collector: Option<RpkiViewsCollector>,
203}
204
205/// Historical RPKI data source.
206///
207/// Used to specify which data source to use when loading historical RPKI data.
208#[derive(Debug, Clone, Default)]
209pub enum HistoricalRpkiSource {
210 /// RIPE NCC historical archives (data from all 5 RIRs)
211 #[default]
212 Ripe,
213 /// RPKIviews collector
214 RpkiViews(RpkiViewsCollector),
215}
216
217impl std::fmt::Display for HistoricalRpkiSource {
218 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
219 match self {
220 HistoricalRpkiSource::Ripe => write!(f, "RIPE NCC"),
221 HistoricalRpkiSource::RpkiViews(collector) => write!(f, "RPKIviews ({})", collector),
222 }
223 }
224}
225
226/// Regional Internet Registry (RIR).
227#[derive(Clone, Debug, Copy, PartialEq, Eq, Serialize, Deserialize)]
228pub enum Rir {
229 AFRINIC,
230 APNIC,
231 ARIN,
232 LACNIC,
233 RIPENCC,
234}
235
236impl FromStr for Rir {
237 type Err = String;
238
239 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
240 match s.to_lowercase().as_str() {
241 "afrinic" => Ok(Rir::AFRINIC),
242 "apnic" => Ok(Rir::APNIC),
243 "arin" => Ok(Rir::ARIN),
244 "lacnic" => Ok(Rir::LACNIC),
245 "ripe" => Ok(Rir::RIPENCC),
246 _ => Err(format!("unknown RIR: {}", s)),
247 }
248 }
249}
250
251impl Display for Rir {
252 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
253 match self {
254 Rir::AFRINIC => write!(f, "AFRINIC"),
255 Rir::APNIC => write!(f, "APNIC"),
256 Rir::ARIN => write!(f, "ARIN"),
257 Rir::LACNIC => write!(f, "LACNIC"),
258 Rir::RIPENCC => write!(f, "RIPENCC"),
259 }
260 }
261}
262
263impl Rir {
264 pub fn to_ripe_ftp_root_url(&self) -> String {
265 match self {
266 Rir::AFRINIC => "https://ftp.ripe.net/rpki/afrinic.tal".to_string(),
267 Rir::APNIC => "https://ftp.ripe.net/rpki/apnic.tal".to_string(),
268 Rir::ARIN => "https://ftp.ripe.net/rpki/arin.tal".to_string(),
269 Rir::LACNIC => "https://ftp.ripe.net/rpki/lacnic.tal".to_string(),
270 Rir::RIPENCC => "https://ftp.ripe.net/rpki/ripencc.tal".to_string(),
271 }
272 }
273}
274
275/// RPKI validation result.
276#[derive(Clone, Debug, PartialEq, Eq)]
277pub enum RpkiValidation {
278 /// The prefix-ASN pair is explicitly authorized by a valid ROA
279 Valid,
280 /// The prefix has ROAs but none authorize the given ASN
281 Invalid,
282 /// No ROAs exist for the prefix, or all ROAs are outside their validity period
283 Unknown,
284}
285
286impl Display for RpkiValidation {
287 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
288 match self {
289 RpkiValidation::Valid => write!(f, "valid"),
290 RpkiValidation::Invalid => write!(f, "invalid"),
291 RpkiValidation::Unknown => write!(f, "unknown"),
292 }
293 }
294}
295
296// ============================================================================
297// Backwards Compatibility Type Aliases
298// ============================================================================
299
300/// Type alias for backwards compatibility. Use [`Roa`] instead.
301/// Deprecated since 0.10.0. This alias will be removed in version 0.12.0.
302#[deprecated(since = "0.10.0", note = "Use Roa instead")]
303pub type RoaEntry = Roa;
304
305// ============================================================================
306// RpkiTrie Implementation
307// ============================================================================
308
309/// The main RPKI data structure storing ROAs and ASPAs.
310#[derive(Clone)]
311pub struct RpkiTrie {
312 /// Trie mapping IP prefixes to ROA entries
313 pub trie: IpnetTrie<Vec<Roa>>,
314 /// AS Provider Authorizations
315 pub aspas: Vec<Aspa>,
316 /// Date for historical data (None for real-time)
317 date: Option<NaiveDate>,
318}
319
320impl Default for RpkiTrie {
321 fn default() -> Self {
322 Self {
323 trie: IpnetTrie::new(),
324 aspas: vec![],
325 date: None,
326 }
327 }
328}
329
330impl RpkiTrie {
331 /// Create a new empty RpkiTrie.
332 pub fn new(date: Option<NaiveDate>) -> Self {
333 Self {
334 trie: IpnetTrie::new(),
335 aspas: vec![],
336 date,
337 }
338 }
339
340 /// Insert a ROA. Returns true if this is a new prefix, false if added to existing prefix.
341 /// Duplicates are avoided - ROAs with same (prefix, asn, max_length) are considered identical.
342 pub fn insert_roa(&mut self, roa: Roa) -> bool {
343 match self.trie.exact_match_mut(roa.prefix) {
344 Some(existing_roas) => {
345 // Check if this ROA already exists (same prefix, asn, max_length)
346 if !existing_roas.iter().any(|existing| {
347 existing.asn == roa.asn && existing.max_length == roa.max_length
348 }) {
349 existing_roas.push(roa);
350 }
351 false
352 }
353 None => {
354 self.trie.insert(roa.prefix, vec![roa]);
355 true
356 }
357 }
358 }
359
360 /// Insert multiple ROAs.
361 pub fn insert_roas(&mut self, roas: Vec<Roa>) {
362 for roa in roas {
363 self.insert_roa(roa);
364 }
365 }
366
367 /// Convert rpki-client data into an RpkiTrie.
368 ///
369 /// This is a shared conversion function used by all data sources
370 /// (Cloudflare, RIPE, RPKIviews) since they all produce the same
371 /// rpki-client JSON format.
372 pub(crate) fn from_rpki_client_data(
373 data: RpkiClientData,
374 date: Option<NaiveDate>,
375 ) -> Result<Self> {
376 let mut trie = RpkiTrie::new(date);
377 trie.merge_rpki_client_data(data);
378 Ok(trie)
379 }
380
381 /// Merge rpki-client data into this trie.
382 ///
383 /// This converts ROAs and ASPAs from rpki-client format and inserts them,
384 /// avoiding duplicates for ASPAs based on customer_asn.
385 pub(crate) fn merge_rpki_client_data(&mut self, data: RpkiClientData) {
386 // Convert and insert ROAs
387 for roa in data.roas {
388 let prefix = match roa.prefix.parse::<IpNet>() {
389 Ok(p) => p,
390 Err(_) => continue,
391 };
392 let rir = Rir::from_str(&roa.ta).ok();
393 let not_after =
394 DateTime::from_timestamp(roa.expires as i64, 0).map(|dt| dt.naive_utc());
395
396 self.insert_roa(Roa {
397 prefix,
398 asn: roa.asn,
399 max_length: roa.max_length,
400 rir,
401 not_before: None,
402 not_after,
403 });
404 }
405
406 // Convert and merge ASPAs (avoiding duplicates based on customer_asn)
407 for aspa in data.aspas {
408 if !self
409 .aspas
410 .iter()
411 .any(|a| a.customer_asn == aspa.customer_asid)
412 {
413 let expires = DateTime::from_timestamp(aspa.expires, 0).map(|dt| dt.naive_utc());
414 self.aspas.push(Aspa {
415 customer_asn: aspa.customer_asid,
416 providers: aspa.providers,
417 expires,
418 });
419 }
420 }
421 }
422
423 /// Lookup all ROAs that match a given prefix, including invalid ones.
424 pub fn lookup_by_prefix(&self, prefix: &IpNet) -> Vec<Roa> {
425 let mut all_matches = vec![];
426 for (p, roas) in self.trie.matches(prefix) {
427 if p.contains(prefix) {
428 for roa in roas {
429 if roa.max_length >= prefix.prefix_len() {
430 all_matches.push(roa.clone());
431 }
432 }
433 }
434 }
435 all_matches
436 }
437
438 /// Validate a prefix with an ASN.
439 ///
440 /// Return values:
441 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid
442 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid
443 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI
444 pub fn validate(&self, prefix: &IpNet, asn: u32) -> RpkiValidation {
445 let matches = self.lookup_by_prefix(prefix);
446 if matches.is_empty() {
447 return RpkiValidation::Unknown;
448 }
449
450 for roa in matches {
451 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
452 return RpkiValidation::Valid;
453 }
454 }
455 // there are matches but none of them is valid
456 RpkiValidation::Invalid
457 }
458
459 /// Validate a prefix with an ASN, checking expiry dates.
460 ///
461 /// Return values:
462 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid and not expired
463 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid (wrong ASN)
464 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI or all matching ROAs are outside their valid time range
465 pub fn validate_check_expiry(
466 &self,
467 prefix: &IpNet,
468 asn: u32,
469 check_time: Option<NaiveDateTime>,
470 ) -> RpkiValidation {
471 let matches = self.lookup_by_prefix(prefix);
472 if matches.is_empty() {
473 return RpkiValidation::Unknown;
474 }
475
476 let check_time = check_time.unwrap_or_else(|| Utc::now().naive_utc());
477
478 let mut found_matching_asn = false;
479
480 for roa in matches {
481 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
482 found_matching_asn = true;
483
484 // Check if ROA is within valid time period
485 let is_valid_time = {
486 if let Some(not_before) = roa.not_before {
487 if check_time < not_before {
488 false // ROA not yet valid
489 } else {
490 true
491 }
492 } else {
493 true // no not_before constraint
494 }
495 } && {
496 if let Some(not_after) = roa.not_after {
497 if check_time > not_after {
498 false // ROA expired
499 } else {
500 true
501 }
502 } else {
503 true // no not_after constraint
504 }
505 };
506
507 if is_valid_time {
508 return RpkiValidation::Valid;
509 }
510 }
511 }
512
513 // If we found matching ASN but all ROAs are outside valid time range, return Unknown
514 if found_matching_asn {
515 return RpkiValidation::Unknown;
516 }
517
518 // There are matches but none of them match the ASN
519 RpkiValidation::Invalid
520 }
521
522 /// Reload the RPKI data from its original source.
523 pub fn reload(&mut self) -> Result<()> {
524 match self.date {
525 Some(date) => {
526 let trie = RpkiTrie::from_ripe_historical(date)?;
527 self.trie = trie.trie;
528 self.aspas = trie.aspas;
529 }
530 None => {
531 let trie = RpkiTrie::from_cloudflare()?;
532 self.trie = trie.trie;
533 self.aspas = trie.aspas;
534 }
535 }
536
537 Ok(())
538 }
539}
540
541impl LazyLoadable for RpkiTrie {
542 fn reload(&mut self) -> Result<()> {
543 self.reload()
544 }
545
546 fn is_loaded(&self) -> bool {
547 !self.trie.is_empty()
548 }
549
550 fn loading_status(&self) -> &'static str {
551 if self.is_loaded() {
552 "RPKI data loaded"
553 } else {
554 "RPKI data not loaded"
555 }
556 }
557}
558
559// ============================================================================
560// BgpkitCommons Integration
561// ============================================================================
562
563impl BgpkitCommons {
564 pub fn rpki_lookup_by_prefix(&self, prefix: &str) -> Result<Vec<Roa>> {
565 if self.rpki_trie.is_none() {
566 return Err(BgpkitCommonsError::module_not_loaded(
567 modules::RPKI,
568 load_methods::LOAD_RPKI,
569 ));
570 }
571
572 let prefix = prefix.parse()?;
573
574 Ok(self.rpki_trie.as_ref().unwrap().lookup_by_prefix(&prefix))
575 }
576
577 pub fn rpki_validate(&self, asn: u32, prefix: &str) -> Result<RpkiValidation> {
578 if self.rpki_trie.is_none() {
579 return Err(BgpkitCommonsError::module_not_loaded(
580 modules::RPKI,
581 load_methods::LOAD_RPKI,
582 ));
583 }
584 let prefix = prefix.parse()?;
585 Ok(self.rpki_trie.as_ref().unwrap().validate(&prefix, asn))
586 }
587
588 pub fn rpki_validate_check_expiry(
589 &self,
590 asn: u32,
591 prefix: &str,
592 check_time: Option<NaiveDateTime>,
593 ) -> Result<RpkiValidation> {
594 if self.rpki_trie.is_none() {
595 return Err(BgpkitCommonsError::module_not_loaded(
596 modules::RPKI,
597 load_methods::LOAD_RPKI,
598 ));
599 }
600 let prefix = prefix.parse()?;
601 Ok(self
602 .rpki_trie
603 .as_ref()
604 .unwrap()
605 .validate_check_expiry(&prefix, asn, check_time))
606 }
607}
608
609// ============================================================================
610// Tests
611// ============================================================================
612
613#[cfg(test)]
614mod tests {
615 use super::*;
616 use chrono::DateTime;
617
618 #[test]
619 fn test_multiple_roas_same_prefix() {
620 let mut trie = RpkiTrie::new(None);
621
622 // Insert first ROA
623 let roa1 = Roa {
624 prefix: "192.0.2.0/24".parse().unwrap(),
625 asn: 64496,
626 max_length: 24,
627 rir: Some(Rir::APNIC),
628 not_before: None,
629 not_after: None,
630 };
631 assert!(trie.insert_roa(roa1.clone()));
632
633 // Insert second ROA with different ASN for same prefix
634 let roa2 = Roa {
635 prefix: "192.0.2.0/24".parse().unwrap(),
636 asn: 64497,
637 max_length: 24,
638 rir: Some(Rir::APNIC),
639 not_before: None,
640 not_after: None,
641 };
642 assert!(!trie.insert_roa(roa2.clone()));
643
644 // Insert duplicate ROA (same prefix, asn, max_length) - should be ignored
645 let roa_dup = Roa {
646 prefix: "192.0.2.0/24".parse().unwrap(),
647 asn: 64496,
648 max_length: 24,
649 rir: Some(Rir::ARIN), // Different RIR shouldn't matter
650 not_before: None,
651 not_after: None,
652 };
653 assert!(!trie.insert_roa(roa_dup));
654
655 // Insert ROA with different max_length - should be added
656 let roa3 = Roa {
657 prefix: "192.0.2.0/24".parse().unwrap(),
658 asn: 64496,
659 max_length: 28,
660 rir: Some(Rir::APNIC),
661 not_before: None,
662 not_after: None,
663 };
664 assert!(!trie.insert_roa(roa3.clone()));
665
666 // Lookup should return 3 ROAs (roa1, roa2, roa3)
667 let prefix: IpNet = "192.0.2.0/24".parse().unwrap();
668 let roas = trie.lookup_by_prefix(&prefix);
669 assert_eq!(roas.len(), 3);
670
671 // Validate AS 64496 - should be valid
672 assert_eq!(trie.validate(&prefix, 64496), RpkiValidation::Valid);
673
674 // Validate AS 64497 - should be valid
675 assert_eq!(trie.validate(&prefix, 64497), RpkiValidation::Valid);
676
677 // Validate AS 64498 - should be invalid (prefix has ROAs but not for this ASN)
678 assert_eq!(trie.validate(&prefix, 64498), RpkiValidation::Invalid);
679
680 // Validate unknown prefix - should be unknown
681 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
682 assert_eq!(
683 trie.validate(&unknown_prefix, 64496),
684 RpkiValidation::Unknown
685 );
686 }
687
688 #[test]
689 fn test_validate_check_expiry_with_time_constraints() {
690 let mut trie = RpkiTrie::new(None);
691
692 // Time references
693 let past_time = DateTime::from_timestamp(1600000000, 0)
694 .map(|dt| dt.naive_utc())
695 .unwrap();
696 let current_time = DateTime::from_timestamp(1700000000, 0)
697 .map(|dt| dt.naive_utc())
698 .unwrap();
699 let future_time = DateTime::from_timestamp(1800000000, 0)
700 .map(|dt| dt.naive_utc())
701 .unwrap();
702
703 // Insert ROA that's currently valid (not_before in past, not_after in future)
704 let roa_valid = Roa {
705 prefix: "192.0.2.0/24".parse().unwrap(),
706 asn: 64496,
707 max_length: 24,
708 rir: Some(Rir::APNIC),
709 not_before: Some(past_time),
710 not_after: Some(future_time),
711 };
712 trie.insert_roa(roa_valid);
713
714 // Insert ROA that's expired
715 let roa_expired = Roa {
716 prefix: "198.51.100.0/24".parse().unwrap(),
717 asn: 64497,
718 max_length: 24,
719 rir: Some(Rir::APNIC),
720 not_before: Some(past_time),
721 not_after: Some(past_time), // Expired in the past
722 };
723 trie.insert_roa(roa_expired);
724
725 // Insert ROA that's not yet valid
726 let roa_future = Roa {
727 prefix: "203.0.113.0/24".parse().unwrap(),
728 asn: 64498,
729 max_length: 24,
730 rir: Some(Rir::APNIC),
731 not_before: Some(future_time), // Not valid yet
732 not_after: None,
733 };
734 trie.insert_roa(roa_future);
735
736 // Test valid ROA at current time
737 let prefix_valid: IpNet = "192.0.2.0/24".parse().unwrap();
738 assert_eq!(
739 trie.validate_check_expiry(&prefix_valid, 64496, Some(current_time)),
740 RpkiValidation::Valid
741 );
742
743 // Test expired ROA at current time - should return Unknown (was valid but expired)
744 let prefix_expired: IpNet = "198.51.100.0/24".parse().unwrap();
745 assert_eq!(
746 trie.validate_check_expiry(&prefix_expired, 64497, Some(current_time)),
747 RpkiValidation::Unknown
748 );
749
750 // Test not-yet-valid ROA at current time - should return Unknown
751 let prefix_future: IpNet = "203.0.113.0/24".parse().unwrap();
752 assert_eq!(
753 trie.validate_check_expiry(&prefix_future, 64498, Some(current_time)),
754 RpkiValidation::Unknown
755 );
756
757 // Test not-yet-valid ROA at future time - should return Valid
758 let far_future = DateTime::from_timestamp(1900000000, 0)
759 .map(|dt| dt.naive_utc())
760 .unwrap();
761 assert_eq!(
762 trie.validate_check_expiry(&prefix_future, 64498, Some(far_future)),
763 RpkiValidation::Valid
764 );
765
766 // Test wrong ASN - should return Invalid
767 assert_eq!(
768 trie.validate_check_expiry(&prefix_valid, 64499, Some(current_time)),
769 RpkiValidation::Invalid
770 );
771 }
772
773 #[test]
774 #[ignore] // Requires network access
775 fn test_load_from_ripe_historical() {
776 // Use a recent date that should have data available
777 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
778 let trie = RpkiTrie::from_ripe_historical(date).expect("Failed to load RIPE data");
779
780 let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
781 println!(
782 "Loaded {} ROAs from RIPE historical for {}",
783 total_roas, date
784 );
785 println!("Loaded {} ASPAs", trie.aspas.len());
786
787 assert!(total_roas > 0, "Should have loaded some ROAs");
788 }
789
790 #[test]
791 #[ignore] // Requires network access
792 fn test_load_from_rpkiviews() {
793 // Note: This test streams from a remote tgz file but stops early
794 // once rpki-client.json is found (typically at position 3-4 in the archive).
795 // Due to streaming optimization, this typically completes in ~8 seconds.
796 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
797 let trie = RpkiTrie::from_rpkiviews(RpkiViewsCollector::default(), date)
798 .expect("Failed to load RPKIviews data");
799
800 let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
801 println!("Loaded {} ROAs from RPKIviews for {}", total_roas, date);
802 println!("Loaded {} ASPAs", trie.aspas.len());
803
804 assert!(total_roas > 0, "Should have loaded some ROAs");
805 }
806
807 #[test]
808 #[ignore] // Requires network access
809 fn test_rpkiviews_file_position() {
810 // Verify that rpki-client.json appears early in the archive
811 // This confirms our early-termination optimization works
812 use crate::rpki::rpkiviews::list_files_in_tgz;
813
814 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
815 let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
816 .expect("Failed to list files");
817
818 assert!(!files.is_empty(), "Should have found some files");
819
820 let tgz_url = &files[0].url;
821 println!("Checking file positions in: {}", tgz_url);
822
823 // List first 50 entries to see where rpki-client.json appears
824 let entries = list_files_in_tgz(tgz_url, Some(50)).expect("Failed to list tgz entries");
825
826 let json_position = entries
827 .iter()
828 .position(|e| e.path.ends_with("rpki-client.json"));
829
830 println!("First {} entries:", entries.len());
831 for (i, entry) in entries.iter().enumerate() {
832 println!(" [{}] {} ({} bytes)", i, entry.path, entry.size);
833 }
834
835 if let Some(pos) = json_position {
836 println!(
837 "\nrpki-client.json found at position {} (early in archive)",
838 pos
839 );
840 assert!(
841 pos < 50,
842 "rpki-client.json should appear early in the archive"
843 );
844 } else {
845 println!("\nrpki-client.json not in first 50 entries - may need to stream more");
846 }
847 }
848
849 #[test]
850 #[ignore] // Requires network access
851 fn test_list_rpkiviews_files() {
852 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
853 let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
854 .expect("Failed to list files");
855
856 println!("Found {} files for {} from Kerfuffle", files.len(), date);
857 for file in files.iter().take(3) {
858 println!(
859 " {} ({} bytes, {})",
860 file.url,
861 file.size.unwrap_or(0),
862 file.timestamp
863 );
864 }
865
866 assert!(!files.is_empty(), "Should have found some files");
867 }
868}