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//! #[derive(Debug, Clone, Serialize, Deserialize)]
45//! pub struct PeeringdbData {
46//!     pub asn: u32,
47//!     pub name: Option<String>,
48//!     pub name_long: Option<String>,
49//!     pub aka: Option<String>,
50//!     pub irr_as_set: Option<String>,
51//! }
52//! ```
53//!
54//! # Example
55//!
56//! Call with `BgpkitCommons` instance:
57//!
58//! ```rust,no_run
59//! use bgpkit_commons::BgpkitCommons;
60//!
61//! let mut bgpkit = BgpkitCommons::new();
62//! bgpkit.load_asinfo(false, false, false, false).unwrap();
63//! let asinfo = bgpkit.asinfo_get(3333).unwrap().unwrap();
64//! assert_eq!(asinfo.name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
65//! ```
66//!
67//! Directly call the module:
68//!
69//! ```rust,no_run
70//! use std::collections::HashMap;
71//! use bgpkit_commons::asinfo::{AsInfo, get_asinfo_map};
72//!
73//! let asinfo: HashMap<u32, AsInfo> = get_asinfo_map(false, false, false, false).unwrap();
74//! assert_eq!(asinfo.get(&3333).unwrap().name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
75//! assert_eq!(asinfo.get(&400644).unwrap().name, "BGPKIT-LLC");
76//! assert_eq!(asinfo.get(&400644).unwrap().country, "US");
77//! ```
78//!
79//! Retrieve all previously generated and cached AS information:
80//! ```rust,no_run
81//! use std::collections::HashMap;
82//! use bgpkit_commons::asinfo::{get_asinfo_map_cached, AsInfo};
83//! let asinfo: HashMap<u32, AsInfo> = get_asinfo_map_cached().unwrap();
84//! assert_eq!(asinfo.get(&3333).unwrap().name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
85//! assert_eq!(asinfo.get(&400644).unwrap().name, "BGPKIT-LLC");
86//! assert_eq!(asinfo.get(&400644).unwrap().country, "US");
87//! ```
88//!
89//! Or with `BgpkitCommons` instance:
90//! ```rust,no_run
91//!
92//! use std::collections::HashMap;
93//! use bgpkit_commons::asinfo::AsInfo;
94//! use bgpkit_commons::BgpkitCommons;
95//!
96//! let mut commons = BgpkitCommons::new();
97//! commons.load_asinfo_cached().unwrap();
98//! let asinfo: HashMap<u32, AsInfo> = commons.asinfo_all().unwrap();
99//! assert_eq!(asinfo.get(&3333).unwrap().name, "RIPE-NCC-AS Reseaux IP Europeens Network Coordination Centre (RIPE NCC)");
100//! assert_eq!(asinfo.get(&400644).unwrap().name, "BGPKIT-LLC");
101//! assert_eq!(asinfo.get(&400644).unwrap().country, "US");
102//! ```
103//!
104//! Check if two ASNs are siblings:
105//!
106//! ```rust,no_run
107//! use bgpkit_commons::BgpkitCommons;
108//!
109//! let mut bgpkit = BgpkitCommons::new();
110//! bgpkit.load_asinfo(true, false, false, false).unwrap();
111//! let are_siblings = bgpkit.asinfo_are_siblings(3333, 3334).unwrap();
112//! ```
113
114mod hegemony;
115mod peeringdb;
116mod population;
117mod sibling_orgs;
118
119use crate::errors::{data_sources, load_methods, modules};
120use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
121use peeringdb::PeeringdbData;
122use serde::{Deserialize, Serialize};
123use sibling_orgs::SiblingOrgsUtils;
124use std::collections::HashMap;
125use tracing::info;
126
127pub use hegemony::HegemonyData;
128pub use population::AsnPopulationData;
129
130#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct AsInfo {
132    pub asn: u32,
133    pub name: String,
134    pub country: String,
135    pub as2org: Option<As2orgInfo>,
136    pub population: Option<AsnPopulationData>,
137    pub hegemony: Option<HegemonyData>,
138    pub peeringdb: Option<PeeringdbData>,
139}
140
141impl AsInfo {
142    /// Returns the preferred name for the AS.
143    ///
144    /// The order of preference is:
145    /// 1. `peeringdb.name` if available
146    /// 2. `as2org.org_name` if available and not empty
147    /// 3. The default `name` field
148    /// ```
149    pub fn get_preferred_name(&self) -> String {
150        if let Some(peeringdb_data) = &self.peeringdb {
151            if let Some(name) = &peeringdb_data.name {
152                return name.clone();
153            }
154        }
155        if let Some(as2org_info) = &self.as2org {
156            if !as2org_info.org_name.is_empty() {
157                return as2org_info.org_name.clone();
158            }
159        }
160        self.name.clone()
161    }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct As2orgInfo {
166    pub name: String,
167    pub country: String,
168    pub org_id: String,
169    pub org_name: String,
170}
171
172const RIPE_RIS_ASN_TXT_URL: &str = "https://ftp.ripe.net/ripe/asnames/asn.txt";
173const BGPKIT_ASN_TXT_MIRROR_URL: &str = "https://data.bgpkit.com/commons/asn.txt";
174const BGPKIT_ASNINFO_URL: &str = "https://data.bgpkit.com/commons/asinfo.jsonl";
175
176pub struct AsInfoUtils {
177    pub asinfo_map: HashMap<u32, AsInfo>,
178    pub sibling_orgs: Option<SiblingOrgsUtils>,
179    pub load_as2org: bool,
180    pub load_population: bool,
181    pub load_hegemony: bool,
182    pub load_peeringdb: bool,
183}
184
185impl AsInfoUtils {
186    pub fn new(
187        load_as2org: bool,
188        load_population: bool,
189        load_hegemony: bool,
190        load_peeringdb: bool,
191    ) -> Result<Self> {
192        let asinfo_map =
193            get_asinfo_map(load_as2org, load_population, load_hegemony, load_peeringdb)?;
194        let sibling_orgs = if load_as2org {
195            Some(SiblingOrgsUtils::new()?)
196        } else {
197            None
198        };
199        Ok(AsInfoUtils {
200            asinfo_map,
201            sibling_orgs,
202            load_as2org,
203            load_population,
204            load_hegemony,
205            load_peeringdb,
206        })
207    }
208
209    pub fn new_from_cached() -> Result<Self> {
210        let asinfo_map = get_asinfo_map_cached()?;
211        let sibling_orgs = Some(SiblingOrgsUtils::new()?);
212        Ok(AsInfoUtils {
213            asinfo_map,
214            sibling_orgs,
215            load_as2org: true,
216            load_population: true,
217            load_hegemony: true,
218            load_peeringdb: true,
219        })
220    }
221
222    pub fn reload(&mut self) -> Result<()> {
223        self.asinfo_map = get_asinfo_map(
224            self.load_as2org,
225            self.load_population,
226            self.load_hegemony,
227            self.load_peeringdb,
228        )?;
229        Ok(())
230    }
231
232    pub fn get(&self, asn: u32) -> Option<&AsInfo> {
233        self.asinfo_map.get(&asn)
234    }
235}
236
237impl LazyLoadable for AsInfoUtils {
238    fn reload(&mut self) -> Result<()> {
239        self.reload()
240    }
241
242    fn is_loaded(&self) -> bool {
243        !self.asinfo_map.is_empty()
244    }
245
246    fn loading_status(&self) -> &'static str {
247        if self.is_loaded() {
248            "ASInfo data loaded"
249        } else {
250            "ASInfo data not loaded"
251        }
252    }
253}
254
255pub fn get_asinfo_map_cached() -> Result<HashMap<u32, AsInfo>> {
256    info!("loading asinfo from previously generated BGPKIT cache file...");
257    let mut asnames_map = HashMap::new();
258    for line in oneio::read_lines(BGPKIT_ASNINFO_URL)? {
259        let line = line?;
260        if line.trim().is_empty() {
261            continue;
262        }
263        let asinfo: AsInfo = serde_json::from_str(&line)?;
264        asnames_map.insert(asinfo.asn, asinfo);
265    }
266    Ok(asnames_map)
267}
268
269pub fn get_asinfo_map(
270    load_as2org: bool,
271    load_population: bool,
272    load_hegemony: bool,
273    load_peeringdb: bool,
274) -> Result<HashMap<u32, AsInfo>> {
275    info!("loading asinfo from RIPE NCC...");
276    let text = match oneio::read_to_string(BGPKIT_ASN_TXT_MIRROR_URL) {
277        Ok(t) => t,
278        Err(_) => match oneio::read_to_string(RIPE_RIS_ASN_TXT_URL) {
279            Ok(t) => t,
280            Err(e) => {
281                return Err(BgpkitCommonsError::data_source_error(
282                    data_sources::BGPKIT,
283                    format!(
284                        "error reading asinfo (neither mirror or original works): {}",
285                        e
286                    ),
287                ));
288            }
289        },
290    };
291
292    let as2org_utils = if load_as2org {
293        info!("loading as2org data from CAIDA...");
294        Some(as2org_rs::As2org::new(None).map_err(|e| {
295            BgpkitCommonsError::data_source_error(data_sources::CAIDA, e.to_string())
296        })?)
297    } else {
298        None
299    };
300    let population_utils = if load_population {
301        info!("loading ASN population data from APNIC...");
302        Some(population::AsnPopulation::new()?)
303    } else {
304        None
305    };
306    let hegemony_utils = if load_hegemony {
307        info!("loading IIJ IHR hegemony score data from BGPKIT mirror...");
308        Some(hegemony::Hegemony::new()?)
309    } else {
310        None
311    };
312    let peeringdb_utils = if load_peeringdb {
313        info!("loading peeringdb data...");
314        Some(peeringdb::Peeringdb::new()?)
315    } else {
316        None
317    };
318
319    let asnames = text
320        .lines()
321        .filter_map(|line| {
322            let (asn_str, name_country_str) = match line.split_once(' ') {
323                Some((asn, name)) => (asn, name),
324                None => return None,
325            };
326            let (name_str, country_str) = match name_country_str.rsplit_once(", ") {
327                Some((name, country)) => (name, country),
328                None => return None,
329            };
330            let asn = asn_str.parse::<u32>().unwrap();
331            let as2org = as2org_utils.as_ref().and_then(|as2org_data| {
332                as2org_data.get_as_info(asn).map(|info| As2orgInfo {
333                    name: info.name.clone(),
334                    country: info.country_code.clone(),
335                    org_id: info.org_id.clone(),
336                    org_name: info.org_name.clone(),
337                })
338            });
339            let population = population_utils.as_ref().and_then(|p| p.get(asn));
340            let hegemony = hegemony_utils
341                .as_ref()
342                .and_then(|h| h.get_score(asn).cloned());
343            let peeringdb = peeringdb_utils
344                .as_ref()
345                .and_then(|h| h.get_data(asn).cloned());
346            Some(AsInfo {
347                asn,
348                name: name_str.to_string(),
349                country: country_str.to_string(),
350                as2org,
351                population,
352                hegemony,
353                peeringdb,
354            })
355        })
356        .collect::<Vec<AsInfo>>();
357
358    let mut asnames_map = HashMap::new();
359    for asname in asnames {
360        asnames_map.insert(asname.asn, asname);
361    }
362    Ok(asnames_map)
363}
364
365impl BgpkitCommons {
366    /// Returns a HashMap containing all AS information.
367    ///
368    /// # Returns
369    ///
370    /// - `Ok(HashMap<u32, AsInfo>)`: A HashMap where the key is the ASN and the value is the corresponding AsInfo.
371    /// - `Err`: If the asinfo is not loaded.
372    ///
373    /// # Examples
374    ///
375    /// ```no_run
376    /// use bgpkit_commons::BgpkitCommons;
377    ///
378    /// let mut bgpkit = BgpkitCommons::new();
379    /// bgpkit.load_asinfo(false, false, false, false).unwrap();
380    /// let all_asinfo = bgpkit.asinfo_all().unwrap();
381    /// ```
382    pub fn asinfo_all(&self) -> Result<HashMap<u32, AsInfo>> {
383        if self.asinfo.is_none() {
384            return Err(BgpkitCommonsError::module_not_loaded(
385                modules::ASINFO,
386                load_methods::LOAD_ASINFO,
387            ));
388        }
389
390        Ok(self.asinfo.as_ref().unwrap().asinfo_map.clone())
391    }
392
393    /// Retrieves AS information for a specific ASN.
394    ///
395    /// # Arguments
396    ///
397    /// * `asn` - The Autonomous System Number to look up.
398    ///
399    /// # Returns
400    ///
401    /// - `Ok(Some(AsInfo))`: The AS information if found.
402    /// - `Ok(None)`: If the ASN is not found in the database.
403    /// - `Err`: If the asinfo is not loaded.
404    ///
405    /// # Examples
406    ///
407    /// ```no_run
408    /// use bgpkit_commons::BgpkitCommons;
409    ///
410    /// let mut bgpkit = BgpkitCommons::new();
411    /// bgpkit.load_asinfo(false, false, false, false).unwrap();
412    /// let asinfo = bgpkit.asinfo_get(3333).unwrap();
413    /// ```
414    pub fn asinfo_get(&self, asn: u32) -> Result<Option<AsInfo>> {
415        if self.asinfo.is_none() {
416            return Err(BgpkitCommonsError::module_not_loaded(
417                modules::ASINFO,
418                load_methods::LOAD_ASINFO,
419            ));
420        }
421
422        Ok(self.asinfo.as_ref().unwrap().get(asn).cloned())
423    }
424
425    /// Checks if two ASNs are siblings (belong to the same organization).
426    ///
427    /// # Arguments
428    ///
429    /// * `asn1` - The first Autonomous System Number.
430    /// * `asn2` - The second Autonomous System Number.
431    ///
432    /// # Returns
433    ///
434    /// - `Ok(bool)`: True if the ASNs are siblings, false otherwise.
435    /// - `Err`: If the asinfo is not loaded or not loaded with as2org data.
436    ///
437    /// # Examples
438    ///
439    /// ```no_run
440    /// use bgpkit_commons::BgpkitCommons;
441    ///
442    /// let mut bgpkit = BgpkitCommons::new();
443    /// bgpkit.load_asinfo(true, false, false, false).unwrap();
444    /// let are_siblings = bgpkit.asinfo_are_siblings(3333, 3334).unwrap();
445    /// ```
446    ///
447    /// # Note
448    ///
449    /// This function requires the asinfo to be loaded with as2org data.
450    pub fn asinfo_are_siblings(&self, asn1: u32, asn2: u32) -> Result<bool> {
451        if self.asinfo.is_none() {
452            return Err(BgpkitCommonsError::module_not_loaded(
453                modules::ASINFO,
454                load_methods::LOAD_ASINFO,
455            ));
456        }
457        if !self.asinfo.as_ref().unwrap().load_as2org {
458            return Err(BgpkitCommonsError::module_not_configured(
459                modules::ASINFO,
460                "as2org data",
461                "load_asinfo() with as2org=true",
462            ));
463        }
464
465        let info_1_opt = self.asinfo_get(asn1)?;
466        let info_2_opt = self.asinfo_get(asn2)?;
467
468        if let (Some(info1), Some(info2)) = (info_1_opt, info_2_opt) {
469            if let (Some(org1), Some(org2)) = (info1.as2org, info2.as2org) {
470                let org_id_1 = org1.org_id;
471                let org_id_2 = org2.org_id;
472
473                return Ok(org_id_1 == org_id_2
474                    || self
475                        .asinfo
476                        .as_ref()
477                        .and_then(|a| a.sibling_orgs.as_ref())
478                        .map(|s| s.are_sibling_orgs(org_id_1.as_str(), org_id_2.as_str()))
479                        .unwrap_or(false));
480            }
481        }
482        Ok(false)
483    }
484}