Skip to main content

bgpkit_commons/
lib.rs

1//! # Overview
2//!
3//! `bgpkit-commons` is a library for common BGP-related data and functions with a lazy-loading
4//! architecture. Each module can be independently enabled via feature flags, allowing for minimal builds.
5//!
6//! ## Quick Start
7//!
8//! Add `bgpkit-commons` to your `Cargo.toml`:
9//!
10//! ```toml
11//! [dependencies]
12//! bgpkit-commons = "0.10"
13//! ```
14//!
15//! All modules follow the same pattern: create a [`BgpkitCommons`] instance, call a `load_xxx()`
16//! method to fetch data, then use `xxx_yyy()` methods to access it.
17//!
18//! ```rust
19//! # #[cfg(feature = "bogons")]
20//! # fn main() {
21//! use bgpkit_commons::BgpkitCommons;
22//!
23//! let mut commons = BgpkitCommons::new();
24//! commons.load_bogons().unwrap();
25//!
26//! if let Ok(is_bogon) = commons.bogons_match("23456") {
27//!     println!("ASN 23456 is a bogon: {}", is_bogon);
28//! }
29//! # }
30//! # #[cfg(not(feature = "bogons"))]
31//! # fn main() {}
32//! ```
33//!
34//! ## Modules
35//!
36//! ### [`asinfo`] — Autonomous System Information
37//!
38//! Feature: `asinfo` | Sources: RIPE NCC, CAIDA as2org, APNIC population, IIJ IHR hegemony, PeeringDB
39//!
40//! - Load: `load_asinfo(as2org, population, hegemony, peeringdb)`, `load_asinfo_cached()`, `load_asinfo_with(builder)`
41//! - Access: `asinfo_get(asn)`, `asinfo_all()`, `asinfo_are_siblings(asn1, asn2)`
42//! - AS name resolution, country mapping, organization data, population statistics, hegemony scores
43//!
44//! ### [`as2rel`] — AS Relationship Data
45//!
46//! Feature: `as2rel` | Source: BGPKIT AS relationship inference
47//!
48//! - Load: `load_as2rel()`
49//! - Access: `as2rel_lookup(asn1, asn2)`
50//! - Provider-customer, peer-to-peer, and sibling relationships between ASes
51//!
52//! ### [`bogons`] — Bogon Detection
53//!
54//! Feature: `bogons` | Source: IANA special registries (IPv4, IPv6, ASN)
55//!
56//! - Load: `load_bogons()`
57//! - Access: `bogons_match(input)`, `bogons_match_prefix(prefix)`, `bogons_match_asn(asn)`, `get_bogon_prefixes()`, `get_bogon_asns()`
58//! - Detect invalid/reserved IP prefixes and ASNs that shouldn't appear in routing
59//!
60//! ### [`countries`] — Country Information
61//!
62//! Feature: `countries` | Source: GeoNames geographical database
63//!
64//! - Load: `load_countries()`
65//! - Access: `country_by_code(code)`, `country_by_code3(code)`, `country_by_name(name)`, `country_all()`
66//! - ISO country code to name mapping and geographical information
67//!
68//! ### [`mrt_collectors`] — MRT Collector Metadata
69//!
70//! Feature: `mrt_collectors` | Sources: RouteViews and RIPE RIS official APIs
71//!
72//! - Load: `load_mrt_collectors()`, `load_mrt_collector_peers()`
73//! - Access: `mrt_collectors_all()`, `mrt_collectors_by_name(name)`, `mrt_collectors_by_country(country)`, `mrt_collector_peers_all()`, `mrt_collector_peers_full_feed()`
74//! - BGP collector information, peer details, full-feed vs partial-feed classification
75//!
76//! ### [`rpki`] — RPKI Validation
77//!
78//! Feature: `rpki` | Sources: Cloudflare (real-time), RIPE NCC historical, RPKIviews historical, RPKISPOOL historical
79//!
80//! - Load: `load_rpki(optional_date)`, `load_rpki_historical(date, source)`, `load_rpki_from_files(urls, source, date)`
81//! - Poll: `RpkiTrie::from_cloudflare_conditional(etag, last_modified)` returns `Ok(None)` on `304 Not Modified`
82//! - Access: `rpki_validate(asn, prefix)`, `rpki_validate_check_expiry(asn, prefix, timestamp)`, `rpki_lookup_by_prefix(prefix)`, `rpki_lookup_aspa(customer_asn)`
83//! - Route Origin Authorization (ROA) and ASPA validation, supports real-time and historical sources
84//! - Poll current Cloudflare data with `RpkiTrie::from_cloudflare_conditional`, retaining the returned
85//!   [`rpki::RpkiLoad`] validators and keeping the existing trie when the result is `Ok(None)`.
86//! - `BgpkitCommons::reload()` performs a full reload; it does not use validators or provide an atomic
87//!   poll-and-swap operation.
88//!
89//! ## Examples
90//!
91//! ### Loading multiple modules
92//!
93//! ```rust
94//! # #[cfg(all(feature = "asinfo", feature = "countries"))]
95//! # fn main() {
96//! use bgpkit_commons::BgpkitCommons;
97//!
98//! let mut commons = BgpkitCommons::new();
99//! commons.load_asinfo(false, false, false, false).unwrap();
100//! commons.load_countries().unwrap();
101//!
102//! if let Ok(Some(asinfo)) = commons.asinfo_get(13335) {
103//!     println!("AS13335: {} ({})", asinfo.name, asinfo.country);
104//! }
105//! # }
106//! # #[cfg(not(all(feature = "asinfo", feature = "countries")))]
107//! # fn main() {}
108//! ```
109//!
110//! ### Using AsInfoBuilder
111//!
112//! ```rust
113//! # #[cfg(feature = "asinfo")]
114//! # fn main() {
115//! use bgpkit_commons::BgpkitCommons;
116//!
117//! let mut commons = BgpkitCommons::new();
118//! let builder = commons.asinfo_builder()
119//!     .with_as2org()
120//!     .with_peeringdb();
121//! commons.load_asinfo_with(builder).unwrap();
122//!
123//! if let Ok(are_siblings) = commons.asinfo_are_siblings(13335, 132892) {
124//!     println!("AS13335 and AS132892 are siblings: {}", are_siblings);
125//! }
126//! # }
127//! # #[cfg(not(feature = "asinfo"))]
128//! # fn main() {}
129//! ```
130//!
131//! ### Loading historical RPKI data
132//!
133//! ```rust,no_run
134//! # #[cfg(feature = "rpki")]
135//! # fn main() {
136//! use bgpkit_commons::BgpkitCommons;
137//! use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
138//! use chrono::NaiveDate;
139//!
140//! let mut commons = BgpkitCommons::new();
141//! let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
142//!
143//! // Load from RIPE NCC historical archives
144//! commons.load_rpki_historical(date, HistoricalRpkiSource::Ripe).unwrap();
145//!
146//! // Or load from RPKIviews collectors
147//! let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::KerfuffleNet);
148//! commons.load_rpki_historical(date, source).unwrap();
149//!
150//! // List available files for a date
151//! let files = commons.list_rpki_files(date, HistoricalRpkiSource::Ripe).unwrap();
152//! # }
153//! # #[cfg(not(feature = "rpki"))]
154//! # fn main() {}
155//! ```
156//!
157//! ### Direct module access
158//!
159//! Modules can also be used directly without `BgpkitCommons`:
160//!
161//! ```rust
162//! # #[cfg(feature = "bogons")]
163//! # fn main() {
164//! use bgpkit_commons::bogons::Bogons;
165//! let bogons = Bogons::new().unwrap();
166//! # }
167//! # #[cfg(not(feature = "bogons"))]
168//! # fn main() {}
169//! ```
170//!
171//! ## Feature Flags
172//!
173//! | Feature | Description |
174//! |---------|-------------|
175//! | `asinfo` | AS information: names, countries, organizations, population, hegemony |
176//! | `as2rel` | AS relationship data |
177//! | `bogons` | Bogon prefix and ASN detection |
178//! | `countries` | Country information lookup |
179//! | `mrt_collectors` | MRT collector metadata |
180//! | `rpki` | RPKI validation (ROA and ASPA) |
181//! | `all` *(default)* | Enables all modules |
182//!
183//! For a minimal build:
184//!
185//! ```toml
186//! [dependencies]
187//! bgpkit-commons = { version = "0.10", default-features = false, features = ["bogons", "countries"] }
188//! ```
189
190#![doc(
191    html_logo_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/icon-transparent.png",
192    html_favicon_url = "https://raw.githubusercontent.com/bgpkit/assets/main/logos/favicon.ico"
193)]
194
195#[cfg(feature = "as2rel")]
196pub mod as2rel;
197#[cfg(feature = "asinfo")]
198pub mod asinfo;
199#[cfg(feature = "bogons")]
200pub mod bogons;
201#[cfg(feature = "countries")]
202pub mod countries;
203#[cfg(feature = "mrt_collectors")]
204pub mod mrt_collectors;
205#[cfg(feature = "rpki")]
206pub mod rpki;
207
208pub mod errors;
209
210// Re-export error types for convenience
211pub use errors::{BgpkitCommonsError, Result};
212
213/// Trait for modules that support lazy loading and reloading of data
214pub trait LazyLoadable {
215    /// Reload the module's data from its external sources
216    fn reload(&mut self) -> Result<()>;
217
218    /// Check if the module's data is currently loaded
219    fn is_loaded(&self) -> bool;
220
221    /// Get a description of the module's current loading status
222    fn loading_status(&self) -> &'static str;
223}
224
225#[derive(Default)]
226pub struct BgpkitCommons {
227    #[cfg(feature = "countries")]
228    countries: Option<crate::countries::Countries>,
229    #[cfg(feature = "rpki")]
230    rpki_trie: Option<crate::rpki::RpkiTrie>,
231    #[cfg(feature = "mrt_collectors")]
232    mrt_collectors: Option<Vec<crate::mrt_collectors::MrtCollector>>,
233    #[cfg(feature = "mrt_collectors")]
234    mrt_collector_peers: Option<Vec<crate::mrt_collectors::MrtCollectorPeer>>,
235    #[cfg(feature = "bogons")]
236    bogons: Option<crate::bogons::Bogons>,
237    #[cfg(feature = "asinfo")]
238    asinfo: Option<crate::asinfo::AsInfoUtils>,
239    #[cfg(feature = "as2rel")]
240    as2rel: Option<crate::as2rel::As2relBgpkit>,
241}
242
243impl BgpkitCommons {
244    pub fn new() -> Self {
245        Self::default()
246    }
247
248    /// Reload all data sources that are already loaded
249    pub fn reload(&mut self) -> Result<()> {
250        #[cfg(feature = "countries")]
251        if self.countries.is_some() {
252            self.load_countries()?;
253        }
254        #[cfg(feature = "rpki")]
255        if let Some(rpki) = self.rpki_trie.as_mut() {
256            rpki.reload()?;
257        }
258        #[cfg(feature = "mrt_collectors")]
259        if self.mrt_collectors.is_some() {
260            self.load_mrt_collectors()?;
261        }
262        #[cfg(feature = "mrt_collectors")]
263        if self.mrt_collector_peers.is_some() {
264            self.load_mrt_collector_peers()?;
265        }
266        #[cfg(feature = "bogons")]
267        if self.bogons.is_some() {
268            self.load_bogons()?;
269        }
270        #[cfg(feature = "asinfo")]
271        if let Some(asinfo) = self.asinfo.as_mut() {
272            asinfo.reload()?;
273        }
274        #[cfg(feature = "as2rel")]
275        if self.as2rel.is_some() {
276            self.load_as2rel()?;
277        }
278
279        Ok(())
280    }
281
282    /// Get loading status for all available modules
283    pub fn loading_status(&self) -> Vec<(&'static str, &'static str)> {
284        #[allow(unused_mut)] // mut needed when any features are enabled
285        let mut status = Vec::new();
286
287        #[cfg(feature = "countries")]
288        if let Some(ref countries) = self.countries {
289            status.push(("countries", countries.loading_status()));
290        } else {
291            status.push(("countries", "Countries data not loaded"));
292        }
293
294        #[cfg(feature = "bogons")]
295        if let Some(ref bogons) = self.bogons {
296            status.push(("bogons", bogons.loading_status()));
297        } else {
298            status.push(("bogons", "Bogons data not loaded"));
299        }
300
301        #[cfg(feature = "rpki")]
302        if let Some(ref rpki) = self.rpki_trie {
303            status.push(("rpki", rpki.loading_status()));
304        } else {
305            status.push(("rpki", "RPKI data not loaded"));
306        }
307
308        #[cfg(feature = "asinfo")]
309        if let Some(ref asinfo) = self.asinfo {
310            status.push(("asinfo", asinfo.loading_status()));
311        } else {
312            status.push(("asinfo", "ASInfo data not loaded"));
313        }
314
315        #[cfg(feature = "as2rel")]
316        if let Some(ref as2rel) = self.as2rel {
317            status.push(("as2rel", as2rel.loading_status()));
318        } else {
319            status.push(("as2rel", "AS2Rel data not loaded"));
320        }
321
322        #[cfg(feature = "mrt_collectors")]
323        {
324            if self.mrt_collectors.is_some() {
325                status.push(("mrt_collectors", "MRT collectors data loaded"));
326            } else {
327                status.push(("mrt_collectors", "MRT collectors data not loaded"));
328            }
329
330            if self.mrt_collector_peers.is_some() {
331                status.push(("mrt_collector_peers", "MRT collector peers data loaded"));
332            } else {
333                status.push(("mrt_collector_peers", "MRT collector peers data not loaded"));
334            }
335        }
336
337        status
338    }
339
340    /// Load countries data
341    #[cfg(feature = "countries")]
342    pub fn load_countries(&mut self) -> Result<()> {
343        self.countries = Some(crate::countries::Countries::new()?);
344        Ok(())
345    }
346
347    /// Load RPKI data from Cloudflare (real-time) or historical archives
348    ///
349    /// - If `date_opt` is `None`, loads real-time data from Cloudflare
350    /// - If `date_opt` is `Some(date)`, loads historical data from RIPE NCC by default
351    ///
352    /// For more control over the data source, use `load_rpki_historical()` instead.
353    #[cfg(feature = "rpki")]
354    pub fn load_rpki(&mut self, date_opt: Option<chrono::NaiveDate>) -> Result<()> {
355        if let Some(date) = date_opt {
356            self.rpki_trie = Some(rpki::RpkiTrie::from_ripe_historical(date)?);
357        } else {
358            self.rpki_trie = Some(rpki::RpkiTrie::from_cloudflare()?);
359        }
360        Ok(())
361    }
362
363    /// Load RPKI data from a specific historical data source
364    ///
365    /// This allows you to choose between RIPE NCC and RPKIviews for historical data.
366    ///
367    /// # Example
368    ///
369    /// ```rust,no_run
370    /// use bgpkit_commons::BgpkitCommons;
371    /// use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
372    /// use chrono::NaiveDate;
373    ///
374    /// let mut commons = BgpkitCommons::new();
375    /// let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
376    ///
377    /// // Load from RIPE NCC
378    /// commons.load_rpki_historical(date, HistoricalRpkiSource::Ripe).unwrap();
379    ///
380    /// // Or load from RPKIviews
381    /// let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::KerfuffleNet);
382    /// commons.load_rpki_historical(date, source).unwrap();
383    /// ```
384    #[cfg(feature = "rpki")]
385    pub fn load_rpki_historical(
386        &mut self,
387        date: chrono::NaiveDate,
388        source: rpki::HistoricalRpkiSource,
389    ) -> Result<()> {
390        match source {
391            rpki::HistoricalRpkiSource::Ripe => {
392                self.rpki_trie = Some(rpki::RpkiTrie::from_ripe_historical(date)?);
393            }
394            rpki::HistoricalRpkiSource::RpkiViews(collector) => {
395                self.rpki_trie = Some(rpki::RpkiTrie::from_rpkiviews(collector, date)?);
396            }
397            rpki::HistoricalRpkiSource::RpkiSpools(collector) => {
398                self.rpki_trie = Some(rpki::RpkiTrie::from_rpkispools(collector, date)?);
399            }
400        }
401        Ok(())
402    }
403
404    /// Load RPKI data from specific file URLs
405    ///
406    /// This allows loading from specific archive files, which is useful when you want
407    /// to process multiple files or use specific timestamps.
408    ///
409    /// # Arguments
410    ///
411    /// * `urls` - A slice of URLs pointing to RPKI data files
412    /// * `source` - The type of data source (RIPE, RPKIviews, or RPKISPOOL) - determines how files are parsed
413    /// * `date` - Optional date to associate with the loaded data
414    ///
415    /// # Example
416    ///
417    /// ```rust,no_run
418    /// use bgpkit_commons::BgpkitCommons;
419    /// use bgpkit_commons::rpki::HistoricalRpkiSource;
420    ///
421    /// let mut commons = BgpkitCommons::new();
422    /// let urls = vec![
423    ///     "https://example.com/rpki-20240104T144128Z.tgz".to_string(),
424    /// ];
425    /// commons.load_rpki_from_files(&urls, HistoricalRpkiSource::RpkiViews(
426    ///     bgpkit_commons::rpki::RpkiViewsCollector::KerfuffleNet
427    /// ), None).unwrap();
428    /// ```
429    #[cfg(feature = "rpki")]
430    pub fn load_rpki_from_files(
431        &mut self,
432        urls: &[String],
433        source: rpki::HistoricalRpkiSource,
434        date: Option<chrono::NaiveDate>,
435    ) -> Result<()> {
436        match source {
437            rpki::HistoricalRpkiSource::Ripe => {
438                self.rpki_trie = Some(rpki::RpkiTrie::from_ripe_files(urls, date)?);
439            }
440            rpki::HistoricalRpkiSource::RpkiViews(_) => {
441                self.rpki_trie = Some(rpki::RpkiTrie::from_rpkiviews_files(urls, date)?);
442            }
443            rpki::HistoricalRpkiSource::RpkiSpools(_) => {
444                // For RPKISPOOL, each URL is a tar.zst archive; load the first one
445                if let Some(url) = urls.first() {
446                    self.rpki_trie = Some(rpki::RpkiTrie::from_rpkispools_url(url, date)?);
447                }
448            }
449        }
450        Ok(())
451    }
452
453    /// List available RPKI files for a given date from a specific source
454    ///
455    /// # Example
456    ///
457    /// ```rust,no_run
458    /// use bgpkit_commons::BgpkitCommons;
459    /// use bgpkit_commons::rpki::{HistoricalRpkiSource, RpkiViewsCollector};
460    /// use chrono::NaiveDate;
461    ///
462    /// let commons = BgpkitCommons::new();
463    /// let date = NaiveDate::from_ymd_opt(2024, 1, 4).unwrap();
464    ///
465    /// // List files from RIPE NCC
466    /// let ripe_files = commons.list_rpki_files(date, HistoricalRpkiSource::Ripe).unwrap();
467    ///
468    /// // List files from RPKIviews
469    /// let source = HistoricalRpkiSource::RpkiViews(RpkiViewsCollector::KerfuffleNet);
470    /// let rpkiviews_files = commons.list_rpki_files(date, source).unwrap();
471    /// ```
472    #[cfg(feature = "rpki")]
473    pub fn list_rpki_files(
474        &self,
475        date: chrono::NaiveDate,
476        source: rpki::HistoricalRpkiSource,
477    ) -> Result<Vec<rpki::RpkiFile>> {
478        match source {
479            rpki::HistoricalRpkiSource::Ripe => rpki::list_ripe_files(date),
480            rpki::HistoricalRpkiSource::RpkiViews(collector) => {
481                rpki::list_rpkiviews_files(collector, date)
482            }
483            rpki::HistoricalRpkiSource::RpkiSpools(collector) => {
484                rpki::list_rpkispools_files(collector, date)
485            }
486        }
487    }
488
489    /// Load MRT mrt_collectors data
490    #[cfg(feature = "mrt_collectors")]
491    pub fn load_mrt_collectors(&mut self) -> Result<()> {
492        self.mrt_collectors = Some(crate::mrt_collectors::get_all_collectors()?);
493        Ok(())
494    }
495
496    /// Load MRT mrt_collectors data
497    #[cfg(feature = "mrt_collectors")]
498    pub fn load_mrt_collector_peers(&mut self) -> Result<()> {
499        self.mrt_collector_peers = Some(crate::mrt_collectors::get_mrt_collector_peers()?);
500        Ok(())
501    }
502
503    /// Load bogons data
504    #[cfg(feature = "bogons")]
505    pub fn load_bogons(&mut self) -> Result<()> {
506        self.bogons = Some(crate::bogons::Bogons::new()?);
507        Ok(())
508    }
509
510    /// Load AS name and country data
511    #[cfg(feature = "asinfo")]
512    pub fn load_asinfo(
513        &mut self,
514        load_as2org: bool,
515        load_population: bool,
516        load_hegemony: bool,
517        load_peeringdb: bool,
518    ) -> Result<()> {
519        self.asinfo = Some(crate::asinfo::AsInfoUtils::new(
520            load_as2org,
521            load_population,
522            load_hegemony,
523            load_peeringdb,
524        )?);
525        Ok(())
526    }
527
528    #[cfg(feature = "asinfo")]
529    pub fn load_asinfo_cached(&mut self) -> Result<()> {
530        self.asinfo = Some(crate::asinfo::AsInfoUtils::new_from_cached()?);
531        Ok(())
532    }
533
534    /// Returns a builder for loading AS information with specific data sources.
535    ///
536    /// This provides a more ergonomic way to configure which data sources to load
537    /// compared to the `load_asinfo()` method with boolean parameters.
538    ///
539    /// # Example
540    ///
541    /// ```rust,no_run
542    /// use bgpkit_commons::BgpkitCommons;
543    ///
544    /// let mut commons = BgpkitCommons::new();
545    /// let builder = commons.asinfo_builder()
546    ///     .with_as2org()
547    ///     .with_peeringdb();
548    /// commons.load_asinfo_with(builder).unwrap();
549    /// ```
550    #[cfg(feature = "asinfo")]
551    pub fn asinfo_builder(&self) -> crate::asinfo::AsInfoBuilder {
552        crate::asinfo::AsInfoBuilder::new()
553    }
554
555    /// Load AS information using a pre-configured builder.
556    ///
557    /// # Example
558    ///
559    /// ```rust,no_run
560    /// use bgpkit_commons::BgpkitCommons;
561    ///
562    /// let mut commons = BgpkitCommons::new();
563    /// let builder = commons.asinfo_builder()
564    ///     .with_as2org()
565    ///     .with_hegemony();
566    /// commons.load_asinfo_with(builder).unwrap();
567    /// ```
568    #[cfg(feature = "asinfo")]
569    pub fn load_asinfo_with(&mut self, builder: crate::asinfo::AsInfoBuilder) -> Result<()> {
570        self.asinfo = Some(builder.build()?);
571        Ok(())
572    }
573
574    /// Load AS-level relationship data
575    #[cfg(feature = "as2rel")]
576    pub fn load_as2rel(&mut self) -> Result<()> {
577        self.as2rel = Some(crate::as2rel::As2relBgpkit::new()?);
578        Ok(())
579    }
580}