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