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.
This commit is contained in:
Julien Viet 2016-06-30 23:12:11 +02:00 committed by Norman Maurer
parent 061899f2a7
commit 79c8ec4d33
8 changed files with 782 additions and 277 deletions

View File

@ -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.
}

View File

@ -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'));
}
}

View File

@ -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<String> list = (List<String>) 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<InetAddress> promise,
DnsCache resolveCache) {
final DnsNameResolverContext<InetAddress> ctx =
new DnsNameResolverContext<InetAddress>(this, hostname, promise, resolveCache) {
@Override
protected boolean finishResolve(
Class<? extends InetAddress> addressType, List<DnsCacheEntry> 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<InetAddress> {
ctx.resolve();
SingleResolverContext(DnsNameResolver parent, String hostname, DnsCache resolveCache) {
super(parent, hostname, resolveCache);
}
@Override
DnsNameResolverContext<InetAddress> newResolverContext(DnsNameResolver parent,
String hostname, DnsCache resolveCache) {
return new SingleResolverContext(parent, hostname, resolveCache);
}
@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)) {
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<List<InetAddress>> promise,
DnsCache resolveCache) {
final DnsNameResolverContext<List<InetAddress>> ctx =
new DnsNameResolverContext<List<InetAddress>>(this, hostname, promise, resolveCache) {
@Override
protected boolean finishResolve(
Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries) {
final class ListResolverContext extends DnsNameResolverContext<List<InetAddress>> {
ListResolverContext(DnsNameResolver parent, String hostname, DnsCache resolveCache) {
super(parent, hostname, resolveCache);
}
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);
}
}
@Override
DnsNameResolverContext<List<InetAddress>> 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<? 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);
}
}
ctx.resolve();
if (result != null) {
promise.trySuccess(result);
return true;
}
return false;
}
}
private void doResolveAllUncached(String hostname,
Promise<List<InetAddress>> promise,
DnsCache resolveCache) {
DnsNameResolverContext<List<InetAddress>> 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;
}
/**

View File

@ -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<String> searchDomains) {
checkNotNull(searchDomains, "searchDomains");
final List<String> 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);
}
}

View File

@ -68,7 +68,6 @@ abstract class DnsNameResolverContext<T> {
private final DnsNameResolver parent;
private final DnsServerAddressStream nameServerAddrs;
private final Promise<T> promise;
private final String hostname;
private final DnsCache resolveCache;
private final boolean traceEnabled;
@ -86,10 +85,8 @@ abstract class DnsNameResolverContext<T> {
protected DnsNameResolverContext(DnsNameResolver parent,
String hostname,
Promise<T> promise,
DnsCache resolveCache) {
this.parent = parent;
this.promise = promise;
this.hostname = hostname;
this.resolveCache = resolveCache;
@ -100,11 +97,44 @@ abstract class DnsNameResolverContext<T> {
allowedQueries = maxAllowedQueries;
}
protected Promise<T> promise() {
return promise;
void resolve(Promise<T> promise) {
boolean directSearch = parent.searchDomains().length == 0 || StringUtil.endsWith(hostname, '.');
if (directSearch) {
internalResolve(promise);
} else {
final Promise<T> original = promise;
promise = parent.executor().newPromise();
promise.addListener(new FutureListener<T>() {
int count;
@Override
public void operationComplete(Future<T> future) throws Exception {
if (future.isSuccess()) {
original.trySuccess(future.getNow());
} else if (count < parent.searchDomains().length) {
String searchDomain = parent.searchDomains()[count++];
Promise<T> nextPromise = parent.executor().newPromise();
String nextHostname = DnsNameResolverContext.this.hostname + "." + searchDomain;
DnsNameResolverContext<T> 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<T> promise) {
InetSocketAddress nameServerAddrToTry = nameServerAddrs.next();
for (InternetProtocolFamily f: resolveAddressTypes) {
final DnsRecordType type;
@ -119,13 +149,13 @@ abstract class DnsNameResolverContext<T> {
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<T> promise) {
if (allowedQueries == 0 || promise.isCancelled()) {
tryToFinishResolve();
tryToFinishResolve(promise);
return;
}
@ -145,31 +175,32 @@ abstract class DnsNameResolverContext<T> {
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<DnsResponse, InetSocketAddress> envelope) {
void onResponse(final DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
Promise<T> 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<T> {
// 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<T> {
}
private void onResponseAorAAAA(
DnsRecordType qType, DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope) {
DnsRecordType qType, DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
Promise<T> 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<T> {
// 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<DnsResponse, InetSocketAddress> envelope) {
onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), true);
private void onResponseCNAME(DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> envelope,
Promise<T> promise) {
onResponseCNAME(question, envelope, buildAliasMap(envelope.content()), true, promise);
}
private void onResponseCNAME(
DnsQuestion question, AddressedEnvelope<DnsResponse, InetSocketAddress> response,
Map<String, String> cnames, boolean trace) {
Map<String, String> cnames, boolean trace, Promise<T> 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<T> {
}
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<T> {
return cnames != null? cnames : Collections.<String, String>emptyMap();
}
void tryToFinishResolve() {
void tryToFinishResolve(Promise<T> 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<T> {
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<T> {
return false;
}
private void finishResolve() {
private void finishResolve(Promise<T> promise) {
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();
@ -402,7 +435,7 @@ abstract class DnsNameResolverContext<T> {
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<T> {
promise.tryFailure(cause);
}
protected abstract boolean finishResolve(
Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries);
abstract boolean finishResolve(Class<? extends InetAddress> addressType, List<DnsCacheEntry> resolvedEntries,
Promise<T> promise);
abstract DnsNameResolverContext<T> newResolverContext(DnsNameResolver parent, String hostname,
DnsCache resolveCache);
static String decodeDomainName(ByteBuf in) {
in.markReaderIndex();
@ -450,7 +486,7 @@ abstract class DnsNameResolverContext<T> {
}
}
private void followCname(InetSocketAddress nameServerAddr, String name, String cname) {
private void followCname(InetSocketAddress nameServerAddr, String name, String cname, Promise<T> promise) {
if (traceEnabled) {
if (trace == null) {
@ -467,8 +503,8 @@ abstract class DnsNameResolverContext<T> {
}
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) {

View File

@ -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<String, byte[]> BYTES = new HashMap<String, byte[]>();
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<ResourceRecord> 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;
}
}
}
}

View File

@ -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<String> domains = new HashSet<String>();
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<String> domains = new HashSet<String>();
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<String> 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<String> domains = new HashSet<String>();
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<String> domains = new HashSet<String>();
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<InetAddress> fut = resolver.resolve(inetHost);
assertTrue(fut.await(10, TimeUnit.SECONDS));
assertFalse(fut.isSuccess());
}
private void assertNotResolveAll(DnsNameResolver resolver, String inetHost) throws InterruptedException {
Future<List<InetAddress>> fut = resolver.resolveAll(inetHost);
assertTrue(fut.await(10, TimeUnit.SECONDS));
assertFalse(fut.isSuccess());
}
private String assertResolve(DnsNameResolver resolver, String inetHost) throws InterruptedException {
Future<InetAddress> fut = resolver.resolve(inetHost);
assertTrue(fut.await(10, TimeUnit.SECONDS));
return fut.getNow().getHostAddress();
}
private List<String> assertResolveAll(DnsNameResolver resolver, String inetHost) throws InterruptedException {
Future<List<InetAddress>> fut = resolver.resolveAll(inetHost);
assertTrue(fut.await(10, TimeUnit.SECONDS));
List<String> list = new ArrayList<String>();
for (InetAddress addr : fut.getNow()) {
list.add(addr.getHostAddress());
}
return list;
}
}

View File

@ -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<String, byte[]> BYTES = new HashMap<String, byte[]>();
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<String> 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<String, List<String>> domainMap;
public MapRecordStoreA(Set<String> domains, int length) {
domainMap = new HashMap<String, List<String>>(domains.size());
for (String domain : domains) {
List<String> addresses = new ArrayList<String>(length);
for (int i = 0; i < length; i++) {
addresses.add(TestRecordStore.nextIp());
}
domainMap.put(domain, addresses);
}
}
public MapRecordStoreA(Set<String> domains) {
this(domains, 1);
}
public String getAddress(String domain) {
return domainMap.get(domain).get(0);
}
public List<String> getAddresses(String domain) {
return domainMap.get(domain);
}
@Override
public Set<ResourceRecord> getRecords(QuestionRecord questionRecord) throws DnsException {
String name = questionRecord.getDomainName();
List<String> addresses = domainMap.get(name);
if (addresses != null && questionRecord.getRecordType() == RecordType.A) {
Set<ResourceRecord> records = new LinkedHashSet<ResourceRecord>();
for (String address : addresses) {
HashMap<String, Object> attributes = new HashMap<String, Object>();
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<String> domains;
public TestRecordStore(Set<String> domains) {
this.domains = domains;
}
@Override
public Set<ResourceRecord> 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;
}
}
}