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,
645                             buildAliasMap(question.name(), envelope.content(), cnameCache(), parent.executor()),
646                             queryLifecycleObserver, promise);
647                     return;
648                 }
649 
650                 for (DnsRecordType expectedType : expectedTypes) {
651                     if (type == expectedType) {
652                         onExpectedResponse(question, envelope, queryLifecycleObserver, promise);
653                         return;
654                     }
655                 }
656 
657                 queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION);
658                 return;
659             }
660 
661             // Retry with the next server if the server did not tell us that the domain does not exist.
662             if (code != DnsResponseCode.NXDOMAIN) {
663                 query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
664                       queryLifecycleObserver.queryNoAnswer(code), true, promise, cause(code));
665             } else {
666                 queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION);
667 
668                 // Try with the next server if is not authoritative for the domain.
669                 //
670                 // From https://tools.ietf.org/html/rfc1035 :
671                 //
672                 //   RCODE        Response code - this 4 bit field is set as part of
673                 //                responses.  The values have the following
674                 //                interpretation:
675                 //
676                 //                ....
677                 //                ....
678                 //
679                 //                3               Name Error - Meaningful only for
680                 //                                responses from an authoritative name
681                 //                                server, this code signifies that the
682                 //                                domain name referenced in the query does
683                 //                                not exist.
684                 //                ....
685                 //                ....
686                 if (!res.isAuthoritativeAnswer()) {
687                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
688                             newDnsQueryLifecycleObserver(question), true, promise, cause(code));
689                 } else {
690                     // Failed with NX cause - distinction between NXDOMAIN vs a timeout
691                     tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question,
692                             queryLifecycleObserver, promise, NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION);
693                 }
694             }
695         } finally {
696             ReferenceCountUtil.safeRelease(envelope);
697         }
698     }
699 
700     /**
701      * Handles a redirect answer if needed and returns {@code true} if a redirect query has been made.
702      */
703     private boolean handleRedirect(
704             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
705             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
706         final DnsResponse res = envelope.content();
707 
708         // Check if we have answers, if not this may be an non authority NS and so redirects must be handled.
709         if (res.count(DnsSection.ANSWER) == 0) {
710             AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.name(), res);
711             if (serverNames != null) {
712                 int additionalCount = res.count(DnsSection.ADDITIONAL);
713 
714                 AuthoritativeDnsServerCache authoritativeDnsServerCache = authoritativeDnsServerCache();
715                 for (int i = 0; i < additionalCount; i++) {
716                     final DnsRecord r = res.recordAt(DnsSection.ADDITIONAL, i);
717 
718                     if (r.type() == DnsRecordType.A && !parent.supportsARecords() ||
719                         r.type() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) {
720                         continue;
721                     }
722 
723                     // We may have multiple ADDITIONAL entries for the same nameserver name. For example one AAAA and
724                     // one A record.
725                     serverNames.handleWithAdditional(parent, r, authoritativeDnsServerCache);
726                 }
727 
728                 // Process all unresolved nameservers as well.
729                 serverNames.handleWithoutAdditionals(parent, resolveCache(), authoritativeDnsServerCache);
730 
731                 List<InetSocketAddress> addresses = serverNames.addressList();
732 
733                 // Give the user the chance to sort or filter the used servers for the query.
734                 DnsServerAddressStream serverStream = parent.newRedirectDnsServerStream(
735                         question.name(), addresses);
736 
737                 if (serverStream != null) {
738                     query(serverStream, 0, question,
739                           queryLifecycleObserver.queryRedirected(new DnsAddressStreamList(serverStream)),
740                           true, promise, null);
741                     return true;
742                 }
743             }
744         }
745         return false;
746     }
747 
748     private static Throwable cause(final DnsResponseCode code) {
749         assert code != null;
750         if (SERVFAIL.intValue() == code.intValue()) {
751             return SERVFAIL_QUERY_FAILED_EXCEPTION;
752         } else if (NXDOMAIN.intValue() == code.intValue()) {
753             return NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION;
754         }
755 
756         return null;
757     }
758 
759     private static final class DnsAddressStreamList extends AbstractList<InetSocketAddress> {
760 
761         private final DnsServerAddressStream duplicate;
762         private List<InetSocketAddress> addresses;
763 
764         DnsAddressStreamList(DnsServerAddressStream stream) {
765             duplicate = stream.duplicate();
766         }
767 
768         @Override
769         public InetSocketAddress get(int index) {
770             if (addresses == null) {
771                 DnsServerAddressStream stream = duplicate.duplicate();
772                 addresses = new ArrayList<InetSocketAddress>(size());
773                 for (int i = 0; i < stream.size(); i++) {
774                     addresses.add(stream.next());
775                 }
776             }
777             return addresses.get(index);
778         }
779 
780         @Override
781         public int size() {
782             return duplicate.size();
783         }
784 
785         @Override
786         public Iterator<InetSocketAddress> iterator() {
787             return new Iterator<InetSocketAddress>() {
788                 private final DnsServerAddressStream stream = duplicate.duplicate();
789                 private int i;
790 
791                 @Override
792                 public boolean hasNext() {
793                     return i < stream.size();
794                 }
795 
796                 @Override
797                 public InetSocketAddress next() {
798                     if (!hasNext()) {
799                         throw new NoSuchElementException();
800                     }
801                     i++;
802                     return stream.next();
803                 }
804 
805                 @Override
806                 public void remove() {
807                     throw new UnsupportedOperationException();
808                 }
809             };
810         }
811     }
812 
813     /**
814      * Returns the {@code {@link AuthoritativeNameServerList} which were included in {@link DnsSection#AUTHORITY}
815      * or {@code null} if non are found.
816      */
817     private static AuthoritativeNameServerList extractAuthoritativeNameServers(String questionName, DnsResponse res) {
818         int authorityCount = res.count(DnsSection.AUTHORITY);
819         if (authorityCount == 0) {
820             return null;
821         }
822 
823         AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName);
824         for (int i = 0; i < authorityCount; i++) {
825             serverNames.add(res.recordAt(DnsSection.AUTHORITY, i));
826         }
827         return serverNames.isEmpty() ? null : serverNames;
828     }
829 
830     private void onExpectedResponse(
831             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
832             final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
833 
834         // We often get a bunch of CNAMES as well when we asked for A/AAAA.
835         final DnsResponse response = envelope.content();
836         final Map<String, String> cnames = buildAliasMap(question.name(), response, cnameCache(), parent.executor());
837         final int answerCount = response.count(DnsSection.ANSWER);
838 
839         boolean found = false;
840         boolean completeEarly = this.completeEarly;
841         boolean cnameNeedsFollow = !cnames.isEmpty();
842         for (int i = 0; i < answerCount; i ++) {
843             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
844             final DnsRecordType type = r.type();
845             boolean matches = false;
846             for (DnsRecordType expectedType : expectedTypes) {
847                 if (type == expectedType) {
848                     matches = true;
849                     break;
850                 }
851             }
852 
853             if (!matches) {
854                 continue;
855             }
856 
857             final String questionName = question.name().toLowerCase(Locale.US);
858             final String recordName = r.name().toLowerCase(Locale.US);
859 
860             // Make sure the record is for the questioned domain.
861             if (!recordName.equals(questionName)) {
862                 Map<String, String> cnamesCopy = new HashMap<String, String>(cnames);
863                 // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records.
864                 String resolved = questionName;
865                 do {
866                     resolved = cnamesCopy.remove(resolved);
867                     if (recordName.equals(resolved)) {
868                         // We followed a CNAME chain that was part of the response without any extra queries.
869                         cnameNeedsFollow = false;
870                         break;
871                     }
872                 } while (resolved != null);
873 
874                 if (resolved == null) {
875                     assert questionName.isEmpty() || questionName.charAt(questionName.length() - 1) == '.';
876 
877                     for (String searchDomain : parent.searchDomains()) {
878                         if (searchDomain.isEmpty()) {
879                             continue;
880                         }
881 
882                         final String fqdn;
883                         if (searchDomain.charAt(searchDomain.length() - 1) == '.') {
884                             fqdn = questionName + searchDomain;
885                         } else {
886                             fqdn = questionName + searchDomain + '.';
887                         }
888                         if (recordName.equals(fqdn)) {
889                             resolved = recordName;
890                             break;
891                         }
892                     }
893                     if (resolved == null) {
894                         if (logger.isDebugEnabled()) {
895                             logger.debug("{} Ignoring record {} for [{}: {}] as it contains a different name than " +
896                                             "the question name [{}]. Cnames: {}, Search domains: {}",
897                                     channel, r.toString(), response.id(), envelope.sender(),
898                                     questionName, cnames, parent.searchDomains());
899                         }
900                         continue;
901                     }
902                 }
903             }
904 
905             final T converted = convertRecord(r, hostname, additionals, parent.executor());
906             if (converted == null) {
907                 if (logger.isDebugEnabled()) {
908                     logger.debug("{} Ignoring record {} for [{}: {}] as the converted record is null. "
909                                     + "Hostname [{}], Additionals: {}",
910                             channel, r.toString(), response.id(),
911                             envelope.sender(), hostname, additionals);
912                 }
913                 continue;
914             }
915 
916             boolean shouldRelease = false;
917             // Check if we did determine we wanted to complete early before. If this is the case we want to not
918             // include the result
919             if (!completeEarly) {
920                 completeEarly = isCompleteEarly(converted);
921             }
922 
923             // Check if the promise was done already, and only if not add things to the finalResult. Otherwise lets
924             // just release things after we cached it.
925             if (!promise.isDone()) {
926                 // We want to ensure we do not have duplicates in finalResult as this may be unexpected.
927                 //
928                 // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an
929                 // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay
930                 // for the extra memory copy and allocations in this cases later on.
931                 if (finalResult == null) {
932                     finalResult = new ArrayList<T>(8);
933                     finalResult.add(converted);
934                 } else if (isDuplicateAllowed() || !finalResult.contains(converted)) {
935                     finalResult.add(converted);
936                 } else {
937                     shouldRelease = true;
938                 }
939             } else {
940                 shouldRelease = true;
941             }
942 
943             cache(hostname, additionals, r, converted);
944             found = true;
945 
946             if (shouldRelease) {
947                 ReferenceCountUtil.release(converted);
948             }
949             // Note that we do not break from the loop here, so we decode/cache all A/AAAA records.
950         }
951 
952         if (found && !cnameNeedsFollow) {
953             // If we found the correct result we can just stop here without following any extra CNAME records in the
954             // response.
955             if (completeEarly) {
956                 this.completeEarly = true;
957             }
958             queryLifecycleObserver.querySucceed();
959         } else if (cnames.isEmpty()) {
960             queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION);
961         } else {
962             queryLifecycleObserver.querySucceed();
963             // We also got a CNAME so we need to ensure we also query it.
964             onResponseCNAME(question, cnames, newDnsQueryLifecycleObserver(question), promise);
965         }
966     }
967 
968     private void onResponseCNAME(
969             DnsQuestion question, Map<String, String> cnames,
970             final DnsQueryLifecycleObserver queryLifecycleObserver,
971             Promise<List<T>> promise) {
972 
973         // Resolve the host name in the question into the real host name.
974         String resolved = question.name().toLowerCase(Locale.US);
975         boolean found = false;
976         while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty
977                                     // because it can be Collections.emptyMap()
978                                     // whose remove() throws a UnsupportedOperationException.
979             final String next = cnames.remove(resolved);
980             if (next != null) {
981                 found = true;
982                 resolved = next;
983             } else {
984                 break;
985             }
986         }
987 
988         if (found) {
989             followCname(question, resolved, queryLifecycleObserver, promise);
990         } else {
991             queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION);
992         }
993     }
994 
995     private static Map<String, String> buildAliasMap(
996             String queryName, DnsResponse response, DnsCnameCache cache, EventLoop loop) {
997         final int answerCount = response.count(DnsSection.ANSWER);
998         Map<String, String> cnames = null;
999         for (int i = 0; i < answerCount; i ++) {
1000             final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
1001             final DnsRecordType type = r.type();
1002             if (type != DnsRecordType.CNAME) {
1003                 continue;
1004             }
1005 
1006             if (!(r instanceof DnsRawRecord)) {
1007                 continue;
1008             }
1009 
1010             final ByteBuf recordContent = ((ByteBufHolder) r).content();
1011             final String domainName = decodeDomainName(recordContent);
1012             if (domainName == null) {
1013                 continue;
1014             }
1015 
1016             if (cnames == null) {
1017                 cnames = new HashMap<String, String>(min(8, answerCount));
1018             }
1019 
1020             String name = r.name().toLowerCase(Locale.US);
1021             String mapping = domainName.toLowerCase(Locale.US);
1022 
1023             // Cache the CNAME as well.
1024             String nameWithDot = hostnameWithDot(name);
1025             String mappingWithDot = hostnameWithDot(mapping);
1026             if (!nameWithDot.equalsIgnoreCase(mappingWithDot)) {
1027                 String queryNameWithDot = hostnameWithDot(queryName.toLowerCase(Locale.US));
1028                 // Only cache the CNAME if the owner is in the bailiwick of the original query name.
1029                 boolean inBailiwick = nameWithDot.equals(queryNameWithDot) ||
1030                         nameWithDot.endsWith("." + queryNameWithDot);
1031                 if (inBailiwick) {
1032                     cache.cache(nameWithDot, mappingWithDot, r.timeToLive(), loop);
1033                 }
1034                 cnames.put(name, mapping);
1035             }
1036         }
1037 
1038         return cnames != null? cnames : Collections.<String, String>emptyMap();
1039     }
1040 
1041     private void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
1042                                     final int nameServerAddrStreamIndex,
1043                                     final DnsQuestion question,
1044                                     final DnsQueryLifecycleObserver queryLifecycleObserver,
1045                                     final Promise<List<T>> promise,
1046                                     final Throwable cause) {
1047 
1048         // There are no queries left to try.
1049         if (!completeEarly && !queriesInProgress.isEmpty()) {
1050             queryLifecycleObserver.queryCancelled(allowedQueries);
1051 
1052             // There are still some queries in process, we will try to notify once the next one finishes until
1053             // all are finished.
1054             return;
1055         }
1056 
1057         // There are no queries left to try.
1058         if (finalResult == null) {
1059             if (nameServerAddrStreamIndex < nameServerAddrStream.size()) {
1060                 if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) {
1061                     // If the queryLifecycleObserver has already been terminated we should create a new one for this
1062                     // fresh query.
1063                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question,
1064                           newDnsQueryLifecycleObserver(question), true, promise, cause);
1065                 } else {
1066                     query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver,
1067                           true, promise, cause);
1068                 }
1069                 return;
1070             }
1071 
1072             queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION);
1073 
1074             // .. and we could not find any expected records.
1075 
1076             // The following is of questionable benefit, but has been around for a while that
1077             // it may be risky to remove. Reference https://datatracker.ietf.org/doc/html/rfc8020
1078             // - If we receive NXDOMAIN we know the domain doesn't exist, any other lookup type is meaningless.
1079             // - If we receive SERVFAIL, and we attempt a CNAME that returns NOERROR with 0 answers, it may lead the
1080             //   call-site to invalidate previously advertised addresses.
1081             // Having said that, there is the case of DNS services that don't respect the protocol either
1082             // - An A lookup could result in NXDOMAIN but a CNAME may succeed with answers.
1083             // It's an imperfect world. Accept it.
1084             // Guarding it with a system property, as an opt-in functionality.
1085             if (TRY_FINAL_CNAME_ON_ADDRESS_LOOKUPS) {
1086                 // If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we
1087                 // won't try to resolve the CNAME as we only should do this if we could not get the expected records
1088                 // because they do not exist and the DNS server did probably signal it.
1089                 final boolean isValidResponse =
1090                         cause == NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION || cause == SERVFAIL_QUERY_FAILED_EXCEPTION;
1091                 if ((cause == null || isValidResponse) && !triedCNAME &&
1092                         (question.type() == DnsRecordType.A || question.type() == DnsRecordType.AAAA)) {
1093                     // As the last resort, try to query CNAME, just in case the name server has it.
1094                     triedCNAME = true;
1095 
1096                     query(hostname, DnsRecordType.CNAME, getNameServers(hostname), true, promise);
1097                     return;
1098                 }
1099             }
1100         } else {
1101             queryLifecycleObserver.queryCancelled(allowedQueries);
1102         }
1103 
1104         // We have at least one resolved record or tried CNAME as the last resort.
1105         finishResolve(promise, cause);
1106     }
1107 
1108     private void finishResolve(Promise<List<T>> promise, Throwable cause) {
1109         // If completeEarly was true we still want to continue processing the queries to ensure we still put everything
1110         // in the cache eventually.
1111         if (!completeEarly && !queriesInProgress.isEmpty()) {
1112             // If there are queries in progress, we should cancel it because we already finished the resolution.
1113             for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
1114                  i.hasNext();) {
1115                 Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = i.next();
1116                 i.remove();
1117 
1118                 f.cancel(false);
1119             }
1120         }
1121 
1122         if (finalResult != null) {
1123             if (!promise.isDone()) {
1124                 // Found at least one resolved record.
1125                 final List<T> result = filterResults(finalResult);
1126                 // Lets replace the previous stored result.
1127                 finalResult = Collections.emptyList();
1128                 if (!DnsNameResolver.trySuccess(promise, result)) {
1129                     for (T item : result) {
1130                         ReferenceCountUtil.safeRelease(item);
1131                     }
1132                 }
1133             } else {
1134                 // This should always be the case as we replaced the list once notify the promise with an empty one
1135                 // and never add to it again.
1136                 assert finalResult.isEmpty();
1137             }
1138             return;
1139         }
1140 
1141         // No resolved address found.
1142         final int maxAllowedQueries = parent.maxQueriesPerResolve();
1143         final int tries = maxAllowedQueries - allowedQueries;
1144         final StringBuilder buf = new StringBuilder(64);
1145 
1146         buf.append("Failed to resolve '").append(hostname).append("' ").append(Arrays.toString(expectedTypes));
1147         if (tries > 1) {
1148             if (tries < maxAllowedQueries) {
1149                 buf.append(" after ")
1150                    .append(tries)
1151                    .append(" queries ");
1152             } else {
1153                 buf.append(". Exceeded max queries per resolve ")
1154                 .append(maxAllowedQueries)
1155                 .append(' ');
1156             }
1157         }
1158         final UnknownHostException unknownHostException = new UnknownHostException(buf.toString());
1159         if (cause == null) {
1160             // Only cache if the failure was not because of an IO error / timeout that was caused by the query
1161             // itself.
1162             cache(hostname, additionals, unknownHostException);
1163         } else {
1164             unknownHostException.initCause(cause);
1165         }
1166         promise.tryFailure(unknownHostException);
1167     }
1168 
1169     static String decodeDomainName(ByteBuf in) {
1170         in.markReaderIndex();
1171         try {
1172             return DefaultDnsRecordDecoder.decodeName(in);
1173         } catch (CorruptedFrameException e) {
1174             // In this case we just return null.
1175             return null;
1176         } finally {
1177             in.resetReaderIndex();
1178         }
1179     }
1180 
1181     private DnsServerAddressStream getNameServers(String name) {
1182         DnsServerAddressStream stream = getNameServersFromCache(name);
1183         if (stream == null) {
1184             // We need to obtain a new stream from the parent DnsNameResolver if the hostname is not the same as
1185             // for the original query (for example we may follow CNAMEs). Otherwise let's just duplicate the
1186             // original nameservers so we correctly update the internal index
1187             if (name.equals(hostname)) {
1188                 return nameServerAddrs.duplicate();
1189             }
1190             return parent.newNameServerAddressStream(name);
1191         }
1192         return stream;
1193     }
1194 
1195     private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
1196                              Promise<List<T>> promise) {
1197         final DnsQuestion cnameQuestion;
1198         final DnsServerAddressStream stream;
1199         try {
1200             cname = cnameResolveFromCache(cnameCache(), cname);
1201             stream = getNameServers(cname);
1202             cnameQuestion = new DefaultDnsQuestion(cname, question.type(), dnsClass);
1203         } catch (Throwable cause) {
1204             queryLifecycleObserver.queryFailed(cause);
1205             PlatformDependent.throwException(cause);
1206             return;
1207         }
1208         query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion),
1209               true, promise, null);
1210     }
1211 
1212     private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
1213                           boolean flush, Promise<List<T>> promise) {
1214         final DnsQuestion question;
1215         try {
1216             question = new DefaultDnsQuestion(hostname, type, dnsClass);
1217         } catch (Throwable cause) {
1218             // Assume a single failure means that queries will succeed. If the hostname is invalid for one type
1219             // there is no case where it is known to be valid for another type.
1220             promise.tryFailure(new IllegalArgumentException("Unable to create DNS Question for: [" + hostname + ", " +
1221                     type + ']', cause));
1222             return false;
1223         }
1224         query(dnsServerAddressStream, 0, question, newDnsQueryLifecycleObserver(question), flush, promise, null);
1225         return true;
1226     }
1227 
1228     private DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) {
1229         return parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question);
1230     }
1231 
1232     private final class CombinedDnsServerAddressStream implements DnsServerAddressStream {
1233         private final InetSocketAddress replaced;
1234         private final DnsServerAddressStream originalStream;
1235         private final List<InetAddress> resolvedAddresses;
1236         private Iterator<InetAddress> resolved;
1237 
1238         CombinedDnsServerAddressStream(InetSocketAddress replaced, List<InetAddress> resolvedAddresses,
1239                                        DnsServerAddressStream originalStream) {
1240             this.replaced = replaced;
1241             this.resolvedAddresses = resolvedAddresses;
1242             this.originalStream = originalStream;
1243             resolved = resolvedAddresses.iterator();
1244         }
1245 
1246         @Override
1247         public InetSocketAddress next() {
1248             if (resolved.hasNext()) {
1249                 return nextResolved0();
1250             }
1251             InetSocketAddress address = originalStream.next();
1252             if (address.equals(replaced)) {
1253                 resolved = resolvedAddresses.iterator();
1254                 return nextResolved0();
1255             }
1256             return address;
1257         }
1258 
1259         private InetSocketAddress nextResolved0() {
1260             return parent.newRedirectServerAddress(resolved.next());
1261         }
1262 
1263         @Override
1264         public int size() {
1265             return originalStream.size() + resolvedAddresses.size() - 1;
1266         }
1267 
1268         @Override
1269         public DnsServerAddressStream duplicate() {
1270             return new CombinedDnsServerAddressStream(replaced, resolvedAddresses, originalStream.duplicate());
1271         }
1272     }
1273 
1274     /**
1275      * Holds the closed DNS Servers for a domain.
1276      */
1277     private static final class AuthoritativeNameServerList {
1278 
1279         private final String questionName;
1280 
1281         // We not expect the linked-list to be very long so a double-linked-list is overkill.
1282         private AuthoritativeNameServer head;
1283 
1284         private int nameServerCount;
1285 
1286         AuthoritativeNameServerList(String questionName) {
1287             this.questionName = questionName.toLowerCase(Locale.US);
1288         }
1289 
1290         void add(DnsRecord r) {
1291             if (r.type() != DnsRecordType.NS || !(r instanceof DnsRawRecord)) {
1292                 return;
1293             }
1294 
1295             // Only include servers that serve the correct domain.
1296             if (questionName.length() <  r.name().length()) {
1297                 return;
1298             }
1299 
1300             String recordName = r.name().toLowerCase(Locale.US);
1301 
1302             int dots = 0;
1303             for (int a = recordName.length() - 1, b = questionName.length() - 1; a >= 0; a--, b--) {
1304                 char c = recordName.charAt(a);
1305                 if (questionName.charAt(b) != c) {
1306                     return;
1307                 }
1308                 if (c == '.') {
1309                     dots++;
1310                 }
1311             }
1312 
1313             if (head != null && head.dots > dots) {
1314                 // We already have a closer match so ignore this one, no need to parse the domainName etc.
1315                 return;
1316             }
1317 
1318             final ByteBuf recordContent = ((ByteBufHolder) r).content();
1319             final String domainName = decodeDomainName(recordContent);
1320             if (domainName == null) {
1321                 // Could not be parsed, ignore.
1322                 return;
1323             }
1324 
1325             // We are only interested in preserving the nameservers which are the closest to our qName, so ensure
1326             // we drop servers that have a smaller dots count.
1327             if (head == null || head.dots < dots) {
1328                 nameServerCount = 1;
1329                 head = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1330             } else if (head.dots == dots) {
1331                 AuthoritativeNameServer serverName = head;
1332                 while (serverName.next != null) {
1333                     serverName = serverName.next;
1334                 }
1335                 serverName.next = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName);
1336                 nameServerCount++;
1337             }
1338         }
1339 
1340         void handleWithAdditional(
1341                 DnsNameResolver parent, DnsRecord r, AuthoritativeDnsServerCache authoritativeCache) {
1342             // Just walk the linked-list and mark the entry as handled when matched.
1343             AuthoritativeNameServer serverName = head;
1344 
1345             String nsName = r.name();
1346             InetAddress resolved = decodeAddress(r, nsName, parent.isDecodeIdn());
1347             if (resolved == null) {
1348                 // Could not parse the address, just ignore.
1349                 return;
1350             }
1351 
1352             while (serverName != null) {
1353                 if (serverName.nsName.equalsIgnoreCase(nsName)) {
1354                     if (serverName.address != null) {
1355                         // We received multiple ADDITIONAL records for the same name.
1356                         // Search for the last we insert before and then append a new one.
1357                         while (serverName.next != null && serverName.next.isCopy) {
1358                             serverName = serverName.next;
1359                         }
1360                         AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1361                         server.next = serverName.next;
1362                         serverName.next = server;
1363                         serverName = server;
1364 
1365                         nameServerCount++;
1366                     }
1367                     // We should replace the TTL if needed with the one of the ADDITIONAL record so we use
1368                     // the smallest for caching.
1369                     serverName.update(parent.newRedirectServerAddress(resolved), r.timeToLive());
1370 
1371                     // Cache the server now.
1372                     cache(serverName, authoritativeCache, parent.executor());
1373                     return;
1374                 }
1375                 serverName = serverName.next;
1376             }
1377         }
1378 
1379         // Now handle all AuthoritativeNameServer for which we had no ADDITIONAL record
1380         void handleWithoutAdditionals(
1381                 DnsNameResolver parent, DnsCache cache, AuthoritativeDnsServerCache authoritativeCache) {
1382             AuthoritativeNameServer serverName = head;
1383 
1384             while (serverName != null) {
1385                 if (serverName.address == null) {
1386                     // These will be resolved on the fly if needed.
1387                     cacheUnresolved(serverName, authoritativeCache, parent.executor());
1388 
1389                     // Try to resolve via cache as we had no ADDITIONAL entry for the server.
1390 
1391                     List<? extends DnsCacheEntry> entries = cache.get(serverName.nsName, null);
1392                     if (entries != null && !entries.isEmpty()) {
1393                         InetAddress address = entries.get(0).address();
1394 
1395                         // If address is null we have a resolution failure cached so just use an unresolved address.
1396                         if (address != null) {
1397                             serverName.update(parent.newRedirectServerAddress(address));
1398 
1399                             for (int i = 1; i < entries.size(); i++) {
1400                                 address = entries.get(i).address();
1401 
1402                                 assert address != null :
1403                                         "Cache returned a cached failure, should never return anything else";
1404 
1405                                 AuthoritativeNameServer server = new AuthoritativeNameServer(serverName);
1406                                 server.next = serverName.next;
1407                                 serverName.next = server;
1408                                 serverName = server;
1409                                 serverName.update(parent.newRedirectServerAddress(address));
1410 
1411                                 nameServerCount++;
1412                             }
1413                         }
1414                     }
1415                 }
1416                 serverName = serverName.next;
1417             }
1418         }
1419 
1420         private void cacheUnresolved(
1421                 AuthoritativeNameServer server, AuthoritativeDnsServerCache authoritativeCache, EventLoop loop) {
1422             // We still want to cached the unresolved address
1423             server.address = InetSocketAddress.createUnresolved(
1424                     server.nsName, DefaultDnsServerAddressStreamProvider.DNS_PORT);
1425 
1426             // Cache the server now.
1427             cache(server, authoritativeCache, loop);
1428         }
1429 
1430         private void cache(AuthoritativeNameServer server, AuthoritativeDnsServerCache cache, EventLoop loop) {
1431             // Cache NS record if not for a root server as we should never cache for root servers.
1432             if (server.isRootServer()) {
1433                 return;
1434             }
1435             // Bailiwick check (RFC 2181 ยง5.4.1): only cache a nameserver entry when its zone
1436             // equals the question name or is a subdomain of it. A server that is authoritative
1437             // for a child zone must not be trusted to supply authoritative NS records for a
1438             // parent zone, which would allow cache poisoning of the parent.
1439             if (!server.domainName.equals(questionName) &&
1440                     !server.domainName.endsWith("." + questionName)) {
1441                 return;
1442             }
1443             cache.cache(server.domainName, server.address, server.ttl, loop);
1444         }
1445 
1446         /**
1447          * Returns {@code true} if empty, {@code false} otherwise.
1448          */
1449         boolean isEmpty() {
1450             return nameServerCount == 0;
1451         }
1452 
1453         /**
1454          * Creates a new {@link List} which holds the {@link InetSocketAddress}es.
1455          */
1456         List<InetSocketAddress> addressList() {
1457             List<InetSocketAddress> addressList = new ArrayList<InetSocketAddress>(nameServerCount);
1458 
1459             AuthoritativeNameServer server = head;
1460             while (server != null) {
1461                 if (server.address != null) {
1462                     addressList.add(server.address);
1463                 }
1464                 server = server.next;
1465             }
1466             return addressList;
1467         }
1468     }
1469 
1470     private static final class AuthoritativeNameServer {
1471         private final int dots;
1472         private final String domainName;
1473         final boolean isCopy;
1474         final String nsName;
1475 
1476         private long ttl;
1477         private InetSocketAddress address;
1478 
1479         AuthoritativeNameServer next;
1480 
1481         AuthoritativeNameServer(int dots, long ttl, String domainName, String nsName) {
1482             this.dots = dots;
1483             this.ttl = ttl;
1484             this.nsName = nsName;
1485             this.domainName = domainName;
1486             isCopy = false;
1487         }
1488 
1489         AuthoritativeNameServer(AuthoritativeNameServer server) {
1490             dots = server.dots;
1491             ttl = server.ttl;
1492             nsName = server.nsName;
1493             domainName = server.domainName;
1494             isCopy = true;
1495         }
1496 
1497         /**
1498          * Returns {@code true} if its a root server.
1499          */
1500         boolean isRootServer() {
1501             return dots == 1;
1502         }
1503 
1504         /**
1505          * Update the server with the given address and TTL if needed.
1506          */
1507         void update(InetSocketAddress address, long ttl) {
1508             assert this.address == null || this.address.isUnresolved();
1509             this.address = address;
1510             this.ttl = min(this.ttl, ttl);
1511         }
1512 
1513         void update(InetSocketAddress address) {
1514             update(address, Long.MAX_VALUE);
1515         }
1516     }
1517 }