From cd4594d29244f7bea9d718e74ec4ecdcb6c62c13 Mon Sep 17 00:00:00 2001 From: Trustin Lee Date: Fri, 30 Mar 2018 05:01:25 +0900 Subject: [PATCH] Add DnsNameResolver.resolveAll(DnsQuestion) (#7803) * Add DnsNameResolver.resolveAll(DnsQuestion) Motivation: A user is currently expected to use DnsNameResolver.query() when he or she wants to look up the full DNS records rather than just InetAddres. However, query() only performs a single query. It does not handle /etc/hosts file, redirection, CNAMEs or multiple name servers. As a result, such a user has to duplicate all the logic in DnsNameResolverContext. Modifications: - Refactor DnsNameResolverContext so that it can send queries for arbitrary record types. - Rename DnsNameResolverContext to DnsResolveContext - Add DnsAddressResolveContext which extends DnsResolveContext for A/AAAA lookup - Add DnsRecordResolveContext which extends DnsResolveContext for arbitrary lookup - Add DnsNameResolverContext.resolveAll(DnsQuestion) and its variants - Change DnsNameResolverContext.resolve() delegates the resolve request to resolveAll() for simplicity - Move the code that decodes A/AAAA record content to DnsAddressDecoder Result: - Fixes #7795 - A user does not have to duplicate DnsNameResolverContext in his or her own code to implement the usual DNS resolver behavior. --- .../netty/resolver/dns/DnsAddressDecoder.java | 67 +++++ .../dns/DnsAddressResolveContext.java | 74 +++++ .../netty/resolver/dns/DnsNameResolver.java | 184 +++++++----- .../resolver/dns/DnsRecordResolveContext.java | 75 +++++ ...verContext.java => DnsResolveContext.java} | 262 +++++++++--------- .../resolver/dns/DnsNameResolverTest.java | 83 +++++- 6 files changed, 536 insertions(+), 209 deletions(-) create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java create mode 100644 resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java rename resolver-dns/src/main/java/io/netty/resolver/dns/{DnsNameResolverContext.java => DnsResolveContext.java} (82%) diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java new file mode 100644 index 0000000000..3dfb56ae8c --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.IDN; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.handler.codec.dns.DnsRawRecord; +import io.netty.handler.codec.dns.DnsRecord; + +/** + * Decodes an {@link InetAddress} from an A or AAAA {@link DnsRawRecord}. + */ +final class DnsAddressDecoder { + + private static final int INADDRSZ4 = 4; + private static final int INADDRSZ6 = 16; + + /** + * Decodes an {@link InetAddress} from an A or AAAA {@link DnsRawRecord}. + * + * @param record the {@link DnsRecord}, most likely a {@link DnsRawRecord} + * @param name the host name of the decoded address + * @param decodeIdn whether to convert {@code name} to a unicode host name + * + * @return the {@link InetAddress}, or {@code null} if {@code record} is not a {@link DnsRawRecord} or + * its content is malformed + */ + static InetAddress decodeAddress(DnsRecord record, String name, boolean decodeIdn) { + if (!(record instanceof DnsRawRecord)) { + return null; + } + final ByteBuf content = ((ByteBufHolder) record).content(); + final int contentLen = content.readableBytes(); + if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) { + return null; + } + + final byte[] addrBytes = new byte[contentLen]; + content.getBytes(content.readerIndex(), addrBytes); + + try { + return InetAddress.getByAddress(decodeIdn ? IDN.toUnicode(name) : name, addrBytes); + } catch (UnknownHostException e) { + // Should never reach here. + throw new Error(e); + } + } + + private DnsAddressDecoder() { } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java new file mode 100644 index 0000000000..95dcb136ed --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java @@ -0,0 +1,74 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; + +final class DnsAddressResolveContext extends DnsResolveContext { + + private final DnsCache resolveCache; + + DnsAddressResolveContext(DnsNameResolver parent, String hostname, DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs, DnsCache resolveCache) { + super(parent, hostname, DnsRecord.CLASS_IN, parent.resolveRecordTypes(), additionals, nameServerAddrs); + this.resolveCache = resolveCache; + } + + @Override + DnsResolveContext newResolverContext(DnsNameResolver parent, String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs) { + return new DnsAddressResolveContext(parent, hostname, additionals, nameServerAddrs, resolveCache); + } + + @Override + InetAddress convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop) { + return decodeAddress(record, hostname, parent.isDecodeIdn()); + } + + @Override + boolean containsExpectedResult(List finalResult) { + final int size = finalResult.size(); + final Class inetAddressType = parent.preferredAddressType().addressType(); + for (int i = 0; i < size; i++) { + InetAddress address = finalResult.get(i); + if (inetAddressType.isInstance(address)) { + return true; + } + } + return false; + } + + @Override + void cache(String hostname, DnsRecord[] additionals, + DnsRecord result, InetAddress convertedResult) { + resolveCache.cache(hostname, additionals, convertedResult, result.timeToLive(), parent.ch.eventLoop()); + } + + @Override + void cache(String hostname, DnsRecord[] additionals, UnknownHostException cause) { + resolveCache.cache(hostname, additionals, cause, parent.ch.eventLoop()); + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java index eaa714026d..5bd7baad25 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java @@ -16,6 +16,8 @@ package io.netty.resolver.dns; import io.netty.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; import io.netty.channel.AddressedEnvelope; import io.netty.channel.Channel; import io.netty.channel.ChannelFactory; @@ -33,11 +35,13 @@ import io.netty.channel.socket.InternetProtocolFamily; import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; import io.netty.handler.codec.dns.DatagramDnsResponse; import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; +import io.netty.handler.codec.dns.DefaultDnsRawRecord; import io.netty.handler.codec.dns.DnsQuestion; import io.netty.handler.codec.dns.DnsRawRecord; import io.netty.handler.codec.dns.DnsRecord; import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponse; +import io.netty.resolver.HostsFileEntries; import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.InetNameResolver; import io.netty.resolver.ResolvedAddressTypes; @@ -45,6 +49,7 @@ import io.netty.util.NetUtil; import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; import io.netty.util.concurrent.Promise; import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; @@ -55,6 +60,8 @@ import io.netty.util.internal.logging.InternalLoggerFactory; import java.lang.reflect.Method; import java.net.IDN; +import java.net.Inet4Address; +import java.net.Inet6Address; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.ArrayList; @@ -527,6 +534,96 @@ public class DnsNameResolver extends InetNameResolver { doResolve(inetHost, EMPTY_ADDITIONALS, promise, resolveCache); } + /** + * Resolves the {@link DnsRecord}s that are matched by the specified {@link DnsQuestion}. Unlike + * {@link #query(DnsQuestion)}, this method handles redirection, CNAMEs and multiple name servers. + * If the specified {@link DnsQuestion} is {@code A} or {@code AAAA}, this method looks up the configured + * {@link HostsFileEntries} before sending a query to the name servers. If a match is found in the + * {@link HostsFileEntries}, a synthetic {@code A} or {@code AAAA} record will be returned. + * + * @param question the question + * + * @return the list of the {@link DnsRecord}s as the result of the resolution + */ + public final Future> resolveAll(DnsQuestion question) { + return resolveAll(question, EMPTY_ADDITIONALS, executor().>newPromise()); + } + + /** + * Resolves the {@link DnsRecord}s that are matched by the specified {@link DnsQuestion}. Unlike + * {@link #query(DnsQuestion)}, this method handles redirection, CNAMEs and multiple name servers. + * If the specified {@link DnsQuestion} is {@code A} or {@code AAAA}, this method looks up the configured + * {@link HostsFileEntries} before sending a query to the name servers. If a match is found in the + * {@link HostsFileEntries}, a synthetic {@code A} or {@code AAAA} record will be returned. + * + * @param question the question + * @param additionals additional records ({@code OPT}) + * + * @return the list of the {@link DnsRecord}s as the result of the resolution + */ + public final Future> resolveAll(DnsQuestion question, Iterable additionals) { + return resolveAll(question, additionals, executor().>newPromise()); + } + + /** + * Resolves the {@link DnsRecord}s that are matched by the specified {@link DnsQuestion}. Unlike + * {@link #query(DnsQuestion)}, this method handles redirection, CNAMEs and multiple name servers. + * If the specified {@link DnsQuestion} is {@code A} or {@code AAAA}, this method looks up the configured + * {@link HostsFileEntries} before sending a query to the name servers. If a match is found in the + * {@link HostsFileEntries}, a synthetic {@code A} or {@code AAAA} record will be returned. + * + * @param question the question + * @param additionals additional records ({@code OPT}) + * @param promise the {@link Promise} which will be fulfilled when the resolution is finished + * + * @return the list of the {@link DnsRecord}s as the result of the resolution + */ + public final Future> resolveAll(DnsQuestion question, Iterable additionals, + Promise> promise) { + final DnsRecord[] additionalsArray = toArray(additionals, true); + return resolveAll(question, additionalsArray, promise); + } + + private Future> resolveAll(DnsQuestion question, DnsRecord[] additionals, + Promise> promise) { + checkNotNull(question, "question"); + checkNotNull(promise, "promise"); + + // Respect /etc/hosts as well if the record type is A or AAAA. + final DnsRecordType type = question.type(); + final String hostname = question.name(); + + if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { + final InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + ByteBuf content = null; + if (hostsFileEntry instanceof Inet4Address) { + if (type == DnsRecordType.A) { + content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress()); + } + } else if (hostsFileEntry instanceof Inet6Address) { + if (type == DnsRecordType.AAAA) { + content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress()); + } + } + + if (content != null) { + // Our current implementation does not support reloading the hosts file, + // so use a fairly large TTL (1 day, i.e. 86400 seconds). + trySuccess(promise, Collections.singletonList( + new DefaultDnsRawRecord(hostname, type, 86400, content))); + return promise; + } + } + } + + // It was not A/AAAA question or there was no entry in /etc/hosts. + final DnsServerAddressStream nameServerAddrs = + dnsServerAddressStreamProvider.nameServerAddressStream(hostname); + new DnsRecordResolveContext(this, question, additionals, nameServerAddrs).resolve(promise); + return promise; + } + private static DnsRecord[] toArray(Iterable additionals, boolean validateType) { checkNotNull(additionals, "additionals"); if (additionals instanceof Collection) { @@ -624,7 +721,7 @@ public class DnsNameResolver extends InetNameResolver { } } - private static void trySuccess(Promise promise, T result) { + static void trySuccess(Promise promise, T result) { if (!promise.trySuccess(result)) { logger.warn("Failed to notify success ({}) to a promise: {}", result, promise); } @@ -638,40 +735,20 @@ public class DnsNameResolver extends InetNameResolver { private void doResolveUncached(String hostname, DnsRecord[] additionals, - Promise promise, + final Promise promise, DnsCache resolveCache) { - new SingleResolverContext(this, hostname, additionals, resolveCache, - dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); - } - - static final class SingleResolverContext extends DnsNameResolverContext { - SingleResolverContext(DnsNameResolver parent, String hostname, - DnsRecord[] additionals, DnsCache resolveCache, DnsServerAddressStream nameServerAddrs) { - super(parent, hostname, additionals, resolveCache, nameServerAddrs); - } - - @Override - DnsNameResolverContext newResolverContext(DnsNameResolver parent, String hostname, - DnsRecord[] additionals, DnsCache resolveCache, - DnsServerAddressStream nameServerAddrs) { - return new SingleResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs); - } - - @Override - boolean finishResolve( - Class addressType, List resolvedEntries, - Promise promise) { - - final int numEntries = resolvedEntries.size(); - for (int i = 0; i < numEntries; i++) { - final InetAddress a = resolvedEntries.get(i).address(); - if (addressType.isInstance(a)) { - trySuccess(promise, a); - return true; + final Promise> allPromise = executor().newPromise(); + doResolveAllUncached(hostname, additionals, allPromise, resolveCache); + allPromise.addListener(new FutureListener>() { + @Override + public void operationComplete(Future> future) { + if (future.isSuccess()) { + trySuccess(promise, future.getNow().get(0)); + } else { + tryFailure(promise, future.cause()); } } - return false; - } + }); } @Override @@ -747,50 +824,13 @@ public class DnsNameResolver extends InetNameResolver { } } - static final class ListResolverContext extends DnsNameResolverContext> { - ListResolverContext(DnsNameResolver parent, String hostname, - DnsRecord[] additionals, DnsCache resolveCache, DnsServerAddressStream nameServerAddrs) { - super(parent, hostname, additionals, resolveCache, nameServerAddrs); - } - - @Override - DnsNameResolverContext> newResolverContext( - DnsNameResolver parent, String hostname, DnsRecord[] additionals, DnsCache resolveCache, - DnsServerAddressStream nameServerAddrs) { - return new ListResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs); - } - - @Override - boolean finishResolve( - Class addressType, List resolvedEntries, - Promise> promise) { - - List result = null; - final int numEntries = resolvedEntries.size(); - for (int i = 0; i < numEntries; i++) { - final InetAddress a = resolvedEntries.get(i).address(); - if (addressType.isInstance(a)) { - if (result == null) { - result = new ArrayList(numEntries); - } - result.add(a); - } - } - - if (result != null) { - promise.trySuccess(result); - return true; - } - return false; - } - } - private void doResolveAllUncached(String hostname, DnsRecord[] additionals, Promise> promise, DnsCache resolveCache) { - new ListResolverContext(this, hostname, additionals, resolveCache, - dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise); + final DnsServerAddressStream nameServerAddrs = + dnsServerAddressStreamProvider.nameServerAddressStream(hostname); + new DnsAddressResolveContext(this, hostname, additionals, nameServerAddrs, resolveCache).resolve(promise); } private static String hostname(String inetHost) { diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java new file mode 100644 index 0000000000..34b548ba62 --- /dev/null +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java @@ -0,0 +1,75 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; + +import io.netty.channel.EventLoop; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.util.ReferenceCountUtil; + +final class DnsRecordResolveContext extends DnsResolveContext { + + DnsRecordResolveContext(DnsNameResolver parent, DnsQuestion question, DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs) { + this(parent, question.name(), question.dnsClass(), + new DnsRecordType[] { question.type() }, + additionals, nameServerAddrs); + } + + private DnsRecordResolveContext(DnsNameResolver parent, String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs) { + super(parent, hostname, dnsClass, expectedTypes, additionals, nameServerAddrs); + } + + @Override + DnsResolveContext newResolverContext(DnsNameResolver parent, String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs) { + return new DnsRecordResolveContext(parent, hostname, dnsClass, expectedTypes, additionals, nameServerAddrs); + } + + @Override + DnsRecord convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop) { + return ReferenceCountUtil.retain(record); + } + + @Override + boolean containsExpectedResult(List finalResult) { + return true; + } + + @Override + void cache(String hostname, DnsRecord[] additionals, DnsRecord result, DnsRecord convertedResult) { + // Do not cache. + // XXX: When we implement cache, we would need to retain the reference count of the result record. + } + + @Override + void cache(String hostname, DnsRecord[] additionals, UnknownHostException cause) { + // Do not cache. + // XXX: When we implement cache, we would need to retain the reference count of the result record. + } +} diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java similarity index 82% rename from resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java rename to resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java index 2eea34536e..8d7c53a824 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java @@ -20,7 +20,7 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufHolder; import io.netty.channel.AddressedEnvelope; import io.netty.channel.ChannelPromise; -import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.EventLoop; import io.netty.handler.codec.CorruptedFrameException; import io.netty.handler.codec.dns.DefaultDnsQuestion; import io.netty.handler.codec.dns.DefaultDnsRecordDecoder; @@ -40,7 +40,6 @@ import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.StringUtil; import io.netty.util.internal.ThrowableUtil; -import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; @@ -54,13 +53,12 @@ import java.util.Locale; import java.util.Map; import java.util.Set; +import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress; +import static io.netty.resolver.dns.DnsNameResolver.trySuccess; import static java.lang.Math.min; import static java.util.Collections.unmodifiableList; -abstract class DnsNameResolverContext { - - private static final int INADDRSZ4 = 4; - private static final int INADDRSZ6 = 16; +abstract class DnsResolveContext { private static final FutureListener> RELEASE_RESPONSE = new FutureListener>() { @@ -73,58 +71,90 @@ abstract class DnsNameResolverContext { }; private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( new RuntimeException("No answer found and NXDOMAIN response code returned"), - DnsNameResolverContext.class, + DnsResolveContext.class, "onResponse(..)"); private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( new RuntimeException("No matching CNAME record found"), - DnsNameResolverContext.class, + DnsResolveContext.class, "onResponseCNAME(..)"); private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( new RuntimeException("No matching record type found"), - DnsNameResolverContext.class, + DnsResolveContext.class, "onResponseAorAAAA(..)"); private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace( new RuntimeException("Response type was unrecognized"), - DnsNameResolverContext.class, + DnsResolveContext.class, "onResponse(..)"); private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = ThrowableUtil.unknownStackTrace( new RuntimeException("No name servers returned an answer"), - DnsNameResolverContext.class, + DnsResolveContext.class, "tryToFinishResolve(..)"); - private final DnsNameResolver parent; + final DnsNameResolver parent; private final DnsServerAddressStream nameServerAddrs; private final String hostname; - private final DnsCache resolveCache; + private final int dnsClass; + private final DnsRecordType[] expectedTypes; private final int maxAllowedQueries; - private final InternetProtocolFamily[] resolvedInternetProtocolFamilies; private final DnsRecord[] additionals; private final Set>> queriesInProgress = Collections.newSetFromMap( new IdentityHashMap>, Boolean>()); - private List resolvedEntries; + private List finalResult; private int allowedQueries; private boolean triedCNAME; - DnsNameResolverContext(DnsNameResolver parent, - String hostname, - DnsRecord[] additionals, - DnsCache resolveCache, - DnsServerAddressStream nameServerAddrs) { + DnsResolveContext(DnsNameResolver parent, + String hostname, int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs) { + + assert expectedTypes.length > 0; + this.parent = parent; this.hostname = hostname; + this.dnsClass = dnsClass; + this.expectedTypes = expectedTypes; this.additionals = additionals; - this.resolveCache = resolveCache; this.nameServerAddrs = ObjectUtil.checkNotNull(nameServerAddrs, "nameServerAddrs"); maxAllowedQueries = parent.maxQueriesPerResolve(); - resolvedInternetProtocolFamilies = parent.resolvedInternetProtocolFamiliesUnsafe(); allowedQueries = maxAllowedQueries; } - void resolve(final Promise promise) { + /** + * Creates a new context with the given parameters. + */ + abstract DnsResolveContext newResolverContext(DnsNameResolver parent, String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs); + + /** + * Converts the given {@link DnsRecord} into {@code T}. + */ + abstract T convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop); + + /** + * Returns {@code true} if the given list contains any expected records. {@code finalResult} always contains + * at least one element. + */ + abstract boolean containsExpectedResult(List finalResult); + + /** + * Caches a successful resolution. + */ + abstract void cache(String hostname, DnsRecord[] additionals, + DnsRecord result, T convertedResult); + + /** + * Caches a failed resolution. + */ + abstract void cache(String hostname, DnsRecord[] additionals, + UnknownHostException cause); + + void resolve(final Promise> promise) { final String[] searchDomains = parent.searchDomains(); if (searchDomains.length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) { internalResolve(promise); @@ -133,10 +163,10 @@ abstract class DnsNameResolverContext { final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0]; final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1; - doSearchDomainQuery(initialHostname, new FutureListener() { + doSearchDomainQuery(initialHostname, new FutureListener>() { private int searchDomainIdx = initialSearchDomainIdx; @Override - public void operationComplete(Future future) throws Exception { + public void operationComplete(Future> future) throws Exception { Throwable cause = future.cause(); if (cause == null) { promise.trySuccess(future.getNow()); @@ -182,26 +212,24 @@ abstract class DnsNameResolverContext { } } - private void doSearchDomainQuery(String hostname, FutureListener listener) { - DnsNameResolverContext nextContext = newResolverContext(parent, hostname, additionals, resolveCache, - nameServerAddrs); - Promise nextPromise = parent.executor().newPromise(); + private void doSearchDomainQuery(String hostname, FutureListener> listener) { + DnsResolveContext nextContext = newResolverContext(parent, hostname, dnsClass, expectedTypes, + additionals, nameServerAddrs); + Promise> nextPromise = parent.executor().newPromise(); nextContext.internalResolve(nextPromise); nextPromise.addListener(listener); } - private void internalResolve(Promise promise) { + private void internalResolve(Promise> promise) { DnsServerAddressStream nameServerAddressStream = getNameServers(hostname); - DnsRecordType[] recordTypes = parent.resolveRecordTypes(); - assert recordTypes.length > 0; - final int end = recordTypes.length - 1; + final int end = expectedTypes.length - 1; for (int i = 0; i < end; ++i) { - if (!query(hostname, recordTypes[i], nameServerAddressStream.duplicate(), promise, null)) { + if (!query(hostname, expectedTypes[i], nameServerAddressStream.duplicate(), promise, null)) { return; } } - query(hostname, recordTypes[end], nameServerAddressStream, promise, null); + query(hostname, expectedTypes[end], nameServerAddressStream, promise, null); } /** @@ -291,7 +319,7 @@ abstract class DnsNameResolverContext { private void query(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex, final DnsQuestion question, - final Promise promise, Throwable cause) { + final Promise> promise, Throwable cause) { query(nameServerAddrStream, nameServerAddrStreamIndex, question, parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question), promise, cause); } @@ -300,7 +328,7 @@ abstract class DnsNameResolverContext { final int nameServerAddrStreamIndex, final DnsQuestion question, final DnsQueryLifecycleObserver queryLifecycleObserver, - final Promise promise, + final Promise> promise, final Throwable cause) { if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) { tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver, @@ -359,7 +387,7 @@ abstract class DnsNameResolverContext { void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex, final DnsQuestion question, AddressedEnvelope envelope, final DnsQueryLifecycleObserver queryLifecycleObserver, - Promise promise) { + Promise> promise) { try { final DnsResponse res = envelope.content(); final DnsResponseCode code = res.code(); @@ -370,13 +398,19 @@ abstract class DnsNameResolverContext { } final DnsRecordType type = question.type(); - if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { - onResponseAorAAAA(type, question, envelope, queryLifecycleObserver, promise); - } else if (type == DnsRecordType.CNAME) { + if (type == DnsRecordType.CNAME) { onResponseCNAME(question, envelope, queryLifecycleObserver, promise); - } else { - queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION); + return; } + + for (DnsRecordType expectedType : expectedTypes) { + if (type == expectedType) { + onExpectedResponse(question, envelope, queryLifecycleObserver, promise); + return; + } + } + + queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION); return; } @@ -397,7 +431,7 @@ abstract class DnsNameResolverContext { */ private boolean handleRedirect( DnsQuestion question, AddressedEnvelope envelope, - final DnsQueryLifecycleObserver queryLifecycleObserver, Promise promise) { + final DnsQueryLifecycleObserver queryLifecycleObserver, Promise> promise) { final DnsResponse res = envelope.content(); // Check if we have answers, if not this may be an non authority NS and so redirects must be handled. @@ -417,15 +451,14 @@ abstract class DnsNameResolverContext { } final String recordName = r.name(); - AuthoritativeNameServer authoritativeNameServer = - serverNames.remove(recordName); + final AuthoritativeNameServer authoritativeNameServer = serverNames.remove(recordName); if (authoritativeNameServer == null) { // Not a server we are interested in. continue; } - InetAddress resolved = parseAddress(r, recordName); + InetAddress resolved = decodeAddress(r, recordName, parent.isDecodeIdn()); if (resolved == null) { // Could not parse it, move to the next. continue; @@ -462,10 +495,9 @@ abstract class DnsNameResolverContext { return serverNames; } - private void onResponseAorAAAA( - DnsRecordType qType, DnsQuestion question, AddressedEnvelope envelope, - final DnsQueryLifecycleObserver queryLifecycleObserver, - Promise promise) { + private void onExpectedResponse( + DnsQuestion question, AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, Promise> promise) { // We often get a bunch of CNAMES as well when we asked for A/AAAA. final DnsResponse response = envelope.content(); @@ -476,7 +508,15 @@ abstract class DnsNameResolverContext { for (int i = 0; i < answerCount; i ++) { final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); final DnsRecordType type = r.type(); - if (type != DnsRecordType.A && type != DnsRecordType.AAAA) { + boolean matches = false; + for (DnsRecordType expectedType : expectedTypes) { + if (type == expectedType) { + matches = true; + break; + } + } + + if (!matches) { continue; } @@ -499,17 +539,17 @@ abstract class DnsNameResolverContext { } } - InetAddress resolved = parseAddress(r, hostname); - if (resolved == null) { + final T converted = convertRecord(r, hostname, additionals, parent.ch.eventLoop()); + if (converted == null) { continue; } - if (resolvedEntries == null) { - resolvedEntries = new ArrayList(8); + if (finalResult == null) { + finalResult = new ArrayList(8); } + finalResult.add(converted); - resolvedEntries.add( - resolveCache.cache(hostname, additionals, resolved, r.timeToLive(), parent.ch.eventLoop())); + cache(hostname, additionals, r, converted); found = true; // Note that we do not break from the loop here, so we decode/cache all A/AAAA records. @@ -523,47 +563,23 @@ abstract class DnsNameResolverContext { if (cnames.isEmpty()) { queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION); } else { - // We asked for A/AAAA but we got only CNAME. - onResponseCNAME(question, envelope, cnames, queryLifecycleObserver, promise); - } - } - - private InetAddress parseAddress(DnsRecord r, String name) { - if (!(r instanceof DnsRawRecord)) { - return null; - } - final ByteBuf content = ((ByteBufHolder) r).content(); - final int contentLen = content.readableBytes(); - if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) { - return null; - } - - final byte[] addrBytes = new byte[contentLen]; - content.getBytes(content.readerIndex(), addrBytes); - - try { - return InetAddress.getByAddress( - parent.isDecodeIdn() ? IDN.toUnicode(name) : name, addrBytes); - } catch (UnknownHostException e) { - // Should never reach here. - throw new Error(e); + // We got only CNAME, not one of expectedTypes. + onResponseCNAME(question, cnames, queryLifecycleObserver, promise); } } private void onResponseCNAME(DnsQuestion question, AddressedEnvelope envelope, - final DnsQueryLifecycleObserver queryLifecycleObserver, - Promise promise) { - onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), queryLifecycleObserver, promise); + final DnsQueryLifecycleObserver queryLifecycleObserver, Promise> promise) { + onResponseCNAME(question, buildAliasMap(envelope.content()), queryLifecycleObserver, promise); } private void onResponseCNAME( - DnsQuestion question, AddressedEnvelope response, - Map cnames, final DnsQueryLifecycleObserver queryLifecycleObserver, - Promise promise) { + DnsQuestion question, Map cnames, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise> promise) { // Resolve the host name in the question into the real host name. - final String name = question.name().toLowerCase(Locale.US); - String resolved = name; + String resolved = question.name().toLowerCase(Locale.US); boolean found = false; while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty // because it can be Collections.emptyMap() @@ -618,24 +634,24 @@ abstract class DnsNameResolverContext { final int nameServerAddrStreamIndex, final DnsQuestion question, final DnsQueryLifecycleObserver queryLifecycleObserver, - final Promise promise, + final Promise> promise, final Throwable cause) { // There are no queries left to try. if (!queriesInProgress.isEmpty()) { queryLifecycleObserver.queryCancelled(allowedQueries); // There are still some queries we did not receive responses for. - if (gotPreferredAddress()) { - // But it's OK to finish the resolution process if we got a resolved address of the preferred type. + if (finalResult != null && containsExpectedResult(finalResult)) { + // But it's OK to finish the resolution process if we got something expected. finishResolve(promise, cause); } - // We did not get any resolved address of the preferred type, so we can't finish the resolution process. + // We did not get an expected result yet, so we can't finish the resolution process. return; } // There are no queries left to try. - if (resolvedEntries == null) { + if (finalResult == null) { if (nameServerAddrStreamIndex < nameServerAddrStream.size()) { if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) { // If the queryLifecycleObserver has already been terminated we should create a new one for this @@ -650,11 +666,11 @@ abstract class DnsNameResolverContext { queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION); - // .. and we could not find any A/AAAA records. + // .. and we could not find any expected records. // If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we - // won't try to resolve the CNAME as we only should do this if we could not get the A/AAAA records because - // these not exists and the DNS server did probably signal it. + // won't try to resolve the CNAME as we only should do this if we could not get the expected records + // because they do not exist and the DNS server did probably signal it. if (cause == null && !triedCNAME) { // As the last resort, try to query CNAME, just in case the name server has it. triedCNAME = true; @@ -666,27 +682,11 @@ abstract class DnsNameResolverContext { queryLifecycleObserver.queryCancelled(allowedQueries); } - // We have at least one resolved address or tried CNAME as the last resort.. + // We have at least one resolved record or tried CNAME as the last resort.. finishResolve(promise, cause); } - private boolean gotPreferredAddress() { - if (resolvedEntries == null) { - return false; - } - - final int size = resolvedEntries.size(); - final Class inetAddressType = parent.preferredAddressType().addressType(); - for (int i = 0; i < size; i++) { - InetAddress address = resolvedEntries.get(i).address(); - if (inetAddressType.isInstance(address)) { - return true; - } - } - return false; - } - - private void finishResolve(Promise promise, Throwable cause) { + private void finishResolve(Promise> promise, Throwable cause) { if (!queriesInProgress.isEmpty()) { // If there are queries in progress, we should cancel it because we already finished the resolution. for (Iterator>> i = queriesInProgress.iterator(); @@ -700,13 +700,10 @@ abstract class DnsNameResolverContext { } } - if (resolvedEntries != null) { - // Found at least one resolved address. - for (InternetProtocolFamily f: resolvedInternetProtocolFamilies) { - if (finishResolve(f.addressType(), resolvedEntries, promise)) { - return; - } - } + if (finalResult != null) { + // Found at least one resolved record. + trySuccess(promise, finalResult); + return; } // No resolved address found. @@ -729,20 +726,13 @@ abstract class DnsNameResolverContext { if (cause == null) { // Only cache if the failure was not because of an IO error / timeout that was caused by the query // itself. - resolveCache.cache(hostname, additionals, unknownHostException, parent.ch.eventLoop()); + cache(hostname, additionals, unknownHostException); } else { unknownHostException.initCause(cause); } promise.tryFailure(unknownHostException); } - abstract boolean finishResolve(Class addressType, List resolvedEntries, - Promise promise); - - abstract DnsNameResolverContext newResolverContext(DnsNameResolver parent, String hostname, - DnsRecord[] additionals, DnsCache resolveCache, - DnsServerAddressStream nameServerAddrs); - static String decodeDomainName(ByteBuf in) { in.markReaderIndex(); try { @@ -760,8 +750,8 @@ abstract class DnsNameResolverContext { return stream == null ? nameServerAddrs.duplicate() : stream; } - private void followCname( - DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver, Promise promise) { + private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver, + Promise> promise) { DnsServerAddressStream stream = getNameServers(cname); final DnsQuestion cnameQuestion; @@ -776,7 +766,7 @@ abstract class DnsNameResolverContext { } private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream, - Promise promise, Throwable cause) { + Promise> promise, Throwable cause) { final DnsQuestion question = newQuestion(hostname, type); if (question == null) { return false; @@ -785,9 +775,9 @@ abstract class DnsNameResolverContext { return true; } - private static DnsQuestion newQuestion(String hostname, DnsRecordType type) { + private DnsQuestion newQuestion(String hostname, DnsRecordType type) { try { - return new DefaultDnsQuestion(hostname, type); + return new DefaultDnsQuestion(hostname, type, dnsClass); } catch (IllegalArgumentException e) { // java.net.IDN.toASCII(...) may throw an IllegalArgumentException if it fails to parse the hostname return null; diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java index 805121c730..c0fdf83b94 100644 --- a/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java @@ -28,6 +28,7 @@ import io.netty.channel.socket.InternetProtocolFamily; import io.netty.channel.socket.nio.NioDatagramChannel; import io.netty.handler.codec.dns.DefaultDnsQuestion; import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRawRecord; import io.netty.handler.codec.dns.DnsRecord; import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponse; @@ -36,6 +37,7 @@ import io.netty.handler.codec.dns.DnsSection; import io.netty.resolver.HostsFileEntriesResolver; import io.netty.resolver.ResolvedAddressTypes; import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.Future; import io.netty.util.internal.PlatformDependent; import io.netty.util.internal.SocketUtils; @@ -52,6 +54,7 @@ import org.apache.directory.server.dns.messages.ResourceRecordModifier; import org.apache.directory.server.dns.messages.ResponseCode; import org.apache.directory.server.dns.store.DnsAttribute; import org.apache.directory.server.dns.store.RecordStore; +import org.hamcrest.Matchers; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; @@ -608,7 +611,7 @@ public class DnsNameResolverTest { buf.append(' '); buf.append(recordContent.readUnsignedShort()); buf.append(' '); - buf.append(DnsNameResolverContext.decodeDomainName(recordContent)); + buf.append(DnsResolveContext.decodeDomainName(recordContent)); } logger.info("{} has the following MX records:{}", hostname, buf); @@ -945,6 +948,84 @@ public class DnsNameResolverTest { } } + @Test + public void testResolveAllMx() { + final DnsNameResolver resolver = newResolver().build(); + try { + assertThat(resolver.isRecursionDesired(), is(true)); + + final Map>> futures = new LinkedHashMap>>(); + for (String name: DOMAINS) { + if (EXCLUSIONS_QUERY_MX.contains(name)) { + continue; + } + + futures.put(name, resolver.resolveAll(new DefaultDnsQuestion(name, DnsRecordType.MX))); + } + + for (Entry>> e: futures.entrySet()) { + String hostname = e.getKey(); + Future> f = e.getValue().awaitUninterruptibly(); + + final List mxList = f.getNow(); + assertThat(mxList.size(), is(greaterThan(0))); + StringBuilder buf = new StringBuilder(); + for (DnsRecord r: mxList) { + ByteBuf recordContent = ((ByteBufHolder) r).content(); + + buf.append(StringUtil.NEWLINE); + buf.append('\t'); + buf.append(r.name()); + buf.append(' '); + buf.append(r.type().name()); + buf.append(' '); + buf.append(recordContent.readUnsignedShort()); + buf.append(' '); + buf.append(DnsResolveContext.decodeDomainName(recordContent)); + + ReferenceCountUtil.release(r); + } + + logger.info("{} has the following MX records:{}", hostname, buf); + } + } finally { + resolver.close(); + } + } + + @Test + public void testResolveAllHostsFile() { + final DnsNameResolver resolver = new DnsNameResolverBuilder(group.next()) + .channelType(NioDatagramChannel.class) + .hostsFileEntriesResolver(new HostsFileEntriesResolver() { + @Override + public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { + if ("foo.com.".equals(inetHost)) { + try { + return InetAddress.getByAddress("foo.com", new byte[] { 1, 2, 3, 4 }); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + return null; + } + }).build(); + + final List records = resolver.resolveAll(new DefaultDnsQuestion("foo.com.", A)) + .syncUninterruptibly().getNow(); + assertThat(records, Matchers.hasSize(1)); + assertThat(records.get(0), Matchers.instanceOf(DnsRawRecord.class)); + + final DnsRawRecord record = (DnsRawRecord) records.get(0); + final ByteBuf content = record.content(); + assertThat(record.name(), is("foo.com.")); + assertThat(record.dnsClass(), is(DnsRecord.CLASS_IN)); + assertThat(record.type(), is(A)); + assertThat(content.readableBytes(), is(4)); + assertThat(content.readInt(), is(0x01020304)); + record.release(); + } + @Test public void testResolveDecodeUnicode() { testResolveUnicode(true);