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