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}