Repository: netty/netty
Issue: Expose DnsNameResolverContext (or something equivalent to it) to users
Labels: feature

A user may want to reuse the logic related with search list enumeration, retries and error handling in his or her resolver implementations. For example, a user could extend or customize `DnsNameResolverContext` to implement DNS-based service discovery that retrieves [SRV records](https://en.wikipedia.org/wiki/SRV_record), while respecting the global resolver configuration (`/etc/resolv.conf`). Currently, the user would have to re-implement search list enumeration, retries and error handling by his or her own.

Let me take a stab of this if it sounds good.

Issue comments:
and does it sound good? :-)
I'll start to work on this if there's no objection.

Pull request review comments:
javadocs please
revert ?
...that are...
...that are...
...that are...
Added Javadocs for `DnsAddressDecoder`
Oops! Reverted.
Fixed all :-)
can we use `NetUtil.IPV6_BYTE_COUNT` here instead of duplicating the definition?
`gotResult` -> `containsExpectedResultType`?
consider adding a comment to provide justification for `86400`.
should this method live next to `protected void doResolveAll(String inetHost,...)` and do effectively the same thing? Differences:

- There is some local host and IP checking up front before losing in local hosts file.
- the cache is consulted first before doing a `new DnsAddressResolveContext(..).resolve(..)`.
- `doResolveAll` is overridable to substitute another cache

to pull off the cache items we may need to modify the cache API to lookup `DnsQuestion` instead of just raw hostname. Is there a technical reason why enhancing the cache would be problematic?
nit: remove `throws Exception`
should this be `!finalResult.isEmpty()`?
consider adding a comment that if/when we cache then we would need to `ReferenceCountUtil.retain` the `DnsRecord` (and maybe the original `DnsQuestion`).
`thing interesting` -> `expected/desired answers`?
dropping the redirect may have undesired results if other behavior in this class changes, or DNS server is not as expected so consider adding a comment here. IIUC we are assuming the following behavior:

- this DNS resolver will issue a query for each `A` and `AAAA` records if they are both supported.
- it is assumed the DNS server will only provide redirects of the same type as the query, and therefore it is safe to drop redirects where types don't match. In addition if the DNS server responds with mismatching (or maybe all types) types it is desirable to filter the redirects to avoid multiple queries for the same redirect.
is it possible to instead use the test DNS server that runs locally and verify results are as expected?
They are private fields and highly unlikely to change, so I'd like to keep these as they are.
Renamed to `containsExpectedResult`
Added
Initial cache API was designed to cache the complete DNS response, but it caused various issues with reference counting because it kept `DnsRawRecord`. We may be able to make this change once we implement all `DnsRecord` implementations for all known DNS record types. For now, we can't use cache for arbitrary record types.
Fixed
It never gives an empty list by contract. Let me update the Javadoc of this method.
Added comment
Fixed
I actually checked if the result contains a valid MX record, just like `queryMx()` does. The log messages are for debugging. `queryMx()` logs the same. And the test case uses the test DNS server rather than a public DNS server.
Actually, it was a bug. This change has to be reverted.
doh ... missed that this is going against the local dns server. validating the mx record contents to ensure decoding is correct may be challenging due to the random nature of DnsTestServer I guess.
I still think there is value in consolidating the definitions, but not a deal breaker.

