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