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