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}