1use super::{parse::TxtRecordParser, verify::DomainKey};
8use crate::{
9 DnssecStatus, Error, IpLookupStrategy, MX, MessageAuthenticator, RecordSet, ResolverCache, Txt,
10 dkim::{Atps, DomainKeyReport},
11 dmarc::Dmarc,
12 mta_sts::{MtaSts, TlsRpt},
13 spf::{Macro, Spf},
14};
15use hickory_resolver::{
16 TokioResolver,
17 config::{CLOUDFLARE, GOOGLE, QUAD9, ResolverConfig, ResolverOpts},
18 net::{DnsError, NetError, runtime::TokioRuntimeProvider},
19 proto::{
20 ProtoError,
21 rr::{Name, RData},
22 },
23 system_conf::read_system_conf,
24};
25use std::{
26 borrow::Cow,
27 net::{IpAddr, Ipv4Addr, Ipv6Addr},
28 sync::Arc,
29 time::Instant,
30};
31
32pub struct DnsEntry<T> {
33 pub entry: T,
34 pub expires: Instant,
35}
36
37impl MessageAuthenticator {
38 pub fn new_cloudflare_tls() -> Result<Self, NetError> {
39 Self::new(ResolverConfig::tls(&CLOUDFLARE), ResolverOpts::default())
40 }
41
42 pub fn new_cloudflare() -> Result<Self, NetError> {
43 Self::new(
44 ResolverConfig::udp_and_tcp(&CLOUDFLARE),
45 ResolverOpts::default(),
46 )
47 }
48
49 pub fn new_google() -> Result<Self, NetError> {
50 Self::new(
51 ResolverConfig::udp_and_tcp(&GOOGLE),
52 ResolverOpts::default(),
53 )
54 }
55
56 pub fn new_quad9() -> Result<Self, NetError> {
57 Self::new(ResolverConfig::udp_and_tcp(&QUAD9), ResolverOpts::default())
58 }
59
60 pub fn new_quad9_tls() -> Result<Self, NetError> {
61 Self::new(ResolverConfig::tls(&QUAD9), ResolverOpts::default())
62 }
63
64 pub fn new_system_conf() -> Result<Self, NetError> {
65 let (config, options) = read_system_conf()?;
66 Self::new(config, options)
67 }
68
69 pub fn new(config: ResolverConfig, options: ResolverOpts) -> Result<Self, NetError> {
70 Ok(MessageAuthenticator(
71 TokioResolver::builder_with_config(config, TokioRuntimeProvider::default())
72 .with_options(options)
73 .build()?,
74 ))
75 }
76
77 pub fn resolver(&self) -> &TokioResolver {
78 &self.0
79 }
80
81 pub async fn txt_raw_lookup(&self, key: impl ToFqdn) -> crate::Result<Vec<u8>> {
82 let mut result = vec![];
83 for record in self
84 .0
85 .txt_lookup(Name::from_str_relaxed::<&str>(key.to_fqdn().as_ref())?)
86 .await?
87 .answers()
88 {
89 if let RData::TXT(txt) = &record.data {
90 for item in &txt.txt_data {
91 result.extend_from_slice(item);
92 }
93 }
94 }
95
96 Ok(result)
97 }
98
99 pub async fn txt_lookup<T: TxtRecordParser + Into<Txt> + UnwrapTxtRecord>(
100 &self,
101 key: impl ToFqdn,
102 cache: Option<&impl ResolverCache<Box<str>, Txt>>,
103 ) -> crate::Result<Arc<T>> {
104 let key = key.to_fqdn();
105 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
106 return T::unwrap_txt(value);
107 }
108
109 #[cfg(any(test, feature = "test"))]
110 if true {
111 return mock_resolve(key.as_ref());
112 }
113
114 let txt_lookup = self
115 .0
116 .txt_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
117 .await?;
118 let mut result = Err(Error::InvalidRecordType);
119 let records = txt_lookup.answers().iter().filter_map(|r| {
120 let RData::TXT(txt) = &r.data else {
121 return None;
122 };
123 let txt_data = &txt.txt_data;
124 match txt_data.len() {
125 1 => Some(Cow::from(txt_data[0].as_ref())),
126 0 => None,
127 _ => {
128 let mut entry = Vec::with_capacity(255 * txt_data.len());
129 for data in txt_data {
130 entry.extend_from_slice(data);
131 }
132 Some(Cow::from(entry))
133 }
134 }
135 });
136
137 for record in records {
138 result = T::parse(record.as_ref());
139 if result.is_ok() {
140 break;
141 }
142 }
143
144 let result: Txt = result.into();
145
146 if let Some(cache) = cache {
147 cache.insert(key, result.clone(), txt_lookup.valid_until());
148 }
149
150 T::unwrap_txt(result)
151 }
152
153 pub async fn mx_lookup(
154 &self,
155 key: impl ToFqdn,
156 cache: Option<&impl ResolverCache<Box<str>, RecordSet<MX>>>,
157 ) -> crate::Result<RecordSet<MX>> {
158 let key = key.to_fqdn();
159 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
160 return Ok(value);
161 }
162
163 #[cfg(any(test, feature = "test"))]
164 if true {
165 return mock_resolve(key.as_ref());
166 }
167
168 let mx_lookup = self
169 .0
170 .mx_lookup(Name::from_str_relaxed::<&str>(key.as_ref())?)
171 .await?;
172 let mx_records = mx_lookup.answers();
173 let mut records: Vec<(u16, Vec<Box<str>>)> = Vec::with_capacity(mx_records.len());
174 for mx_record in mx_records {
175 if let RData::MX(mx) = &mx_record.data {
176 let preference = mx.preference;
177 let exchange = mx.exchange.to_lowercase().to_string().into_boxed_str();
178
179 if let Some(record) = records.iter_mut().find(|r| r.0 == preference) {
180 record.1.push(exchange);
181 } else {
182 records.push((preference, vec![exchange]));
183 }
184 }
185 }
186
187 records.sort_unstable_by_key(|a| a.0);
188 let records: Arc<[MX]> = records
189 .into_iter()
190 .map(|(preference, exchanges)| MX {
191 preference,
192 exchanges: exchanges.into_boxed_slice(),
193 })
194 .collect::<Arc<[MX]>>();
195 let records = RecordSet {
196 rrset: records,
197 dnssec_status: DnssecStatus::Indeterminate,
198 };
199
200 if let Some(cache) = cache {
201 cache.insert(key, records.clone(), mx_lookup.valid_until());
202 }
203
204 Ok(records)
205 }
206
207 pub async fn ipv4_lookup(
208 &self,
209 key: impl ToFqdn,
210 cache: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
211 ) -> crate::Result<RecordSet<Ipv4Addr>> {
212 let key = key.to_fqdn();
213 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
214 return Ok(value);
215 }
216
217 let ipv4_lookup = self.ipv4_lookup_raw(key.as_ref()).await?;
218 let records = RecordSet {
219 rrset: ipv4_lookup.entry,
220 dnssec_status: DnssecStatus::Indeterminate,
221 };
222
223 if let Some(cache) = cache {
224 cache.insert(key, records.clone(), ipv4_lookup.expires);
225 }
226
227 Ok(records)
228 }
229
230 pub async fn ipv4_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv4Addr]>>> {
231 #[cfg(any(test, feature = "test"))]
232 if true {
233 return mock_resolve(key);
234 }
235
236 let ipv4_lookup = self
237 .0
238 .ipv4_lookup(Name::from_str_relaxed::<&str>(key)?)
239 .await?;
240 let ips: Arc<[Ipv4Addr]> = ipv4_lookup
241 .answers()
242 .iter()
243 .filter_map(|r| {
244 if let RData::A(a) = &r.data {
245 Some(a.0)
246 } else {
247 None
248 }
249 })
250 .collect::<Vec<Ipv4Addr>>()
251 .into();
252
253 Ok(DnsEntry {
254 entry: ips,
255 expires: ipv4_lookup.valid_until(),
256 })
257 }
258
259 pub async fn ipv6_lookup(
260 &self,
261 key: impl ToFqdn,
262 cache: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
263 ) -> crate::Result<RecordSet<Ipv6Addr>> {
264 let key = key.to_fqdn();
265 if let Some(value) = cache.as_ref().and_then(|c| c.get::<str>(key.as_ref())) {
266 return Ok(value);
267 }
268
269 let ipv6_lookup = self.ipv6_lookup_raw(key.as_ref()).await?;
270 let records = RecordSet {
271 rrset: ipv6_lookup.entry,
272 dnssec_status: DnssecStatus::Indeterminate,
273 };
274
275 if let Some(cache) = cache {
276 cache.insert(key, records.clone(), ipv6_lookup.expires);
277 }
278
279 Ok(records)
280 }
281
282 pub async fn ipv6_lookup_raw(&self, key: &str) -> crate::Result<DnsEntry<Arc<[Ipv6Addr]>>> {
283 #[cfg(any(test, feature = "test"))]
284 if true {
285 return mock_resolve(key);
286 }
287
288 let ipv6_lookup = self
289 .0
290 .ipv6_lookup(Name::from_str_relaxed::<&str>(key)?)
291 .await?;
292 let ips: Arc<[Ipv6Addr]> = ipv6_lookup
293 .answers()
294 .iter()
295 .filter_map(|r| {
296 if let RData::AAAA(aaaa) = &r.data {
297 Some(aaaa.0)
298 } else {
299 None
300 }
301 })
302 .collect::<Vec<Ipv6Addr>>()
303 .into();
304
305 Ok(DnsEntry {
306 entry: ips,
307 expires: ipv6_lookup.valid_until(),
308 })
309 }
310
311 pub async fn ip_lookup(
312 &self,
313 key: &str,
314 mut strategy: IpLookupStrategy,
315 max_results: usize,
316 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
317 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
318 ) -> crate::Result<Vec<IpAddr>> {
319 loop {
320 match strategy {
321 IpLookupStrategy::Ipv4Only | IpLookupStrategy::Ipv4thenIpv6 => {
322 match (self.ipv4_lookup(key, cache_ipv4).await, strategy) {
323 (Ok(result), _) => {
324 return Ok(result
325 .rrset
326 .iter()
327 .take(max_results)
328 .copied()
329 .map(IpAddr::from)
330 .collect());
331 }
332 (Err(err), IpLookupStrategy::Ipv4Only) => return Err(err),
333 _ => {
334 strategy = IpLookupStrategy::Ipv6Only;
335 }
336 }
337 }
338 IpLookupStrategy::Ipv6Only | IpLookupStrategy::Ipv6thenIpv4 => {
339 match (self.ipv6_lookup(key, cache_ipv6).await, strategy) {
340 (Ok(result), _) => {
341 return Ok(result
342 .rrset
343 .iter()
344 .take(max_results)
345 .copied()
346 .map(IpAddr::from)
347 .collect());
348 }
349 (Err(err), IpLookupStrategy::Ipv6Only) => return Err(err),
350 _ => {
351 strategy = IpLookupStrategy::Ipv4Only;
352 }
353 }
354 }
355 }
356 }
357 }
358
359 pub async fn ptr_lookup(
360 &self,
361 addr: IpAddr,
362 cache: Option<&impl ResolverCache<IpAddr, RecordSet<Box<str>>>>,
363 ) -> crate::Result<RecordSet<Box<str>>> {
364 if let Some(value) = cache.as_ref().and_then(|c| c.get(&addr)) {
365 return Ok(value);
366 }
367
368 #[cfg(any(test, feature = "test"))]
369 if true {
370 return mock_resolve(&addr.to_string());
371 }
372
373 let ptr_lookup = self.0.reverse_lookup(addr).await?;
374 let ptr: Arc<[Box<str>]> = ptr_lookup
375 .answers()
376 .iter()
377 .filter_map(|r| {
378 let RData::PTR(ptr) = &r.data else {
379 return None;
380 };
381 if !ptr.is_empty() {
382 Some(ptr.to_lowercase().to_string().into_boxed_str())
383 } else {
384 None
385 }
386 })
387 .collect::<Arc<[Box<str>]>>();
388 let ptr = RecordSet {
389 rrset: ptr,
390 dnssec_status: DnssecStatus::Indeterminate,
391 };
392
393 if let Some(cache) = cache {
394 cache.insert(addr, ptr.clone(), ptr_lookup.valid_until());
395 }
396
397 Ok(ptr)
398 }
399
400 #[cfg(any(test, feature = "test"))]
401 pub async fn exists(
402 &self,
403 key: impl ToFqdn,
404 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
405 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
406 ) -> crate::Result<bool> {
407 let key = key.to_fqdn();
408 match self.ipv4_lookup(key.as_ref(), cache_ipv4).await {
409 Ok(_) => Ok(true),
410 Err(Error::DnsRecordNotFound(_)) => {
411 match self.ipv6_lookup(key.as_ref(), cache_ipv6).await {
412 Ok(_) => Ok(true),
413 Err(Error::DnsRecordNotFound(_)) => Ok(false),
414 Err(err) => Err(err),
415 }
416 }
417 Err(err) => Err(err),
418 }
419 }
420
421 #[cfg(not(any(test, feature = "test")))]
422 pub async fn exists(
423 &self,
424 key: impl ToFqdn,
425 cache_ipv4: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv4Addr>>>,
426 cache_ipv6: Option<&impl ResolverCache<Box<str>, RecordSet<Ipv6Addr>>>,
427 ) -> crate::Result<bool> {
428 let key = key.to_fqdn();
429
430 if cache_ipv4.is_some_and(|c| c.get::<str>(key.as_ref()).is_some())
431 || cache_ipv6.is_some_and(|c| c.get::<str>(key.as_ref()).is_some())
432 {
433 return Ok(true);
434 }
435
436 match self
437 .0
438 .lookup_ip(Name::from_str_relaxed::<&str>(key.as_ref())?)
439 .await
440 {
441 Ok(result) => Ok(result.as_lookup().answers().iter().any(|r| {
442 matches!(
443 &r.data.record_type(),
444 hickory_resolver::proto::rr::RecordType::A
445 | hickory_resolver::proto::rr::RecordType::AAAA
446 )
447 })),
448 Err(err) if err.is_no_records_found() => Ok(false),
449 Err(err) => Err(err.into()),
450 }
451 }
452}
453
454impl From<ProtoError> for Error {
455 fn from(err: ProtoError) -> Self {
456 Error::DnsError(err.to_string())
457 }
458}
459
460impl From<NetError> for Error {
461 fn from(err: NetError) -> Self {
462 match &err {
463 NetError::Dns(DnsError::NoRecordsFound(no_records)) => {
464 Error::DnsRecordNotFound(no_records.response_code)
465 }
466 _ => Error::DnsError(err.to_string()),
467 }
468 }
469}
470
471impl From<DomainKey> for Txt {
472 fn from(v: DomainKey) -> Self {
473 Txt::DomainKey(v.into())
474 }
475}
476
477impl From<DomainKeyReport> for Txt {
478 fn from(v: DomainKeyReport) -> Self {
479 Txt::DomainKeyReport(v.into())
480 }
481}
482
483impl From<Atps> for Txt {
484 fn from(v: Atps) -> Self {
485 Txt::Atps(v.into())
486 }
487}
488
489impl From<Spf> for Txt {
490 fn from(v: Spf) -> Self {
491 Txt::Spf(v.into())
492 }
493}
494
495impl From<Macro> for Txt {
496 fn from(v: Macro) -> Self {
497 Txt::SpfMacro(v.into())
498 }
499}
500
501impl From<Dmarc> for Txt {
502 fn from(v: Dmarc) -> Self {
503 Txt::Dmarc(v.into())
504 }
505}
506
507impl From<MtaSts> for Txt {
508 fn from(v: MtaSts) -> Self {
509 Txt::MtaSts(v.into())
510 }
511}
512
513impl From<TlsRpt> for Txt {
514 fn from(v: TlsRpt) -> Self {
515 Txt::TlsRpt(v.into())
516 }
517}
518
519impl<T: Into<Txt>> From<crate::Result<T>> for Txt {
520 fn from(v: crate::Result<T>) -> Self {
521 match v {
522 Ok(v) => v.into(),
523 Err(err) => Txt::Error(err),
524 }
525 }
526}
527
528pub trait UnwrapTxtRecord: Sized {
529 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>>;
530}
531
532impl UnwrapTxtRecord for DomainKey {
533 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
534 match txt {
535 Txt::DomainKey(a) => Ok(a),
536 Txt::Error(err) => Err(err),
537 _ => Err(Error::Io("Invalid record type".to_string())),
538 }
539 }
540}
541
542impl UnwrapTxtRecord for DomainKeyReport {
543 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
544 match txt {
545 Txt::DomainKeyReport(a) => Ok(a),
546 Txt::Error(err) => Err(err),
547 _ => Err(Error::Io("Invalid record type".to_string())),
548 }
549 }
550}
551
552impl UnwrapTxtRecord for Atps {
553 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
554 match txt {
555 Txt::Atps(a) => Ok(a),
556 Txt::Error(err) => Err(err),
557 _ => Err(Error::Io("Invalid record type".to_string())),
558 }
559 }
560}
561
562impl UnwrapTxtRecord for Spf {
563 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
564 match txt {
565 Txt::Spf(a) => Ok(a),
566 Txt::Error(err) => Err(err),
567 _ => Err(Error::Io("Invalid record type".to_string())),
568 }
569 }
570}
571
572impl UnwrapTxtRecord for Macro {
573 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
574 match txt {
575 Txt::SpfMacro(a) => Ok(a),
576 Txt::Error(err) => Err(err),
577 _ => Err(Error::Io("Invalid record type".to_string())),
578 }
579 }
580}
581
582impl UnwrapTxtRecord for Dmarc {
583 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
584 match txt {
585 Txt::Dmarc(a) => Ok(a),
586 Txt::Error(err) => Err(err),
587 _ => Err(Error::Io("Invalid record type".to_string())),
588 }
589 }
590}
591
592impl UnwrapTxtRecord for MtaSts {
593 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
594 match txt {
595 Txt::MtaSts(a) => Ok(a),
596 Txt::Error(err) => Err(err),
597 _ => Err(Error::Io("Invalid record type".to_string())),
598 }
599 }
600}
601
602impl UnwrapTxtRecord for TlsRpt {
603 fn unwrap_txt(txt: Txt) -> crate::Result<Arc<Self>> {
604 match txt {
605 Txt::TlsRpt(a) => Ok(a),
606 Txt::Error(err) => Err(err),
607 _ => Err(Error::Io("Invalid record type".to_string())),
608 }
609 }
610}
611
612pub trait ToFqdn {
613 fn to_fqdn(&self) -> Box<str>;
614}
615
616impl<T: AsRef<str>> ToFqdn for T {
617 fn to_fqdn(&self) -> Box<str> {
618 let value = self.as_ref();
619 if value.ends_with('.') {
620 value.to_lowercase().into()
621 } else {
622 format!("{}.", value.to_lowercase()).into()
623 }
624 }
625}
626
627pub trait ToReverseName {
628 fn to_reverse_name(&self) -> String;
629}
630
631impl ToReverseName for IpAddr {
632 fn to_reverse_name(&self) -> String {
633 use std::fmt::Write;
634
635 match self {
636 IpAddr::V4(ip) => {
637 let mut segments = String::with_capacity(15);
638 for octet in ip.octets().iter().rev() {
639 if !segments.is_empty() {
640 segments.push('.');
641 }
642 let _ = write!(&mut segments, "{}", octet);
643 }
644 segments
645 }
646 IpAddr::V6(ip) => {
647 let mut segments = String::with_capacity(63);
648 for segment in ip.segments().iter().rev() {
649 for &p in format!("{segment:04x}").as_bytes().iter().rev() {
650 if !segments.is_empty() {
651 segments.push('.');
652 }
653 segments.push(char::from(p));
654 }
655 }
656 segments
657 }
658 }
659 }
660}
661
662#[cfg(any(test, feature = "test"))]
663pub fn mock_resolve<T>(domain: &str) -> crate::Result<T> {
664 Err(if domain.contains("_parse_error.") {
665 Error::ParseError
666 } else if domain.contains("_invalid_record.") {
667 Error::InvalidRecordType
668 } else if domain.contains("_dns_error.") {
669 Error::DnsError("".to_string())
670 } else {
671 Error::DnsRecordNotFound(hickory_resolver::proto::op::ResponseCode::NXDomain)
672 })
673}
674
675#[cfg(test)]
676mod test {
677 use std::net::IpAddr;
678
679 use crate::common::resolver::ToReverseName;
680
681 #[test]
682 fn reverse_lookup_addr() {
683 for (addr, expected) in [
684 ("1.2.3.4", "4.3.2.1"),
685 (
686 "2001:db8::cb01",
687 "1.0.b.c.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2",
688 ),
689 (
690 "2a01:4f9:c011:b43c::1",
691 "1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.c.3.4.b.1.1.0.c.9.f.4.0.1.0.a.2",
692 ),
693 ] {
694 assert_eq!(addr.parse::<IpAddr>().unwrap().to_reverse_name(), expected);
695 }
696 }
697}