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