View Javadoc
1   /*
2    * Copyright 2014 The Netty Project
3    *
4    * The Netty Project licenses this file to you under the Apache License,
5    * version 2.0 (the "License"); you may not use this file except in compliance
6    * with the License. You may obtain a copy of the License at:
7    *
8    *   https://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12   * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13   * License for the specific language governing permissions and limitations
14   * under the License.
15   */
16  
17  package io.netty.resolver.dns;
18  
19  import io.netty.buffer.ByteBuf;
20  import io.netty.buffer.ByteBufHolder;
21  import io.netty.channel.AddressedEnvelope;
22  import io.netty.channel.Channel;
23  import io.netty.channel.EventLoop;
24  import io.netty.handler.codec.CorruptedFrameException;
25  import io.netty.handler.codec.dns.DefaultDnsQuestion;
26  import io.netty.handler.codec.dns.DefaultDnsRecordDecoder;
27  import io.netty.handler.codec.dns.DnsQuestion;
28  import io.netty.handler.codec.dns.DnsRawRecord;
29  import io.netty.handler.codec.dns.DnsRecord;
30  import io.netty.handler.codec.dns.DnsRecordType;
31  import io.netty.handler.codec.dns.DnsResponse;
32  import io.netty.handler.codec.dns.DnsResponseCode;
33  import io.netty.handler.codec.dns.DnsSection;
34  import io.netty.util.NetUtil;
35  import io.netty.util.ReferenceCountUtil;
36  import io.netty.util.concurrent.Future;
37  import io.netty.util.concurrent.FutureListener;
38  import io.netty.util.concurrent.Promise;
39  import io.netty.util.internal.ObjectUtil;
40  import io.netty.util.internal.PlatformDependent;
41  import io.netty.util.internal.StringUtil;
42  import io.netty.util.internal.SuppressJava6Requirement;
43  import io.netty.util.internal.SystemPropertyUtil;
44  import io.netty.util.internal.ThrowableUtil;
45  import io.netty.util.internal.logging.InternalLogger;
46  import io.netty.util.internal.logging.InternalLoggerFactory;
47  
48  import java.net.InetAddress;
49  import java.net.InetSocketAddress;
50  import java.net.UnknownHostException;
51  import java.util.AbstractList;
52  import java.util.ArrayList;
53  import java.util.Arrays;
54  import java.util.Collections;
55  import java.util.HashMap;
56  import java.util.IdentityHashMap;
57  import java.util.Iterator;
58  import java.util.List;
59  import java.util.Locale;
60  import java.util.Map;
61  import java.util.NoSuchElementException;
62  import java.util.Set;
63  
64  import static io.netty.handler.codec.dns.DnsResponseCode.NXDOMAIN;
65  import static io.netty.handler.codec.dns.DnsResponseCode.SERVFAIL;
66  import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress;
67  import static java.lang.Math.min;
68  
69  abstract class DnsResolveContext<T> {
70      private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsResolveContext.class);
71      private static final String PROP_TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS =
72              "io.netty.resolver.dns.tryCnameOnAddressLookups";
73      static boolean TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS;
74  
75      static {
76          TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS =
77                  SystemPropertyUtil.getBoolean(PROP_TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS, false);
78  
79          if (logger.isDebugEnabled()) {
80              logger.debug("-D{}: {}", PROP_TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS, TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS);
81          }
82      }
83  
84      private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION =
85              DnsResolveContextException.newStatic("No answer found and NXDOMAIN response code returned",
86              DnsResolveContext.class, "onResponse(..)");
87      private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION =
88              DnsResolveContextException.newStatic("No matching CNAME record found",
89              DnsResolveContext.class, "onResponseCNAME(..)");
90      private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION =
91              DnsResolveContextException.newStatic("No matching record type found",
92              DnsResolveContext.class, "onResponseAorAAAA(..)");
93      private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION =
94              DnsResolveContextException.newStatic("Response type was unrecognized",
95              DnsResolveContext.class, "onResponse(..)");
96      private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION =
97              DnsResolveContextException.newStatic("No name servers returned an answer",
98              DnsResolveContext.class, "tryToFinishResolve(..)");
99      private static final RuntimeException SERVFAIL_QUERY_FAILED_EXCEPTION =
100             DnsErrorCauseException.newStatic("Query failed with SERVFAIL", SERVFAIL,
101                     DnsResolveContext.class, "onResponse(..)");
102     private static final RuntimeException NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION =
103             DnsErrorCauseException.newStatic("Query failed with NXDOMAIN", NXDOMAIN,
104                     DnsResolveContext.class, "onResponse(..)");
105 
106     final DnsNameResolver parent;
107     private final Channel channel;
108     private final Future<? extends Channel> channelReadyFuture;
109     private final Promise<?> originalPromise;
110     private final DnsServerAddressStream nameServerAddrs;
111     private final String hostname;
112     private final int dnsClass;
113     private final DnsRecordType[] expectedTypes;
114     final DnsRecord[] additionals;
115 
116     private final Set<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> queriesInProgress =
117             Collections.newSetFromMap(
118                     new IdentityHashMap<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>, Boolean>());
119 
120     private List<T> finalResult;
121     private int allowedQueries;
122     private boolean triedCNAME;
123     private boolean completeEarly;
124 
125     DnsResolveContext(DnsNameResolver parent, Channel channel, Future<? extends Channel> channelReadyFuture,
126                       Promise<?> originalPromise, String hostname, int dnsClass, DnsRecordType[] expectedTypes,
127                       DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs, int allowedQueries) {
128         assert expectedTypes.length > 0;
129 
130         this.parent = parent;
131         this.channel = channel;
132         this.channelReadyFuture = channelReadyFuture;
133         this.originalPromise = originalPromise;
134         this.hostname = hostname;
135         this.dnsClass = dnsClass;
136         this.expectedTypes = expectedTypes;
137         this.additionals = additionals;
138 
139         this.nameServerAddrs = ObjectUtil.checkNotNull(nameServerAddrs, "nameServerAddrs");
140         this.allowedQueries = allowedQueries;
141     }
142 
143     static final class DnsResolveContextException extends RuntimeException {
144 
145         private static final long serialVersionUID = 1209303419266433003L;
146 
147         private DnsResolveContextException(String message) {
148             super(message);
149         }
150 
151         @SuppressJava6Requirement(reason = "uses Java 7+ Exception.<init>(String, Throwable, boolean, boolean)" +
152                 " but is guarded by version checks")
153         private DnsResolveContextException(String message, boolean shared) {
154             super(message, null, false, true);
155             assert shared;
156         }
157 
158         // Override fillInStackTrace() so we not populate the backtrace via a native call and so leak the
159         // Classloader.
160         @Override
161         public Throwable fillInStackTrace() {
162             return this;
163         }
164 
165         static DnsResolveContextException newStatic(String message, Class<?> clazz, String method) {
166             final DnsResolveContextException exception;
167             if (PlatformDependent.javaVersion() >= 7) {
168                 exception = new DnsResolveContextException(message, true);
169             } else {
170                 exception = new DnsResolveContextException(message);
171             }
172             return ThrowableUtil.unknownStackTrace(exception, clazz, method);
173         }
174     }
175 
176     /**
177      * The {@link Channel} used.
178      */
179     Channel channel() {
180         return channel;
181     }
182 
183     /**
184      * The {@link DnsCache} to use while resolving.
185      */
186     DnsCache resolveCache() {
187         return parent.resolveCache();
188     }
189 
190     /**
191      * The {@link DnsCnameCache} that is used for resolving.
192      */
193     DnsCnameCache cnameCache() {
194         return parent.cnameCache();
195     }
196 
197     /**
198      * The {@link AuthoritativeDnsServerCache} to use while resolving.
199      */
200     AuthoritativeDnsServerCache authoritativeDnsServerCache() {
201         return parent.authoritativeDnsServerCache();
202     }
203 
204     /**
205      * Creates a new context with the given parameters.
206      */
207     abstract DnsResolveContext<T> newResolverContext(DnsNameResolver parent, Channel channel,
208                                                      Future<? extends Channel> channelReadyFuture,
209                                                      Promise<?> originalPromise,
210                                                      String hostname,
211                                                      int dnsClass, DnsRecordType[] expectedTypes,
212                                                      DnsRecord[] additionals,
213                                                      DnsServerAddressStream nameServerAddrs, int allowedQueries);
214 
215     /**
216      * Converts the given {@link DnsRecord} into {@code T}.
217      */
218     abstract T convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop);
219 
220     /**
221      * Returns a filtered list of results which should be the final result of DNS resolution. This must take into
222      * account JDK semantics such as {@link NetUtil#isIpV6AddressesPreferred()}.
223      */
224     abstract List<T> filterResults(List<T> unfiltered);
225 
226     abstract boolean isCompleteEarly(T resolved);
227 
228     /**
229      * Returns {@code true} if we should allow duplicates in the result or {@code false} if no duplicates should
230      * be included.
231      */
232     abstract boolean isDuplicateAllowed();
233 
234     /**
235      * Caches a successful resolution.
236      */
237     abstract void cache(String hostname, DnsRecord[] additionals,
238                         DnsRecord result, T convertedResult);
239 
240     /**
241      * Caches a failed resolution.
242      */
243     abstract void cache(String hostname, DnsRecord[] additionals,
244                         UnknownHostException cause);
245 
246     void resolve(final Promise<List<T>> promise) {
247         final String[] searchDomains = parent.searchDomains();
248         if (searchDomains.length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) {
249             internalResolve(hostname, promise);
250         } else {
251             final boolean startWithoutSearchDomain = hasNDots();
252             final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0];
253             final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1;
254 
255             final Promise<List<T>> searchDomainPromise = parent.executor().newPromise();
256             searchDomainPromise.addListener(new FutureListener<List<T>>() {
257                 private int searchDomainIdx = initialSearchDomainIdx;
258                 @Override
259                 public void operationComplete(Future<List<T>> future) {
260                     Throwable cause = future.cause();
261                     if (cause == null) {
262                         final List<T> result = future.getNow();
263                         if (!promise.trySuccess(result)) {
264                             for (T item : result) {
265                                 ReferenceCountUtil.safeRelease(item);
266                             }
267                         }
268                     } else {
269                         if (DnsNameResolver.isTransportOrTimeoutError(cause)) {
270                             promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname, expectedTypes,
271                                     searchDomains));
272                         } else if (searchDomainIdx < searchDomains.length) {
273                             Promise<List<T>> newPromise = parent.executor().newPromise();
274                             newPromise.addListener(this);
275                             doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], newPromise);
276                         } else if (!startWithoutSearchDomain) {
277                             internalResolve(hostname, promise);
278                         } else {
279                             promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname, expectedTypes,
280                                     searchDomains));
281                         }
282                     }
283                 }
284             });
285             doSearchDomainQuery(initialHostname, searchDomainPromise);
286         }
287     }
288 
289     private boolean hasNDots() {
290         for (int idx = hostname.length() - 1, dots = 0; idx >= 0; idx--) {
291             if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) {
292                 return true;
293             }
294         }
295         return false;
296     }
297 
298     private static final class SearchDomainUnknownHostException extends UnknownHostException {
299         private static final long serialVersionUID = -8573510133644997085L;
300 
301         SearchDomainUnknownHostException(Throwable cause, String originalHostname,
302                 DnsRecordType[] queryTypes, String[] searchDomains) {
303             super("Failed to resolve '" + originalHostname + "' " + Arrays.toString(queryTypes) +
304                     " and search domain query for configured domains failed as well: " +
305                     Arrays.toString(searchDomains));
306             setStackTrace(cause.getStackTrace());
307             // Preserve the cause
308             initCause(cause.getCause());
309         }
310 
311         // Suppress a warning since this method doesn't need synchronization
312         @Override
313         public Throwable fillInStackTrace() {
314             return this;
315         }
316     }
317 
318     void doSearchDomainQuery(String hostname, Promise<List<T>> nextPromise) {
319         DnsResolveContext<T> nextContext = newResolverContext(parent, channel, channelReadyFuture,
320                 originalPromise, hostname, dnsClass,
321                 expectedTypes, additionals, nameServerAddrs,
322                 parent.maxQueriesPerResolve());
323         nextContext.internalResolve(hostname, nextPromise);
324     }
325 
326     private static String hostnameWithDot(String name) {
327         if (StringUtil.endsWith(name, '.')) {
328             return name;
329         }
330         return name + '.';
331     }
332 
333     // Resolve the final name from the CNAME cache until there is nothing to follow anymore. This also
334     // guards against loops in the cache but early return once a loop is detected.
335     //
336     // Visible for testing only
337     static String cnameResolveFromCache(DnsCnameCache cnameCache, String name) throws UnknownHostException {
338         String first = cnameCache.get(hostnameWithDot(name));
339         if (first == null) {
340             // Nothing in the cache at all
341             return name;
342         }
343 
344         String second = cnameCache.get(hostnameWithDot(first));
345         if (second == null) {
346             // Nothing else to follow, return first match.
347             return first;
348         }
349 
350         checkCnameLoop(name, first, second);
351         return cnameResolveFromCacheLoop(cnameCache, name, first, second);
352     }
353 
354     private static String cnameResolveFromCacheLoop(
355             DnsCnameCache cnameCache, String hostname, String first, String mapping) throws UnknownHostException {
356         // Detect loops by advance only every other iteration.
357         // See https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_Tortoise_and_Hare
358         boolean advance = false;
359 
360         String name = mapping;
361         // Resolve from cnameCache() until there is no more cname entry cached.
362         while ((mapping = cnameCache.get(hostnameWithDot(name))) != null) {
363             checkCnameLoop(hostname, first, mapping);
364             name = mapping;
365             if (advance) {
366                 first = cnameCache.get(first);
367             }
368             advance = !advance;
369         }
370         return name;
371     }
372 
373     private static void checkCnameLoop(String hostname, String first, String second) throws UnknownHostException {
374         if (first.equals(second)) {
375             // Follow CNAME from cache would loop. Lets throw and so fail the resolution.
376             throw new UnknownHostException("CNAME loop detected for '" + hostname + '\'');
377         }
378     }
379     private void internalResolve(String name, Promise<List<T>> promise) {
380         try {
381             // Resolve from cnameCache() until there is no more cname entry cached.
382             name = cnameResolveFromCache(cnameCache(), name);
383         } catch (Throwable cause) {
384             promise.tryFailure(cause);
385             return;
386         }
387 
388         try {
389             DnsServerAddressStream nameServerAddressStream = getNameServers(name);
390 
391             final int end = expectedTypes.length - 1;
392             for (int i = 0; i < end; ++i) {
393                 if (!query(name, expectedTypes[i], nameServerAddressStream.duplicate(), false, promise)) {
394                     return;
395                 }
396             }
397             query(name, expectedTypes[end], nameServerAddressStream, false, promise);
398         } finally {
399             // Now flush everything we submitted before for the Channel.
400             channel.flush();
401         }
402     }
403 
404     /**
405      * Returns the {@link DnsServerAddressStream} that was cached for the given hostname or {@code null} if non
406      *  could be found.
407      */
408     private DnsServerAddressStream getNameServersFromCache(String hostname) {
409         int len = hostname.length();
410 
411         if (len == 0) {
412             // We never cache for root servers.
413             return null;
414         }
415 
416         // We always store in the cache with a trailing '.'.
417         if (hostname.charAt(len - 1) != '.') {
418             hostname += ".";
419         }
420 
421         int idx = hostname.indexOf('.');
422         if (idx == hostname.length() - 1) {
423             // We are not interested in handling '.' as we should never serve the root servers from cache.
424             return null;
425         }
426 
427         // We start from the closed match and then move down.
428         for (;;) {
429             // Skip '.' as well.
430             hostname = hostname.substring(idx + 1);
431 
432             int idx2 = hostname.indexOf('.');
433             if (idx2 <= 0 || idx2 == hostname.length() - 1) {
434                 // We are not interested in handling '.TLD.' as we should never serve the root servers from cache.
435                 return null;
436             }
437             idx = idx2;
438 
439             DnsServerAddressStream entries = authoritativeDnsServerCache().get(hostname);
440             if (entries != null) {
441                 // The returned List may contain unresolved InetSocketAddress instances that will be
442                 // resolved on the fly in query(....).
443                 return entries;
444             }
445         }
446     }
447 
448     private void query(final DnsServerAddressStream nameServerAddrStream,
449                        final int nameServerAddrStreamIndex,
450                        final DnsQuestion question,
451                        final DnsQueryLifecycleObserver queryLifecycleObserver,
452                        final boolean flush,
453                        final Promise<List<T>> promise,
454                        final Throwable cause) {
455         if (completeEarly || nameServerAddrStreamIndex >= nameServerAddrStream.size() ||
456                 allowedQueries == 0 || originalPromise.isCancelled() || promise.isCancelled()) {
457             tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver,
458                                promise, cause);
459             return;
460         }
461 
462         --allowedQueries;
463 
464         final InetSocketAddress nameServerAddr = nameServerAddrStream.next();
465         if (nameServerAddr.isUnresolved()) {
466             queryUnresolvedNameServer(nameServerAddr, nameServerAddrStream, nameServerAddrStreamIndex, question,
467                                       queryLifecycleObserver, promise, cause);
468             return;
469         }
470         final Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> queryPromise =
471                 channel.eventLoop().newPromise();
472 
473         final long queryStartTimeNanos;
474         final boolean isFeedbackAddressStream;
475         if (nameServerAddrStream instanceof DnsServerResponseFeedbackAddressStream) {
476             queryStartTimeNanos = System.nanoTime();
477             isFeedbackAddressStream = true;
478         } else {
479             queryStartTimeNanos = -1;
480             isFeedbackAddressStream = false;
481         }
482 
483         final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f =
484                 parent.doQuery(channel, channelReadyFuture, nameServerAddr, question,
485                         queryLifecycleObserver, additionals, flush, queryPromise);
486 
487         queriesInProgress.add(f);
488 
489         f.addListener(new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
490             @Override
491             public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
492                 queriesInProgress.remove(future);
493 
494                 if (promise.isDone() || future.isCancelled()) {
495                     queryLifecycleObserver.queryCancelled(allowedQueries);
496 
497                     // Check if we need to release the envelope itself. If the query was cancelled the getNow() will
498                     // return null as well as the Future will be failed with a CancellationException.
499                     AddressedEnvelope<DnsResponse, InetSocketAddress> result = future.getNow();
500                     if (result != null) {
501                         result.release();
502                     }
503                     return;
504                 }
505 
506                 final Throwable queryCause = future.cause();
507                 try {
508                     if (queryCause == null) {
509                         if (isFeedbackAddressStream) {
510                             final DnsServerResponseFeedbackAddressStream feedbackNameServerAddrStream =
511                                     (DnsServerResponseFeedbackAddressStream) nameServerAddrStream;
512                             feedbackNameServerAddrStream.feedbackSuccess(nameServerAddr,
513                                     System.nanoTime() - queryStartTimeNanos);
514                         }
515                         onResponse(nameServerAddrStream, nameServerAddrStreamIndex, question, future.getNow(),
516                                    queryLifecycleObserver, promise);
517                     } else {
518                         // Server did not respond or I/O error occurred; try again.
519                         if (isFeedbackAddressStream) {
520                             final DnsServerResponseFeedbackAddressStream feedbackNameServerAddrStream =
521                                     (DnsServerResponseFeedbackAddressStream) nameServerAddrStream;
522                             feedbackNameServerAddrStream.feedbackFailure(nameServerAddr, queryCause,
523                                     System.nanoTime() - queryStartTimeNanos);
524                         }
525                         queryLifecycleObserver.queryFailed(queryCause);
526                         query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
527                               newDnsQueryLifecycleObserver(question), true, promise, queryCause);
528                     }
529                 } finally {
530                     tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question,
531                                        // queryLifecycleObserver has already been terminated at this point so we must
532                                        // not allow it to be terminated again by tryToFinishResolve.
533                                        NoopDnsQueryLifecycleObserver.INSTANCE,
534                                        promise, queryCause);
535                 }
536             }
537         });
538     }
539 
540     private void queryUnresolvedNameServer(final InetSocketAddress nameServerAddr,
541                                            final DnsServerAddressStream nameServerAddrStream,
542                                            final int nameServerAddrStreamIndex,
543                                            final DnsQuestion question,
544                                            final DnsQueryLifecycleObserver queryLifecycleObserver,
545                                            final Promise<List<T>> promise,
546                                            final Throwable cause) {
547         final String nameServerName = PlatformDependent.javaVersion() >= 7 ?
548                 nameServerAddr.getHostString() : nameServerAddr.getHostName();
549         assert nameServerName != null;
550 
551         // Placeholder so we will not try to finish the original query yet.
552         final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> resolveFuture = parent.executor()
553                 .newSucceededFuture(null);
554         queriesInProgress.add(resolveFuture);
555 
556         Promise<List<InetAddress>> resolverPromise = parent.executor().newPromise();
557         resolverPromise.addListener(new FutureListener<List<InetAddress>>() {
558             @Override
559             public void operationComplete(final Future<List<InetAddress>> future) {
560                 // Remove placeholder.
561                 queriesInProgress.remove(resolveFuture);
562 
563                 if (future.isSuccess()) {
564                     List<InetAddress> resolvedAddresses = future.getNow();
565                     DnsServerAddressStream addressStream = new CombinedDnsServerAddressStream(
566                             nameServerAddr, resolvedAddresses, nameServerAddrStream);
567                     query(addressStream, nameServerAddrStreamIndex, question,
568                           queryLifecycleObserver, true, promise, cause);
569                 } else {
570                     // Ignore the server and try the next one...
571                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1,
572                           question, queryLifecycleObserver, true, promise, cause);
573                 }
574             }
575         });
576         DnsCache resolveCache = resolveCache();
577         if (!DnsNameResolver.doResolveAllCached(nameServerName, additionals, resolverPromise, resolveCache,
578                 parent.resolvedInternetProtocolFamiliesUnsafe())) {
579 
580             new DnsAddressResolveContext(parent, channel, channelReadyFuture,
581                     originalPromise, nameServerName, additionals, parent.newNameServerAddressStream(nameServerName),
582                     // Resolving the unresolved nameserver must be limited by allowedQueries
583                     // so we eventually fail
584                     allowedQueries,
585                     resolveCache,
586                     redirectAuthoritativeDnsServerCache(authoritativeDnsServerCache()), false)
587                     .resolve(resolverPromise);
588         }
589     }
590 
591     private static AuthoritativeDnsServerCache redirectAuthoritativeDnsServerCache(
592             AuthoritativeDnsServerCache authoritativeDnsServerCache) {
593         // Don't wrap again to prevent the possibility of an StackOverflowError when wrapping another
594         // RedirectAuthoritativeDnsServerCache.
595         if (authoritativeDnsServerCache instanceof RedirectAuthoritativeDnsServerCache) {
596             return authoritativeDnsServerCache;
597         }
598         return new RedirectAuthoritativeDnsServerCache(authoritativeDnsServerCache);
599     }
600 
601     private static final class RedirectAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache {
602         private final AuthoritativeDnsServerCache wrapped;
603 
604         RedirectAuthoritativeDnsServerCache(AuthoritativeDnsServerCache authoritativeDnsServerCache) {
605             this.wrapped = authoritativeDnsServerCache;
606         }
607 
608         @Override
609         public DnsServerAddressStream get(String hostname) {
610             // To not risk falling into any loop, we will not use the cache while following redirects but only
611             // on the initial query.
612             return null;
613         }
614 
615         @Override
616         public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) {
617             wrapped.cache(hostname, address, originalTtl, loop);
618         }
619 
620         @Override
621         public void clear() {
622             wrapped.clear();
623         }
624 
625         @Override
626         public boolean clear(String hostname) {
627             return wrapped.clear(hostname);
628         }
629     }
630 
631     private void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex,
632                             final DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
633                             final DnsQueryLifecycleObserver queryLifecycleObserver,
634                             Promise<List<T>> promise) {
635         try {
636             final DnsResponse res = envelope.content();
637             final DnsResponseCode code = res.code();
638             if (code == DnsResponseCode.NOERROR) {
639                 if (handleRedirect(question, envelope, queryLifecycleObserver, promise)) {
640                     // Was a redirect so return here as everything else is handled in handleRedirect(...)
641                     return;
642                 }
643                 final DnsRecordType type = question.type();
644 
645                 if (type == DnsRecordType.CNAME) {
646                     onResponseCNAME(question, buildAliasMap(envelope.content(), cnameCache(), parent.executor()),
647                                     queryLifecycleObserver, promise);
648                     return;
649                 }
650 
651                 for (DnsRecordType expectedType : expectedTypes) {
652                     if (type == expectedType) {
653                         onExpectedResponse(question, envelope, queryLifecycleObserver, promise);
654                         return;
655                     }
656                 }
657 
658                 queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION);
659                 return;
660             }
661 
662             // Retry with the next server if the server did not tell us that the domain does not exist.
663             if (code != DnsResponseCode.NXDOMAIN) {
664                 query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
665                       queryLifecycleObserver.queryNoAnswer(code), true, promise, cause(code));
666             } else {
667                 queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION);
668 
669                 // Try with the next server if is not authoritative for the domain.
670                 //
671                 // From https://tools.ietf.org/html/rfc1035 :
672                 //
673                 //   RCODE        Response code - this 4 bit field is set as part of
674                 //                responses.  The values have the following
675                 //                interpretation:
676                 //
677                 //                ....
678                 //                ....
679                 //
680                 //                3               Name Error - Meaningful only for
681                 //                                responses from an authoritative name
682                 //                                server, this code signifies that the
683                 //                                domain name referenced in the query does
684                 //                                not exist.
685                 //                ....
686                 //                ....
687                 if (!res.isAuthoritativeAnswer()) {
688                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
689                             newDnsQueryLifecycleObserver(question), true, promise, cause(code));
690                 } else {
691                     // Failed with NX cause - distinction between NXDOMAIN vs a timeout
692                     tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question,
693                             queryLifecycleObserver, promise, NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION);
694                 }
695             }
696         } finally {
697             ReferenceCountUtil.safeRelease(envelope);
698         }
699     }
700 
701     /**
702      * Handles a redirect answer if needed and returns {@code true} if a redirect query has been made.
703      */
704     private boolean handleRedirect(
705             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
706             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
707         final DnsResponse res = envelope.content();
708 
709         // Check if we have answers, if not this may be an non authority NS and so redirects must be handled.
710         if (res.count(DnsSection.ANSWER) == 0) {
711             AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.name(), res);
712             if (serverNames != null) {
713                 int additionalCount = res.count(DnsSection.ADDITIONAL);
714 
715                 AuthoritativeDnsServerCache authoritativeDnsServerCache = authoritativeDnsServerCache();
716                 for (int i = 0; i < additionalCount; i++) {
717                     final DnsRecord r = res.recordAt(DnsSection.ADDITIONAL, i);
718 
719                     if (r.type() == DnsRecordType.A && !parent.supportsARecords() ||
720                         r.type() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) {
721                         continue;
722                     }
723 
724                     // We may have multiple ADDITIONAL entries for the same nameserver name. For example one AAAA and
725                     // one A record.
726                     serverNames.handleWithAdditional(parent, r, authoritativeDnsServerCache);
727                 }
728 
729                 // Process all unresolved nameservers as well.
730                 serverNames.handleWithoutAdditionals(parent, resolveCache(), authoritativeDnsServerCache);
731 
732                 List<InetSocketAddress> addresses = serverNames.addressList();
733 
734                 // Give the user the chance to sort or filter the used servers for the query.
735                 DnsServerAddressStream serverStream = parent.newRedirectDnsServerStream(
736                         question.name(), addresses);
737 
738                 if (serverStream != null) {
739                     query(serverStream, 0, question,
740                           queryLifecycleObserver.queryRedirected(new DnsAddressStreamList(serverStream)),
741                           true, promise, null);
742                     return true;
743                 }
744             }
745         }
746         return false;
747     }
748 
749     private static Throwable cause(final DnsResponseCode code) {
750         assert code != null;
751         if (SERVFAIL.intValue() == code.intValue()) {
752             return SERVFAIL_QUERY_FAILED_EXCEPTION;
753         } else if (NXDOMAIN.intValue() == code.intValue()) {
754             return NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION;
755         }
756 
757         return null;
758     }
759 
760     private static final class DnsAddressStreamList extends AbstractList<InetSocketAddress> {
761 
762         private final DnsServerAddressStream duplicate;
763         private List<InetSocketAddress> addresses;
764 
765         DnsAddressStreamList(DnsServerAddressStream stream) {
766             duplicate = stream.duplicate();
767         }
768 
769         @Override
770         public InetSocketAddress get(int index) {
771             if (addresses == null) {
772                 DnsServerAddressStream stream = duplicate.duplicate();
773                 addresses = new ArrayList<InetSocketAddress>(size());
774                 for (int i = 0; i < stream.size(); i++) {
775                     addresses.add(stream.next());
776                 }
777             }
778             return addresses.get(index);
779         }
780 
781         @Override
782         public int size() {
783             return duplicate.size();
784         }
785 
786         @Override
787         public Iterator<InetSocketAddress> iterator() {
788             return new Iterator<InetSocketAddress>() {
789                 private final DnsServerAddressStream stream = duplicate.duplicate();
790                 private int i;
791 
792                 @Override
793                 public boolean hasNext() {
794                     return i < stream.size();
795                 }
796 
797                 @Override
798                 public InetSocketAddress next() {
799                     if (!hasNext()) {
800                         throw new NoSuchElementException();
801                     }
802                     i++;
803                     return stream.next();
804                 }
805 
806                 @Override
807                 public void remove() {
808                     throw new UnsupportedOperationException();
809                 }
810             };
811         }
812     }
813 
814     /**
815      * Returns the {@code {@link AuthoritativeNameServerList} which were included in {@link DnsSection#AUTHORITY}
816      * or {@code null} if non are found.
817      */
818     private static AuthoritativeNameServerList extractAuthoritativeNameServers(String questionName, DnsResponse res) {
819         int authorityCount = res.count(DnsSection.AUTHORITY);
820         if (authorityCount == 0) {
821             return null;
822         }
823 
824         AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName);
825         for (int i = 0; i < authorityCount; i++) {
826             serverNames.add(res.recordAt(DnsSection.AUTHORITY, i));
827         }
828         return serverNames.isEmpty() ? null : serverNames;
829     }
830 
831     private void onExpectedResponse(
832             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
833             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
834 
835         // We often get a bunch of CNAMES as well when we asked for A/AAAA.
836         final DnsResponse response = envelope.content();
837         final Map<String, String> cnames = buildAliasMap(response, cnameCache(), parent.executor());
838         final int answerCount = response.count(DnsSection.ANSWER);
839 
840         boolean found = false;
841         boolean completeEarly = this.completeEarly;
842         boolean cnameNeedsFollow = !cnames.isEmpty();
843         for (int i = 0; i < answerCount; i ++) {
844             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
845             final DnsRecordType type = r.type();
846             boolean matches = false;
847             for (DnsRecordType expectedType : expectedTypes) {
848                 if (type == expectedType) {
849                     matches = true;
850                     break;
851                 }
852             }
853 
854             if (!matches) {
855                 continue;
856             }
857 
858             final String questionName = question.name().toLowerCase(Locale.US);
859             final String recordName = r.name().toLowerCase(Locale.US);
860 
861             // Make sure the record is for the questioned domain.
862             if (!recordName.equals(questionName)) {
863                 Map<String, String> cnamesCopy = new HashMap<String, String>(cnames);
864                 // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
865                 String resolved = questionName;
866                 do {
867                     resolved = cnamesCopy.remove(resolved);
868                     if (recordName.equals(resolved)) {
869                         // We followed a CNAME chain that was part of the response without any extra queries.
870                         cnameNeedsFollow = false;
871                         break;
872                     }
873                 } while (resolved != null);
874 
875                 if (resolved == null) {
876                     assert questionName.isEmpty() || questionName.charAt(questionName.length() - 1) == '.';
877 
878                     for (String searchDomain : parent.searchDomains()) {
879                         if (searchDomain.isEmpty()) {
880                             continue;
881                         }
882 
883                         final String fqdn;
884                         if (searchDomain.charAt(searchDomain.length() - 1) == '.') {
885                             fqdn = questionName + searchDomain;
886                         } else {
887                             fqdn = questionName + searchDomain + '.';
888                         }
889                         if (recordName.equals(fqdn)) {
890                             resolved = recordName;
891                             break;
892                         }
893                     }
894                     if (resolved == null) {
895                         if (logger.isDebugEnabled()) {
896                             logger.debug("{} Ignoring record {} for [{}: {}] as it contains a different name than " +
897                                             "the question name [{}]. Cnames: {}, Search domains: {}",
898                                     channel, r.toString(), response.id(), envelope.sender(), questionName, cnames,
899                                     parent.searchDomains());
900                         }
901                         continue;
902                     }
903                 }
904             }
905 
906             final T converted = convertRecord(r, hostname, additionals, parent.executor());
907             if (converted == null) {
908                 if (logger.isDebugEnabled()) {
909                     logger.debug("{} Ignoring record {} for [{}: {}] as the converted record is null. "
910                                     + "Hostname [{}], Additionals: {}",
911                             channel, r.toString(), response.id(), envelope.sender(), hostname, additionals);
912                 }
913                 continue;
914             }
915 
916             boolean shouldRelease = false;
917             // Check if we did determine we wanted to complete early before. If this is the case we want to not
918             // include the result
919             if (!completeEarly) {
920                 completeEarly = isCompleteEarly(converted);
921             }
922 
923             // Check if the promise was done already, and only if not add things to the finalResult. Otherwise lets
924             // just release things after we cached it.
925             if (!promise.isDone()) {
926                 // We want to ensure we do not have duplicates in finalResult as this may be unexpected.
927                 //
928                 // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an
929                 // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay
930                 // for the extra memory copy and allocations in this cases later on.
931                 if (finalResult == null) {
932                     finalResult = new ArrayList<T>(8);
933                     finalResult.add(converted);
934                 } else if (isDuplicateAllowed() || !finalResult.contains(converted)) {
935                     finalResult.add(converted);
936                 } else {
937                     shouldRelease = true;
938                 }
939             } else {
940                 shouldRelease = true;
941             }
942 
943             cache(hostname, additionals, r, converted);
944             found = true;
945 
946             if (shouldRelease) {
947                 ReferenceCountUtil.release(converted);
948             }
949             // Note that we do not break from the loop here, so we decode/cache all A/AAAA records.
950         }
951 
952         if (found && !cnameNeedsFollow) {
953             // If we found the correct result we can just stop here without following any extra CNAME records in the
954             // response.
955             if (completeEarly) {
956                 this.completeEarly = true;
957             }
958             queryLifecycleObserver.querySucceed();
959         } else if (cnames.isEmpty()) {
960             queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION);
961         } else {
962             queryLifecycleObserver.querySucceed();
963             // We also got a CNAME so we need to ensure we also query it.
964             onResponseCNAME(question, cnames, newDnsQueryLifecycleObserver(question), promise);
965         }
966     }
967 
968     private void onResponseCNAME(
969             DnsQuestion question, Map<String, String> cnames,
970             final DnsQueryLifecycleObserver queryLifecycleObserver,
971             Promise<List<T>> promise) {
972 
973         // Resolve the host name in the question into the real host name.
974         String resolved = question.name().toLowerCase(Locale.US);
975         boolean found = false;
976         while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty
977                                     // because it can be Collections.emptyMap()
978                                     // whose remove() throws a UnsupportedOperationException.
979             final String next = cnames.remove(resolved);
980             if (next != null) {
981                 found = true;
982                 resolved = next;
983             } else {
984                 break;
985             }
986         }
987 
988         if (found) {
989             followCname(question, resolved, queryLifecycleObserver, promise);
990         } else {
991             queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION);
992         }
993     }
994 
995     private static Map<String, String> buildAliasMap(DnsResponse response, DnsCnameCache cache, EventLoop loop) {
996         final int answerCount = response.count(DnsSection.ANSWER);
997         Map<String, String> cnames = null;
998         for (int i = 0; i < answerCount; i ++) {
999             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
1000             final DnsRecordType type = r.type();
1001             if (type != DnsRecordType.CNAME) {
1002                 continue;
1003             }
1004 
1005             if (!(r instanceof DnsRawRecord)) {
1006                 continue;
1007             }
1008 
1009             final ByteBuf recordContent = ((ByteBufHolder) r).content();
1010             final String domainName = decodeDomainName(recordContent);
1011             if (domainName == null) {
1012                 continue;
1013             }
1014 
1015             if (cnames == null) {
1016                 cnames = new HashMap<String, String>(min(8, answerCount));
1017             }
1018 
1019             String name = r.name().toLowerCase(Locale.US);
1020             String mapping = domainName.toLowerCase(Locale.US);
1021 
1022             // Cache the CNAME as well.
1023             String nameWithDot = hostnameWithDot(name);
1024             String mappingWithDot = hostnameWithDot(mapping);
1025             if (!nameWithDot.equalsIgnoreCase(mappingWithDot)) {
1026                 cache.cache(nameWithDot, mappingWithDot, r.timeToLive(), loop);
1027                 cnames.put(name, mapping);
1028             }
1029         }
1030 
1031         return cnames != null? cnames : Collections.<String, String>emptyMap();
1032     }
1033 
1034     private void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
1035                                     final int nameServerAddrStreamIndex,
1036                                     final DnsQuestion question,
1037                                     final DnsQueryLifecycleObserver queryLifecycleObserver,
1038                                     final Promise<List<T>> promise,
1039                                     final Throwable cause) {
1040 
1041         // There are no queries left to try.
1042         if (!completeEarly && !queriesInProgress.isEmpty()) {
1043             queryLifecycleObserver.queryCancelled(allowedQueries);
1044 
1045             // There are still some queries in process, we will try to notify once the next one finishes until
1046             // all are finished.
1047             return;
1048         }
1049 
1050         // There are no queries left to try.
1051         if (finalResult == null) {
1052             if (nameServerAddrStreamIndex < nameServerAddrStream.size()) {
1053                 if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) {
1054                     // If the queryLifecycleObserver has already been terminated we should create a new one for this
1055                     // fresh query.
1056                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
1057                           newDnsQueryLifecycleObserver(question), true, promise, cause);
1058                 } else {
1059                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver,
1060                           true, promise, cause);
1061                 }
1062                 return;
1063             }
1064 
1065             queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION);
1066 
1067             // .. and we could not find any expected records.
1068 
1069             // The following is of questionable benefit, but has been around for a while that
1070             // it may be risky to remove. Reference https://datatracker.ietf.org/doc/html/rfc8020
1071             // - If we receive NXDOMAIN we know the domain doesn't exist, any other lookup type is meaningless.
1072             // - If we receive SERVFAIL, and we attempt a CNAME that returns NOERROR with 0 answers, it may lead the
1073             //   call-site to invalidate previously advertised addresses.
1074             // Having said that, there is the case of DNS services that don't respect the protocol either
1075             // - An A lookup could result in NXDOMAIN but a CNAME may succeed with answers.
1076             // It's an imperfect world. Accept it.
1077             // Guarding it with a system property, as an opt-in functionality.
1078             if (TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS) {
1079                 // If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we
1080                 // won't try to resolve the CNAME as we only should do this if we could not get the expected records
1081                 // because they do not exist and the DNS server did probably signal it.
1082                 final boolean isValidResponse =
1083                         cause == NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION || cause == SERVFAIL_QUERY_FAILED_EXCEPTION;
1084                 if ((cause == null || isValidResponse) && !triedCNAME &&
1085                         (question.type() == DnsRecordType.A || question.type() == DnsRecordType.AAAA)) {
1086                     // As the last resort, try to query CNAME, just in case the name server has it.
1087                     triedCNAME = true;
1088 
1089                     query(hostname, DnsRecordType.CNAME, getNameServers(hostname), true, promise);
1090                     return;
1091                 }
1092             }
1093         } else {
1094             queryLifecycleObserver.queryCancelled(allowedQueries);
1095         }
1096 
1097         // We have at least one resolved record or tried CNAME as the last resort.
1098         finishResolve(promise, cause);
1099     }
1100 
1101     private void finishResolve(Promise<List<T>> promise, Throwable cause) {
1102         // If completeEarly was true we still want to continue processing the queries to ensure we still put everything
1103         // in the cache eventually.
1104         if (!completeEarly && !queriesInProgress.isEmpty()) {
1105             // If there are queries in progress, we should cancel it because we already finished the resolution.
1106             for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
1107                  i.hasNext();) {
1108                 Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next();
1109                 i.remove();
1110 
1111                 f.cancel(false);
1112             }
1113         }
1114 
1115         if (finalResult != null) {
1116             if (!promise.isDone()) {
1117                 // Found at least one resolved record.
1118                 final List<T> result = filterResults(finalResult);
1119                 // Lets replace the previous stored result.
1120                 finalResult = Collections.emptyList();
1121                 if (!DnsNameResolver.trySuccess(promise, result)) {
1122                     for (T item : result) {
1123                         ReferenceCountUtil.safeRelease(item);
1124                     }
1125                 }
1126             } else {
1127                 // This should always be the case as we replaced the list once notify the promise with an empty one
1128                 // and never add to it again.
1129                 assert finalResult.isEmpty();
1130             }
1131             return;
1132         }
1133 
1134         // No resolved address found.
1135         final int maxAllowedQueries = parent.maxQueriesPerResolve();
1136         final int tries = maxAllowedQueries - allowedQueries;
1137         final StringBuilder buf = new StringBuilder(64);
1138 
1139         buf.append("Failed to resolve '").append(hostname).append("' ").append(Arrays.toString(expectedTypes));
1140         if (tries > 1) {
1141             if (tries < maxAllowedQueries) {
1142                 buf.append(" after ")
1143                    .append(tries)
1144                    .append(" queries ");
1145             } else {
1146                 buf.append(". Exceeded max queries per resolve ")
1147                 .append(maxAllowedQueries)
1148                 .append(' ');
1149             }
1150         }
1151         final UnknownHostException unknownHostException = new UnknownHostException(buf.toString());
1152         if (cause == null) {
1153             // Only cache if the failure was not because of an IO error / timeout that was caused by the query
1154             // itself.
1155             cache(hostname, additionals, unknownHostException);
1156         } else {
1157             unknownHostException.initCause(cause);
1158         }
1159         promise.tryFailure(unknownHostException);
1160     }
1161 
1162     static String decodeDomainName(ByteBuf in) {
1163         in.markReaderIndex();
1164         try {
1165             return DefaultDnsRecordDecoder.decodeName(in);
1166         } catch (CorruptedFrameException e) {
1167             // In this case we just return null.
1168             return null;
1169         } finally {
1170             in.resetReaderIndex();
1171         }
1172     }
1173 
1174     private DnsServerAddressStream getNameServers(String name) {
1175         DnsServerAddressStream stream = getNameServersFromCache(name);
1176         if (stream == null) {
1177             // We need to obtain a new stream from the parent DnsNameResolver if the hostname is not the same as
1178             // for the original query (for example we may follow CNAMEs). Otherwise let's just duplicate the
1179             // original nameservers so we correctly update the internal index
1180             if (name.equals(hostname)) {
1181                 return nameServerAddrs.duplicate();
1182             }
1183             return parent.newNameServerAddressStream(name);
1184         }
1185         return stream;
1186     }
1187 
1188     private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
1189                              Promise<List<T>> promise) {
1190         final DnsQuestion cnameQuestion;
1191         final DnsServerAddressStream stream;
1192         try {
1193             cname = cnameResolveFromCache(cnameCache(), cname);
1194             stream = getNameServers(cname);
1195             cnameQuestion = new DefaultDnsQuestion(cname, question.type(), dnsClass);
1196         } catch (Throwable cause) {
1197             queryLifecycleObserver.queryFailed(cause);
1198             PlatformDependent.throwException(cause);
1199             return;
1200         }
1201         query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion),
1202               true, promise, null);
1203     }
1204 
1205     private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
1206                           boolean flush, Promise<List<T>> promise) {
1207         final DnsQuestion question;
1208         try {
1209             question = new DefaultDnsQuestion(hostname, type, dnsClass);
1210         } catch (Throwable cause) {
1211             // Assume a single failure means that queries will succeed. If the hostname is invalid for one type
1212             // there is no case where it is known to be valid for another type.
1213             promise.tryFailure(new IllegalArgumentException("Unable to create DNS Question for: [" + hostname + ", " +
1214                     type + ']', cause));
1215             return false;
1216         }
1217         query(dnsServerAddressStream, 0, question, newDnsQueryLifecycleObserver(question), flush, promise, null);
1218         return true;
1219     }
1220 
1221     private DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) {
1222         return parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question);
1223     }
1224 
1225     private final class CombinedDnsServerAddressStream implements DnsServerAddressStream {
1226         private final InetSocketAddress replaced;
1227         private final DnsServerAddressStream originalStream;
1228         private final List<InetAddress> resolvedAddresses;
1229         private Iterator<InetAddress> resolved;
1230 
1231         CombinedDnsServerAddressStream(InetSocketAddress replaced, List<InetAddress> resolvedAddresses,
1232                                        DnsServerAddressStream originalStream) {
1233             this.replaced = replaced;
1234             this.resolvedAddresses = resolvedAddresses;
1235             this.originalStream = originalStream;
1236             resolved = resolvedAddresses.iterator();
1237         }
1238 
1239         @Override
1240         public InetSocketAddress next() {
1241             if (resolved.hasNext()) {
1242                 return nextResolved0();
1243             }
1244             InetSocketAddress address = originalStream.next();
1245             if (address.equals(replaced)) {
1246                 resolved = resolvedAddresses.iterator();
1247                 return nextResolved0();
1248             }
1249             return address;
1250         }
1251 
1252         private InetSocketAddress nextResolved0() {
1253             return parent.newRedirectServerAddress(resolved.next());
1254         }
1255 
1256         @Override
1257         public int size() {
1258             return originalStream.size() + resolvedAddresses.size() - 1;
1259         }
1260 
1261         @Override
1262         public DnsServerAddressStream duplicate() {
1263             return new CombinedDnsServerAddressStream(replaced, resolvedAddresses, originalStream.duplicate());
1264         }
1265     }
1266 
1267     /**
1268      * Holds the closed DNS Servers for a domain.
1269      */
1270     private static final class AuthoritativeNameServerList {
1271 
1272         private final String questionName;
1273 
1274         // We not expect the linked-list to be very long so a double-linked-list is overkill.
1275         private AuthoritativeNameServer head;
1276 
1277         private int nameServerCount;
1278 
1279         AuthoritativeNameServerList(String questionName) {
1280             this.questionName = questionName.toLowerCase(Locale.US);
1281         }
1282 
1283         void add(DnsRecord r) {
1284             if (r.type() != DnsRecordType.NS || !(r instanceof DnsRawRecord)) {
1285                 return;
1286             }
1287 
1288             // Only include servers that serve the correct domain.
1289             if (questionName.length() <  r.name().length()) {
1290                 return;
1291             }
1292 
1293             String recordName = r.name().toLowerCase(Locale.US);
1294 
1295             int dots = 0;
1296             for (int a = recordName.length() - 1, b = questionName.length() - 1; a >= 0; a--, b--) {
1297                 char c = recordName.charAt(a);
1298                 if (questionName.charAt(b) != c) {
1299                     return;
1300                 }
1301                 if (c == '.') {
1302                     dots++;
1303                 }
1304             }
1305 
1306             if (head != null && head.dots > dots) {
1307                 // We already have a closer match so ignore this one, no need to parse the domainName etc.
1308                 return;
1309             }
1310 
1311             final ByteBuf recordContent = ((ByteBufHolder) r).content();
1312             final String domainName = decodeDomainName(recordContent);
1313             if (domainName == null) {
1314                 // Could not be parsed, ignore.
1315                 return;
1316             }
1317 
1318             // We are only interested in preserving the nameservers which are the closest to our qName, so ensure
1319             // we drop servers that have a smaller dots count.
1320             if (head == null || head.dots < dots) {
1321                 nameServerCount = 1;
1322                 head = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1323             } else if (head.dots == dots) {
1324                 AuthoritativeNameServer serverName = head;
1325                 while (serverName.next != null) {
1326                     serverName = serverName.next;
1327                 }
1328                 serverName.next = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1329                 nameServerCount++;
1330             }
1331         }
1332 
1333         void handleWithAdditional(
1334                 DnsNameResolver parent, DnsRecord r, AuthoritativeDnsServerCache authoritativeCache) {
1335             // Just walk the linked-list and mark the entry as handled when matched.
1336             AuthoritativeNameServer serverName = head;
1337 
1338             String nsName = r.name();
1339             InetAddress resolved = decodeAddress(r, nsName, parent.isDecodeIdn());
1340             if (resolved == null) {
1341                 // Could not parse the address, just ignore.
1342                 return;
1343             }
1344 
1345             while (serverName != null) {
1346                 if (serverName.nsName.equalsIgnoreCase(nsName)) {
1347                     if (serverName.address != null) {
1348                         // We received multiple ADDITIONAL records for the same name.
1349                         // Search for the last we insert before and then append a new one.
1350                         while (serverName.next != null && serverName.next.isCopy) {
1351                             serverName = serverName.next;
1352                         }
1353                         AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1354                         server.next = serverName.next;
1355                         serverName.next = server;
1356                         serverName = server;
1357 
1358                         nameServerCount++;
1359                     }
1360                     // We should replace the TTL if needed with the one of the ADDITIONAL record so we use
1361                     // the smallest for caching.
1362                     serverName.update(parent.newRedirectServerAddress(resolved), r.timeToLive());
1363 
1364                     // Cache the server now.
1365                     cache(serverName, authoritativeCache, parent.executor());
1366                     return;
1367                 }
1368                 serverName = serverName.next;
1369             }
1370         }
1371 
1372         // Now handle all AuthoritativeNameServer for which we had no ADDITIONAL record
1373         void handleWithoutAdditionals(
1374                 DnsNameResolver parent, DnsCache cache, AuthoritativeDnsServerCache authoritativeCache) {
1375             AuthoritativeNameServer serverName = head;
1376 
1377             while (serverName != null) {
1378                 if (serverName.address == null) {
1379                     // These will be resolved on the fly if needed.
1380                     cacheUnresolved(serverName, authoritativeCache, parent.executor());
1381 
1382                     // Try to resolve via cache as we had no ADDITIONAL entry for the server.
1383 
1384                     List<? extends DnsCacheEntry> entries = cache.get(serverName.nsName, null);
1385                     if (entries != null && !entries.isEmpty()) {
1386                         InetAddress address = entries.get(0).address();
1387 
1388                         // If address is null we have a resolution failure cached so just use an unresolved address.
1389                         if (address != null) {
1390                             serverName.update(parent.newRedirectServerAddress(address));
1391 
1392                             for (int i = 1; i < entries.size(); i++) {
1393                                 address = entries.get(i).address();
1394 
1395                                 assert address != null :
1396                                         "Cache returned a cached failure, should never return anything else";
1397 
1398                                 AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1399                                 server.next = serverName.next;
1400                                 serverName.next = server;
1401                                 serverName = server;
1402                                 serverName.update(parent.newRedirectServerAddress(address));
1403 
1404                                 nameServerCount++;
1405                             }
1406                         }
1407                     }
1408                 }
1409                 serverName = serverName.next;
1410             }
1411         }
1412 
1413         private static void cacheUnresolved(
1414                 AuthoritativeNameServer server, AuthoritativeDnsServerCache authoritativeCache, EventLoop loop) {
1415             // We still want to cached the unresolved address
1416             server.address = InetSocketAddress.createUnresolved(
1417                     server.nsName, DefaultDnsServerAddressStreamProvider.DNS_PORT);
1418 
1419             // Cache the server now.
1420             cache(server, authoritativeCache, loop);
1421         }
1422 
1423         private static void cache(AuthoritativeNameServer server, AuthoritativeDnsServerCache cache, EventLoop loop) {
1424             // Cache NS record if not for a root server as we should never cache for root servers.
1425             if (!server.isRootServer()) {
1426                 cache.cache(server.domainName, server.address, server.ttl, loop);
1427             }
1428         }
1429 
1430         /**
1431          * Returns {@code true} if empty, {@code false} otherwise.
1432          */
1433         boolean isEmpty() {
1434             return nameServerCount == 0;
1435         }
1436 
1437         /**
1438          * Creates a new {@link List} which holds the {@link InetSocketAddress}es.
1439          */
1440         List<InetSocketAddress> addressList() {
1441             List<InetSocketAddress> addressList = new ArrayList<InetSocketAddress>(nameServerCount);
1442 
1443             AuthoritativeNameServer server = head;
1444             while (server != null) {
1445                 if (server.address != null) {
1446                     addressList.add(server.address);
1447                 }
1448                 server = server.next;
1449             }
1450             return addressList;
1451         }
1452     }
1453 
1454     private static final class AuthoritativeNameServer {
1455         private final int dots;
1456         private final String domainName;
1457         final boolean isCopy;
1458         final String nsName;
1459 
1460         private long ttl;
1461         private InetSocketAddress address;
1462 
1463         AuthoritativeNameServer next;
1464 
1465         AuthoritativeNameServer(int dots, long ttl, String domainName, String nsName) {
1466             this.dots = dots;
1467             this.ttl = ttl;
1468             this.nsName = nsName;
1469             this.domainName = domainName;
1470             isCopy = false;
1471         }
1472 
1473         AuthoritativeNameServer(AuthoritativeNameServer server) {
1474             dots = server.dots;
1475             ttl = server.ttl;
1476             nsName = server.nsName;
1477             domainName = server.domainName;
1478             isCopy = true;
1479         }
1480 
1481         /**
1482          * Returns {@code true} if its a root server.
1483          */
1484         boolean isRootServer() {
1485             return dots == 1;
1486         }
1487 
1488         /**
1489          * Update the server with the given address and TTL if needed.
1490          */
1491         void update(InetSocketAddress address, long ttl) {
1492             assert this.address == null || this.address.isUnresolved();
1493             this.address = address;
1494             this.ttl = min(this.ttl, ttl);
1495         }
1496 
1497         void update(InetSocketAddress address) {
1498             update(address, Long.MAX_VALUE);
1499         }
1500     }
1501 }