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 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}