1mod as2org;
87mod hegemony;
88mod population;
89mod sibling_orgs;
90
91use crate::errors::{data_sources, load_methods, modules};
92use crate::peeringdb::{Network, Peeringdb};
93use crate::{BgpkitCommons, BgpkitCommonsError, LazyLoadable, Result};
94use ipnet::{IpNet, Ipv4Net, Ipv6Net};
95use serde::{Deserialize, Serialize};
96use sibling_orgs::SiblingOrgsUtils;
97use std::collections::HashMap;
98use std::io::{BufRead, Read};
99use tracing::{info, warn};
100
101pub use hegemony::HegemonyData;
102pub use population::AsnPopulationData;
103
104#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct DelegatedInfo {
112 pub registry: String,
114 pub country: String,
116 pub date: String,
118 pub status: String,
120}
121
122#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct IrrAsnInfo {
134 pub as_name: String,
136 pub descr: Vec<String>,
138 pub source: String,
140 pub mnt_by: Vec<String>,
142 pub route_prefixes: Vec<Ipv4Net>,
144 pub route6_prefixes: Vec<Ipv6Net>,
146 pub member_of_sets: Vec<String>,
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
151pub struct AsInfo {
152 pub asn: u32,
153 pub name: String,
154 pub country: String,
155 #[serde(default, skip_serializing_if = "Option::is_none")]
158 pub as2org: Option<As2orgInfo>,
159 #[serde(default, skip_serializing_if = "Option::is_none")]
160 pub population: Option<AsnPopulationData>,
161 #[serde(default, skip_serializing_if = "Option::is_none")]
162 pub hegemony: Option<HegemonyData>,
163 #[serde(default, skip_serializing_if = "Option::is_none")]
164 pub peeringdb: Option<Network>,
165 #[serde(default, skip_serializing_if = "Option::is_none")]
167 pub delegated: Option<DelegatedInfo>,
168 #[serde(default, skip_serializing_if = "Vec::is_empty")]
171 pub irr: Vec<IrrAsnInfo>,
172}
173
174impl AsInfo {
175 pub fn get_preferred_name(&self) -> String {
184 if let Some(peeringdb_data) = &self.peeringdb {
185 if let Some(name) = &peeringdb_data.name {
186 if !name.is_empty() {
187 return name.clone();
188 }
189 }
190 }
191 if let Some(as2org_info) = &self.as2org {
192 if !as2org_info.org_name.is_empty() {
193 return as2org_info.org_name.clone();
194 }
195 }
196 self.name.clone()
197 }
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize)]
201pub struct As2orgInfo {
202 pub name: String,
203 pub country: String,
204 pub org_id: String,
205 pub org_name: String,
206}
207
208const RIPE_RIS_ASN_TXT_URL: &str = "https://ftp.ripe.net/ripe/asnames/asn.txt";
209const BGPKIT_ASN_TXT_MIRROR_URL: &str = "https://data.bgpkit.com/commons/asn.txt";
210const BGPKIT_ASNINFO_URL: &str = "https://data.bgpkit.com/commons/asinfo.jsonl";
211
212#[derive(Debug, Clone, Default)]
231pub struct IrrSourceConfig {
232 sources: Vec<String>,
234}
235
236impl IrrSourceConfig {
237 pub fn sources(names: &[&str]) -> Result<Self> {
239 Self::only(names)
240 }
241
242 pub fn only(names: &[&str]) -> Result<Self> {
244 if names.is_empty() {
245 return Err(BgpkitCommonsError::invalid_format(
246 "IRR source selection",
247 "[]",
248 "explicit source selection must not be empty",
249 ));
250 }
251 let selected = crate::irr::sources_by_name(names)?;
252 Ok(Self {
253 sources: selected
254 .into_iter()
255 .map(|source| source.name.to_string())
256 .collect(),
257 })
258 }
259
260 pub fn all() -> Self {
262 Self {
263 sources: Vec::new(),
264 }
265 }
266
267 fn resolve(&self) -> Result<Vec<crate::irr::IrrSource>> {
269 if self.sources.is_empty() {
270 Ok(crate::irr::all_sources())
271 } else {
272 let names = self.sources.iter().map(String::as_str).collect::<Vec<_>>();
273 crate::irr::sources_by_name(&names)
274 }
275 }
276}
277
278#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
301pub enum AsInfoProfile {
302 Minimum,
304
305 #[default]
308 Default,
309
310 Full,
313}
314
315impl AsInfoProfile {
316 pub fn builder(self) -> AsInfoBuilder {
318 match self {
319 AsInfoProfile::Minimum => AsInfoBuilder::new(),
320 AsInfoProfile::Default => AsInfoBuilder::new()
321 .with_as2org()
322 .with_population()
323 .with_hegemony()
324 .with_peeringdb(),
325 AsInfoProfile::Full => AsInfoBuilder::new()
326 .with_as2org()
327 .with_population()
328 .with_hegemony()
329 .with_peeringdb()
330 .with_delegated()
331 .with_irr()
332 .with_irr_route_prefixes(),
333 }
334 }
335}
336
337#[derive(Default)]
369pub struct AsInfoBuilder {
370 load_as2org: bool,
371 load_population: bool,
372 load_hegemony: bool,
373 load_peeringdb: bool,
374 load_delegated: bool,
375 load_irr: bool,
376 irr_config: IrrSourceConfig,
377 irr_route_prefixes: bool,
378}
379
380impl AsInfoBuilder {
381 pub fn new() -> Self {
383 Self::default()
384 }
385
386 pub fn with_as2org(mut self) -> Self {
388 self.load_as2org = true;
389 self
390 }
391
392 pub fn with_population(mut self) -> Self {
394 self.load_population = true;
395 self
396 }
397
398 pub fn with_hegemony(mut self) -> Self {
400 self.load_hegemony = true;
401 self
402 }
403
404 pub fn with_peeringdb(mut self) -> Self {
406 self.load_peeringdb = true;
407 self
408 }
409
410 pub fn with_delegated(mut self) -> Self {
413 self.load_delegated = true;
414 self
415 }
416
417 pub fn with_irr(mut self) -> Self {
419 self.load_irr = true;
420 self
421 }
422
423 pub fn with_irr_sources(mut self, config: IrrSourceConfig) -> Self {
436 self.load_irr = true;
437 self.irr_config = config;
438 self
439 }
440
441 pub fn with_irr_route_prefixes(mut self) -> Self {
447 self.irr_route_prefixes = true;
448 self
449 }
450
451 pub fn with_all(mut self) -> Self {
453 self.load_as2org = true;
454 self.load_population = true;
455 self.load_hegemony = true;
456 self.load_peeringdb = true;
457 self.load_delegated = true;
458 self.load_irr = true;
459 self.irr_config = IrrSourceConfig::all();
460 self.irr_route_prefixes = true;
461 self
462 }
463
464 pub fn build(self) -> Result<AsInfoUtils> {
466 AsInfoUtils::from_builder(&self)
467 }
468
469 fn config(&self) -> Result<AsInfoLoadConfig> {
471 Ok(AsInfoLoadConfig {
472 load_as2org: self.load_as2org,
473 load_population: self.load_population,
474 load_hegemony: self.load_hegemony,
475 load_peeringdb: self.load_peeringdb,
476 load_delegated: self.load_delegated,
477 load_irr: self.load_irr,
478 irr_sources: self.irr_config.resolve()?,
479 irr_route_prefixes: self.irr_route_prefixes,
480 })
481 }
482}
483
484#[derive(Debug, Clone)]
486struct AsInfoLoadConfig {
487 load_as2org: bool,
488 load_population: bool,
489 load_hegemony: bool,
490 load_peeringdb: bool,
491 load_delegated: bool,
492 load_irr: bool,
493 irr_sources: Vec<crate::irr::IrrSource>,
494 irr_route_prefixes: bool,
495}
496
497pub struct AsInfoUtils {
498 pub asinfo_map: HashMap<u32, AsInfo>,
499 pub sibling_orgs: Option<SiblingOrgsUtils>,
500 config: AsInfoLoadConfig,
501}
502
503impl AsInfoUtils {
504 fn from_builder(builder: &AsInfoBuilder) -> Result<Self> {
506 let config = builder.config()?;
507 let asinfo_map = get_asinfo_map(&config)?;
508 let sibling_orgs = if config.load_as2org {
509 Some(SiblingOrgsUtils::new()?)
510 } else {
511 None
512 };
513 Ok(AsInfoUtils {
514 asinfo_map,
515 sibling_orgs,
516 config,
517 })
518 }
519
520 pub fn new_from_cached() -> Result<Self> {
521 let asinfo_map = get_asinfo_map_cached()?;
522 let sibling_orgs = Some(SiblingOrgsUtils::new()?);
523 Ok(AsInfoUtils {
524 asinfo_map,
525 sibling_orgs,
526 config: AsInfoLoadConfig {
527 load_as2org: true,
528 load_population: true,
529 load_hegemony: true,
530 load_peeringdb: true,
531 load_delegated: true,
532 load_irr: true,
533 irr_sources: crate::irr::all_sources(),
534 irr_route_prefixes: false,
535 },
536 })
537 }
538
539 pub fn reload(&mut self) -> Result<()> {
540 self.asinfo_map = get_asinfo_map(&self.config)?;
541 Ok(())
542 }
543
544 pub fn get(&self, asn: u32) -> Option<&AsInfo> {
545 self.asinfo_map.get(&asn)
546 }
547}
548
549impl LazyLoadable for AsInfoUtils {
550 fn reload(&mut self) -> Result<()> {
551 self.reload()
552 }
553
554 fn is_loaded(&self) -> bool {
555 !self.asinfo_map.is_empty()
556 }
557
558 fn loading_status(&self) -> &'static str {
559 if self.is_loaded() {
560 "ASInfo data loaded"
561 } else {
562 "ASInfo data not loaded"
563 }
564 }
565}
566
567pub fn get_asinfo_map_cached() -> Result<HashMap<u32, AsInfo>> {
568 info!("loading asinfo from previously generated BGPKIT cache file...");
569 let mut asnames_map = HashMap::new();
570 let reader = oneio::get_reader(BGPKIT_ASNINFO_URL)?;
571 for line in std::io::BufReader::new(reader).lines() {
572 let line = line?;
573 if line.trim().is_empty() {
574 continue;
575 }
576 let asinfo: AsInfo = serde_json::from_str(&line)?;
577 asnames_map.insert(asinfo.asn, asinfo);
578 }
579 Ok(asnames_map)
580}
581
582fn project_delegated_record(
589 record: crate::delegated::DelegatedRecord,
590 map: &mut HashMap<u32, DelegatedInfo>,
591) {
592 if record.record_type != "asn" {
593 return;
594 }
595 let status = record.status.trim();
596 if status != "allocated" && status != "assigned" {
597 return;
598 }
599 let cc = record.country.trim();
600 if cc.is_empty() || cc == "*" {
601 return;
602 }
603 let (Ok(start), Ok(count)) = (record.start.parse::<u64>(), record.value.parse::<u64>()) else {
604 return;
605 };
606 let registry = record.registry.trim().to_lowercase();
607 let country = cc.to_uppercase();
608 let date = record.date.trim().to_string();
609 for asn in start..start.saturating_add(count) {
610 if asn > u32::MAX as u64 {
611 break;
612 }
613 let asn = asn as u32;
614 if (64512..=65534).contains(&asn) || asn >= 4_200_000_000 {
615 continue;
616 }
617 map.entry(asn).or_insert(DelegatedInfo {
618 registry: registry.clone(),
619 country: country.clone(),
620 date: date.clone(),
621 status: status.to_string(),
622 });
623 }
624}
625
626#[cfg(test)]
627fn project_delegated_stats(text: &str, map: &mut HashMap<u32, DelegatedInfo>) {
628 for record in crate::delegated::parse_reader(text.as_bytes()).flatten() {
629 project_delegated_record(record, map);
630 }
631}
632
633#[allow(clippy::type_complexity)]
637fn lookup_enrichment(
638 asn: u32,
639 as2org_utils: Option<&as2org::As2org>,
640 population_utils: Option<&population::AsnPopulation>,
641 hegemony_utils: Option<&hegemony::Hegemony>,
642 peeringdb_utils: Option<&Peeringdb>,
643) -> (
644 Option<As2orgInfo>,
645 Option<AsnPopulationData>,
646 Option<HegemonyData>,
647 Option<Network>,
648) {
649 let as2org = as2org_utils.and_then(|as2org_data| {
650 as2org_data.get_as_info(asn).map(|info| As2orgInfo {
651 name: info.name.clone(),
652 country: info.country_code.clone(),
653 org_id: info.org_id.clone(),
654 org_name: info.org_name.clone(),
655 })
656 });
657 let population = population_utils.and_then(|p| p.get(asn));
658 let hegemony = hegemony_utils.and_then(|h| h.get_score(asn).cloned());
659 let peeringdb = peeringdb_utils.and_then(|h| h.get_network(asn).cloned());
660 (as2org, population, hegemony, peeringdb)
661}
662
663fn fill_delegated_data(
673 asnames_map: &mut HashMap<u32, AsInfo>,
674 as2org_utils: Option<&as2org::As2org>,
675 population_utils: Option<&population::AsnPopulation>,
676 hegemony_utils: Option<&hegemony::Hegemony>,
677 peeringdb_utils: Option<&Peeringdb>,
678) {
679 let mut delegated: HashMap<u32, DelegatedInfo> = HashMap::new();
680 for url in crate::delegated::RIR_DELEGATED_STATS_URLS {
681 match crate::delegated::fetch(url) {
682 Ok(reader) => {
683 for record in crate::delegated::parse_reader(reader) {
684 match record {
685 Ok(record) => project_delegated_record(record, &mut delegated),
686 Err(e) => warn!("failed to parse delegated stats from {url}: {e}"),
687 }
688 }
689 }
690 Err(e) => warn!("failed to load delegated stats from {}: {}", url, e),
691 }
692 }
693 attach_delegated_data(
694 asnames_map,
695 delegated,
696 as2org_utils,
697 population_utils,
698 hegemony_utils,
699 peeringdb_utils,
700 );
701}
702
703fn attach_delegated_data(
713 asnames_map: &mut HashMap<u32, AsInfo>,
714 delegated: HashMap<u32, DelegatedInfo>,
715 as2org_utils: Option<&as2org::As2org>,
716 population_utils: Option<&population::AsnPopulation>,
717 hegemony_utils: Option<&hegemony::Hegemony>,
718 peeringdb_utils: Option<&Peeringdb>,
719) {
720 let mut new_entries = 0usize;
721 let mut attached = 0usize;
722 for (asn, delegated_info) in delegated {
723 asnames_map
724 .entry(asn)
725 .and_modify(|info| {
726 info.delegated = Some(delegated_info.clone());
727 attached += 1;
728 })
729 .or_insert_with(|| {
730 new_entries += 1;
731 let (as2org, population, hegemony, peeringdb) = lookup_enrichment(
732 asn,
733 as2org_utils,
734 population_utils,
735 hegemony_utils,
736 peeringdb_utils,
737 );
738 AsInfo {
739 asn,
740 name: "UNKNOWN".to_string(),
741 country: delegated_info.country.clone(),
742 as2org,
743 population,
744 hegemony,
745 peeringdb,
746 delegated: Some(delegated_info.clone()),
747 irr: Vec::new(),
748 }
749 });
750 }
751 info!(
752 "delegated stats: {attached} existing entries enriched, {new_entries} new entries created"
753 );
754}
755fn enrich_from_irr(
769 asnames_map: &mut HashMap<u32, AsInfo>,
770 irr_sources: &[crate::irr::IrrSource],
771 collect_route_prefixes: bool,
772) {
773 use crate::irr::sources::DumpFormat;
774 use crate::irr::types::{IrrObject, IrrObjectType};
775 use std::collections::HashMap as StdMap;
776
777 let mut per_source: StdMap<String, StdMap<u32, IrrAsnInfoBuilder>> = StdMap::new();
779
780 let mut parsed_urls: std::collections::HashSet<String> = std::collections::HashSet::new();
782
783 let wanted_types: Vec<IrrObjectType> = if collect_route_prefixes {
784 vec![
785 IrrObjectType::AutNum,
786 IrrObjectType::Route,
787 IrrObjectType::Route6,
788 IrrObjectType::AsSet,
789 ]
790 } else {
791 vec![IrrObjectType::AutNum, IrrObjectType::AsSet]
795 };
796
797 for source in irr_sources.iter().cloned() {
798 let source_name = source.name.to_string();
799
800 let mut urls_to_parse: Vec<(String, Vec<IrrObjectType>)> = Vec::new();
804
805 if source.format == DumpFormat::WholeDb {
806 let url = source.dump_urls(IrrObjectType::AutNum);
808 if let Some(dump) = url.first() {
809 urls_to_parse.push((dump.url.clone(), wanted_types.to_vec()));
810 }
811 } else {
812 for obj_type in &wanted_types {
814 for dump in source.dump_urls(*obj_type) {
815 urls_to_parse.push((dump.url.clone(), vec![*obj_type]));
816 }
817 }
818 }
819
820 for (url, _types_for_url) in urls_to_parse {
821 if parsed_urls.contains(&url) {
822 continue;
823 }
824 parsed_urls.insert(url.clone());
825
826 let sn = source_name.clone();
827
828 match crate::irr::parse_dump(
829 &crate::irr::IrrDumpUrl {
830 url: url.clone(),
831 transport: source.transport,
832 format: source.format,
833 },
834 |obj| {
835 let source_map = per_source.entry(sn.clone()).or_default();
836 match &obj {
837 IrrObject::AutNum(a) => {
838 let entry = source_map.entry(a.asn).or_default();
839 entry.source = a.source.clone();
840 entry.as_name = a.as_name.clone();
841 entry.descr = a.descr.clone();
842 if let Some(mnt) = a.extra.get("mnt-by") {
843 entry.mnt_by = mnt.clone();
844 }
845 }
846 IrrObject::Route(r) if collect_route_prefixes => {
851 let entry = source_map.entry(r.origin).or_default();
852 if entry.source.is_empty() {
853 entry.source = r.source.clone();
854 }
855 if let IpNet::V4(prefix) = r.prefix {
856 entry.route_prefixes.push(prefix);
857 }
858 }
859 IrrObject::Route6(r) if collect_route_prefixes => {
860 let entry = source_map.entry(r.origin).or_default();
861 if entry.source.is_empty() {
862 entry.source = r.source.clone();
863 }
864 if let IpNet::V6(prefix) = r.prefix {
865 entry.route6_prefixes.push(prefix);
866 }
867 }
868 IrrObject::AsSet(s) => {
869 let set_name = s.name.clone();
870 for &member_asn in &s.members {
871 let entry = source_map.entry(member_asn).or_default();
872 if entry.source.is_empty() {
873 entry.source = s.source.clone();
874 }
875 entry.member_of_sets.push(set_name.clone());
876 }
877 }
878 _ => {}
879 }
880 },
881 ) {
882 Ok(stats) => info!(
883 "IRR from {source_name} ({url}): {} objects extracted",
884 stats.extracted
885 ),
886 Err(e) => warn!("failed to load IRR from {source_name} ({url}): {e}"),
887 }
888 }
889 }
890
891 attach_irr_data(asnames_map, per_source, irr_sources);
892}
893
894fn attach_irr_data(
900 asnames_map: &mut HashMap<u32, AsInfo>,
901 per_source: std::collections::HashMap<
902 String,
903 std::collections::HashMap<u32, IrrAsnInfoBuilder>,
904 >,
905 irr_sources: &[crate::irr::IrrSource],
906) {
907 let mut irr_attached = 0usize;
908
909 for (asn, info) in asnames_map.iter_mut() {
910 let mut irr_entries: Vec<IrrAsnInfo> = Vec::new();
911
912 for source in irr_sources.iter().cloned() {
913 if let Some(source_map) = per_source.get(source.name) {
914 if let Some(builder) = source_map.get(asn) {
915 irr_entries.push(builder.clone().build());
916 }
917 }
918 }
919
920 if !irr_entries.is_empty() {
921 info.irr = irr_entries;
922 irr_attached += 1;
923 }
924 }
925
926 info!("IRR data attached to {irr_attached} ASNs");
927}
928
929#[derive(Debug, Clone, Default)]
932struct IrrAsnInfoBuilder {
933 as_name: String,
934 descr: Vec<String>,
935 source: String,
936 mnt_by: Vec<String>,
937 route_prefixes: Vec<Ipv4Net>,
938 route6_prefixes: Vec<Ipv6Net>,
939 member_of_sets: Vec<String>,
940}
941
942impl IrrAsnInfoBuilder {
943 fn build(self) -> IrrAsnInfo {
944 IrrAsnInfo {
945 as_name: self.as_name,
946 descr: self.descr,
947 source: self.source,
948 mnt_by: self.mnt_by,
949 route_prefixes: self.route_prefixes,
950 route6_prefixes: self.route6_prefixes,
951 member_of_sets: self.member_of_sets,
952 }
953 }
954}
955
956fn get_asinfo_map(config: &AsInfoLoadConfig) -> Result<HashMap<u32, AsInfo>> {
971 let load_as2org = config.load_as2org;
972 let load_population = config.load_population;
973 let load_hegemony = config.load_hegemony;
974 let load_peeringdb = config.load_peeringdb;
975 let read_text = |url: &str| -> Result<String> {
976 let mut text = String::new();
977 oneio::get_reader(url)?.read_to_string(&mut text)?;
978 Ok(text)
979 };
980 let text = match read_text(BGPKIT_ASN_TXT_MIRROR_URL) {
981 Ok(t) => t,
982 Err(_) => match read_text(RIPE_RIS_ASN_TXT_URL) {
983 Ok(t) => t,
984 Err(e) => {
985 return Err(BgpkitCommonsError::data_source_error(
986 data_sources::BGPKIT,
987 format!(
988 "error reading asinfo (neither mirror or original works): {}",
989 e
990 ),
991 ));
992 }
993 },
994 };
995
996 let as2org_utils = if load_as2org {
997 info!("loading as2org data from CAIDA...");
998 match as2org::As2org::new(None) {
999 Ok(data) => Some(data),
1000 Err(e) => {
1001 warn!("failed to load as2org data, proceeding without it: {e}");
1002 None
1003 }
1004 }
1005 } else {
1006 None
1007 };
1008 let population_utils = if load_population {
1009 info!("loading ASN population data from APNIC...");
1010 match population::AsnPopulation::new() {
1011 Ok(data) => Some(data),
1012 Err(e) => {
1013 warn!("failed to load population data, proceeding without it: {e}");
1014 None
1015 }
1016 }
1017 } else {
1018 None
1019 };
1020 let hegemony_utils = if load_hegemony {
1021 info!("loading IIJ IHR hegemony score data from BGPKIT mirror...");
1022 match hegemony::Hegemony::new() {
1023 Ok(data) => Some(data),
1024 Err(e) => {
1025 warn!("failed to load hegemony data, proceeding without it: {e}");
1026 None
1027 }
1028 }
1029 } else {
1030 None
1031 };
1032 let peeringdb_utils = if load_peeringdb {
1033 info!("loading peeringdb data...");
1034 match Peeringdb::new_networks_only() {
1035 Ok(data) => Some(data),
1036 Err(e) => {
1037 warn!(
1038 "failed to load peeringdb data, proceeding without it: {e} \
1039 (hint: set PEERINGDB_API_KEY to avoid rate limiting)"
1040 );
1041 None
1042 }
1043 }
1044 } else {
1045 None
1046 };
1047
1048 let asnames = text
1049 .lines()
1050 .filter_map(|line| {
1051 let (asn_str, name_country_str) = match line.split_once(' ') {
1052 Some((asn, name)) => (asn, name),
1053 None => return None,
1054 };
1055 let (name_str, country_str) = match name_country_str.rsplit_once(", ") {
1056 Some((name, country)) => (name, country),
1057 None => return None,
1058 };
1059 let asn = asn_str.parse::<u32>().unwrap();
1060 let (as2org, population, hegemony, peeringdb) = lookup_enrichment(
1061 asn,
1062 as2org_utils.as_ref(),
1063 population_utils.as_ref(),
1064 hegemony_utils.as_ref(),
1065 peeringdb_utils.as_ref(),
1066 );
1067 Some(AsInfo {
1068 asn,
1069 name: name_str.to_string(),
1070 country: country_str.to_string(),
1071 as2org,
1072 population,
1073 hegemony,
1074 peeringdb,
1075 delegated: None,
1076 irr: Vec::new(),
1077 })
1078 })
1079 .collect::<Vec<AsInfo>>();
1080
1081 let mut asnames_map = HashMap::new();
1082 for asname in asnames {
1083 asnames_map.insert(asname.asn, asname);
1084 }
1085
1086 if config.load_delegated {
1087 info!("loading delegated stats data...");
1088 fill_delegated_data(
1089 &mut asnames_map,
1090 as2org_utils.as_ref(),
1091 population_utils.as_ref(),
1092 hegemony_utils.as_ref(),
1093 peeringdb_utils.as_ref(),
1094 );
1095 }
1096
1097 if config.load_irr {
1098 info!("enriching from IRR data...");
1099 enrich_from_irr(
1100 &mut asnames_map,
1101 &config.irr_sources,
1102 config.irr_route_prefixes,
1103 );
1104 }
1105
1106 Ok(asnames_map)
1107}
1108
1109impl BgpkitCommons {
1110 pub fn asinfo_all(&self) -> Result<HashMap<u32, AsInfo>> {
1127 if self.asinfo.is_none() {
1128 return Err(BgpkitCommonsError::module_not_loaded(
1129 modules::ASINFO,
1130 load_methods::LOAD_ASINFO,
1131 ));
1132 }
1133
1134 Ok(self.asinfo.as_ref().unwrap().asinfo_map.clone())
1135 }
1136
1137 pub fn asinfo_get(&self, asn: u32) -> Result<Option<AsInfo>> {
1159 if self.asinfo.is_none() {
1160 return Err(BgpkitCommonsError::module_not_loaded(
1161 modules::ASINFO,
1162 load_methods::LOAD_ASINFO,
1163 ));
1164 }
1165
1166 Ok(self.asinfo.as_ref().unwrap().get(asn).cloned())
1167 }
1168
1169 pub fn asinfo_are_siblings(&self, asn1: u32, asn2: u32) -> Result<bool> {
1195 if self.asinfo.is_none() {
1196 return Err(BgpkitCommonsError::module_not_loaded(
1197 modules::ASINFO,
1198 load_methods::LOAD_ASINFO,
1199 ));
1200 }
1201 if !self.asinfo.as_ref().unwrap().config.load_as2org {
1202 return Err(BgpkitCommonsError::module_not_configured(
1203 modules::ASINFO,
1204 "as2org data",
1205 "load_asinfo() with as2org=true",
1206 ));
1207 }
1208
1209 let info_1_opt = self.asinfo_get(asn1)?;
1210 let info_2_opt = self.asinfo_get(asn2)?;
1211
1212 if let (Some(info1), Some(info2)) = (info_1_opt, info_2_opt) {
1213 if let (Some(org1), Some(org2)) = (info1.as2org, info2.as2org) {
1214 let org_id_1 = org1.org_id;
1215 let org_id_2 = org2.org_id;
1216
1217 return Ok(org_id_1 == org_id_2
1218 || self
1219 .asinfo
1220 .as_ref()
1221 .and_then(|a| a.sibling_orgs.as_ref())
1222 .map(|s| s.are_sibling_orgs(org_id_1.as_str(), org_id_2.as_str()))
1223 .unwrap_or(false));
1224 }
1225 }
1226 Ok(false)
1227 }
1228}
1229
1230#[cfg(test)]
1231mod tests {
1232 use super::*;
1233
1234 fn cc(map: &HashMap<u32, DelegatedInfo>, asn: u32) -> Option<&str> {
1236 map.get(&asn).map(|d| d.country.as_str())
1237 }
1238
1239 #[test]
1240 fn test_parse_delegated_stats_basic() {
1241 let text = "\
12422|ripencc|ZZ|209|20250704|00000000+00000000+00000000|UTF-8
1243ripencc|*|asn|*|39634|summary
1244ripencc|GB|asn|219157|1|20260722|allocated
1245ripencc|DE|asn|219125|1|20260728|allocated
1246arin||asn|212|1||reserved|
1247arin|*|asn|*|32843|summary
1248arin|US|asn|402598|1|20260604|assigned|
1249apnic|BD|asn|154708|1|20260609|allocated
1250ripencc|NL|asn|1000|4|19970901|allocated
1251ripencc|NL|ipv4|185.0.0.0|65536|20000101|allocated
1252";
1253 let mut map = HashMap::new();
1254 project_delegated_stats(text, &mut map);
1255 assert_eq!(cc(&map, 219157), Some("GB"));
1256 assert_eq!(cc(&map, 219125), Some("DE"));
1257 assert_eq!(cc(&map, 402598), Some("US"));
1258 assert_eq!(cc(&map, 154708), Some("BD"));
1259 assert_eq!(cc(&map, 1000), Some("NL"));
1261 assert_eq!(cc(&map, 1003), Some("NL"));
1262 assert!(!map.contains_key(&1004));
1263 assert!(!map.contains_key(&212));
1265 assert_eq!(map.len(), 8);
1267 let info = &map[&219157];
1269 assert_eq!(info.registry, "ripencc");
1270 assert_eq!(info.status, "allocated");
1271 assert_eq!(info.date, "20260722");
1272 }
1273
1274 #[test]
1275 fn test_parse_delegated_stats_skips_private_and_invalid() {
1276 let text = "\
1277arin|US|asn|64512|1023|19891201|reserved
1278arin|US|asn|4200000000|9999|19891201|reserved
1279arin|US|asn|notanumber|1|20200101|allocated
1280arin|US|asn|123|notacount|20200101|allocated
1281";
1282 let mut map = HashMap::new();
1283 project_delegated_stats(text, &mut map);
1284 assert!(map.is_empty());
1285 }
1286
1287 #[test]
1288 fn test_parse_delegated_stats_status_filter() {
1289 let text = "\
1292arin|US|asn|300000|1|20200101|reserved
1293arin|US|asn|300001|1|20200101|available
1294arin|US|asn|300002|1|20200101|allocated
1295arin|US|asn|300003|1|20200101|assigned
1296";
1297 let mut map = HashMap::new();
1298 project_delegated_stats(text, &mut map);
1299 assert!(!map.contains_key(&300000));
1300 assert!(!map.contains_key(&300001));
1301 assert_eq!(cc(&map, 300002), Some("US"));
1302 assert_eq!(cc(&map, 300003), Some("US"));
1303 assert_eq!(map.len(), 2);
1304 }
1305
1306 #[test]
1307 fn test_parse_delegated_stats_private_boundary() {
1308 let text = "\
1312arin|US|asn|65535|1|19891201|allocated
1313arin|US|asn|65534|1|19891201|allocated
1314arin|US|asn|64496|1|19891201|allocated
1315arin|US|asn|4199999999|1|19891201|allocated
1316arin|US|asn|4200000000|1|19891201|allocated
1317";
1318 let mut map = HashMap::new();
1319 project_delegated_stats(text, &mut map);
1320 assert_eq!(cc(&map, 65535), Some("US"));
1321 assert!(!map.contains_key(&65534));
1322 assert_eq!(cc(&map, 64496), Some("US"));
1323 assert_eq!(cc(&map, 4199999999), Some("US"));
1324 assert!(!map.contains_key(&4200000000));
1325 }
1326
1327 #[test]
1328 fn test_parse_delegated_stats_case_normalization() {
1329 let text = "lacnic|br|asn|269000|1|20150101|allocated\n";
1330 let mut map = HashMap::new();
1331 project_delegated_stats(text, &mut map);
1332 assert_eq!(cc(&map, 269000), Some("BR"));
1333 assert_eq!(map[&269000].registry, "lacnic");
1334 }
1335
1336 #[test]
1337 fn test_parse_delegated_stats_malformed_lines() {
1338 let text = "\
1339# comment line
1340
1341ripencc|GB|asn
1342ripencc|GB|ipv6|2001:db8::|32|20200101|allocated
1343some garbage line with no pipes at all
1344|GB|asn|100|1|20200101|allocated
1345ripencc|GB|asn|100|1|20200101
1346ripencc|GB|asn|100|1|20200101|allocated|extra|fields|ok
1347";
1348 let mut map = HashMap::new();
1349 project_delegated_stats(text, &mut map);
1350 assert_eq!(cc(&map, 100), Some("GB"));
1353 assert_eq!(map.len(), 1);
1354 }
1355
1356 #[test]
1357 fn test_profiles_match_asninfo_v1_and_full_uses_all_sources() {
1358 let minimum = AsInfoProfile::Minimum.builder().config().unwrap();
1359 assert!(!minimum.load_as2org);
1360 assert!(!minimum.load_population);
1361 assert!(!minimum.load_hegemony);
1362 assert!(!minimum.load_peeringdb);
1363 assert!(!minimum.load_delegated);
1364 assert!(!minimum.load_irr);
1365
1366 let default = AsInfoProfile::Default.builder().config().unwrap();
1367 assert!(default.load_as2org);
1368 assert!(default.load_population);
1369 assert!(default.load_hegemony);
1370 assert!(default.load_peeringdb);
1371 assert!(!default.load_delegated);
1372 assert!(!default.load_irr);
1373
1374 let full = AsInfoProfile::Full.builder().config().unwrap();
1375 assert!(full.load_delegated);
1376 assert!(full.load_irr);
1377 assert!(full.irr_route_prefixes);
1378 assert_eq!(full.irr_sources.len(), crate::irr::all_sources().len());
1379
1380 let all = AsInfoBuilder::new().with_all().config().unwrap();
1381 assert!(all.irr_route_prefixes);
1382 assert_eq!(all.irr_sources.len(), crate::irr::all_sources().len());
1383 }
1384
1385 #[test]
1386 fn test_custom_irr_sources_are_validated() {
1387 assert!(IrrSourceConfig::only(&[]).is_err());
1388 assert!(IrrSourceConfig::sources(&[]).is_err());
1389 assert!(IrrSourceConfig::sources(&["RIPE", "NOT-A-REGISTRY"]).is_err());
1390
1391 let selected = IrrSourceConfig::sources(&["RIPE", "RADB"]).unwrap();
1392 let config = AsInfoBuilder::new()
1393 .with_irr_sources(selected)
1394 .config()
1395 .unwrap();
1396 assert_eq!(
1397 config
1398 .irr_sources
1399 .iter()
1400 .map(|source| source.name)
1401 .collect::<Vec<_>>(),
1402 vec!["RIPE", "RADB"]
1403 );
1404 }
1405
1406 #[test]
1407 fn delegated_enrichment_never_overwrites_name_or_country() {
1408 let mut map = HashMap::new();
1409 map.insert(
1410 13335,
1411 AsInfo {
1412 asn: 13335,
1413 name: "CLOUDFLARENET".to_string(),
1414 country: "US".to_string(),
1415 as2org: None,
1416 population: None,
1417 hegemony: None,
1418 peeringdb: None,
1419 delegated: None,
1420 irr: Vec::new(),
1421 },
1422 );
1423
1424 let mut delegated = HashMap::new();
1425 delegated.insert(
1426 13335,
1427 DelegatedInfo {
1428 registry: "ripencc".to_string(),
1429 country: "GB".to_string(),
1430 date: "20260722".to_string(),
1431 status: "allocated".to_string(),
1432 },
1433 );
1434 delegated.insert(
1436 400644,
1437 DelegatedInfo {
1438 registry: "arin".to_string(),
1439 country: "US".to_string(),
1440 date: "20200101".to_string(),
1441 status: "allocated".to_string(),
1442 },
1443 );
1444
1445 attach_delegated_data(&mut map, delegated, None, None, None, None);
1446
1447 let existing = &map[&13335];
1449 assert_eq!(existing.name, "CLOUDFLARENET");
1450 assert_eq!(existing.country, "US");
1451 assert_eq!(existing.delegated.as_ref().unwrap().registry, "ripencc");
1452
1453 let new_entry = &map[&400644];
1455 assert_eq!(new_entry.name, "UNKNOWN");
1456 assert_eq!(new_entry.country, "US");
1457 assert_eq!(new_entry.delegated.as_ref().unwrap().registry, "arin");
1458 }
1459
1460 #[test]
1461 fn irr_enrichment_never_overwrites_name_or_country() {
1462 let mut map = HashMap::new();
1463 map.insert(
1464 13335,
1465 AsInfo {
1466 asn: 13335,
1467 name: "CLOUDFLARENET".to_string(),
1468 country: "US".to_string(),
1469 as2org: None,
1470 population: None,
1471 hegemony: None,
1472 peeringdb: None,
1473 delegated: None,
1474 irr: Vec::new(),
1475 },
1476 );
1477
1478 let mut per_source: std::collections::HashMap<
1481 String,
1482 std::collections::HashMap<u32, IrrAsnInfoBuilder>,
1483 > = std::collections::HashMap::new();
1484 let mut builder = IrrAsnInfoBuilder::default();
1485 builder.source = "RIPE".to_string();
1486 builder.as_name = "CLOUDFLARE-NET".to_string();
1487 per_source.insert("RIPE".to_string(), [(13335, builder)].into_iter().collect());
1488
1489 let ripe = crate::irr::sources::all_sources()
1490 .into_iter()
1491 .find(|source| source.name == "RIPE")
1492 .unwrap();
1493 attach_irr_data(&mut map, per_source, &[ripe]);
1494
1495 let info = &map[&13335];
1496 assert_eq!(info.name, "CLOUDFLARENET");
1497 assert_eq!(info.country, "US");
1498 assert_eq!(info.irr.len(), 1);
1499 assert_eq!(info.irr[0].as_name, "CLOUDFLARE-NET");
1500 assert_eq!(info.irr[0].source, "RIPE");
1501 }
1502}