use arc_swap::ArcSwap;
use hickory_resolver::error::ResolveResult;
pub use hickory_resolver::proto::rr::rdata::tlsa::TLSA;
use hickory_resolver::proto::rr::RecordType;
use hickory_resolver::Name;
use kumo_address::host::HostAddress;
use kumo_log_types::ResolvedAddress;
use lruttl::LruCacheWithTtl;
use rand::prelude::SliceRandom;
use serde::Serialize;
use std::collections::BTreeMap;
use std::net::{IpAddr, Ipv6Addr};
use std::sync::{Arc, LazyLock, Mutex as StdMutex};
use std::time::Instant;
mod resolver;
#[cfg(feature = "unbound")]
pub use resolver::UnboundResolver;
pub use resolver::{ptr_host, DnsError, HickoryResolver, IpDisplay, Resolver, TestResolver};
static RESOLVER: LazyLock<ArcSwap<Box<dyn Resolver>>> =
LazyLock::new(|| ArcSwap::from_pointee(Box::new(default_resolver())));
static MX_CACHE: LazyLock<StdMutex<LruCacheWithTtl<Name, Arc<MailExchanger>>>> =
LazyLock::new(|| StdMutex::new(LruCacheWithTtl::new_named("dns_resolver_mx", 64 * 1024)));
static IPV4_CACHE: LazyLock<StdMutex<LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>>> =
LazyLock::new(|| StdMutex::new(LruCacheWithTtl::new_named("dns_resolver_ipv4", 1024)));
static IPV6_CACHE: LazyLock<StdMutex<LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>>> =
LazyLock::new(|| StdMutex::new(LruCacheWithTtl::new_named("dns_resolver_ipv6", 1024)));
static IP_CACHE: LazyLock<StdMutex<LruCacheWithTtl<Name, Arc<Vec<IpAddr>>>>> =
LazyLock::new(|| StdMutex::new(LruCacheWithTtl::new_named("dns_resolver_ip", 1024)));
static MX_IN_PROGRESS: LazyLock<prometheus::IntGauge> = LazyLock::new(|| {
prometheus::register_int_gauge!(
"dns_mx_resolve_in_progress",
"number of MailExchanger::resolve calls currently in progress"
)
.unwrap()
});
static MX_SUCCESS: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
prometheus::register_int_counter!(
"dns_mx_resolve_status_ok",
"total number of successful MailExchanger::resolve calls"
)
.unwrap()
});
static MX_FAIL: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
prometheus::register_int_counter!(
"dns_mx_resolve_status_fail",
"total number of failed MailExchanger::resolve calls"
)
.unwrap()
});
static MX_CACHED: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
prometheus::register_int_counter!(
"dns_mx_resolve_cache_hit",
"total number of MailExchanger::resolve calls satisfied by level 1 cache"
)
.unwrap()
});
static MX_QUERIES: LazyLock<prometheus::IntCounter> = LazyLock::new(|| {
prometheus::register_int_counter!(
"dns_mx_resolve_cache_miss",
"total number of MailExchanger::resolve calls that resulted in an \
MX DNS request to the next level of cache"
)
.unwrap()
});
fn default_resolver() -> impl Resolver {
#[cfg(feature = "default-unbound")]
return UnboundResolver::new().unwrap();
#[cfg(not(feature = "default-unbound"))]
return HickoryResolver::new().expect("Parsing /etc/resolv.conf failed");
}
fn mx_cache_get(name: &Name) -> Option<Arc<MailExchanger>> {
MX_CACHE.lock().unwrap().get(name).clone()
}
fn ip_cache_get(ip: &Name) -> Option<(Arc<Vec<IpAddr>>, Instant)> {
IP_CACHE.lock().unwrap().get_with_expiry(ip)
}
fn ipv4_cache_get(ip: &Name) -> Option<(Arc<Vec<IpAddr>>, Instant)> {
IPV4_CACHE.lock().unwrap().get_with_expiry(ip)
}
fn ipv6_cache_get(ip: &Name) -> Option<(Arc<Vec<IpAddr>>, Instant)> {
IPV6_CACHE.lock().unwrap().get_with_expiry(ip)
}
#[derive(Clone, Debug, Serialize)]
pub struct MailExchanger {
pub domain_name: String,
pub hosts: Vec<String>,
pub site_name: String,
pub by_pref: BTreeMap<u16, Vec<String>>,
pub is_domain_literal: bool,
pub is_secure: bool,
pub is_mx: bool,
#[serde(skip)]
expires: Option<Instant>,
}
pub fn fully_qualify(domain_name: &str) -> ResolveResult<Name> {
let mut name = Name::from_str_relaxed(domain_name)?.to_lowercase();
name.set_fqdn(true);
Ok(name)
}
pub fn reconfigure_resolver(resolver: impl Resolver) {
RESOLVER.store(Arc::new(Box::new(resolver)));
}
pub fn get_resolver() -> Arc<Box<dyn Resolver>> {
RESOLVER.load_full()
}
pub async fn resolve_dane(hostname: &str, port: u16) -> anyhow::Result<Vec<TLSA>> {
let name = fully_qualify(&format!("_{port}._tcp.{hostname}"))?;
let answer = RESOLVER.load().resolve(name, RecordType::TLSA).await?;
tracing::info!("resolve_dane {hostname}:{port} TLSA answer is: {answer:?}");
if answer.bogus {
anyhow::bail!(
"DANE result for {hostname}:{port} unusable because: {}",
answer
.why_bogus
.as_deref()
.unwrap_or("DNSSEC validation failed")
);
}
let mut result = vec![];
if answer.secure {
for r in &answer.records {
if let Some(tlsa) = r.as_tlsa() {
result.push(tlsa.clone());
}
}
result.sort_by(|a, b| a.to_string().cmp(&b.to_string()));
}
tracing::info!("resolve_dane {hostname}:{port} result is: {result:?}");
Ok(result)
}
pub async fn resolve_a_or_aaaa(domain_name: &str) -> anyhow::Result<Vec<ResolvedAddress>> {
if domain_name.starts_with('[') {
if !domain_name.ends_with(']') {
anyhow::bail!(
"domain_name `{domain_name}` is a malformed literal \
domain with no trailing `]`"
);
}
let lowered = domain_name.to_ascii_lowercase();
let literal = &lowered[1..lowered.len() - 1];
if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
match v6_literal.parse::<Ipv6Addr>() {
Ok(addr) => {
return Ok(vec![ResolvedAddress {
name: domain_name.to_string(),
addr: std::net::IpAddr::V6(addr).into(),
}]);
}
Err(err) => {
anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
}
}
}
match literal.parse::<HostAddress>() {
Ok(addr) => {
return Ok(vec![ResolvedAddress {
name: domain_name.to_string(),
addr: addr.into(),
}]);
}
Err(err) => {
anyhow::bail!("invalid address: `{literal}`: {err:#}");
}
}
} else {
if let Ok(addr) = domain_name.parse::<HostAddress>() {
return Ok(vec![ResolvedAddress {
name: domain_name.to_string(),
addr,
}]);
}
}
match ip_lookup(domain_name).await {
Ok((addrs, _expires)) => {
let addrs = addrs
.iter()
.map(|&addr| ResolvedAddress {
name: domain_name.to_string(),
addr: addr.into(),
})
.collect();
Ok(addrs)
}
Err(err) => anyhow::bail!("{err:#}"),
}
}
impl MailExchanger {
pub async fn resolve(domain_name: &str) -> anyhow::Result<Arc<Self>> {
MX_IN_PROGRESS.inc();
let result = Self::resolve_impl(domain_name).await;
MX_IN_PROGRESS.dec();
if result.is_ok() {
MX_SUCCESS.inc();
} else {
MX_FAIL.inc();
}
result
}
async fn resolve_impl(domain_name: &str) -> anyhow::Result<Arc<Self>> {
if domain_name.starts_with('[') {
if !domain_name.ends_with(']') {
anyhow::bail!(
"domain_name `{domain_name}` is a malformed literal \
domain with no trailing `]`"
);
}
let lowered = domain_name.to_ascii_lowercase();
let literal = &lowered[1..lowered.len() - 1];
if let Some(v6_literal) = literal.strip_prefix("ipv6:") {
match v6_literal.parse::<Ipv6Addr>() {
Ok(addr) => {
let mut by_pref = BTreeMap::new();
by_pref.insert(1, vec![addr.to_string()]);
return Ok(Arc::new(Self {
domain_name: domain_name.to_string(),
hosts: vec![addr.to_string()],
site_name: addr.to_string(),
by_pref,
is_domain_literal: true,
is_secure: false,
is_mx: false,
expires: None,
}));
}
Err(err) => {
anyhow::bail!("invalid ipv6 address: `{v6_literal}`: {err:#}");
}
}
}
match literal.parse::<IpAddr>() {
Ok(addr) => {
let mut by_pref = BTreeMap::new();
by_pref.insert(1, vec![addr.to_string()]);
return Ok(Arc::new(Self {
domain_name: domain_name.to_string(),
hosts: vec![addr.to_string()],
site_name: addr.to_string(),
by_pref,
is_domain_literal: true,
is_secure: false,
is_mx: false,
expires: None,
}));
}
Err(err) => {
anyhow::bail!("invalid address: `{literal}`: {err:#}");
}
}
}
let name_fq = fully_qualify(domain_name)?;
if let Some(mx) = mx_cache_get(&name_fq) {
MX_CACHED.inc();
return Ok(mx);
}
let start = Instant::now();
MX_QUERIES.inc();
let (by_pref, expires) = match lookup_mx_record(&name_fq).await {
Ok((by_pref, expires)) => (by_pref, expires),
Err(err) => anyhow::bail!(
"MX lookup for {domain_name} failed after {elapsed:?}: {err:#}",
elapsed = start.elapsed()
),
};
let mut hosts = vec![];
for pref in &by_pref {
for host in &pref.hosts {
hosts.push(host.to_string());
}
}
let is_secure = by_pref.iter().all(|p| p.is_secure);
let is_mx = by_pref.iter().all(|p| p.is_mx);
let by_pref = by_pref
.into_iter()
.map(|pref| (pref.pref, pref.hosts))
.collect();
let site_name = factor_names(&hosts);
let mx = Self {
hosts,
domain_name: name_fq.to_ascii(),
site_name,
by_pref,
is_domain_literal: false,
is_secure,
is_mx,
expires: Some(expires),
};
let mx = Arc::new(mx);
MX_CACHE
.lock()
.unwrap()
.insert(name_fq, mx.clone(), expires);
Ok(mx)
}
pub fn has_expired(&self) -> bool {
match self.expires {
Some(deadline) => deadline <= Instant::now(),
None => false,
}
}
pub async fn resolve_addresses(&self) -> ResolvedMxAddresses {
let mut result = vec![];
for hosts in self.by_pref.values().rev() {
let mut by_pref = vec![];
for mx_host in hosts {
if mx_host == "." {
return ResolvedMxAddresses::NullMx;
}
if let Ok(addr) = mx_host.parse::<IpAddr>() {
by_pref.push(ResolvedAddress {
name: mx_host.to_string(),
addr: addr.into(),
});
continue;
}
match ip_lookup(mx_host).await {
Err(err) => {
tracing::error!("failed to resolve {mx_host}: {err:#}");
continue;
}
Ok((addresses, _expires)) => {
for addr in addresses.iter() {
by_pref.push(ResolvedAddress {
name: mx_host.to_string(),
addr: (*addr).into(),
});
}
}
}
}
let mut rng = rand::thread_rng();
by_pref.shuffle(&mut rng);
result.append(&mut by_pref);
}
ResolvedMxAddresses::Addresses(result)
}
}
#[derive(Debug, Clone, Serialize)]
pub enum ResolvedMxAddresses {
NullMx,
Addresses(Vec<ResolvedAddress>),
}
struct ByPreference {
hosts: Vec<String>,
pref: u16,
is_secure: bool,
is_mx: bool,
}
async fn lookup_mx_record(domain_name: &Name) -> anyhow::Result<(Vec<ByPreference>, Instant)> {
let mx_lookup = RESOLVER
.load()
.resolve(domain_name.clone(), RecordType::MX)
.await?;
let mx_records = mx_lookup.records;
if mx_records.is_empty() {
if mx_lookup.nxdomain {
anyhow::bail!("NXDOMAIN");
}
return Ok((
vec![ByPreference {
hosts: vec![domain_name.to_lowercase().to_ascii()],
pref: 1,
is_secure: false,
is_mx: false,
}],
mx_lookup.expires,
));
}
let mut records: Vec<ByPreference> = Vec::with_capacity(mx_records.len());
for mx_record in mx_records {
if let Some(mx) = mx_record.as_mx() {
let pref = mx.preference();
let host = mx.exchange().to_lowercase().to_string();
if let Some(record) = records.iter_mut().find(|r| r.pref == pref) {
record.hosts.push(host);
} else {
records.push(ByPreference {
hosts: vec![host],
pref,
is_secure: mx_lookup.secure,
is_mx: true,
});
}
}
}
records.sort_unstable_by(|a, b| a.pref.cmp(&b.pref));
for mx in &mut records {
mx.hosts.sort();
}
Ok((records, mx_lookup.expires))
}
pub async fn ip_lookup(key: &str) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
let key_fq = fully_qualify(key)?;
if let Some(value) = ip_cache_get(&key_fq) {
return Ok(value);
}
let (v4, v6) = tokio::join!(ipv4_lookup(key), ipv6_lookup(key));
let mut results = vec![];
let mut errors = vec![];
let mut expires = None;
match v4 {
Ok((addrs, exp)) => {
expires.replace(exp);
for a in addrs.iter() {
results.push(*a);
}
}
Err(err) => errors.push(err),
}
match v6 {
Ok((addrs, exp)) => {
let exp = match expires.take() {
Some(existing) => exp.min(existing),
None => exp,
};
expires.replace(exp);
for a in addrs.iter() {
results.push(*a);
}
}
Err(err) => errors.push(err),
}
if results.is_empty() && !errors.is_empty() {
return Err(errors.remove(0));
}
let addr = Arc::new(results);
let exp = expires.take().unwrap_or_else(|| Instant::now());
IP_CACHE.lock().unwrap().insert(key_fq, addr.clone(), exp);
Ok((addr, exp))
}
pub async fn ipv4_lookup(key: &str) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
let key_fq = fully_qualify(key)?;
if let Some(value) = ipv4_cache_get(&key_fq) {
return Ok(value);
}
let answer = RESOLVER
.load()
.resolve(key_fq.clone(), RecordType::A)
.await?;
let ips = answer.as_addr();
let ips = Arc::new(ips);
let expires = answer.expires;
IPV4_CACHE
.lock()
.unwrap()
.insert(key_fq, ips.clone(), expires);
Ok((ips, expires))
}
pub async fn ipv6_lookup(key: &str) -> anyhow::Result<(Arc<Vec<IpAddr>>, Instant)> {
let key_fq = fully_qualify(key)?;
if let Some(value) = ipv6_cache_get(&key_fq) {
return Ok(value);
}
let answer = RESOLVER
.load()
.resolve(key_fq.clone(), RecordType::AAAA)
.await?;
let ips = answer.as_addr();
let ips = Arc::new(ips);
let expires = answer.expires;
IPV6_CACHE
.lock()
.unwrap()
.insert(key_fq, ips.clone(), expires);
Ok((ips, expires))
}
fn factor_names<S: AsRef<str>>(name_strings: &[S]) -> String {
let mut max_element_count = 0;
let mut names = vec![];
for name in name_strings {
if let Ok(name) = fully_qualify(name.as_ref()) {
names.push(name.to_lowercase());
}
}
let mut elements: Vec<Vec<&str>> = vec![];
let mut split_names = vec![];
for name in names {
let mut fields: Vec<_> = name
.iter()
.map(|s| String::from_utf8_lossy(s).to_string())
.collect();
fields.reverse();
max_element_count = max_element_count.max(fields.len());
split_names.push(fields);
}
fn add_element<'a>(elements: &mut Vec<Vec<&'a str>>, field: &'a str, i: usize) {
match elements.get_mut(i) {
Some(ele) => {
if !ele.contains(&field) {
ele.push(field);
}
}
None => {
elements.push(vec![field]);
}
}
}
for fields in &split_names {
for (i, field) in fields.iter().enumerate() {
add_element(&mut elements, field, i);
}
for i in fields.len()..max_element_count {
add_element(&mut elements, "?", i);
}
}
let mut result = vec![];
for mut ele in elements {
let has_q = ele.contains(&"?");
ele.retain(|&e| e != "?");
let mut item_text = if ele.len() == 1 {
ele[0].to_string()
} else {
format!("({})", ele.join("|"))
};
if has_q {
item_text.push('?');
}
result.push(item_text);
}
result.reverse();
result.join(".")
}
#[cfg(test)]
mod test {
use super::*;
#[tokio::test]
async fn literal_resolve() {
let v4_loopback = MailExchanger::resolve("[127.0.0.1]").await.unwrap();
k9::snapshot!(
&v4_loopback,
r#"
MailExchanger {
domain_name: "[127.0.0.1]",
hosts: [
"127.0.0.1",
],
site_name: "127.0.0.1",
by_pref: {
1: [
"127.0.0.1",
],
},
is_domain_literal: true,
is_secure: false,
is_mx: false,
expires: None,
}
"#
);
k9::snapshot!(
v4_loopback.resolve_addresses().await,
r#"
Addresses(
[
ResolvedAddress {
name: "127.0.0.1",
addr: 127.0.0.1,
},
],
)
"#
);
let v6_loopback_non_conforming = MailExchanger::resolve("[::1]").await.unwrap();
k9::snapshot!(
&v6_loopback_non_conforming,
r#"
MailExchanger {
domain_name: "[::1]",
hosts: [
"::1",
],
site_name: "::1",
by_pref: {
1: [
"::1",
],
},
is_domain_literal: true,
is_secure: false,
is_mx: false,
expires: None,
}
"#
);
k9::snapshot!(
v6_loopback_non_conforming.resolve_addresses().await,
r#"
Addresses(
[
ResolvedAddress {
name: "::1",
addr: ::1,
},
],
)
"#
);
let v6_loopback = MailExchanger::resolve("[IPv6:::1]").await.unwrap();
k9::snapshot!(
&v6_loopback,
r#"
MailExchanger {
domain_name: "[IPv6:::1]",
hosts: [
"::1",
],
site_name: "::1",
by_pref: {
1: [
"::1",
],
},
is_domain_literal: true,
is_secure: false,
is_mx: false,
expires: None,
}
"#
);
k9::snapshot!(
v6_loopback.resolve_addresses().await,
r#"
Addresses(
[
ResolvedAddress {
name: "::1",
addr: ::1,
},
],
)
"#
);
}
#[test]
fn name_factoring() {
assert_eq!(
factor_names(&[
"mta5.am0.yahoodns.net",
"mta6.am0.yahoodns.net",
"mta7.am0.yahoodns.net"
]),
"(mta5|mta6|mta7).am0.yahoodns.net".to_string()
);
assert_eq!(
factor_names(&[
"mta5.AM0.yahoodns.net",
"mta6.am0.yAHOodns.net",
"mta7.am0.yahoodns.net"
]),
"(mta5|mta6|mta7).am0.yahoodns.net".to_string()
);
assert_eq!(
factor_names(&[
"gmail-smtp-in.l.google.com",
"alt1.gmail-smtp-in.l.google.com",
"alt2.gmail-smtp-in.l.google.com",
"alt3.gmail-smtp-in.l.google.com",
"alt4.gmail-smtp-in.l.google.com",
]),
"(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com".to_string()
);
}
#[test]
fn mx_order_name_factor() {
assert_eq!(
factor_names(&[
"example-com.mail.protection.outlook.com.",
"mx-biz.mail.am0.yahoodns.net.",
"mx-biz.mail.am0.yahoodns.net.",
]),
"(example-com|mx-biz).mail.(protection|am0).(outlook|yahoodns).(com|net)".to_string()
);
assert_eq!(
factor_names(&[
"mx-biz.mail.am0.yahoodns.net.",
"mx-biz.mail.am0.yahoodns.net.",
"example-com.mail.protection.outlook.com.",
]),
"(mx-biz|example-com).mail.(am0|protection).(yahoodns|outlook).(net|com)".to_string()
);
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn lookup_gmail_mx() {
let mut gmail = (*MailExchanger::resolve("gmail.com").await.unwrap()).clone();
gmail.expires.take();
k9::snapshot!(
&gmail,
r#"
MailExchanger {
domain_name: "gmail.com.",
hosts: [
"gmail-smtp-in.l.google.com.",
"alt1.gmail-smtp-in.l.google.com.",
"alt2.gmail-smtp-in.l.google.com.",
"alt3.gmail-smtp-in.l.google.com.",
"alt4.gmail-smtp-in.l.google.com.",
],
site_name: "(alt1|alt2|alt3|alt4)?.gmail-smtp-in.l.google.com",
by_pref: {
5: [
"gmail-smtp-in.l.google.com.",
],
10: [
"alt1.gmail-smtp-in.l.google.com.",
],
20: [
"alt2.gmail-smtp-in.l.google.com.",
],
30: [
"alt3.gmail-smtp-in.l.google.com.",
],
40: [
"alt4.gmail-smtp-in.l.google.com.",
],
},
is_domain_literal: false,
is_secure: false,
is_mx: true,
expires: None,
}
"#
);
k9::snapshot!(
gmail.resolve_addresses().await,
r#"
Addresses(
[
ResolvedAddress {
name: "alt4.gmail-smtp-in.l.google.com.",
addr: 2607:f8b0:4023:401::1b,
},
ResolvedAddress {
name: "alt4.gmail-smtp-in.l.google.com.",
addr: 173.194.77.27,
},
ResolvedAddress {
name: "alt3.gmail-smtp-in.l.google.com.",
addr: 2607:f8b0:4023:1::1a,
},
ResolvedAddress {
name: "alt3.gmail-smtp-in.l.google.com.",
addr: 172.253.113.26,
},
ResolvedAddress {
name: "alt2.gmail-smtp-in.l.google.com.",
addr: 2607:f8b0:4001:c1d::1b,
},
ResolvedAddress {
name: "alt2.gmail-smtp-in.l.google.com.",
addr: 74.125.126.27,
},
ResolvedAddress {
name: "alt1.gmail-smtp-in.l.google.com.",
addr: 2607:f8b0:4003:c04::1b,
},
ResolvedAddress {
name: "alt1.gmail-smtp-in.l.google.com.",
addr: 108.177.104.27,
},
ResolvedAddress {
name: "gmail-smtp-in.l.google.com.",
addr: 2607:f8b0:4023:c06::1b,
},
ResolvedAddress {
name: "gmail-smtp-in.l.google.com.",
addr: 142.251.2.26,
},
],
)
"#
);
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn lookup_punycode_no_mx_only_a() {
let mx = MailExchanger::resolve("xn--bb-eka.at").await.unwrap();
assert_eq!(mx.domain_name, "xn--bb-eka.at.");
assert_eq!(mx.hosts[0], "xn--bb-eka.at.");
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn lookup_bogus_aasland() {
let err = MailExchanger::resolve("not-mairs.aasland.com")
.await
.unwrap_err();
k9::snapshot!(err, "MX lookup for not-mairs.aasland.com failed: NXDOMAIN");
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn lookup_example_com() {
let mx = MailExchanger::resolve("example.com").await.unwrap();
k9::snapshot!(
mx,
r#"
MailExchanger {
domain_name: "example.com.",
hosts: [
".",
],
site_name: "",
by_pref: {
0: [
".",
],
},
is_domain_literal: false,
is_secure: true,
is_mx: true,
}
"#
);
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn lookup_have_dane() {
let mx = MailExchanger::resolve("do.havedane.net").await.unwrap();
k9::snapshot!(
mx,
r#"
MailExchanger {
domain_name: "do.havedane.net.",
hosts: [
"do.havedane.net.",
],
site_name: "do.havedane.net",
by_pref: {
10: [
"do.havedane.net.",
],
},
is_domain_literal: false,
is_secure: true,
is_mx: true,
}
"#
);
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn tlsa_have_dane() {
let tlsa = resolve_dane("do.havedane.net", 25).await.unwrap();
k9::snapshot!(
tlsa,
"
[
TLSA {
cert_usage: TrustAnchor,
selector: Spki,
matching: Sha256,
cert_data: [
39,
182,
148,
181,
29,
31,
239,
136,
133,
55,
42,
207,
179,
145,
147,
117,
151,
34,
183,
54,
176,
66,
104,
100,
220,
28,
121,
208,
101,
31,
239,
115,
],
},
TLSA {
cert_usage: DomainIssued,
selector: Spki,
matching: Sha256,
cert_data: [
85,
58,
207,
136,
249,
238,
24,
204,
170,
230,
53,
202,
84,
15,
50,
203,
132,
172,
167,
124,
71,
145,
102,
130,
188,
181,
66,
213,
29,
170,
135,
31,
],
},
]
"
);
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn mx_lookup_www_example_com() {
let mx = MailExchanger::resolve("www.example.com").await.unwrap();
k9::snapshot!(
mx,
r#"
MailExchanger {
domain_name: "www.example.com.",
hosts: [
"www.example.com.",
],
site_name: "www.example.com",
by_pref: {
1: [
"www.example.com.",
],
},
is_domain_literal: false,
is_secure: false,
is_mx: false,
}
"#
);
}
#[cfg(feature = "live-dns-tests")]
#[tokio::test]
async fn txt_lookup_gmail() {
let name = Name::from_utf8("_mta-sts.gmail.com").unwrap();
let answer = get_resolver().resolve(name, RecordType::TXT).await.unwrap();
k9::snapshot!(
answer.as_txt(),
r#"
[
"v=STSv1; id=20190429T010101;",
]
"#
);
}
}