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