1use async_trait::async_trait;
2use hickory_resolver::net::{DnsError as NetDnsError, NetError};
3use hickory_resolver::proto::dnssec::Proof;
4use hickory_resolver::proto::op::ResponseCode;
5use hickory_resolver::proto::rr::rdata::{A, AAAA, PTR, TXT};
6#[cfg(feature = "unbound")]
7use hickory_resolver::proto::rr::DNSClass;
8use hickory_resolver::proto::rr::{
9 LowerName, Name, RData, RecordData, RecordSet, RecordType, RrKey,
10};
11use hickory_resolver::proto::serialize::txt::Parser;
12use hickory_resolver::TokioResolver;
13#[cfg(feature = "unbound")]
14use libunbound::{AsyncContext, Context};
15use std::collections::{BTreeMap, BTreeSet};
16use std::fmt;
17use std::net::IpAddr;
18use std::str::FromStr;
19use std::time::{Duration, Instant};
20use thiserror::Error;
21
22pub struct IpDisplay {
23 pub ip: IpAddr,
24 pub reverse: bool,
25}
26
27impl fmt::Display for IpDisplay {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self.ip {
30 IpAddr::V4(v4) => {
31 let mut bytes = v4.octets();
32 if self.reverse {
33 bytes.reverse();
34 }
35 let mut first = true;
36 for byte in bytes {
37 if !first {
38 f.write_str(".")?;
39 }
40 write!(f, "{byte}")?;
41 first = false;
42 }
43 Ok(())
44 }
45 IpAddr::V6(v6) => {
46 let mut bytes = v6.octets();
47 if self.reverse {
48 bytes.reverse();
49 }
50 let mut first = true;
51 for byte in bytes {
52 if !first {
53 f.write_str(".")?;
54 }
55 let (upper, lower) = (byte >> 4, byte & 0xf);
56 if self.reverse {
57 write!(f, "{lower:x}.{upper:x}")?;
58 } else {
59 write!(f, "{upper:x}.{lower:x}")?;
60 }
61 first = false;
62 }
63 Ok(())
64 }
65 }
66 }
67}
68
69pub fn ptr_host(ip: IpAddr) -> String {
70 let mut out = reverse_ip(ip);
71 out.push_str(match ip {
72 IpAddr::V4(_) => ".in-addr.arpa",
73 IpAddr::V6(_) => ".ip6.arpa",
74 });
75 out
76}
77
78pub fn reverse_ip(ip: IpAddr) -> String {
79 IpDisplay { ip, reverse: true }.to_string()
80}
81
82#[derive(Debug)]
83pub struct Answer {
84 pub canon_name: Option<String>,
85 pub records: Vec<RData>,
86 pub nxdomain: bool,
87 pub secure: bool,
88 pub bogus: bool,
89 pub why_bogus: Option<String>,
90 pub expires: Instant,
91 pub response_code: ResponseCode,
92}
93
94impl Answer {
95 pub fn as_txt(&self) -> Vec<String> {
96 let mut result = vec![];
97 for r in &self.records {
98 if let RData::TXT(txt) = r {
99 let mut joined = String::new();
100 for t in txt.txt_data.iter() {
101 joined.push_str(&String::from_utf8_lossy(t));
102 }
103 result.push(joined);
104 }
105 }
106 result
107 }
108
109 pub fn as_addr(&self) -> Vec<IpAddr> {
110 let mut result = vec![];
111 for r in &self.records {
112 match r {
113 RData::A(a) => result.push(a.0.into()),
114 RData::AAAA(a) => result.push(a.0.into()),
115 _ => {}
116 }
117 }
118 result
119 }
120}
121
122#[derive(Clone, Debug, Error, PartialEq)]
123pub enum DnsError {
124 #[error("invalid DNS name: {0}")]
125 InvalidName(String),
126 #[error("DNS: {0}")]
127 ResolveFailed(String),
128}
129
130impl DnsError {
131 pub(crate) fn from_resolve(name: &impl fmt::Display, err: NetError) -> Self {
132 DnsError::ResolveFailed(format!("failed to query DNS for {name}: {err}"))
133 }
134}
135
136fn no_records(err: &NetError) -> Option<&hickory_resolver::net::NoRecords> {
137 match err {
138 NetError::Dns(NetDnsError::NoRecordsFound(nr)) => Some(nr),
139 _ => None,
140 }
141}
142
143fn dnssec_status(proofs: impl Iterator<Item = Proof>) -> (bool, bool) {
151 let mut any = false;
152 let mut all_secure = true;
153 let mut bogus = false;
154 for proof in proofs {
155 any = true;
156 all_secure &= proof == Proof::Secure;
157 bogus |= proof == Proof::Bogus;
158 }
159 (any && all_secure, bogus)
160}
161
162#[async_trait]
163pub trait Resolver: Send + Sync + 'static {
164 async fn resolve_ip(&self, host: &str) -> Result<Vec<IpAddr>, DnsError>;
165
166 async fn resolve_mx(&self, host: &str) -> Result<Vec<Name>, DnsError>;
167
168 async fn resolve_ptr(&self, ip: IpAddr) -> Result<Vec<Name>, DnsError>;
169
170 async fn resolve_txt(&self, name: &str) -> Result<Answer, DnsError> {
171 let name = Name::from_str_relaxed(name)
172 .map_err(|err| DnsError::InvalidName(format!("invalid name {name}: {err}")))?;
173 self.resolve(name, RecordType::TXT).await
174 }
175
176 async fn resolve(&self, name: Name, rrtype: RecordType) -> Result<Answer, DnsError>;
177}
178
179#[async_trait]
180impl Resolver for Box<dyn Resolver> {
181 async fn resolve_ip(&self, host: &str) -> Result<Vec<IpAddr>, DnsError> {
182 (**self).resolve_ip(host).await
183 }
184
185 async fn resolve_mx(&self, host: &str) -> Result<Vec<Name>, DnsError> {
186 (**self).resolve_mx(host).await
187 }
188
189 async fn resolve_ptr(&self, ip: IpAddr) -> Result<Vec<Name>, DnsError> {
190 (**self).resolve_ptr(ip).await
191 }
192
193 async fn resolve_txt(&self, name: &str) -> Result<Answer, DnsError> {
194 (**self).resolve_txt(name).await
195 }
196
197 async fn resolve(&self, name: Name, rrtype: RecordType) -> Result<Answer, DnsError> {
198 (**self).resolve(name, rrtype).await
199 }
200}
201
202#[derive(Debug, Default)]
203struct TestZone {
204 records: BTreeMap<RrKey, RecordSet>,
205 secure: bool,
207}
208
209#[derive(Debug, Default)]
210pub struct TestResolver {
211 zones: BTreeMap<Name, TestZone>,
212 servfail: BTreeSet<Name>,
215}
216
217impl TestResolver {
218 pub fn with_zone(self, zone: &str) -> Result<Self, String> {
220 self.add_zone(zone, false)
221 }
222
223 pub fn with_secure_zone(self, zone: &str) -> Result<Self, String> {
225 self.add_zone(zone, true)
226 }
227
228 fn add_zone(mut self, zone: &str, secure: bool) -> Result<Self, String> {
229 let (mut name, records) = Parser::new(zone, None, None)
230 .parse()
231 .map_err(|err| format!("{err:#}"))?;
232 name.set_fqdn(true);
236 let fqdn_records = records
237 .into_iter()
238 .map(|(key, value)| {
239 if key.name().is_fqdn() {
240 (key, value)
241 } else {
242 let mut name: Name = key.name().into();
243 name.set_fqdn(true);
244 (RrKey::new(LowerName::new(&name), key.record_type), value)
245 }
246 })
247 .collect();
248 self.zones.insert(
249 name,
250 TestZone {
251 records: fqdn_records,
252 secure,
253 },
254 );
255 Ok(self)
256 }
257
258 pub fn with_servfail(mut self, name: &str) -> Self {
260 let mut name = Name::from_str_relaxed(name)
261 .expect("valid name passed to with_servfail")
262 .to_lowercase();
263 name.set_fqdn(true);
264 self.servfail.insert(name);
265 self
266 }
267
268 pub fn with_txt(self, domain: &str, value: impl Into<String>) -> Self {
269 self.with_txt_multiple(domain, vec![value.into()])
270 }
271
272 pub fn with_txt_multiple(mut self, domain: &str, value: Vec<String>) -> Self {
274 let fqdn = format!("{}.", domain);
275 let authority = Name::from_str_relaxed(&fqdn).unwrap();
276 let key = RrKey {
277 name: LowerName::from_str(&fqdn).unwrap(),
278 record_type: RecordType::TXT,
279 };
280
281 let mut records = RecordSet::new(authority.clone(), RecordType::TXT, 0);
282 for item in value {
283 records.add_rdata(RData::TXT(TXT::new(vec![item])));
284 }
285 self.zones
286 .entry(authority)
287 .or_default()
288 .records
289 .insert(key, records);
290
291 self
292 }
293
294 fn get(&self, full: &Name, record_type: RecordType) -> Result<Answer, DnsError> {
295 let mut full_fqdn = full.clone();
296 full_fqdn.set_fqdn(true);
297
298 if self.servfail.contains(&full_fqdn.to_lowercase()) {
299 return Ok(Answer {
300 canon_name: None,
301 records: vec![],
302 nxdomain: false,
303 secure: false,
304 bogus: false,
305 why_bogus: None,
306 expires: Instant::now() + Duration::from_secs(60),
307 response_code: ResponseCode::ServFail,
308 });
309 }
310
311 let mut authority = full_fqdn.clone();
312
313 let zone = loop {
314 if let Some(zone) = self.zones.get(&authority) {
315 break zone;
316 };
317
318 if authority.num_labels() > 1 {
319 authority = authority.base_name();
320 continue;
321 }
322
323 return Ok(Answer {
324 canon_name: None,
325 records: vec![],
326 nxdomain: true,
327 secure: false,
328 bogus: false,
329 why_bogus: None,
330 expires: Instant::now() + Duration::from_secs(60),
331 response_code: ResponseCode::NXDomain,
332 });
333 };
334
335 let records = zone.records.get(&RrKey {
336 name: LowerName::from(&full_fqdn),
337 record_type,
338 });
339
340 let Some(records) = records else {
341 if record_type != RecordType::CNAME {
349 if let Some(cname_rs) = zone.records.get(&RrKey {
350 name: LowerName::from(&full_fqdn),
351 record_type: RecordType::CNAME,
352 }) {
353 if let Some(target) =
354 cname_rs
355 .records_without_rrsigs()
356 .find_map(|r| match &r.data {
357 RData::CNAME(cname) => Some(cname.0.clone()),
358 _ => None,
359 })
360 {
361 let mut target_answer = self.get(&target, record_type)?;
362 target_answer.canon_name = Some(target.to_ascii());
363 target_answer.secure = target_answer.secure && zone.secure;
364 return Ok(target_answer);
365 }
366 }
367 }
368
369 return Ok(Answer {
370 canon_name: None,
371 records: vec![],
372 nxdomain: false,
373 secure: zone.secure,
374 bogus: false,
375 why_bogus: None,
376 expires: Instant::now() + Duration::from_secs(60),
377 response_code: ResponseCode::NoError,
378 });
379 };
380
381 Ok(Answer {
382 canon_name: None,
383 records: records
384 .records_without_rrsigs()
385 .map(|r| r.data.clone())
386 .collect(),
387 nxdomain: false,
388 secure: zone.secure,
389 bogus: false,
390 why_bogus: None,
391 expires: Instant::now() + Duration::from_secs(60),
392 response_code: ResponseCode::NoError,
393 })
394 }
395}
396
397#[async_trait]
398impl Resolver for TestResolver {
399 async fn resolve_ip(&self, full: &str) -> Result<Vec<IpAddr>, DnsError> {
400 let name = Name::from_str_relaxed(full)
401 .map_err(|err| DnsError::InvalidName(format!("invalid name {full}: {err}")))?;
402
403 let mut values = vec![];
404 let answer = self.get(&name, RecordType::A)?;
405 for record in answer.records {
406 let a = A::try_borrow(&record).unwrap();
407 values.push(IpAddr::V4(a.0));
408 }
409
410 let answer = self.get(&name, RecordType::AAAA)?;
411 for record in answer.records {
412 let a = AAAA::try_borrow(&record).unwrap();
413 values.push(IpAddr::V6(a.0));
414 }
415
416 Ok(values)
417 }
418
419 async fn resolve_mx(&self, full: &str) -> Result<Vec<Name>, DnsError> {
420 let name = Name::from_str_relaxed(full)
421 .map_err(|err| DnsError::InvalidName(format!("invalid name {full}: {err}")))?;
422
423 let mut values = vec![];
424 let answer = self.get(&name, RecordType::MX)?;
425 for record in answer.records {
426 if let RData::MX(mx) = record {
427 values.push(mx.exchange.clone());
428 }
429 }
430
431 Ok(values)
432 }
433
434 async fn resolve_ptr(&self, ip: IpAddr) -> Result<Vec<Name>, DnsError> {
435 let name = ptr_host(ip);
436 let name = Name::from_str_relaxed(ptr_host(ip))
437 .map_err(|err| DnsError::InvalidName(format!("invalid name {name}: {err}")))?;
438
439 let mut values = vec![];
440 let answer = self.get(&name, RecordType::PTR)?;
441 for record in answer.records {
442 match PTR::try_borrow(&record) {
443 Some(ptr) => values.push(ptr.0.clone()),
444 None => {
445 println!("invalid record found for PTR record for {ip}");
446 return Err(DnsError::ResolveFailed(format!(
447 "invalid record found for PTR record for {ip}"
448 )));
449 }
450 };
451 }
452
453 Ok(values)
454 }
455
456 async fn resolve(&self, name: Name, rrtype: RecordType) -> Result<Answer, DnsError> {
457 self.get(&name, rrtype)
458 }
459}
460
461#[cfg(feature = "unbound")]
462pub struct UnboundResolver {
463 cx: AsyncContext,
464}
465
466#[cfg(feature = "unbound")]
467impl UnboundResolver {
468 pub fn new() -> Result<Self, libunbound::Error> {
469 let context = Context::new()?;
471 context.add_builtin_trust_anchors()?;
473 Ok(Self {
474 cx: context.into_async()?,
475 })
476 }
477}
478
479#[cfg(feature = "unbound")]
480#[async_trait]
481impl Resolver for UnboundResolver {
482 async fn resolve_ip(&self, host: &str) -> Result<Vec<IpAddr>, DnsError> {
483 let (a, aaaa) = tokio::join!(
484 self.cx.resolve(host, RecordType::A, DNSClass::IN),
485 self.cx.resolve(host, RecordType::AAAA, DNSClass::IN),
486 );
487
488 let mut records = vec![];
489 match (a, aaaa) {
490 (Ok(a), Ok(aaaa)) => {
491 records.extend(a.rdata().filter_map(|r| match r {
492 Ok(RData::A(a)) => Some(IpAddr::from(a.0)),
493 _ => None,
494 }));
495 records.extend(aaaa.rdata().filter_map(|r| match r {
496 Ok(RData::AAAA(aaaa)) => Some(IpAddr::from(aaaa.0)),
497 _ => None,
498 }));
499 }
500 (Ok(a), Err(_)) => {
501 records.extend(a.rdata().filter_map(|r| match r {
502 Ok(RData::A(a)) => Some(IpAddr::from(a.0)),
503 _ => None,
504 }));
505 }
506 (Err(_), Ok(aaaa)) => {
507 records.extend(aaaa.rdata().filter_map(|r| match r {
508 Ok(RData::AAAA(aaaa)) => Some(IpAddr::from(aaaa.0)),
509 _ => None,
510 }));
511 }
512 (Err(err), Err(_)) => {
513 return Err(DnsError::ResolveFailed(format!(
514 "failed to query DNS for {host}: {err}"
515 )))
516 }
517 }
518
519 Ok(records)
520 }
521
522 async fn resolve_mx(&self, host: &str) -> Result<Vec<Name>, DnsError> {
523 let answer = self
524 .cx
525 .resolve(host, RecordType::A, DNSClass::IN)
526 .await
527 .map_err(|err| {
528 DnsError::ResolveFailed(format!("failed to query DNS for {host}: {err}"))
529 })?;
530
531 Ok(answer
532 .rdata()
533 .filter_map(|r| match r {
534 Ok(RData::MX(mx)) => Some(mx.exchange.clone()),
535 _ => None,
536 })
537 .collect())
538 }
539
540 async fn resolve_ptr(&self, ip: IpAddr) -> Result<Vec<Name>, DnsError> {
541 let name = ptr_host(ip);
542 let answer = self
543 .cx
544 .resolve(&name, RecordType::PTR, DNSClass::IN)
545 .await
546 .map_err(|err| {
547 DnsError::ResolveFailed(format!("failed to query DNS for {name}: {err}"))
548 })?;
549
550 Ok(answer
551 .rdata()
552 .filter_map(|r| match r {
553 Ok(RData::PTR(ptr)) => Some(ptr.0.clone()),
554 _ => None,
555 })
556 .collect())
557 }
558
559 async fn resolve(&self, name: Name, rrtype: RecordType) -> Result<Answer, DnsError> {
560 let name = name.to_ascii();
561 let answer = self
562 .cx
563 .resolve(&name, rrtype, DNSClass::IN)
564 .await
565 .map_err(|err| {
566 DnsError::ResolveFailed(format!("failed to query DNS for {name}: {err}"))
567 })?;
568
569 let mut records = vec![];
570 for r in answer.rdata() {
571 if let Ok(r) = r {
572 records.push(r);
573 }
574 }
575
576 Ok(Answer {
577 canon_name: answer.canon_name().map(|s| s.to_string()),
578 records,
579 nxdomain: answer.nxdomain(),
580 secure: answer.secure(),
581 bogus: answer.bogus(),
582 why_bogus: answer.why_bogus().map(|s| s.to_string()),
583 response_code: answer.rcode(),
584 expires: Instant::now() + Duration::from_secs(answer.ttl() as u64),
585 })
586 }
587}
588
589#[cfg(feature = "unbound")]
590impl From<AsyncContext> for UnboundResolver {
591 fn from(cx: AsyncContext) -> Self {
592 Self { cx }
593 }
594}
595
596pub struct HickoryResolver {
597 inner: TokioResolver,
598}
599
600impl HickoryResolver {
601 pub fn new() -> Result<Self, NetError> {
602 Ok(Self {
603 inner: TokioResolver::builder_tokio()?.build()?,
604 })
605 }
606}
607
608#[async_trait]
609impl Resolver for HickoryResolver {
610 async fn resolve_ip(&self, host: &str) -> Result<Vec<IpAddr>, DnsError> {
611 let name = Name::from_str_relaxed(host)
612 .map_err(|err| DnsError::InvalidName(format!("invalid name {host}: {err}")))?;
613
614 match self.inner.lookup_ip(name.clone()).await {
615 Ok(result) => Ok(result.iter().collect()),
616 Err(err) => match no_records(&err) {
617 Some(_) => Ok(vec![]),
618 None => Err(DnsError::from_resolve(&name, err)),
619 },
620 }
621 }
622
623 async fn resolve_mx(&self, host: &str) -> Result<Vec<Name>, DnsError> {
624 let name = Name::from_str_relaxed(host)
625 .map_err(|err| DnsError::InvalidName(format!("invalid name {host}: {err}")))?;
626
627 match self.inner.mx_lookup(name.clone()).await {
628 Ok(result) => Ok(result
629 .answers()
630 .iter()
631 .filter_map(|r| match &r.data {
632 RData::MX(mx) => Some(mx.exchange.clone()),
633 _ => None,
634 })
635 .collect()),
636 Err(err) => match no_records(&err) {
637 Some(_) => Ok(vec![]),
638 None => Err(DnsError::from_resolve(&name, err)),
639 },
640 }
641 }
642
643 async fn resolve_ptr(&self, ip: IpAddr) -> Result<Vec<Name>, DnsError> {
644 match self.inner.reverse_lookup(ip).await {
645 Ok(result) => Ok(result
646 .answers()
647 .iter()
648 .filter_map(|r| match &r.data {
649 RData::PTR(ptr) => Some(ptr.0.clone()),
650 _ => None,
651 })
652 .collect()),
653 Err(err) => match no_records(&err) {
654 Some(_) => Ok(vec![]),
655 None => Err(DnsError::from_resolve(&ip, err)),
656 },
657 }
658 }
659
660 async fn resolve(&self, name: Name, rrtype: RecordType) -> Result<Answer, DnsError> {
661 match self.inner.lookup(name.clone(), rrtype).await {
662 Ok(result) => {
663 let expires = result.valid_until();
664 let answers = result.answers();
665 let (secure, bogus) = dnssec_status(answers.iter().map(|r| r.proof));
668 let records = answers.iter().map(|r| r.data.clone()).collect();
669 Ok(Answer {
670 canon_name: None,
671 records,
672 nxdomain: false,
673 secure,
674 bogus,
675 why_bogus: None,
676 expires,
677 response_code: ResponseCode::NoError,
678 })
679 }
680 Err(err) => match no_records(&err) {
681 Some(no_records) => {
682 let authorities = no_records.authorities.as_deref().unwrap_or(&[]);
689 let (secure, bogus) = dnssec_status(authorities.iter().map(|r| r.proof));
690 Ok(Answer {
691 canon_name: None,
692 records: vec![],
693 nxdomain: no_records.response_code == ResponseCode::NXDomain,
694 secure,
695 bogus,
696 why_bogus: None,
697 response_code: no_records.response_code,
698 expires: Instant::now()
699 + Duration::from_secs(no_records.negative_ttl.unwrap_or(60) as u64),
700 })
701 }
702 None => Err(DnsError::from_resolve(&name, err)),
703 },
704 }
705 }
706}
707
708impl From<TokioResolver> for HickoryResolver {
709 fn from(inner: TokioResolver) -> Self {
710 Self { inner }
711 }
712}
713
714pub struct AggregateResolver {
722 resolvers: Vec<Box<dyn Resolver>>,
723}
724
725impl AggregateResolver {
726 pub fn new() -> Self {
727 Self { resolvers: vec![] }
728 }
729
730 pub fn push_resolver(&mut self, resolver: Box<dyn Resolver>) {
731 self.resolvers.push(resolver);
732 }
733}
734
735#[async_trait]
736impl Resolver for AggregateResolver {
737 async fn resolve_ip(&self, full: &str) -> Result<Vec<IpAddr>, DnsError> {
738 let mut errors = vec![];
739 for resolver in &self.resolvers {
740 match resolver.resolve_ip(full).await {
741 Ok(ips) if ips.is_empty() => {}
742 Ok(ips) => {
743 return Ok(ips);
744 }
745 Err(err) => errors.push(err),
746 }
747 }
748
749 if let Some(err) = errors.pop() {
750 Err(err)
751 } else {
752 Ok(vec![])
753 }
754 }
755
756 async fn resolve_mx(&self, full: &str) -> Result<Vec<Name>, DnsError> {
757 let mut errors = vec![];
758 for resolver in &self.resolvers {
759 match resolver.resolve_mx(full).await {
760 Ok(ips) if ips.is_empty() => {}
761 Ok(ips) => {
762 return Ok(ips);
763 }
764 Err(err) => errors.push(err),
765 }
766 }
767
768 if let Some(err) = errors.pop() {
769 Err(err)
770 } else {
771 Ok(vec![])
772 }
773 }
774
775 async fn resolve_ptr(&self, ip: IpAddr) -> Result<Vec<Name>, DnsError> {
776 let mut errors = vec![];
777 for resolver in &self.resolvers {
778 match resolver.resolve_ptr(ip).await {
779 Ok(ips) if ips.is_empty() => {}
780 Ok(ips) => {
781 return Ok(ips);
782 }
783 Err(err) => errors.push(err),
784 }
785 }
786
787 if let Some(err) = errors.pop() {
788 Err(err)
789 } else {
790 Ok(vec![])
791 }
792 }
793
794 async fn resolve(&self, name: Name, rrtype: RecordType) -> Result<Answer, DnsError> {
795 let mut errors = vec![];
796 let mut nxdomain = None;
797 for resolver in &self.resolvers {
798 match resolver.resolve(name.clone(), rrtype).await {
799 Ok(answer) => {
800 if !answer.nxdomain {
801 return Ok(answer);
802 }
803 nxdomain.replace(answer);
804 }
805 Err(err) => errors.push(err),
806 }
807 }
808
809 if let Some(err) = errors.pop() {
810 Err(err)
811 } else {
812 match nxdomain.take() {
813 Some(answer) => Ok(answer),
814 None => Ok(Answer {
815 canon_name: None,
816 records: vec![],
817 nxdomain: true,
818 secure: false,
819 bogus: false,
820 why_bogus: None,
821 expires: Instant::now() + Duration::from_secs(60),
822 response_code: ResponseCode::NXDomain,
823 }),
824 }
825 }
826 }
827}
828
829#[cfg(test)]
830mod tests {
831 use super::ptr_host;
832 use std::net::{Ipv4Addr, Ipv6Addr};
833 use std::str::FromStr;
834
835 #[test]
836 fn test_ptr_host() {
837 assert_eq!(
838 ptr_host(Ipv4Addr::new(192, 0, 2, 1).into()),
839 "1.2.0.192.in-addr.arpa"
840 );
841 assert_eq!(
842 ptr_host(Ipv6Addr::from_str("2001:db8::1").unwrap().into()),
843 "1.0.0.0.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.ip6.arpa"
844 );
845 }
846}