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