diff --git a/CHANGELOG.md b/CHANGELOG.md index 195720f..608824c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ All notable changes to this project will be documented in this file. ### Code Improvements * **Data refresh logging**: CLI now shows specific reason for data refresh ("data is empty" vs "data is outdated") instead of generic "empty or outdated" message +* **AS name display**: ASN names are now displayed using a preferred source hierarchy: + * Priority order: PeeringDB `aka` → PeeringDB `name_long` → PeeringDB `name` → AS2Org `org_name` → AS2Org `name` → Core `name` + * This provides more recognizable, commonly-used AS names from PeeringDB when available + * Affects all commands that display AS names: `inspect`, `as2rel`, `rpki`, `pfx2as` ### Breaking Changes diff --git a/src/database/monocle/as2rel.rs b/src/database/monocle/as2rel.rs index 465f957..99476bd 100644 --- a/src/database/monocle/as2rel.rs +++ b/src/database/monocle/as2rel.rs @@ -229,7 +229,16 @@ impl<'a> As2relRepository<'a> { SELECT :asn as asn1, CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END as asn2, - o.org_name as asn2_name, + COALESCE( + NULLIF(p.aka, ''), + NULLIF(p.name_long, ''), + NULLIF(p.name, ''), + NULLIF(ai.org_name, ''), + NULLIF(ai.name, ''), + NULLIF(o.org_name, ''), + NULLIF(o.as_name, ''), + c.name + ) as asn2_name, MAX(CASE WHEN r.rel = 0 THEN r.peers_count ELSE 0 END) as connected_count, SUM(CASE WHEN r.asn1 = :asn AND r.rel = 1 THEN r.peers_count @@ -242,7 +251,14 @@ impl<'a> As2relRepository<'a> { ELSE 0 END) as as2_upstream_count FROM as2rel r - LEFT JOIN as2org_all o ON o.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END + LEFT JOIN asinfo_core c + ON c.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END + LEFT JOIN asinfo_as2org ai + ON ai.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END + LEFT JOIN asinfo_peeringdb p + ON p.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END + LEFT JOIN as2org_all o + ON o.asn = CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END WHERE r.asn1 = :asn OR r.asn2 = :asn GROUP BY CASE WHEN r.asn1 = :asn THEN r.asn2 ELSE r.asn1 END "#; @@ -277,7 +293,16 @@ impl<'a> As2relRepository<'a> { SELECT :asn1 as asn1, :asn2 as asn2, - o.org_name as asn2_name, + COALESCE( + NULLIF(p.aka, ''), + NULLIF(p.name_long, ''), + NULLIF(p.name, ''), + NULLIF(ai.org_name, ''), + NULLIF(ai.name, ''), + NULLIF(o.org_name, ''), + NULLIF(o.as_name, ''), + c.name + ) as asn2_name, MAX(CASE WHEN r.rel = 0 THEN r.peers_count ELSE 0 END) as connected_count, SUM(CASE WHEN r.asn1 = :asn1 AND r.rel = 1 THEN r.peers_count @@ -290,6 +315,9 @@ impl<'a> As2relRepository<'a> { ELSE 0 END) as as2_upstream_count FROM as2rel r + LEFT JOIN asinfo_core c ON c.asn = :asn2 + LEFT JOIN asinfo_as2org ai ON ai.asn = :asn2 + LEFT JOIN asinfo_peeringdb p ON p.asn = :asn2 LEFT JOIN as2org_all o ON o.asn = :asn2 WHERE (r.asn1 = :asn1 AND r.asn2 = :asn2) OR (r.asn1 = :asn2 AND r.asn2 = :asn1) "#; @@ -618,9 +646,21 @@ impl<'a> As2relRepository<'a> { SELECT h.customer_asn, h.visibility, - o.org_name + COALESCE( + NULLIF(p.aka, ''), + NULLIF(p.name_long, ''), + NULLIF(p.name, ''), + NULLIF(ai.org_name, ''), + NULLIF(ai.name, ''), + NULLIF(o.org_name, ''), + NULLIF(o.as_name, ''), + c.name + ) as asn_name FROM has_target_upstream h JOIN upstream_counts u ON h.customer_asn = u.customer_asn + LEFT JOIN asinfo_core c ON c.asn = h.customer_asn + LEFT JOIN asinfo_as2org ai ON ai.asn = h.customer_asn + LEFT JOIN asinfo_peeringdb p ON p.asn = h.customer_asn LEFT JOIN as2org_all o ON o.asn = h.customer_asn WHERE u.upstream_count = 1 AND h.visibility >= :min_peers @@ -855,7 +895,16 @@ impl<'a> As2relRepository<'a> { SELECT CASE WHEN r.asn1 < r.asn2 THEN r.asn1 ELSE r.asn2 END as asn1, CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END as asn2, - o.org_name as asn2_name, + COALESCE( + NULLIF(p.aka, ''), + NULLIF(p.name_long, ''), + NULLIF(p.name, ''), + NULLIF(ai.org_name, ''), + NULLIF(ai.name, ''), + NULLIF(o.org_name, ''), + NULLIF(o.as_name, ''), + c.name + ) as asn2_name, MAX(CASE WHEN r.rel = 0 THEN r.peers_count ELSE 0 END) as connected_count, SUM(CASE WHEN r.asn1 < r.asn2 AND r.rel = 1 THEN r.peers_count @@ -868,7 +917,14 @@ impl<'a> As2relRepository<'a> { ELSE 0 END) as as2_upstream_count FROM as2rel r - LEFT JOIN as2org_all o ON o.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END + LEFT JOIN asinfo_core c + ON c.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END + LEFT JOIN asinfo_as2org ai + ON ai.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END + LEFT JOIN asinfo_peeringdb p + ON p.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END + LEFT JOIN as2org_all o + ON o.asn = CASE WHEN r.asn1 < r.asn2 THEN r.asn2 ELSE r.asn1 END WHERE {} GROUP BY CASE WHEN r.asn1 < r.asn2 THEN r.asn1 ELSE r.asn2 END, diff --git a/src/database/monocle/asinfo.rs b/src/database/monocle/asinfo.rs index 6dc81d3..7327c48 100644 --- a/src/database/monocle/asinfo.rs +++ b/src/database/monocle/asinfo.rs @@ -695,6 +695,58 @@ impl<'a> AsinfoRepository<'a> { result } + /// Batch lookup of preferred AS names with source preference: + /// peeringdb.name_long/name -> as2org.org_name -> as2org.name -> core.name + pub fn lookup_preferred_names_batch(&self, asns: &[u32]) -> HashMap { + let mut result = HashMap::new(); + + if asns.is_empty() { + return result; + } + + let placeholders: Vec = asns.iter().map(|_| "?".to_string()).collect(); + let query = format!( + r#" + SELECT + c.asn, + COALESCE( + NULLIF(p.aka, ''), + NULLIF(p.name_long, ''), + NULLIF(p.name, ''), + NULLIF(a.org_name, ''), + NULLIF(a.name, ''), + c.name + ) AS preferred_name + FROM asinfo_core c + LEFT JOIN asinfo_as2org a ON c.asn = a.asn + LEFT JOIN asinfo_peeringdb p ON c.asn = p.asn + WHERE c.asn IN ({}) + "#, + placeholders.join(",") + ); + + if let Ok(mut stmt) = self.conn.prepare(&query) { + let params: Vec<&dyn rusqlite::ToSql> = + asns.iter().map(|a| a as &dyn rusqlite::ToSql).collect(); + + if let Ok(rows) = stmt.query_map(params.as_slice(), |row| { + Ok((row.get::<_, u32>(0)?, row.get::<_, String>(1)?)) + }) { + for row in rows.flatten() { + result.insert(row.0, row.1); + } + } + } + + result + } + + /// Lookup preferred AS name for a single ASN + pub fn lookup_preferred_name(&self, asn: u32) -> Option { + let mut result = self.lookup_preferred_names_batch(&[asn]); + result.remove(&asn) + } + /// Batch lookup of org names (from as2org table) pub fn lookup_orgs_batch(&self, asns: &[u32]) -> HashMap { let mut result = HashMap::new(); @@ -1026,6 +1078,104 @@ mod tests { assert!(names.get(&99999).is_none()); } + #[test] + fn test_lookup_preferred_names_batch() { + let db = setup_test_db(); + let repo = AsinfoRepository::new(&db.conn); + + let records = vec![ + JsonlRecord { + asn: 1, + name: "CORE1".to_string(), + country: "US".to_string(), + as2org: Some(JsonlAs2org { + country: "US".to_string(), + name: "AS2ORG_NAME1".to_string(), + org_id: "ORG1".to_string(), + org_name: "AS2ORG_ORG1".to_string(), + }), + peeringdb: Some(JsonlPeeringdb { + aka: Some("PDB1_AKA".to_string()), + asn: 1, + irr_as_set: None, + name: "PDB1".to_string(), + name_long: Some("PDB1_LONG".to_string()), + website: None, + }), + hegemony: None, + population: None, + }, + JsonlRecord { + asn: 2, + name: "CORE2".to_string(), + country: "US".to_string(), + as2org: Some(JsonlAs2org { + country: "US".to_string(), + name: "AS2ORG_NAME2".to_string(), + org_id: "ORG2".to_string(), + org_name: "AS2ORG_ORG2".to_string(), + }), + peeringdb: Some(JsonlPeeringdb { + aka: None, + asn: 2, + irr_as_set: None, + name: "PDB2".to_string(), + name_long: None, + website: None, + }), + hegemony: None, + population: None, + }, + JsonlRecord { + asn: 3, + name: "CORE3".to_string(), + country: "US".to_string(), + as2org: Some(JsonlAs2org { + country: "US".to_string(), + name: "AS2ORG_NAME3".to_string(), + org_id: "ORG3".to_string(), + org_name: "AS2ORG_ORG3".to_string(), + }), + peeringdb: None, + hegemony: None, + population: None, + }, + JsonlRecord { + asn: 4, + name: "CORE4".to_string(), + country: "US".to_string(), + as2org: Some(JsonlAs2org { + country: "US".to_string(), + name: "AS2ORG_NAME4".to_string(), + org_id: "ORG4".to_string(), + org_name: "".to_string(), + }), + peeringdb: None, + hegemony: None, + population: None, + }, + JsonlRecord { + asn: 5, + name: "CORE5".to_string(), + country: "US".to_string(), + as2org: None, + peeringdb: None, + hegemony: None, + population: None, + }, + ]; + + repo.store_from_jsonl(&records, "test://source").unwrap(); + + let names = repo.lookup_preferred_names_batch(&[1, 2, 3, 4, 5, 99999]); + assert_eq!(names.get(&1), Some(&"PDB1_AKA".to_string())); + assert_eq!(names.get(&2), Some(&"PDB2".to_string())); + assert_eq!(names.get(&3), Some(&"AS2ORG_ORG3".to_string())); + assert_eq!(names.get(&4), Some(&"AS2ORG_NAME4".to_string())); + assert_eq!(names.get(&5), Some(&"CORE5".to_string())); + assert!(names.get(&99999).is_none()); + } + #[test] fn test_clear() { let db = setup_test_db(); diff --git a/src/database/monocle/rpki.rs b/src/database/monocle/rpki.rs index 5834646..d150718 100644 --- a/src/database/monocle/rpki.rs +++ b/src/database/monocle/rpki.rs @@ -607,16 +607,25 @@ impl<'a> RpkiRepository<'a> { return Ok(Vec::new()); } - // Query that joins with asinfo_core for customer info + // Query that joins with asinfo tables for preferred customer info let mut stmt = self.conn.prepare( r#" SELECT a.customer_asn, - c.name as customer_name, + COALESCE( + NULLIF(pc.aka, ''), + NULLIF(pc.name_long, ''), + NULLIF(pc.name, ''), + NULLIF(ac.org_name, ''), + NULLIF(ac.name, ''), + c.name + ) as customer_name, c.country as customer_country, GROUP_CONCAT(a.provider_asn) as provider_asns FROM rpki_aspa a LEFT JOIN asinfo_core c ON a.customer_asn = c.asn + LEFT JOIN asinfo_as2org ac ON a.customer_asn = ac.asn + LEFT JOIN asinfo_peeringdb pc ON a.customer_asn = pc.asn GROUP BY a.customer_asn ORDER BY a.customer_asn "#, @@ -634,15 +643,24 @@ impl<'a> RpkiRepository<'a> { .filter_map(|r| r.ok()) .collect(); - // Now get provider names with a second query + // Now get provider names with a second query (preferred name) let mut provider_stmt = self.conn.prepare( r#" SELECT a.customer_asn, a.provider_asn, - c.name as provider_name + COALESCE( + NULLIF(pp.aka, ''), + NULLIF(pp.name_long, ''), + NULLIF(pp.name, ''), + NULLIF(ap.org_name, ''), + NULLIF(ap.name, ''), + c.name + ) as provider_name FROM rpki_aspa a LEFT JOIN asinfo_core c ON a.provider_asn = c.asn + LEFT JOIN asinfo_as2org ap ON a.provider_asn = ap.asn + LEFT JOIN asinfo_peeringdb pp ON a.provider_asn = pp.asn ORDER BY a.customer_asn, a.provider_asn "#, )?; @@ -697,15 +715,24 @@ impl<'a> RpkiRepository<'a> { return Ok(Vec::new()); } - // Get customer info + // Get customer info (preferred name) let mut customer_stmt = self.conn.prepare( r#" SELECT a.customer_asn, - c.name as customer_name, + COALESCE( + NULLIF(pc.aka, ''), + NULLIF(pc.name_long, ''), + NULLIF(pc.name, ''), + NULLIF(ac.org_name, ''), + NULLIF(ac.name, ''), + c.name + ) as customer_name, c.country as customer_country FROM rpki_aspa a LEFT JOIN asinfo_core c ON a.customer_asn = c.asn + LEFT JOIN asinfo_as2org ac ON a.customer_asn = ac.asn + LEFT JOIN asinfo_peeringdb pc ON a.customer_asn = pc.asn WHERE a.customer_asn = ?1 LIMIT 1 "#, @@ -725,14 +752,23 @@ impl<'a> RpkiRepository<'a> { return Ok(Vec::new()); }; - // Get providers with names + // Get providers with names (preferred name) let mut provider_stmt = self.conn.prepare( r#" SELECT a.provider_asn, - c.name as provider_name + COALESCE( + NULLIF(pp.aka, ''), + NULLIF(pp.name_long, ''), + NULLIF(pp.name, ''), + NULLIF(ap.org_name, ''), + NULLIF(ap.name, ''), + c.name + ) as provider_name FROM rpki_aspa a LEFT JOIN asinfo_core c ON a.provider_asn = c.asn + LEFT JOIN asinfo_as2org ap ON a.provider_asn = ap.asn + LEFT JOIN asinfo_peeringdb pp ON a.provider_asn = pp.asn WHERE a.customer_asn = ?1 ORDER BY a.provider_asn "#, diff --git a/src/lens/inspect/mod.rs b/src/lens/inspect/mod.rs index cb19ee5..f006e55 100644 --- a/src/lens/inspect/mod.rs +++ b/src/lens/inspect/mod.rs @@ -1072,7 +1072,7 @@ impl<'a> InspectLens<'a> { // Use the repository's get_connectivity_summary method let asinfo = self.db.asinfo(); let name_lookup = - |asns: &[u32]| -> HashMap { asinfo.lookup_names_batch(asns) }; + |asns: &[u32]| -> HashMap { asinfo.lookup_preferred_names_batch(asns) }; let summary = match as2rel.get_connectivity_summary(asn, max_neighbors, name_lookup) { Ok(Some(s)) => s, @@ -1137,22 +1137,25 @@ impl<'a> InspectLens<'a> { .get_core(aspa_record.customer_asn) .ok() .flatten() - .map(|r| (Some(r.name), Some(r.country))) + .map(|r| { + ( + self.db.asinfo().lookup_preferred_name(r.asn), + Some(r.country), + ) + }) .unwrap_or((None, None)); // Get providers with names + let provider_names = self + .db + .asinfo() + .lookup_preferred_names_batch(&aspa_record.provider_asns); let providers: Vec = aspa_record .provider_asns .iter() - .map(|asn| { - let name = self - .db - .asinfo() - .get_core(*asn) - .ok() - .flatten() - .map(|r| r.name); - AspaProvider { asn: *asn, name } + .map(|asn| AspaProvider { + asn: *asn, + name: provider_names.get(asn).cloned(), }) .collect(); @@ -1322,7 +1325,9 @@ impl<'a> InspectLens<'a> { // Get AS info for the queried ASN let asinfo = self.db.asinfo(); let origin_info = asinfo.get_core(asn).ok().flatten(); - let origin_name = origin_info.as_ref().map(|i| i.name.clone()); + let origin_name = asinfo + .lookup_preferred_name(asn) + .or_else(|| origin_info.as_ref().map(|i| i.name.clone())); let origin_country = origin_info.as_ref().map(|i| i.country.clone()); let prefix_entries: Vec = sorted_prefixes @@ -1353,12 +1358,7 @@ impl<'a> InspectLens<'a> { /// Lookup AS name by ASN pub fn lookup_name(&self, asn: u32) -> Option { - self.db - .asinfo() - .get_core(asn) - .ok() - .flatten() - .map(|r| r.name) + self.db.asinfo().lookup_preferred_name(asn) } /// Lookup organization name by ASN @@ -1373,7 +1373,7 @@ impl<'a> InspectLens<'a> { /// Batch lookup of AS names pub fn lookup_names_batch(&self, asns: &[u32]) -> HashMap { - self.db.asinfo().lookup_names_batch(asns) + self.db.asinfo().lookup_preferred_names_batch(asns) } // ========================================================================= @@ -1502,7 +1502,10 @@ impl<'a> InspectLens<'a> { // Core info - full names, no truncation lines.push(format!("ASN: AS{}", detail.core.asn)); - lines.push(format!("Name: {}", detail.core.name)); + lines.push(format!( + "Name: {}", + self.preferred_name_from_full(detail) + )); lines.push(format!("Country: {}", detail.core.country)); if let Some(ref as2org) = detail.as2org { @@ -1585,7 +1588,7 @@ impl<'a> InspectLens<'a> { let row = [ format!("AS{}", detail.core.asn), - self.truncate_name(&detail.core.name, config), + self.truncate_name(&self.preferred_name_from_full(detail), config), detail.core.country.clone(), org, ]; @@ -1625,7 +1628,7 @@ impl<'a> InspectLens<'a> { .unwrap_or_else(|| "-".to_string()); Some(SimpleRow { asn: format!("AS{}", detail.core.asn), - name: self.truncate_name(&detail.core.name, config), + name: self.truncate_name(&self.preferred_name_from_full(detail), config), country: detail.core.country.clone(), org, }) @@ -1700,7 +1703,7 @@ impl<'a> InspectLens<'a> { lines.push(format!("ASN: AS{}", detail.core.asn)); lines.push(format!( "Name: {}", - self.truncate_name(&detail.core.name, config) + self.truncate_name(&self.preferred_name_from_full(detail), config) )); lines.push(format!("Country: {}", detail.core.country)); @@ -1765,7 +1768,7 @@ impl<'a> InspectLens<'a> { .iter() .map(|o| OriginRow { asn: format!("AS{}", o.core.asn), - name: self.truncate_name(&o.core.name, config), + name: self.truncate_name(&self.preferred_name_from_full(o), config), country: o.core.country.clone(), }) .collect(); @@ -2201,12 +2204,20 @@ impl<'a> InspectLens<'a> { country: String, } + let preferred_names: HashMap = + self.db.asinfo().lookup_preferred_names_batch( + &search.results.iter().map(|r| r.asn).collect::>(), + ); + let mut rows: Vec = search .results .iter() .map(|r| SearchRow { asn: format!("AS{}", r.asn), - name: self.truncate_name(&r.name, config), + name: self.truncate_name( + preferred_names.get(&r.asn).unwrap_or(&r.name).as_str(), + config, + ), country: r.country.clone(), }) .collect(); @@ -2235,6 +2246,35 @@ impl<'a> InspectLens<'a> { lines.join("\n") } + fn preferred_name_from_full(&self, detail: &AsinfoFullRecord) -> String { + if let Some(ref pdb) = detail.peeringdb { + if let Some(ref aka) = pdb.aka { + if !aka.is_empty() { + return aka.clone(); + } + } + if let Some(ref name_long) = pdb.name_long { + if !name_long.is_empty() { + return name_long.clone(); + } + } + if !pdb.name.is_empty() { + return pdb.name.clone(); + } + } + + if let Some(ref as2org) = detail.as2org { + if !as2org.org_name.is_empty() { + return as2org.org_name.clone(); + } + if !as2org.name.is_empty() { + return as2org.name.clone(); + } + } + + detail.core.name.clone() + } + /// Truncate a name based on display config fn truncate_name(&self, name: &str, config: &InspectDisplayConfig) -> String { if !config.truncate_names || name.len() <= config.name_max_width { diff --git a/src/lens/pfx2as/mod.rs b/src/lens/pfx2as/mod.rs index 1fe6bcd..37102f0 100644 --- a/src/lens/pfx2as/mod.rs +++ b/src/lens/pfx2as/mod.rs @@ -669,16 +669,7 @@ impl<'a> Pfx2asLens<'a> { /// Get AS names for a list of ASNs fn get_as_names(&self, asns: &[u32]) -> HashMap { - let mut names = HashMap::new(); - let asinfo = self.db.asinfo(); - - for asn in asns { - if let Ok(Some(record)) = asinfo.get_full(*asn) { - names.insert(*asn, record.core.name); - } - } - - names + self.db.asinfo().lookup_preferred_names_batch(asns) } // ========================================================================= diff --git a/src/lens/rpki/mod.rs b/src/lens/rpki/mod.rs index 23c2a7c..2383c28 100644 --- a/src/lens/rpki/mod.rs +++ b/src/lens/rpki/mod.rs @@ -696,7 +696,9 @@ impl<'a> RpkiLens<'a> { // Historical query: use bgpkit-commons let trie = self.load_historical_data(args.date, &args.source, args.collector.as_ref())?; - commons::get_aspas(trie, args.customer_asn, args.provider_asn) + let mut aspas = commons::get_aspas(trie, args.customer_asn, args.provider_asn)?; + self.enrich_aspa_names(&mut aspas); + Ok(aspas) } else { // Current query: use cache self.get_aspas_from_cache(args.customer_asn, args.provider_asn) @@ -759,6 +761,29 @@ impl<'a> RpkiLens<'a> { .collect()) } + fn enrich_aspa_names(&self, aspas: &mut [RpkiAspaEntry]) { + let mut asns = Vec::new(); + for aspa in aspas.iter() { + asns.push(aspa.customer_asn); + asns.extend(aspa.providers.iter().map(|p| p.asn)); + } + if asns.is_empty() { + return; + } + + let names = self.db.asinfo().lookup_preferred_names_batch(&asns); + for aspa in aspas.iter_mut() { + if aspa.customer_name.is_none() { + aspa.customer_name = names.get(&aspa.customer_asn).cloned(); + } + for provider in aspa.providers.iter_mut() { + if provider.name.is_none() { + provider.name = names.get(&provider.asn).cloned(); + } + } + } + } + /// Get ASPA by customer ASN from cache pub fn get_aspa_by_customer(&self, customer_asn: u32) -> Result> { let aspas = self.db.rpki().get_aspas_by_customer(customer_asn)?;