Skip to main content

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}