Patch that resolved the issue:
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 00000000000..3dfb56ae8cc
--- /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 00000000000..95dcb136ed6
--- /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<InetAddress> {
+
+    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<InetAddress> 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<InetAddress> finalResult) {
+        final int size = finalResult.size();
+        final Class<? extends InetAddress> 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 eaa714026d2..5bd7baad259 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.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.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 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 @@ protected void doResolve(String inetHost, Promise<InetAddress> promise) throws E
         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<List<DnsRecord>> resolveAll(DnsQuestion question) {
+        return resolveAll(question, EMPTY_ADDITIONALS, executor().<List<DnsRecord>>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<List<DnsRecord>> resolveAll(DnsQuestion question, Iterable<DnsRecord> additionals) {
+        return resolveAll(question, additionals, executor().<List<DnsRecord>>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<List<DnsRecord>> resolveAll(DnsQuestion question, Iterable<DnsRecord> additionals,
+                                                    Promise<List<DnsRecord>> promise) {
+        final DnsRecord[] additionalsArray = toArray(additionals, true);
+        return resolveAll(question, additionalsArray, promise);
+    }
+
+    private Future<List<DnsRecord>> resolveAll(DnsQuestion question, DnsRecord[] additionals,
+                                               Promise<List<DnsRecord>> 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.<DnsRecord>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<DnsRecord> additionals, boolean validateType) {
         checkNotNull(additionals, "additionals");
         if (additionals instanceof Collection) {
@@ -624,7 +721,7 @@ private boolean doResolveCached(String hostname,
         }
     }
 
-    private static <T> void trySuccess(Promise<T> promise, T result) {
+    static <T> void trySuccess(Promise<T> promise, T result) {
         if (!promise.trySuccess(result)) {
             logger.warn("Failed to notify success ({}) to a promise: {}", result, promise);
         }
@@ -638,40 +735,20 @@ private static void tryFailure(Promise<?> promise, Throwable cause) {
 
     private void doResolveUncached(String hostname,
                                    DnsRecord[] additionals,
-                                   Promise<InetAddress> promise,
+                                   final Promise<InetAddress> promise,
                                    DnsCache resolveCache) {
-        new SingleResolverContext(this, hostname, additionals, resolveCache,
-                                  dnsServerAddressStreamProvider.nameServerAddressStream(hostname)).resolve(promise);
-    }
-
-    static final class SingleResolverContext extends DnsNameResolverContext<InetAddress> {
-        SingleResolverContext(DnsNameResolver parent, String hostname,
-                              DnsRecord[] additionals, DnsCache resolveCache, DnsServerAddressStream nameServerAddrs) {
-            super(parent, hostname, additionals, resolveCache, nameServerAddrs);
-        }
-
-        @Override
-        DnsNameResolverContext<InetAddress> newResolverContext(DnsNameResolver parent, String hostname,
-                                                               DnsRecord[] additionals, DnsCache resolveCache,
-                                                               DnsServerAddressStream nameServerAddrs) {
-            return new SingleResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs);
-        }
-
-        @Override
-        boolean finishResolve(
-            Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries,
-            Promise<InetAddress> 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<List<InetAddress>> allPromise = executor().newPromise();
+        doResolveAllUncached(hostname, additionals, allPromise, resolveCache);
+        allPromise.addListener(new FutureListener<List<InetAddress>>() {
+            @Override
+            public void operationComplete(Future<List<InetAddress>> future) {
+                if (future.isSuccess()) {
+                    trySuccess(promise, future.getNow().get(0));
+                } else {
+                    tryFailure(promise, future.cause());
                 }
             }
-            return false;
-        }
+        });
     }
 
     @Override
@@ -747,50 +824,13 @@ private boolean doResolveAllCached(String hostname,
         }
     }
 
-    static final class ListResolverContext extends DnsNameResolverContext<List<InetAddress>> {
-        ListResolverContext(DnsNameResolver parent, String hostname,
-                            DnsRecord[] additionals, DnsCache resolveCache, DnsServerAddressStream nameServerAddrs) {
-            super(parent, hostname, additionals, resolveCache, nameServerAddrs);
-        }
-
-        @Override
-        DnsNameResolverContext<List<InetAddress>> newResolverContext(
-                DnsNameResolver parent, String hostname,  DnsRecord[] additionals, DnsCache resolveCache,
-                DnsServerAddressStream nameServerAddrs) {
-            return new ListResolverContext(parent, hostname, additionals, resolveCache, nameServerAddrs);
-        }
-
-        @Override
-        boolean finishResolve(
-            Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries,
-            Promise<List<InetAddress>> promise) {
-
-            List<InetAddress> 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<InetAddress>(numEntries);
-                    }
-                    result.add(a);
-                }
-            }
-
-            if (result != null) {
-                promise.trySuccess(result);
-                return true;
-            }
-            return false;
-        }
-    }
-
     private void doResolveAllUncached(String hostname,
                                       DnsRecord[] additionals,
                                       Promise<List<InetAddress>> 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 00000000000..34b548ba623
--- /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<DnsRecord> {
+
+    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<DnsRecord> 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<DnsRecord> 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 2eea34536ee..8d7c53a8243 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.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.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.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<T> {
-
-    private static final int INADDRSZ4 = 4;
-    private static final int INADDRSZ6 = 16;
+abstract class DnsResolveContext<T> {
 
     private static final FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>> RELEASE_RESPONSE =
             new FutureListener<AddressedEnvelope<DnsResponse, InetSocketAddress>>() {
@@ -73,58 +71,90 @@ public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAd
             };
     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<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> queriesInProgress =
             Collections.newSetFromMap(
                     new IdentityHashMap<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>, Boolean>());
 
-    private List<DnsCacheEntry> resolvedEntries;
+    private List<T> 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<T> promise) {
+    /**
+     * Creates a new context with the given parameters.
+     */
+    abstract DnsResolveContext<T> 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<T> 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<List<T>> promise) {
         final String[] searchDomains = parent.searchDomains();
         if (searchDomains.length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) {
             internalResolve(promise);
@@ -133,10 +163,10 @@ void resolve(final Promise<T> promise) {
             final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0];
             final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1;
 
-            doSearchDomainQuery(initialHostname, new FutureListener<T>() {
+            doSearchDomainQuery(initialHostname, new FutureListener<List<T>>() {
                 private int searchDomainIdx = initialSearchDomainIdx;
                 @Override
-                public void operationComplete(Future<T> future) throws Exception {
+                public void operationComplete(Future<List<T>> future) throws Exception {
                     Throwable cause = future.cause();
                     if (cause == null) {
                         promise.trySuccess(future.getNow());
@@ -182,26 +212,24 @@ public Throwable fillInStackTrace() {
         }
     }
 
-    private void doSearchDomainQuery(String hostname, FutureListener<T> listener) {
-        DnsNameResolverContext<T> nextContext = newResolverContext(parent, hostname, additionals, resolveCache,
-                nameServerAddrs);
-        Promise<T> nextPromise = parent.executor().newPromise();
+    private void doSearchDomainQuery(String hostname, FutureListener<List<T>> listener) {
+        DnsResolveContext<T> nextContext = newResolverContext(parent, hostname, dnsClass, expectedTypes,
+                                                              additionals, nameServerAddrs);
+        Promise<List<T>> nextPromise = parent.executor().newPromise();
         nextContext.internalResolve(nextPromise);
         nextPromise.addListener(listener);
     }
 
-    private void internalResolve(Promise<T> promise) {
+    private void internalResolve(Promise<List<T>> 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 @@ public void remove() {
 
     private void query(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex,
                        final DnsQuestion question,
-                       final Promise<T> promise, Throwable cause) {
+                       final Promise<List<T>> promise, Throwable cause) {
         query(nameServerAddrStream, nameServerAddrStreamIndex, question,
                 parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question), promise, cause);
     }
@@ -300,7 +328,7 @@ private void query(final DnsServerAddressStream nameServerAddrStream,
                        final int nameServerAddrStreamIndex,
                        final DnsQuestion question,
                        final DnsQueryLifecycleObserver queryLifecycleObserver,
-                       final Promise<T> promise,
+                       final Promise<List<T>> promise,
                        final Throwable cause) {
         if (nameServerAddrStreamIndex >= nameServerAddrStream.size() || allowedQueries == 0 || promise.isCancelled()) {
             tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver,
@@ -359,7 +387,7 @@ public void operationComplete(Future<AddressedEnvelope<DnsResponse, InetSocketAd
     void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex,
                     final DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
                     final DnsQueryLifecycleObserver queryLifecycleObserver,
-                    Promise<T> promise) {
+                    Promise<List<T>> promise) {
         try {
             final DnsResponse res = envelope.content();
             final DnsResponseCode code = res.code();
@@ -370,13 +398,19 @@ void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nam
                 }
                 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 @@ void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nam
      */
     private boolean handleRedirect(
             DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
-            final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<T> promise) {
+            final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<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.
@@ -417,15 +451,14 @@ private boolean handleRedirect(
                     }
 
                     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 @@ private static AuthoritativeNameServerList extractAuthoritativeNameServers(Strin
         return serverNames;
     }
 
-    private void onResponseAorAAAA(
-            DnsRecordType qType, DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
-            final DnsQueryLifecycleObserver queryLifecycleObserver,
-            Promise<T> promise) {
+    private void onExpectedResponse(
+            DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
+            final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> 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 @@ private void onResponseAorAAAA(
         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 @@ private void onResponseAorAAAA(
                 }
             }
 
-            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<DnsCacheEntry>(8);
+            if (finalResult == null) {
+                finalResult = new ArrayList<T>(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 @@ private void onResponseAorAAAA(
         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<DnsResponse, InetSocketAddress> envelope,
-                                 final DnsQueryLifecycleObserver queryLifecycleObserver,
-                                 Promise<T> promise) {
-        onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), queryLifecycleObserver, promise);
+                                 final DnsQueryLifecycleObserver queryLifecycleObserver, Promise<List<T>> promise) {
+        onResponseCNAME(question, buildAliasMap(envelope.content()), queryLifecycleObserver, promise);
     }
 
     private void onResponseCNAME(
-            DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> response,
-            Map<String, String> cnames, final DnsQueryLifecycleObserver queryLifecycleObserver,
-            Promise<T> promise) {
+            DnsQuestion question, Map<String, String> cnames,
+            final DnsQueryLifecycleObserver queryLifecycleObserver,
+            Promise<List<T>> 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 @@ void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
                             final int nameServerAddrStreamIndex,
                             final DnsQuestion question,
                             final DnsQueryLifecycleObserver queryLifecycleObserver,
-                            final Promise<T> promise,
+                            final Promise<List<T>> 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 @@ void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
 
             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 @@ void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream,
             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<? extends InetAddress> 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<T> promise, Throwable cause) {
+    private void finishResolve(Promise<List<T>> promise, Throwable cause) {
         if (!queriesInProgress.isEmpty()) {
             // If there are queries in progress, we should cancel it because we already finished the resolution.
             for (Iterator<Future<AddressedEnvelope<DnsResponse, InetSocketAddress>>> i = queriesInProgress.iterator();
@@ -700,13 +700,10 @@ private void finishResolve(Promise<T> promise, Throwable cause) {
             }
         }
 
-        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 @@ private void finishResolve(Promise<T> promise, Throwable cause) {
         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<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries,
-                                   Promise<T> promise);
-
-    abstract DnsNameResolverContext<T> newResolverContext(DnsNameResolver parent, String hostname,
-                                                          DnsRecord[] additionals, DnsCache resolveCache,
-                                                          DnsServerAddressStream nameServerAddrs);
-
     static String decodeDomainName(ByteBuf in) {
         in.markReaderIndex();
         try {
@@ -760,8 +750,8 @@ private DnsServerAddressStream getNameServers(String hostname) {
         return stream == null ? nameServerAddrs.duplicate() : stream;
     }
 
-    private void followCname(
-            DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver, Promise<T> promise) {
+    private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver,
+                             Promise<List<T>> promise) {
         DnsServerAddressStream stream = getNameServers(cname);
 
         final DnsQuestion cnameQuestion;
@@ -776,7 +766,7 @@ private void followCname(
     }
 
     private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream,
-                          Promise<T> promise, Throwable cause) {
+                          Promise<List<T>> promise, Throwable cause) {
         final DnsQuestion question = newQuestion(hostname, type);
         if (question == null) {
             return false;
@@ -785,9 +775,9 @@ private boolean query(String hostname, DnsRecordType type, DnsServerAddressStrea
         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;


Test patch:
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 805121c7304..c0fdf83b945 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.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.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.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 void testQueryMx() throws Exception {
                     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 @@ private static void testResolveAll0(ResolvedAddressTypes addressTypes, InetAddre
         }
     }
 
+    @Test
+    public void testResolveAllMx() {
+        final DnsNameResolver resolver = newResolver().build();
+        try {
+            assertThat(resolver.isRecursionDesired(), is(true));
+
+            final Map<String, Future<List<DnsRecord>>> futures = new LinkedHashMap<String, Future<List<DnsRecord>>>();
+            for (String name: DOMAINS) {
+                if (EXCLUSIONS_QUERY_MX.contains(name)) {
+                    continue;
+                }
+
+                futures.put(name, resolver.resolveAll(new DefaultDnsQuestion(name, DnsRecordType.MX)));
+            }
+
+            for (Entry<String, Future<List<DnsRecord>>> e: futures.entrySet()) {
+                String hostname = e.getKey();
+                Future<List<DnsRecord>> f = e.getValue().awaitUninterruptibly();
+
+                final List<DnsRecord> 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<DnsRecord> records = resolver.resolveAll(new DefaultDnsQuestion("foo.com.", A))
+                                                .syncUninterruptibly().getNow();
+        assertThat(records, Matchers.<DnsRecord>hasSize(1));
+        assertThat(records.get(0), Matchers.<DnsRecord>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);