1use std::net::IpAddr;
12use std::str::FromStr;
13use std::time::Duration;
14
15use hickory_resolver::config::{NameServerConfig, ResolveHosts, ResolverConfig, GOOGLE};
16use hickory_resolver::net::runtime::TokioRuntimeProvider;
17use hickory_resolver::net::NetError;
18use hickory_resolver::proto::dnssec::PublicKey;
19use hickory_resolver::proto::rr::rdata::CAA;
20use hickory_resolver::proto::rr::{RData as HickoryRData, RecordType as HickoryRecordType};
21use hickory_resolver::TokioResolver;
22use tracing::{debug, instrument};
23
24use super::records::{DnsRecord, RecordData, RecordType};
25use crate::error::{Result, SeerError};
26use crate::validation::normalize_domain;
27
28fn dns_lookup_or_empty<T>(
32 result: std::result::Result<T, NetError>,
33 record_type: &str,
34) -> Result<Option<T>> {
35 match result {
36 Ok(response) => Ok(Some(response)),
37 Err(e) if e.is_no_records_found() => Ok(None),
38 Err(e) => Err(SeerError::DnsError(format!(
39 "{} lookup failed: {}",
40 record_type, e
41 ))),
42 }
43}
44
45const DEFAULT_TIMEOUT: Duration = Duration::from_secs(5);
48
49fn build_resolver(config: ResolverConfig, timeout: Duration) -> TokioResolver {
56 let mut builder = TokioResolver::builder_with_config(config, TokioRuntimeProvider::default());
57 {
58 let opts = builder.options_mut();
59 opts.timeout = timeout;
60 opts.attempts = 2;
61 opts.use_hosts_file = ResolveHosts::Never;
62 }
63 builder
64 .build()
65 .expect("hickory resolver build is infallible without TLS features")
66}
67
68#[derive(Clone)]
74pub struct DnsResolver {
75 timeout: Duration,
76 default_resolver: TokioResolver,
79}
80
81impl std::fmt::Debug for DnsResolver {
82 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83 f.debug_struct("DnsResolver")
84 .field("timeout", &self.timeout)
85 .finish()
86 }
87}
88
89impl Default for DnsResolver {
90 fn default() -> Self {
91 Self::new()
92 }
93}
94
95impl DnsResolver {
96 pub fn new() -> Self {
98 Self {
99 timeout: DEFAULT_TIMEOUT,
100 default_resolver: build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), DEFAULT_TIMEOUT),
101 }
102 }
103
104 pub fn with_timeout(mut self, timeout: Duration) -> Self {
108 self.timeout = timeout;
109 self.default_resolver = build_resolver(ResolverConfig::udp_and_tcp(&GOOGLE), timeout);
110 self
111 }
112
113 async fn create_custom_resolver(&self, nameserver: &str) -> Result<TokioResolver> {
114 let ips: Vec<IpAddr> = if let Ok(ip) = nameserver.parse::<IpAddr>() {
122 vec![ip]
123 } else {
124 let response = self
125 .default_resolver
126 .lookup_ip(nameserver)
127 .await
128 .map_err(|e| {
129 SeerError::DnsError(format!(
130 "failed to resolve nameserver hostname {}: {}",
131 nameserver, e
132 ))
133 })?;
134 let resolved: Vec<IpAddr> = response.iter().collect();
135 if resolved.is_empty() {
136 return Err(SeerError::DnsError(format!(
137 "nameserver {} did not resolve to any addresses",
138 nameserver
139 )));
140 }
141 resolved
142 };
143
144 for ip in &ips {
148 if let Some(reason) = crate::validation::describe_reserved_ip(ip) {
149 return Err(SeerError::DnsError(format!(
150 "nameserver {} blocked: {}",
151 nameserver, reason
152 )));
153 }
154 }
155
156 let mut config = ResolverConfig::from_parts(None, vec![], vec![]);
161 for ip in ips {
162 config.add_name_server(NameServerConfig::udp(ip));
163 }
164
165 Ok(build_resolver(config, self.timeout))
166 }
167
168 #[instrument(skip(self), fields(domain = %domain, record_type = %record_type))]
175 pub async fn resolve(
176 &self,
177 domain: &str,
178 record_type: RecordType,
179 nameserver: Option<&str>,
180 ) -> Result<Vec<DnsRecord>> {
181 let custom_resolver;
183 let resolver = if let Some(ns) = nameserver {
184 custom_resolver = self.create_custom_resolver(ns).await?;
185 &custom_resolver
186 } else {
187 &self.default_resolver
188 };
189 let domain = normalize_domain(domain)?;
190
191 debug!(nameserver = nameserver.unwrap_or("system"), "Resolving DNS");
192
193 match record_type {
194 RecordType::A => self.resolve_a(resolver, &domain).await,
195 RecordType::AAAA => self.resolve_aaaa(resolver, &domain).await,
196 RecordType::CNAME => self.resolve_cname(resolver, &domain).await,
197 RecordType::MX => self.resolve_mx(resolver, &domain).await,
198 RecordType::NS => self.resolve_ns(resolver, &domain).await,
199 RecordType::TXT => self.resolve_txt(resolver, &domain).await,
200 RecordType::SOA => self.resolve_soa(resolver, &domain).await,
201 RecordType::PTR => self.resolve_ptr(resolver, &domain).await,
202 RecordType::SRV => Err(SeerError::DnsError(
203 "SRV records require service name format: _service._proto.name".to_string(),
204 )),
205 RecordType::CAA => self.resolve_caa(resolver, &domain).await,
206 RecordType::DNSKEY => self.resolve_dnskey(resolver, &domain).await,
207 RecordType::DS => self.resolve_ds(resolver, &domain).await,
208 RecordType::TLSA => self.resolve_tlsa(resolver, &domain).await,
209 RecordType::SSHFP => self.resolve_sshfp(resolver, &domain).await,
210 RecordType::ANY => self.resolve_any(resolver, &domain).await,
211 _ => Err(SeerError::DnsError(format!(
212 "Record type {} not implemented",
213 record_type
214 ))),
215 }
216 }
217
218 #[instrument(skip(self), fields(domain = %domain, service = %service, protocol = %protocol))]
226 pub async fn resolve_srv(
227 &self,
228 service: &str,
229 protocol: &str,
230 domain: &str,
231 nameserver: Option<&str>,
232 ) -> Result<Vec<DnsRecord>> {
233 if !is_valid_srv_label(service) {
235 return Err(SeerError::DnsError(format!(
236 "invalid SRV service name: {}",
237 service
238 )));
239 }
240 if !is_valid_srv_label(protocol) {
241 return Err(SeerError::DnsError(format!(
242 "invalid SRV protocol name: {}",
243 protocol
244 )));
245 }
246
247 let custom_resolver;
248 let resolver = if let Some(ns) = nameserver {
249 custom_resolver = self.create_custom_resolver(ns).await?;
250 &custom_resolver
251 } else {
252 &self.default_resolver
253 };
254 let query_name = format!("_{}._{}.{}", service, protocol, domain);
255
256 let Some(response) = dns_lookup_or_empty(
257 resolver.lookup(&query_name, HickoryRecordType::SRV).await,
258 "SRV",
259 )?
260 else {
261 return Ok(vec![]);
262 };
263
264 let records = response
265 .answers()
266 .iter()
267 .filter_map(|record| {
268 if let HickoryRData::SRV(srv) = &record.data {
269 Some(DnsRecord {
270 name: query_name.clone(),
271 record_type: RecordType::SRV,
272 ttl: record.ttl,
273 data: RecordData::SRV {
274 priority: srv.priority,
275 weight: srv.weight,
276 port: srv.port,
277 target: srv.target.to_string(),
278 },
279 })
280 } else {
281 None
282 }
283 })
284 .collect();
285
286 Ok(records)
287 }
288
289 async fn resolve_a(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
290 let Some(response) =
291 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::A).await, "A")?
292 else {
293 return Ok(vec![]);
294 };
295
296 let records = response
297 .answers()
298 .iter()
299 .filter_map(|record| {
300 if let HickoryRData::A(addr) = &record.data {
301 Some(DnsRecord {
302 name: domain.to_string(),
303 record_type: RecordType::A,
304 ttl: record.ttl,
305 data: RecordData::A {
306 address: addr.0.to_string(),
307 },
308 })
309 } else {
310 None
311 }
312 })
313 .collect();
314
315 Ok(records)
316 }
317
318 async fn resolve_aaaa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
319 let Some(response) = dns_lookup_or_empty(
320 resolver.lookup(domain, HickoryRecordType::AAAA).await,
321 "AAAA",
322 )?
323 else {
324 return Ok(vec![]);
325 };
326
327 let records = response
328 .answers()
329 .iter()
330 .filter_map(|record| {
331 if let HickoryRData::AAAA(addr) = &record.data {
332 Some(DnsRecord {
333 name: domain.to_string(),
334 record_type: RecordType::AAAA,
335 ttl: record.ttl,
336 data: RecordData::AAAA {
337 address: addr.0.to_string(),
338 },
339 })
340 } else {
341 None
342 }
343 })
344 .collect();
345
346 Ok(records)
347 }
348
349 async fn resolve_cname(
350 &self,
351 resolver: &TokioResolver,
352 domain: &str,
353 ) -> Result<Vec<DnsRecord>> {
354 let Some(response) = dns_lookup_or_empty(
355 resolver.lookup(domain, HickoryRecordType::CNAME).await,
356 "CNAME",
357 )?
358 else {
359 return Ok(vec![]);
360 };
361
362 let records = response
363 .answers()
364 .iter()
365 .filter_map(|record| {
366 if let HickoryRData::CNAME(cname) = &record.data {
367 Some(DnsRecord {
368 name: domain.to_string(),
369 record_type: RecordType::CNAME,
370 ttl: record.ttl,
371 data: RecordData::CNAME {
372 target: cname.0.to_string(),
373 },
374 })
375 } else {
376 None
377 }
378 })
379 .collect();
380
381 Ok(records)
382 }
383
384 async fn resolve_mx(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
385 let Some(response) =
386 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::MX).await, "MX")?
387 else {
388 return Ok(vec![]);
389 };
390
391 let mut records: Vec<DnsRecord> = response
392 .answers()
393 .iter()
394 .filter_map(|record| {
395 if let HickoryRData::MX(mx) = &record.data {
396 Some(DnsRecord {
397 name: domain.to_string(),
398 record_type: RecordType::MX,
399 ttl: record.ttl,
400 data: RecordData::MX {
401 preference: mx.preference,
402 exchange: mx.exchange.to_string(),
403 },
404 })
405 } else {
406 None
407 }
408 })
409 .collect();
410
411 records.sort_by_key(|r| {
412 if let RecordData::MX { preference, .. } = &r.data {
413 *preference
414 } else {
415 0
416 }
417 });
418
419 Ok(records)
420 }
421
422 async fn resolve_ns(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
423 let Some(response) =
424 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::NS).await, "NS")?
425 else {
426 return Ok(vec![]);
427 };
428
429 let records = response
430 .answers()
431 .iter()
432 .filter_map(|record| {
433 if let HickoryRData::NS(ns) = &record.data {
434 Some(DnsRecord {
435 name: domain.to_string(),
436 record_type: RecordType::NS,
437 ttl: record.ttl,
438 data: RecordData::NS {
439 nameserver: ns.0.to_string(),
440 },
441 })
442 } else {
443 None
444 }
445 })
446 .collect();
447
448 Ok(records)
449 }
450
451 async fn resolve_txt(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
452 let Some(response) =
453 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::TXT).await, "TXT")?
454 else {
455 return Ok(vec![]);
456 };
457
458 let records = response
459 .answers()
460 .iter()
461 .filter_map(|record| {
462 if let HickoryRData::TXT(txt) = &record.data {
463 let text = txt
464 .txt_data
465 .iter()
466 .map(|data| String::from_utf8_lossy(data).to_string())
467 .collect::<Vec<_>>()
468 .join("");
469
470 Some(DnsRecord {
471 name: domain.to_string(),
472 record_type: RecordType::TXT,
473 ttl: record.ttl,
474 data: RecordData::TXT { text },
475 })
476 } else {
477 None
478 }
479 })
480 .collect();
481
482 Ok(records)
483 }
484
485 async fn resolve_soa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
486 let Some(response) =
487 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::SOA).await, "SOA")?
488 else {
489 return Ok(vec![]);
490 };
491
492 let records = response
493 .answers()
494 .iter()
495 .filter_map(|record| {
496 if let HickoryRData::SOA(soa) = &record.data {
497 Some(DnsRecord {
498 name: domain.to_string(),
499 record_type: RecordType::SOA,
500 ttl: record.ttl,
501 data: RecordData::SOA {
502 mname: soa.mname.to_string(),
503 rname: soa.rname.to_string(),
504 serial: soa.serial,
505 refresh: soa.refresh.try_into().unwrap_or(0),
506 retry: soa.retry.try_into().unwrap_or(0),
507 expire: soa.expire.try_into().unwrap_or(0),
508 minimum: soa.minimum,
509 },
510 })
511 } else {
512 None
513 }
514 })
515 .collect();
516
517 Ok(records)
518 }
519
520 async fn resolve_ptr(&self, resolver: &TokioResolver, query: &str) -> Result<Vec<DnsRecord>> {
521 let query = if let Ok(ip) = IpAddr::from_str(query) {
523 reverse_dns_name(&ip)
524 } else {
525 query.to_string()
526 };
527
528 let Some(response) =
529 dns_lookup_or_empty(resolver.lookup(&query, HickoryRecordType::PTR).await, "PTR")?
530 else {
531 return Ok(vec![]);
532 };
533
534 let records = response
535 .answers()
536 .iter()
537 .filter_map(|record| {
538 if let HickoryRData::PTR(ptr) = &record.data {
539 Some(DnsRecord {
540 name: query.clone(),
541 record_type: RecordType::PTR,
542 ttl: record.ttl,
543 data: RecordData::PTR {
544 target: ptr.0.to_string(),
545 },
546 })
547 } else {
548 None
549 }
550 })
551 .collect();
552
553 Ok(records)
554 }
555
556 async fn resolve_caa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
557 let Some(response) =
558 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::CAA).await, "CAA")?
559 else {
560 return Ok(vec![]);
561 };
562
563 let records = response
564 .answers()
565 .iter()
566 .filter_map(|record| {
567 if let HickoryRData::CAA(caa) = &record.data {
568 let (flags, tag, value) = parse_caa(caa);
569 Some(DnsRecord {
570 name: domain.to_string(),
571 record_type: RecordType::CAA,
572 ttl: record.ttl,
573 data: RecordData::CAA { flags, tag, value },
574 })
575 } else {
576 None
577 }
578 })
579 .collect();
580
581 Ok(records)
582 }
583
584 async fn resolve_dnskey(
585 &self,
586 resolver: &TokioResolver,
587 domain: &str,
588 ) -> Result<Vec<DnsRecord>> {
589 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
590
591 let Some(response) = dns_lookup_or_empty(
592 resolver.lookup(domain, HickoryRecordType::DNSKEY).await,
593 "DNSKEY",
594 )?
595 else {
596 return Ok(vec![]);
597 };
598
599 let records = response
600 .answers()
601 .iter()
602 .filter_map(|record| {
603 if let HickoryRData::DNSSEC(DNSSECRData::DNSKEY(dnskey)) = &record.data {
604 use base64::{engine::general_purpose::STANDARD, Engine};
605 let public_key_buf = dnskey.public_key();
606 let public_key = STANDARD.encode(public_key_buf.public_bytes());
607 Some(DnsRecord {
608 name: domain.to_string(),
609 record_type: RecordType::DNSKEY,
610 ttl: record.ttl,
611 data: RecordData::DNSKEY {
612 flags: dnskey.flags(),
613 protocol: 3,
615 algorithm: u8::from(public_key_buf.algorithm()),
616 public_key,
617 },
618 })
619 } else {
620 None
621 }
622 })
623 .collect();
624
625 Ok(records)
626 }
627
628 async fn resolve_ds(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
629 use hickory_resolver::proto::dnssec::rdata::DNSSECRData;
630
631 let Some(response) =
632 dns_lookup_or_empty(resolver.lookup(domain, HickoryRecordType::DS).await, "DS")?
633 else {
634 return Ok(vec![]);
635 };
636
637 let records = response
638 .answers()
639 .iter()
640 .filter_map(|record| {
641 if let HickoryRData::DNSSEC(DNSSECRData::DS(ds)) = &record.data {
642 let digest = ds
643 .digest()
644 .iter()
645 .map(|b| format!("{:02X}", b))
646 .collect::<String>();
647 Some(DnsRecord {
648 name: domain.to_string(),
649 record_type: RecordType::DS,
650 ttl: record.ttl,
651 data: RecordData::DS {
652 key_tag: ds.key_tag(),
653 algorithm: u8::from(ds.algorithm()),
654 digest_type: u8::from(ds.digest_type()),
655 digest,
656 },
657 })
658 } else {
659 None
660 }
661 })
662 .collect();
663
664 Ok(records)
665 }
666
667 async fn resolve_tlsa(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
668 let Some(response) = dns_lookup_or_empty(
674 resolver.lookup(domain, HickoryRecordType::TLSA).await,
675 "TLSA",
676 )?
677 else {
678 return Ok(vec![]);
679 };
680
681 let records = response
682 .answers()
683 .iter()
684 .filter_map(|record| {
685 if let HickoryRData::TLSA(tlsa) = &record.data {
686 let cert_data = tlsa
687 .cert_data
688 .iter()
689 .map(|b| format!("{:02X}", b))
690 .collect::<String>();
691 Some(DnsRecord {
692 name: domain.to_string(),
693 record_type: RecordType::TLSA,
694 ttl: record.ttl,
695 data: RecordData::TLSA {
696 cert_usage: u8::from(tlsa.cert_usage),
697 selector: u8::from(tlsa.selector),
698 matching: u8::from(tlsa.matching),
699 cert_data,
700 },
701 })
702 } else {
703 None
704 }
705 })
706 .collect();
707
708 Ok(records)
709 }
710
711 async fn resolve_sshfp(
712 &self,
713 resolver: &TokioResolver,
714 domain: &str,
715 ) -> Result<Vec<DnsRecord>> {
716 let Some(response) = dns_lookup_or_empty(
717 resolver.lookup(domain, HickoryRecordType::SSHFP).await,
718 "SSHFP",
719 )?
720 else {
721 return Ok(vec![]);
722 };
723
724 let records = response
725 .answers()
726 .iter()
727 .filter_map(|record| {
728 if let HickoryRData::SSHFP(sshfp) = &record.data {
729 let fingerprint = sshfp
730 .fingerprint
731 .iter()
732 .map(|b| format!("{:02X}", b))
733 .collect::<String>();
734 Some(DnsRecord {
735 name: domain.to_string(),
736 record_type: RecordType::SSHFP,
737 ttl: record.ttl,
738 data: RecordData::SSHFP {
739 algorithm: u8::from(sshfp.algorithm),
740 fingerprint_type: u8::from(sshfp.fingerprint_type),
741 fingerprint,
742 },
743 })
744 } else {
745 None
746 }
747 })
748 .collect();
749
750 Ok(records)
751 }
752
753 async fn resolve_any(&self, resolver: &TokioResolver, domain: &str) -> Result<Vec<DnsRecord>> {
754 let mut all_records = Vec::new();
755
756 let record_types = [
758 RecordType::A,
759 RecordType::AAAA,
760 RecordType::MX,
761 RecordType::NS,
762 RecordType::TXT,
763 RecordType::SOA,
764 RecordType::CAA,
765 ];
766
767 for record_type in record_types {
768 match self.resolve_type(resolver, domain, record_type).await {
769 Ok(records) => all_records.extend(records),
770 Err(_) => continue, }
772 }
773
774 Ok(all_records)
775 }
776
777 async fn resolve_type(
778 &self,
779 resolver: &TokioResolver,
780 domain: &str,
781 record_type: RecordType,
782 ) -> Result<Vec<DnsRecord>> {
783 match record_type {
784 RecordType::A => self.resolve_a(resolver, domain).await,
785 RecordType::AAAA => self.resolve_aaaa(resolver, domain).await,
786 RecordType::CNAME => self.resolve_cname(resolver, domain).await,
787 RecordType::MX => self.resolve_mx(resolver, domain).await,
788 RecordType::NS => self.resolve_ns(resolver, domain).await,
789 RecordType::TXT => self.resolve_txt(resolver, domain).await,
790 RecordType::SOA => self.resolve_soa(resolver, domain).await,
791 RecordType::CAA => self.resolve_caa(resolver, domain).await,
792 RecordType::DNSKEY => self.resolve_dnskey(resolver, domain).await,
793 RecordType::DS => self.resolve_ds(resolver, domain).await,
794 _ => Err(SeerError::DnsError("unsupported record type".to_string())),
795 }
796 }
797}
798
799#[derive(Debug, Clone, Copy, PartialEq, Eq)]
804pub enum DnsPresence {
805 Present,
807 Absent,
809 Unknown,
811}
812
813fn classify_ns_presence(result: &Result<Vec<DnsRecord>>) -> DnsPresence {
818 match result {
819 Ok(records) if records.is_empty() => DnsPresence::Absent,
820 Ok(_) => DnsPresence::Present,
821 Err(_) => DnsPresence::Unknown,
822 }
823}
824
825impl DnsResolver {
826 pub async fn presence(&self, domain: &str) -> DnsPresence {
834 classify_ns_presence(&self.resolve(domain, RecordType::NS, None).await)
835 }
836}
837
838fn reverse_dns_name(ip: &IpAddr) -> String {
841 match ip {
842 IpAddr::V4(addr) => {
843 let octets = addr.octets();
844 format!(
845 "{}.{}.{}.{}.in-addr.arpa",
846 octets[3], octets[2], octets[1], octets[0]
847 )
848 }
849 IpAddr::V6(addr) => {
850 let segments = addr.segments();
851 let mut result = String::with_capacity(72);
853 let mut first = true;
854 for segment in segments.iter().rev() {
855 for shift in [0, 4, 8, 12] {
856 if !first {
857 result.push('.');
858 }
859 first = false;
860 let nibble = (segment >> shift) & 0xF;
861 result
862 .push(char::from_digit(nibble as u32, 16).expect("nibble is always 0-15"));
863 }
864 }
865 result.push_str(".ip6.arpa");
866 result
867 }
868 }
869}
870
871fn parse_caa(caa: &CAA) -> (u8, String, String) {
872 let flags = if caa.issuer_critical { 128 } else { 0 };
879 let tag = caa.tag.clone();
880 let value = String::from_utf8_lossy(&caa.value).to_string();
881 (flags, tag, value)
882}
883
884fn is_valid_srv_label(label: &str) -> bool {
886 !label.is_empty()
887 && label.len() <= 63
888 && label.chars().all(|c| c.is_ascii_alphanumeric() || c == '-')
889 && !label.starts_with('-')
890 && !label.ends_with('-')
891}
892
893#[cfg(test)]
894mod tests {
895 use super::*;
905 use std::net::{Ipv4Addr, Ipv6Addr};
906
907 #[test]
910 fn record_type_from_str_accepts_lowercase() {
911 assert_eq!(RecordType::from_str("a").unwrap(), RecordType::A);
912 assert_eq!(RecordType::from_str("mx").unwrap(), RecordType::MX);
913 assert_eq!(RecordType::from_str("cname").unwrap(), RecordType::CNAME);
914 assert_eq!(RecordType::from_str("dnskey").unwrap(), RecordType::DNSKEY);
915 }
916
917 #[test]
918 fn record_type_from_str_accepts_mixed_case() {
919 assert_eq!(RecordType::from_str("Mx").unwrap(), RecordType::MX);
920 assert_eq!(RecordType::from_str("cNaMe").unwrap(), RecordType::CNAME);
921 }
922
923 #[test]
924 fn record_type_from_str_rejects_whitespace_padded() {
925 assert!(RecordType::from_str(" A").is_err());
929 assert!(RecordType::from_str("A ").is_err());
930 assert!(RecordType::from_str("\tA\n").is_err());
931 }
932
933 #[test]
934 fn record_type_from_str_rejects_unknown() {
935 assert!(RecordType::from_str("NOTAREAL").is_err());
936 assert!(RecordType::from_str("A1").is_err());
937 assert!(RecordType::from_str("").is_err());
938 }
939
940 #[test]
941 fn record_type_from_str_accepts_star_as_any() {
942 assert_eq!(RecordType::from_str("*").unwrap(), RecordType::ANY);
943 assert_eq!(RecordType::from_str("ANY").unwrap(), RecordType::ANY);
944 assert_eq!(RecordType::from_str("any").unwrap(), RecordType::ANY);
945 }
946
947 #[test]
950 fn srv_label_accepts_alphanumeric_and_hyphen() {
951 assert!(is_valid_srv_label("http"));
952 assert!(is_valid_srv_label("ldap-tls"));
953 assert!(is_valid_srv_label("a1"));
954 assert!(is_valid_srv_label("tcp"));
955 }
956
957 #[test]
958 fn srv_label_rejects_empty() {
959 assert!(!is_valid_srv_label(""));
960 }
961
962 #[test]
963 fn srv_label_rejects_leading_or_trailing_hyphen() {
964 assert!(!is_valid_srv_label("-http"));
965 assert!(!is_valid_srv_label("http-"));
966 assert!(!is_valid_srv_label("-"));
967 }
968
969 #[test]
970 fn srv_label_rejects_dots() {
971 assert!(!is_valid_srv_label("http.evil"));
974 assert!(!is_valid_srv_label("a.b"));
975 }
976
977 #[test]
978 fn srv_label_rejects_special_chars() {
979 assert!(!is_valid_srv_label("http evil"));
980 assert!(!is_valid_srv_label("http/evil"));
981 assert!(!is_valid_srv_label("http\0"));
982 assert!(!is_valid_srv_label("http\n"));
983 }
984
985 #[test]
986 fn srv_label_rejects_over_63_chars() {
987 let too_long = "a".repeat(64);
988 assert!(!is_valid_srv_label(&too_long));
989 let exactly_63 = "a".repeat(63);
990 assert!(is_valid_srv_label(&exactly_63));
991 }
992
993 #[test]
996 fn classify_ns_presence_absent_on_empty_ok() {
997 let r: Result<Vec<DnsRecord>> = Ok(vec![]);
999 assert_eq!(classify_ns_presence(&r), DnsPresence::Absent);
1000 }
1001
1002 #[test]
1003 fn classify_ns_presence_present_on_records() {
1004 let rec = DnsRecord {
1005 name: "example.test.".to_string(),
1006 record_type: RecordType::NS,
1007 ttl: 3600,
1008 data: RecordData::NS {
1009 nameserver: "ns1.example.net.".to_string(),
1010 },
1011 };
1012 let r: Result<Vec<DnsRecord>> = Ok(vec![rec]);
1013 assert_eq!(classify_ns_presence(&r), DnsPresence::Present);
1014 }
1015
1016 #[test]
1017 fn classify_ns_presence_unknown_on_error() {
1018 let r: Result<Vec<DnsRecord>> = Err(SeerError::DnsError("servfail".to_string()));
1019 assert_eq!(classify_ns_presence(&r), DnsPresence::Unknown);
1020 }
1021
1022 #[test]
1025 fn reverse_dns_name_formats_ipv4_correctly() {
1026 let ip: IpAddr = Ipv4Addr::new(192, 0, 2, 1).into();
1027 assert_eq!(reverse_dns_name(&ip), "1.2.0.192.in-addr.arpa");
1028 }
1029
1030 #[test]
1031 fn reverse_dns_name_formats_ipv6_correctly() {
1032 let ip: IpAddr = Ipv6Addr::LOCALHOST.into();
1034 let name = reverse_dns_name(&ip);
1035 assert!(
1036 name.ends_with(".ip6.arpa"),
1037 "must end with .ip6.arpa; got: {}",
1038 name
1039 );
1040 assert!(
1042 name.starts_with("1."),
1043 "expected '1.' prefix, got: {}",
1044 name
1045 );
1046 assert_eq!(name.len(), 72);
1048 }
1049
1050 #[test]
1053 fn resolver_new_has_default_timeout() {
1054 let r = DnsResolver::new();
1055 assert_eq!(r.timeout, DEFAULT_TIMEOUT);
1056 }
1057
1058 #[test]
1059 fn resolver_with_timeout_overrides_default() {
1060 let custom = Duration::from_secs(42);
1061 let r = DnsResolver::new().with_timeout(custom);
1062 assert_eq!(r.timeout, custom);
1063 }
1064
1065 #[test]
1066 fn resolver_default_matches_new() {
1067 let a = DnsResolver::default();
1068 let b = DnsResolver::new();
1069 assert_eq!(a.timeout, b.timeout);
1070 }
1071
1072 #[tokio::test]
1075 async fn custom_resolver_rejects_invalid_input() {
1076 let r = DnsResolver::new();
1081 let err = r.create_custom_resolver("..").await.unwrap_err();
1082 let msg = err.to_string().to_lowercase();
1083 assert!(
1084 msg.contains("dns resolution failed") || msg.contains("invalid"),
1085 "expected resolution failure, got: {}",
1086 msg
1087 );
1088 }
1089
1090 #[tokio::test]
1091 async fn custom_resolver_rejects_private_ipv4() {
1092 let r = DnsResolver::new();
1095 for reserved in ["127.0.0.1", "10.0.0.1", "192.168.1.1", "169.254.169.254"] {
1096 let err = r.create_custom_resolver(reserved).await.unwrap_err();
1097 let msg = err.to_string().to_lowercase();
1098 assert!(
1099 msg.contains("blocked") || msg.contains("reserved"),
1100 "reserved IP {} must be rejected, got error: {}",
1101 reserved,
1102 msg
1103 );
1104 }
1105 }
1106
1107 #[tokio::test]
1108 async fn custom_resolver_rejects_loopback_ipv6() {
1109 let r = DnsResolver::new();
1110 let err = r.create_custom_resolver("::1").await.unwrap_err();
1111 let msg = err.to_string().to_lowercase();
1112 assert!(
1113 msg.contains("blocked") || msg.contains("reserved"),
1114 "::1 must be rejected, got error: {}",
1115 msg
1116 );
1117 }
1118
1119 #[tokio::test]
1120 async fn custom_resolver_accepts_public_ipv4() {
1121 let r = DnsResolver::new();
1123 let result = r.create_custom_resolver("8.8.8.8").await;
1124 assert!(
1125 result.is_ok(),
1126 "8.8.8.8 must be accepted as a public nameserver, got: {:?}",
1127 result.err()
1128 );
1129 }
1130
1131 #[tokio::test]
1134 async fn resolve_srv_rejects_invalid_service_label() {
1135 let r = DnsResolver::new();
1136 let result = r.resolve_srv("http.evil", "tcp", "example.com", None).await;
1138 assert!(result.is_err());
1139 let msg = result.unwrap_err().to_string().to_lowercase();
1140 assert!(
1141 msg.contains("invalid srv service"),
1142 "expected SRV service validation error, got: {}",
1143 msg
1144 );
1145 }
1146
1147 #[tokio::test]
1148 async fn resolve_srv_rejects_invalid_protocol_label() {
1149 let r = DnsResolver::new();
1150 let result = r.resolve_srv("http", "tcp.evil", "example.com", None).await;
1151 assert!(result.is_err());
1152 let msg = result.unwrap_err().to_string().to_lowercase();
1153 assert!(
1154 msg.contains("invalid srv protocol"),
1155 "expected SRV protocol validation error, got: {}",
1156 msg
1157 );
1158 }
1159
1160 #[tokio::test]
1163 async fn resolve_normalizes_uppercase_domain_input() {
1164 let r = DnsResolver::new();
1169 let result = r.resolve(".bad.example", RecordType::A, None).await;
1170 assert!(result.is_err(), "leading-dot domain must be rejected");
1171 }
1172
1173 #[tokio::test]
1176 async fn resolve_rejects_srv_record_type_without_srv_helper() {
1177 let r = DnsResolver::new();
1180 let result = r.resolve("example.com", RecordType::SRV, None).await;
1181 assert!(result.is_err());
1182 let msg = result.unwrap_err().to_string();
1183 assert!(
1184 msg.contains("SRV records require service name format"),
1185 "expected helpful SRV error, got: {}",
1186 msg
1187 );
1188 }
1189}