From 79c8ec4d33506ed39b4d941a8bc218eb914f348d Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Thu, 30 Jun 2016 23:12:11 +0200 Subject: [PATCH] DnsNameResolver search domains support Motivation: The current DnsNameResolver does not support search domains resolution. Search domains resolution is supported out of the box by the java.net resolver, making the DnsNameResolver not able to be a drop in replacement for io.netty.resolver.DefaultNameResolver. Modifications: The DnsNameResolverContext resolution has been modified to resolve a list of search path first when it is configured so. The resolve method now uses the following algorithm: if (hostname is absolute (start with dot) || no search domains) { searchAsIs } else { if (numDots(name) >= ndots) { searchAsIs } if (searchAsIs wasn't performed or failed) { searchWithSearchDomainsSequenciallyUntilOneSucceeds } } The DnsNameResolverBuilder provides configuration for the search domains and the ndots value. The default search domains value is configured with the OS search domains using the same native configuration the java.net resolver uses. Result: The DnsNameResolver performs search domains resolution when they are present. --- .../io/netty/util/internal/StringUtil.java | 12 + .../netty/util/internal/StringUtilTest.java | 9 + .../netty/resolver/dns/DnsNameResolver.java | 156 ++++++--- .../resolver/dns/DnsNameResolverBuilder.java | 46 ++- .../resolver/dns/DnsNameResolverContext.java | 102 ++++-- .../resolver/dns/DnsNameResolverTest.java | 199 +----------- .../netty/resolver/dns/SearchDomainTest.java | 238 ++++++++++++++ .../io/netty/resolver/dns/TestDnsServer.java | 297 ++++++++++++++++++ 8 files changed, 782 insertions(+), 277 deletions(-) create mode 100644 resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java create mode 100644 resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java diff --git a/common/src/main/java/io/netty/util/internal/StringUtil.java b/common/src/main/java/io/netty/util/internal/StringUtil.java index 10461e348d..e40d08bce6 100644 --- a/common/src/main/java/io/netty/util/internal/StringUtil.java +++ b/common/src/main/java/io/netty/util/internal/StringUtil.java @@ -548,6 +548,18 @@ public final class StringUtil { return c == DOUBLE_QUOTE; } + /** + * Determine if the string {@code s} ends with the char {@code c}. + * + * @param s the string to test + * @param c the tested char + * @return true if {@code s} ends with the char {@code c} + */ + public static boolean endsWith(CharSequence s, char c) { + int len = s.length(); + return len > 0 && s.charAt(len - 1) == c; + } + private StringUtil() { // Unused. } diff --git a/common/src/test/java/io/netty/util/internal/StringUtilTest.java b/common/src/test/java/io/netty/util/internal/StringUtilTest.java index 1ee27e59ff..7075153e6f 100644 --- a/common/src/test/java/io/netty/util/internal/StringUtilTest.java +++ b/common/src/test/java/io/netty/util/internal/StringUtilTest.java @@ -440,4 +440,13 @@ public class StringUtilTest { } private static final class TestClass { } + + @Test + public void testEndsWith() { + assertFalse(StringUtil.endsWith("", 'u')); + assertTrue(StringUtil.endsWith("u", 'u')); + assertTrue(StringUtil.endsWith("-u", 'u')); + assertFalse(StringUtil.endsWith("-", 'u')); + assertFalse(StringUtil.endsWith("u-", 'u')); + } } 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 1fc557ac07..6f2e4192d7 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 @@ -42,11 +42,14 @@ import io.netty.util.ReferenceCountUtil; import io.netty.util.concurrent.FastThreadLocal; import io.netty.util.concurrent.Future; import io.netty.util.concurrent.Promise; +import io.netty.util.internal.EmptyArrays; import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; import io.netty.util.internal.UnstableApi; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; +import java.lang.reflect.Method; import java.net.IDN; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -68,6 +71,7 @@ public class DnsNameResolver extends InetNameResolver { private static final InetAddress LOCALHOST_ADDRESS; static final InternetProtocolFamily[] DEFAULT_RESOLVE_ADDRESS_TYPES = new InternetProtocolFamily[2]; + static final String[] DEFAULT_SEACH_DOMAINS; static { // Note that we did not use SystemPropertyUtil.getBoolean() here to emulate the behavior of JDK. @@ -84,6 +88,24 @@ public class DnsNameResolver extends InetNameResolver { } } + static { + String[] searchDomains; + try { + Class configClass = Class.forName("sun.net.dns.ResolverConfiguration"); + Method open = configClass.getMethod("open"); + Method nameservers = configClass.getMethod("searchlist"); + Object instance = open.invoke(null); + + @SuppressWarnings("unchecked") + List list = (List) nameservers.invoke(instance); + searchDomains = list.toArray(new String[list.size()]); + } catch (Exception ignore) { + // Failed to get the system name search domain list. + searchDomains = EmptyArrays.EMPTY_STRINGS; + } + DEFAULT_SEACH_DOMAINS = searchDomains; + } + private static final DatagramDnsResponseDecoder DECODER = new DatagramDnsResponseDecoder(); private static final DatagramDnsQueryEncoder ENCODER = new DatagramDnsQueryEncoder(); @@ -117,6 +139,8 @@ public class DnsNameResolver extends InetNameResolver { private final int maxPayloadSize; private final boolean optResourceEnabled; private final HostsFileEntriesResolver hostsFileEntriesResolver; + private final String[] searchDomains; + private final int ndots; /** * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. @@ -135,6 +159,8 @@ public class DnsNameResolver extends InetNameResolver { * @param maxPayloadSize the capacity of the datagram packet buffer * @param optResourceEnabled if automatic inclusion of a optional records is enabled * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to check for local aliases + * @param searchDomains the list of search domain + * @param ndots the ndots value */ public DnsNameResolver( EventLoop eventLoop, @@ -148,7 +174,9 @@ public class DnsNameResolver extends InetNameResolver { boolean traceEnabled, int maxPayloadSize, boolean optResourceEnabled, - HostsFileEntriesResolver hostsFileEntriesResolver) { + HostsFileEntriesResolver hostsFileEntriesResolver, + String[] searchDomains, + int ndots) { super(eventLoop); checkNotNull(channelFactory, "channelFactory"); @@ -162,6 +190,8 @@ public class DnsNameResolver extends InetNameResolver { this.optResourceEnabled = optResourceEnabled; this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver, "hostsFileEntriesResolver"); this.resolveCache = resolveCache; + this.searchDomains = checkNotNull(searchDomains, "searchDomains").clone(); + this.ndots = checkPositive(ndots, "ndots"); Bootstrap b = new Bootstrap(); b.group(executor()); @@ -215,6 +245,14 @@ public class DnsNameResolver extends InetNameResolver { return resolvedAddressTypes; } + final String[] searchDomains() { + return searchDomains; + } + + final int ndots() { + return ndots; + } + /** * Returns {@code true} if and only if this resolver sends a DNS query with the RD (recursion desired) flag set. * The default value is {@code true}. @@ -376,25 +414,37 @@ public class DnsNameResolver extends InetNameResolver { private void doResolveUncached(String hostname, Promise promise, DnsCache resolveCache) { - final DnsNameResolverContext ctx = - new DnsNameResolverContext(this, hostname, promise, resolveCache) { - @Override - protected boolean finishResolve( - Class addressType, List resolvedEntries) { + SingleResolverContext ctx = new SingleResolverContext(this, hostname, resolveCache); + ctx.resolve(promise); + } - final int numEntries = resolvedEntries.size(); - for (int i = 0; i < numEntries; i++) { - final InetAddress a = resolvedEntries.get(i).address(); - if (addressType.isInstance(a)) { - setSuccess(promise(), a); - return true; - } - } - return false; - } - }; + final class SingleResolverContext extends DnsNameResolverContext { - ctx.resolve(); + SingleResolverContext(DnsNameResolver parent, String hostname, DnsCache resolveCache) { + super(parent, hostname, resolveCache); + } + + @Override + DnsNameResolverContext newResolverContext(DnsNameResolver parent, + String hostname, DnsCache resolveCache) { + return new SingleResolverContext(parent, hostname, resolveCache); + } + + @Override + boolean finishResolve( + Class addressType, List resolvedEntries, + Promise promise) { + + final int numEntries = resolvedEntries.size(); + for (int i = 0; i < numEntries; i++) { + final InetAddress a = resolvedEntries.get(i).address(); + if (addressType.isInstance(a)) { + setSuccess(promise, a); + return true; + } + } + return false; + } } @Override @@ -472,40 +522,56 @@ public class DnsNameResolver extends InetNameResolver { return true; } - private void doResolveAllUncached(final String hostname, - final Promise> promise, - DnsCache resolveCache) { - final DnsNameResolverContext> ctx = - new DnsNameResolverContext>(this, hostname, promise, resolveCache) { - @Override - protected boolean finishResolve( - Class addressType, List resolvedEntries) { + final class ListResolverContext extends DnsNameResolverContext> { + ListResolverContext(DnsNameResolver parent, String hostname, DnsCache resolveCache) { + super(parent, hostname, resolveCache); + } - List result = null; - final int numEntries = resolvedEntries.size(); - for (int i = 0; i < numEntries; i++) { - final InetAddress a = resolvedEntries.get(i).address(); - if (addressType.isInstance(a)) { - if (result == null) { - result = new ArrayList(numEntries); - } - result.add(a); - } - } + @Override + DnsNameResolverContext> newResolverContext(DnsNameResolver parent, String hostname, + DnsCache resolveCache) { + return new ListResolverContext(parent, hostname, resolveCache); + } - if (result != null) { - promise().trySuccess(result); - return true; - } - return false; + @Override + boolean finishResolve( + Class addressType, List resolvedEntries, + Promise> promise) { + + List result = null; + final int numEntries = resolvedEntries.size(); + for (int i = 0; i < numEntries; i++) { + final InetAddress a = resolvedEntries.get(i).address(); + if (addressType.isInstance(a)) { + if (result == null) { + result = new ArrayList(numEntries); } - }; + result.add(a); + } + } - ctx.resolve(); + if (result != null) { + promise.trySuccess(result); + return true; + } + return false; + } + } + + private void doResolveAllUncached(String hostname, + Promise> promise, + DnsCache resolveCache) { + DnsNameResolverContext> ctx = new ListResolverContext(this, hostname, resolveCache); + ctx.resolve(promise); } private static String hostname(String inetHost) { - return IDN.toASCII(inetHost); + String hostname = IDN.toASCII(inetHost); + // Check for http://bugs.java.com/bugdatabase/view_bug.do?bug_id=6894622 + if (StringUtil.endsWith(inetHost, '.') && !StringUtil.endsWith(hostname, '.')) { + hostname += "."; + } + return hostname; } /** diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java index f6887e69df..ffba72547c 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java @@ -51,6 +51,8 @@ public final class DnsNameResolverBuilder { private int maxPayloadSize = 4096; private boolean optResourceEnabled = true; private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT; + private String[] searchDomains = DnsNameResolver.DEFAULT_SEACH_DOMAINS; + private int ndots = 1; /** * Creates a new builder. @@ -288,6 +290,46 @@ public final class DnsNameResolverBuilder { return this; } + /** + * Set the list of search domains of the resolver. + * + * @param searchDomains the search domains + * @return {@code this} + */ + public DnsNameResolverBuilder searchDomains(Iterable searchDomains) { + checkNotNull(searchDomains, "searchDomains"); + + final List list = + InternalThreadLocalMap.get().arrayList(4); + + for (String f : searchDomains) { + if (f == null) { + break; + } + + // Avoid duplicate entries. + if (list.contains(f)) { + continue; + } + + list.add(f); + } + + this.searchDomains = list.toArray(new String[list.size()]); + return this; + } + + /** + * Set the number of dots which must appear in a name before an initial absolute query is made. + * + * @param ndots the ndots value + * @return {@code this} + */ + public DnsNameResolverBuilder ndots(int ndots) { + this.ndots = ndots; + return this; + } + /** * Returns a new {@link DnsNameResolver} instance. * @@ -314,6 +356,8 @@ public final class DnsNameResolverBuilder { traceEnabled, maxPayloadSize, optResourceEnabled, - hostsFileEntriesResolver); + hostsFileEntriesResolver, + searchDomains, + ndots); } } diff --git a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java index cfeb057e36..fabb9da7e8 100644 --- a/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java +++ b/resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverContext.java @@ -68,7 +68,6 @@ abstract class DnsNameResolverContext { private final DnsNameResolver parent; private final DnsServerAddressStream nameServerAddrs; - private final Promise promise; private final String hostname; private final DnsCache resolveCache; private final boolean traceEnabled; @@ -86,10 +85,8 @@ abstract class DnsNameResolverContext { protected DnsNameResolverContext(DnsNameResolver parent, String hostname, - Promise promise, DnsCache resolveCache) { this.parent = parent; - this.promise = promise; this.hostname = hostname; this.resolveCache = resolveCache; @@ -100,11 +97,44 @@ abstract class DnsNameResolverContext { allowedQueries = maxAllowedQueries; } - protected Promise promise() { - return promise; + void resolve(Promise promise) { + boolean directSearch = parent.searchDomains().length == 0 || StringUtil.endsWith(hostname, '.'); + if (directSearch) { + internalResolve(promise); + } else { + final Promise original = promise; + promise = parent.executor().newPromise(); + promise.addListener(new FutureListener() { + int count; + @Override + public void operationComplete(Future future) throws Exception { + if (future.isSuccess()) { + original.trySuccess(future.getNow()); + } else if (count < parent.searchDomains().length) { + String searchDomain = parent.searchDomains()[count++]; + Promise nextPromise = parent.executor().newPromise(); + String nextHostname = DnsNameResolverContext.this.hostname + "." + searchDomain; + DnsNameResolverContext nextContext = newResolverContext(parent, + nextHostname, resolveCache); + nextContext.internalResolve(nextPromise); + nextPromise.addListener(this); + } else { + original.tryFailure(future.cause()); + } + } + }); + int dots = 0; + for (int idx = hostname.length() - 1; idx >= 0; idx--) { + if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) { + internalResolve(promise); + return; + } + } + promise.tryFailure(new UnknownHostException(hostname)); + } } - void resolve() { + private void internalResolve(Promise promise) { InetSocketAddress nameServerAddrToTry = nameServerAddrs.next(); for (InternetProtocolFamily f: resolveAddressTypes) { final DnsRecordType type; @@ -119,13 +149,13 @@ abstract class DnsNameResolverContext { throw new Error(); } - query(nameServerAddrToTry, new DefaultDnsQuestion(hostname, type)); + query(nameServerAddrToTry, new DefaultDnsQuestion(hostname, type), promise); } } - private void query(InetSocketAddress nameServerAddr, final DnsQuestion question) { + private void query(InetSocketAddress nameServerAddr, final DnsQuestion question, final Promise promise) { if (allowedQueries == 0 || promise.isCancelled()) { - tryToFinishResolve(); + tryToFinishResolve(promise); return; } @@ -145,31 +175,32 @@ abstract class DnsNameResolverContext { try { if (future.isSuccess()) { - onResponse(question, future.getNow()); + onResponse(question, future.getNow(), promise); } else { // Server did not respond or I/O error occurred; try again. if (traceEnabled) { addTrace(future.cause()); } - query(nameServerAddrs.next(), question); + query(nameServerAddrs.next(), question, promise); } } finally { - tryToFinishResolve(); + tryToFinishResolve(promise); } } }); } - void onResponse(final DnsQuestion question, AddressedEnvelope envelope) { + void onResponse(final DnsQuestion question, AddressedEnvelope envelope, + Promise promise) { try { final DnsResponse res = envelope.content(); final DnsResponseCode code = res.code(); if (code == DnsResponseCode.NOERROR) { final DnsRecordType type = question.type(); if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { - onResponseAorAAAA(type, question, envelope); + onResponseAorAAAA(type, question, envelope, promise); } else if (type == DnsRecordType.CNAME) { - onResponseCNAME(question, envelope); + onResponseCNAME(question, envelope, promise); } return; } @@ -182,7 +213,7 @@ abstract class DnsNameResolverContext { // Retry with the next server if the server did not tell us that the domain does not exist. if (code != DnsResponseCode.NXDOMAIN) { - query(nameServerAddrs.next(), question); + query(nameServerAddrs.next(), question, promise); } } finally { ReferenceCountUtil.safeRelease(envelope); @@ -190,7 +221,8 @@ abstract class DnsNameResolverContext { } private void onResponseAorAAAA( - DnsRecordType qType, DnsQuestion question, AddressedEnvelope envelope) { + DnsRecordType qType, DnsQuestion question, AddressedEnvelope envelope, + Promise promise) { // We often get a bunch of CNAMES as well when we asked for A/AAAA. final DnsResponse response = envelope.content(); @@ -267,17 +299,18 @@ abstract class DnsNameResolverContext { // We aked for A/AAAA but we got only CNAME. if (!cnames.isEmpty()) { - onResponseCNAME(question, envelope, cnames, false); + onResponseCNAME(question, envelope, cnames, false, promise); } } - private void onResponseCNAME(DnsQuestion question, AddressedEnvelope envelope) { - onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), true); + private void onResponseCNAME(DnsQuestion question, AddressedEnvelope envelope, + Promise promise) { + onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), true, promise); } private void onResponseCNAME( DnsQuestion question, AddressedEnvelope response, - Map cnames, boolean trace) { + Map cnames, boolean trace, Promise promise) { // Resolve the host name in the question into the real host name. final String name = question.name().toLowerCase(Locale.US); @@ -296,7 +329,7 @@ abstract class DnsNameResolverContext { } if (found) { - followCname(response.sender(), name, resolved); + followCname(response.sender(), name, resolved, promise); } else if (trace && traceEnabled) { addTrace(response.sender(), "no matching CNAME record found"); } @@ -332,12 +365,12 @@ abstract class DnsNameResolverContext { return cnames != null? cnames : Collections.emptyMap(); } - void tryToFinishResolve() { + void tryToFinishResolve(Promise promise) { if (!queriesInProgress.isEmpty()) { // 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. - finishResolve(); + finishResolve(promise); } // We did not get any resolved address of the preferred type, so we can't finish the resolution process. @@ -350,13 +383,13 @@ abstract class DnsNameResolverContext { if (!triedCNAME) { // As the last resort, try to query CNAME, just in case the name server has it. triedCNAME = true; - query(nameServerAddrs.next(), new DefaultDnsQuestion(hostname, DnsRecordType.CNAME)); + query(nameServerAddrs.next(), new DefaultDnsQuestion(hostname, DnsRecordType.CNAME), promise); return; } } // We have at least one resolved address or tried CNAME as the last resort.. - finishResolve(); + finishResolve(promise); } private boolean gotPreferredAddress() { @@ -385,7 +418,7 @@ abstract class DnsNameResolverContext { return false; } - private void finishResolve() { + private void finishResolve(Promise promise) { if (!queriesInProgress.isEmpty()) { // If there are queries in progress, we should cancel it because we already finished the resolution. for (Iterator>> i = queriesInProgress.iterator(); @@ -402,7 +435,7 @@ abstract class DnsNameResolverContext { if (resolvedEntries != null) { // Found at least one resolved address. for (InternetProtocolFamily f: resolveAddressTypes) { - if (finishResolve(f.addressType(), resolvedEntries)) { + if (finishResolve(f.addressType(), resolvedEntries, promise)) { return; } } @@ -435,8 +468,11 @@ abstract class DnsNameResolverContext { promise.tryFailure(cause); } - protected abstract boolean finishResolve( - Class addressType, List resolvedEntries); + abstract boolean finishResolve(Class addressType, List resolvedEntries, + Promise promise); + + abstract DnsNameResolverContext newResolverContext(DnsNameResolver parent, String hostname, + DnsCache resolveCache); static String decodeDomainName(ByteBuf in) { in.markReaderIndex(); @@ -450,7 +486,7 @@ abstract class DnsNameResolverContext { } } - private void followCname(InetSocketAddress nameServerAddr, String name, String cname) { + private void followCname(InetSocketAddress nameServerAddr, String name, String cname, Promise promise) { if (traceEnabled) { if (trace == null) { @@ -467,8 +503,8 @@ abstract class DnsNameResolverContext { } final InetSocketAddress nextAddr = nameServerAddrs.next(); - query(nextAddr, new DefaultDnsQuestion(cname, DnsRecordType.A)); - query(nextAddr, new DefaultDnsQuestion(cname, DnsRecordType.AAAA)); + query(nextAddr, new DefaultDnsQuestion(cname, DnsRecordType.A), promise); + query(nextAddr, new DefaultDnsQuestion(cname, DnsRecordType.AAAA), promise); } private void addTrace(InetSocketAddress nameServerAddr, String msg) { 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 6277553f5f..8f9dbf9ca0 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,41 +28,14 @@ import io.netty.handler.codec.dns.DnsRecordType; import io.netty.handler.codec.dns.DnsResponse; import io.netty.handler.codec.dns.DnsResponseCode; import io.netty.handler.codec.dns.DnsSection; -import io.netty.util.NetUtil; import io.netty.util.concurrent.Future; import io.netty.util.internal.StringUtil; -import io.netty.util.internal.ThreadLocalRandom; import io.netty.util.internal.logging.InternalLogger; import io.netty.util.internal.logging.InternalLoggerFactory; -import org.apache.directory.server.dns.DnsServer; -import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder; -import org.apache.directory.server.dns.io.encoder.ResourceRecordEncoder; -import org.apache.directory.server.dns.messages.DnsMessage; -import org.apache.directory.server.dns.messages.QuestionRecord; -import org.apache.directory.server.dns.messages.RecordClass; -import org.apache.directory.server.dns.messages.RecordType; -import org.apache.directory.server.dns.messages.ResourceRecord; -import org.apache.directory.server.dns.messages.ResourceRecordModifier; -import org.apache.directory.server.dns.protocol.DnsProtocolHandler; -import org.apache.directory.server.dns.protocol.DnsUdpDecoder; -import org.apache.directory.server.dns.protocol.DnsUdpEncoder; -import org.apache.directory.server.dns.store.DnsAttribute; -import org.apache.directory.server.dns.store.RecordStore; -import org.apache.directory.server.protocol.shared.transport.UdpTransport; -import org.apache.mina.core.buffer.IoBuffer; -import org.apache.mina.core.session.IoSession; -import org.apache.mina.filter.codec.ProtocolCodecFactory; -import org.apache.mina.filter.codec.ProtocolCodecFilter; -import org.apache.mina.filter.codec.ProtocolDecoder; -import org.apache.mina.filter.codec.ProtocolEncoder; -import org.apache.mina.filter.codec.ProtocolEncoderOutput; -import org.apache.mina.transport.socket.DatagramAcceptor; -import org.apache.mina.transport.socket.DatagramSessionConfig; import org.junit.AfterClass; import org.junit.BeforeClass; import org.junit.Test; -import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; @@ -262,7 +235,7 @@ public class DnsNameResolverTest { StringUtil.EMPTY_STRING); } - private static final TestDnsServer dnsServer = new TestDnsServer(); + private static final TestDnsServer dnsServer = new TestDnsServer(DOMAINS); private static final EventLoopGroup group = new NioEventLoopGroup(1); private static DnsNameResolverBuilder newResolver() { @@ -532,174 +505,4 @@ public class DnsNameResolverTest { futures.put(hostname, resolver.query(new DefaultDnsQuestion(hostname, DnsRecordType.MX))); } - private static final class TestDnsServer extends DnsServer { - private static final Map BYTES = new HashMap(); - private static final String[] IPV6_ADDRESSES; - static { - BYTES.put("::1", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}); - BYTES.put("0:0:0:0:0:0:1:1", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1}); - BYTES.put("0:0:0:0:0:1:1:1", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1}); - BYTES.put("0:0:0:0:1:1:1:1", new byte[] {0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1}); - BYTES.put("0:0:0:1:1:1:1:1", new byte[] {0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); - BYTES.put("0:0:1:1:1:1:1:1", new byte[] {0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); - BYTES.put("0:1:1:1:1:1:1:1", new byte[] {0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); - BYTES.put("1:1:1:1:1:1:1:1", new byte[] {0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); - - IPV6_ADDRESSES = BYTES.keySet().toArray(new String[BYTES.size()]); - } - - @Override - public void start() throws IOException { - InetSocketAddress address = new InetSocketAddress(NetUtil.LOCALHOST4, 0); - UdpTransport transport = new UdpTransport(address.getHostName(), address.getPort()); - setTransports(transport); - - DatagramAcceptor acceptor = transport.getAcceptor(); - - acceptor.setHandler(new DnsProtocolHandler(this, new TestRecordStore()) { - @Override - public void sessionCreated(IoSession session) throws Exception { - // USe our own codec to support AAAA testing - session.getFilterChain() - .addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory())); - } - }); - - ((DatagramSessionConfig) acceptor.getSessionConfig()).setReuseAddress(true); - - // Start the listener - acceptor.bind(); - } - - public InetSocketAddress localAddress() { - return (InetSocketAddress) getTransports()[0].getAcceptor().getLocalAddress(); - } - - /** - * {@link ProtocolCodecFactory} which allows to test AAAA resolution. - */ - private static final class TestDnsProtocolUdpCodecFactory implements ProtocolCodecFactory { - private final DnsMessageEncoder encoder = new DnsMessageEncoder(); - private final TestAAAARecordEncoder recordEncoder = new TestAAAARecordEncoder(); - - @Override - public ProtocolEncoder getEncoder(IoSession session) throws Exception { - return new DnsUdpEncoder() { - - @Override - public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { - IoBuffer buf = IoBuffer.allocate(1024); - DnsMessage dnsMessage = (DnsMessage) message; - encoder.encode(buf, dnsMessage); - for (ResourceRecord record: dnsMessage.getAnswerRecords()) { - // This is a hack to allow to also test for AAAA resolution as DnsMessageEncoder - // does not support it and it is hard to extend, because the interesting methods - // are private... - // In case of RecordType.AAAA we need to encode the RecordType by ourselves. - if (record.getRecordType() == RecordType.AAAA) { - try { - recordEncoder.put(buf, record); - } catch (IOException e) { - // Should never happen - throw new IllegalStateException(e); - } - } - } - buf.flip(); - - out.write(buf); - } - }; - } - - @Override - public ProtocolDecoder getDecoder(IoSession session) throws Exception { - return new DnsUdpDecoder(); - } - - private static final class TestAAAARecordEncoder extends ResourceRecordEncoder { - - @Override - protected void putResourceRecordData(IoBuffer ioBuffer, ResourceRecord resourceRecord) { - byte[] bytes = BYTES.get(resourceRecord.get(DnsAttribute.IP_ADDRESS)); - if (bytes == null) { - throw new IllegalStateException(); - } - // encode the ::1 - ioBuffer.put(bytes); - } - } - } - - private static final class TestRecordStore implements RecordStore { - private static final int[] NUMBERS = new int[254]; - private static final char[] CHARS = new char[26]; - - static { - for (int i = 0; i < NUMBERS.length; i++) { - NUMBERS[i] = i + 1; - } - - for (int i = 0; i < CHARS.length; i++) { - CHARS[i] = (char) ('a' + i); - } - } - - private static int index(int arrayLength) { - return Math.abs(ThreadLocalRandom.current().nextInt()) % arrayLength; - } - - private static String nextDomain() { - return CHARS[index(CHARS.length)] + ".netty.io"; - } - - private static String nextIp() { - return ipPart() + "." + ipPart() + '.' + ipPart() + '.' + ipPart(); - } - - private static int ipPart() { - return NUMBERS[index(NUMBERS.length)]; - } - - private static String nextIp6() { - return IPV6_ADDRESSES[index(IPV6_ADDRESSES.length)]; - } - - @Override - public Set getRecords(QuestionRecord questionRecord) { - String name = questionRecord.getDomainName(); - if (DOMAINS.contains(name)) { - ResourceRecordModifier rm = new ResourceRecordModifier(); - rm.setDnsClass(RecordClass.IN); - rm.setDnsName(name); - rm.setDnsTtl(100); - rm.setDnsType(questionRecord.getRecordType()); - - switch (questionRecord.getRecordType()) { - case A: - do { - rm.put(DnsAttribute.IP_ADDRESS, nextIp()); - } while (ThreadLocalRandom.current().nextBoolean()); - break; - case AAAA: - do { - rm.put(DnsAttribute.IP_ADDRESS, nextIp6()); - } while (ThreadLocalRandom.current().nextBoolean()); - break; - case MX: - int priority = 0; - do { - rm.put(DnsAttribute.DOMAIN_NAME, nextDomain()); - rm.put(DnsAttribute.MX_PREFERENCE, String.valueOf(++priority)); - } while (ThreadLocalRandom.current().nextBoolean()); - break; - default: - return null; - } - return Collections.singleton(rm.getEntry()); - } - return null; - } - } - } } diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java b/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java new file mode 100644 index 0000000000..c3e677b4c9 --- /dev/null +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java @@ -0,0 +1,238 @@ +/* + * Copyright 2016 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.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.util.concurrent.Future; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import java.net.InetAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.TimeUnit; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +public class SearchDomainTest { + + private DnsNameResolverBuilder newResolver() { + return new DnsNameResolverBuilder(group.next()) + .channelType(NioDatagramChannel.class) + .nameServerAddresses(DnsServerAddresses.singleton(dnsServer.localAddress())) + .maxQueriesPerResolve(1) + .optResourceEnabled(false); + } + + private TestDnsServer dnsServer; + private EventLoopGroup group; + + @Before + public void before() { + group = new NioEventLoopGroup(1); + } + + @After + public void destroy() { + if (dnsServer != null) { + dnsServer.stop(); + dnsServer = null; + } + group.shutdownGracefully(); + } + + @Test + public void testResolve() throws Exception { + Set domains = new HashSet(); + domains.add("host1.foo.com"); + domains.add("host1"); + domains.add("host3"); + domains.add("host4.sub.foo.com"); + domains.add("host5.sub.foo.com"); + domains.add("host5.sub"); + + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(domains); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + DnsNameResolver resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).build(); + + String a = "host1.foo.com"; + String resolved = assertResolve(resolver, a); + assertEquals(store.getAddress("host1.foo.com"), resolved); + + // host1 resolves host1.foo.com with foo.com search domain + resolved = assertResolve(resolver, "host1"); + assertEquals(store.getAddress("host1.foo.com"), resolved); + + // "host1." absolute query + resolved = assertResolve(resolver, "host1."); + assertEquals(store.getAddress("host1"), resolved); + + // "host2" not resolved + assertNotResolve(resolver, "host2"); + + // "host3" does not contain a dot or is not absolute + assertNotResolve(resolver, "host3"); + + // "host3." does not contain a dot but is absolute + resolved = assertResolve(resolver, "host3."); + assertEquals(store.getAddress("host3"), resolved); + + // "host4.sub" contains a dot but not resolved then resolved to "host4.sub.foo.com" with "foo.com" search domain + resolved = assertResolve(resolver, "host4.sub"); + assertEquals(store.getAddress("host4.sub.foo.com"), resolved); + + // "host5.sub" contains a dot and is resolved + resolved = assertResolve(resolver, "host5.sub"); + assertEquals(store.getAddress("host5.sub"), resolved); + } + + @Test + public void testResolveAll() throws Exception { + Set domains = new HashSet(); + domains.add("host1.foo.com"); + domains.add("host1"); + domains.add("host3"); + domains.add("host4.sub.foo.com"); + domains.add("host5.sub.foo.com"); + domains.add("host5.sub"); + + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(domains, 2); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + DnsNameResolver resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).build(); + + String a = "host1.foo.com"; + List resolved = assertResolveAll(resolver, a); + assertEquals(store.getAddresses("host1.foo.com"), resolved); + + // host1 resolves host1.foo.com with foo.com search domain + resolved = assertResolveAll(resolver, "host1"); + assertEquals(store.getAddresses("host1.foo.com"), resolved); + + // "host1." absolute query + resolved = assertResolveAll(resolver, "host1."); + assertEquals(store.getAddresses("host1"), resolved); + + // "host2" not resolved + assertNotResolveAll(resolver, "host2"); + + // "host3" does not contain a dot or is not absolute + assertNotResolveAll(resolver, "host3"); + + // "host3." does not contain a dot but is absolute + resolved = assertResolveAll(resolver, "host3."); + assertEquals(store.getAddresses("host3"), resolved); + + // "host4.sub" contains a dot but not resolved then resolved to "host4.sub.foo.com" with "foo.com" search domain + resolved = assertResolveAll(resolver, "host4.sub"); + assertEquals(store.getAddresses("host4.sub.foo.com"), resolved); + + // "host5.sub" contains a dot and is resolved + resolved = assertResolveAll(resolver, "host5.sub"); + assertEquals(store.getAddresses("host5.sub"), resolved); + } + + @Test + public void testMultipleSearchDomain() throws Exception { + Set domains = new HashSet(); + domains.add("host1.foo.com"); + domains.add("host2.bar.com"); + domains.add("host3.bar.com"); + domains.add("host3.foo.com"); + + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(domains); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + DnsNameResolver resolver = newResolver().searchDomains(Arrays.asList("foo.com", "bar.com")).build(); + + // "host1" resolves via the "foo.com" search path + String resolved = assertResolve(resolver, "host1"); + assertEquals(store.getAddress("host1.foo.com"), resolved); + + // "host2" resolves via the "bar.com" search path + resolved = assertResolve(resolver, "host2"); + assertEquals(store.getAddress("host2.bar.com"), resolved); + + // "host3" resolves via the the "foo.com" search path as it is the first one + resolved = assertResolve(resolver, "host3"); + assertEquals(store.getAddress("host3.foo.com"), resolved); + + // "host4" does not resolve + assertNotResolve(resolver, "host4"); + } + + @Test + public void testSearchDomainWithNdots2() throws Exception { + Set domains = new HashSet(); + domains.add("host1.sub.foo.com"); + domains.add("host2.sub.foo.com"); + domains.add("host2.sub"); + + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(domains); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + DnsNameResolver resolver = newResolver().searchDomains(Collections.singleton("foo.com")).ndots(2).build(); + + String resolved = assertResolve(resolver, "host1.sub"); + assertEquals(store.getAddress("host1.sub.foo.com"), resolved); + + // "host2.sub" is resolved with the foo.com search domain as ndots = 2 + resolved = assertResolve(resolver, "host2.sub"); + assertEquals(store.getAddress("host2.sub.foo.com"), resolved); + } + + private void assertNotResolve(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future fut = resolver.resolve(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + assertFalse(fut.isSuccess()); + } + + private void assertNotResolveAll(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future> fut = resolver.resolveAll(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + assertFalse(fut.isSuccess()); + } + + private String assertResolve(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future fut = resolver.resolve(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + return fut.getNow().getHostAddress(); + } + + private List assertResolveAll(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future> fut = resolver.resolveAll(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + List list = new ArrayList(); + for (InetAddress addr : fut.getNow()) { + list.add(addr.getHostAddress()); + } + return list; + } +} diff --git a/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java b/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java new file mode 100644 index 0000000000..ade7b729ed --- /dev/null +++ b/resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java @@ -0,0 +1,297 @@ +/* + * Copyright 2016 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.util.NetUtil; +import io.netty.util.internal.ThreadLocalRandom; +import org.apache.directory.server.dns.DnsException; +import org.apache.directory.server.dns.DnsServer; +import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder; +import org.apache.directory.server.dns.io.encoder.ResourceRecordEncoder; +import org.apache.directory.server.dns.messages.DnsMessage; +import org.apache.directory.server.dns.messages.QuestionRecord; +import org.apache.directory.server.dns.messages.RecordClass; +import org.apache.directory.server.dns.messages.RecordType; +import org.apache.directory.server.dns.messages.ResourceRecord; +import org.apache.directory.server.dns.messages.ResourceRecordImpl; +import org.apache.directory.server.dns.messages.ResourceRecordModifier; +import org.apache.directory.server.dns.protocol.DnsProtocolHandler; +import org.apache.directory.server.dns.protocol.DnsUdpDecoder; +import org.apache.directory.server.dns.protocol.DnsUdpEncoder; +import org.apache.directory.server.dns.store.DnsAttribute; +import org.apache.directory.server.dns.store.RecordStore; +import org.apache.directory.server.protocol.shared.transport.UdpTransport; +import org.apache.mina.core.buffer.IoBuffer; +import org.apache.mina.core.session.IoSession; +import org.apache.mina.filter.codec.ProtocolCodecFactory; +import org.apache.mina.filter.codec.ProtocolCodecFilter; +import org.apache.mina.filter.codec.ProtocolDecoder; +import org.apache.mina.filter.codec.ProtocolEncoder; +import org.apache.mina.filter.codec.ProtocolEncoderOutput; +import org.apache.mina.transport.socket.DatagramAcceptor; +import org.apache.mina.transport.socket.DatagramSessionConfig; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; + +final class TestDnsServer extends DnsServer { + private static final Map BYTES = new HashMap(); + private static final String[] IPV6_ADDRESSES; + + static { + BYTES.put("::1", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}); + BYTES.put("0:0:0:0:0:0:1:1", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1}); + BYTES.put("0:0:0:0:0:1:1:1", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1}); + BYTES.put("0:0:0:0:1:1:1:1", new byte[]{0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1}); + BYTES.put("0:0:0:1:1:1:1:1", new byte[]{0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); + BYTES.put("0:0:1:1:1:1:1:1", new byte[]{0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); + BYTES.put("0:1:1:1:1:1:1:1", new byte[]{0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); + BYTES.put("1:1:1:1:1:1:1:1", new byte[]{0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1, 0, 1}); + + IPV6_ADDRESSES = BYTES.keySet().toArray(new String[BYTES.size()]); + } + + private final RecordStore store; + + TestDnsServer(Set domains) { + this.store = new TestRecordStore(domains); + } + + TestDnsServer(RecordStore store) { + this.store = store; + } + + @Override + public void start() throws IOException { + InetSocketAddress address = new InetSocketAddress(NetUtil.LOCALHOST4, 50000); + UdpTransport transport = new UdpTransport(address.getHostName(), address.getPort()); + setTransports(transport); + + DatagramAcceptor acceptor = transport.getAcceptor(); + + acceptor.setHandler(new DnsProtocolHandler(this, store) { + @Override + public void sessionCreated(IoSession session) throws Exception { + // USe our own codec to support AAAA testing + session.getFilterChain() + .addFirst("codec", new ProtocolCodecFilter(new TestDnsProtocolUdpCodecFactory())); + } + }); + + ((DatagramSessionConfig) acceptor.getSessionConfig()).setReuseAddress(true); + + // Start the listener + acceptor.bind(); + } + + public InetSocketAddress localAddress() { + return (InetSocketAddress) getTransports()[0].getAcceptor().getLocalAddress(); + } + + /** + * {@link ProtocolCodecFactory} which allows to test AAAA resolution. + */ + private static final class TestDnsProtocolUdpCodecFactory implements ProtocolCodecFactory { + private final DnsMessageEncoder encoder = new DnsMessageEncoder(); + private final TestAAAARecordEncoder recordEncoder = new TestAAAARecordEncoder(); + + @Override + public ProtocolEncoder getEncoder(IoSession session) throws Exception { + return new DnsUdpEncoder() { + + @Override + public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { + IoBuffer buf = IoBuffer.allocate(1024); + DnsMessage dnsMessage = (DnsMessage) message; + encoder.encode(buf, dnsMessage); + for (ResourceRecord record : dnsMessage.getAnswerRecords()) { + // This is a hack to allow to also test for AAAA resolution as DnsMessageEncoder + // does not support it and it is hard to extend, because the interesting methods + // are private... + // In case of RecordType.AAAA we need to encode the RecordType by ourselves. + if (record.getRecordType() == RecordType.AAAA) { + try { + recordEncoder.put(buf, record); + } catch (IOException e) { + // Should never happen + throw new IllegalStateException(e); + } + } + } + buf.flip(); + + out.write(buf); + } + }; + } + + @Override + public ProtocolDecoder getDecoder(IoSession session) throws Exception { + return new DnsUdpDecoder(); + } + + private static final class TestAAAARecordEncoder extends ResourceRecordEncoder { + + @Override + protected void putResourceRecordData(IoBuffer ioBuffer, ResourceRecord resourceRecord) { + byte[] bytes = BYTES.get(resourceRecord.get(DnsAttribute.IP_ADDRESS)); + if (bytes == null) { + throw new IllegalStateException(); + } + // encode the ::1 + ioBuffer.put(bytes); + } + } + } + + public static final class MapRecordStoreA implements RecordStore { + + private final Map> domainMap; + + public MapRecordStoreA(Set domains, int length) { + domainMap = new HashMap>(domains.size()); + for (String domain : domains) { + List addresses = new ArrayList(length); + for (int i = 0; i < length; i++) { + addresses.add(TestRecordStore.nextIp()); + } + domainMap.put(domain, addresses); + } + } + + public MapRecordStoreA(Set domains) { + this(domains, 1); + } + + public String getAddress(String domain) { + return domainMap.get(domain).get(0); + } + + public List getAddresses(String domain) { + return domainMap.get(domain); + } + + @Override + public Set getRecords(QuestionRecord questionRecord) throws DnsException { + String name = questionRecord.getDomainName(); + List addresses = domainMap.get(name); + if (addresses != null && questionRecord.getRecordType() == RecordType.A) { + Set records = new LinkedHashSet(); + for (String address : addresses) { + HashMap attributes = new HashMap(); + attributes.put(DnsAttribute.IP_ADDRESS.toLowerCase(), address); + records.add(new ResourceRecordImpl(name, questionRecord.getRecordType(), + RecordClass.IN, 100, attributes) { + @Override + public int hashCode() { + return System.identityHashCode(this); + } + @Override + public boolean equals(Object o) { + return false; + } + }); + } + return records; + } + return null; + } + } + + private static final class TestRecordStore implements RecordStore { + private static final int[] NUMBERS = new int[254]; + private static final char[] CHARS = new char[26]; + + static { + for (int i = 0; i < NUMBERS.length; i++) { + NUMBERS[i] = i + 1; + } + + for (int i = 0; i < CHARS.length; i++) { + CHARS[i] = (char) ('a' + i); + } + } + + private static int index(int arrayLength) { + return Math.abs(ThreadLocalRandom.current().nextInt()) % arrayLength; + } + + private static String nextDomain() { + return CHARS[index(CHARS.length)] + ".netty.io"; + } + + private static String nextIp() { + return ipPart() + "." + ipPart() + '.' + ipPart() + '.' + ipPart(); + } + + private static int ipPart() { + return NUMBERS[index(NUMBERS.length)]; + } + + private static String nextIp6() { + return IPV6_ADDRESSES[index(IPV6_ADDRESSES.length)]; + } + + private final Set domains; + + public TestRecordStore(Set domains) { + this.domains = domains; + } + + @Override + public Set getRecords(QuestionRecord questionRecord) { + String name = questionRecord.getDomainName(); + if (domains.contains(name)) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(name); + rm.setDnsTtl(100); + rm.setDnsType(questionRecord.getRecordType()); + + switch (questionRecord.getRecordType()) { + case A: + do { + rm.put(DnsAttribute.IP_ADDRESS, nextIp()); + } while (ThreadLocalRandom.current().nextBoolean()); + break; + case AAAA: + do { + rm.put(DnsAttribute.IP_ADDRESS, nextIp6()); + } while (ThreadLocalRandom.current().nextBoolean()); + break; + case MX: + int priority = 0; + do { + rm.put(DnsAttribute.DOMAIN_NAME, nextDomain()); + rm.put(DnsAttribute.MX_PREFERENCE, String.valueOf(++priority)); + } while (ThreadLocalRandom.current().nextBoolean()); + break; + default: + return null; + } + return Collections.singleton(rm.getEntry()); + } + return null; + } + } +}