DNS Resolver visibility into individual queries

Motivation:
A single DNS query may follow many different paths through resolver-dns. The query may fail for various reasons related to the DNS protocol, general IO errors, it may be cancelled due to the query count being exceeded, or other reasons. A query may also result in other queries as we follow the DNS protocol (e.g. redirects, CNAME, etc...). It is currently impossible to collect information about the life cycle of an individual query though resolver-dns. This information may be valuable when considering which DNS servers are preferred over others.

Modifications:
- Introduce an interface which can provide visibility into all the potential outcomes of an individual DNS query

Result:
resolver-dns provides visibility into individual DNS queries which can be used to avoid poorly performing DNS servers.
This commit is contained in:
Scott Mitchell 2017-04-06 18:09:28 -07:00
parent 0123190214
commit 5a2d04684e
9 changed files with 616 additions and 54 deletions

View File

@ -25,6 +25,7 @@ import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInboundHandlerAdapter;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.ChannelPromise;
import io.netty.channel.EventLoop;
import io.netty.channel.FixedRecvByteBufAllocator;
import io.netty.channel.socket.DatagramChannel;
@ -172,6 +173,7 @@ public class DnsNameResolver extends InetNameResolver {
private final InternetProtocolFamily preferredAddressType;
private final DnsRecordType[] resolveRecordTypes;
private final boolean decodeIdn;
private final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory;
/**
* Creates a new DNS-based name resolver that communicates with the specified list of DNS servers.
@ -180,6 +182,8 @@ public class DnsNameResolver extends InetNameResolver {
* @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel}
* @param resolveCache the DNS resolved entries cache
* @param authoritativeDnsServerCache the cache used to find the authoritative DNS server for a domain
* @param dnsQueryLifecycleObserverFactory used to generate new instances of {@link DnsQueryLifecycleObserver} which
* can be used to track metrics for DNS servers.
* @param queryTimeoutMillis timeout of each DNS query in millis
* @param resolvedAddressTypes the preferred address types
* @param recursionDesired if recursion desired flag must be set
@ -200,6 +204,7 @@ public class DnsNameResolver extends InetNameResolver {
ChannelFactory<? extends DatagramChannel> channelFactory,
final DnsCache resolveCache,
DnsCache authoritativeDnsServerCache,
DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory,
long queryTimeoutMillis,
ResolvedAddressTypes resolvedAddressTypes,
boolean recursionDesired,
@ -213,7 +218,6 @@ public class DnsNameResolver extends InetNameResolver {
int ndots,
boolean decodeIdn) {
super(eventLoop);
checkNotNull(channelFactory, "channelFactory");
this.queryTimeoutMillis = checkPositive(queryTimeoutMillis, "queryTimeoutMillis");
this.resolvedAddressTypes = resolvedAddressTypes != null ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES;
this.recursionDesired = recursionDesired;
@ -226,6 +230,8 @@ public class DnsNameResolver extends InetNameResolver {
checkNotNull(dnsServerAddressStreamProvider, "dnsServerAddressStreamProvider");
this.resolveCache = checkNotNull(resolveCache, "resolveCache");
this.authoritativeDnsServerCache = checkNotNull(authoritativeDnsServerCache, "authoritativeDnsServerCache");
this.dnsQueryLifecycleObserverFactory =
checkNotNull(dnsQueryLifecycleObserverFactory, "dnsQueryLifecycleObserverFactory");
this.searchDomains = checkNotNull(searchDomains, "searchDomains").clone();
this.ndots = checkPositiveOrZero(ndots, "ndots");
this.decodeIdn = decodeIdn;
@ -292,6 +298,19 @@ public class DnsNameResolver extends InetNameResolver {
return DNS_PORT;
}
final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory() {
return dnsQueryLifecycleObserverFactory;
}
/**
* Provides the opportunity to sort the name servers before following a redirected DNS query.
* @param nameServers The addresses of the DNS servers which are used in the event of a redirect.
* @return A {@link DnsServerAddressStream} which will be used to follow the DNS redirect.
*/
protected DnsServerAddressStream uncachedRedirectDnsServerStream(List<InetSocketAddress> nameServers) {
return DnsServerAddresses.sequential(nameServers).stream();
}
/**
* Returns the resolution cache.
*/
@ -868,15 +887,24 @@ public class DnsNameResolver extends InetNameResolver {
return query0(nameServerAddr, question, toArray(additionals, false), promise);
}
Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query0(
final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query0(
InetSocketAddress nameServerAddr, DnsQuestion question,
DnsRecord[] additionals,
Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> promise) {
return query0(nameServerAddr, question, additionals, ch.newPromise(), promise);
}
final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> query0(
InetSocketAddress nameServerAddr, DnsQuestion question,
DnsRecord[] additionals,
ChannelPromise writePromise,
Promise<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>> promise) {
assert !writePromise.isVoid();
final Promise<AddressedEnvelope<DnsResponse, InetSocketAddress>> castPromise = cast(
checkNotNull(promise, "promise"));
try {
new DnsQueryContext(this, nameServerAddr, question, additionals, castPromise).query();
new DnsQueryContext(this, nameServerAddr, question, additionals, castPromise).query(writePromise);
return castPromise;
} catch (Exception e) {
return castPromise.setFailure(e);

View File

@ -52,6 +52,8 @@ public final class DnsNameResolverBuilder {
private boolean optResourceEnabled = true;
private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT;
private DnsServerAddressStreamProvider dnsServerAddressStreamProvider = platformDefault();
private DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory =
NoopDnsQueryLifecycleObserverFactory.INSTANCE;
private String[] searchDomains = DnsNameResolver.DEFAULT_SEARCH_DOMAINS;
private int ndots = 1;
private boolean decodeIdn = true;
@ -99,6 +101,17 @@ public final class DnsNameResolverBuilder {
return this;
}
/**
* Set the factory used to generate objects which can observe individual DNS queries.
* @param lifecycleObserverFactory the factory used to generate objects which can observe individual DNS queries.
* @return {@code this}
*/
public DnsNameResolverBuilder dnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory
lifecycleObserverFactory) {
this.dnsQueryLifecycleObserverFactory = checkNotNull(lifecycleObserverFactory, "lifecycleObserverFactory");
return this;
}
/**
* Sets the cache for authoritative NS servers
*
@ -350,6 +363,7 @@ public final class DnsNameResolverBuilder {
channelFactory,
resolveCache,
authoritativeDnsServerCache,
dnsQueryLifecycleObserverFactory,
queryTimeoutMillis,
resolvedAddressTypes,
recursionDesired,

View File

@ -19,6 +19,7 @@ package io.netty.resolver.dns;
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.handler.codec.CorruptedFrameException;
import io.netty.handler.codec.dns.DefaultDnsQuestion;
@ -35,7 +36,9 @@ import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.FutureListener;
import io.netty.util.concurrent.Promise;
import io.netty.util.internal.ObjectUtil;
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;
@ -51,6 +54,9 @@ import java.util.Locale;
import java.util.Map;
import java.util.Set;
import static java.lang.Math.min;
import static java.util.Collections.unmodifiableList;
abstract class DnsNameResolverContext<T> {
private static final int INADDRSZ4 = 4;
@ -65,6 +71,22 @@ abstract class DnsNameResolverContext<T> {
}
}
};
private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
new RuntimeException("No answer found and NXDOMAIN response code returned"),
DnsNameResolverContext.class,
"onResponse(..)");
private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
new RuntimeException("No matching CNAME record found"),
DnsNameResolverContext.class,
"onResponseCNAME(..)");
private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
new RuntimeException("No matching record type record found"),
DnsNameResolverContext.class,
"onResponseAorAAAA(..)");
private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = ThrowableUtil.unknownStackTrace(
new RuntimeException("Response type was unrecognized"),
DnsNameResolverContext.class,
"onResponse(..)");
private final DnsNameResolver parent;
private final DnsServerAddressStream nameServerAddrs;
@ -203,8 +225,7 @@ abstract class DnsNameResolverContext<T> {
List<DnsCacheEntry> entries = parent.authoritativeDnsServerCache().get(hostname, additionals);
if (entries != null && !entries.isEmpty()) {
// Found a match in the cache... Also shuffle them so we not always use the same order for lookup.
return DnsServerAddresses.shuffled(new DnsCacheIterable(entries)).stream();
return DnsServerAddresses.sequential(new DnsCacheIterable(entries)).stream();
}
}
}
@ -242,34 +263,49 @@ abstract class DnsNameResolverContext<T> {
private void query(final DnsServerAddressStream nameServerAddrStream, final DnsQuestion question,
final Promise<T> promise) {
query(nameServerAddrStream, question, parent.dnsQueryLifecycleObserverFactory()
.newDnsQueryLifecycleObserver(question), promise);
}
private void query(final DnsServerAddressStream nameServerAddrStream, final DnsQuestion question,
final DnsQueryLifecycleObserver queryLifecycleObserver,
final Promise<T> promise) {
if (allowedQueries == 0 || promise.isCancelled()) {
queryLifecycleObserver.queryCancelled(allowedQueries);
tryToFinishResolve(promise);
return;
}
allowedQueries --;
final InetSocketAddress nameServerAddr = nameServerAddrStream.next();
final ChannelPromise writePromise = parent.ch.newPromise();
final Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> f = parent.query0(
nameServerAddrStream.next(), question, additionals,
nameServerAddr, question, additionals, writePromise,
parent.ch.eventLoop().<AddressedEnvelope<? extends DnsResponse, InetSocketAddress>>newPromise());
queriesInProgress.add(f);
queryLifecycleObserver.queryWritten(nameServerAddr, writePromise);
f.addListener(new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
@Override
public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAddress>> future) {
queriesInProgress.remove(future);
if (promise.isDone() || future.isCancelled()) {
queryLifecycleObserver.queryCancelled(allowedQueries);
return;
}
try {
if (future.isSuccess()) {
onResponse(nameServerAddrStream, question, future.getNow(), promise);
onResponse(nameServerAddrStream, question, future.getNow(), queryLifecycleObserver, promise);
} else {
// Server did not respond or I/O error occurred; try again.
Throwable cause = future.cause();
queryLifecycleObserver.queryFailed(cause);
if (traceEnabled) {
addTrace(future.cause());
addTrace(cause);
}
query(nameServerAddrStream, question, promise);
}
@ -281,21 +317,25 @@ abstract class DnsNameResolverContext<T> {
}
void onResponse(final DnsServerAddressStream nameServerAddrStream, final DnsQuestion question,
AddressedEnvelope<DnsResponse, InetSocketAddress> envelope, Promise<T> promise) {
AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
final DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<T> promise) {
try {
final DnsResponse res = envelope.content();
final DnsResponseCode code = res.code();
if (code == DnsResponseCode.NOERROR) {
if (handleRedirect(question, envelope, promise)) {
if (handleRedirect(question, envelope, queryLifecycleObserver, promise)) {
// Was a redirect so return here as everything else is handled in handleRedirect(...)
return;
}
final DnsRecordType type = question.type();
if (type == DnsRecordType.A || type == DnsRecordType.AAAA) {
onResponseAorAAAA(type, question, envelope, promise);
onResponseAorAAAA(type, question, envelope, queryLifecycleObserver, promise);
} else if (type == DnsRecordType.CNAME) {
onResponseCNAME(question, envelope, promise);
onResponseCNAME(question, envelope, queryLifecycleObserver, promise);
} else {
queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION);
}
return;
}
@ -308,7 +348,9 @@ abstract class DnsNameResolverContext<T> {
// Retry with the next server if the server did not tell us that the domain does not exist.
if (code != DnsResponseCode.NXDOMAIN) {
query(nameServerAddrStream, question, promise);
query(nameServerAddrStream, question, queryLifecycleObserver.queryNoAnswer(code), promise);
} else {
queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION);
}
} finally {
ReferenceCountUtil.safeRelease(envelope);
@ -319,7 +361,8 @@ abstract class DnsNameResolverContext<T> {
* Handles a redirect answer if needed and returns {@code true} if a redirect query has been made.
*/
private boolean handleRedirect(
DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope, Promise<T> promise) {
DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<T> 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.
@ -363,8 +406,8 @@ abstract class DnsNameResolverContext<T> {
"no matching authoritative name server found in the ADDITIONALS section");
}
} else {
// Shuffle as we want to re-distribute the load across name servers.
query(DnsServerAddresses.shuffled(nameServers).stream(), question, promise);
query(parent.uncachedRedirectDnsServerStream(nameServers), question,
queryLifecycleObserver.queryRedirected(unmodifiableList(nameServers)), promise);
return true;
}
}
@ -391,6 +434,7 @@ abstract class DnsNameResolverContext<T> {
private void onResponseAorAAAA(
DnsRecordType qType, DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
final DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<T> promise) {
// We often get a bunch of CNAMES as well when we asked for A/AAAA.
@ -443,6 +487,7 @@ abstract class DnsNameResolverContext<T> {
}
if (found) {
queryLifecycleObserver.querySucceed();
return;
}
@ -450,9 +495,11 @@ abstract class DnsNameResolverContext<T> {
addTrace(envelope.sender(), "no matching " + qType + " record found");
}
// We aked for A/AAAA but we got only CNAME.
if (!cnames.isEmpty()) {
onResponseCNAME(question, envelope, cnames, false, promise);
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, false, queryLifecycleObserver, promise);
}
}
@ -479,13 +526,15 @@ abstract class DnsNameResolverContext<T> {
}
private void onResponseCNAME(DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
final DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<T> promise) {
onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), true, promise);
onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), true, queryLifecycleObserver, promise);
}
private void onResponseCNAME(
DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> response,
Map<String, String> cnames, boolean trace, Promise<T> promise) {
Map<String, String> cnames, boolean trace, final DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<T> promise) {
// Resolve the host name in the question into the real host name.
final String name = question.name().toLowerCase(Locale.US);
@ -504,11 +553,14 @@ abstract class DnsNameResolverContext<T> {
}
if (found) {
followCname(response.sender(), name, resolved, promise);
} else if (trace && traceEnabled) {
followCname(response.sender(), name, resolved, queryLifecycleObserver, promise);
} else {
queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION);
if (trace && traceEnabled) {
addTrace(response.sender(), "no matching CNAME record found");
}
}
}
private static Map<String, String> buildAliasMap(DnsResponse response) {
final int answerCount = response.count(DnsSection.ANSWER);
@ -531,7 +583,7 @@ abstract class DnsNameResolverContext<T> {
}
if (cnames == null) {
cnames = new HashMap<String, String>();
cnames = new HashMap<String, String>(min(8, answerCount));
}
cnames.put(r.name().toLowerCase(Locale.US), domainName.toLowerCase(Locale.US));
@ -663,7 +715,9 @@ abstract class DnsNameResolverContext<T> {
return stream == null ? nameServerAddrs : stream;
}
private void followCname(InetSocketAddress nameServerAddr, String name, String cname, Promise<T> promise) {
private void followCname(InetSocketAddress nameServerAddr, String name, String cname,
final DnsQueryLifecycleObserver queryLifecycleObserver,
Promise<T> promise) {
if (traceEnabled) {
if (trace == null) {
@ -682,27 +736,51 @@ abstract class DnsNameResolverContext<T> {
// Use the same server for both CNAME queries
DnsServerAddressStream stream = DnsServerAddresses.singleton(getNameServers(cname).next()).stream();
if (parent.supportsARecords() && !query(hostname, DnsRecordType.A, stream, promise)) {
DnsQuestion cnameQuestion = null;
if (parent.supportsARecords()) {
try {
if ((cnameQuestion = newQuestion(hostname, DnsRecordType.A)) == null) {
return;
}
} catch (Throwable cause) {
queryLifecycleObserver.queryFailed(cause);
PlatformDependent.throwException(cause);
}
query(stream, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise);
}
if (parent.supportsAAAARecords()) {
query(hostname, DnsRecordType.AAAA, stream, promise);
try {
if ((cnameQuestion = newQuestion(hostname, DnsRecordType.AAAA)) == null) {
return;
}
} catch (Throwable cause) {
queryLifecycleObserver.queryFailed(cause);
PlatformDependent.throwException(cause);
}
query(stream, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), promise);
}
}
private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream nextAddr, Promise<T> promise) {
final DnsQuestion question;
private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
Promise<T> promise) {
final DnsQuestion question = newQuestion(hostname, type);
if (question == null) {
return false;
}
query(dnsServerAddressStream, question, promise);
return true;
}
private DnsQuestion newQuestion(String hostname, DnsRecordType type) {
try {
question = new DefaultDnsQuestion(hostname, type);
return new DefaultDnsQuestion(hostname, type);
} catch (IllegalArgumentException e) {
// java.net.IDN.toASCII(...) may throw an IllegalArgumentException if it fails to parse the hostname
if (traceEnabled) {
addTrace(e);
}
return false;
return null;
}
query(nextAddr, question, promise);
return true;
}
private void addTrace(InetSocketAddress nameServerAddr, String msg) {

View File

@ -19,6 +19,7 @@ import io.netty.channel.AddressedEnvelope;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelPromise;
import io.netty.handler.codec.dns.DatagramDnsQuery;
import io.netty.handler.codec.dns.AbstractDnsOptPseudoRrRecord;
import io.netty.handler.codec.dns.DnsQuery;
@ -84,7 +85,7 @@ final class DnsQueryContext {
return question;
}
void query() {
void query(ChannelPromise writePromise) {
final DnsQuestion question = question();
final InetSocketAddress nameServerAddr = nameServerAddr();
final DatagramDnsQuery query = new DatagramDnsQuery(null, nameServerAddr, id);
@ -105,28 +106,30 @@ final class DnsQueryContext {
logger.debug("{} WRITE: [{}: {}], {}", parent.ch, id, nameServerAddr, question);
}
sendQuery(query);
sendQuery(query, writePromise);
}
private void sendQuery(final DnsQuery query) {
private void sendQuery(final DnsQuery query, final ChannelPromise writePromise) {
if (parent.channelFuture.isDone()) {
writeQuery(query);
writeQuery(query, writePromise);
} else {
parent.channelFuture.addListener(new GenericFutureListener<Future<? super Channel>>() {
@Override
public void operationComplete(Future<? super Channel> future) throws Exception {
if (future.isSuccess()) {
writeQuery(query);
writeQuery(query, writePromise);
} else {
promise.tryFailure(future.cause());
Throwable cause = future.cause();
promise.tryFailure(cause);
writePromise.setFailure(cause);
}
}
});
}
}
private void writeQuery(final DnsQuery query) {
final ChannelFuture writeFuture = parent.ch.writeAndFlush(query);
private void writeQuery(final DnsQuery query, final ChannelPromise writePromise) {
final ChannelFuture writeFuture = parent.ch.writeAndFlush(query, writePromise);
if (writeFuture.isDone()) {
onQueryWriteCompletion(writeFuture);
} else {

View File

@ -0,0 +1,101 @@
/*
* Copyright 2017 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 io.netty.channel.ChannelFuture;
import io.netty.handler.codec.dns.DnsQuestion;
import io.netty.handler.codec.dns.DnsRecordType;
import io.netty.handler.codec.dns.DnsResponseCode;
import io.netty.util.internal.UnstableApi;
import java.net.InetSocketAddress;
import java.util.List;
/**
* This interface provides visibility into individual DNS queries. The lifecycle of an objects is as follows:
* <ol>
* <li>Object creation</li>
* <li>{@link #queryCancelled(int)}</li>
* </ol>
* OR
* <ol>
* <li>Object creation</li>
* <li>{@link #queryWritten(InetSocketAddress, ChannelFuture)}</li>
* <li>{@link #queryRedirected(List)} or {@link #queryCNAMEd(DnsQuestion)} or
* {@link #queryNoAnswer(DnsResponseCode)} or {@link #queryCancelled(int)} or
* {@link #queryFailed(Throwable)} or {@link #querySucceed()}</li>
* </ol>
* <p>
* This interface can be used to track metrics for individual DNS servers. Methods which may lead to another DNS query
* return an object of type {@link DnsQueryLifecycleObserver}. Implementations may use this to build a query tree to
* understand the "sub queries" generated by a single query.
*/
@UnstableApi
public interface DnsQueryLifecycleObserver {
/**
* The query has been written.
* @param dnsServerAddress The DNS server address which the query was sent to.
* @param future The future which represents the status of the write operation for the DNS query.
*/
void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future);
/**
* The query may have been written but it was cancelled at some point.
* @param queriesRemaining The number of queries remaining.
*/
void queryCancelled(int queriesRemaining);
/**
* The query has been redirected to another list of DNS servers.
* @param nameServers The name servers the query has been redirected to.
* @return An observer for the new query which we may issue.
*/
DnsQueryLifecycleObserver queryRedirected(List<InetSocketAddress> nameServers);
/**
* The query returned a CNAME which we may attempt to follow with a new query.
* <p>
* Note that multiple queries may be encountering a CNAME. For example a if both {@link DnsRecordType#AAAA} and
* {@link DnsRecordType#A} are supported we may query for both.
* @param cnameQuestion the question we would use if we issue a new query.
* @return An observer for the new query which we may issue.
*/
DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion);
/**
* The response to the query didn't provide the expected response code, but it didn't return
* {@link DnsResponseCode#NXDOMAIN} so we may try to query again.
* @param code the unexpected response code.
* @return An observer for the new query which we may issue.
*/
DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code);
/**
* The following criteria are possible:
* <ul>
* <li>IO Error</li>
* <li>Server responded with an invalid DNS response</li>
* <li>Server responded with a valid DNS response, but it didn't progress the resolution</li>
* </ul>
* @param cause The cause which for the failure.
*/
void queryFailed(Throwable cause);
/**
* The query received the expected results.
*/
void querySucceed();
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2017 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 io.netty.handler.codec.dns.DnsQuestion;
import io.netty.util.internal.UnstableApi;
/**
* Used to generate new instances of {@link DnsQueryLifecycleObserver}.
*/
@UnstableApi
public interface DnsQueryLifecycleObserverFactory {
/**
* Create a new instance of a {@link DnsQueryLifecycleObserver}. This will be called at the start of a new query.
* @param question The question being asked.
* @return a new instance of a {@link DnsQueryLifecycleObserver}.
*/
DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question);
}

View File

@ -0,0 +1,61 @@
/*
* Copyright 2017 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 io.netty.channel.ChannelFuture;
import io.netty.handler.codec.dns.DnsQuestion;
import io.netty.handler.codec.dns.DnsResponseCode;
import java.net.InetSocketAddress;
import java.util.List;
final class NoopDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver {
static final NoopDnsQueryLifecycleObserver INSTANCE = new NoopDnsQueryLifecycleObserver();
private NoopDnsQueryLifecycleObserver() {
}
@Override
public void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) {
}
@Override
public void queryCancelled(int queriesRemaining) {
}
@Override
public DnsQueryLifecycleObserver queryRedirected(List<InetSocketAddress> nameServers) {
return this;
}
@Override
public DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion) {
return this;
}
@Override
public DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code) {
return this;
}
@Override
public void queryFailed(Throwable cause) {
}
@Override
public void querySucceed() {
}
}

View File

@ -0,0 +1,32 @@
/*
* Copyright 2017 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 io.netty.handler.codec.dns.DnsQuestion;
import io.netty.util.internal.UnstableApi;
@UnstableApi
public final class NoopDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory {
public static final NoopDnsQueryLifecycleObserverFactory INSTANCE = new NoopDnsQueryLifecycleObserverFactory();
private NoopDnsQueryLifecycleObserverFactory() {
}
@Override
public DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) {
return NoopDnsQueryLifecycleObserver.INSTANCE;
}
}

View File

@ -18,6 +18,7 @@ package io.netty.resolver.dns;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.AddressedEnvelope;
import io.netty.channel.ChannelFuture;
import io.netty.channel.EventLoop;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ReflectiveChannelFactory;
@ -26,6 +27,7 @@ import io.netty.channel.socket.DatagramChannel;
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.DnsRecord;
import io.netty.handler.codec.dns.DnsRecordType;
import io.netty.handler.codec.dns.DnsResponse;
@ -57,6 +59,7 @@ import java.io.IOException;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
@ -66,9 +69,14 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.TimeUnit;
import static io.netty.handler.codec.dns.DnsRecordType.A;
import static io.netty.handler.codec.dns.DnsRecordType.AAAA;
import static io.netty.handler.codec.dns.DnsRecordType.CNAME;
import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT;
import static io.netty.resolver.dns.DnsServerAddresses.sequential;
import static org.hamcrest.Matchers.greaterThan;
@ -80,6 +88,7 @@ import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
public class DnsNameResolverTest {
@ -285,6 +294,7 @@ public class DnsNameResolverTest {
private static DnsNameResolverBuilder newResolver(boolean decodeToUnicode,
DnsServerAddressStreamProvider dnsServerAddressStreamProvider) {
DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next())
.dnsQueryLifecycleObserverFactory(new TestRecursiveCacheDnsQueryLifecycleObserverFactory())
.channelType(NioDatagramChannel.class)
.maxQueriesPerResolve(1)
.decodeIdn(decodeToUnicode)
@ -329,7 +339,7 @@ public class DnsNameResolverTest {
public void testResolveAorAAAA() throws Exception {
DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_PREFERRED).build();
try {
testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
testResolve0(resolver, EXCLUSIONS_RESOLVE_A, AAAA);
} finally {
resolver.close();
}
@ -339,7 +349,7 @@ public class DnsNameResolverTest {
public void testResolveAAAAorA() throws Exception {
DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV6_PREFERRED).build();
try {
testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
testResolve0(resolver, EXCLUSIONS_RESOLVE_A, A);
} finally {
resolver.close();
}
@ -391,7 +401,7 @@ public class DnsNameResolverTest {
}
}).build();
try {
final Map<String, InetAddress> resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
final Map<String, InetAddress> resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A, AAAA);
for (Entry<String, InetAddress> resolvedEntry : resultA.entrySet()) {
if (resolvedEntry.getValue().isLoopbackAddress()) {
continue;
@ -419,13 +429,13 @@ public class DnsNameResolverTest {
.ttl(Integer.MAX_VALUE, Integer.MAX_VALUE)
.build();
try {
final Map<String, InetAddress> resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
final Map<String, InetAddress> resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A, null);
// Now, try to resolve again to see if it's cached.
// This test works because the DNS servers usually randomizes the order of the records in a response.
// If cached, the resolved addresses must be always same, because we reuse the same response.
final Map<String, InetAddress> resultB = testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
final Map<String, InetAddress> resultB = testResolve0(resolver, EXCLUSIONS_RESOLVE_A, null);
// Ensure the result from the cache is identical from the uncached one.
assertThat(resultB.size(), is(resultA.size()));
@ -447,7 +457,7 @@ public class DnsNameResolverTest {
public void testResolveAAAA() throws Exception {
DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV6_ONLY).build();
try {
testResolve0(resolver, EXCLUSIONS_RESOLVE_AAAA);
testResolve0(resolver, EXCLUSIONS_RESOLVE_AAAA, null);
} finally {
resolver.close();
}
@ -457,7 +467,7 @@ public class DnsNameResolverTest {
public void testNonCachedResolve() throws Exception {
DnsNameResolver resolver = newNonCachedResolver(ResolvedAddressTypes.IPV4_ONLY).build();
try {
testResolve0(resolver, EXCLUSIONS_RESOLVE_A);
testResolve0(resolver, EXCLUSIONS_RESOLVE_A, null);
} finally {
resolver.close();
}
@ -504,7 +514,8 @@ public class DnsNameResolverTest {
}
}
private static Map<String, InetAddress> testResolve0(DnsNameResolver resolver, Set<String> excludedDomains)
private static Map<String, InetAddress> testResolve0(DnsNameResolver resolver, Set<String> excludedDomains,
DnsRecordType cancelledType)
throws InterruptedException {
assertThat(resolver.isRecursionDesired(), is(true));
@ -542,6 +553,8 @@ public class DnsNameResolverTest {
results.put(resolved.getHostName(), resolved);
}
assertQueryObserver(resolver, cancelledType);
return results;
}
@ -595,6 +608,10 @@ public class DnsNameResolverTest {
logger.info("{} has the following MX records:{}", hostname, buf);
response.release();
// We only track query lifecycle if it is managed by the DnsNameResolverContext, and not direct calls
// to query.
assertNoQueriesMade(resolver);
}
} finally {
resolver.close();
@ -644,6 +661,22 @@ public class DnsNameResolverTest {
return null;
} catch (Exception e) {
assertThat(e, is(instanceOf(UnknownHostException.class)));
TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory =
(TestRecursiveCacheDnsQueryLifecycleObserverFactory) resolver.dnsQueryLifecycleObserverFactory();
TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll();
if (observer != null) {
Object o = observer.events.poll();
if (o instanceof QueryCancelledEvent) {
assertTrue("unexpected type: " + observer.question,
observer.question.type() == CNAME || observer.question.type() == AAAA);
} else if (o instanceof QueryWrittenEvent) {
QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll();
} else {
fail("unexpected event type: " + o);
}
assertTrue(observer.events.isEmpty());
}
return (UnknownHostException) e;
}
}
@ -655,6 +688,9 @@ public class DnsNameResolverTest {
InetAddress address = resolver.resolve("10.0.0.1").syncUninterruptibly().getNow();
assertEquals("10.0.0.1", address.getHostAddress());
// This address is already resolved, and so we shouldn't have to query for anything.
assertNoQueriesMade(resolver);
} finally {
resolver.close();
}
@ -685,6 +721,9 @@ public class DnsNameResolverTest {
try {
InetAddress address = resolver.resolve(name).syncUninterruptibly().getNow();
assertEquals(expectedAddr, address);
// We are resolving the local address, so we shouldn't make any queries.
assertNoQueriesMade(resolver);
} finally {
resolver.close();
}
@ -716,6 +755,9 @@ public class DnsNameResolverTest {
List<InetAddress> addresses = resolver.resolveAll(name).syncUninterruptibly().getNow();
assertEquals(1, addresses.size());
assertEquals(expectedAddr, addresses.get(0));
// We are resolving the local address, so we shouldn't make any queries.
assertNoQueriesMade(resolver);
} finally {
resolver.close();
}
@ -738,6 +780,8 @@ public class DnsNameResolverTest {
InetAddress address = resolver.resolve(entries.getKey()).syncUninterruptibly().getNow();
assertEquals(decode ? entries.getKey() : entries.getValue(), address.getHostName());
}
assertQueryObserver(resolver, AAAA);
} finally {
resolver.close();
}
@ -753,7 +797,8 @@ public class DnsNameResolverTest {
testRecursiveResolveCache(true);
}
private static void testRecursiveResolveCache(boolean cache) throws Exception {
private static void testRecursiveResolveCache(boolean cache)
throws Exception {
final String hostname = "some.record.netty.io";
final String hostname2 = "some2.record.netty.io";
@ -766,11 +811,14 @@ public class DnsNameResolverTest {
dnsServer.start();
TestDnsCache nsCache = new TestDnsCache(cache ? new DefaultDnsCache() : NoopDnsCache.INSTANCE);
TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory =
new TestRecursiveCacheDnsQueryLifecycleObserverFactory();
EventLoopGroup group = new NioEventLoopGroup(1);
DnsNameResolver resolver = new DnsNameResolver(
group.next(), new ReflectiveChannelFactory<DatagramChannel>(NioDatagramChannel.class),
NoopDnsCache.INSTANCE, nsCache, 3000, ResolvedAddressTypes.IPV4_ONLY, true, 10, true, 4096, false,
HostsFileEntriesResolver.DEFAULT, new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()),
NoopDnsCache.INSTANCE, nsCache, lifecycleObserverFactory, 3000, ResolvedAddressTypes.IPV4_ONLY, true,
10, true, 4096, false, HostsFileEntriesResolver.DEFAULT,
new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()),
DnsNameResolver.DEFAULT_SEARCH_DOMAINS, 0, true) {
@Override
int dnsRedirectPort(InetAddress server) {
@ -782,6 +830,19 @@ public class DnsNameResolverTest {
try {
resolver.resolveAll(hostname).syncUninterruptibly();
TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll();
assertNotNull(observer);
assertTrue(lifecycleObserverFactory.observers.isEmpty());
assertEquals(4, observer.events.size());
QueryWrittenEvent writtenEvent1 = (QueryWrittenEvent) observer.events.poll();
assertEquals(dnsServer.localAddress(), writtenEvent1.dnsServerAddress);
QueryRedirectedEvent redirectedEvent = (QueryRedirectedEvent) observer.events.poll();
assertEquals("dns4.some.record.netty.io.", redirectedEvent.nameServers.get(0).getHostName());
assertEquals(dnsServerAuthority.localAddress(), redirectedEvent.nameServers.get(0));
QueryWrittenEvent writtenEvent2 = (QueryWrittenEvent) observer.events.poll();
assertEquals(dnsServerAuthority.localAddress(), writtenEvent2.dnsServerAddress);
QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll();
if (cache) {
assertNull(nsCache.cache.get("io.", null));
assertNull(nsCache.cache.get("netty.io.", null));
@ -792,8 +853,27 @@ public class DnsNameResolverTest {
// Test again via cache.
resolver.resolveAll(hostname).syncUninterruptibly();
observer = lifecycleObserverFactory.observers.poll();
assertNotNull(observer);
assertTrue(lifecycleObserverFactory.observers.isEmpty());
assertEquals(2, observer.events.size());
writtenEvent1 = (QueryWrittenEvent) observer.events.poll();
assertEquals("dns4.some.record.netty.io.", writtenEvent1.dnsServerAddress.getHostName());
assertEquals(dnsServerAuthority.localAddress(), writtenEvent1.dnsServerAddress);
succeededEvent = (QuerySucceededEvent) observer.events.poll();
resolver.resolveAll(hostname2).syncUninterruptibly();
observer = lifecycleObserverFactory.observers.poll();
assertNotNull(observer);
assertTrue(lifecycleObserverFactory.observers.isEmpty());
assertEquals(2, observer.events.size());
writtenEvent1 = (QueryWrittenEvent) observer.events.poll();
assertEquals("dns4.some.record.netty.io.", writtenEvent1.dnsServerAddress.getHostName());
assertEquals(dnsServerAuthority.localAddress(), writtenEvent1.dnsServerAddress);
succeededEvent = (QuerySucceededEvent) observer.events.poll();
// Check that it only queried the cache for record.netty.io.
assertNull(nsCache.cacheHits.get("io."));
assertNull(nsCache.cacheHits.get("netty.io."));
@ -819,6 +899,139 @@ public class DnsNameResolverTest {
futures.put(hostname, resolver.query(new DefaultDnsQuestion(hostname, DnsRecordType.MX)));
}
private static void assertNoQueriesMade(DnsNameResolver resolver) {
TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory =
(TestRecursiveCacheDnsQueryLifecycleObserverFactory) resolver.dnsQueryLifecycleObserverFactory();
assertTrue(lifecycleObserverFactory.observers.isEmpty());
}
private static void assertQueryObserver(DnsNameResolver resolver, DnsRecordType cancelledType) {
TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory =
(TestRecursiveCacheDnsQueryLifecycleObserverFactory) resolver.dnsQueryLifecycleObserverFactory();
TestDnsQueryLifecycleObserver observer;
while ((observer = lifecycleObserverFactory.observers.poll()) != null) {
Object o = observer.events.poll();
if (o instanceof QueryCancelledEvent) {
assertEquals(cancelledType, observer.question.type());
} else if (o instanceof QueryWrittenEvent) {
QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll();
} else {
fail("unexpected event type: " + o);
}
assertTrue(observer.events.isEmpty());
}
}
private static final class TestRecursiveCacheDnsQueryLifecycleObserverFactory
implements DnsQueryLifecycleObserverFactory {
final Queue<TestDnsQueryLifecycleObserver> observers =
new ConcurrentLinkedQueue<TestDnsQueryLifecycleObserver>();
@Override
public DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) {
TestDnsQueryLifecycleObserver observer = new TestDnsQueryLifecycleObserver(question);
observers.add(observer);
return observer;
}
}
private static final class QueryWrittenEvent {
final InetSocketAddress dnsServerAddress;
QueryWrittenEvent(InetSocketAddress dnsServerAddress) {
this.dnsServerAddress = dnsServerAddress;
}
}
private static final class QueryCancelledEvent {
final int queriesRemaining;
QueryCancelledEvent(int queriesRemaining) {
this.queriesRemaining = queriesRemaining;
}
}
private static final class QueryRedirectedEvent {
final List<InetSocketAddress> nameServers;
QueryRedirectedEvent(List<InetSocketAddress> nameServers) {
this.nameServers = nameServers;
}
}
private static final class QueryCnamedEvent {
final DnsQuestion question;
QueryCnamedEvent(DnsQuestion question) {
this.question = question;
}
}
private static final class QueryNoAnswerEvent {
final DnsResponseCode code;
QueryNoAnswerEvent(DnsResponseCode code) {
this.code = code;
}
}
private static final class QueryFailedEvent {
final Throwable cause;
QueryFailedEvent(Throwable cause) {
this.cause = cause;
}
}
private static final class QuerySucceededEvent {
}
private static final class TestDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver {
final Queue<Object> events = new ArrayDeque<Object>();
final DnsQuestion question;
TestDnsQueryLifecycleObserver(DnsQuestion question) {
this.question = question;
}
@Override
public void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) {
events.add(new QueryWrittenEvent(dnsServerAddress));
}
@Override
public void queryCancelled(int queriesRemaining) {
events.add(new QueryCancelledEvent(queriesRemaining));
}
@Override
public DnsQueryLifecycleObserver queryRedirected(List<InetSocketAddress> nameServers) {
events.add(new QueryRedirectedEvent(nameServers));
return this;
}
@Override
public DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion) {
events.add(new QueryCnamedEvent(cnameQuestion));
return this;
}
@Override
public DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code) {
events.add(new QueryNoAnswerEvent(code));
return this;
}
@Override
public void queryFailed(Throwable cause) {
events.add(new QueryFailedEvent(cause));
}
@Override
public void querySucceed() {
events.add(new QuerySucceededEvent());
}
}
private static final class TestDnsCache implements DnsCache {
private final DnsCache cache;
final Map<String, List<DnsCacheEntry>> cacheHits = new HashMap<String, List<DnsCacheEntry>>();