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}