1#![doc = include_str!("../README.md")]
2#![deny(missing_docs)]
3#![deny(rustdoc::broken_intra_doc_links)]
4
5use std::fmt;
6use std::net::IpAddr;
7
8use async_trait::async_trait;
9
10#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum DnsError {
18 Temp(String),
20 Perm(String),
22}
23
24impl fmt::Display for DnsError {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 match self {
27 DnsError::Temp(s) => write!(f, "dns temp error: {s}"),
28 DnsError::Perm(s) => write!(f, "dns perm error: {s}"),
29 }
30 }
31}
32
33impl std::error::Error for DnsError {}
34
35#[async_trait]
41pub trait DnsResolver: Send + Sync {
42 async fn lookup_txt(&self, domain: &str) -> Result<Vec<String>, DnsError>;
44 async fn lookup_a(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError>;
46 async fn lookup_aaaa(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError>;
48 async fn lookup_mx(&self, domain: &str) -> Result<Vec<(u16, String)>, DnsError>;
50 async fn lookup_ptr(&self, ip: IpAddr) -> Result<Vec<String>, DnsError>;
52}
53
54#[cfg(feature = "hickory")]
57pub mod hickory {
58 use super::*;
59 use hickory_resolver::proto::rr::RData;
60 use hickory_resolver::TokioResolver;
61
62 pub struct HickoryResolver {
64 inner: TokioResolver,
65 }
66
67 impl HickoryResolver {
68 pub fn new(resolver: TokioResolver) -> Self {
70 Self { inner: resolver }
71 }
72 }
73
74 fn is_no_records<E: std::fmt::Display>(e: &E) -> bool {
78 let s = e.to_string();
79 s.contains("no record")
80 || s.contains("NXDOMAIN")
81 || s.contains("no records found")
82 || s.contains("NoRecordsFound")
83 }
84
85 #[async_trait]
86 impl DnsResolver for HickoryResolver {
87 async fn lookup_txt(&self, domain: &str) -> Result<Vec<String>, DnsError> {
88 match self.inner.txt_lookup(domain).await {
89 Ok(resp) => {
90 let mut out = Vec::new();
91 for record in resp.answers() {
92 if let RData::TXT(txt) = &record.data {
93 out.push(txt.to_string());
94 }
95 }
96 Ok(out)
97 }
98 Err(e) if is_no_records(&e) => Ok(Vec::new()),
99 Err(e) => Err(DnsError::Temp(e.to_string())),
100 }
101 }
102
103 async fn lookup_a(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError> {
104 match self.inner.ipv4_lookup(domain).await {
105 Ok(resp) => {
106 let mut out = Vec::new();
107 for record in resp.answers() {
108 if let RData::A(a) = &record.data {
109 out.push(IpAddr::V4(a.0));
110 }
111 }
112 Ok(out)
113 }
114 Err(e) if is_no_records(&e) => Ok(Vec::new()),
115 Err(e) => Err(DnsError::Temp(e.to_string())),
116 }
117 }
118
119 async fn lookup_aaaa(&self, domain: &str) -> Result<Vec<IpAddr>, DnsError> {
120 match self.inner.ipv6_lookup(domain).await {
121 Ok(resp) => {
122 let mut out = Vec::new();
123 for record in resp.answers() {
124 if let RData::AAAA(a) = &record.data {
125 out.push(IpAddr::V6(a.0));
126 }
127 }
128 Ok(out)
129 }
130 Err(e) if is_no_records(&e) => Ok(Vec::new()),
131 Err(e) => Err(DnsError::Temp(e.to_string())),
132 }
133 }
134
135 async fn lookup_mx(&self, domain: &str) -> Result<Vec<(u16, String)>, DnsError> {
136 match self.inner.mx_lookup(domain).await {
137 Ok(resp) => {
138 let mut out = Vec::new();
139 for record in resp.answers() {
140 if let RData::MX(mx) = &record.data {
141 out.push((mx.preference, mx.exchange.to_utf8()));
142 }
143 }
144 Ok(out)
145 }
146 Err(e) if is_no_records(&e) => Ok(Vec::new()),
147 Err(e) => Err(DnsError::Temp(e.to_string())),
148 }
149 }
150
151 async fn lookup_ptr(&self, ip: IpAddr) -> Result<Vec<String>, DnsError> {
152 match self.inner.reverse_lookup(ip).await {
153 Ok(resp) => {
154 let mut out = Vec::new();
155 for record in resp.answers() {
156 if let RData::PTR(ptr) = &record.data {
157 out.push(ptr.to_utf8());
158 }
159 }
160 Ok(out)
161 }
162 Err(e) if is_no_records(&e) => Ok(Vec::new()),
163 Err(e) => Err(DnsError::Temp(e.to_string())),
164 }
165 }
166 }
167}
168
169#[cfg(feature = "hickory")]
170pub use crate::hickory::HickoryResolver;
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn dns_error_display_includes_context() {
178 let e = DnsError::Temp("connection refused".into());
179 let s = format!("{e}");
180 assert!(s.contains("connection refused"));
181 assert!(s.contains("temp"));
182 }
183
184 #[test]
185 fn dns_error_eq_works() {
186 assert_eq!(
187 DnsError::Perm("nxdomain".into()),
188 DnsError::Perm("nxdomain".into())
189 );
190 assert_ne!(
191 DnsError::Temp("x".into()),
192 DnsError::Perm("x".into())
193 );
194 }
195}