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 authorize a given prefix (matching ASN and max_length).
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 /// Lookup all ROAs that cover a given prefix, regardless of max_length.
439 ///
440 /// This returns all ROAs whose prefix contains the given prefix,
441 /// without filtering by max_length. Used to determine if a prefix
442 /// is covered by RPKI data at all.
443 fn lookup_covering_roas(&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 all_matches.push(roa.clone());
449 }
450 }
451 }
452 all_matches
453 }
454
455 /// Validate a prefix with an ASN.
456 ///
457 /// Return values:
458 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid
459 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid
460 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI
461 pub fn validate(&self, prefix: &IpNet, asn: u32) -> RpkiValidation {
462 // First check if there are ANY covering ROAs (regardless of max_length)
463 let covering_roas = self.lookup_covering_roas(prefix);
464 if covering_roas.is_empty() {
465 return RpkiValidation::Unknown;
466 }
467
468 // Now check for valid matches (matching ASN and max_length)
469 let matches = self.lookup_by_prefix(prefix);
470 for roa in matches {
471 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
472 return RpkiValidation::Valid;
473 }
474 }
475 // There are covering ROAs but none authorize this prefix/ASN
476 RpkiValidation::Invalid
477 }
478
479 /// Validate a prefix with an ASN, checking expiry dates.
480 ///
481 /// Return values:
482 /// - `RpkiValidation::Valid` if the prefix-asn pair is valid and not expired
483 /// - `RpkiValidation::Invalid` if the prefix-asn pair is invalid (wrong ASN or max_length exceeded)
484 /// - `RpkiValidation::Unknown` if the prefix-asn pair is not found in RPKI or all matching ROAs are outside their valid time range
485 pub fn validate_check_expiry(
486 &self,
487 prefix: &IpNet,
488 asn: u32,
489 check_time: Option<NaiveDateTime>,
490 ) -> RpkiValidation {
491 // First check if there are ANY covering ROAs (regardless of max_length)
492 let covering_roas = self.lookup_covering_roas(prefix);
493 if covering_roas.is_empty() {
494 return RpkiValidation::Unknown;
495 }
496
497 let check_time = check_time.unwrap_or_else(|| Utc::now().naive_utc());
498
499 let mut found_matching_asn = false;
500
501 // Check for valid matches (matching ASN and max_length)
502 let matches = self.lookup_by_prefix(prefix);
503 for roa in matches {
504 if roa.asn == asn && roa.max_length >= prefix.prefix_len() {
505 found_matching_asn = true;
506
507 // Check if ROA is within valid time period
508 let is_valid_time = {
509 if let Some(not_before) = roa.not_before {
510 if check_time < not_before {
511 false // ROA not yet valid
512 } else {
513 true
514 }
515 } else {
516 true // no not_before constraint
517 }
518 } && {
519 if let Some(not_after) = roa.not_after {
520 if check_time > not_after {
521 false // ROA expired
522 } else {
523 true
524 }
525 } else {
526 true // no not_after constraint
527 }
528 };
529
530 if is_valid_time {
531 return RpkiValidation::Valid;
532 }
533 }
534 }
535
536 // If we found matching ASN but all ROAs are outside valid time range, return Unknown
537 if found_matching_asn {
538 return RpkiValidation::Unknown;
539 }
540
541 // There are covering ROAs but none authorize this prefix/ASN
542 RpkiValidation::Invalid
543 }
544
545 /// Reload the RPKI data from its original source.
546 pub fn reload(&mut self) -> Result<()> {
547 match self.date {
548 Some(date) => {
549 let trie = RpkiTrie::from_ripe_historical(date)?;
550 self.trie = trie.trie;
551 self.aspas = trie.aspas;
552 }
553 None => {
554 let trie = RpkiTrie::from_cloudflare()?;
555 self.trie = trie.trie;
556 self.aspas = trie.aspas;
557 }
558 }
559
560 Ok(())
561 }
562}
563
564impl LazyLoadable for RpkiTrie {
565 fn reload(&mut self) -> Result<()> {
566 self.reload()
567 }
568
569 fn is_loaded(&self) -> bool {
570 !self.trie.is_empty()
571 }
572
573 fn loading_status(&self) -> &'static str {
574 if self.is_loaded() {
575 "RPKI data loaded"
576 } else {
577 "RPKI data not loaded"
578 }
579 }
580}
581
582// ============================================================================
583// BgpkitCommons Integration
584// ============================================================================
585
586impl BgpkitCommons {
587 pub fn rpki_lookup_by_prefix(&self, prefix: &str) -> Result<Vec<Roa>> {
588 if self.rpki_trie.is_none() {
589 return Err(BgpkitCommonsError::module_not_loaded(
590 modules::RPKI,
591 load_methods::LOAD_RPKI,
592 ));
593 }
594
595 let prefix = prefix.parse()?;
596
597 Ok(self.rpki_trie.as_ref().unwrap().lookup_by_prefix(&prefix))
598 }
599
600 pub fn rpki_validate(&self, asn: u32, prefix: &str) -> Result<RpkiValidation> {
601 if self.rpki_trie.is_none() {
602 return Err(BgpkitCommonsError::module_not_loaded(
603 modules::RPKI,
604 load_methods::LOAD_RPKI,
605 ));
606 }
607 let prefix = prefix.parse()?;
608 Ok(self.rpki_trie.as_ref().unwrap().validate(&prefix, asn))
609 }
610
611 pub fn rpki_validate_check_expiry(
612 &self,
613 asn: u32,
614 prefix: &str,
615 check_time: Option<NaiveDateTime>,
616 ) -> Result<RpkiValidation> {
617 if self.rpki_trie.is_none() {
618 return Err(BgpkitCommonsError::module_not_loaded(
619 modules::RPKI,
620 load_methods::LOAD_RPKI,
621 ));
622 }
623 let prefix = prefix.parse()?;
624 Ok(self
625 .rpki_trie
626 .as_ref()
627 .unwrap()
628 .validate_check_expiry(&prefix, asn, check_time))
629 }
630}
631
632// ============================================================================
633// Tests
634// ============================================================================
635
636#[cfg(test)]
637mod tests {
638 use super::*;
639 use chrono::DateTime;
640
641 #[test]
642 fn test_multiple_roas_same_prefix() {
643 let mut trie = RpkiTrie::new(None);
644
645 // Insert first ROA
646 let roa1 = Roa {
647 prefix: "192.0.2.0/24".parse().unwrap(),
648 asn: 64496,
649 max_length: 24,
650 rir: Some(Rir::APNIC),
651 not_before: None,
652 not_after: None,
653 };
654 assert!(trie.insert_roa(roa1.clone()));
655
656 // Insert second ROA with different ASN for same prefix
657 let roa2 = Roa {
658 prefix: "192.0.2.0/24".parse().unwrap(),
659 asn: 64497,
660 max_length: 24,
661 rir: Some(Rir::APNIC),
662 not_before: None,
663 not_after: None,
664 };
665 assert!(!trie.insert_roa(roa2.clone()));
666
667 // Insert duplicate ROA (same prefix, asn, max_length) - should be ignored
668 let roa_dup = Roa {
669 prefix: "192.0.2.0/24".parse().unwrap(),
670 asn: 64496,
671 max_length: 24,
672 rir: Some(Rir::ARIN), // Different RIR shouldn't matter
673 not_before: None,
674 not_after: None,
675 };
676 assert!(!trie.insert_roa(roa_dup));
677
678 // Insert ROA with different max_length - should be added
679 let roa3 = Roa {
680 prefix: "192.0.2.0/24".parse().unwrap(),
681 asn: 64496,
682 max_length: 28,
683 rir: Some(Rir::APNIC),
684 not_before: None,
685 not_after: None,
686 };
687 assert!(!trie.insert_roa(roa3.clone()));
688
689 // Lookup should return 3 ROAs (roa1, roa2, roa3)
690 let prefix: IpNet = "192.0.2.0/24".parse().unwrap();
691 let roas = trie.lookup_by_prefix(&prefix);
692 assert_eq!(roas.len(), 3);
693
694 // Validate AS 64496 - should be valid
695 assert_eq!(trie.validate(&prefix, 64496), RpkiValidation::Valid);
696
697 // Validate AS 64497 - should be valid
698 assert_eq!(trie.validate(&prefix, 64497), RpkiValidation::Valid);
699
700 // Validate AS 64498 - should be invalid (prefix has ROAs but not for this ASN)
701 assert_eq!(trie.validate(&prefix, 64498), RpkiValidation::Invalid);
702
703 // Validate unknown prefix - should be unknown
704 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
705 assert_eq!(
706 trie.validate(&unknown_prefix, 64496),
707 RpkiValidation::Unknown
708 );
709 }
710
711 #[test]
712 fn test_validate_check_expiry_with_time_constraints() {
713 let mut trie = RpkiTrie::new(None);
714
715 // Time references
716 let past_time = DateTime::from_timestamp(1600000000, 0)
717 .map(|dt| dt.naive_utc())
718 .unwrap();
719 let current_time = DateTime::from_timestamp(1700000000, 0)
720 .map(|dt| dt.naive_utc())
721 .unwrap();
722 let future_time = DateTime::from_timestamp(1800000000, 0)
723 .map(|dt| dt.naive_utc())
724 .unwrap();
725
726 // Insert ROA that's currently valid (not_before in past, not_after in future)
727 let roa_valid = Roa {
728 prefix: "192.0.2.0/24".parse().unwrap(),
729 asn: 64496,
730 max_length: 24,
731 rir: Some(Rir::APNIC),
732 not_before: Some(past_time),
733 not_after: Some(future_time),
734 };
735 trie.insert_roa(roa_valid);
736
737 // Insert ROA that's expired
738 let roa_expired = Roa {
739 prefix: "198.51.100.0/24".parse().unwrap(),
740 asn: 64497,
741 max_length: 24,
742 rir: Some(Rir::APNIC),
743 not_before: Some(past_time),
744 not_after: Some(past_time), // Expired in the past
745 };
746 trie.insert_roa(roa_expired);
747
748 // Insert ROA that's not yet valid
749 let roa_future = Roa {
750 prefix: "203.0.113.0/24".parse().unwrap(),
751 asn: 64498,
752 max_length: 24,
753 rir: Some(Rir::APNIC),
754 not_before: Some(future_time), // Not valid yet
755 not_after: None,
756 };
757 trie.insert_roa(roa_future);
758
759 // Test valid ROA at current time
760 let prefix_valid: IpNet = "192.0.2.0/24".parse().unwrap();
761 assert_eq!(
762 trie.validate_check_expiry(&prefix_valid, 64496, Some(current_time)),
763 RpkiValidation::Valid
764 );
765
766 // Test expired ROA at current time - should return Unknown (was valid but expired)
767 let prefix_expired: IpNet = "198.51.100.0/24".parse().unwrap();
768 assert_eq!(
769 trie.validate_check_expiry(&prefix_expired, 64497, Some(current_time)),
770 RpkiValidation::Unknown
771 );
772
773 // Test not-yet-valid ROA at current time - should return Unknown
774 let prefix_future: IpNet = "203.0.113.0/24".parse().unwrap();
775 assert_eq!(
776 trie.validate_check_expiry(&prefix_future, 64498, Some(current_time)),
777 RpkiValidation::Unknown
778 );
779
780 // Test not-yet-valid ROA at future time - should return Valid
781 let far_future = DateTime::from_timestamp(1900000000, 0)
782 .map(|dt| dt.naive_utc())
783 .unwrap();
784 assert_eq!(
785 trie.validate_check_expiry(&prefix_future, 64498, Some(far_future)),
786 RpkiValidation::Valid
787 );
788
789 // Test wrong ASN - should return Invalid
790 assert_eq!(
791 trie.validate_check_expiry(&prefix_valid, 64499, Some(current_time)),
792 RpkiValidation::Invalid
793 );
794 }
795
796 #[test]
797 #[ignore] // Requires network access
798 fn test_load_from_ripe_historical() {
799 // Use a recent date that should have data available
800 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
801 let trie = RpkiTrie::from_ripe_historical(date).expect("Failed to load RIPE data");
802
803 let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
804 println!(
805 "Loaded {} ROAs from RIPE historical for {}",
806 total_roas, date
807 );
808 println!("Loaded {} ASPAs", trie.aspas.len());
809
810 assert!(total_roas > 0, "Should have loaded some ROAs");
811 }
812
813 #[test]
814 #[ignore] // Requires network access
815 fn test_load_from_rpkiviews() {
816 // Note: This test streams from a remote tgz file but stops early
817 // once rpki-client.json is found (typically at position 3-4 in the archive).
818 // Due to streaming optimization, this typically completes in ~8 seconds.
819 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
820 let trie = RpkiTrie::from_rpkiviews(RpkiViewsCollector::default(), date)
821 .expect("Failed to load RPKIviews data");
822
823 let total_roas: usize = trie.trie.iter().map(|(_, roas)| roas.len()).sum();
824 println!("Loaded {} ROAs from RPKIviews for {}", total_roas, date);
825 println!("Loaded {} ASPAs", trie.aspas.len());
826
827 assert!(total_roas > 0, "Should have loaded some ROAs");
828 }
829
830 #[test]
831 #[ignore] // Requires network access
832 fn test_rpkiviews_file_position() {
833 // Verify that rpki-client.json appears early in the archive
834 // This confirms our early-termination optimization works
835 use crate::rpki::rpkiviews::list_files_in_tgz;
836
837 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
838 let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
839 .expect("Failed to list files");
840
841 assert!(!files.is_empty(), "Should have found some files");
842
843 let tgz_url = &files[0].url;
844 println!("Checking file positions in: {}", tgz_url);
845
846 // List first 50 entries to see where rpki-client.json appears
847 let entries = list_files_in_tgz(tgz_url, Some(50)).expect("Failed to list tgz entries");
848
849 let json_position = entries
850 .iter()
851 .position(|e| e.path.ends_with("rpki-client.json"));
852
853 println!("First {} entries:", entries.len());
854 for (i, entry) in entries.iter().enumerate() {
855 println!(" [{}] {} ({} bytes)", i, entry.path, entry.size);
856 }
857
858 if let Some(pos) = json_position {
859 println!(
860 "\nrpki-client.json found at position {} (early in archive)",
861 pos
862 );
863 assert!(
864 pos < 50,
865 "rpki-client.json should appear early in the archive"
866 );
867 } else {
868 println!("\nrpki-client.json not in first 50 entries - may need to stream more");
869 }
870 }
871
872 #[test]
873 #[ignore] // Requires network access
874 fn test_list_rpkiviews_files() {
875 let date = NaiveDate::from_ymd_opt(2024, 6, 1).unwrap();
876 let files = list_rpkiviews_files(RpkiViewsCollector::default(), date)
877 .expect("Failed to list files");
878
879 println!("Found {} files for {} from Kerfuffle", files.len(), date);
880 for file in files.iter().take(3) {
881 println!(
882 " {} ({} bytes, {})",
883 file.url,
884 file.size.unwrap_or(0),
885 file.timestamp
886 );
887 }
888
889 assert!(!files.is_empty(), "Should have found some files");
890 }
891
892 #[test]
893 fn test_validate_max_length_exceeded() {
894 // Test the bug where a prefix covered by an ROA but with max_length exceeded
895 // should return Invalid, not Unknown
896 let mut trie = RpkiTrie::new(None);
897
898 // Insert ROA for /23 with max_length 23 (no more specific allowed)
899 let roa = Roa {
900 prefix: "103.21.244.0/23".parse().unwrap(),
901 asn: 13335, // Cloudflare
902 max_length: 23,
903 rir: Some(Rir::APNIC),
904 not_before: None,
905 not_after: None,
906 };
907 trie.insert_roa(roa);
908
909 // /24 is covered by /23 but max_length is 23, so this should be Invalid
910 let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
911
912 // Test with correct ASN - should be Invalid (covered by RPKI but not authorized due to max_length)
913 assert_eq!(
914 trie.validate(&prefix_24, 13335),
915 RpkiValidation::Invalid,
916 "Prefix covered by ROA but max_length exceeded should be Invalid"
917 );
918
919 // Test with wrong ASN - should also be Invalid
920 assert_eq!(
921 trie.validate(&prefix_24, 64496),
922 RpkiValidation::Invalid,
923 "Prefix covered by ROA with wrong ASN should be Invalid"
924 );
925
926 // The /23 itself with correct ASN should be Valid
927 let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
928 assert_eq!(
929 trie.validate(&prefix_23, 13335),
930 RpkiValidation::Valid,
931 "Exact prefix match with correct ASN should be Valid"
932 );
933
934 // Completely unrelated prefix should be Unknown
935 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
936 assert_eq!(
937 trie.validate(&unknown_prefix, 13335),
938 RpkiValidation::Unknown,
939 "Prefix not covered by any ROA should be Unknown"
940 );
941 }
942
943 #[test]
944 fn test_validate_check_expiry_max_length_exceeded() {
945 // Same test but for validate_check_expiry
946 let mut trie = RpkiTrie::new(None);
947
948 let current_time = DateTime::from_timestamp(1700000000, 0)
949 .map(|dt| dt.naive_utc())
950 .unwrap();
951 let future_time = DateTime::from_timestamp(1800000000, 0)
952 .map(|dt| dt.naive_utc())
953 .unwrap();
954
955 // Insert ROA for /23 with max_length 23
956 let roa = Roa {
957 prefix: "103.21.244.0/23".parse().unwrap(),
958 asn: 13335,
959 max_length: 23,
960 rir: Some(Rir::APNIC),
961 not_before: Some(current_time),
962 not_after: Some(future_time),
963 };
964 trie.insert_roa(roa);
965
966 // /24 is covered by /23 but max_length is 23, so this should be Invalid
967 let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
968
969 // Test with correct ASN - should be Invalid
970 assert_eq!(
971 trie.validate_check_expiry(&prefix_24, 13335, Some(current_time)),
972 RpkiValidation::Invalid,
973 "Prefix covered by ROA but max_length exceeded should be Invalid"
974 );
975
976 // Test with wrong ASN - should also be Invalid
977 assert_eq!(
978 trie.validate_check_expiry(&prefix_24, 64496, Some(current_time)),
979 RpkiValidation::Invalid,
980 "Prefix covered by ROA with wrong ASN should be Invalid"
981 );
982
983 // The /23 itself with correct ASN should be Valid
984 let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
985 assert_eq!(
986 trie.validate_check_expiry(&prefix_23, 13335, Some(current_time)),
987 RpkiValidation::Valid,
988 "Exact prefix match with correct ASN should be Valid"
989 );
990
991 // Completely unrelated prefix should be Unknown
992 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
993 assert_eq!(
994 trie.validate_check_expiry(&unknown_prefix, 13335, Some(current_time)),
995 RpkiValidation::Unknown,
996 "Prefix not covered by any ROA should be Unknown"
997 );
998 }
999
1000 #[test]
1001 fn test_lookup_covering_roas() {
1002 // Test the helper method that finds all covering ROAs
1003 let mut trie = RpkiTrie::new(None);
1004
1005 // Insert ROA for /23 with max_length 23
1006 let roa = Roa {
1007 prefix: "103.21.244.0/23".parse().unwrap(),
1008 asn: 13335,
1009 max_length: 23,
1010 rir: Some(Rir::APNIC),
1011 not_before: None,
1012 not_after: None,
1013 };
1014 trie.insert_roa(roa);
1015
1016 // Insert another ROA for a different prefix
1017 let roa2 = Roa {
1018 prefix: "192.0.2.0/24".parse().unwrap(),
1019 asn: 64496,
1020 max_length: 24,
1021 rir: Some(Rir::ARIN),
1022 not_before: None,
1023 not_after: None,
1024 };
1025 trie.insert_roa(roa2);
1026
1027 // lookup_covering_roas should find the /23 ROA for the /24 prefix
1028 let prefix_24: IpNet = "103.21.244.0/24".parse().unwrap();
1029 let covering = trie.lookup_covering_roas(&prefix_24);
1030 assert_eq!(covering.len(), 1, "Should find 1 covering ROA");
1031 assert_eq!(covering[0].asn, 13335);
1032
1033 // lookup_by_prefix should return empty (max_length filter)
1034 let matching = trie.lookup_by_prefix(&prefix_24);
1035 assert!(
1036 matching.is_empty(),
1037 "lookup_by_prefix should filter by max_length"
1038 );
1039
1040 // For the exact /23 prefix, both should return the ROA
1041 let prefix_23: IpNet = "103.21.244.0/23".parse().unwrap();
1042 let covering_exact = trie.lookup_covering_roas(&prefix_23);
1043 let matching_exact = trie.lookup_by_prefix(&prefix_23);
1044 assert_eq!(covering_exact.len(), 1);
1045 assert_eq!(matching_exact.len(), 1);
1046
1047 // Unrelated prefix should find nothing
1048 let unknown_prefix: IpNet = "10.0.0.0/8".parse().unwrap();
1049 assert!(trie.lookup_covering_roas(&unknown_prefix).is_empty());
1050 assert!(trie.lookup_by_prefix(&unknown_prefix).is_empty());
1051 }
1052}