use crate::asn::cache::AsnCache;
use crate::dns::resolver;
use crate::traceroute::{AsnInfo, is_cgnat, is_internal_ip};
use ip_network::{Ipv4Network, Ipv6Network};
use std::net::{Ipv4Addr, Ipv6Addr};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum AsnLookupError {
#[error("DNS resolution failed: {0}")]
DnsError(String),
#[error("Invalid ASN response format")]
InvalidFormat,
#[error("No ASN data found")]
NotFound,
}
struct CymruOrigin {
asn: u32,
prefix: String,
country_code: String,
registry: String,
}
fn parse_cymru_origin_txt(txt: &str) -> Result<CymruOrigin, AsnLookupError> {
let parts: Vec<&str> = txt.split('|').map(str::trim).collect();
if parts.len() < 3 {
return Err(AsnLookupError::InvalidFormat);
}
let asn_str = parts[0]
.split_whitespace()
.next()
.unwrap_or("")
.trim_start_matches("AS");
let asn = asn_str.parse::<u32>().unwrap_or(0);
Ok(CymruOrigin {
asn,
prefix: parts[1].to_string(),
country_code: parts[2].to_string(),
registry: if parts.len() > 3 {
parts[3].to_string()
} else {
String::new()
},
})
}
async fn query_cymru_origin(query: &str) -> Result<CymruOrigin, AsnLookupError> {
let txts = resolver::resolve_txt(query)
.await
.map_err(|e| AsnLookupError::DnsError(e.to_string()))?;
let txt_data = txts.first().ok_or(AsnLookupError::NotFound)?;
parse_cymru_origin_txt(txt_data)
}
async fn lookup_as_name(asn: u32, country_code: &str) -> String {
let as_query = format!("AS{asn}.asn.cymru.com");
match resolver::resolve_txt(&as_query).await {
Ok(as_txts) => {
if let Some(as_txt) = as_txts.first() {
let as_parts: Vec<&str> = as_txt.split('|').map(str::trim).collect();
if as_parts.len() >= 5 {
let mut as_name = as_parts[4].to_string();
if as_name.ends_with(&format!(", {country_code}")) {
as_name.truncate(as_name.len() - country_code.len() - 2);
}
as_name
} else if as_parts.len() >= 2 {
as_parts[1].to_string()
} else {
String::new()
}
} else {
String::new()
}
}
Err(_) => String::new(),
}
}
fn origin6_query_name(addr: &Ipv6Addr) -> String {
const SUFFIX: &str = "origin6.asn.cymru.com";
let mut name = String::with_capacity(32 * 2 + SUFFIX.len());
for byte in addr.octets().iter().rev() {
name.push_str(&format!("{:x}.{:x}.", byte & 0x0f, byte >> 4));
}
name.push_str(SUFFIX);
name
}
fn special_ipv6_name(addr: &Ipv6Addr) -> Option<&'static str> {
let seg = addr.segments();
if addr.is_loopback() {
Some("Loopback")
} else if (seg[0] & 0xfe00) == 0xfc00 {
Some("Private Network")
} else if addr.is_unspecified()
|| (seg[0] & 0xffc0) == 0xfe80
|| (seg[0] == 0x2001 && seg[1] == 0xdb8)
|| (seg[0] & 0xff00) == 0xff00
{
Some("Special Use")
} else {
None
}
}
pub(crate) async fn lookup_asn_with_cache(
ipv4_addr: Ipv4Addr,
cache: &Arc<RwLock<AsnCache>>,
) -> Result<AsnInfo, AsnLookupError> {
if is_internal_ip(&ipv4_addr)
|| is_cgnat(&ipv4_addr)
|| ipv4_addr.is_link_local()
|| ipv4_addr.is_broadcast()
|| ipv4_addr.is_documentation()
|| ipv4_addr.is_unspecified()
{
let name = if ipv4_addr.is_loopback() {
"Loopback"
} else if ipv4_addr.is_private() {
"Private Network"
} else if is_cgnat(&ipv4_addr) {
"Carrier Grade NAT"
} else {
"Special Use"
}
.to_string();
let asn_info = AsnInfo {
asn: 0, prefix: ipv4_addr.to_string() + "/32",
country_code: "N/A".to_string(),
registry: "N/A".to_string(),
name,
};
if let Ok(net) = asn_info.prefix.parse::<Ipv4Network>() {
let cache_write = cache.write().await;
cache_write.insert(net, asn_info.clone());
}
return Ok(asn_info);
}
{
let cache_read = cache.read().await;
if let Some(cached) = cache_read.get(&ipv4_addr) {
return Ok(cached);
}
}
let octets = ipv4_addr.octets();
let query = format!(
"{}.{}.{}.{}.origin.asn.cymru.com",
octets[3], octets[2], octets[1], octets[0]
);
let origin = query_cymru_origin(&query).await?;
let net = origin
.prefix
.parse::<Ipv4Network>()
.map_err(|_| AsnLookupError::InvalidFormat)?;
let name = lookup_as_name(origin.asn, &origin.country_code).await;
let asn_info = AsnInfo {
asn: origin.asn,
prefix: origin.prefix,
country_code: origin.country_code,
registry: origin.registry,
name,
};
let cache_write = cache.write().await;
cache_write.insert(net, asn_info.clone());
Ok(asn_info)
}
pub(crate) async fn lookup_asn_v6_with_cache(
ipv6_addr: Ipv6Addr,
cache: &Arc<RwLock<AsnCache>>,
) -> Result<AsnInfo, AsnLookupError> {
if let Some(v4) = ipv6_addr.to_ipv4_mapped() {
return lookup_asn_with_cache(v4, cache).await;
}
if let Some(name) = special_ipv6_name(&ipv6_addr) {
let asn_info = AsnInfo {
asn: 0, prefix: format!("{ipv6_addr}/128"),
country_code: "N/A".to_string(),
registry: "N/A".to_string(),
name: name.to_string(),
};
if let Ok(net) = asn_info.prefix.parse::<Ipv6Network>() {
let cache_write = cache.write().await;
cache_write.insert_ipv6(net, asn_info.clone());
}
return Ok(asn_info);
}
{
let cache_read = cache.read().await;
if let Some(cached) = cache_read.get_ipv6(&ipv6_addr) {
return Ok(cached);
}
}
let origin = query_cymru_origin(&origin6_query_name(&ipv6_addr)).await?;
let net = origin
.prefix
.parse::<Ipv6Network>()
.map_err(|_| AsnLookupError::InvalidFormat)?;
let name = lookup_as_name(origin.asn, &origin.country_code).await;
let asn_info = AsnInfo {
asn: origin.asn,
prefix: origin.prefix,
country_code: origin.country_code,
registry: origin.registry,
name,
};
let cache_write = cache.write().await;
cache_write.insert_ipv6(net, asn_info.clone());
Ok(asn_info)
}
#[cfg(test)]
#[path = "lookup_tests.rs"]
mod tests;