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