Skip to main content

bgpkit_commons/asinfo/
mod.rs

1//! asinfo is a module for simple Autonomous System (AS) names and country lookup
2//!
3//! # Data source
4//!
5//! - RIPE NCC asinfo: <https://ftp.ripe.net/ripe/asnames/asn.txt>
6//! - (Optional) CAIDA as-to-organization mapping: <https://www.caida.org/catalog/datasets/as-organizations/>
7//! - (Optional) APNIC AS population data: <https://stats.labs.apnic.net/cgi-bin/aspop>
8//! - (Optional) IIJ IHR Hegemony data: <https://ihr-archive.iijlab.net/>
9//! - (Optional) PeeringDB data: <https://www.peeringdb.com>
10//!
11//! # Data structure
12//!
13//! ```rust,no_run
14//! use serde::{Deserialize, Serialize};
15//! #[derive(Debug, Clone, Serialize, Deserialize)]
16//! pub struct AsInfo {
17//!     pub asn: u32,
18//!     pub name: String,
19//!     pub country: String,
20//!     pub as2org: Option<As2orgInfo>,
21//!     pub population: Option<AsnPopulationData>,
22//!     pub hegemony: Option<HegemonyData>,
23//! }
24//! #[derive(Debug, Clone, Serialize, Deserialize)]
25//! pub struct As2orgInfo {
26//!     pub name: String,
27//!     pub country: String,
28//!     pub org_id: String,
29//!     pub org_name: String,
30//! }
31//! #[derive(Debug, Clone, Serialize, Deserialize)]
32//! pub struct AsnPopulationData {
33//!     pub user_count: i64,
34//!     pub percent_country: f64,
35//!     pub percent_global: f64,
36//!     pub sample_count: i64,
37//! }
38//! #[derive(Debug, Clone, Serialize, Deserialize)]
39//! pub struct HegemonyData {
40//!     pub asn: u32,
41//!     pub ipv4: f64,
42//!     pub ipv6: f64,
43//! }
44//! ```
45//!
46//! The `peeringdb` field of `AsInfo` uses [`crate::peeringdb::Network`], which
47//! mirrors the full PeeringDB `/net` API record.
48//!
49//! # Example
50//!
51//! Call with `BgpkitCommons` instance:
52//!
53//! ```rust,no_run
54//! use bgpkit_commons::BgpkitCommons;
55//!
56//! let mut bgpkit = BgpkitCommons::new();
57//! bgpkit.load_asinfo(false, false, false, false).unwrap();
58//! let asinfo = bgpkit.asinfo_get(3333).unwrap().unwrap();
59//! assert_eq!(asinfo.name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
60//! ```
61//!
62//! Directly call the module:
63//!
64//! ```rust,no_run
65//! use std::collections::HashMap;
66//! use bgpkit_commons::asinfo::{AsInfo, get_asinfo_map};
67//!
68//! let asinfo: HashMap<u32, AsInfo> = get_asinfo_map(false, false, false, false).unwrap();
69//! assert_eq!(asinfo.get(&3333).unwrap().name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
70//! assert_eq!(asinfo.get(&400644).unwrap().name, "BGPKIT-LLC");
71//! assert_eq!(asinfo.get(&400644).unwrap().country, "US");
72//! ```
73//!
74//! Retrieve all previously generated and cached AS information:
75//! ```rust,no_run
76//! use std::collections::HashMap;
77//! use bgpkit_commons::asinfo::{get_asinfo_map_cached, AsInfo};
78//! let asinfo: HashMap<u32, AsInfo> = get_asinfo_map_cached().unwrap();
79//! assert_eq!(asinfo.get(&3333).unwrap().name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
80//! assert_eq!(asinfo.get(&400644).unwrap().name, "BGPKIT-LLC");
81//! assert_eq!(asinfo.get(&400644).unwrap().country, "US");
82//! ```
83//!
84//! Or with `BgpkitCommons` instance:
85//! ```rust,no_run
86//!
87//! use std::collections::HashMap;
88//! use bgpkit_commons::asinfo::AsInfo;
89//! use bgpkit_commons::BgpkitCommons;
90//!
91//! let mut commons = BgpkitCommons::new();
92//! commons.load_asinfo_cached().unwrap();
93//! let asinfo: HashMap<u32, AsInfo> = commons.asinfo_all().unwrap();
94//! assert_eq!(asinfo.get(&3333).unwrap().name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
95//! assert_eq!(asinfo.get(&400644).unwrap().name, "BGPKIT-LLC");
96//! assert_eq!(asinfo.get(&400644).unwrap().country, "US");
97//! ```
98//!
99//! Check if two ASNs are siblings:
100//!
101//! ```rust,no_run
102//! use bgpkit_commons::BgpkitCommons;
103//!
104//! let mut bgpkit = BgpkitCommons::new();
105//! bgpkit.load_asinfo(true, false, false, false).unwrap();
106//! let are_siblings = bgpkit.asinfo_are_siblings(3333, 3334).unwrap();
107//! ```
108
109mod as2org;
110mod hegemony;
111mod population;
112mod sibling_orgs;
113
114use crate::errors::{data_sources, load_methods, modules};
115use crate::peeringdb::{Network, Peeringdb};
116use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
117use serde::{Deserialize, Serialize};
118use sibling_orgs::SiblingOrgsUtils;
119use std::collections::HashMap;
120use std::io::{BufRead, Read};
121use tracing::info;
122
123pub use hegemony::HegemonyData;
124pub use population::AsnPopulationData;
125
126#[derive(Debug, Clone, Serialize, Deserialize)]
127pub struct AsInfo {
128    pub asn: u32,
129    pub name: String,
130    pub country: String,
131    pub as2org: Option<As2orgInfo>,
132    pub population: Option<AsnPopulationData>,
133    pub hegemony: Option<HegemonyData>,
134    pub peeringdb: Option<Network>,
135}
136
137impl AsInfo {
138    /// Returns the preferred name for the AS.
139    ///
140    /// The order of preference is:
141    /// 1. `peeringdb.name` if available
142    /// 2. `as2org.org_name` if available and not empty
143    /// 3. The default `name` field
144    ///
145    /// This method does not perform any network access.
146    pub fn get_preferred_name(&self) -> String {
147        if let Some(peeringdb_data) = &self.peeringdb {
148            if let Some(name) = &peeringdb_data.name {
149                if !name.is_empty() {
150                    return name.clone();
151                }
152            }
153        }
154        if let Some(as2org_info) = &self.as2org {
155            if !as2org_info.org_name.is_empty() {
156                return as2org_info.org_name.clone();
157            }
158        }
159        self.name.clone()
160    }
161}
162
163#[derive(Debug, Clone, Serialize, Deserialize)]
164pub struct As2orgInfo {
165    pub name: String,
166    pub country: String,
167    pub org_id: String,
168    pub org_name: String,
169}
170
171const RIPE_RIS_ASN_TXT_URL: &str = "https://ftp.ripe.net/ripe/asnames/asn.txt";
172const BGPKIT_ASN_TXT_MIRROR_URL: &str = "https://data.bgpkit.com/commons/asn.txt";
173const BGPKIT_ASNINFO_URL: &str = "https://data.bgpkit.com/commons/asinfo.jsonl";
174
175/// Builder for configuring which data sources to load for AS information.
176///
177/// # Example
178///
179/// ```rust,no_run
180/// use bgpkit_commons::asinfo::AsInfoBuilder;
181///
182/// let asinfo = AsInfoBuilder::new()
183///     .with_as2org()
184///     .with_peeringdb()
185///     .build()
186///     .unwrap();
187/// ```
188#[derive(Default)]
189pub struct AsInfoBuilder {
190    load_as2org: bool,
191    load_population: bool,
192    load_hegemony: bool,
193    load_peeringdb: bool,
194}
195
196impl AsInfoBuilder {
197    /// Create a new builder with all data sources disabled by default.
198    pub fn new() -> Self {
199        Self::default()
200    }
201
202    /// Enable loading CAIDA AS-to-Organization mapping data.
203    pub fn with_as2org(mut self) -> Self {
204        self.load_as2org = true;
205        self
206    }
207
208    /// Enable loading APNIC AS population data.
209    pub fn with_population(mut self) -> Self {
210        self.load_population = true;
211        self
212    }
213
214    /// Enable loading IIJ IHR hegemony score data.
215    pub fn with_hegemony(mut self) -> Self {
216        self.load_hegemony = true;
217        self
218    }
219
220    /// Enable loading PeeringDB data.
221    pub fn with_peeringdb(mut self) -> Self {
222        self.load_peeringdb = true;
223        self
224    }
225
226    /// Enable all optional data sources.
227    pub fn with_all(mut self) -> Self {
228        self.load_as2org = true;
229        self.load_population = true;
230        self.load_hegemony = true;
231        self.load_peeringdb = true;
232        self
233    }
234
235    /// Build the AsInfoUtils with the configured data sources.
236    pub fn build(self) -> Result<AsInfoUtils> {
237        AsInfoUtils::new(
238            self.load_as2org,
239            self.load_population,
240            self.load_hegemony,
241            self.load_peeringdb,
242        )
243    }
244}
245
246pub struct AsInfoUtils {
247    pub asinfo_map: HashMap<u32, AsInfo>,
248    pub sibling_orgs: Option<SiblingOrgsUtils>,
249    pub load_as2org: bool,
250    pub load_population: bool,
251    pub load_hegemony: bool,
252    pub load_peeringdb: bool,
253}
254
255impl AsInfoUtils {
256    pub fn new(
257        load_as2org: bool,
258        load_population: bool,
259        load_hegemony: bool,
260        load_peeringdb: bool,
261    ) -> Result<Self> {
262        let asinfo_map =
263            get_asinfo_map(load_as2org, load_population, load_hegemony, load_peeringdb)?;
264        let sibling_orgs = if load_as2org {
265            Some(SiblingOrgsUtils::new()?)
266        } else {
267            None
268        };
269        Ok(AsInfoUtils {
270            asinfo_map,
271            sibling_orgs,
272            load_as2org,
273            load_population,
274            load_hegemony,
275            load_peeringdb,
276        })
277    }
278
279    pub fn new_from_cached() -> Result<Self> {
280        let asinfo_map = get_asinfo_map_cached()?;
281        let sibling_orgs = Some(SiblingOrgsUtils::new()?);
282        Ok(AsInfoUtils {
283            asinfo_map,
284            sibling_orgs,
285            load_as2org: true,
286            load_population: true,
287            load_hegemony: true,
288            load_peeringdb: true,
289        })
290    }
291
292    pub fn reload(&mut self) -> Result<()> {
293        self.asinfo_map = get_asinfo_map(
294            self.load_as2org,
295            self.load_population,
296            self.load_hegemony,
297            self.load_peeringdb,
298        )?;
299        Ok(())
300    }
301
302    pub fn get(&self, asn: u32) -> Option<&AsInfo> {
303        self.asinfo_map.get(&asn)
304    }
305}
306
307impl LazyLoadable for AsInfoUtils {
308    fn reload(&mut self) -> Result<()> {
309        self.reload()
310    }
311
312    fn is_loaded(&self) -> bool {
313        !self.asinfo_map.is_empty()
314    }
315
316    fn loading_status(&self) -> &'static str {
317        if self.is_loaded() {
318            "ASInfo data loaded"
319        } else {
320            "ASInfo data not loaded"
321        }
322    }
323}
324
325pub fn get_asinfo_map_cached() -> Result<HashMap<u32, AsInfo>> {
326    info!("loading asinfo from previously generated BGPKIT cache file...");
327    let mut asnames_map = HashMap::new();
328    let reader = oneio::get_reader(BGPKIT_ASNINFO_URL)?;
329    for line in std::io::BufReader::new(reader).lines() {
330        let line = line?;
331        if line.trim().is_empty() {
332            continue;
333        }
334        let asinfo: AsInfo = serde_json::from_str(&line)?;
335        asnames_map.insert(asinfo.asn, asinfo);
336    }
337    Ok(asnames_map)
338}
339
340pub fn get_asinfo_map(
341    load_as2org: bool,
342    load_population: bool,
343    load_hegemony: bool,
344    load_peeringdb: bool,
345) -> Result<HashMap<u32, AsInfo>> {
346    info!("loading asinfo from RIPE NCC...");
347    let read_text = |url: &str| -> Result<String> {
348        let mut text = String::new();
349        oneio::get_reader(url)?.read_to_string(&mut text)?;
350        Ok(text)
351    };
352    let text = match read_text(BGPKIT_ASN_TXT_MIRROR_URL) {
353        Ok(t) => t,
354        Err(_) => match read_text(RIPE_RIS_ASN_TXT_URL) {
355            Ok(t) => t,
356            Err(e) => {
357                return Err(BgpkitCommonsError::data_source_error(
358                    data_sources::BGPKIT,
359                    format!(
360                        "error reading asinfo (neither mirror or original works): {}",
361                        e
362                    ),
363                ));
364            }
365        },
366    };
367
368    let as2org_utils = if load_as2org {
369        info!("loading as2org data from CAIDA...");
370        Some(as2org::As2org::new(None)?)
371    } else {
372        None
373    };
374    let population_utils = if load_population {
375        info!("loading ASN population data from APNIC...");
376        Some(population::AsnPopulation::new()?)
377    } else {
378        None
379    };
380    let hegemony_utils = if load_hegemony {
381        info!("loading IIJ IHR hegemony score data from BGPKIT mirror...");
382        Some(hegemony::Hegemony::new()?)
383    } else {
384        None
385    };
386    let peeringdb_utils = if load_peeringdb {
387        info!("loading peeringdb data...");
388        Some(Peeringdb::new_networks_only()?)
389    } else {
390        None
391    };
392
393    let asnames = text
394        .lines()
395        .filter_map(|line| {
396            let (asn_str, name_country_str) = match line.split_once(' ') {
397                Some((asn, name)) => (asn, name),
398                None => return None,
399            };
400            let (name_str, country_str) = match name_country_str.rsplit_once(", ") {
401                Some((name, country)) => (name, country),
402                None => return None,
403            };
404            let asn = asn_str.parse::<u32>().unwrap();
405            let as2org = as2org_utils.as_ref().and_then(|as2org_data| {
406                as2org_data.get_as_info(asn).map(|info| As2orgInfo {
407                    name: info.name.clone(),
408                    country: info.country_code.clone(),
409                    org_id: info.org_id.clone(),
410                    org_name: info.org_name.clone(),
411                })
412            });
413            let population = population_utils.as_ref().and_then(|p| p.get(asn));
414            let hegemony = hegemony_utils
415                .as_ref()
416                .and_then(|h| h.get_score(asn).cloned());
417            let peeringdb = peeringdb_utils
418                .as_ref()
419                .and_then(|h| h.get_network(asn).cloned());
420            Some(AsInfo {
421                asn,
422                name: name_str.to_string(),
423                country: country_str.to_string(),
424                as2org,
425                population,
426                hegemony,
427                peeringdb,
428            })
429        })
430        .collect::<Vec<AsInfo>>();
431
432    let mut asnames_map = HashMap::new();
433    for asname in asnames {
434        asnames_map.insert(asname.asn, asname);
435    }
436    Ok(asnames_map)
437}
438
439impl BgpkitCommons {
440    /// Returns a HashMap containing all AS information.
441    ///
442    /// # Returns
443    ///
444    /// - `Ok(HashMap<u32, AsInfo>)`: A HashMap where the key is the ASN and the value is the corresponding AsInfo.
445    /// - `Err`: If the asinfo is not loaded.
446    ///
447    /// # Examples
448    ///
449    /// ```no_run
450    /// use bgpkit_commons::BgpkitCommons;
451    ///
452    /// let mut bgpkit = BgpkitCommons::new();
453    /// bgpkit.load_asinfo(false, false, false, false).unwrap();
454    /// let all_asinfo = bgpkit.asinfo_all().unwrap();
455    /// ```
456    pub fn asinfo_all(&self) -> Result<HashMap<u32, AsInfo>> {
457        if self.asinfo.is_none() {
458            return Err(BgpkitCommonsError::module_not_loaded(
459                modules::ASINFO,
460                load_methods::LOAD_ASINFO,
461            ));
462        }
463
464        Ok(self.asinfo.as_ref().unwrap().asinfo_map.clone())
465    }
466
467    /// Retrieves AS information for a specific ASN.
468    ///
469    /// # Arguments
470    ///
471    /// * `asn` - The Autonomous System Number to look up.
472    ///
473    /// # Returns
474    ///
475    /// - `Ok(Some(AsInfo))`: The AS information if found.
476    /// - `Ok(None)`: If the ASN is not found in the database.
477    /// - `Err`: If the asinfo is not loaded.
478    ///
479    /// # Examples
480    ///
481    /// ```no_run
482    /// use bgpkit_commons::BgpkitCommons;
483    ///
484    /// let mut bgpkit = BgpkitCommons::new();
485    /// bgpkit.load_asinfo(false, false, false, false).unwrap();
486    /// let asinfo = bgpkit.asinfo_get(3333).unwrap();
487    /// ```
488    pub fn asinfo_get(&self, asn: u32) -> Result<Option<AsInfo>> {
489        if self.asinfo.is_none() {
490            return Err(BgpkitCommonsError::module_not_loaded(
491                modules::ASINFO,
492                load_methods::LOAD_ASINFO,
493            ));
494        }
495
496        Ok(self.asinfo.as_ref().unwrap().get(asn).cloned())
497    }
498
499    /// Checks if two ASNs are siblings (belong to the same organization).
500    ///
501    /// # Arguments
502    ///
503    /// * `asn1` - The first Autonomous System Number.
504    /// * `asn2` - The second Autonomous System Number.
505    ///
506    /// # Returns
507    ///
508    /// - `Ok(bool)`: True if the ASNs are siblings, false otherwise.
509    /// - `Err`: If the asinfo is not loaded or not loaded with as2org data.
510    ///
511    /// # Examples
512    ///
513    /// ```no_run
514    /// use bgpkit_commons::BgpkitCommons;
515    ///
516    /// let mut bgpkit = BgpkitCommons::new();
517    /// bgpkit.load_asinfo(true, false, false, false).unwrap();
518    /// let are_siblings = bgpkit.asinfo_are_siblings(3333, 3334).unwrap();
519    /// ```
520    ///
521    /// # Note
522    ///
523    /// This function requires the asinfo to be loaded with as2org data.
524    pub fn asinfo_are_siblings(&self, asn1: u32, asn2: u32) -> Result<bool> {
525        if self.asinfo.is_none() {
526            return Err(BgpkitCommonsError::module_not_loaded(
527                modules::ASINFO,
528                load_methods::LOAD_ASINFO,
529            ));
530        }
531        if !self.asinfo.as_ref().unwrap().load_as2org {
532            return Err(BgpkitCommonsError::module_not_configured(
533                modules::ASINFO,
534                "as2org data",
535                "load_asinfo() with as2org=true",
536            ));
537        }
538
539        let info_1_opt = self.asinfo_get(asn1)?;
540        let info_2_opt = self.asinfo_get(asn2)?;
541
542        if let (Some(info1), Some(info2)) = (info_1_opt, info_2_opt) {
543            if let (Some(org1), Some(org2)) = (info1.as2org, info2.as2org) {
544                let org_id_1 = org1.org_id;
545                let org_id_2 = org2.org_id;
546
547                return Ok(org_id_1 == org_id_2
548                    || self
549                        .asinfo
550                        .as_ref()
551                        .and_then(|a| a.sibling_orgs.as_ref())
552                        .map(|s| s.are_sibling_orgs(org_id_1.as_str(), org_id_2.as_str()))
553                        .unwrap_or(false));
554            }
555        }
556        Ok(false)
557    }
558}