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