dorkbox.network.DnsClient Maven / Gradle / Ivy
/*
* Copyright 2010 dorkbox, llc
*
* Licensed 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 dorkbox.network;
import dorkbox.network.connection.EndPoint;
import dorkbox.network.dns.decoder.DomainDecoder;
import dorkbox.network.dns.decoder.MailExchangerDecoder;
import dorkbox.network.dns.decoder.RecordDecoder;
import dorkbox.network.dns.decoder.ServiceDecoder;
import dorkbox.network.dns.decoder.StartOfAuthorityDecoder;
import dorkbox.network.dns.decoder.TextDecoder;
import dorkbox.network.dns.record.MailExchangerRecord;
import dorkbox.network.dns.record.ServiceRecord;
import dorkbox.network.dns.record.StartOfAuthorityRecord;
import dorkbox.util.NamedThreadFactory;
import dorkbox.util.OS;
import dorkbox.util.Property;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.ByteBufHolder;
import io.netty.channel.AddressedEnvelope;
import io.netty.channel.EventLoopGroup;
import io.netty.channel.ReflectiveChannelFactory;
import io.netty.channel.epoll.EpollDatagramChannel;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.oio.OioEventLoopGroup;
import io.netty.channel.socket.DatagramChannel;
import io.netty.channel.socket.InternetProtocolFamily;
import io.netty.channel.socket.nio.NioDatagramChannel;
import io.netty.channel.socket.oio.OioDatagramChannel;
import io.netty.handler.codec.dns.DefaultDnsQuestion;
import io.netty.handler.codec.dns.DnsRecord;
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.resolver.dns.DnsNameResolver;
import io.netty.resolver.dns.DnsNameResolverBuilder;
import io.netty.resolver.dns.DnsServerAddresses;
import io.netty.util.concurrent.Future;
import io.netty.util.internal.PlatformDependent;
import org.slf4j.Logger;
import java.net.IDN;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.security.AccessControlException;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
/**
* for now, we only support ipv4
*/
@SuppressWarnings("unused")
public
class DnsClient {
// duplicated in EndPoint
static {
//noinspection Duplicates
try {
// doesn't work when running from inside eclipse.
// Needed for NIO selectors on Android 2.2, and to force IPv4.
System.setProperty("java.net.preferIPv4Stack", Boolean.TRUE.toString());
System.setProperty("java.net.preferIPv6Addresses", Boolean.FALSE.toString());
// java6 has stack overflow problems when loading certain classes in it's classloader. The result is a StackOverflow when
// loading them normally
if (OS.javaVersion == 6) {
if (PlatformDependent.hasUnsafe()) {
PlatformDependent.newFixedMpscQueue(8);
}
}
} catch (AccessControlException ignored) {
}
}
// @formatter:off
@Property
public static List DNS_SERVER_LIST = Arrays.asList(
new InetSocketAddress("8.8.8.8", 53), // Google Public DNS
new InetSocketAddress("8.8.4.4", 53),
new InetSocketAddress("208.67.222.222", 53), // OpenDNS
new InetSocketAddress("208.67.220.220", 53),
new InetSocketAddress("37.235.1.174", 53), // FreeDNS
new InetSocketAddress("37.235.1.177", 53)
);
// @formatter:on
private static final String ptrSuffix = ".in-addr.arpa";
/**
* Gets the version number.
*/
public static
String getVersion() {
return "1.20";
}
/**
* Retrieve the public facing IP address of this system using DNS.
*
* Same command as
*
* dig +short myip.opendns.com @resolver1.opendns.com
*
* @return the public IP address if found, or null if it didn't find it
*/
public static
String getPublicIp() {
final InetSocketAddress dnsServer = new InetSocketAddress("208.67.222.222", 53); // openDNS
DnsClient dnsClient = new DnsClient(dnsServer);
final String resolve = dnsClient.resolve("myip.opendns.com", DnsRecordType.A);
dnsClient.stop();
return resolve;
}
private final org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(getClass());
private final DnsNameResolver resolver;
private final Map> customDecoders = new HashMap>();
private ThreadGroup threadGroup;
public static final String THREAD_NAME = "DnsClient";
private EventLoopGroup eventLoopGroup;
/**
* Creates a new DNS client, using the provided server (default port 53) for DNS query resolution, with a cache that will obey the TTL of the response
*
* @param nameServerAddresses the server to receive your DNS questions.
*/
public
DnsClient(final String nameServerAddresses) {
this(Collections.singletonList(new InetSocketAddress(nameServerAddresses, 53)));
}
/**
* Creates a new DNS client, using the provided server for DNS query resolution, with a cache that will obey the TTL of the response
*
* @param nameServerAddresses the server to receive your DNS questions.
*/
public
DnsClient(final InetSocketAddress nameServerAddresses) {
this(Collections.singletonList(nameServerAddresses));
}
/**
* Creates a new DNS client, with a cache that will obey the TTL of the response
*
* @param nameServerAddresses the list of servers to receive your DNS questions, until it succeeds
*/
public
DnsClient(Collection nameServerAddresses) {
this(nameServerAddresses, 0, Integer.MAX_VALUE);
}
/**
* Creates a new DNS client.
*
* The default TTL value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to
* respect the TTL from the DNS server.
*
* @param nameServerAddresses the list of servers to receive your DNS questions, until it succeeds
* @param minTtl the minimum TTL
* @param maxTtl the maximum TTL
*/
public
DnsClient(Collection nameServerAddresses, int minTtl, int maxTtl) {
Class extends DatagramChannel> channelType;
// setup the thread group to easily ID what the following threads belong to (and their spawned threads...)
SecurityManager s = System.getSecurityManager();
threadGroup = new ThreadGroup(s != null
? s.getThreadGroup()
: Thread.currentThread()
.getThreadGroup(), THREAD_NAME);
threadGroup.setDaemon(true);
if (PlatformDependent.isAndroid()) {
// android ONLY supports OIO (not NIO)
eventLoopGroup = new OioEventLoopGroup(1, new NamedThreadFactory(THREAD_NAME + "-DNS", threadGroup));
channelType = OioDatagramChannel.class;
}
else if (OS.isLinux()) {
// JNI network stack is MUCH faster (but only on linux)
eventLoopGroup = new EpollEventLoopGroup(1, new NamedThreadFactory(THREAD_NAME + "-DNS", threadGroup));
channelType = EpollDatagramChannel.class;
}
else {
eventLoopGroup = new NioEventLoopGroup(1, new NamedThreadFactory(THREAD_NAME + "-DNS", threadGroup));
channelType = NioDatagramChannel.class;
}
DnsNameResolverBuilder builder = new DnsNameResolverBuilder(eventLoopGroup.next());
builder.channelFactory(new ReflectiveChannelFactory(channelType))
.channelType(channelType)
.ttl(minTtl, maxTtl)
.resolvedAddressTypes(InternetProtocolFamily.IPv4) // for now, we only support ipv4
.nameServerAddresses(DnsServerAddresses.sequential(nameServerAddresses));
resolver = builder.build();
// A/AAAA use the built-in decoder
customDecoders.put(DnsRecordType.MX, new MailExchangerDecoder());
customDecoders.put(DnsRecordType.TXT, new TextDecoder());
customDecoders.put(DnsRecordType.SRV, new ServiceDecoder());
RecordDecoder> decoder = new DomainDecoder();
customDecoders.put(DnsRecordType.NS, decoder);
customDecoders.put(DnsRecordType.CNAME, decoder);
customDecoders.put(DnsRecordType.PTR, decoder);
customDecoders.put(DnsRecordType.SOA, new StartOfAuthorityDecoder());
}
/**
* Resolves a specific hostname A record
*
* @param hostname the hostname, ie: google.com, that you want to resolve
*/
public
String resolve(String hostname) {
if (resolver.resolvedAddressTypes()
.get(0) == InternetProtocolFamily.IPv4) {
return resolve(hostname, DnsRecordType.A);
}
else {
return resolve(hostname, DnsRecordType.AAAA);
}
}
/**
* Resolves a specific hostname record, of the specified type (PTR, MX, TXT, etc)
*
* Note that PTR absolutely MUST end in '.in-addr.arpa' in order for the DNS server to understand it.
* -- because of this, we will automatically fix this in case that clients are unaware of this requirement
*
* @param hostname the hostname, ie: google.com, that you want to resolve
* @param type the DnsRecordType you want to resolve (PTR, MX, TXT, etc)
* @return null indicates there was an error resolving the hostname
*/
@SuppressWarnings("unchecked")
private
T resolve(String hostname, DnsRecordType type) {
hostname = IDN.toASCII(hostname);
final int value = type.intValue();
// we can use the included DNS resolver.
if (value == DnsRecordType.A.intValue() || value == DnsRecordType.AAAA.intValue()) {
// use "resolve", since it handles A/AAAA records
final Future resolve = resolver.resolve(hostname); // made up port, because it doesn't matter
final Future result = resolve.awaitUninterruptibly();
// now return whatever value we had
if (result.isSuccess() && result.isDone()) {
try {
return (T) result.getNow().getHostAddress();
} catch (Exception e) {
logger.error("Could not ask question to DNS server", e);
return null;
}
}
Throwable cause = result.cause();
Logger logger2 = this.logger;
if (logger2.isDebugEnabled() && cause != null) {
logger2.error("Could not ask question to DNS server.", cause);
return null;
}
}
else {
// we use our own resolvers
final Future> query = resolver.query(new DefaultDnsQuestion(hostname, type));
final Future> result = query.awaitUninterruptibly();
// now return whatever value we had
if (result.isSuccess() && result.isDone()) {
AddressedEnvelope envelope = result.getNow();
DnsResponse response = envelope.content();
try {
final DnsResponseCode code = response.code();
if (code == DnsResponseCode.NOERROR) {
final RecordDecoder> decoder = customDecoders.get(type);
if (decoder != null) {
final int answerCount = response.count(DnsSection.ANSWER);
for (int i = 0; i < answerCount; i++) {
final DnsRecord r = response.recordAt(DnsSection.ANSWER, i);
if (r.type() == type) {
ByteBuf recordContent = ((ByteBufHolder) r).content();
return (T) decoder.decode(r, recordContent);
}
}
}
logger.error("Could not ask question to DNS server: Issue with decoder for type: {}", type);
return null;
}
logger.error("Could not ask question to DNS server: Error code {}", code);
return null;
} finally {
response.release();
}
}
else {
Throwable cause = result.cause();
Logger logger2 = this.logger;
if (logger2.isDebugEnabled() && cause != null) {
logger2.error("Could not ask question to DNS server.", cause);
return null;
}
}
}
logger.error("Could not ask question to DNS server for type: {}", type.getClass().getSimpleName());
return null;
}
/**
* Clears the DNS resolver cache
*/
public
void reset() {
resolver.resolveCache()
.clear();
}
/**
* Safely closes all associated resources/threads/connections
*/
public
void stop() {
// we also want to stop the thread group (but NOT in our current thread!)
if (Thread.currentThread()
.getThreadGroup()
.getName()
.equals(THREAD_NAME)) {
Thread thread = new Thread(new Runnable() {
@Override
public
void run() {
DnsClient.this.stopInThread();
}
});
thread.setDaemon(false);
thread.setName("DnsClient Shutdown");
thread.start();
}
else {
stopInThread();
}
}
private
void stopInThread() {
reset();
resolver.close(); // also closes the UDP channel that DNS client uses
// Sometimes there might be "lingering" connections (ie, halfway though registration) that need to be closed.
long maxShutdownWaitTimeInMilliSeconds = EndPoint.maxShutdownWaitTimeInMilliSeconds;
// we want to WAIT until after the event executors have completed shutting down.
List> shutdownThreadList = new LinkedList>();
// now wait it them to finish!
// It can take a few seconds to shut down the executor. This will affect unit testing, where connections are quickly created/stopped
eventLoopGroup.shutdownGracefully(maxShutdownWaitTimeInMilliSeconds, maxShutdownWaitTimeInMilliSeconds * 4, TimeUnit.MILLISECONDS)
.syncUninterruptibly();
threadGroup.interrupt();
}
public
ServiceRecord resolveSRV(final String hostname) {
return resolve(hostname, DnsRecordType.SRV);
}
public
MailExchangerRecord resolveMX(final String hostname) {
return resolve(hostname, DnsRecordType.MX);
}
public
String resolveCNAME(final String hostname) {
return resolve(hostname, DnsRecordType.CNAME);
}
public
String resolveNS(final String hostname) {
return resolve(hostname, DnsRecordType.NS);
}
public
String resolvePTR(String hostname) {
// PTR absolutely MUST end in ".in-addr.arpa"
if (!hostname.endsWith(ptrSuffix)) {
hostname += ptrSuffix;
}
return resolve(hostname, DnsRecordType.PTR);
}
public
String resolveA(final String hostname) {
return resolve(hostname, DnsRecordType.A);
}
public
String resolveAAAA(final String hostname) {
return resolve(hostname, DnsRecordType.AAAA);
}
public
StartOfAuthorityRecord resolveSOA(final String hostname) {
return resolve(hostname, DnsRecordType.SOA);
}
public
List resolveTXT(final String hostname) {
return resolve(hostname, DnsRecordType.TXT);
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy