diff --git a/netty-handler-codec-dns/build.gradle b/netty-handler-codec-dns/build.gradle new file mode 100644 index 0000000..094f67f --- /dev/null +++ b/netty-handler-codec-dns/build.gradle @@ -0,0 +1,3 @@ +dependencies { + api project(':netty-handler') +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsMessage.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsMessage.java new file mode 100644 index 0000000..0320d07 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsMessage.java @@ -0,0 +1,478 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.AbstractReferenceCounted; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.ReferenceCounted; +import io.netty.util.ResourceLeakDetector; +import io.netty.util.ResourceLeakDetectorFactory; +import io.netty.util.ResourceLeakTracker; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +import java.util.ArrayList; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * A skeletal implementation of {@link DnsMessage}. + */ +@UnstableApi +public abstract class AbstractDnsMessage extends AbstractReferenceCounted implements DnsMessage { + + private static final ResourceLeakDetector leakDetector = + ResourceLeakDetectorFactory.instance().newResourceLeakDetector(DnsMessage.class); + + private static final int SECTION_QUESTION = DnsSection.QUESTION.ordinal(); + private static final int SECTION_COUNT = 4; + + private final ResourceLeakTracker leak = leakDetector.track(this); + private short id; + private DnsOpCode opCode; + private boolean recursionDesired; + private byte z; + + // To reduce the memory footprint of a message, + // each of the following fields is a single record or a list of records. + private Object questions; + private Object answers; + private Object authorities; + private Object additionals; + + /** + * Creates a new instance with the specified {@code id} and {@link DnsOpCode#QUERY} opCode. + */ + protected AbstractDnsMessage(int id) { + this(id, DnsOpCode.QUERY); + } + + /** + * Creates a new instance with the specified {@code id} and {@code opCode}. + */ + protected AbstractDnsMessage(int id, DnsOpCode opCode) { + setId(id); + setOpCode(opCode); + } + + @Override + public int id() { + return id & 0xFFFF; + } + + @Override + public DnsMessage setId(int id) { + this.id = (short) id; + return this; + } + + @Override + public DnsOpCode opCode() { + return opCode; + } + + @Override + public DnsMessage setOpCode(DnsOpCode opCode) { + this.opCode = checkNotNull(opCode, "opCode"); + return this; + } + + @Override + public boolean isRecursionDesired() { + return recursionDesired; + } + + @Override + public DnsMessage setRecursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + @Override + public int z() { + return z; + } + + @Override + public DnsMessage setZ(int z) { + this.z = (byte) (z & 7); + return this; + } + + @Override + public int count(DnsSection section) { + return count(sectionOrdinal(section)); + } + + private int count(int section) { + final Object records = sectionAt(section); + if (records == null) { + return 0; + } + if (records instanceof DnsRecord) { + return 1; + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + return recordList.size(); + } + + @Override + public int count() { + int count = 0; + for (int i = 0; i < SECTION_COUNT; i ++) { + count += count(i); + } + return count; + } + + @Override + public T recordAt(DnsSection section) { + return recordAt(sectionOrdinal(section)); + } + + private T recordAt(int section) { + final Object records = sectionAt(section); + if (records == null) { + return null; + } + + if (records instanceof DnsRecord) { + return castRecord(records); + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + if (recordList.isEmpty()) { + return null; + } + + return castRecord(recordList.get(0)); + } + + @Override + public T recordAt(DnsSection section, int index) { + return recordAt(sectionOrdinal(section), index); + } + + private T recordAt(int section, int index) { + final Object records = sectionAt(section); + if (records == null) { + throw new IndexOutOfBoundsException("index: " + index + " (expected: none)"); + } + + if (records instanceof DnsRecord) { + if (index == 0) { + return castRecord(records); + } else { + throw new IndexOutOfBoundsException("index: " + index + "' (expected: 0)"); + } + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + return castRecord(recordList.get(index)); + } + + @Override + public DnsMessage setRecord(DnsSection section, DnsRecord record) { + setRecord(sectionOrdinal(section), record); + return this; + } + + private void setRecord(int section, DnsRecord record) { + clear(section); + setSection(section, checkQuestion(section, record)); + } + + @Override + public T setRecord(DnsSection section, int index, DnsRecord record) { + return setRecord(sectionOrdinal(section), index, record); + } + + private T setRecord(int section, int index, DnsRecord record) { + checkQuestion(section, record); + + final Object records = sectionAt(section); + if (records == null) { + throw new IndexOutOfBoundsException("index: " + index + " (expected: none)"); + } + + if (records instanceof DnsRecord) { + if (index == 0) { + setSection(section, record); + return castRecord(records); + } else { + throw new IndexOutOfBoundsException("index: " + index + " (expected: 0)"); + } + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + return castRecord(recordList.set(index, record)); + } + + @Override + public DnsMessage addRecord(DnsSection section, DnsRecord record) { + addRecord(sectionOrdinal(section), record); + return this; + } + + private void addRecord(int section, DnsRecord record) { + checkQuestion(section, record); + + final Object records = sectionAt(section); + if (records == null) { + setSection(section, record); + return; + } + + if (records instanceof DnsRecord) { + final List recordList = newRecordList(); + recordList.add(castRecord(records)); + recordList.add(record); + setSection(section, recordList); + return; + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + recordList.add(record); + } + + @Override + public DnsMessage addRecord(DnsSection section, int index, DnsRecord record) { + addRecord(sectionOrdinal(section), index, record); + return this; + } + + private void addRecord(int section, int index, DnsRecord record) { + checkQuestion(section, record); + + final Object records = sectionAt(section); + if (records == null) { + if (index != 0) { + throw new IndexOutOfBoundsException("index: " + index + " (expected: 0)"); + } + + setSection(section, record); + return; + } + + if (records instanceof DnsRecord) { + final List recordList; + if (index == 0) { + recordList = newRecordList(); + recordList.add(record); + recordList.add(castRecord(records)); + } else if (index == 1) { + recordList = newRecordList(); + recordList.add(castRecord(records)); + recordList.add(record); + } else { + throw new IndexOutOfBoundsException("index: " + index + " (expected: 0 or 1)"); + } + setSection(section, recordList); + return; + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + recordList.add(index, record); + } + + @Override + public T removeRecord(DnsSection section, int index) { + return removeRecord(sectionOrdinal(section), index); + } + + private T removeRecord(int section, int index) { + final Object records = sectionAt(section); + if (records == null) { + throw new IndexOutOfBoundsException("index: " + index + " (expected: none)"); + } + + if (records instanceof DnsRecord) { + if (index != 0) { + throw new IndexOutOfBoundsException("index: " + index + " (expected: 0)"); + } + + T record = castRecord(records); + setSection(section, null); + return record; + } + + @SuppressWarnings("unchecked") + final List recordList = (List) records; + return castRecord(recordList.remove(index)); + } + + @Override + public DnsMessage clear(DnsSection section) { + clear(sectionOrdinal(section)); + return this; + } + + @Override + public DnsMessage clear() { + for (int i = 0; i < SECTION_COUNT; i ++) { + clear(i); + } + return this; + } + + private void clear(int section) { + final Object recordOrList = sectionAt(section); + setSection(section, null); + if (recordOrList instanceof ReferenceCounted) { + ((ReferenceCounted) recordOrList).release(); + } else if (recordOrList instanceof List) { + @SuppressWarnings("unchecked") + List list = (List) recordOrList; + if (!list.isEmpty()) { + for (Object r : list) { + ReferenceCountUtil.release(r); + } + } + } + } + + @Override + public DnsMessage touch() { + return (DnsMessage) super.touch(); + } + + @Override + public DnsMessage touch(Object hint) { + if (leak != null) { + leak.record(hint); + } + return this; + } + + @Override + public DnsMessage retain() { + return (DnsMessage) super.retain(); + } + + @Override + public DnsMessage retain(int increment) { + return (DnsMessage) super.retain(increment); + } + + @Override + protected void deallocate() { + clear(); + + final ResourceLeakTracker leak = this.leak; + if (leak != null) { + boolean closed = leak.close(this); + assert closed; + } + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof DnsMessage)) { + return false; + } + + final DnsMessage that = (DnsMessage) obj; + if (id() != that.id()) { + return false; + } + + if (this instanceof DnsQuery) { + if (!(that instanceof DnsQuery)) { + return false; + } + } else if (that instanceof DnsQuery) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + return id() * 31 + (this instanceof DnsQuery? 0 : 1); + } + + private Object sectionAt(int section) { + switch (section) { + case 0: + return questions; + case 1: + return answers; + case 2: + return authorities; + case 3: + return additionals; + default: + break; + } + + throw new Error(); // Should never reach here. + } + + private void setSection(int section, Object value) { + switch (section) { + case 0: + questions = value; + return; + case 1: + answers = value; + return; + case 2: + authorities = value; + return; + case 3: + additionals = value; + return; + default: + break; + } + + throw new Error(); // Should never reach here. + } + + private static int sectionOrdinal(DnsSection section) { + return checkNotNull(section, "section").ordinal(); + } + + private static DnsRecord checkQuestion(int section, DnsRecord record) { + if (section == SECTION_QUESTION && !(checkNotNull(record, "record") instanceof DnsQuestion)) { + throw new IllegalArgumentException( + "record: " + record + " (expected: " + StringUtil.simpleClassName(DnsQuestion.class) + ')'); + } + return record; + } + + @SuppressWarnings("unchecked") + private static T castRecord(Object record) { + return (T) record; + } + + private static ArrayList newRecordList() { + return new ArrayList(2); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsOptPseudoRrRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsOptPseudoRrRecord.java new file mode 100644 index 0000000..13439c1 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsOptPseudoRrRecord.java @@ -0,0 +1,78 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +/** + * An OPT RR record. + * + * This is used for + * Extension Mechanisms for DNS (EDNS(0)). + */ +@UnstableApi +public abstract class AbstractDnsOptPseudoRrRecord extends AbstractDnsRecord implements DnsOptPseudoRecord { + + protected AbstractDnsOptPseudoRrRecord(int maxPayloadSize, int extendedRcode, int version) { + super(StringUtil.EMPTY_STRING, DnsRecordType.OPT, maxPayloadSize, packIntoLong(extendedRcode, version)); + } + + protected AbstractDnsOptPseudoRrRecord(int maxPayloadSize) { + super(StringUtil.EMPTY_STRING, DnsRecordType.OPT, maxPayloadSize, 0); + } + + // See https://tools.ietf.org/html/rfc6891#section-6.1.3 + private static long packIntoLong(int val, int val2) { + // We are currently not support DO and Z fields, just use 0. + return ((val & 0xffL) << 24 | (val2 & 0xff) << 16) & 0xFFFFFFFFL; + } + + @Override + public int extendedRcode() { + return (short) (((int) timeToLive() >> 24) & 0xff); + } + + @Override + public int version() { + return (short) (((int) timeToLive() >> 16) & 0xff); + } + + @Override + public int flags() { + return (short) ((short) timeToLive() & 0xff); + } + + @Override + public String toString() { + return toStringBuilder().toString(); + } + + final StringBuilder toStringBuilder() { + return new StringBuilder(64) + .append(StringUtil.simpleClassName(this)) + .append('(') + .append("OPT flags:") + .append(flags()) + .append(" version:") + .append(version()) + .append(" extendedRecode:") + .append(extendedRcode()) + .append(" udp:") + .append(dnsClass()) + .append(')'); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java new file mode 100644 index 0000000..4c8df10 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/AbstractDnsRecord.java @@ -0,0 +1,165 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +import java.net.IDN; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * A skeletal implementation of {@link DnsRecord}. + */ +@UnstableApi +public abstract class AbstractDnsRecord implements DnsRecord { + + private final String name; + private final DnsRecordType type; + private final short dnsClass; + private final long timeToLive; + private int hashCode; + + /** + * Creates a new {@link #CLASS_IN IN-class} record. + * + * @param name the domain name + * @param type the type of the record + * @param timeToLive the TTL value of the record + */ + protected AbstractDnsRecord(String name, DnsRecordType type, long timeToLive) { + this(name, type, CLASS_IN, timeToLive); + } + + /** + * Creates a new record. + * + * @param name the domain name + * @param type the type of the record + * @param dnsClass the class of the record, usually one of the following: + *
    + *
  • {@link #CLASS_IN}
  • + *
  • {@link #CLASS_CSNET}
  • + *
  • {@link #CLASS_CHAOS}
  • + *
  • {@link #CLASS_HESIOD}
  • + *
  • {@link #CLASS_NONE}
  • + *
  • {@link #CLASS_ANY}
  • + *
+ * @param timeToLive the TTL value of the record + */ + protected AbstractDnsRecord(String name, DnsRecordType type, int dnsClass, long timeToLive) { + checkPositiveOrZero(timeToLive, "timeToLive"); + // Convert to ASCII which will also check that the length is not too big. + // See: + // - https://github.com/netty/netty/issues/4937 + // - https://github.com/netty/netty/issues/4935 + this.name = appendTrailingDot(IDNtoASCII(name)); + this.type = checkNotNull(type, "type"); + this.dnsClass = (short) dnsClass; + this.timeToLive = timeToLive; + } + + private static String IDNtoASCII(String name) { + checkNotNull(name, "name"); + if (PlatformDependent.isAndroid() && DefaultDnsRecordDecoder.ROOT.equals(name)) { + // Prior Android 10 there was a bug that did not correctly parse ".". + // + // See https://github.com/netty/netty/issues/10034 + return name; + } + return IDN.toASCII(name); + } + + private static String appendTrailingDot(String name) { + if (name.length() > 0 && name.charAt(name.length() - 1) != '.') { + return name + '.'; + } + return name; + } + + @Override + public String name() { + return name; + } + + @Override + public DnsRecordType type() { + return type; + } + + @Override + public int dnsClass() { + return dnsClass & 0xFFFF; + } + + @Override + public long timeToLive() { + return timeToLive; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof DnsRecord)) { + return false; + } + + final DnsRecord that = (DnsRecord) obj; + final int hashCode = this.hashCode; + if (hashCode != 0 && hashCode != that.hashCode()) { + return false; + } + + return type().intValue() == that.type().intValue() && + dnsClass() == that.dnsClass() && + name().equals(that.name()); + } + + @Override + public int hashCode() { + final int hashCode = this.hashCode; + if (hashCode != 0) { + return hashCode; + } + + return this.hashCode = name.hashCode() * 31 + type().intValue() * 31 + dnsClass(); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64); + + buf.append(StringUtil.simpleClassName(this)) + .append('(') + .append(name()) + .append(' ') + .append(timeToLive()) + .append(' '); + + DnsMessageUtil.appendRecordClass(buf, dnsClass()) + .append(' ') + .append(type().name()) + .append(')'); + + return buf.toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQuery.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQuery.java new file mode 100644 index 0000000..1720888 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQuery.java @@ -0,0 +1,192 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.channel.AddressedEnvelope; +import io.netty.util.internal.UnstableApi; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +/** + * A {@link DnsQuery} implementation for UDP/IP. + */ +@UnstableApi +public class DatagramDnsQuery extends DefaultDnsQuery + implements AddressedEnvelope { + + private final InetSocketAddress sender; + private final InetSocketAddress recipient; + + /** + * Creates a new instance with the {@link DnsOpCode#QUERY} {@code opCode}. + * + * @param sender the address of the sender + * @param recipient the address of the recipient + * @param id the {@code ID} of the DNS query + */ + public DatagramDnsQuery( + InetSocketAddress sender, InetSocketAddress recipient, int id) { + this(sender, recipient, id, DnsOpCode.QUERY); + } + + /** + * Creates a new instance. + * + * @param sender the address of the sender + * @param recipient the address of the recipient + * @param id the {@code ID} of the DNS query + * @param opCode the {@code opCode} of the DNS query + */ + public DatagramDnsQuery( + InetSocketAddress sender, InetSocketAddress recipient, int id, DnsOpCode opCode) { + super(id, opCode); + + if (recipient == null && sender == null) { + throw new NullPointerException("recipient and sender"); + } + + this.sender = sender; + this.recipient = recipient; + } + + @Override + public DatagramDnsQuery content() { + return this; + } + + @Override + public InetSocketAddress sender() { + return sender; + } + + @Override + public InetSocketAddress recipient() { + return recipient; + } + + @Override + public DatagramDnsQuery setId(int id) { + return (DatagramDnsQuery) super.setId(id); + } + + @Override + public DatagramDnsQuery setOpCode(DnsOpCode opCode) { + return (DatagramDnsQuery) super.setOpCode(opCode); + } + + @Override + public DatagramDnsQuery setRecursionDesired(boolean recursionDesired) { + return (DatagramDnsQuery) super.setRecursionDesired(recursionDesired); + } + + @Override + public DatagramDnsQuery setZ(int z) { + return (DatagramDnsQuery) super.setZ(z); + } + + @Override + public DatagramDnsQuery setRecord(DnsSection section, DnsRecord record) { + return (DatagramDnsQuery) super.setRecord(section, record); + } + + @Override + public DatagramDnsQuery addRecord(DnsSection section, DnsRecord record) { + return (DatagramDnsQuery) super.addRecord(section, record); + } + + @Override + public DatagramDnsQuery addRecord(DnsSection section, int index, DnsRecord record) { + return (DatagramDnsQuery) super.addRecord(section, index, record); + } + + @Override + public DatagramDnsQuery clear(DnsSection section) { + return (DatagramDnsQuery) super.clear(section); + } + + @Override + public DatagramDnsQuery clear() { + return (DatagramDnsQuery) super.clear(); + } + + @Override + public DatagramDnsQuery touch() { + return (DatagramDnsQuery) super.touch(); + } + + @Override + public DatagramDnsQuery touch(Object hint) { + return (DatagramDnsQuery) super.touch(hint); + } + + @Override + public DatagramDnsQuery retain() { + return (DatagramDnsQuery) super.retain(); + } + + @Override + public DatagramDnsQuery retain(int increment) { + return (DatagramDnsQuery) super.retain(increment); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!super.equals(obj)) { + return false; + } + + if (!(obj instanceof AddressedEnvelope)) { + return false; + } + + @SuppressWarnings("unchecked") + final AddressedEnvelope that = (AddressedEnvelope) obj; + if (sender() == null) { + if (that.sender() != null) { + return false; + } + } else if (!sender().equals(that.sender())) { + return false; + } + + if (recipient() == null) { + if (that.recipient() != null) { + return false; + } + } else if (!recipient().equals(that.recipient())) { + return false; + } + + return true; + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + if (sender() != null) { + hashCode = hashCode * 31 + sender().hashCode(); + } + if (recipient() != null) { + hashCode = hashCode * 31 + recipient().hashCode(); + } + return hashCode; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryDecoder.java new file mode 100644 index 0000000..1632c72 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryDecoder.java @@ -0,0 +1,62 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.internal.UnstableApi; + +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Decodes a {@link DatagramPacket} into a {@link DatagramDnsQuery}. + */ +@UnstableApi +@ChannelHandler.Sharable +public class DatagramDnsQueryDecoder extends MessageToMessageDecoder { + + private final DnsRecordDecoder recordDecoder; + + /** + * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}. + */ + public DatagramDnsQueryDecoder() { + this(DnsRecordDecoder.DEFAULT); + } + + /** + * Creates a new decoder with the specified {@code recordDecoder}. + */ + public DatagramDnsQueryDecoder(DnsRecordDecoder recordDecoder) { + this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder"); + } + + @Override + protected void decode(ChannelHandlerContext ctx, final DatagramPacket packet, List out) throws Exception { + DnsQuery query = DnsMessageUtil.decodeDnsQuery(recordDecoder, packet.content(), + new DnsMessageUtil.DnsQueryFactory() { + @Override + public DnsQuery newQuery(int id, DnsOpCode dnsOpCode) { + return new DatagramDnsQuery(packet.sender(), packet.recipient(), id, dnsOpCode); + } + }); + out.add(query); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java new file mode 100644 index 0000000..528bf8d --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsQueryEncoder.java @@ -0,0 +1,84 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.util.internal.UnstableApi; + +import java.net.InetSocketAddress; +import java.util.List; + +/** + * Encodes a {@link DatagramDnsQuery} (or an {@link AddressedEnvelope} of {@link DnsQuery}} into a + * {@link DatagramPacket}. + */ +@UnstableApi +@ChannelHandler.Sharable +public class DatagramDnsQueryEncoder extends MessageToMessageEncoder> { + + private final DnsQueryEncoder encoder; + + /** + * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}. + */ + public DatagramDnsQueryEncoder() { + this(DnsRecordEncoder.DEFAULT); + } + + /** + * Creates a new encoder with the specified {@code recordEncoder}. + */ + public DatagramDnsQueryEncoder(DnsRecordEncoder recordEncoder) { + this.encoder = new DnsQueryEncoder(recordEncoder); + } + + @Override + protected void encode( + ChannelHandlerContext ctx, + AddressedEnvelope in, List out) throws Exception { + + final InetSocketAddress recipient = in.recipient(); + final DnsQuery query = in.content(); + final ByteBuf buf = allocateBuffer(ctx, in); + + boolean success = false; + try { + encoder.encode(query, buf); + success = true; + } finally { + if (!success) { + buf.release(); + } + } + + out.add(new DatagramPacket(buf, recipient, null)); + } + + /** + * Allocate a {@link ByteBuf} which will be used for constructing a datagram packet. + * Sub-classes may override this method to return a {@link ByteBuf} with a perfect matching initial capacity. + */ + protected ByteBuf allocateBuffer( + ChannelHandlerContext ctx, + @SuppressWarnings("unused") AddressedEnvelope msg) throws Exception { + return ctx.alloc().ioBuffer(1024); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponse.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponse.java new file mode 100644 index 0000000..e3f76a9 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponse.java @@ -0,0 +1,222 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.channel.AddressedEnvelope; +import io.netty.util.internal.UnstableApi; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; + +/** + * A {@link DnsResponse} implementation for UDP/IP. + */ +@UnstableApi +public class DatagramDnsResponse extends DefaultDnsResponse + implements AddressedEnvelope { + + private final InetSocketAddress sender; + private final InetSocketAddress recipient; + + /** + * Creates a new instance with the {@link DnsOpCode#QUERY} {@code opCode} and + * the {@link DnsResponseCode#NOERROR} {@code RCODE}. + * + * @param sender the address of the sender + * @param recipient the address of the recipient + * @param id the {@code ID} of the DNS response + */ + public DatagramDnsResponse(InetSocketAddress sender, InetSocketAddress recipient, int id) { + this(sender, recipient, id, DnsOpCode.QUERY, DnsResponseCode.NOERROR); + } + + /** + * Creates a new instance with the {@link DnsResponseCode#NOERROR} responseCode. + * + * @param sender the address of the sender + * @param recipient the address of the recipient + * @param id the {@code ID} of the DNS response + * @param opCode the {@code opCode} of the DNS response + */ + public DatagramDnsResponse(InetSocketAddress sender, InetSocketAddress recipient, int id, DnsOpCode opCode) { + this(sender, recipient, id, opCode, DnsResponseCode.NOERROR); + } + + /** + * Creates a new instance. + * + * @param sender the address of the sender + * @param recipient the address of the recipient + * @param id the {@code ID} of the DNS response + * @param opCode the {@code opCode} of the DNS response + * @param responseCode the {@code RCODE} of the DNS response + */ + public DatagramDnsResponse( + InetSocketAddress sender, InetSocketAddress recipient, + int id, DnsOpCode opCode, DnsResponseCode responseCode) { + super(id, opCode, responseCode); + + if (recipient == null && sender == null) { + throw new NullPointerException("recipient and sender"); + } + + this.sender = sender; + this.recipient = recipient; + } + + @Override + public DatagramDnsResponse content() { + return this; + } + + @Override + public InetSocketAddress sender() { + return sender; + } + + @Override + public InetSocketAddress recipient() { + return recipient; + } + + @Override + public DatagramDnsResponse setAuthoritativeAnswer(boolean authoritativeAnswer) { + return (DatagramDnsResponse) super.setAuthoritativeAnswer(authoritativeAnswer); + } + + @Override + public DatagramDnsResponse setTruncated(boolean truncated) { + return (DatagramDnsResponse) super.setTruncated(truncated); + } + + @Override + public DatagramDnsResponse setRecursionAvailable(boolean recursionAvailable) { + return (DatagramDnsResponse) super.setRecursionAvailable(recursionAvailable); + } + + @Override + public DatagramDnsResponse setCode(DnsResponseCode code) { + return (DatagramDnsResponse) super.setCode(code); + } + + @Override + public DatagramDnsResponse setId(int id) { + return (DatagramDnsResponse) super.setId(id); + } + + @Override + public DatagramDnsResponse setOpCode(DnsOpCode opCode) { + return (DatagramDnsResponse) super.setOpCode(opCode); + } + + @Override + public DatagramDnsResponse setRecursionDesired(boolean recursionDesired) { + return (DatagramDnsResponse) super.setRecursionDesired(recursionDesired); + } + + @Override + public DatagramDnsResponse setZ(int z) { + return (DatagramDnsResponse) super.setZ(z); + } + + @Override + public DatagramDnsResponse setRecord(DnsSection section, DnsRecord record) { + return (DatagramDnsResponse) super.setRecord(section, record); + } + + @Override + public DatagramDnsResponse addRecord(DnsSection section, DnsRecord record) { + return (DatagramDnsResponse) super.addRecord(section, record); + } + + @Override + public DatagramDnsResponse addRecord(DnsSection section, int index, DnsRecord record) { + return (DatagramDnsResponse) super.addRecord(section, index, record); + } + + @Override + public DatagramDnsResponse clear(DnsSection section) { + return (DatagramDnsResponse) super.clear(section); + } + + @Override + public DatagramDnsResponse clear() { + return (DatagramDnsResponse) super.clear(); + } + + @Override + public DatagramDnsResponse touch() { + return (DatagramDnsResponse) super.touch(); + } + + @Override + public DatagramDnsResponse touch(Object hint) { + return (DatagramDnsResponse) super.touch(hint); + } + + @Override + public DatagramDnsResponse retain() { + return (DatagramDnsResponse) super.retain(); + } + + @Override + public DatagramDnsResponse retain(int increment) { + return (DatagramDnsResponse) super.retain(increment); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!super.equals(obj)) { + return false; + } + + if (!(obj instanceof AddressedEnvelope)) { + return false; + } + + @SuppressWarnings("unchecked") + final AddressedEnvelope that = (AddressedEnvelope) obj; + if (sender() == null) { + if (that.sender() != null) { + return false; + } + } else if (!sender().equals(that.sender())) { + return false; + } + + if (recipient() == null) { + return that.recipient() == null; + } else { + return recipient().equals(that.recipient()); + } + } + + @Override + public int hashCode() { + int hashCode = super.hashCode(); + if (sender() != null) { + hashCode = hashCode * 31 + sender().hashCode(); + } + if (recipient() != null) { + hashCode = hashCode * 31 + recipient().hashCode(); + } + return hashCode; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java new file mode 100644 index 0000000..487f598 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseDecoder.java @@ -0,0 +1,69 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.MessageToMessageDecoder; +import io.netty.util.internal.UnstableApi; + +import java.net.InetSocketAddress; +import java.util.List; + +/** + * Decodes a {@link DatagramPacket} into a {@link DatagramDnsResponse}. + */ +@UnstableApi +@ChannelHandler.Sharable +public class DatagramDnsResponseDecoder extends MessageToMessageDecoder { + + private final DnsResponseDecoder responseDecoder; + + /** + * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}. + */ + public DatagramDnsResponseDecoder() { + this(DnsRecordDecoder.DEFAULT); + } + + /** + * Creates a new decoder with the specified {@code recordDecoder}. + */ + public DatagramDnsResponseDecoder(DnsRecordDecoder recordDecoder) { + this.responseDecoder = new DnsResponseDecoder(recordDecoder) { + @Override + protected DnsResponse newResponse(InetSocketAddress sender, InetSocketAddress recipient, + int id, DnsOpCode opCode, DnsResponseCode responseCode) { + return new DatagramDnsResponse(sender, recipient, id, opCode, responseCode); + } + }; + } + + @Override + protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List out) throws Exception { + try { + out.add(decodeResponse(ctx, packet)); + } catch (IndexOutOfBoundsException e) { + throw new CorruptedFrameException("Unable to decode response", e); + } + } + + protected DnsResponse decodeResponse(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + return responseDecoder.decode(packet.sender(), packet.recipient(), packet.content()); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseEncoder.java new file mode 100644 index 0000000..e8f5d88 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DatagramDnsResponseEncoder.java @@ -0,0 +1,78 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.util.internal.UnstableApi; + +import java.net.InetSocketAddress; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Encodes a {@link DatagramDnsResponse} (or an {@link AddressedEnvelope} of {@link DnsResponse}} into a + * {@link DatagramPacket}. + */ +@UnstableApi +@ChannelHandler.Sharable +public class DatagramDnsResponseEncoder + extends MessageToMessageEncoder> { + + private final DnsRecordEncoder recordEncoder; + + /** + * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}. + */ + public DatagramDnsResponseEncoder() { + this(DnsRecordEncoder.DEFAULT); + } + + /** + * Creates a new encoder with the specified {@code recordEncoder}. + */ + public DatagramDnsResponseEncoder(DnsRecordEncoder recordEncoder) { + this.recordEncoder = checkNotNull(recordEncoder, "recordEncoder"); + } + + @Override + protected void encode(ChannelHandlerContext ctx, + AddressedEnvelope in, List out) throws Exception { + + final InetSocketAddress recipient = in.recipient(); + final DnsResponse response = in.content(); + final ByteBuf buf = allocateBuffer(ctx, in); + + DnsMessageUtil.encodeDnsResponse(recordEncoder, response, buf); + + out.add(new DatagramPacket(buf, recipient, null)); + } + + /** + * Allocate a {@link ByteBuf} which will be used for constructing a datagram packet. + * Sub-classes may override this method to return a {@link ByteBuf} with a perfect matching initial capacity. + */ + protected ByteBuf allocateBuffer( + ChannelHandlerContext ctx, + @SuppressWarnings("unused") AddressedEnvelope msg) throws Exception { + return ctx.alloc().ioBuffer(1024); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsOptEcsRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsOptEcsRecord.java new file mode 100644 index 0000000..e10e396 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsOptEcsRecord.java @@ -0,0 +1,104 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.UnstableApi; + +import java.net.InetAddress; +import java.util.Arrays; + +/** + * Default {@link DnsOptEcsRecord} implementation. + */ +@UnstableApi +public final class DefaultDnsOptEcsRecord extends AbstractDnsOptPseudoRrRecord implements DnsOptEcsRecord { + private final int srcPrefixLength; + private final byte[] address; + + /** + * Creates a new instance. + * + * @param maxPayloadSize the suggested max payload size in bytes + * @param extendedRcode the extended rcode + * @param version the version + * @param srcPrefixLength the prefix length + * @param address the bytes of the {@link InetAddress} to use + */ + public DefaultDnsOptEcsRecord(int maxPayloadSize, int extendedRcode, int version, + int srcPrefixLength, byte[] address) { + super(maxPayloadSize, extendedRcode, version); + this.srcPrefixLength = srcPrefixLength; + this.address = verifyAddress(address).clone(); + } + + /** + * Creates a new instance. + * + * @param maxPayloadSize the suggested max payload size in bytes + * @param srcPrefixLength the prefix length + * @param address the bytes of the {@link InetAddress} to use + */ + public DefaultDnsOptEcsRecord(int maxPayloadSize, int srcPrefixLength, byte[] address) { + this(maxPayloadSize, 0, 0, srcPrefixLength, address); + } + + /** + * Creates a new instance. + * + * @param maxPayloadSize the suggested max payload size in bytes + * @param protocolFamily the {@link InternetProtocolFamily} to use. This should be the same as the one used to + * send the query. + */ + public DefaultDnsOptEcsRecord(int maxPayloadSize, InternetProtocolFamily protocolFamily) { + this(maxPayloadSize, 0, 0, 0, protocolFamily.localhost().getAddress()); + } + + private static byte[] verifyAddress(byte[] bytes) { + if (bytes.length == 4 || bytes.length == 16) { + return bytes; + } + throw new IllegalArgumentException("bytes.length must either 4 or 16"); + } + + @Override + public int sourcePrefixLength() { + return srcPrefixLength; + } + + @Override + public int scopePrefixLength() { + return 0; + } + + @Override + public byte[] address() { + return address.clone(); + } + + @Override + public String toString() { + StringBuilder sb = toStringBuilder(); + sb.setLength(sb.length() - 1); + return sb.append(" address:") + .append(Arrays.toString(address)) + .append(" sourcePrefixLength:") + .append(sourcePrefixLength()) + .append(" scopePrefixLength:") + .append(scopePrefixLength()) + .append(')').toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsPtrRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsPtrRecord.java new file mode 100644 index 0000000..59463d4 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsPtrRecord.java @@ -0,0 +1,73 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +@UnstableApi +public class DefaultDnsPtrRecord extends AbstractDnsRecord implements DnsPtrRecord { + + private final String hostname; + + /** + * Creates a new PTR record. + * + * @param name the domain name + * @param dnsClass the class of the record, usually one of the following: + *
    + *
  • {@link #CLASS_IN}
  • + *
  • {@link #CLASS_CSNET}
  • + *
  • {@link #CLASS_CHAOS}
  • + *
  • {@link #CLASS_HESIOD}
  • + *
  • {@link #CLASS_NONE}
  • + *
  • {@link #CLASS_ANY}
  • + *
+ * @param timeToLive the TTL value of the record + * @param hostname the hostname this PTR record resolves to. + */ + public DefaultDnsPtrRecord( + String name, int dnsClass, long timeToLive, String hostname) { + super(name, DnsRecordType.PTR, dnsClass, timeToLive); + this.hostname = checkNotNull(hostname, "hostname"); + } + + @Override + public String hostname() { + return hostname; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(64).append(StringUtil.simpleClassName(this)).append('('); + final DnsRecordType type = type(); + buf.append(name().isEmpty()? "" : name()) + .append(' ') + .append(timeToLive()) + .append(' '); + + DnsMessageUtil.appendRecordClass(buf, dnsClass()) + .append(' ') + .append(type.name()); + + buf.append(' ') + .append(hostname); + + return buf.toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsQuery.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsQuery.java new file mode 100644 index 0000000..6fcebcd --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsQuery.java @@ -0,0 +1,114 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * The default {@link DnsQuery} implementation. + */ +@UnstableApi +public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery { + + /** + * Creates a new instance with the {@link DnsOpCode#QUERY} {@code opCode}. + * + * @param id the {@code ID} of the DNS query + */ + public DefaultDnsQuery(int id) { + super(id); + } + + /** + * Creates a new instance. + * + * @param id the {@code ID} of the DNS query + * @param opCode the {@code opCode} of the DNS query + */ + public DefaultDnsQuery(int id, DnsOpCode opCode) { + super(id, opCode); + } + + @Override + public DnsQuery setId(int id) { + return (DnsQuery) super.setId(id); + } + + @Override + public DnsQuery setOpCode(DnsOpCode opCode) { + return (DnsQuery) super.setOpCode(opCode); + } + + @Override + public DnsQuery setRecursionDesired(boolean recursionDesired) { + return (DnsQuery) super.setRecursionDesired(recursionDesired); + } + + @Override + public DnsQuery setZ(int z) { + return (DnsQuery) super.setZ(z); + } + + @Override + public DnsQuery setRecord(DnsSection section, DnsRecord record) { + return (DnsQuery) super.setRecord(section, record); + } + + @Override + public DnsQuery addRecord(DnsSection section, DnsRecord record) { + return (DnsQuery) super.addRecord(section, record); + } + + @Override + public DnsQuery addRecord(DnsSection section, int index, DnsRecord record) { + return (DnsQuery) super.addRecord(section, index, record); + } + + @Override + public DnsQuery clear(DnsSection section) { + return (DnsQuery) super.clear(section); + } + + @Override + public DnsQuery clear() { + return (DnsQuery) super.clear(); + } + + @Override + public DnsQuery touch() { + return (DnsQuery) super.touch(); + } + + @Override + public DnsQuery touch(Object hint) { + return (DnsQuery) super.touch(hint); + } + + @Override + public DnsQuery retain() { + return (DnsQuery) super.retain(); + } + + @Override + public DnsQuery retain(int increment) { + return (DnsQuery) super.retain(increment); + } + + @Override + public String toString() { + return DnsMessageUtil.appendQuery(new StringBuilder(128), this).toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsQuestion.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsQuestion.java new file mode 100644 index 0000000..f32320e --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsQuestion.java @@ -0,0 +1,72 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +/** + * The default {@link DnsQuestion} implementation. + */ +@UnstableApi +public class DefaultDnsQuestion extends AbstractDnsRecord implements DnsQuestion { + + /** + * Creates a new {@link #CLASS_IN IN-class} question. + * + * @param name the domain name of the DNS question + * @param type the type of the DNS question + */ + public DefaultDnsQuestion(String name, DnsRecordType type) { + super(name, type, 0); + } + + /** + * Creates a new question. + * + * @param name the domain name of the DNS question + * @param type the type of the DNS question + * @param dnsClass the class of the record, usually one of the following: + *
    + *
  • {@link #CLASS_IN}
  • + *
  • {@link #CLASS_CSNET}
  • + *
  • {@link #CLASS_CHAOS}
  • + *
  • {@link #CLASS_HESIOD}
  • + *
  • {@link #CLASS_NONE}
  • + *
  • {@link #CLASS_ANY}
  • + *
+ */ + public DefaultDnsQuestion(String name, DnsRecordType type, int dnsClass) { + super(name, type, dnsClass, 0); + } + + @Override + public String toString() { + StringBuilder buf = new StringBuilder(64); + + buf.append(StringUtil.simpleClassName(this)) + .append('(') + .append(name()) + .append(' '); + + DnsMessageUtil.appendRecordClass(buf, dnsClass()) + .append(' ') + .append(type().name()) + .append(')'); + + return buf.toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRawRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRawRecord.java new file mode 100644 index 0000000..5415a6e --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRawRecord.java @@ -0,0 +1,155 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * The default {@code DnsRawRecord} implementation. + */ +@UnstableApi +public class DefaultDnsRawRecord extends AbstractDnsRecord implements DnsRawRecord { + + private final ByteBuf content; + + /** + * Creates a new {@link #CLASS_IN IN-class} record. + * + * @param name the domain name + * @param type the type of the record + * @param timeToLive the TTL value of the record + */ + public DefaultDnsRawRecord(String name, DnsRecordType type, long timeToLive, ByteBuf content) { + this(name, type, DnsRecord.CLASS_IN, timeToLive, content); + } + + /** + * Creates a new record. + * + * @param name the domain name + * @param type the type of the record + * @param dnsClass the class of the record, usually one of the following: + *
    + *
  • {@link #CLASS_IN}
  • + *
  • {@link #CLASS_CSNET}
  • + *
  • {@link #CLASS_CHAOS}
  • + *
  • {@link #CLASS_HESIOD}
  • + *
  • {@link #CLASS_NONE}
  • + *
  • {@link #CLASS_ANY}
  • + *
+ * @param timeToLive the TTL value of the record + */ + public DefaultDnsRawRecord( + String name, DnsRecordType type, int dnsClass, long timeToLive, ByteBuf content) { + super(name, type, dnsClass, timeToLive); + this.content = checkNotNull(content, "content"); + } + + @Override + public ByteBuf content() { + return content; + } + + @Override + public DnsRawRecord copy() { + return replace(content().copy()); + } + + @Override + public DnsRawRecord duplicate() { + return replace(content().duplicate()); + } + + @Override + public DnsRawRecord retainedDuplicate() { + return replace(content().retainedDuplicate()); + } + + @Override + public DnsRawRecord replace(ByteBuf content) { + return new DefaultDnsRawRecord(name(), type(), dnsClass(), timeToLive(), content); + } + + @Override + public int refCnt() { + return content().refCnt(); + } + + @Override + public DnsRawRecord retain() { + content().retain(); + return this; + } + + @Override + public DnsRawRecord retain(int increment) { + content().retain(increment); + return this; + } + + @Override + public boolean release() { + return content().release(); + } + + @Override + public boolean release(int decrement) { + return content().release(decrement); + } + + @Override + public DnsRawRecord touch() { + content().touch(); + return this; + } + + @Override + public DnsRawRecord touch(Object hint) { + content().touch(hint); + return this; + } + + @Override + public String toString() { + final StringBuilder buf = new StringBuilder(64).append(StringUtil.simpleClassName(this)).append('('); + final DnsRecordType type = type(); + if (type != DnsRecordType.OPT) { + buf.append(name().isEmpty()? "" : name()) + .append(' ') + .append(timeToLive()) + .append(' '); + + DnsMessageUtil.appendRecordClass(buf, dnsClass()) + .append(' ') + .append(type.name()); + } else { + buf.append("OPT flags:") + .append(timeToLive()) + .append(" udp:") + .append(dnsClass()); + } + + buf.append(' ') + .append(content().readableBytes()) + .append("B)"); + + return buf.toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java new file mode 100644 index 0000000..33a1e02 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoder.java @@ -0,0 +1,131 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.util.internal.UnstableApi; + +/** + * The default {@link DnsRecordDecoder} implementation. + * + * @see DefaultDnsRecordEncoder + */ +@UnstableApi +public class DefaultDnsRecordDecoder implements DnsRecordDecoder { + + static final String ROOT = "."; + + /** + * Creates a new instance. + */ + protected DefaultDnsRecordDecoder() { } + + @Override + public final DnsQuestion decodeQuestion(ByteBuf in) throws Exception { + String name = decodeName(in); + DnsRecordType type = DnsRecordType.valueOf(in.readUnsignedShort()); + int qClass = in.readUnsignedShort(); + return new DefaultDnsQuestion(name, type, qClass); + } + + @Override + public final T decodeRecord(ByteBuf in) throws Exception { + final int startOffset = in.readerIndex(); + final String name = decodeName(in); + + final int endOffset = in.writerIndex(); + if (endOffset - in.readerIndex() < 10) { + // Not enough data + in.readerIndex(startOffset); + return null; + } + + final DnsRecordType type = DnsRecordType.valueOf(in.readUnsignedShort()); + final int aClass = in.readUnsignedShort(); + final long ttl = in.readUnsignedInt(); + final int length = in.readUnsignedShort(); + final int offset = in.readerIndex(); + + if (endOffset - offset < length) { + // Not enough data + in.readerIndex(startOffset); + return null; + } + + @SuppressWarnings("unchecked") + T record = (T) decodeRecord(name, type, aClass, ttl, in, offset, length); + in.readerIndex(offset + length); + return record; + } + + /** + * Decodes a record from the information decoded so far by {@link #decodeRecord(ByteBuf)}. + * + * @param name the domain name of the record + * @param type the type of the record + * @param dnsClass the class of the record + * @param timeToLive the TTL of the record + * @param in the {@link ByteBuf} that contains the RDATA + * @param offset the start offset of the RDATA in {@code in} + * @param length the length of the RDATA + * + * @return a {@link DnsRawRecord}. Override this method to decode RDATA and return other record implementation. + */ + protected DnsRecord decodeRecord( + String name, DnsRecordType type, int dnsClass, long timeToLive, + ByteBuf in, int offset, int length) throws Exception { + + // DNS message compression means that domain names may contain "pointers" to other positions in the packet + // to build a full message. This means the indexes are meaningful and we need the ability to reference the + // indexes un-obstructed, and thus we cannot use a slice here. + // See https://www.ietf.org/rfc/rfc1035 [4.1.4. Message compression] + if (type == DnsRecordType.PTR) { + return new DefaultDnsPtrRecord( + name, dnsClass, timeToLive, decodeName0(in.duplicate().setIndex(offset, offset + length))); + } + if (type == DnsRecordType.CNAME || type == DnsRecordType.NS) { + return new DefaultDnsRawRecord(name, type, dnsClass, timeToLive, + DnsCodecUtil.decompressDomainName( + in.duplicate().setIndex(offset, offset + length))); + } + return new DefaultDnsRawRecord( + name, type, dnsClass, timeToLive, in.retainedDuplicate().setIndex(offset, offset + length)); + } + + /** + * Retrieves a domain name given a buffer containing a DNS packet. If the + * name contains a pointer, the position of the buffer will be set to + * directly after the pointer's index after the name has been read. + * + * @param in the byte buffer containing the DNS packet + * @return the domain name for an entry + */ + protected String decodeName0(ByteBuf in) { + return decodeName(in); + } + + /** + * Retrieves a domain name given a buffer containing a DNS packet. If the + * name contains a pointer, the position of the buffer will be set to + * directly after the pointer's index after the name has been read. + * + * @param in the byte buffer containing the DNS packet + * @return the domain name for an entry + */ + public static String decodeName(ByteBuf in) { + return DnsCodecUtil.decodeDomainName(in); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java new file mode 100644 index 0000000..a5134ec --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoder.java @@ -0,0 +1,168 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.handler.codec.UnsupportedMessageTypeException; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.UnstableApi; + +/** + * The default {@link DnsRecordEncoder} implementation. + * + * @see DefaultDnsRecordDecoder + */ +@UnstableApi +public class DefaultDnsRecordEncoder implements DnsRecordEncoder { + private static final int PREFIX_MASK = Byte.SIZE - 1; + + /** + * Creates a new instance. + */ + protected DefaultDnsRecordEncoder() { } + + @Override + public final void encodeQuestion(DnsQuestion question, ByteBuf out) throws Exception { + encodeName(question.name(), out); + out.writeShort(question.type().intValue()); + out.writeShort(question.dnsClass()); + } + + @Override + public void encodeRecord(DnsRecord record, ByteBuf out) throws Exception { + if (record instanceof DnsQuestion) { + encodeQuestion((DnsQuestion) record, out); + } else if (record instanceof DnsPtrRecord) { + encodePtrRecord((DnsPtrRecord) record, out); + } else if (record instanceof DnsOptEcsRecord) { + encodeOptEcsRecord((DnsOptEcsRecord) record, out); + } else if (record instanceof DnsOptPseudoRecord) { + encodeOptPseudoRecord((DnsOptPseudoRecord) record, out); + } else if (record instanceof DnsRawRecord) { + encodeRawRecord((DnsRawRecord) record, out); + } else { + throw new UnsupportedMessageTypeException(StringUtil.simpleClassName(record)); + } + } + + private void encodeRecord0(DnsRecord record, ByteBuf out) throws Exception { + encodeName(record.name(), out); + out.writeShort(record.type().intValue()); + out.writeShort(record.dnsClass()); + out.writeInt((int) record.timeToLive()); + } + + private void encodePtrRecord(DnsPtrRecord record, ByteBuf out) throws Exception { + encodeRecord0(record, out); + encodeName(record.hostname(), out); + } + + private void encodeOptPseudoRecord(DnsOptPseudoRecord record, ByteBuf out) throws Exception { + encodeRecord0(record, out); + out.writeShort(0); + } + + private void encodeOptEcsRecord(DnsOptEcsRecord record, ByteBuf out) throws Exception { + encodeRecord0(record, out); + + int sourcePrefixLength = record.sourcePrefixLength(); + int scopePrefixLength = record.scopePrefixLength(); + int lowOrderBitsToPreserve = sourcePrefixLength & PREFIX_MASK; + + byte[] bytes = record.address(); + int addressBits = bytes.length << 3; + if (addressBits < sourcePrefixLength || sourcePrefixLength < 0) { + throw new IllegalArgumentException(sourcePrefixLength + ": " + + sourcePrefixLength + " (expected: 0 >= " + addressBits + ')'); + } + + // See https://www.iana.org/assignments/address-family-numbers/address-family-numbers.xhtml + final short addressNumber = (short) (bytes.length == 4 ? + InternetProtocolFamily.IPv4.addressNumber() : InternetProtocolFamily.IPv6.addressNumber()); + int payloadLength = calculateEcsAddressLength(sourcePrefixLength, lowOrderBitsToPreserve); + + int fullPayloadLength = 2 + // OPTION-CODE + 2 + // OPTION-LENGTH + 2 + // FAMILY + 1 + // SOURCE PREFIX-LENGTH + 1 + // SCOPE PREFIX-LENGTH + payloadLength; // ADDRESS... + + out.writeShort(fullPayloadLength); + out.writeShort(8); // This is the defined type for ECS. + + out.writeShort(fullPayloadLength - 4); // Not include OPTION-CODE and OPTION-LENGTH + out.writeShort(addressNumber); + out.writeByte(sourcePrefixLength); + out.writeByte(scopePrefixLength); // Must be 0 in queries. + + if (lowOrderBitsToPreserve > 0) { + int bytesLength = payloadLength - 1; + out.writeBytes(bytes, 0, bytesLength); + + // Pad the leftover of the last byte with zeros. + out.writeByte(padWithZeros(bytes[bytesLength], lowOrderBitsToPreserve)); + } else { + // The sourcePrefixLength align with Byte so just copy in the bytes directly. + out.writeBytes(bytes, 0, payloadLength); + } + } + + // Package-Private for testing + static int calculateEcsAddressLength(int sourcePrefixLength, int lowOrderBitsToPreserve) { + return (sourcePrefixLength >>> 3) + (lowOrderBitsToPreserve != 0 ? 1 : 0); + } + + private void encodeRawRecord(DnsRawRecord record, ByteBuf out) throws Exception { + encodeRecord0(record, out); + + ByteBuf content = record.content(); + int contentLen = content.readableBytes(); + + out.writeShort(contentLen); + out.writeBytes(content, content.readerIndex(), contentLen); + } + + protected void encodeName(String name, ByteBuf buf) throws Exception { + DnsCodecUtil.encodeDomainName(name, buf); + } + + private static byte padWithZeros(byte b, int lowOrderBitsToPreserve) { + switch (lowOrderBitsToPreserve) { + case 0: + return 0; + case 1: + return (byte) (0x80 & b); + case 2: + return (byte) (0xC0 & b); + case 3: + return (byte) (0xE0 & b); + case 4: + return (byte) (0xF0 & b); + case 5: + return (byte) (0xF8 & b); + case 6: + return (byte) (0xFC & b); + case 7: + return (byte) (0xFE & b); + case 8: + return b; + default: + throw new IllegalArgumentException("lowOrderBitsToPreserve: " + lowOrderBitsToPreserve); + } + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsResponse.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsResponse.java new file mode 100644 index 0000000..e0ad790 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DefaultDnsResponse.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * The default {@link DnsResponse} implementation. + */ +@UnstableApi +public class DefaultDnsResponse extends AbstractDnsMessage implements DnsResponse { + + private boolean authoritativeAnswer; + private boolean truncated; + private boolean recursionAvailable; + private DnsResponseCode code; + + /** + * Creates a new instance with the {@link DnsOpCode#QUERY} {@code opCode} and + * the {@link DnsResponseCode#NOERROR} {@code RCODE}. + * + * @param id the {@code ID} of the DNS response + */ + public DefaultDnsResponse(int id) { + this(id, DnsOpCode.QUERY, DnsResponseCode.NOERROR); + } + + /** + * Creates a new instance with the {@link DnsResponseCode#NOERROR} {@code RCODE}. + * + * @param id the {@code ID} of the DNS response + * @param opCode the {@code opCode} of the DNS response + */ + public DefaultDnsResponse(int id, DnsOpCode opCode) { + this(id, opCode, DnsResponseCode.NOERROR); + } + + /** + * Creates a new instance. + * + * @param id the {@code ID} of the DNS response + * @param opCode the {@code opCode} of the DNS response + * @param code the {@code RCODE} of the DNS response + */ + public DefaultDnsResponse(int id, DnsOpCode opCode, DnsResponseCode code) { + super(id, opCode); + setCode(code); + } + + @Override + public boolean isAuthoritativeAnswer() { + return authoritativeAnswer; + } + + @Override + public DnsResponse setAuthoritativeAnswer(boolean authoritativeAnswer) { + this.authoritativeAnswer = authoritativeAnswer; + return this; + } + + @Override + public boolean isTruncated() { + return truncated; + } + + @Override + public DnsResponse setTruncated(boolean truncated) { + this.truncated = truncated; + return this; + } + + @Override + public boolean isRecursionAvailable() { + return recursionAvailable; + } + + @Override + public DnsResponse setRecursionAvailable(boolean recursionAvailable) { + this.recursionAvailable = recursionAvailable; + return this; + } + + @Override + public DnsResponseCode code() { + return code; + } + + @Override + public DnsResponse setCode(DnsResponseCode code) { + this.code = checkNotNull(code, "code"); + return this; + } + + @Override + public DnsResponse setId(int id) { + return (DnsResponse) super.setId(id); + } + + @Override + public DnsResponse setOpCode(DnsOpCode opCode) { + return (DnsResponse) super.setOpCode(opCode); + } + + @Override + public DnsResponse setRecursionDesired(boolean recursionDesired) { + return (DnsResponse) super.setRecursionDesired(recursionDesired); + } + + @Override + public DnsResponse setZ(int z) { + return (DnsResponse) super.setZ(z); + } + + @Override + public DnsResponse setRecord(DnsSection section, DnsRecord record) { + return (DnsResponse) super.setRecord(section, record); + } + + @Override + public DnsResponse addRecord(DnsSection section, DnsRecord record) { + return (DnsResponse) super.addRecord(section, record); + } + + @Override + public DnsResponse addRecord(DnsSection section, int index, DnsRecord record) { + return (DnsResponse) super.addRecord(section, index, record); + } + + @Override + public DnsResponse clear(DnsSection section) { + return (DnsResponse) super.clear(section); + } + + @Override + public DnsResponse clear() { + return (DnsResponse) super.clear(); + } + + @Override + public DnsResponse touch() { + return (DnsResponse) super.touch(); + } + + @Override + public DnsResponse touch(Object hint) { + return (DnsResponse) super.touch(hint); + } + + @Override + public DnsResponse retain() { + return (DnsResponse) super.retain(); + } + + @Override + public DnsResponse retain(int increment) { + return (DnsResponse) super.retain(increment); + } + + @Override + public String toString() { + return DnsMessageUtil.appendResponse(new StringBuilder(128), this).toString(); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java new file mode 100644 index 0000000..a702771 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsCodecUtil.java @@ -0,0 +1,131 @@ +/* + * Copyright 2019 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.util.CharsetUtil; + +import static io.netty.handler.codec.dns.DefaultDnsRecordDecoder.*; + +final class DnsCodecUtil { + private DnsCodecUtil() { + // Util class + } + + static void encodeDomainName(String name, ByteBuf buf) { + if (ROOT.equals(name)) { + // Root domain + buf.writeByte(0); + return; + } + + final String[] labels = name.split("\\."); + for (String label : labels) { + final int labelLen = label.length(); + if (labelLen == 0) { + // zero-length label means the end of the name. + break; + } + + buf.writeByte(labelLen); + ByteBufUtil.writeAscii(buf, label); + } + + buf.writeByte(0); // marks end of name field + } + + static String decodeDomainName(ByteBuf in) { + int position = -1; + int checked = 0; + final int end = in.writerIndex(); + final int readable = in.readableBytes(); + + // Looking at the spec we should always have at least enough readable bytes to read a byte here but it seems + // some servers do not respect this for empty names. So just workaround this and return an empty name in this + // case. + // + // See: + // - https://github.com/netty/netty/issues/5014 + // - https://www.ietf.org/rfc/rfc1035.txt , Section 3.1 + if (readable == 0) { + return ROOT; + } + + final StringBuilder name = new StringBuilder(readable << 1); + while (in.isReadable()) { + final int len = in.readUnsignedByte(); + final boolean pointer = (len & 0xc0) == 0xc0; + if (pointer) { + if (position == -1) { + position = in.readerIndex() + 1; + } + + if (!in.isReadable()) { + throw new CorruptedFrameException("truncated pointer in a name"); + } + + final int next = (len & 0x3f) << 8 | in.readUnsignedByte(); + if (next >= end) { + throw new CorruptedFrameException("name has an out-of-range pointer"); + } + in.readerIndex(next); + + // check for loops + checked += 2; + if (checked >= end) { + throw new CorruptedFrameException("name contains a loop."); + } + } else if (len != 0) { + if (!in.isReadable(len)) { + throw new CorruptedFrameException("truncated label in a name"); + } + name.append(in.toString(in.readerIndex(), len, CharsetUtil.UTF_8)).append('.'); + in.skipBytes(len); + } else { // len == 0 + break; + } + } + + if (position != -1) { + in.readerIndex(position); + } + + if (name.length() == 0) { + return ROOT; + } + + if (name.charAt(name.length() - 1) != '.') { + name.append('.'); + } + + return name.toString(); + } + + /** + * Decompress pointer data. + * @param compression compressed data + * @return decompressed data + */ + static ByteBuf decompressDomainName(ByteBuf compression) { + String domainName = decodeDomainName(compression); + ByteBuf result = compression.alloc().buffer(domainName.length() << 1); + encodeDomainName(domainName, result); + return result; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsMessage.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsMessage.java new file mode 100644 index 0000000..46be48a --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsMessage.java @@ -0,0 +1,158 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.ReferenceCounted; +import io.netty.util.internal.UnstableApi; + +/** + * The superclass which contains core information concerning a {@link DnsQuery} and a {@link DnsResponse}. + */ +@UnstableApi +public interface DnsMessage extends ReferenceCounted { + + /** + * Returns the {@code ID} of this DNS message. + */ + int id(); + + /** + * Sets the {@code ID} of this DNS message. + */ + DnsMessage setId(int id); + + /** + * Returns the {@code opCode} of this DNS message. + */ + DnsOpCode opCode(); + + /** + * Sets the {@code opCode} of this DNS message. + */ + DnsMessage setOpCode(DnsOpCode opCode); + + /** + * Returns the {@code RD} (recursion desired} field of this DNS message. + */ + boolean isRecursionDesired(); + + /** + * Sets the {@code RD} (recursion desired} field of this DNS message. + */ + DnsMessage setRecursionDesired(boolean recursionDesired); + + /** + * Returns the {@code Z} (reserved for future use) field of this DNS message. + */ + int z(); + + /** + * Sets the {@code Z} (reserved for future use) field of this DNS message. + */ + DnsMessage setZ(int z); + + /** + * Returns the number of records in the specified {@code section} of this DNS message. + */ + int count(DnsSection section); + + /** + * Returns the number of records in this DNS message. + */ + int count(); + + /** + * Returns the first record in the specified {@code section} of this DNS message. + * When the specified {@code section} is {@link DnsSection#QUESTION}, the type of the returned record is + * always {@link DnsQuestion}. + * + * @return {@code null} if this message doesn't have any records in the specified {@code section} + */ + T recordAt(DnsSection section); + + /** + * Returns the record at the specified {@code index} of the specified {@code section} of this DNS message. + * When the specified {@code section} is {@link DnsSection#QUESTION}, the type of the returned record is + * always {@link DnsQuestion}. + * + * @throws IndexOutOfBoundsException if the specified {@code index} is out of bounds + */ + T recordAt(DnsSection section, int index); + + /** + * Sets the specified {@code section} of this DNS message to the specified {@code record}, + * making it a single-record section. When the specified {@code section} is {@link DnsSection#QUESTION}, + * the specified {@code record} must be a {@link DnsQuestion}. + */ + DnsMessage setRecord(DnsSection section, DnsRecord record); + + /** + * Sets the specified {@code record} at the specified {@code index} of the specified {@code section} + * of this DNS message. When the specified {@code section} is {@link DnsSection#QUESTION}, + * the specified {@code record} must be a {@link DnsQuestion}. + * + * @return the old record + * @throws IndexOutOfBoundsException if the specified {@code index} is out of bounds + */ + T setRecord(DnsSection section, int index, DnsRecord record); + + /** + * Adds the specified {@code record} at the end of the specified {@code section} of this DNS message. + * When the specified {@code section} is {@link DnsSection#QUESTION}, the specified {@code record} + * must be a {@link DnsQuestion}. + */ + DnsMessage addRecord(DnsSection section, DnsRecord record); + + /** + * Adds the specified {@code record} at the specified {@code index} of the specified {@code section} + * of this DNS message. When the specified {@code section} is {@link DnsSection#QUESTION}, the specified + * {@code record} must be a {@link DnsQuestion}. + * + * @throws IndexOutOfBoundsException if the specified {@code index} is out of bounds + */ + DnsMessage addRecord(DnsSection section, int index, DnsRecord record); + + /** + * Removes the record at the specified {@code index} of the specified {@code section} from this DNS message. + * When the specified {@code section} is {@link DnsSection#QUESTION}, the type of the returned record is + * always {@link DnsQuestion}. + * + * @return the removed record + */ + T removeRecord(DnsSection section, int index); + + /** + * Removes all the records in the specified {@code section} of this DNS message. + */ + DnsMessage clear(DnsSection section); + + /** + * Removes all the records in this DNS message. + */ + DnsMessage clear(); + + @Override + DnsMessage touch(); + + @Override + DnsMessage touch(Object hint); + + @Override + DnsMessage retain(); + + @Override + DnsMessage retain(int increment); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsMessageUtil.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsMessageUtil.java new file mode 100644 index 0000000..8beb418 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsMessageUtil.java @@ -0,0 +1,304 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.AddressedEnvelope; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.util.internal.StringUtil; + +import java.net.SocketAddress; + +/** + * Provides some utility methods for DNS message implementations. + */ +final class DnsMessageUtil { + + static StringBuilder appendQuery(StringBuilder buf, DnsQuery query) { + appendQueryHeader(buf, query); + appendAllRecords(buf, query); + return buf; + } + + static StringBuilder appendResponse(StringBuilder buf, DnsResponse response) { + appendResponseHeader(buf, response); + appendAllRecords(buf, response); + return buf; + } + + static StringBuilder appendRecordClass(StringBuilder buf, int dnsClass) { + final String name; + switch (dnsClass &= 0xFFFF) { + case DnsRecord.CLASS_IN: + name = "IN"; + break; + case DnsRecord.CLASS_CSNET: + name = "CSNET"; + break; + case DnsRecord.CLASS_CHAOS: + name = "CHAOS"; + break; + case DnsRecord.CLASS_HESIOD: + name = "HESIOD"; + break; + case DnsRecord.CLASS_NONE: + name = "NONE"; + break; + case DnsRecord.CLASS_ANY: + name = "ANY"; + break; + default: + name = null; + break; + } + + if (name != null) { + buf.append(name); + } else { + buf.append("UNKNOWN(").append(dnsClass).append(')'); + } + + return buf; + } + + private static void appendQueryHeader(StringBuilder buf, DnsQuery msg) { + buf.append(StringUtil.simpleClassName(msg)) + .append('('); + + appendAddresses(buf, msg) + .append("id: ") + .append(msg.id()) + .append(", ") + .append(msg.opCode()); + + if (msg.isRecursionDesired()) { + buf.append(", RD"); + } + if (msg.z() != 0) { + buf.append(", Z: ") + .append(msg.z()); + } + buf.append(')'); + } + + private static void appendResponseHeader(StringBuilder buf, DnsResponse msg) { + buf.append(StringUtil.simpleClassName(msg)) + .append('('); + + appendAddresses(buf, msg) + .append("id: ") + .append(msg.id()) + .append(", ") + .append(msg.opCode()) + .append(", ") + .append(msg.code()) + .append(','); + + boolean hasComma = true; + if (msg.isRecursionDesired()) { + hasComma = false; + buf.append(" RD"); + } + if (msg.isAuthoritativeAnswer()) { + hasComma = false; + buf.append(" AA"); + } + if (msg.isTruncated()) { + hasComma = false; + buf.append(" TC"); + } + if (msg.isRecursionAvailable()) { + hasComma = false; + buf.append(" RA"); + } + if (msg.z() != 0) { + if (!hasComma) { + buf.append(','); + } + buf.append(" Z: ") + .append(msg.z()); + } + + if (hasComma) { + buf.setCharAt(buf.length() - 1, ')'); + } else { + buf.append(')'); + } + } + + private static StringBuilder appendAddresses(StringBuilder buf, DnsMessage msg) { + + if (!(msg instanceof AddressedEnvelope)) { + return buf; + } + + @SuppressWarnings("unchecked") + AddressedEnvelope envelope = (AddressedEnvelope) msg; + + SocketAddress addr = envelope.sender(); + if (addr != null) { + buf.append("from: ") + .append(addr) + .append(", "); + } + + addr = envelope.recipient(); + if (addr != null) { + buf.append("to: ") + .append(addr) + .append(", "); + } + + return buf; + } + + private static void appendAllRecords(StringBuilder buf, DnsMessage msg) { + appendRecords(buf, msg, DnsSection.QUESTION); + appendRecords(buf, msg, DnsSection.ANSWER); + appendRecords(buf, msg, DnsSection.AUTHORITY); + appendRecords(buf, msg, DnsSection.ADDITIONAL); + } + + private static void appendRecords(StringBuilder buf, DnsMessage message, DnsSection section) { + final int count = message.count(section); + for (int i = 0; i < count; i ++) { + buf.append(StringUtil.NEWLINE) + .append(StringUtil.TAB) + .append(message.recordAt(section, i)); + } + } + + static DnsQuery decodeDnsQuery(DnsRecordDecoder decoder, ByteBuf buf, DnsQueryFactory supplier) throws Exception { + DnsQuery query = newQuery(buf, supplier); + boolean success = false; + try { + int questionCount = buf.readUnsignedShort(); + int answerCount = buf.readUnsignedShort(); + int authorityRecordCount = buf.readUnsignedShort(); + int additionalRecordCount = buf.readUnsignedShort(); + decodeQuestions(decoder, query, buf, questionCount); + decodeRecords(decoder, query, DnsSection.ANSWER, buf, answerCount); + decodeRecords(decoder, query, DnsSection.AUTHORITY, buf, authorityRecordCount); + decodeRecords(decoder, query, DnsSection.ADDITIONAL, buf, additionalRecordCount); + success = true; + return query; + } finally { + if (!success) { + query.release(); + } + } + } + + private static DnsQuery newQuery(ByteBuf buf, DnsQueryFactory supplier) { + int id = buf.readUnsignedShort(); + int flags = buf.readUnsignedShort(); + if (flags >> 15 == 1) { + throw new CorruptedFrameException("not a query"); + } + + DnsQuery query = supplier.newQuery(id, DnsOpCode.valueOf((byte) (flags >> 11 & 0xf))); + query.setRecursionDesired((flags >> 8 & 1) == 1); + query.setZ(flags >> 4 & 0x7); + return query; + } + + private static void decodeQuestions(DnsRecordDecoder decoder, + DnsQuery query, ByteBuf buf, int questionCount) throws Exception { + for (int i = questionCount; i > 0; --i) { + query.addRecord(DnsSection.QUESTION, decoder.decodeQuestion(buf)); + } + } + + private static void decodeRecords(DnsRecordDecoder decoder, + DnsQuery query, DnsSection section, ByteBuf buf, int count) throws Exception { + for (int i = count; i > 0; --i) { + DnsRecord r = decoder.decodeRecord(buf); + if (r == null) { + break; + } + query.addRecord(section, r); + } + } + + static void encodeDnsResponse(DnsRecordEncoder encoder, DnsResponse response, ByteBuf buf) throws Exception { + boolean success = false; + try { + encodeHeader(response, buf); + encodeQuestions(encoder, response, buf); + encodeRecords(encoder, response, DnsSection.ANSWER, buf); + encodeRecords(encoder, response, DnsSection.AUTHORITY, buf); + encodeRecords(encoder, response, DnsSection.ADDITIONAL, buf); + success = true; + } finally { + if (!success) { + buf.release(); + } + } + } + + /** + * Encodes the header that is always 12 bytes long. + * + * @param response the response header being encoded + * @param buf the buffer the encoded data should be written to + */ + private static void encodeHeader(DnsResponse response, ByteBuf buf) { + buf.writeShort(response.id()); + int flags = 32768; + flags |= (response.opCode().byteValue() & 0xFF) << 11; + if (response.isAuthoritativeAnswer()) { + flags |= 1 << 10; + } + if (response.isTruncated()) { + flags |= 1 << 9; + } + if (response.isRecursionDesired()) { + flags |= 1 << 8; + } + if (response.isRecursionAvailable()) { + flags |= 1 << 7; + } + flags |= response.z() << 4; + flags |= response.code().intValue(); + buf.writeShort(flags); + buf.writeShort(response.count(DnsSection.QUESTION)); + buf.writeShort(response.count(DnsSection.ANSWER)); + buf.writeShort(response.count(DnsSection.AUTHORITY)); + buf.writeShort(response.count(DnsSection.ADDITIONAL)); + } + + private static void encodeQuestions(DnsRecordEncoder encoder, DnsResponse response, ByteBuf buf) throws Exception { + int count = response.count(DnsSection.QUESTION); + for (int i = 0; i < count; ++i) { + encoder.encodeQuestion(response.recordAt(DnsSection.QUESTION, i), buf); + } + } + + private static void encodeRecords(DnsRecordEncoder encoder, + DnsResponse response, DnsSection section, ByteBuf buf) throws Exception { + int count = response.count(section); + for (int i = 0; i < count; ++i) { + encoder.encodeRecord(response.recordAt(section, i), buf); + } + } + + interface DnsQueryFactory { + DnsQuery newQuery(int id, DnsOpCode dnsOpCode); + } + + private DnsMessageUtil() { + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOpCode.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOpCode.java new file mode 100644 index 0000000..8316ef4 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOpCode.java @@ -0,0 +1,123 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * The DNS {@code OpCode} as defined in RFC2929. + */ +@UnstableApi +public class DnsOpCode implements Comparable { + + /** + * The 'Query' DNS OpCode, as defined in RFC1035. + */ + public static final DnsOpCode QUERY = new DnsOpCode(0x00, "QUERY"); + + /** + * The 'IQuery' DNS OpCode, as defined in RFC1035. + */ + public static final DnsOpCode IQUERY = new DnsOpCode(0x01, "IQUERY"); + + /** + * The 'Status' DNS OpCode, as defined in RFC1035. + */ + public static final DnsOpCode STATUS = new DnsOpCode(0x02, "STATUS"); + + /** + * The 'Notify' DNS OpCode, as defined in RFC1996. + */ + public static final DnsOpCode NOTIFY = new DnsOpCode(0x04, "NOTIFY"); + + /** + * The 'Update' DNS OpCode, as defined in RFC2136. + */ + public static final DnsOpCode UPDATE = new DnsOpCode(0x05, "UPDATE"); + + /** + * Returns the {@link DnsOpCode} instance of the specified byte value. + */ + public static DnsOpCode valueOf(int b) { + switch (b) { + case 0x00: + return QUERY; + case 0x01: + return IQUERY; + case 0x02: + return STATUS; + case 0x04: + return NOTIFY; + case 0x05: + return UPDATE; + default: + break; + } + + return new DnsOpCode(b); + } + + private final byte byteValue; + private final String name; + private String text; + + private DnsOpCode(int byteValue) { + this(byteValue, "UNKNOWN"); + } + + public DnsOpCode(int byteValue, String name) { + this.byteValue = (byte) byteValue; + this.name = checkNotNull(name, "name"); + } + + public byte byteValue() { + return byteValue; + } + + @Override + public int hashCode() { + return byteValue; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof DnsOpCode)) { + return false; + } + + return byteValue == ((DnsOpCode) obj).byteValue; + } + + @Override + public int compareTo(DnsOpCode o) { + return byteValue - o.byteValue; + } + + @Override + public String toString() { + String text = this.text; + if (text == null) { + this.text = text = name + '(' + (byteValue & 0xFF) + ')'; + } + return text; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOptEcsRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOptEcsRecord.java new file mode 100644 index 0000000..d602dd6 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOptEcsRecord.java @@ -0,0 +1,43 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +import java.net.InetAddress; + +/** + * An ECS record as defined in Client Subnet in DNS Queries. + */ +@UnstableApi +public interface DnsOptEcsRecord extends DnsOptPseudoRecord { + + /** + * Returns the leftmost number of significant bits of ADDRESS to be used for the lookup. + */ + int sourcePrefixLength(); + + /** + * Returns the leftmost number of significant bits of ADDRESS that the response covers. + * In queries, it MUST be 0. + */ + int scopePrefixLength(); + + /** + * Returns the bytes of the {@link InetAddress} to use. + */ + byte[] address(); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOptPseudoRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOptPseudoRecord.java new file mode 100644 index 0000000..4269d80 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsOptPseudoRecord.java @@ -0,0 +1,44 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * An OPT RR record. + *

+ * This is used for Extension + * Mechanisms for DNS (EDNS(0)). + */ +@UnstableApi +public interface DnsOptPseudoRecord extends DnsRecord { + + /** + * Returns the {@code EXTENDED-RCODE} which is encoded into {@link DnsOptPseudoRecord#timeToLive()}. + */ + int extendedRcode(); + + /** + * Returns the {@code VERSION} which is encoded into {@link DnsOptPseudoRecord#timeToLive()}. + */ + int version(); + + /** + * Returns the {@code flags} which includes {@code DO} and {@code Z} which is encoded + * into {@link DnsOptPseudoRecord#timeToLive()}. + */ + int flags(); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsPtrRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsPtrRecord.java new file mode 100644 index 0000000..3ba5622 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsPtrRecord.java @@ -0,0 +1,28 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +@UnstableApi +public interface DnsPtrRecord extends DnsRecord { + + /** + * Returns the hostname this PTR record resolves to. + */ + String hostname(); + +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQuery.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQuery.java new file mode 100644 index 0000000..18b100d --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQuery.java @@ -0,0 +1,63 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * A DNS query message. + */ +@UnstableApi +public interface DnsQuery extends DnsMessage { + @Override + DnsQuery setId(int id); + + @Override + DnsQuery setOpCode(DnsOpCode opCode); + + @Override + DnsQuery setRecursionDesired(boolean recursionDesired); + + @Override + DnsQuery setZ(int z); + + @Override + DnsQuery setRecord(DnsSection section, DnsRecord record); + + @Override + DnsQuery addRecord(DnsSection section, DnsRecord record); + + @Override + DnsQuery addRecord(DnsSection section, int index, DnsRecord record); + + @Override + DnsQuery clear(DnsSection section); + + @Override + DnsQuery clear(); + + @Override + DnsQuery touch(); + + @Override + DnsQuery touch(Object hint); + + @Override + DnsQuery retain(); + + @Override + DnsQuery retain(int increment); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java new file mode 100644 index 0000000..fb9d780 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQueryEncoder.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +final class DnsQueryEncoder { + + private final DnsRecordEncoder recordEncoder; + + /** + * Creates a new encoder with the specified {@code recordEncoder}. + */ + DnsQueryEncoder(DnsRecordEncoder recordEncoder) { + this.recordEncoder = checkNotNull(recordEncoder, "recordEncoder"); + } + + /** + * Encodes the given {@link DnsQuery} into a {@link ByteBuf}. + */ + void encode(DnsQuery query, ByteBuf out) throws Exception { + encodeHeader(query, out); + encodeQuestions(query, out); + encodeRecords(query, DnsSection.ADDITIONAL, out); + } + + /** + * Encodes the header that is always 12 bytes long. + * + * @param query the query header being encoded + * @param buf the buffer the encoded data should be written to + */ + private static void encodeHeader(DnsQuery query, ByteBuf buf) { + buf.writeShort(query.id()); + int flags = 0; + flags |= (query.opCode().byteValue() & 0xFF) << 14; + if (query.isRecursionDesired()) { + flags |= 1 << 8; + } + buf.writeShort(flags); + buf.writeShort(query.count(DnsSection.QUESTION)); + buf.writeShort(0); // answerCount + buf.writeShort(0); // authorityResourceCount + buf.writeShort(query.count(DnsSection.ADDITIONAL)); + } + + private void encodeQuestions(DnsQuery query, ByteBuf buf) throws Exception { + final int count = query.count(DnsSection.QUESTION); + for (int i = 0; i < count; i++) { + recordEncoder.encodeQuestion((DnsQuestion) query.recordAt(DnsSection.QUESTION, i), buf); + } + } + + private void encodeRecords(DnsQuery query, DnsSection section, ByteBuf buf) throws Exception { + final int count = query.count(section); + for (int i = 0; i < count; i++) { + recordEncoder.encodeRecord(query.recordAt(section, i), buf); + } + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQuestion.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQuestion.java new file mode 100644 index 0000000..5469718 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsQuestion.java @@ -0,0 +1,30 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * A DNS question. + */ +@UnstableApi +public interface DnsQuestion extends DnsRecord { + /** + * An unused property. This method will always return {@code 0}. + */ + @Override + long timeToLive(); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRawRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRawRecord.java new file mode 100644 index 0000000..07f39a7 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRawRecord.java @@ -0,0 +1,50 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.util.internal.UnstableApi; + +/** + * A generic {@link DnsRecord} that contains an undecoded {@code RDATA}. + */ +@UnstableApi +public interface DnsRawRecord extends DnsRecord, ByteBufHolder { + @Override + DnsRawRecord copy(); + + @Override + DnsRawRecord duplicate(); + + @Override + DnsRawRecord retainedDuplicate(); + + @Override + DnsRawRecord replace(ByteBuf content); + + @Override + DnsRawRecord retain(); + + @Override + DnsRawRecord retain(int increment); + + @Override + DnsRawRecord touch(); + + @Override + DnsRawRecord touch(Object hint); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecord.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecord.java new file mode 100644 index 0000000..85630df --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecord.java @@ -0,0 +1,85 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * A DNS resource record. + */ +@UnstableApi +public interface DnsRecord { + + /** + * DNS resource record class: {@code IN} + */ + int CLASS_IN = 0x0001; + + /** + * DNS resource record class: {@code CSNET} + */ + int CLASS_CSNET = 0x0002; + + /** + * DNS resource record class: {@code CHAOS} + */ + int CLASS_CHAOS = 0x0003; + + /** + * DNS resource record class: {@code HESIOD} + */ + int CLASS_HESIOD = 0x0004; + + /** + * DNS resource record class: {@code NONE} + */ + int CLASS_NONE = 0x00fe; + + /** + * DNS resource record class: {@code ANY} + */ + int CLASS_ANY = 0x00ff; + + /** + * Returns the name of this resource record. + */ + String name(); + + /** + * Returns the type of this resource record. + */ + DnsRecordType type(); + + /** + * Returns the class of this resource record. + * + * @return the class value, usually one of the following: + *

    + *
  • {@link #CLASS_IN}
  • + *
  • {@link #CLASS_CSNET}
  • + *
  • {@link #CLASS_CHAOS}
  • + *
  • {@link #CLASS_HESIOD}
  • + *
  • {@link #CLASS_NONE}
  • + *
  • {@link #CLASS_ANY}
  • + *
+ */ + int dnsClass(); + + /** + * Returns the time to live after reading for this resource record. + */ + long timeToLive(); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordDecoder.java new file mode 100644 index 0000000..13c510e --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordDecoder.java @@ -0,0 +1,46 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.util.internal.UnstableApi; + +/** + * Decodes a DNS record into its object representation. + * + * @see DatagramDnsResponseDecoder + */ +@UnstableApi +public interface DnsRecordDecoder { + + DnsRecordDecoder DEFAULT = new DefaultDnsRecordDecoder(); + + /** + * Decodes a DNS question into its object representation. + * + * @param in the input buffer which contains a DNS question at its reader index + */ + DnsQuestion decodeQuestion(ByteBuf in) throws Exception; + + /** + * Decodes a DNS record into its object representation. + * + * @param in the input buffer which contains a DNS record at its reader index + * + * @return the decoded record, or {@code null} if there are not enough data in the input buffer + */ + T decodeRecord(ByteBuf in) throws Exception; +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordEncoder.java new file mode 100644 index 0000000..a5ce418 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordEncoder.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.util.internal.UnstableApi; + +/** + * Encodes a {@link DnsRecord} into binary representation. + * + * @see DatagramDnsQueryEncoder + */ +@UnstableApi +public interface DnsRecordEncoder { + + DnsRecordEncoder DEFAULT = new DefaultDnsRecordEncoder(); + + /** + * Encodes a {@link DnsQuestion}. + * + * @param out the output buffer where the encoded question will be written to + */ + void encodeQuestion(DnsQuestion question, ByteBuf out) throws Exception; + + /** + * Encodes a {@link DnsRecord}. + * + * @param out the output buffer where the encoded record will be written to + */ + void encodeRecord(DnsRecord record, ByteBuf out) throws Exception; +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordType.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordType.java new file mode 100644 index 0000000..ca9f6be --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsRecordType.java @@ -0,0 +1,404 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.collection.IntObjectHashMap; +import io.netty.util.internal.UnstableApi; + +import java.util.HashMap; +import java.util.Map; + +/** + * Represents a DNS record type. + */ +@UnstableApi +public class DnsRecordType implements Comparable { + + /** + * Address record RFC 1035 Returns a 32-bit IPv4 address, most commonly used + * to map hostnames to an IP address of the host, but also used for DNSBLs, + * storing subnet masks in RFC 1101, etc. + */ + public static final DnsRecordType A = new DnsRecordType(0x0001, "A"); + + /** + * Name server record RFC 1035 Delegates a DNS zone to use the given + * authoritative name servers + */ + public static final DnsRecordType NS = new DnsRecordType(0x0002, "NS"); + + /** + * Canonical name record RFC 1035 Alias of one name to another: the DNS + * lookup will continue by retrying the lookup with the new name. + */ + public static final DnsRecordType CNAME = new DnsRecordType(0x0005, "CNAME"); + + /** + * Start of [a zone of] authority record RFC 1035 and RFC 2308 Specifies + * authoritative information about a DNS zone, including the primary name + * server, the email of the domain administrator, the domain serial number, + * and several timers relating to refreshing the zone. + */ + public static final DnsRecordType SOA = new DnsRecordType(0x0006, "SOA"); + + /** + * Pointer record RFC 1035 Pointer to a canonical name. Unlike a CNAME, DNS + * processing does NOT proceed, just the name is returned. The most common + * use is for implementing reverse DNS lookups, but other uses include such + * things as DNS-SD. + */ + public static final DnsRecordType PTR = new DnsRecordType(0x000c, "PTR"); + + /** + * Mail exchange record RFC 1035 Maps a domain name to a list of message + * transfer agents for that domain. + */ + public static final DnsRecordType MX = new DnsRecordType(0x000f, "MX"); + + /** + * Text record RFC 1035 Originally for arbitrary human-readable text in a + * DNS record. Since the early 1990s, however, this record more often + * carries machine-readable data, such as specified by RFC 1464, + * opportunistic encryption, Sender Policy Framework, DKIM, DMARC DNS-SD, + * etc. + */ + public static final DnsRecordType TXT = new DnsRecordType(0x0010, "TXT"); + + /** + * Responsible person record RFC 1183 Information about the responsible + * person(s) for the domain. Usually an email address with the @ replaced by + * a . + */ + public static final DnsRecordType RP = new DnsRecordType(0x0011, "RP"); + + /** + * AFS database record RFC 1183 Location of database servers of an AFS cell. + * This record is commonly used by AFS clients to contact AFS cells outside + * their local domain. A subtype of this record is used by the obsolete + * DCE/DFS file system. + */ + public static final DnsRecordType AFSDB = new DnsRecordType(0x0012, "AFSDB"); + + /** + * Signature record RFC 2535 Signature record used in SIG(0) (RFC 2931) and + * TKEY (RFC 2930). RFC 3755 designated RRSIG as the replacement for SIG for + * use within DNSSEC. + */ + public static final DnsRecordType SIG = new DnsRecordType(0x0018, "SIG"); + + /** + * key record RFC 2535 and RFC 2930 Used only for SIG(0) (RFC 2931) and TKEY + * (RFC 2930). RFC 3445 eliminated their use for application keys and + * limited their use to DNSSEC. RFC 3755 designates DNSKEY as the + * replacement within DNSSEC. RFC 4025 designates IPSECKEY as the + * replacement for use with IPsec. + */ + public static final DnsRecordType KEY = new DnsRecordType(0x0019, "KEY"); + + /** + * IPv6 address record RFC 3596 Returns a 128-bit IPv6 address, most + * commonly used to map hostnames to an IP address of the host. + */ + public static final DnsRecordType AAAA = new DnsRecordType(0x001c, "AAAA"); + + /** + * Location record RFC 1876 Specifies a geographical location associated + * with a domain name. + */ + public static final DnsRecordType LOC = new DnsRecordType(0x001d, "LOC"); + + /** + * Service locator RFC 2782 Generalized service location record, used for + * newer protocols instead of creating protocol-specific records such as MX. + */ + public static final DnsRecordType SRV = new DnsRecordType(0x0021, "SRV"); + + /** + * Naming Authority Pointer record RFC 3403 Allows regular expression based + * rewriting of domain names which can then be used as URIs, further domain + * names to lookups, etc. + */ + public static final DnsRecordType NAPTR = new DnsRecordType(0x0023, "NAPTR"); + + /** + * Key eXchanger record RFC 2230 Used with some cryptographic systems (not + * including DNSSEC) to identify a key management agent for the associated + * domain-name. Note that this has nothing to do with DNS Security. It is + * Informational status, rather than being on the IETF standards-track. It + * has always had limited deployment, but is still in use. + */ + public static final DnsRecordType KX = new DnsRecordType(0x0024, "KX"); + + /** + * Certificate record RFC 4398 Stores PKIX, SPKI, PGP, etc. + */ + public static final DnsRecordType CERT = new DnsRecordType(0x0025, "CERT"); + + /** + * Delegation name record RFC 2672 DNAME creates an alias for a name and all + * its subnames, unlike CNAME, which aliases only the exact name in its + * label. Like the CNAME record, the DNS lookup will continue by retrying + * the lookup with the new name. + */ + public static final DnsRecordType DNAME = new DnsRecordType(0x0027, "DNAME"); + + /** + * Option record RFC 2671 This is a pseudo DNS record type needed to support + * EDNS. + */ + public static final DnsRecordType OPT = new DnsRecordType(0x0029, "OPT"); + + /** + * Address Prefix List record RFC 3123 Specify lists of address ranges, e.g. + * in CIDR format, for various address families. Experimental. + */ + public static final DnsRecordType APL = new DnsRecordType(0x002a, "APL"); + + /** + * Delegation signer record RFC 4034 The record used to identify the DNSSEC + * signing key of a delegated zone. + */ + public static final DnsRecordType DS = new DnsRecordType(0x002b, "DS"); + + /** + * SSH Public Key Fingerprint record RFC 4255 Resource record for publishing + * SSH public host key fingerprints in the DNS System, in order to aid in + * verifying the authenticity of the host. RFC 6594 defines ECC SSH keys and + * SHA-256 hashes. See the IANA SSHFP RR parameters registry for details. + */ + public static final DnsRecordType SSHFP = new DnsRecordType(0x002c, "SSHFP"); + + /** + * IPsec Key record RFC 4025 Key record that can be used with IPsec. + */ + public static final DnsRecordType IPSECKEY = new DnsRecordType(0x002d, "IPSECKEY"); + + /** + * DNSSEC signature record RFC 4034 Signature for a DNSSEC-secured record + * set. Uses the same format as the SIG record. + */ + public static final DnsRecordType RRSIG = new DnsRecordType(0x002e, "RRSIG"); + + /** + * Next-Secure record RFC 4034 Part of DNSSEC, used to prove a name does not + * exist. Uses the same format as the (obsolete) NXT record. + */ + public static final DnsRecordType NSEC = new DnsRecordType(0x002f, "NSEC"); + + /** + * DNS Key record RFC 4034 The key record used in DNSSEC. Uses the same + * format as the KEY record. + */ + public static final DnsRecordType DNSKEY = new DnsRecordType(0x0030, "DNSKEY"); + + /** + * DHCP identifier record RFC 4701 Used in conjunction with the FQDN option + * to DHCP. + */ + public static final DnsRecordType DHCID = new DnsRecordType(0x0031, "DHCID"); + + /** + * NSEC record version 3 RFC 5155 An extension to DNSSEC that allows proof + * of nonexistence for a name without permitting zonewalking. + */ + public static final DnsRecordType NSEC3 = new DnsRecordType(0x0032, "NSEC3"); + + /** + * NSEC3 parameters record RFC 5155 Parameter record for use with NSEC3. + */ + public static final DnsRecordType NSEC3PARAM = new DnsRecordType(0x0033, "NSEC3PARAM"); + + /** + * TLSA certificate association record RFC 6698 A record for DNS-based + * Authentication of Named Entities (DANE). RFC 6698 defines The TLSA DNS + * resource record is used to associate a TLS server certificate or public + * key with the domain name where the record is found, thus forming a 'TLSA + * certificate association'. + */ + public static final DnsRecordType TLSA = new DnsRecordType(0x0034, "TLSA"); + + /** + * Host Identity Protocol record RFC 5205 Method of separating the end-point + * identifier and locator roles of IP addresses. + */ + public static final DnsRecordType HIP = new DnsRecordType(0x0037, "HIP"); + + /** + * Sender Policy Framework record RFC 4408 Specified as part of the SPF + * protocol as an alternative to of storing SPF data in TXT records. Uses + * the same format as the earlier TXT record. + */ + public static final DnsRecordType SPF = new DnsRecordType(0x0063, "SPF"); + + /** + * Secret key record RFC 2930 A method of providing keying material to be + * used with TSIG that is encrypted under the public key in an accompanying + * KEY RR.. + */ + public static final DnsRecordType TKEY = new DnsRecordType(0x00f9, "TKEY"); + + /** + * Transaction Signature record RFC 2845 Can be used to authenticate dynamic + * updates as coming from an approved client, or to authenticate responses + * as coming from an approved recursive name server similar to DNSSEC. + */ + public static final DnsRecordType TSIG = new DnsRecordType(0x00fa, "TSIG"); + + /** + * Incremental Zone Transfer record RFC 1996 Requests a zone transfer of the + * given zone but only differences from a previous serial number. This + * request may be ignored and a full (AXFR) sent in response if the + * authoritative server is unable to fulfill the request due to + * configuration or lack of required deltas. + */ + public static final DnsRecordType IXFR = new DnsRecordType(0x00fb, "IXFR"); + + /** + * Authoritative Zone Transfer record RFC 1035 Transfer entire zone file + * from the master name server to secondary name servers. + */ + public static final DnsRecordType AXFR = new DnsRecordType(0x00fc, "AXFR"); + + /** + * All cached records RFC 1035 Returns all records of all types known to the + * name server. If the name server does not have any information on the + * name, the request will be forwarded on. The records returned may not be + * complete. For example, if there is both an A and an MX for a name, but + * the name server has only the A record cached, only the A record will be + * returned. Sometimes referred to as ANY, for example in Windows nslookup + * and Wireshark. + */ + public static final DnsRecordType ANY = new DnsRecordType(0x00ff, "ANY"); + + /** + * Certification Authority Authorization record RFC 6844 CA pinning, + * constraining acceptable CAs for a host/domain. + */ + public static final DnsRecordType CAA = new DnsRecordType(0x0101, "CAA"); + + /** + * DNSSEC Trust Authorities record N/A Part of a deployment proposal for + * DNSSEC without a signed DNS root. See the IANA database and Weiler Spec + * for details. Uses the same format as the DS record. + */ + public static final DnsRecordType TA = new DnsRecordType(0x8000, "TA"); + + /** + * DNSSEC Lookaside Validation record RFC 4431 For publishing DNSSEC trust + * anchors outside of the DNS delegation chain. Uses the same format as the + * DS record. RFC 5074 describes a way of using these records. + */ + public static final DnsRecordType DLV = new DnsRecordType(0x8001, "DLV"); + + private static final Map BY_NAME = new HashMap(); + private static final IntObjectHashMap BY_TYPE = new IntObjectHashMap(); + private static final String EXPECTED; + + static { + DnsRecordType[] all = { + A, NS, CNAME, SOA, PTR, MX, TXT, RP, AFSDB, SIG, KEY, AAAA, LOC, SRV, NAPTR, KX, CERT, DNAME, OPT, APL, + DS, SSHFP, IPSECKEY, RRSIG, NSEC, DNSKEY, DHCID, NSEC3, NSEC3PARAM, TLSA, HIP, SPF, TKEY, TSIG, IXFR, + AXFR, ANY, CAA, TA, DLV + }; + + final StringBuilder expected = new StringBuilder(512); + + expected.append(" (expected: "); + for (DnsRecordType type: all) { + BY_NAME.put(type.name(), type); + BY_TYPE.put(type.intValue(), type); + + expected.append(type.name()) + .append('(') + .append(type.intValue()) + .append("), "); + } + + expected.setLength(expected.length() - 2); + expected.append(')'); + EXPECTED = expected.toString(); + } + + public static DnsRecordType valueOf(int intValue) { + DnsRecordType result = BY_TYPE.get(intValue); + if (result == null) { + return new DnsRecordType(intValue); + } + return result; + } + + public static DnsRecordType valueOf(String name) { + DnsRecordType result = BY_NAME.get(name); + if (result == null) { + throw new IllegalArgumentException("name: " + name + EXPECTED); + } + return result; + } + + private final int intValue; + private final String name; + private String text; + + private DnsRecordType(int intValue) { + this(intValue, "UNKNOWN"); + } + + public DnsRecordType(int intValue, String name) { + if ((intValue & 0xffff) != intValue) { + throw new IllegalArgumentException("intValue: " + intValue + " (expected: 0 ~ 65535)"); + } + this.intValue = intValue; + this.name = name; + } + + /** + * Returns the name of this type, as seen in bind config files + */ + public String name() { + return name; + } + + /** + * Returns the value of this DnsType as it appears in DNS protocol + */ + public int intValue() { + return intValue; + } + + @Override + public int hashCode() { + return intValue; + } + + @Override + public boolean equals(Object o) { + return o instanceof DnsRecordType && ((DnsRecordType) o).intValue == intValue; + } + + @Override + public int compareTo(DnsRecordType o) { + return intValue() - o.intValue(); + } + + @Override + public String toString() { + String text = this.text; + if (text == null) { + this.text = text = name + '(' + intValue() + ')'; + } + return text; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponse.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponse.java new file mode 100644 index 0000000..537e216 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponse.java @@ -0,0 +1,116 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * A DNS response message. + */ +@UnstableApi +public interface DnsResponse extends DnsMessage { + + /** + * Returns {@code true} if responding server is authoritative for the domain + * name in the query message. + */ + boolean isAuthoritativeAnswer(); + + /** + * Set to {@code true} if responding server is authoritative for the domain + * name in the query message. + * + * @param authoritativeAnswer flag for authoritative answer + */ + DnsResponse setAuthoritativeAnswer(boolean authoritativeAnswer); + + /** + * Returns {@code true} if response has been truncated, usually if it is + * over 512 bytes. + */ + boolean isTruncated(); + + /** + * Set to {@code true} if response has been truncated (usually happens for + * responses over 512 bytes). + * + * @param truncated flag for truncation + */ + DnsResponse setTruncated(boolean truncated); + + /** + * Returns {@code true} if DNS server can handle recursive queries. + */ + boolean isRecursionAvailable(); + + /** + * Set to {@code true} if DNS server can handle recursive queries. + * + * @param recursionAvailable flag for recursion availability + */ + DnsResponse setRecursionAvailable(boolean recursionAvailable); + + /** + * Returns the 4 bit return code. + */ + DnsResponseCode code(); + + /** + * Sets the response code for this message. + * + * @param code the response code + */ + DnsResponse setCode(DnsResponseCode code); + + @Override + DnsResponse setId(int id); + + @Override + DnsResponse setOpCode(DnsOpCode opCode); + + @Override + DnsResponse setRecursionDesired(boolean recursionDesired); + + @Override + DnsResponse setZ(int z); + + @Override + DnsResponse setRecord(DnsSection section, DnsRecord record); + + @Override + DnsResponse addRecord(DnsSection section, DnsRecord record); + + @Override + DnsResponse addRecord(DnsSection section, int index, DnsRecord record); + + @Override + DnsResponse clear(DnsSection section); + + @Override + DnsResponse clear(); + + @Override + DnsResponse touch(); + + @Override + DnsResponse touch(Object hint); + + @Override + DnsResponse retain(); + + @Override + DnsResponse retain(int increment); +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseCode.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseCode.java new file mode 100644 index 0000000..0d9b40e --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseCode.java @@ -0,0 +1,219 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * The DNS {@code RCODE}, as defined in RFC2929. + */ +@UnstableApi +public class DnsResponseCode implements Comparable { + + /** + * The 'NoError' DNS RCODE (0), as defined in RFC1035. + */ + public static final DnsResponseCode NOERROR = new DnsResponseCode(0, "NoError"); + + /** + * The 'FormErr' DNS RCODE (1), as defined in RFC1035. + */ + public static final DnsResponseCode FORMERR = new DnsResponseCode(1, "FormErr"); + + /** + * The 'ServFail' DNS RCODE (2), as defined in RFC1035. + */ + public static final DnsResponseCode SERVFAIL = new DnsResponseCode(2, "ServFail"); + + /** + * The 'NXDomain' DNS RCODE (3), as defined in RFC1035. + */ + public static final DnsResponseCode NXDOMAIN = new DnsResponseCode(3, "NXDomain"); + + /** + * The 'NotImp' DNS RCODE (4), as defined in RFC1035. + */ + public static final DnsResponseCode NOTIMP = new DnsResponseCode(4, "NotImp"); + + /** + * The 'Refused' DNS RCODE (5), as defined in RFC1035. + */ + public static final DnsResponseCode REFUSED = new DnsResponseCode(5, "Refused"); + + /** + * The 'YXDomain' DNS RCODE (6), as defined in RFC2136. + */ + public static final DnsResponseCode YXDOMAIN = new DnsResponseCode(6, "YXDomain"); + + /** + * The 'YXRRSet' DNS RCODE (7), as defined in RFC2136. + */ + public static final DnsResponseCode YXRRSET = new DnsResponseCode(7, "YXRRSet"); + + /** + * The 'NXRRSet' DNS RCODE (8), as defined in RFC2136. + */ + public static final DnsResponseCode NXRRSET = new DnsResponseCode(8, "NXRRSet"); + + /** + * The 'NotAuth' DNS RCODE (9), as defined in RFC2136. + */ + public static final DnsResponseCode NOTAUTH = new DnsResponseCode(9, "NotAuth"); + + /** + * The 'NotZone' DNS RCODE (10), as defined in RFC2136. + */ + public static final DnsResponseCode NOTZONE = new DnsResponseCode(10, "NotZone"); + + /** + * The 'BADVERS' or 'BADSIG' DNS RCODE (16), as defined in RFC2671 + * and RFC2845. + */ + public static final DnsResponseCode BADVERS_OR_BADSIG = new DnsResponseCode(16, "BADVERS_OR_BADSIG"); + + /** + * The 'BADKEY' DNS RCODE (17), as defined in RFC2845. + */ + public static final DnsResponseCode BADKEY = new DnsResponseCode(17, "BADKEY"); + + /** + * The 'BADTIME' DNS RCODE (18), as defined in RFC2845. + */ + public static final DnsResponseCode BADTIME = new DnsResponseCode(18, "BADTIME"); + + /** + * The 'BADMODE' DNS RCODE (19), as defined in RFC2930. + */ + public static final DnsResponseCode BADMODE = new DnsResponseCode(19, "BADMODE"); + + /** + * The 'BADNAME' DNS RCODE (20), as defined in RFC2930. + */ + public static final DnsResponseCode BADNAME = new DnsResponseCode(20, "BADNAME"); + + /** + * The 'BADALG' DNS RCODE (21), as defined in RFC2930. + */ + public static final DnsResponseCode BADALG = new DnsResponseCode(21, "BADALG"); + + /** + * Returns the {@link DnsResponseCode} that corresponds with the given {@code responseCode}. + * + * @param responseCode the DNS RCODE + * + * @return the corresponding {@link DnsResponseCode} + */ + public static DnsResponseCode valueOf(int responseCode) { + switch (responseCode) { + case 0: + return NOERROR; + case 1: + return FORMERR; + case 2: + return SERVFAIL; + case 3: + return NXDOMAIN; + case 4: + return NOTIMP; + case 5: + return REFUSED; + case 6: + return YXDOMAIN; + case 7: + return YXRRSET; + case 8: + return NXRRSET; + case 9: + return NOTAUTH; + case 10: + return NOTZONE; + case 16: + return BADVERS_OR_BADSIG; + case 17: + return BADKEY; + case 18: + return BADTIME; + case 19: + return BADMODE; + case 20: + return BADNAME; + case 21: + return BADALG; + default: + return new DnsResponseCode(responseCode); + } + } + + private final int code; + private final String name; + private String text; + + private DnsResponseCode(int code) { + this(code, "UNKNOWN"); + } + + public DnsResponseCode(int code, String name) { + if (code < 0 || code > 65535) { + throw new IllegalArgumentException("code: " + code + " (expected: 0 ~ 65535)"); + } + + this.code = code; + this.name = checkNotNull(name, "name"); + } + + /** + * Returns the error code for this {@link DnsResponseCode}. + */ + public int intValue() { + return code; + } + + @Override + public int compareTo(DnsResponseCode o) { + return intValue() - o.intValue(); + } + + @Override + public int hashCode() { + return intValue(); + } + + /** + * Equality of {@link DnsResponseCode} only depends on {@link #intValue()}. + */ + @Override + public boolean equals(Object o) { + if (!(o instanceof DnsResponseCode)) { + return false; + } + + return intValue() == ((DnsResponseCode) o).intValue(); + } + + /** + * Returns a formatted error message for this {@link DnsResponseCode}. + */ + @Override + public String toString() { + String text = this.text; + if (text == null) { + this.text = text = name + '(' + intValue() + ')'; + } + return text; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java new file mode 100644 index 0000000..3eedccc --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsResponseDecoder.java @@ -0,0 +1,105 @@ +/* + * Copyright 2019 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.handler.codec.CorruptedFrameException; + +import java.net.SocketAddress; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +abstract class DnsResponseDecoder { + + private final DnsRecordDecoder recordDecoder; + + /** + * Creates a new decoder with the specified {@code recordDecoder}. + */ + DnsResponseDecoder(DnsRecordDecoder recordDecoder) { + this.recordDecoder = checkNotNull(recordDecoder, "recordDecoder"); + } + + final DnsResponse decode(A sender, A recipient, ByteBuf buffer) throws Exception { + final int id = buffer.readUnsignedShort(); + + final int flags = buffer.readUnsignedShort(); + if (flags >> 15 == 0) { + throw new CorruptedFrameException("not a response"); + } + + final DnsResponse response = newResponse( + sender, + recipient, + id, + DnsOpCode.valueOf((byte) (flags >> 11 & 0xf)), DnsResponseCode.valueOf((byte) (flags & 0xf))); + + response.setRecursionDesired((flags >> 8 & 1) == 1); + response.setAuthoritativeAnswer((flags >> 10 & 1) == 1); + response.setTruncated((flags >> 9 & 1) == 1); + response.setRecursionAvailable((flags >> 7 & 1) == 1); + response.setZ(flags >> 4 & 0x7); + + boolean success = false; + try { + final int questionCount = buffer.readUnsignedShort(); + final int answerCount = buffer.readUnsignedShort(); + final int authorityRecordCount = buffer.readUnsignedShort(); + final int additionalRecordCount = buffer.readUnsignedShort(); + + decodeQuestions(response, buffer, questionCount); + if (!decodeRecords(response, DnsSection.ANSWER, buffer, answerCount)) { + success = true; + return response; + } + if (!decodeRecords(response, DnsSection.AUTHORITY, buffer, authorityRecordCount)) { + success = true; + return response; + } + + decodeRecords(response, DnsSection.ADDITIONAL, buffer, additionalRecordCount); + success = true; + return response; + } finally { + if (!success) { + response.release(); + } + } + } + + protected abstract DnsResponse newResponse(A sender, A recipient, int id, + DnsOpCode opCode, DnsResponseCode responseCode) throws Exception; + + private void decodeQuestions(DnsResponse response, ByteBuf buf, int questionCount) throws Exception { + for (int i = questionCount; i > 0; i --) { + response.addRecord(DnsSection.QUESTION, recordDecoder.decodeQuestion(buf)); + } + } + + private boolean decodeRecords( + DnsResponse response, DnsSection section, ByteBuf buf, int count) throws Exception { + for (int i = count; i > 0; i --) { + final DnsRecord r = recordDecoder.decodeRecord(buf); + if (r == null) { + // Truncated response + return false; + } + + response.addRecord(section, r); + } + return true; + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsSection.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsSection.java new file mode 100644 index 0000000..fcf9e76 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/DnsSection.java @@ -0,0 +1,41 @@ +/* + * Copyright 2015 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: + * + * https://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.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; + +/** + * Represents a section of a {@link DnsMessage}. + */ +@UnstableApi +public enum DnsSection { + /** + * The section that contains {@link DnsQuestion}s. + */ + QUESTION, + /** + * The section that contains the answer {@link DnsRecord}s. + */ + ANSWER, + /** + * The section that contains the authority {@link DnsRecord}s. + */ + AUTHORITY, + /** + * The section that contains the additional {@link DnsRecord}s. + */ + ADDITIONAL +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryDecoder.java new file mode 100644 index 0000000..98d0a01 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryDecoder.java @@ -0,0 +1,57 @@ +/* + * Copyright 2021 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.UnstableApi; + +@UnstableApi +public final class TcpDnsQueryDecoder extends LengthFieldBasedFrameDecoder { + private final DnsRecordDecoder decoder; + + /** + * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}. + */ + public TcpDnsQueryDecoder() { + this(DnsRecordDecoder.DEFAULT, 65535); + } + + /** + * Creates a new decoder with the specified {@code decoder}. + */ + public TcpDnsQueryDecoder(DnsRecordDecoder decoder, int maxFrameLength) { + super(maxFrameLength, 0, 2, 0, 2); + this.decoder = ObjectUtil.checkNotNull(decoder, "decoder"); + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, in); + if (frame == null) { + return null; + } + + return DnsMessageUtil.decodeDnsQuery(decoder, frame.slice(), new DnsMessageUtil.DnsQueryFactory() { + @Override + public DnsQuery newQuery(int id, DnsOpCode dnsOpCode) { + return new DefaultDnsQuery(id, dnsOpCode); + } + }); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java new file mode 100644 index 0000000..401d3b4 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsQueryEncoder.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToByteEncoder; +import io.netty.util.internal.UnstableApi; + +@ChannelHandler.Sharable +@UnstableApi +public final class TcpDnsQueryEncoder extends MessageToByteEncoder { + + private final DnsQueryEncoder encoder; + + /** + * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}. + */ + public TcpDnsQueryEncoder() { + this(DnsRecordEncoder.DEFAULT); + } + + /** + * Creates a new encoder with the specified {@code recordEncoder}. + */ + public TcpDnsQueryEncoder(DnsRecordEncoder recordEncoder) { + this.encoder = new DnsQueryEncoder(recordEncoder); + } + + @Override + protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception { + // Length is two octets as defined by RFC-7766 + // See https://tools.ietf.org/html/rfc7766#section-8 + out.writerIndex(out.writerIndex() + 2); + encoder.encode(msg, out); + + // Now fill in the correct length based on the amount of data that we wrote the ByteBuf. + out.setShort(0, out.readableBytes() - 2); + } + + @Override + protected ByteBuf allocateBuffer(ChannelHandlerContext ctx, @SuppressWarnings("unused") DnsQuery msg, + boolean preferDirect) { + if (preferDirect) { + return ctx.alloc().ioBuffer(1024); + } else { + return ctx.alloc().heapBuffer(1024); + } + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java new file mode 100644 index 0000000..126a820 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseDecoder.java @@ -0,0 +1,72 @@ +/* + * Copyright 2019 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.LengthFieldBasedFrameDecoder; +import io.netty.util.internal.UnstableApi; + +import java.net.SocketAddress; + +@UnstableApi +public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder { + + private final DnsResponseDecoder responseDecoder; + + /** + * Creates a new decoder with {@linkplain DnsRecordDecoder#DEFAULT the default record decoder}. + */ + public TcpDnsResponseDecoder() { + this(DnsRecordDecoder.DEFAULT, 64 * 1024); + } + + /** + * Creates a new decoder with the specified {@code recordDecoder} and {@code maxFrameLength} + */ + public TcpDnsResponseDecoder(DnsRecordDecoder recordDecoder, int maxFrameLength) { + // Length is two octets as defined by RFC-7766 + // See https://tools.ietf.org/html/rfc7766#section-8 + super(maxFrameLength, 0, 2, 0, 2); + + this.responseDecoder = new DnsResponseDecoder(recordDecoder) { + @Override + protected DnsResponse newResponse(SocketAddress sender, SocketAddress recipient, + int id, DnsOpCode opCode, DnsResponseCode responseCode) { + return new DefaultDnsResponse(id, opCode, responseCode); + } + }; + } + + @Override + protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception { + ByteBuf frame = (ByteBuf) super.decode(ctx, in); + if (frame == null) { + return null; + } + + try { + return responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice()); + } finally { + frame.release(); + } + } + + @Override + protected ByteBuf extractFrame(ChannelHandlerContext ctx, ByteBuf buffer, int index, int length) { + return buffer.copy(index, length); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseEncoder.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseEncoder.java new file mode 100644 index 0000000..2a9d5d0 --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/TcpDnsResponseEncoder.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerContext; +import io.netty.handler.codec.MessageToMessageEncoder; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.UnstableApi; + +import java.util.List; + +@UnstableApi +@ChannelHandler.Sharable +public final class TcpDnsResponseEncoder extends MessageToMessageEncoder { + private final DnsRecordEncoder encoder; + + /** + * Creates a new encoder with {@linkplain DnsRecordEncoder#DEFAULT the default record encoder}. + */ + public TcpDnsResponseEncoder() { + this(DnsRecordEncoder.DEFAULT); + } + + /** + * Creates a new encoder with the specified {@code encoder}. + */ + public TcpDnsResponseEncoder(DnsRecordEncoder encoder) { + this.encoder = ObjectUtil.checkNotNull(encoder, "encoder"); + } + + @Override + protected void encode(ChannelHandlerContext ctx, DnsResponse response, List out) throws Exception { + ByteBuf buf = ctx.alloc().ioBuffer(1024); + + buf.writerIndex(buf.writerIndex() + 2); + DnsMessageUtil.encodeDnsResponse(encoder, response, buf); + buf.setShort(0, buf.readableBytes() - 2); + + out.add(buf); + } +} diff --git a/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/package-info.java b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/package-info.java new file mode 100644 index 0000000..fd3d6bc --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/io/netty/handler/codec/dns/package-info.java @@ -0,0 +1,23 @@ +/* + * Copyright 2015 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: + * + * https://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. + */ + +/** + * DNS codec. + */ +@UnstableApi +package io.netty.handler.codec.dns; + +import io.netty.util.internal.UnstableApi; diff --git a/netty-handler-codec-dns/src/main/java/module-info.java b/netty-handler-codec-dns/src/main/java/module-info.java new file mode 100644 index 0000000..caf28bf --- /dev/null +++ b/netty-handler-codec-dns/src/main/java/module-info.java @@ -0,0 +1,8 @@ +module org.xbib.io.netty.handler.codec.dns { + exports io.netty.handler.codec.dns; + requires org.xbib.io.netty.buffer; + requires org.xbib.io.netty.channel; + requires org.xbib.io.netty.handler; + requires org.xbib.io.netty.handler.codec; + requires org.xbib.io.netty.util; +} diff --git a/netty-handler-codec-dns/src/main/resources/META-INF/native-image/io.netty/netty-codec-dns/generated/handlers/reflect-config.json b/netty-handler-codec-dns/src/main/resources/META-INF/native-image/io.netty/netty-codec-dns/generated/handlers/reflect-config.json new file mode 100644 index 0000000..a160b19 --- /dev/null +++ b/netty-handler-codec-dns/src/main/resources/META-INF/native-image/io.netty/netty-codec-dns/generated/handlers/reflect-config.json @@ -0,0 +1,58 @@ +[ + { + "name": "io.netty.handler.codec.dns.DatagramDnsQueryDecoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.DatagramDnsQueryDecoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.DatagramDnsQueryEncoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.DatagramDnsQueryEncoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.DatagramDnsResponseDecoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.DatagramDnsResponseDecoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.DatagramDnsResponseEncoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.DatagramDnsResponseEncoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.TcpDnsQueryDecoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.TcpDnsQueryDecoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.TcpDnsQueryEncoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.TcpDnsQueryEncoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.TcpDnsResponseDecoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.TcpDnsResponseDecoder" + }, + "queryAllPublicMethods": true + }, + { + "name": "io.netty.handler.codec.dns.TcpDnsResponseEncoder", + "condition": { + "typeReachable": "io.netty.handler.codec.dns.TcpDnsResponseEncoder" + }, + "queryAllPublicMethods": true + } +] \ No newline at end of file diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/AbstractDnsRecordTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/AbstractDnsRecordTest.java new file mode 100644 index 0000000..f9c4a4f --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/AbstractDnsRecordTest.java @@ -0,0 +1,75 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class AbstractDnsRecordTest { + + @Test + public void testValidDomainName() { + String name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + AbstractDnsRecord record = new AbstractDnsRecord(name, DnsRecordType.A, 0) { }; + assertEquals(name + '.', record.name()); + } + + @Test + public void testValidDomainNameUmlaut() { + String name = "ä"; + AbstractDnsRecord record = new AbstractDnsRecord(name, DnsRecordType.A, 0) { }; + assertEquals("xn--4ca.", record.name()); + } + + @Test + public void testValidDomainNameTrailingDot() { + String name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa."; + AbstractDnsRecord record = new AbstractDnsRecord(name, DnsRecordType.A, 0) { }; + assertEquals(name, record.name()); + } + + @Test + public void testValidDomainNameUmlautTrailingDot() { + String name = "ä."; + AbstractDnsRecord record = new AbstractDnsRecord(name, DnsRecordType.A, 0) { }; + assertEquals("xn--4ca.", record.name()); + } + + @Test + public void testValidDomainNameLength() { + final String name = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + assertThrows(IllegalArgumentException.class, new Executable() { + @Override + public void execute() { + new AbstractDnsRecord(name, DnsRecordType.A, 0) { }; + } + }); + } + + @Test + public void testValidDomainNameUmlautLength() { + final String name = "äaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; + assertThrows(IllegalArgumentException.class, new Executable() { + @Override + public void execute() { + new AbstractDnsRecord(name, DnsRecordType.A, 0) { }; + } + }); + } +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java new file mode 100644 index 0000000..a8379f6 --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordDecoderTest.java @@ -0,0 +1,256 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufUtil; +import io.netty.buffer.Unpooled; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DefaultDnsRecordDecoderTest { + + @Test + public void testDecodeName() { + testDecodeName("netty.io.", Unpooled.wrappedBuffer(new byte[] { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0 + })); + } + + @Test + public void testDecodeNameWithoutTerminator() { + testDecodeName("netty.io.", Unpooled.wrappedBuffer(new byte[] { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o' + })); + } + + @Test + public void testDecodeNameWithExtraTerminator() { + // Should not be decoded as 'netty.io..' + testDecodeName("netty.io.", Unpooled.wrappedBuffer(new byte[] { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0, 0 + })); + } + + @Test + public void testDecodeEmptyName() { + testDecodeName(".", Unpooled.buffer().writeByte(0)); + } + + @Test + public void testDecodeEmptyNameFromEmptyBuffer() { + testDecodeName(".", Unpooled.EMPTY_BUFFER); + } + + @Test + public void testDecodeEmptyNameFromExtraZeroes() { + testDecodeName(".", Unpooled.wrappedBuffer(new byte[] { 0, 0 })); + } + + private static void testDecodeName(String expected, ByteBuf buffer) { + try { + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + assertEquals(expected, decoder.decodeName0(buffer)); + } finally { + buffer.release(); + } + } + + @Test + public void testDecodePtrRecord() throws Exception { + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + ByteBuf buffer = Unpooled.buffer().writeByte(0); + int readerIndex = buffer.readerIndex(); + int writerIndex = buffer.writerIndex(); + try { + DnsPtrRecord record = (DnsPtrRecord) decoder.decodeRecord( + "netty.io", DnsRecordType.PTR, DnsRecord.CLASS_IN, 60, buffer, 0, 1); + assertEquals("netty.io.", record.name()); + assertEquals(DnsRecord.CLASS_IN, record.dnsClass()); + assertEquals(60, record.timeToLive()); + assertEquals(DnsRecordType.PTR, record.type()); + assertEquals(readerIndex, buffer.readerIndex()); + assertEquals(writerIndex, buffer.writerIndex()); + } finally { + buffer.release(); + } + } + + @Test + public void testdecompressCompressPointer() { + byte[] compressionPointer = { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0, + (byte) 0xC0, 0 + }; + ByteBuf buffer = Unpooled.wrappedBuffer(compressionPointer); + ByteBuf uncompressed = null; + try { + uncompressed = DnsCodecUtil.decompressDomainName(buffer.duplicate().setIndex(10, 12)); + assertEquals(0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), uncompressed)); + } finally { + buffer.release(); + if (uncompressed != null) { + uncompressed.release(); + } + } + } + + @Test + public void testdecompressNestedCompressionPointer() { + byte[] nestedCompressionPointer = { + 6, 'g', 'i', 't', 'h', 'u', 'b', 2, 'i', 'o', 0, // github.io + 5, 'n', 'e', 't', 't', 'y', (byte) 0xC0, 0, // netty.github.io + (byte) 0xC0, 11, // netty.github.io + }; + ByteBuf buffer = Unpooled.wrappedBuffer(nestedCompressionPointer); + ByteBuf uncompressed = null; + try { + uncompressed = DnsCodecUtil.decompressDomainName(buffer.duplicate().setIndex(19, 21)); + assertEquals(0, ByteBufUtil.compare( + Unpooled.wrappedBuffer(new byte[] { + 5, 'n', 'e', 't', 't', 'y', 6, 'g', 'i', 't', 'h', 'u', 'b', 2, 'i', 'o', 0 + }), uncompressed)); + } finally { + buffer.release(); + if (uncompressed != null) { + uncompressed.release(); + } + } + } + + @Test + public void testDecodeCompressionRDataPointer() throws Exception { + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + byte[] compressionPointer = { + 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0, + (byte) 0xC0, 0 + }; + ByteBuf buffer = Unpooled.wrappedBuffer(compressionPointer); + DefaultDnsRawRecord cnameRecord = null; + DefaultDnsRawRecord nsRecord = null; + try { + cnameRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + "netty.github.io", DnsRecordType.CNAME, DnsRecord.CLASS_IN, 60, buffer, 10, 2); + assertEquals(0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), cnameRecord.content()), + "The rdata of CNAME-type record should be decompressed in advance"); + assertEquals("netty.io.", DnsCodecUtil.decodeDomainName(cnameRecord.content())); + nsRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + "netty.github.io", DnsRecordType.NS, DnsRecord.CLASS_IN, 60, buffer, 10, 2); + assertEquals(0, ByteBufUtil.compare(buffer.duplicate().setIndex(0, 10), nsRecord.content()), + "The rdata of NS-type record should be decompressed in advance"); + assertEquals("netty.io.", DnsCodecUtil.decodeDomainName(nsRecord.content())); + } finally { + buffer.release(); + if (cnameRecord != null) { + cnameRecord.release(); + } + + if (nsRecord != null) { + nsRecord.release(); + } + } + } + + @Test + public void testDecodeMessageCompression() throws Exception { + // See https://www.ietf.org/rfc/rfc1035 [4.1.4. Message compression] + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + byte[] rfcExample = { 1, 'F', 3, 'I', 'S', 'I', 4, 'A', 'R', 'P', 'A', + 0, 3, 'F', 'O', 'O', + (byte) 0xC0, 0, // this is 20 in the example + (byte) 0xC0, 6, // this is 26 in the example + }; + DefaultDnsRawRecord rawPlainRecord = null; + DefaultDnsRawRecord rawUncompressedRecord = null; + DefaultDnsRawRecord rawUncompressedIndexedRecord = null; + ByteBuf buffer = Unpooled.wrappedBuffer(rfcExample); + try { + // First lets test that our utility function can correctly handle index references and decompression. + String plainName = DefaultDnsRecordDecoder.decodeName(buffer.duplicate()); + assertEquals("F.ISI.ARPA.", plainName); + String uncompressedPlainName = DefaultDnsRecordDecoder.decodeName(buffer.duplicate().setIndex(16, 20)); + assertEquals(plainName, uncompressedPlainName); + String uncompressedIndexedName = DefaultDnsRecordDecoder.decodeName(buffer.duplicate().setIndex(12, 20)); + assertEquals("FOO." + plainName, uncompressedIndexedName); + + // Now lets make sure out object parsing produces the same results for non PTR type (just use CNAME). + rawPlainRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + plainName, DnsRecordType.CNAME, DnsRecord.CLASS_IN, 60, buffer, 0, 11); + assertEquals(plainName, rawPlainRecord.name()); + assertEquals(plainName, DefaultDnsRecordDecoder.decodeName(rawPlainRecord.content())); + + rawUncompressedRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + uncompressedPlainName, DnsRecordType.CNAME, DnsRecord.CLASS_IN, 60, buffer, 16, 4); + assertEquals(uncompressedPlainName, rawUncompressedRecord.name()); + assertEquals(uncompressedPlainName, DefaultDnsRecordDecoder.decodeName(rawUncompressedRecord.content())); + + rawUncompressedIndexedRecord = (DefaultDnsRawRecord) decoder.decodeRecord( + uncompressedIndexedName, DnsRecordType.CNAME, DnsRecord.CLASS_IN, 60, buffer, 12, 8); + assertEquals(uncompressedIndexedName, rawUncompressedIndexedRecord.name()); + assertEquals(uncompressedIndexedName, + DefaultDnsRecordDecoder.decodeName(rawUncompressedIndexedRecord.content())); + + // Now lets make sure out object parsing produces the same results for PTR type. + DnsPtrRecord ptrRecord = (DnsPtrRecord) decoder.decodeRecord( + plainName, DnsRecordType.PTR, DnsRecord.CLASS_IN, 60, buffer, 0, 11); + assertEquals(plainName, ptrRecord.name()); + assertEquals(plainName, ptrRecord.hostname()); + + ptrRecord = (DnsPtrRecord) decoder.decodeRecord( + uncompressedPlainName, DnsRecordType.PTR, DnsRecord.CLASS_IN, 60, buffer, 16, 4); + assertEquals(uncompressedPlainName, ptrRecord.name()); + assertEquals(uncompressedPlainName, ptrRecord.hostname()); + + ptrRecord = (DnsPtrRecord) decoder.decodeRecord( + uncompressedIndexedName, DnsRecordType.PTR, DnsRecord.CLASS_IN, 60, buffer, 12, 8); + assertEquals(uncompressedIndexedName, ptrRecord.name()); + assertEquals(uncompressedIndexedName, ptrRecord.hostname()); + } finally { + if (rawPlainRecord != null) { + rawPlainRecord.release(); + } + if (rawUncompressedRecord != null) { + rawUncompressedRecord.release(); + } + if (rawUncompressedIndexedRecord != null) { + rawUncompressedIndexedRecord.release(); + } + buffer.release(); + } + } + + @Test + public void testTruncatedPacket() throws Exception { + ByteBuf buffer = Unpooled.buffer(); + buffer.writeByte(0); + buffer.writeShort(DnsRecordType.A.intValue()); + buffer.writeShort(1); + buffer.writeInt(32); + + // Write a truncated last value. + buffer.writeByte(0); + DefaultDnsRecordDecoder decoder = new DefaultDnsRecordDecoder(); + try { + int readerIndex = buffer.readerIndex(); + assertNull(decoder.decodeRecord(buffer)); + assertEquals(readerIndex, buffer.readerIndex()); + } finally { + buffer.release(); + } + } +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoderTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoderTest.java new file mode 100644 index 0000000..260db4e --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DefaultDnsRecordEncoderTest.java @@ -0,0 +1,151 @@ +/* + * 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SocketUtils; +import io.netty.util.internal.StringUtil; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class DefaultDnsRecordEncoderTest { + + @Test + public void testEncodeName() throws Exception { + testEncodeName(new byte[] { 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0 }, "netty.io."); + } + + @Test + public void testEncodeNameWithoutTerminator() throws Exception { + testEncodeName(new byte[] { 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0 }, "netty.io"); + } + + @Test + public void testEncodeNameWithExtraTerminator() throws Exception { + testEncodeName(new byte[] { 5, 'n', 'e', 't', 't', 'y', 2, 'i', 'o', 0 }, "netty.io.."); + } + + // Test for https://github.com/netty/netty/issues/5014 + @Test + public void testEncodeEmptyName() throws Exception { + testEncodeName(new byte[] { 0 }, StringUtil.EMPTY_STRING); + } + + @Test + public void testEncodeRootName() throws Exception { + testEncodeName(new byte[] { 0 }, "."); + } + + private static void testEncodeName(byte[] expected, String name) throws Exception { + DefaultDnsRecordEncoder encoder = new DefaultDnsRecordEncoder(); + ByteBuf out = Unpooled.buffer(); + ByteBuf expectedBuf = Unpooled.wrappedBuffer(expected); + try { + encoder.encodeName(name, out); + assertEquals(expectedBuf, out); + } finally { + out.release(); + expectedBuf.release(); + } + } + + @Test + public void testOptEcsRecordIpv4() throws Exception { + testOptEcsRecordIp(SocketUtils.addressByName("1.2.3.4")); + testOptEcsRecordIp(SocketUtils.addressByName("1.2.3.255")); + } + + @Test + public void testOptEcsRecordIpv6() throws Exception { + testOptEcsRecordIp(SocketUtils.addressByName("::0")); + testOptEcsRecordIp(SocketUtils.addressByName("::FF")); + } + + private static void testOptEcsRecordIp(InetAddress address) throws Exception { + int addressBits = address.getAddress().length * Byte.SIZE; + for (int i = 0; i <= addressBits; ++i) { + testIp(address, i); + } + } + + private static void testIp(InetAddress address, int prefix) throws Exception { + int lowOrderBitsToPreserve = prefix % Byte.SIZE; + + ByteBuf addressPart = Unpooled.wrappedBuffer(address.getAddress(), 0, + DefaultDnsRecordEncoder.calculateEcsAddressLength(prefix, lowOrderBitsToPreserve)); + + if (lowOrderBitsToPreserve > 0) { + // Pad the leftover of the last byte with zeros. + int idx = addressPart.writerIndex() - 1; + byte lastByte = addressPart.getByte(idx); + int paddingMask = -1 << 8 - lowOrderBitsToPreserve; + addressPart.setByte(idx, lastByte & paddingMask); + } + + int payloadSize = nextInt(Short.MAX_VALUE); + int extendedRcode = nextInt(Byte.MAX_VALUE * 2); // Unsigned + int version = nextInt(Byte.MAX_VALUE * 2); // Unsigned + + DefaultDnsRecordEncoder encoder = new DefaultDnsRecordEncoder(); + ByteBuf out = Unpooled.buffer(); + try { + DnsOptEcsRecord record = new DefaultDnsOptEcsRecord( + payloadSize, extendedRcode, version, prefix, address.getAddress()); + encoder.encodeRecord(record, out); + + assertEquals(0, out.readByte()); // Name + assertEquals(DnsRecordType.OPT.intValue(), out.readUnsignedShort()); // Opt + assertEquals(payloadSize, out.readUnsignedShort()); // payload + assertEquals(record.timeToLive(), out.getUnsignedInt(out.readerIndex())); + + // Read unpacked TTL. + assertEquals(extendedRcode, out.readUnsignedByte()); + assertEquals(version, out.readUnsignedByte()); + assertEquals(extendedRcode, record.extendedRcode()); + assertEquals(version, record.version()); + assertEquals(0, record.flags()); + + assertEquals(0, out.readShort()); + + int payloadLength = out.readUnsignedShort(); + assertEquals(payloadLength, out.readableBytes()); + + assertEquals(8, out.readShort()); // As defined by RFC. + + int rdataLength = out.readUnsignedShort(); + assertEquals(rdataLength, out.readableBytes()); + + assertEquals((short) InternetProtocolFamily.of(address).addressNumber(), out.readShort()); + + assertEquals(prefix, out.readUnsignedByte()); + assertEquals(0, out.readUnsignedByte()); // This must be 0 for requests. + assertEquals(addressPart, out); + } finally { + addressPart.release(); + out.release(); + } + } + + private static int nextInt(int max) { + return PlatformDependent.threadLocalRandom().nextInt(max); + } +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsQueryTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsQueryTest.java new file mode 100644 index 0000000..a8cde46 --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsQueryTest.java @@ -0,0 +1,83 @@ +/* + * Copyright 2013 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: + * + * https://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.handler.codec.dns; + +import io.netty.channel.embedded.EmbeddedChannel; + +import io.netty.channel.socket.DatagramPacket; +import io.netty.util.internal.SocketUtils; +import org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DnsQueryTest { + + @Test + public void testEncodeAndDecodeQuery() { + InetSocketAddress addr = SocketUtils.socketAddress("8.8.8.8", 53); + EmbeddedChannel writeChannel = new EmbeddedChannel(new DatagramDnsQueryEncoder()); + EmbeddedChannel readChannel = new EmbeddedChannel(new DatagramDnsQueryDecoder()); + + List queries = new ArrayList(5); + queries.add(new DatagramDnsQuery(null, addr, 1).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion("1.0.0.127.in-addr.arpa", DnsRecordType.PTR))); + queries.add(new DatagramDnsQuery(null, addr, 1).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion("www.example.com", DnsRecordType.A))); + queries.add(new DatagramDnsQuery(null, addr, 1).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion("example.com", DnsRecordType.AAAA))); + queries.add(new DatagramDnsQuery(null, addr, 1).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion("example.com", DnsRecordType.MX))); + queries.add(new DatagramDnsQuery(null, addr, 1).setRecord( + DnsSection.QUESTION, + new DefaultDnsQuestion("example.com", DnsRecordType.CNAME))); + + for (DnsQuery query: queries) { + assertThat(query.count(DnsSection.QUESTION), is(1)); + assertThat(query.count(DnsSection.ANSWER), is(0)); + assertThat(query.count(DnsSection.AUTHORITY), is(0)); + assertThat(query.count(DnsSection.ADDITIONAL), is(0)); + + assertTrue(writeChannel.writeOutbound(query)); + + DatagramPacket packet = writeChannel.readOutbound(); + assertTrue(packet.content().isReadable()); + assertTrue(readChannel.writeInbound(packet)); + + DnsQuery decodedDnsQuery = readChannel.readInbound(); + assertEquals(query, decodedDnsQuery); + assertTrue(decodedDnsQuery.release()); + + assertNull(writeChannel.readOutbound()); + assertNull(readChannel.readInbound()); + } + + assertFalse(writeChannel.finish()); + assertFalse(readChannel.finish()); + } +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsRecordTypeTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsRecordTypeTest.java new file mode 100644 index 0000000..ec349b0 --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsRecordTypeTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2014 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: + * + * https://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.handler.codec.dns; + +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Field; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertSame; + +public class DnsRecordTypeTest { + + private static List allTypes() throws Exception { + List result = new ArrayList(); + for (Field field : DnsRecordType.class.getFields()) { + if ((field.getModifiers() & Modifier.STATIC) != 0 && field.getType() == DnsRecordType.class) { + result.add((DnsRecordType) field.get(null)); + } + } + assertFalse(result.isEmpty()); + return result; + } + + @Test + public void testSanity() throws Exception { + assertEquals(allTypes().size(), new HashSet(allTypes()).size(), + "More than one type has the same int value"); + } + + /** + * Test of hashCode method, of class DnsRecordType. + */ + @Test + public void testHashCode() throws Exception { + for (DnsRecordType t : allTypes()) { + assertEquals(t.intValue(), t.hashCode()); + } + } + + /** + * Test of equals method, of class DnsRecordType. + */ + @Test + public void testEquals() throws Exception { + for (DnsRecordType t1 : allTypes()) { + for (DnsRecordType t2 : allTypes()) { + if (t1 != t2) { + assertNotEquals(t1, t2); + } + } + } + } + + /** + * Test of find method, of class DnsRecordType. + */ + @Test + public void testFind() throws Exception { + for (DnsRecordType t : allTypes()) { + DnsRecordType found = DnsRecordType.valueOf(t.intValue()); + assertSame(t, found); + found = DnsRecordType.valueOf(t.name()); + assertSame(t, found, t.name()); + } + } +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsResponseTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsResponseTest.java new file mode 100644 index 0000000..392f1ec --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/DnsResponseTest.java @@ -0,0 +1,128 @@ +/* + * Copyright 2013 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.channel.socket.DatagramPacket; +import io.netty.handler.codec.CorruptedFrameException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.net.InetSocketAddress; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertThrows; + +public class DnsResponseTest { + + private static final byte[][] packets = { + { + 0, 1, -127, -128, 0, 1, 0, 1, 0, 0, 0, 0, 3, 119, 119, 119, 7, 101, 120, 97, 109, 112, 108, 101, 3, + 99, 111, 109, 0, 0, 1, 0, 1, -64, 12, 0, 1, 0, 1, 0, 0, 16, -113, 0, 4, -64, 0, 43, 10 + }, + { + 0, 1, -127, -128, 0, 1, 0, 1, 0, 0, 0, 0, 3, 119, 119, 119, 7, 101, 120, 97, 109, 112, 108, 101, 3, + 99, 111, 109, 0, 0, 28, 0, 1, -64, 12, 0, 28, 0, 1, 0, 0, 69, -8, 0, 16, 32, 1, 5, 0, 0, -120, 2, + 0, 0, 0, 0, 0, 0, 0, 0, 16 + }, + { + 0, 2, -127, -128, 0, 1, 0, 0, 0, 1, 0, 0, 3, 119, 119, 119, 7, 101, 120, 97, 109, 112, 108, 101, 3, + 99, 111, 109, 0, 0, 15, 0, 1, -64, 16, 0, 6, 0, 1, 0, 0, 3, -43, 0, 45, 3, 115, 110, 115, 3, 100, + 110, 115, 5, 105, 99, 97, 110, 110, 3, 111, 114, 103, 0, 3, 110, 111, 99, -64, 49, 119, -4, 39, + 112, 0, 0, 28, 32, 0, 0, 14, 16, 0, 18, 117, 0, 0, 0, 14, 16 + }, + { + 0, 3, -127, -128, 0, 1, 0, 1, 0, 0, 0, 0, 3, 119, 119, 119, 7, 101, 120, 97, 109, 112, 108, 101, 3, + 99, 111, 109, 0, 0, 16, 0, 1, -64, 12, 0, 16, 0, 1, 0, 0, 84, 75, 0, 12, 11, 118, 61, 115, 112, + 102, 49, 32, 45, 97, 108, 108 + }, + { + -105, 19, -127, 0, 0, 1, 0, 0, 0, 13, 0, 0, 2, 104, 112, 11, 116, 105, 109, 98, 111, 117, 100, 114, + 101, 97, 117, 3, 111, 114, 103, 0, 0, 1, 0, 1, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 20, 1, 68, 12, 82, + 79, 79, 84, 45, 83, 69, 82, 86, 69, 82, 83, 3, 78, 69, 84, 0, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, + 70, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, 69, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, + 1, 75, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, 67, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, + 4, 1, 76, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, 71, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, + 0, 4, 1, 73, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, 66, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, + 0, 0, 4, 1, 77, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, 65, -64, 49, 0, 0, 2, 0, 1, 0, 7, + -23, 0, 0, 4, 1, 72, -64, 49, 0, 0, 2, 0, 1, 0, 7, -23, 0, 0, 4, 1, 74, -64, 49 + } + }; + + private static final byte[] malformedLoopPacket = { + 0, 4, -127, -128, 0, 1, 0, 0, 0, 0, 0, 0, -64, 12, 0, 1, 0, 1 + }; + + @Test + public void readResponseTest() { + EmbeddedChannel embedder = new EmbeddedChannel(new DatagramDnsResponseDecoder()); + for (byte[] p: packets) { + ByteBuf packet = embedder.alloc().buffer(512).writeBytes(p); + embedder.writeInbound(new DatagramPacket(packet, null, new InetSocketAddress(0))); + AddressedEnvelope envelope = embedder.readInbound(); + assertThat(envelope, is(instanceOf(DatagramDnsResponse.class))); + DnsResponse response = envelope.content(); + assertThat(response, is(sameInstance((Object) envelope))); + + ByteBuf raw = Unpooled.wrappedBuffer(p); + assertThat(response.id(), is(raw.getUnsignedShort(0))); + assertThat(response.count(DnsSection.QUESTION), is(raw.getUnsignedShort(4))); + assertThat(response.count(DnsSection.ANSWER), is(raw.getUnsignedShort(6))); + assertThat(response.count(DnsSection.AUTHORITY), is(raw.getUnsignedShort(8))); + assertThat(response.count(DnsSection.ADDITIONAL), is(raw.getUnsignedShort(10))); + + envelope.release(); + } + assertFalse(embedder.finish()); + } + + @Test + public void readMalformedResponseTest() { + final EmbeddedChannel embedder = new EmbeddedChannel(new DatagramDnsResponseDecoder()); + final ByteBuf packet = embedder.alloc().buffer(512).writeBytes(malformedLoopPacket); + try { + assertThrows(CorruptedFrameException.class, new Executable() { + @Override + public void execute() { + embedder.writeInbound(new DatagramPacket(packet, null, new InetSocketAddress(0))); + } + }); + } finally { + assertFalse(embedder.finish()); + } + } + + @Test + public void readIncompleteResponseTest() { + final EmbeddedChannel embedder = new EmbeddedChannel(new DatagramDnsResponseDecoder()); + final ByteBuf packet = embedder.alloc().buffer(512); + try { + assertThrows(CorruptedFrameException.class, new Executable() { + @Override + public void execute() { + embedder.writeInbound(new DatagramPacket(packet, null, new InetSocketAddress(0))); + } + }); + } finally { + assertFalse(embedder.finish()); + } + } +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/NativeImageHandlerMetadataTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/NativeImageHandlerMetadataTest.java new file mode 100644 index 0000000..644e268 --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/NativeImageHandlerMetadataTest.java @@ -0,0 +1,28 @@ +/* + * Copyright 2022 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: + * + * https://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.handler.codec.dns; + +import io.netty.nativeimage.ChannelHandlerMetadataUtil; +import org.junit.jupiter.api.Test; + +public class NativeImageHandlerMetadataTest { + + @Test + public void collectAndCompareMetadata() { + ChannelHandlerMetadataUtil.generateMetadata("io.netty.handler.codec.dns"); + } + +} diff --git a/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/TcpDnsTest.java b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/TcpDnsTest.java new file mode 100644 index 0000000..042dd0f --- /dev/null +++ b/netty-handler-codec-dns/src/test/java/io/netty/handler/codec/dns/TcpDnsTest.java @@ -0,0 +1,85 @@ +/* + * Copyright 2021 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: + * + * https://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.handler.codec.dns; + +import io.netty.buffer.Unpooled; +import io.netty.channel.embedded.EmbeddedChannel; +import io.netty.util.ReferenceCountUtil; +import org.junit.jupiter.api.Test; + +import java.util.Random; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class TcpDnsTest { + private static final String QUERY_DOMAIN = "www.example.com"; + private static final long TTL = 600; + private static final byte[] QUERY_RESULT = new byte[]{(byte) 192, (byte) 168, 1, 1}; + + @Test + public void testQueryDecode() { + EmbeddedChannel channel = new EmbeddedChannel(new TcpDnsQueryDecoder()); + + int randomID = new Random().nextInt(60000 - 1000) + 1000; + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(QUERY_DOMAIN, DnsRecordType.A)); + assertTrue(channel.writeInbound(query)); + + DnsQuery readQuery = channel.readInbound(); + assertThat(readQuery, is(query)); + assertThat(readQuery.recordAt(DnsSection.QUESTION).name(), is(query.recordAt(DnsSection.QUESTION).name())); + readQuery.release(); + assertFalse(channel.finish()); + } + + @Test + public void testResponseEncode() { + EmbeddedChannel channel = new EmbeddedChannel(new TcpDnsResponseEncoder()); + + int randomID = new Random().nextInt(60000 - 1000) + 1000; + DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY) + .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(QUERY_DOMAIN, DnsRecordType.A)); + + DnsQuestion question = query.recordAt(DnsSection.QUESTION); + channel.writeInbound(newResponse(query, question, QUERY_RESULT)); + + DnsResponse readResponse = channel.readInbound(); + assertThat(readResponse.recordAt(DnsSection.QUESTION), is((DnsRecord) question)); + DnsRawRecord record = new DefaultDnsRawRecord(question.name(), + DnsRecordType.A, TTL, Unpooled.wrappedBuffer(QUERY_RESULT)); + assertThat(readResponse.recordAt(DnsSection.ANSWER), is((DnsRecord) record)); + assertThat(readResponse.recordAt(DnsSection.ANSWER).content(), is(record.content())); + ReferenceCountUtil.release(readResponse); + ReferenceCountUtil.release(record); + query.release(); + assertFalse(channel.finish()); + } + + private static DefaultDnsResponse newResponse(DnsQuery query, DnsQuestion question, byte[]... addresses) { + DefaultDnsResponse response = new DefaultDnsResponse(query.id()); + response.addRecord(DnsSection.QUESTION, question); + + for (byte[] address : addresses) { + DefaultDnsRawRecord queryAnswer = new DefaultDnsRawRecord(question.name(), + DnsRecordType.A, TTL, Unpooled.wrappedBuffer(address)); + response.addRecord(DnsSection.ANSWER, queryAnswer); + } + return response; + } +} diff --git a/netty-resolver-dns/build.gradle b/netty-resolver-dns/build.gradle new file mode 100644 index 0000000..6176fdc --- /dev/null +++ b/netty-resolver-dns/build.gradle @@ -0,0 +1,5 @@ +dependencies { + api project(':netty-handler-codec-dns') + testImplementation testLibs.apache.ds.dns + testImplementation testLibs.assertj +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java new file mode 100644 index 0000000..8ae43f0 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCache.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; + +import java.net.InetSocketAddress; + +/** + * Cache which stores the nameservers that should be used to resolve a specific hostname. + */ +public interface AuthoritativeDnsServerCache { + + /** + * Returns the cached nameservers that should be used to resolve the given hostname. The returned + * {@link DnsServerAddressStream} may contain unresolved {@link InetSocketAddress}es that will be resolved + * when needed while resolving other domain names. + * + * @param hostname the hostname + * @return the cached entries or an {@code null} if none. + */ + DnsServerAddressStream get(String hostname); + + /** + * Caches a nameserver that should be used to resolve the given hostname. + * + * @param hostname the hostname + * @param address the nameserver address (which may be unresolved). + * @param originalTtl the TTL as returned by the DNS server + * @param loop the {@link EventLoop} used to register the TTL timeout + */ + void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop); + + /** + * Clears all cached nameservers. + * + * @see #clear(String) + */ + void clear(); + + /** + * Clears the cached nameservers for the specified hostname. + * + * @return {@code true} if and only if there was an entry for the specified host name in the cache and + * it has been removed by this method + */ + boolean clear(String hostname); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java new file mode 100644 index 0000000..ee713c4 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/AuthoritativeDnsServerCacheAdapter.java @@ -0,0 +1,78 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; +import io.netty.handler.codec.dns.DnsRecord; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * {@link AuthoritativeDnsServerCache} implementation which delegates all operations to a wrapped {@link DnsCache}. + * This implementation is only present to preserve a upgrade story. + */ +final class AuthoritativeDnsServerCacheAdapter implements AuthoritativeDnsServerCache { + + private static final DnsRecord[] EMPTY = new DnsRecord[0]; + private final DnsCache cache; + + AuthoritativeDnsServerCacheAdapter(DnsCache cache) { + this.cache = checkNotNull(cache, "cache"); + } + + @Override + public DnsServerAddressStream get(String hostname) { + List entries = cache.get(hostname, EMPTY); + if (entries == null || entries.isEmpty()) { + return null; + } + if (entries.get(0).cause() != null) { + return null; + } + + List addresses = new ArrayList(entries.size()); + + int i = 0; + do { + InetAddress addr = entries.get(i).address(); + addresses.add(new InetSocketAddress(addr, DefaultDnsServerAddressStreamProvider.DNS_PORT)); + } while (++i < entries.size()); + return new SequentialDnsServerAddressStream(addresses, 0); + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + // We only cache resolved addresses. + if (!address.isUnresolved()) { + cache.cache(hostname, EMPTY, address.getAddress(), originalTtl, loop); + } + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + public boolean clear(String hostname) { + return cache.clear(hostname); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java new file mode 100644 index 0000000..02678c0 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserver.java @@ -0,0 +1,109 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.channel.ChannelFuture; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResponseCode; + +import java.net.InetSocketAddress; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Combines two {@link DnsQueryLifecycleObserver} into a single {@link DnsQueryLifecycleObserver}. + */ +public final class BiDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + private final DnsQueryLifecycleObserver a; + private final DnsQueryLifecycleObserver b; + + /** + * Create a new instance. + * @param a The {@link DnsQueryLifecycleObserver} that will receive events first. + * @param b The {@link DnsQueryLifecycleObserver} that will receive events second. + */ + public BiDnsQueryLifecycleObserver(DnsQueryLifecycleObserver a, DnsQueryLifecycleObserver b) { + this.a = checkNotNull(a, "a"); + this.b = checkNotNull(b, "b"); + } + + @Override + public void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + try { + a.queryWritten(dnsServerAddress, future); + } finally { + b.queryWritten(dnsServerAddress, future); + } + } + + @Override + public void queryCancelled(int queriesRemaining) { + try { + a.queryCancelled(queriesRemaining); + } finally { + b.queryCancelled(queriesRemaining); + } + } + + @Override + public DnsQueryLifecycleObserver queryRedirected(List nameServers) { + try { + a.queryRedirected(nameServers); + } finally { + b.queryRedirected(nameServers); + } + return this; + } + + @Override + public DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion) { + try { + a.queryCNAMEd(cnameQuestion); + } finally { + b.queryCNAMEd(cnameQuestion); + } + return this; + } + + @Override + public DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code) { + try { + a.queryNoAnswer(code); + } finally { + b.queryNoAnswer(code); + } + return this; + } + + @Override + public void queryFailed(Throwable cause) { + try { + a.queryFailed(cause); + } finally { + b.queryFailed(cause); + } + } + + @Override + public void querySucceed() { + try { + a.querySucceed(); + } finally { + b.querySucceed(); + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java new file mode 100644 index 0000000..0eec7f9 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/BiDnsQueryLifecycleObserverFactory.java @@ -0,0 +1,44 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsQuestion; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * Combines two {@link DnsQueryLifecycleObserverFactory} into a single {@link DnsQueryLifecycleObserverFactory}. + */ +public final class BiDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory { + private final DnsQueryLifecycleObserverFactory a; + private final DnsQueryLifecycleObserverFactory b; + + /** + * Create a new instance. + * @param a The {@link DnsQueryLifecycleObserverFactory} that will receive events first. + * @param b The {@link DnsQueryLifecycleObserverFactory} that will receive events second. + */ + public BiDnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory a, DnsQueryLifecycleObserverFactory b) { + this.a = checkNotNull(a, "a"); + this.b = checkNotNull(b, "b"); + } + + @Override + public DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) { + return new BiDnsQueryLifecycleObserver(a.newDnsQueryLifecycleObserver(question), + b.newDnsQueryLifecycleObserver(question)); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/Cache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/Cache.java new file mode 100644 index 0000000..ab85d84 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/Cache.java @@ -0,0 +1,292 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; +import io.netty.util.internal.PlatformDependent; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentMap; +import java.util.concurrent.Delayed; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.concurrent.atomic.AtomicReferenceFieldUpdater; + +import static java.util.Collections.singletonList; + +/** + * Abstract cache that automatically removes entries for a hostname once the TTL for an entry is reached. + * + * @param + */ +abstract class Cache { + private static final AtomicReferenceFieldUpdater FUTURE_UPDATER = + AtomicReferenceFieldUpdater.newUpdater(Cache.Entries.class, ScheduledFuture.class, "expirationFuture"); + + private static final ScheduledFuture CANCELLED = new ScheduledFuture() { + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public long getDelay(TimeUnit unit) { + // We ignore unit and always return the minimum value to ensure the TTL of the cancelled marker is + // the smallest. + return Long.MIN_VALUE; + } + + @Override + public int compareTo(Delayed o) { + throw new UnsupportedOperationException(); + } + + @Override + public boolean isCancelled() { + return true; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public Object get() { + throw new UnsupportedOperationException(); + } + + @Override + public Object get(long timeout, TimeUnit unit) { + throw new UnsupportedOperationException(); + } + }; + + // Two years are supported by all our EventLoop implementations and so safe to use as maximum. + // See also: https://github.com/netty/netty/commit/b47fb817991b42ec8808c7d26538f3f2464e1fa6 + static final int MAX_SUPPORTED_TTL_SECS = (int) TimeUnit.DAYS.toSeconds(365 * 2); + + private final ConcurrentMap resolveCache = PlatformDependent.newConcurrentHashMap(); + + /** + * Remove everything from the cache. + */ + final void clear() { + while (!resolveCache.isEmpty()) { + for (Iterator> i = resolveCache.entrySet().iterator(); i.hasNext();) { + Map.Entry e = i.next(); + i.remove(); + + e.getValue().clearAndCancel(); + } + } + } + + /** + * Clear all entries (if anything exists) for the given hostname and return {@code true} if anything was removed. + */ + final boolean clear(String hostname) { + Entries entries = resolveCache.remove(hostname); + return entries != null && entries.clearAndCancel(); + } + + /** + * Returns all caches entries for the given hostname. + */ + final List get(String hostname) { + Entries entries = resolveCache.get(hostname); + return entries == null ? null : entries.get(); + } + + /** + * Cache a value for the given hostname that will automatically expire once the TTL is reached. + */ + final void cache(String hostname, E value, int ttl, EventLoop loop) { + Entries entries = resolveCache.get(hostname); + if (entries == null) { + entries = new Entries(hostname); + Entries oldEntries = resolveCache.putIfAbsent(hostname, entries); + if (oldEntries != null) { + entries = oldEntries; + } + } + entries.add(value, ttl, loop); + } + + /** + * Return the number of hostnames for which we have cached something. + */ + final int size() { + return resolveCache.size(); + } + + /** + * Returns {@code true} if this entry should replace all other entries that are already cached for the hostname. + */ + protected abstract boolean shouldReplaceAll(E entry); + + /** + * Sort the {@link List} for a {@code hostname} before caching these. + */ + protected void sortEntries( + @SuppressWarnings("unused") String hostname, @SuppressWarnings("unused") List entries) { + // NOOP. + } + + /** + * Returns {@code true} if both entries are equal. + */ + protected abstract boolean equals(E entry, E otherEntry); + + // Directly extend AtomicReference for intrinsics and also to keep memory overhead low. + private final class Entries extends AtomicReference> implements Runnable { + + private final String hostname; + // Needs to be package-private to be able to access it via the AtomicReferenceFieldUpdater + volatile ScheduledFuture expirationFuture; + + Entries(String hostname) { + super(Collections.emptyList()); + this.hostname = hostname; + } + + void add(E e, int ttl, EventLoop loop) { + if (!shouldReplaceAll(e)) { + for (;;) { + List entries = get(); + if (!entries.isEmpty()) { + final E firstEntry = entries.get(0); + if (shouldReplaceAll(firstEntry)) { + assert entries.size() == 1; + + if (compareAndSet(entries, singletonList(e))) { + scheduleCacheExpirationIfNeeded(ttl, loop); + return; + } else { + // Need to try again as CAS failed + continue; + } + } + + // Create a new List for COW semantics + List newEntries = new ArrayList(entries.size() + 1); + int i = 0; + E replacedEntry = null; + do { + E entry = entries.get(i); + // Only add old entry if the address is not the same as the one we try to add as well. + // In this case we will skip it and just add the new entry as this may have + // more up-to-date data and cancel the old after we were able to update the cache. + if (!Cache.this.equals(e, entry)) { + newEntries.add(entry); + } else { + replacedEntry = entry; + newEntries.add(e); + + ++i; + for (; i < entries.size(); ++i) { + newEntries.add(entries.get(i)); + } + break; + } + } while (++i < entries.size()); + if (replacedEntry == null) { + newEntries.add(e); + } + sortEntries(hostname, newEntries); + + if (compareAndSet(entries, Collections.unmodifiableList(newEntries))) { + scheduleCacheExpirationIfNeeded(ttl, loop); + return; + } + } else if (compareAndSet(entries, singletonList(e))) { + scheduleCacheExpirationIfNeeded(ttl, loop); + return; + } + } + } else { + set(singletonList(e)); + scheduleCacheExpirationIfNeeded(ttl, loop); + } + } + + private void scheduleCacheExpirationIfNeeded(int ttl, EventLoop loop) { + for (;;) { + // We currently don't calculate a new TTL when we need to retry the CAS as we don't expect this to + // be invoked very concurrently and also we use SECONDS anyway. If this ever becomes a problem + // we can reconsider. + ScheduledFuture oldFuture = FUTURE_UPDATER.get(this); + if (oldFuture == null || oldFuture.getDelay(TimeUnit.SECONDS) > ttl) { + ScheduledFuture newFuture = loop.schedule(this, ttl, TimeUnit.SECONDS); + // It is possible that + // 1. task will fire in between this line, or + // 2. multiple timers may be set if there is concurrency + // (1) Shouldn't be a problem because we will fail the CAS and then the next loop will see CANCELLED + // so the ttl will not be less, and we will bail out of the loop. + // (2) This is a trade-off to avoid concurrency resulting in contention on a synchronized block. + if (FUTURE_UPDATER.compareAndSet(this, oldFuture, newFuture)) { + if (oldFuture != null) { + oldFuture.cancel(true); + } + break; + } else { + // There was something else scheduled in the meantime... Cancel and try again. + newFuture.cancel(true); + } + } else { + break; + } + } + } + + boolean clearAndCancel() { + List entries = getAndSet(Collections.emptyList()); + if (entries.isEmpty()) { + return false; + } + + ScheduledFuture expirationFuture = FUTURE_UPDATER.getAndSet(this, CANCELLED); + if (expirationFuture != null) { + expirationFuture.cancel(false); + } + + return true; + } + + @Override + public void run() { + // We always remove all entries for a hostname once one entry expire. This is not the + // most efficient to do but this way we can guarantee that if a DnsResolver + // be configured to prefer one ip family over the other we will not return unexpected + // results to the enduser if one of the A or AAAA records has different TTL settings. + // + // As a TTL is just a hint of the maximum time a cache is allowed to cache stuff it's + // completely fine to remove the entry even if the TTL is not reached yet. + // + // See https://github.com/netty/netty/issues/7329 + resolveCache.remove(hostname, this); + + clearAndCancel(); + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java new file mode 100644 index 0000000..ca382dc --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DatagramDnsQueryContext.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 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: + * + * https://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.bootstrap.Bootstrap; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.handler.codec.dns.DatagramDnsQuery; +import io.netty.handler.codec.dns.DnsQuery; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import java.net.InetSocketAddress; + +final class DatagramDnsQueryContext extends DnsQueryContext { + + DatagramDnsQueryContext(Channel channel, Future channelReadyFuture, + InetSocketAddress nameServerAddr, + DnsQueryContextManager queryContextManager, + int maxPayLoadSize, boolean recursionDesired, + long queryTimeoutMillis, + DnsQuestion question, DnsRecord[] additionals, + Promise> promise, + Bootstrap socketBootstrap, boolean retryWithTcpOnTimeout) { + super(channel, channelReadyFuture, nameServerAddr, queryContextManager, maxPayLoadSize, recursionDesired, + queryTimeoutMillis, question, additionals, promise, socketBootstrap, retryWithTcpOnTimeout); + } + + @Override + protected DnsQuery newQuery(int id, InetSocketAddress nameServerAddr) { + return new DatagramDnsQuery(null, nameServerAddr, id); + } + + @Override + protected String protocol() { + return "UDP"; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java new file mode 100644 index 0000000..b75db05 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCache.java @@ -0,0 +1,136 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; +import io.netty.util.internal.PlatformDependent; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import static io.netty.util.internal.ObjectUtil.*; + +/** + * Default implementation of {@link AuthoritativeDnsServerCache}, backed by a {@link ConcurrentMap}. + */ +public class DefaultAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { + + private final int minTtl; + private final int maxTtl; + private final Comparator comparator; + private final Cache resolveCache = new Cache() { + @Override + protected boolean shouldReplaceAll(InetSocketAddress entry) { + return false; + } + + @Override + protected boolean equals(InetSocketAddress entry, InetSocketAddress otherEntry) { + if (PlatformDependent.javaVersion() >= 7) { + return entry.getHostString().equalsIgnoreCase(otherEntry.getHostString()); + } + return entry.getHostName().equalsIgnoreCase(otherEntry.getHostName()); + } + + @Override + protected void sortEntries(String hostname, List entries) { + if (comparator != null) { + Collections.sort(entries, comparator); + } + } + }; + + /** + * Create a cache that respects the TTL returned by the DNS server. + */ + public DefaultAuthoritativeDnsServerCache() { + this(0, Cache.MAX_SUPPORTED_TTL_SECS, null); + } + + /** + * Create a cache. + * + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + * @param comparator the {@link Comparator} to order the {@link InetSocketAddress} for a hostname or {@code null} + * if insertion order should be used. + */ + public DefaultAuthoritativeDnsServerCache(int minTtl, int maxTtl, Comparator comparator) { + this.minTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(minTtl, "minTtl")); + this.maxTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositive(maxTtl, "maxTtl")); + if (minTtl > maxTtl) { + throw new IllegalArgumentException( + "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); + } + this.comparator = comparator; + } + + @SuppressWarnings("unchecked") + @Override + public DnsServerAddressStream get(String hostname) { + checkNotNull(hostname, "hostname"); + + List addresses = resolveCache.get(hostname); + if (addresses == null || addresses.isEmpty()) { + return null; + } + return new SequentialDnsServerAddressStream(addresses, 0); + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + checkNotNull(hostname, "hostname"); + checkNotNull(address, "address"); + checkNotNull(loop, "loop"); + + if (PlatformDependent.javaVersion() >= 7 && address.getHostString() == null) { + // We only cache addresses that have also a host string as we will need it later when trying to replace + // unresolved entries in the cache. + return; + } + + resolveCache.cache(hostname, address, Math.max(minTtl, (int) Math.min(maxTtl, originalTtl)), loop); + } + + @Override + public void clear() { + resolveCache.clear(); + } + + @Override + public boolean clear(String hostname) { + return resolveCache.clear(checkNotNull(hostname, "hostname")); + } + + @Override + public String toString() { + return "DefaultAuthoritativeDnsServerCache(minTtl=" + minTtl + ", maxTtl=" + maxTtl + ", cached nameservers=" + + resolveCache.size() + ')'; + } + + // Package visibility for testing purposes + int minTtl() { + return minTtl; + } + + // Package visibility for testing purposes + int maxTtl() { + return maxTtl; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java new file mode 100644 index 0000000..be68fcb --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCache.java @@ -0,0 +1,346 @@ +/* + * 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: + * + * https://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.EventLoop; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.util.internal.StringUtil; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectInputStream; +import java.io.ObjectOutputStream; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.AbstractList; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositiveOrZero; + +/** + * Default implementation of {@link DnsCache}, backed by a {@link ConcurrentMap}. + * If any additional {@link DnsRecord} is used, no caching takes place. + */ +public class DefaultDnsCache implements DnsCache { + + private final Cache resolveCache = new Cache() { + + @Override + protected boolean shouldReplaceAll(DefaultDnsCacheEntry entry) { + return entry.cause() != null; + } + + @Override + protected boolean equals(DefaultDnsCacheEntry entry, DefaultDnsCacheEntry otherEntry) { + if (entry.address() != null) { + return entry.address().equals(otherEntry.address()); + } + if (otherEntry.address() != null) { + return false; + } + return entry.cause().equals(otherEntry.cause()); + } + }; + + private final int minTtl; + private final int maxTtl; + private final int negativeTtl; + + /** + * Create a cache that respects the TTL returned by the DNS server + * and doesn't cache negative responses. + */ + public DefaultDnsCache() { + this(0, Cache.MAX_SUPPORTED_TTL_SECS, 0); + } + + /** + * Create a cache. + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + * @param negativeTtl the TTL for failed queries + */ + public DefaultDnsCache(int minTtl, int maxTtl, int negativeTtl) { + this.minTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(minTtl, "minTtl")); + this.maxTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(maxTtl, "maxTtl")); + if (minTtl > maxTtl) { + throw new IllegalArgumentException( + "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); + } + this.negativeTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(negativeTtl, "negativeTtl")); + } + + /** + * Returns the minimum TTL of the cached DNS resource records (in seconds). + * + * @see #maxTtl() + */ + public int minTtl() { + return minTtl; + } + + /** + * Returns the maximum TTL of the cached DNS resource records (in seconds). + * + * @see #minTtl() + */ + public int maxTtl() { + return maxTtl; + } + + /** + * Returns the TTL of the cache for the failed DNS queries (in seconds). The default value is {@code 0}, which + * disables the cache for negative results. + */ + public int negativeTtl() { + return negativeTtl; + } + + @Override + public void clear() { + resolveCache.clear(); + } + + @Override + public boolean clear(String hostname) { + checkNotNull(hostname, "hostname"); + return resolveCache.clear(appendDot(hostname)); + } + + private static boolean emptyAdditionals(DnsRecord[] additionals) { + return additionals == null || additionals.length == 0; + } + + @Override + public List get(String hostname, DnsRecord[] additionals) { + checkNotNull(hostname, "hostname"); + if (!emptyAdditionals(additionals)) { + return Collections.emptyList(); + } + + final List entries = resolveCache.get(appendDot(hostname)); + if (entries == null || entries.isEmpty()) { + return entries; + } + return new DnsCacheEntryList(entries); + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, + InetAddress address, long originalTtl, EventLoop loop) { + checkNotNull(hostname, "hostname"); + checkNotNull(address, "address"); + checkNotNull(loop, "loop"); + DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, address); + if (maxTtl == 0 || !emptyAdditionals(additionals)) { + return e; + } + resolveCache.cache(appendDot(hostname), e, Math.max(minTtl, (int) Math.min(maxTtl, originalTtl)), loop); + return e; + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) { + checkNotNull(hostname, "hostname"); + checkNotNull(cause, "cause"); + checkNotNull(loop, "loop"); + + DefaultDnsCacheEntry e = new DefaultDnsCacheEntry(hostname, cause); + if (negativeTtl == 0 || !emptyAdditionals(additionals)) { + return e; + } + + resolveCache.cache(appendDot(hostname), e, negativeTtl, loop); + return e; + } + + @Override + public String toString() { + return new StringBuilder() + .append("DefaultDnsCache(minTtl=") + .append(minTtl).append(", maxTtl=") + .append(maxTtl).append(", negativeTtl=") + .append(negativeTtl).append(", cached resolved hostname=") + .append(resolveCache.size()).append(')') + .toString(); + } + + private static final class DefaultDnsCacheEntry implements DnsCacheEntry { + private final String hostname; + private final InetAddress address; + private final Throwable cause; + private final int hash; + + DefaultDnsCacheEntry(String hostname, InetAddress address) { + this.hostname = hostname; + this.address = address; + cause = null; + hash = System.identityHashCode(this); + } + + DefaultDnsCacheEntry(String hostname, Throwable cause) { + this.hostname = hostname; + this.cause = cause; + address = null; + hash = System.identityHashCode(this); + } + + private DefaultDnsCacheEntry(DefaultDnsCacheEntry entry) { + this.hostname = entry.hostname; + if (entry.cause == null) { + this.address = entry.address; + this.cause = null; + } else { + this.address = null; + this.cause = copyThrowable(entry.cause); + } + this.hash = entry.hash; + } + + @Override + public InetAddress address() { + return address; + } + + @Override + public Throwable cause() { + return cause; + } + + String hostname() { + return hostname; + } + + @Override + public String toString() { + if (cause != null) { + return hostname + '/' + cause; + } else { + return address.toString(); + } + } + + @Override + public int hashCode() { + return hash; + } + + @Override + public boolean equals(Object obj) { + return (obj instanceof DefaultDnsCacheEntry) && ((DefaultDnsCacheEntry) obj).hash == hash; + } + + DnsCacheEntry copyIfNeeded() { + if (cause == null) { + return this; + } + return new DefaultDnsCacheEntry(this); + } + } + + private static String appendDot(String hostname) { + return StringUtil.endsWith(hostname, '.') ? hostname : hostname + '.'; + } + + private static Throwable copyThrowable(Throwable error) { + if (error.getClass() == UnknownHostException.class) { + // Fast-path as this is the only type of Throwable that our implementation ever add to the cache. + UnknownHostException copy = new UnknownHostException(error.getMessage()) { + @Override + public Throwable fillInStackTrace() { + // noop. + return this; + } + }; + copy.initCause(error.getCause()); + copy.setStackTrace(error.getStackTrace()); + return copy; + } + ObjectOutputStream oos = null; + ObjectInputStream ois = null; + + try { + // Throwable is Serializable so lets just do a deep copy. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + oos = new ObjectOutputStream(baos); + oos.writeObject(error); + + ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); + ois = new ObjectInputStream(bais); + return (Throwable) ois.readObject(); + } catch (IOException e) { + throw new IllegalStateException(e); + } catch (ClassNotFoundException e) { + throw new IllegalStateException(e); + } finally { + if (oos != null) { + try { + oos.close(); + } catch (IOException ignore) { + // noop + } + } + if (ois != null) { + try { + ois.close(); + } catch (IOException ignore) { + // noop + } + } + } + } + + private static final class DnsCacheEntryList extends AbstractList { + private final List entries; + + DnsCacheEntryList(List entries) { + this.entries = entries; + } + + @Override + public DnsCacheEntry get(int index) { + DefaultDnsCacheEntry entry = (DefaultDnsCacheEntry) entries.get(index); + // As we dont know what exactly the user is doing with the returned exception (for example + // using addSuppressed(...) and so hold up a lot of memory until the entry expires) we do + // create a copy. + return entry.copyIfNeeded(); + } + + @Override + public int size() { + return entries.size(); + } + + @Override + public int hashCode() { + // Just delegate to super to make checkstyle happy + return super.hashCode(); + } + + @Override + public boolean equals(Object o) { + if (o instanceof DnsCacheEntryList) { + // Fast-path. + return entries.equals(((DnsCacheEntryList) o).entries); + } + return super.equals(o); + } + }; +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java new file mode 100644 index 0000000..9f5c6d0 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsCnameCache.java @@ -0,0 +1,105 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; +import io.netty.util.AsciiString; + +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.*; + +/** + * Default implementation of a {@link DnsCnameCache}. + */ +public final class DefaultDnsCnameCache implements DnsCnameCache { + private final int minTtl; + private final int maxTtl; + + private final Cache cache = new Cache() { + @Override + protected boolean shouldReplaceAll(String entry) { + // Only one 1:1 mapping is supported as specified in the RFC. + return true; + } + + @Override + protected boolean equals(String entry, String otherEntry) { + return AsciiString.contentEqualsIgnoreCase(entry, otherEntry); + } + }; + + /** + * Create a cache that respects the TTL returned by the DNS server. + */ + public DefaultDnsCnameCache() { + this(0, Cache.MAX_SUPPORTED_TTL_SECS); + } + + /** + * Create a cache. + * + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + */ + public DefaultDnsCnameCache(int minTtl, int maxTtl) { + this.minTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositiveOrZero(minTtl, "minTtl")); + this.maxTtl = Math.min(Cache.MAX_SUPPORTED_TTL_SECS, checkPositive(maxTtl, "maxTtl")); + if (minTtl > maxTtl) { + throw new IllegalArgumentException( + "minTtl: " + minTtl + ", maxTtl: " + maxTtl + " (expected: 0 <= minTtl <= maxTtl)"); + } + } + + @SuppressWarnings("unchecked") + @Override + public String get(String hostname) { + List cached = cache.get(checkNotNull(hostname, "hostname")); + if (cached == null || cached.isEmpty()) { + return null; + } + // We can never have more then one record. + return cached.get(0); + } + + @Override + public void cache(String hostname, String cname, long originalTtl, EventLoop loop) { + checkNotNull(hostname, "hostname"); + checkNotNull(cname, "cname"); + checkNotNull(loop, "loop"); + cache.cache(hostname, cname, Math.max(minTtl, (int) Math.min(maxTtl, originalTtl)), loop); + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + public boolean clear(String hostname) { + return cache.clear(checkNotNull(hostname, "hostname")); + } + + // Package visibility for testing purposes + int minTtl() { + return minTtl; + } + + // Package visibility for testing purposes + int maxTtl() { + return maxTtl; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..b21fb95 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddressStreamProvider.java @@ -0,0 +1,165 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.PlatformDependent; +import io.netty.util.internal.SocketUtils; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.lang.reflect.Method; +import java.net.Inet6Address; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static io.netty.resolver.dns.DnsServerAddresses.sequential; + +/** + * A {@link DnsServerAddressStreamProvider} which will use predefined default DNS servers to use for DNS resolution. + * These defaults do not respect your host's machines defaults. + *

+ * This may use the JDK's blocking DNS resolution to bootstrap the default DNS server addresses. + */ +public final class DefaultDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(DefaultDnsServerAddressStreamProvider.class); + private static final String DEFAULT_FALLBACK_SERVER_PROPERTY = "io.netty.resolver.dns.defaultNameServerFallback"; + public static final DefaultDnsServerAddressStreamProvider INSTANCE = new DefaultDnsServerAddressStreamProvider(); + + private static final List DEFAULT_NAME_SERVER_LIST; + private static final DnsServerAddresses DEFAULT_NAME_SERVERS; + static final int DNS_PORT = 53; + + static { + final List defaultNameServers = new ArrayList(2); + if (!PlatformDependent.isAndroid()) { + // Only try to use when not on Android as the classes not exists there: + // See https://github.com/netty/netty/issues/8654 + DirContextUtils.addNameServers(defaultNameServers, DNS_PORT); + } + + // Only try when using on Java8 and lower as otherwise it will produce: + // WARNING: Illegal reflective access by io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider + if (PlatformDependent.javaVersion() < 9 && defaultNameServers.isEmpty()) { + try { + Class configClass = Class.forName("sun.net.dns.ResolverConfiguration"); + Method open = configClass.getMethod("open"); + Method nameservers = configClass.getMethod("nameservers"); + Object instance = open.invoke(null); + + @SuppressWarnings("unchecked") + final List list = (List) nameservers.invoke(instance); + for (String a: list) { + if (a != null) { + defaultNameServers.add(new InetSocketAddress(SocketUtils.addressByName(a), DNS_PORT)); + } + } + } catch (Exception ignore) { + // Failed to get the system name server list via reflection. + // Will add the default name servers afterwards. + } + } + + if (!defaultNameServers.isEmpty()) { + if (logger.isDebugEnabled()) { + logger.debug( + "Default DNS servers: {} (sun.net.dns.ResolverConfiguration)", defaultNameServers); + } + } else { + String defaultNameserverString = SystemPropertyUtil.get(DEFAULT_FALLBACK_SERVER_PROPERTY, null); + if (defaultNameserverString != null) { + for (String server : defaultNameserverString.split(",")) { + String dns = server.trim(); + if (!NetUtil.isValidIpV4Address(dns) && !NetUtil.isValidIpV6Address(dns)) { + throw new ExceptionInInitializerError(DEFAULT_FALLBACK_SERVER_PROPERTY + " doesn't" + + " contain a valid list of NameServers: " + defaultNameserverString); + } + defaultNameServers.add(SocketUtils.socketAddress(server.trim(), DNS_PORT)); + } + if (defaultNameServers.isEmpty()) { + throw new ExceptionInInitializerError(DEFAULT_FALLBACK_SERVER_PROPERTY + " doesn't" + + " contain a valid list of NameServers: " + defaultNameserverString); + } + + if (logger.isWarnEnabled()) { + logger.warn( + "Default DNS servers: {} (Configured by {} system property)", + defaultNameServers, DEFAULT_FALLBACK_SERVER_PROPERTY); + } + } else { + // Depending if IPv6 or IPv4 is used choose the correct DNS servers provided by google: + // https://developers.google.com/speed/public-dns/docs/using + // https://docs.oracle.com/javase/7/docs/api/java/net/doc-files/net-properties.html + if (NetUtil.isIpV6AddressesPreferred() || + (NetUtil.LOCALHOST instanceof Inet6Address && !NetUtil.isIpV4StackPreferred())) { + Collections.addAll( + defaultNameServers, + SocketUtils.socketAddress("2001:4860:4860::8888", DNS_PORT), + SocketUtils.socketAddress("2001:4860:4860::8844", DNS_PORT)); + } else { + Collections.addAll( + defaultNameServers, + SocketUtils.socketAddress("8.8.8.8", DNS_PORT), + SocketUtils.socketAddress("8.8.4.4", DNS_PORT)); + } + + if (logger.isWarnEnabled()) { + logger.warn( + "Default DNS servers: {} (Google Public DNS as a fallback)", defaultNameServers); + } + } + } + + DEFAULT_NAME_SERVER_LIST = Collections.unmodifiableList(defaultNameServers); + DEFAULT_NAME_SERVERS = sequential(DEFAULT_NAME_SERVER_LIST); + } + + private DefaultDnsServerAddressStreamProvider() { + } + + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + return DEFAULT_NAME_SERVERS.stream(); + } + + /** + * Returns the list of the system DNS server addresses. If it failed to retrieve the list of the system DNS server + * addresses from the environment, it will return {@code "8.8.8.8"} and {@code "8.8.4.4"}, the addresses of the + * Google public DNS servers. + */ + public static List defaultAddressList() { + return DEFAULT_NAME_SERVER_LIST; + } + + /** + * Returns the {@link DnsServerAddresses} that yields the system DNS server addresses sequentially. If it failed to + * retrieve the list of the system DNS server addresses from the environment, it will use {@code "8.8.8.8"} and + * {@code "8.8.4.4"}, the addresses of the Google public DNS servers. + *

+ * This method has the same effect with the following code: + *

+     * DnsServerAddresses.sequential(DnsServerAddresses.defaultAddressList());
+     * 
+ *

+ */ + public static DnsServerAddresses defaultAddresses() { + return DEFAULT_NAME_SERVERS; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddresses.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddresses.java new file mode 100644 index 0000000..936b99c --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DefaultDnsServerAddresses.java @@ -0,0 +1,47 @@ +/* + * Copyright 2015 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; +import java.util.List; + +abstract class DefaultDnsServerAddresses extends DnsServerAddresses { + + protected final List addresses; + private final String strVal; + + DefaultDnsServerAddresses(String type, List addresses) { + this.addresses = addresses; + + final StringBuilder buf = new StringBuilder(type.length() + 2 + addresses.size() * 16); + buf.append(type).append('('); + + for (InetSocketAddress a: addresses) { + buf.append(a).append(", "); + } + + buf.setLength(buf.length() - 2); + buf.append(')'); + + strVal = buf.toString(); + } + + @Override + public String toString() { + return strVal; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DirContextUtils.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DirContextUtils.java new file mode 100644 index 0000000..0f40c65 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DirContextUtils.java @@ -0,0 +1,77 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.internal.SocketUtils; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import javax.naming.Context; +import javax.naming.NamingException; +import javax.naming.directory.DirContext; +import javax.naming.directory.InitialDirContext; +import java.net.InetSocketAddress; +import java.net.URI; +import java.net.URISyntaxException; +import java.util.Hashtable; +import java.util.List; + +final class DirContextUtils { + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(DirContextUtils.class); + + private DirContextUtils() { } + + static void addNameServers(List defaultNameServers, int defaultPort) { + // Using jndi-dns to obtain the default name servers. + // + // See: + // - https://docs.oracle.com/javase/8/docs/technotes/guides/jndi/jndi-dns.html + // - https://mail.openjdk.java.net/pipermail/net-dev/2017-March/010695.html + Hashtable env = new Hashtable(); + env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory"); + env.put("java.naming.provider.url", "dns://"); + + try { + DirContext ctx = new InitialDirContext(env); + String dnsUrls = (String) ctx.getEnvironment().get("java.naming.provider.url"); + // Only try if not empty as otherwise we will produce an exception + if (dnsUrls != null && !dnsUrls.isEmpty()) { + String[] servers = dnsUrls.split(" "); + for (String server : servers) { + try { + URI uri = new URI(server); + String host = new URI(server).getHost(); + + if (host == null || host.isEmpty()) { + logger.debug( + "Skipping a nameserver URI as host portion could not be extracted: {}", server); + // If the host portion can not be parsed we should just skip this entry. + continue; + } + int port = uri.getPort(); + defaultNameServers.add(SocketUtils.socketAddress(uri.getHost(), port == -1 ? + defaultPort : port)); + } catch (URISyntaxException e) { + logger.debug("Skipping a malformed nameserver URI: {}", server, e); + } + } + } + } catch (NamingException ignore) { + // Will try reflection if this fails. + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java new file mode 100644 index 0000000..d3f7545 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressDecoder.java @@ -0,0 +1,67 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.IDN; +import java.net.InetAddress; +import java.net.UnknownHostException; + +import io.netty.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.handler.codec.dns.DnsRawRecord; +import io.netty.handler.codec.dns.DnsRecord; + +/** + * Decodes an {@link InetAddress} from an A or AAAA {@link DnsRawRecord}. + */ +final class DnsAddressDecoder { + + private static final int INADDRSZ4 = 4; + private static final int INADDRSZ6 = 16; + + /** + * Decodes an {@link InetAddress} from an A or AAAA {@link DnsRawRecord}. + * + * @param record the {@link DnsRecord}, most likely a {@link DnsRawRecord} + * @param name the host name of the decoded address + * @param decodeIdn whether to convert {@code name} to a unicode host name + * + * @return the {@link InetAddress}, or {@code null} if {@code record} is not a {@link DnsRawRecord} or + * its content is malformed + */ + static InetAddress decodeAddress(DnsRecord record, String name, boolean decodeIdn) { + if (!(record instanceof DnsRawRecord)) { + return null; + } + final ByteBuf content = ((ByteBufHolder) record).content(); + final int contentLen = content.readableBytes(); + if (contentLen != INADDRSZ4 && contentLen != INADDRSZ6) { + return null; + } + + final byte[] addrBytes = new byte[contentLen]; + content.getBytes(content.readerIndex(), addrBytes); + + try { + return InetAddress.getByAddress(decodeIdn ? IDN.toUnicode(name) : name, addrBytes); + } catch (UnknownHostException e) { + // Should never reach here. + throw new Error(e); + } + } + + private DnsAddressDecoder() { } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java new file mode 100644 index 0000000..ce54638 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolveContext.java @@ -0,0 +1,111 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.Collections; +import java.util.List; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.util.concurrent.Promise; + +final class DnsAddressResolveContext extends DnsResolveContext { + + private final DnsCache resolveCache; + private final AuthoritativeDnsServerCache authoritativeDnsServerCache; + private final boolean completeEarlyIfPossible; + + DnsAddressResolveContext(DnsNameResolver parent, Channel channel, Promise originalPromise, + String hostname, DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs, int allowedQueries, DnsCache resolveCache, + AuthoritativeDnsServerCache authoritativeDnsServerCache, + boolean completeEarlyIfPossible) { + super(parent, channel, originalPromise, hostname, DnsRecord.CLASS_IN, + parent.resolveRecordTypes(), additionals, nameServerAddrs, allowedQueries); + this.resolveCache = resolveCache; + this.authoritativeDnsServerCache = authoritativeDnsServerCache; + this.completeEarlyIfPossible = completeEarlyIfPossible; + } + + @Override + DnsResolveContext newResolverContext(DnsNameResolver parent, Channel channel, + Promise originalPromise, + String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs, int allowedQueries) { + return new DnsAddressResolveContext(parent, channel, originalPromise, hostname, additionals, nameServerAddrs, + allowedQueries, resolveCache, authoritativeDnsServerCache, completeEarlyIfPossible); + } + + @Override + InetAddress convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop) { + return decodeAddress(record, hostname, parent.isDecodeIdn()); + } + + @Override + List filterResults(List unfiltered) { + Collections.sort(unfiltered, PreferredAddressTypeComparator.comparator(parent.preferredAddressType())); + return unfiltered; + } + + @Override + boolean isCompleteEarly(InetAddress resolved) { + return completeEarlyIfPossible && parent.preferredAddressType().addressType() == resolved.getClass(); + } + + @Override + boolean isDuplicateAllowed() { + // We don't want include duplicates to mimic JDK behaviour. + return false; + } + + @Override + void cache(String hostname, DnsRecord[] additionals, + DnsRecord result, InetAddress convertedResult) { + resolveCache.cache(hostname, additionals, convertedResult, result.timeToLive(), channel().eventLoop()); + } + + @Override + void cache(String hostname, DnsRecord[] additionals, UnknownHostException cause) { + resolveCache.cache(hostname, additionals, cause, channel().eventLoop()); + } + + @Override + void doSearchDomainQuery(String hostname, Promise> nextPromise) { + // Query the cache for the hostname first and only do a query if we could not find it in the cache. + if (!DnsNameResolver.doResolveAllCached( + hostname, additionals, nextPromise, resolveCache, parent.resolvedInternetProtocolFamiliesUnsafe())) { + super.doSearchDomainQuery(hostname, nextPromise); + } + } + + @Override + DnsCache resolveCache() { + return resolveCache; + } + + @Override + AuthoritativeDnsServerCache authoritativeDnsServerCache() { + return authoritativeDnsServerCache; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java new file mode 100644 index 0000000..2b1da20 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsAddressResolverGroup.java @@ -0,0 +1,126 @@ +/* + * Copyright 2014 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: + * + * https://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.ChannelFactory; +import io.netty.channel.EventLoop; +import io.netty.channel.socket.DatagramChannel; +import io.netty.resolver.AddressResolver; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.InetSocketAddressResolver; +import io.netty.resolver.NameResolver; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.StringUtil; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import static io.netty.util.internal.PlatformDependent.newConcurrentHashMap; + +/** + * A {@link AddressResolverGroup} of {@link DnsNameResolver}s. + */ +public class DnsAddressResolverGroup extends AddressResolverGroup { + + private final DnsNameResolverBuilder dnsResolverBuilder; + + private final ConcurrentMap> resolvesInProgress = newConcurrentHashMap(); + private final ConcurrentMap>> resolveAllsInProgress = newConcurrentHashMap(); + + public DnsAddressResolverGroup(DnsNameResolverBuilder dnsResolverBuilder) { + this.dnsResolverBuilder = dnsResolverBuilder.copy(); + } + + public DnsAddressResolverGroup( + Class channelType, + DnsServerAddressStreamProvider nameServerProvider) { + this.dnsResolverBuilder = new DnsNameResolverBuilder(); + dnsResolverBuilder.channelType(channelType).nameServerProvider(nameServerProvider); + } + + public DnsAddressResolverGroup( + ChannelFactory channelFactory, + DnsServerAddressStreamProvider nameServerProvider) { + this.dnsResolverBuilder = new DnsNameResolverBuilder(); + dnsResolverBuilder.channelFactory(channelFactory).nameServerProvider(nameServerProvider); + } + + @SuppressWarnings("deprecation") + @Override + protected final AddressResolver newResolver(EventExecutor executor) throws Exception { + if (!(executor instanceof EventLoop)) { + throw new IllegalStateException( + "unsupported executor type: " + StringUtil.simpleClassName(executor) + + " (expected: " + StringUtil.simpleClassName(EventLoop.class)); + } + + // we don't really need to pass channelFactory and nameServerProvider separately, + // but still keep this to ensure backward compatibility with (potentially) override methods + EventLoop loop = dnsResolverBuilder.eventLoop; + return newResolver(loop == null ? (EventLoop) executor : loop, + dnsResolverBuilder.channelFactory(), + dnsResolverBuilder.nameServerProvider()); + } + + /** + * @deprecated Override {@link #newNameResolver(EventLoop, ChannelFactory, DnsServerAddressStreamProvider)}. + */ + @Deprecated + protected AddressResolver newResolver( + EventLoop eventLoop, ChannelFactory channelFactory, + DnsServerAddressStreamProvider nameServerProvider) throws Exception { + + final NameResolver resolver = new InflightNameResolver( + eventLoop, + newNameResolver(eventLoop, channelFactory, nameServerProvider), + resolvesInProgress, + resolveAllsInProgress); + + return newAddressResolver(eventLoop, resolver); + } + + /** + * Creates a new {@link NameResolver}. Override this method to create an alternative {@link NameResolver} + * implementation or override the default configuration. + */ + protected NameResolver newNameResolver(EventLoop eventLoop, + ChannelFactory channelFactory, + DnsServerAddressStreamProvider nameServerProvider) + throws Exception { + DnsNameResolverBuilder builder = dnsResolverBuilder.copy(); + + // once again, channelFactory and nameServerProvider are most probably set in builder already, + // but I do reassign them again to avoid corner cases with override methods + return builder.eventLoop(eventLoop) + .channelFactory(channelFactory) + .nameServerProvider(nameServerProvider) + .build(); + } + + /** + * Creates a new {@link AddressResolver}. Override this method to create an alternative {@link AddressResolver} + * implementation or override the default configuration. + */ + protected AddressResolver newAddressResolver(EventLoop eventLoop, + NameResolver resolver) + throws Exception { + return new InetSocketAddressResolver(eventLoop, resolver); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java new file mode 100644 index 0000000..21550c1 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCache.java @@ -0,0 +1,76 @@ +/* + * 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: + * + * https://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.EventLoop; +import io.netty.handler.codec.dns.DnsRecord; + +import java.net.InetAddress; +import java.util.List; + +/** + * A cache for DNS resolution entries. + */ +public interface DnsCache { + + /** + * Clears all the resolved addresses cached by this resolver. + * + * @see #clear(String) + */ + void clear(); + + /** + * Clears the resolved addresses of the specified host name from the cache of this resolver. + * + * @return {@code true} if and only if there was an entry for the specified host name in the cache and + * it has been removed by this method + */ + boolean clear(String hostname); + + /** + * Return the cached entries for the given hostname. + * @param hostname the hostname + * @param additionals the additional records + * @return the cached entries + */ + List get(String hostname, DnsRecord[] additionals); + + /** + * Create a new {@link DnsCacheEntry} and cache a resolved address for a given hostname. + * @param hostname the hostname + * @param additionals the additional records + * @param address the resolved address + * @param originalTtl the TTL as returned by the DNS server + * @param loop the {@link EventLoop} used to register the TTL timeout + * @return The {@link DnsCacheEntry} corresponding to this cache entry. + */ + DnsCacheEntry cache(String hostname, DnsRecord[] additionals, InetAddress address, long originalTtl, + EventLoop loop); + + /** + * Cache the resolution failure for a given hostname. + * Be aware this won't be called with timeout / cancel / transport exceptions. + * + * @param hostname the hostname + * @param additionals the additional records + * @param cause the resolution failure + * @param loop the {@link EventLoop} used to register the TTL timeout + * @return The {@link DnsCacheEntry} corresponding to this cache entry, or {@code null} if this cache doesn't + * support caching failed responses. + */ + DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java new file mode 100644 index 0000000..272e5b8 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCacheEntry.java @@ -0,0 +1,37 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.InetAddress; + +/** + * Represents the results from a previous DNS query which can be cached. + */ +public interface DnsCacheEntry { + /** + * Get the resolved address. + *

+ * This may be null if the resolution failed, and in that case {@link #cause()} will describe the failure. + * @return the resolved address. + */ + InetAddress address(); + + /** + * If the DNS query failed this will provide the rational. + * @return the rational for why the DNS query failed, or {@code null} if the query hasn't failed. + */ + Throwable cause(); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java new file mode 100644 index 0000000..c21399f --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsCnameCache.java @@ -0,0 +1,57 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; + +/** + * A cache for {@code CNAME}s. + */ +public interface DnsCnameCache { + + /** + * Returns the cached cname for the given hostname. + * + * @param hostname the hostname + * @return the cached entries or an {@code null} if none. + */ + String get(String hostname); + + /** + * Caches a cname entry that should be used for the given hostname. + * + * @param hostname the hostname + * @param cname the cname mapping. + * @param originalTtl the TTL as returned by the DNS server + * @param loop the {@link EventLoop} used to register the TTL timeout + */ + void cache(String hostname, String cname, long originalTtl, EventLoop loop); + + /** + * Clears all cached nameservers. + * + * @see #clear(String) + */ + void clear(); + + /** + * Clears the cached nameservers for the specified hostname. + * + * @return {@code true} if and only if there was an entry for the specified host name in the cache and + * it has been removed by this method + */ + boolean clear(String hostname); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsErrorCauseException.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsErrorCauseException.java new file mode 100644 index 0000000..ef39693 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsErrorCauseException.java @@ -0,0 +1,74 @@ +/* + * Copyright 2023 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsResponseCode; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SuppressJava6Requirement; +import io.netty.util.internal.ThrowableUtil; + +import java.net.UnknownHostException; + +/** + * A metadata carrier exception, to propagate {@link DnsResponseCode} information as an enrichment + * within the {@link UnknownHostException} cause. + */ +public final class DnsErrorCauseException extends RuntimeException { + + private static final long serialVersionUID = 7485145036717494533L; + + private final DnsResponseCode code; + + private DnsErrorCauseException(String message, DnsResponseCode code) { + super(message); + this.code = code; + } + + @SuppressJava6Requirement(reason = "uses Java 7+ Exception.(String, Throwable, boolean, boolean)" + + " but is guarded by version checks") + private DnsErrorCauseException(String message, DnsResponseCode code, boolean shared) { + super(message, null, false, true); + this.code = code; + assert shared; + } + + // Override fillInStackTrace() so we not populate the backtrace via a native call and so leak the + // Classloader. + @Override + public Throwable fillInStackTrace() { + return this; + } + + /** + * Returns the DNS error-code that caused the {@link UnknownHostException}. + * + * @return the DNS error-code that caused the {@link UnknownHostException}. + */ + public DnsResponseCode getCode() { + return code; + } + + static DnsErrorCauseException newStatic(String message, DnsResponseCode code, Class clazz, String method) { + final DnsErrorCauseException exception; + if (PlatformDependent.javaVersion() >= 7) { + exception = new DnsErrorCauseException(message, code, true); + } else { + exception = new DnsErrorCauseException(message, code); + } + return ThrowableUtil.unknownStackTrace(exception, clazz, method); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java new file mode 100644 index 0000000..d286bac --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolver.java @@ -0,0 +1,1405 @@ +/* + * Copyright 2014 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: + * + * https://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.bootstrap.Bootstrap; +import io.netty.buffer.ByteBuf; +import io.netty.buffer.Unpooled; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFactory; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandler; +import io.netty.channel.ChannelHandlerAdapter; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelOption; +import io.netty.channel.EventLoop; +import io.netty.channel.FixedRecvByteBufAllocator; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.SocketChannel; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.dns.DatagramDnsQueryEncoder; +import io.netty.handler.codec.dns.DatagramDnsResponse; +import io.netty.handler.codec.dns.DatagramDnsResponseDecoder; +import io.netty.handler.codec.dns.DefaultDnsRawRecord; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRawRecord; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.resolver.DefaultHostsFileEntriesResolver; +import io.netty.resolver.HostsFileEntries; +import io.netty.resolver.HostsFileEntriesResolver; +import io.netty.resolver.InetNameResolver; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.util.AttributeKey; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.FastThreadLocal; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GenericFutureListener; +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.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.lang.reflect.Method; +import java.net.IDN; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.NetworkInterface; +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; +import java.util.Enumeration; +import java.util.HashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.concurrent.TimeUnit; + +import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkPositive; + +/** + * A DNS-based {@link InetNameResolver}. + */ +public class DnsNameResolver extends InetNameResolver { + /** + * An attribute used to mark all channels created by the {@link DnsNameResolver}. + */ + public static final AttributeKey DNS_PIPELINE_ATTRIBUTE = + AttributeKey.newInstance("io.netty.resolver.dns.pipeline"); + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); + private static final String LOCALHOST = "localhost"; + private static final String WINDOWS_HOST_NAME; + private static final InetAddress LOCALHOST_ADDRESS; + private static final DnsRecord[] EMPTY_ADDITIONALS = new DnsRecord[0]; + private static final DnsRecordType[] IPV4_ONLY_RESOLVED_RECORD_TYPES = + {DnsRecordType.A}; + private static final InternetProtocolFamily[] IPV4_ONLY_RESOLVED_PROTOCOL_FAMILIES = + {InternetProtocolFamily.IPv4}; + private static final DnsRecordType[] IPV4_PREFERRED_RESOLVED_RECORD_TYPES = + {DnsRecordType.A, DnsRecordType.AAAA}; + private static final InternetProtocolFamily[] IPV4_PREFERRED_RESOLVED_PROTOCOL_FAMILIES = + {InternetProtocolFamily.IPv4, InternetProtocolFamily.IPv6}; + private static final DnsRecordType[] IPV6_ONLY_RESOLVED_RECORD_TYPES = + {DnsRecordType.AAAA}; + private static final InternetProtocolFamily[] IPV6_ONLY_RESOLVED_PROTOCOL_FAMILIES = + {InternetProtocolFamily.IPv6}; + private static final DnsRecordType[] IPV6_PREFERRED_RESOLVED_RECORD_TYPES = + {DnsRecordType.AAAA, DnsRecordType.A}; + private static final InternetProtocolFamily[] IPV6_PREFERRED_RESOLVED_PROTOCOL_FAMILIES = + {InternetProtocolFamily.IPv6, InternetProtocolFamily.IPv4}; + + private static final ChannelHandler NOOP_HANDLER = new ChannelHandlerAdapter() { + @Override + public boolean isSharable() { + return true; + } + }; + + static final ResolvedAddressTypes DEFAULT_RESOLVE_ADDRESS_TYPES; + static final String[] DEFAULT_SEARCH_DOMAINS; + private static final UnixResolverOptions DEFAULT_OPTIONS; + + static { + if (NetUtil.isIpV4StackPreferred() || !anyInterfaceSupportsIpV6()) { + DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_ONLY; + LOCALHOST_ADDRESS = NetUtil.LOCALHOST4; + } else { + if (NetUtil.isIpV6AddressesPreferred()) { + DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV6_PREFERRED; + LOCALHOST_ADDRESS = NetUtil.LOCALHOST6; + } else { + DEFAULT_RESOLVE_ADDRESS_TYPES = ResolvedAddressTypes.IPV4_PREFERRED; + LOCALHOST_ADDRESS = NetUtil.LOCALHOST4; + } + } + logger.debug("Default ResolvedAddressTypes: {}", DEFAULT_RESOLVE_ADDRESS_TYPES); + logger.debug("Localhost address: {}", LOCALHOST_ADDRESS); + + String hostName; + try { + hostName = PlatformDependent.isWindows() ? InetAddress.getLocalHost().getHostName() : null; + } catch (Exception ignore) { + hostName = null; + } + WINDOWS_HOST_NAME = hostName; + logger.debug("Windows hostname: {}", WINDOWS_HOST_NAME); + + String[] searchDomains; + try { + List list = PlatformDependent.isWindows() + ? getSearchDomainsHack() + : UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(); + searchDomains = list.toArray(EmptyArrays.EMPTY_STRINGS); + } catch (Exception ignore) { + // Failed to get the system name search domain list. + searchDomains = EmptyArrays.EMPTY_STRINGS; + } + DEFAULT_SEARCH_DOMAINS = searchDomains; + logger.debug("Default search domains: {}", Arrays.toString(DEFAULT_SEARCH_DOMAINS)); + + UnixResolverOptions options; + try { + options = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverOptions(); + } catch (Exception ignore) { + options = UnixResolverOptions.newBuilder().build(); + } + DEFAULT_OPTIONS = options; + logger.debug("Default {}", DEFAULT_OPTIONS); + } + + /** + * Returns {@code true} if any {@link NetworkInterface} supports {@code IPv6}, {@code false} otherwise. + */ + private static boolean anyInterfaceSupportsIpV6() { + for (NetworkInterface iface : NetUtil.NETWORK_INTERFACES) { + Enumeration addresses = iface.getInetAddresses(); + while (addresses.hasMoreElements()) { + InetAddress inetAddress = addresses.nextElement(); + if (inetAddress instanceof Inet6Address && !inetAddress.isAnyLocalAddress() && + !inetAddress.isLoopbackAddress() && !inetAddress.isLinkLocalAddress()) { + return true; + } + } + } + return false; + } + + @SuppressWarnings("unchecked") + private static List getSearchDomainsHack() throws Exception { + // Only try if not using Java9 and later + // See https://github.com/netty/netty/issues/9500 + if (PlatformDependent.javaVersion() < 9) { + // This code on Java 9+ yields a warning about illegal reflective access that will be denied in + // a future release. There doesn't seem to be a better way to get search domains for Windows yet. + Class configClass = Class.forName("sun.net.dns.ResolverConfiguration"); + Method open = configClass.getMethod("open"); + Method nameservers = configClass.getMethod("searchlist"); + Object instance = open.invoke(null); + + return (List) nameservers.invoke(instance); + } + return Collections.emptyList(); + } + + private static final DatagramDnsResponseDecoder DATAGRAM_DECODER = new DatagramDnsResponseDecoder() { + @Override + protected DnsResponse decodeResponse(ChannelHandlerContext ctx, DatagramPacket packet) throws Exception { + DnsResponse response = super.decodeResponse(ctx, packet); + if (packet.content().isReadable()) { + // If there is still something to read we did stop parsing because of a truncated message. + // This can happen if we enabled EDNS0 but our MTU is not big enough to handle all the + // data. + response.setTruncated(true); + + if (logger.isDebugEnabled()) { + logger.debug("{} RECEIVED: UDP [{}: {}] truncated packet received, consider adjusting " + + "maxPayloadSize for the {}.", ctx.channel(), response.id(), packet.sender(), + StringUtil.simpleClassName(DnsNameResolver.class)); + } + } + return response; + } + }; + private static final DatagramDnsQueryEncoder DATAGRAM_ENCODER = new DatagramDnsQueryEncoder(); + + private final Promise channelReadyPromise; + private final Channel ch; + + // Comparator that ensures we will try first to use the nameservers that use our preferred address type. + private final Comparator nameServerComparator; + /** + * Manages the {@link DnsQueryContext}s in progress and their query IDs. + */ + private final DnsQueryContextManager queryContextManager = new DnsQueryContextManager(); + + /** + * Cache for {@link #doResolve(String, Promise)} and {@link #doResolveAll(String, Promise)}. + */ + private final DnsCache resolveCache; + private final AuthoritativeDnsServerCache authoritativeDnsServerCache; + private final DnsCnameCache cnameCache; + + private final FastThreadLocal nameServerAddrStream = + new FastThreadLocal() { + @Override + protected DnsServerAddressStream initialValue() { + return dnsServerAddressStreamProvider.nameServerAddressStream(""); + } + }; + + private final long queryTimeoutMillis; + private final int maxQueriesPerResolve; + private final ResolvedAddressTypes resolvedAddressTypes; + private final InternetProtocolFamily[] resolvedInternetProtocolFamilies; + private final boolean recursionDesired; + private final int maxPayloadSize; + private final boolean optResourceEnabled; + private final HostsFileEntriesResolver hostsFileEntriesResolver; + private final DnsServerAddressStreamProvider dnsServerAddressStreamProvider; + private final String[] searchDomains; + private final int ndots; + private final boolean supportsAAAARecords; + private final boolean supportsARecords; + private final InternetProtocolFamily preferredAddressType; + private final DnsRecordType[] resolveRecordTypes; + private final boolean decodeIdn; + private final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory; + private final boolean completeOncePreferredResolved; + private final Bootstrap socketBootstrap; + private final boolean retryWithTcpOnTimeout; + + private final int maxNumConsolidation; + private final Map>> inflightLookups; + + /** + * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. + * + * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers + * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel} + * @param resolveCache the DNS resolved entries cache + * @param authoritativeDnsServerCache the cache used to find the authoritative DNS server for a domain + * @param dnsQueryLifecycleObserverFactory used to generate new instances of {@link DnsQueryLifecycleObserver} which + * can be used to track metrics for DNS servers. + * @param queryTimeoutMillis timeout of each DNS query in millis. {@code 0} disables the timeout. If not set or a + * negative number is set, the default timeout is used. + * @param resolvedAddressTypes the preferred address types + * @param recursionDesired if recursion desired flag must be set + * @param maxQueriesPerResolve the maximum allowed number of DNS queries for a given name resolution + * @param traceEnabled if trace is enabled + * @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 dnsServerAddressStreamProvider The {@link DnsServerAddressStreamProvider} used to determine the name + * servers for each hostname lookup. + * @param searchDomains the list of search domain + * (can be null, if so, will try to default to the underlying platform ones) + * @param ndots the ndots value + * @param decodeIdn {@code true} if domain / host names should be decoded to unicode when received. + * See rfc3492. + * @deprecated Use {@link DnsNameResolverBuilder}. + */ + @Deprecated + public DnsNameResolver( + EventLoop eventLoop, + ChannelFactory channelFactory, + final DnsCache resolveCache, + final DnsCache authoritativeDnsServerCache, + DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory, + long queryTimeoutMillis, + ResolvedAddressTypes resolvedAddressTypes, + boolean recursionDesired, + int maxQueriesPerResolve, + boolean traceEnabled, + int maxPayloadSize, + boolean optResourceEnabled, + HostsFileEntriesResolver hostsFileEntriesResolver, + DnsServerAddressStreamProvider dnsServerAddressStreamProvider, + String[] searchDomains, + int ndots, + boolean decodeIdn) { + this(eventLoop, channelFactory, resolveCache, + new AuthoritativeDnsServerCacheAdapter(authoritativeDnsServerCache), dnsQueryLifecycleObserverFactory, + queryTimeoutMillis, resolvedAddressTypes, recursionDesired, maxQueriesPerResolve, traceEnabled, + maxPayloadSize, optResourceEnabled, hostsFileEntriesResolver, dnsServerAddressStreamProvider, + searchDomains, ndots, decodeIdn); + } + + /** + * Creates a new DNS-based name resolver that communicates with the specified list of DNS servers. + * + * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS servers + * @param channelFactory the {@link ChannelFactory} that will create a {@link DatagramChannel} + * @param resolveCache the DNS resolved entries cache + * @param authoritativeDnsServerCache the cache used to find the authoritative DNS server for a domain + * @param dnsQueryLifecycleObserverFactory used to generate new instances of {@link DnsQueryLifecycleObserver} which + * can be used to track metrics for DNS servers. + * @param queryTimeoutMillis timeout of each DNS query in millis. {@code 0} disables the timeout. If not set or a + * negative number is set, the default timeout is used. + * @param resolvedAddressTypes the preferred address types + * @param recursionDesired if recursion desired flag must be set + * @param maxQueriesPerResolve the maximum allowed number of DNS queries for a given name resolution + * @param traceEnabled if trace is enabled + * @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 dnsServerAddressStreamProvider The {@link DnsServerAddressStreamProvider} used to determine the name + * servers for each hostname lookup. + * @param searchDomains the list of search domain + * (can be null, if so, will try to default to the underlying platform ones) + * @param ndots the ndots value + * @param decodeIdn {@code true} if domain / host names should be decoded to unicode when received. + * See rfc3492. + * @deprecated Use {@link DnsNameResolverBuilder}. + */ + @Deprecated + public DnsNameResolver( + EventLoop eventLoop, + ChannelFactory channelFactory, + final DnsCache resolveCache, + final AuthoritativeDnsServerCache authoritativeDnsServerCache, + DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory, + long queryTimeoutMillis, + ResolvedAddressTypes resolvedAddressTypes, + boolean recursionDesired, + int maxQueriesPerResolve, + boolean traceEnabled, + int maxPayloadSize, + boolean optResourceEnabled, + HostsFileEntriesResolver hostsFileEntriesResolver, + DnsServerAddressStreamProvider dnsServerAddressStreamProvider, + String[] searchDomains, + int ndots, + boolean decodeIdn) { + this(eventLoop, channelFactory, null, false, resolveCache, + NoopDnsCnameCache.INSTANCE, authoritativeDnsServerCache, null, + dnsQueryLifecycleObserverFactory, queryTimeoutMillis, resolvedAddressTypes, recursionDesired, + maxQueriesPerResolve, traceEnabled, maxPayloadSize, optResourceEnabled, hostsFileEntriesResolver, + dnsServerAddressStreamProvider, searchDomains, ndots, decodeIdn, false, 0); + } + + DnsNameResolver( + EventLoop eventLoop, + ChannelFactory channelFactory, + ChannelFactory socketChannelFactory, + boolean retryWithTcpOnTimeout, + final DnsCache resolveCache, + final DnsCnameCache cnameCache, + final AuthoritativeDnsServerCache authoritativeDnsServerCache, + SocketAddress localAddress, + DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory, + long queryTimeoutMillis, + ResolvedAddressTypes resolvedAddressTypes, + boolean recursionDesired, + int maxQueriesPerResolve, + boolean traceEnabled, + int maxPayloadSize, + boolean optResourceEnabled, + HostsFileEntriesResolver hostsFileEntriesResolver, + DnsServerAddressStreamProvider dnsServerAddressStreamProvider, + String[] searchDomains, + int ndots, + boolean decodeIdn, + boolean completeOncePreferredResolved, + int maxNumConsolidation) { + super(eventLoop); + this.queryTimeoutMillis = queryTimeoutMillis >= 0 + ? queryTimeoutMillis + : TimeUnit.SECONDS.toMillis(DEFAULT_OPTIONS.timeout()); + this.resolvedAddressTypes = resolvedAddressTypes != null ? resolvedAddressTypes : DEFAULT_RESOLVE_ADDRESS_TYPES; + this.recursionDesired = recursionDesired; + this.maxQueriesPerResolve = maxQueriesPerResolve > 0 ? maxQueriesPerResolve : DEFAULT_OPTIONS.attempts(); + this.maxPayloadSize = checkPositive(maxPayloadSize, "maxPayloadSize"); + this.optResourceEnabled = optResourceEnabled; + this.hostsFileEntriesResolver = checkNotNull(hostsFileEntriesResolver, "hostsFileEntriesResolver"); + this.dnsServerAddressStreamProvider = + checkNotNull(dnsServerAddressStreamProvider, "dnsServerAddressStreamProvider"); + this.resolveCache = checkNotNull(resolveCache, "resolveCache"); + this.cnameCache = checkNotNull(cnameCache, "cnameCache"); + this.dnsQueryLifecycleObserverFactory = traceEnabled ? + dnsQueryLifecycleObserverFactory instanceof NoopDnsQueryLifecycleObserverFactory ? + new LoggingDnsQueryLifeCycleObserverFactory() : + new BiDnsQueryLifecycleObserverFactory(new LoggingDnsQueryLifeCycleObserverFactory(), + dnsQueryLifecycleObserverFactory) : + checkNotNull(dnsQueryLifecycleObserverFactory, "dnsQueryLifecycleObserverFactory"); + this.searchDomains = searchDomains != null ? searchDomains.clone() : DEFAULT_SEARCH_DOMAINS; + this.ndots = ndots >= 0 ? ndots : DEFAULT_OPTIONS.ndots(); + this.decodeIdn = decodeIdn; + this.completeOncePreferredResolved = completeOncePreferredResolved; + this.retryWithTcpOnTimeout = retryWithTcpOnTimeout; + if (socketChannelFactory == null) { + socketBootstrap = null; + } else { + socketBootstrap = new Bootstrap(); + socketBootstrap.option(ChannelOption.SO_REUSEADDR, true) + .group(executor()) + .channelFactory(socketChannelFactory) + .attr(DNS_PIPELINE_ATTRIBUTE, Boolean.TRUE) + .handler(NOOP_HANDLER); + if (queryTimeoutMillis > 0 && queryTimeoutMillis <= Integer.MAX_VALUE) { + // Set the connect timeout to the same as queryTimeout as otherwise it might take a long + // time for the query to fail in case of a connection timeout. + socketBootstrap.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, (int) queryTimeoutMillis); + } + } + switch (this.resolvedAddressTypes) { + case IPV4_ONLY: + supportsAAAARecords = false; + supportsARecords = true; + resolveRecordTypes = IPV4_ONLY_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV4_ONLY_RESOLVED_PROTOCOL_FAMILIES; + break; + case IPV4_PREFERRED: + supportsAAAARecords = true; + supportsARecords = true; + resolveRecordTypes = IPV4_PREFERRED_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV4_PREFERRED_RESOLVED_PROTOCOL_FAMILIES; + break; + case IPV6_ONLY: + supportsAAAARecords = true; + supportsARecords = false; + resolveRecordTypes = IPV6_ONLY_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV6_ONLY_RESOLVED_PROTOCOL_FAMILIES; + break; + case IPV6_PREFERRED: + supportsAAAARecords = true; + supportsARecords = true; + resolveRecordTypes = IPV6_PREFERRED_RESOLVED_RECORD_TYPES; + resolvedInternetProtocolFamilies = IPV6_PREFERRED_RESOLVED_PROTOCOL_FAMILIES; + break; + default: + throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes); + } + preferredAddressType = preferredAddressType(this.resolvedAddressTypes); + this.authoritativeDnsServerCache = checkNotNull(authoritativeDnsServerCache, "authoritativeDnsServerCache"); + nameServerComparator = new NameServerComparator(preferredAddressType.addressType()); + this.maxNumConsolidation = maxNumConsolidation; + if (maxNumConsolidation > 0) { + inflightLookups = new HashMap>>(); + } else { + inflightLookups = null; + } + + Bootstrap b = new Bootstrap() + .group(executor()) + .channelFactory(channelFactory) + .attr(DNS_PIPELINE_ATTRIBUTE, Boolean.TRUE); + this.channelReadyPromise = executor().newPromise(); + final DnsResponseHandler responseHandler = + new DnsResponseHandler(channelReadyPromise); + b.handler(new ChannelInitializer() { + @Override + protected void initChannel(DatagramChannel ch) { + ch.pipeline().addLast(DATAGRAM_ENCODER, DATAGRAM_DECODER, responseHandler); + } + }); + + final ChannelFuture future; + if (localAddress == null) { + b.option(ChannelOption.DATAGRAM_CHANNEL_ACTIVE_ON_REGISTRATION, true); + future = b.register(); + } else { + future = b.bind(localAddress); + } + if (future.isDone()) { + Throwable cause = future.cause(); + if (cause != null) { + if (cause instanceof RuntimeException) { + throw (RuntimeException) cause; + } + if (cause instanceof Error) { + throw (Error) cause; + } + throw new IllegalStateException("Unable to create / register Channel", cause); + } + } else { + future.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + Throwable cause = future.cause(); + if (cause != null) { + channelReadyPromise.tryFailure(cause); + } + } + }); + } + ch = future.channel(); + ch.config().setRecvByteBufAllocator(new FixedRecvByteBufAllocator(maxPayloadSize)); + + ch.closeFuture().addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + resolveCache.clear(); + cnameCache.clear(); + authoritativeDnsServerCache.clear(); + } + }); + } + + static InternetProtocolFamily preferredAddressType(ResolvedAddressTypes resolvedAddressTypes) { + switch (resolvedAddressTypes) { + case IPV4_ONLY: + case IPV4_PREFERRED: + return InternetProtocolFamily.IPv4; + case IPV6_ONLY: + case IPV6_PREFERRED: + return InternetProtocolFamily.IPv6; + default: + throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes); + } + } + + // Only here to override in unit tests. + InetSocketAddress newRedirectServerAddress(InetAddress server) { + return new InetSocketAddress(server, DNS_PORT); + } + + final DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory() { + return dnsQueryLifecycleObserverFactory; + } + + /** + * Creates a new {@link DnsServerAddressStream} to following a redirected DNS query. By overriding this + * it provides the opportunity to sort the name servers before following a redirected DNS query. + * + * @param hostname the hostname. + * @param nameservers The addresses of the DNS servers which are used in the event of a redirect. This may + * contain resolved and unresolved addresses so the used {@link DnsServerAddressStream} must + * allow unresolved addresses if you want to include these as well. + * @return A {@link DnsServerAddressStream} which will be used to follow the DNS redirect or {@code null} if + * none should be followed. + */ + protected DnsServerAddressStream newRedirectDnsServerStream( + @SuppressWarnings("unused") String hostname, List nameservers) { + DnsServerAddressStream cached = authoritativeDnsServerCache().get(hostname); + if (cached == null || cached.size() == 0) { + // If there is no cache hit (which may be the case for example when a NoopAuthoritativeDnsServerCache + // is used), we will just directly use the provided nameservers. + Collections.sort(nameservers, nameServerComparator); + return new SequentialDnsServerAddressStream(nameservers, 0); + } + return cached; + } + + /** + * Returns the resolution cache. + */ + public DnsCache resolveCache() { + return resolveCache; + } + + /** + * Returns the {@link DnsCnameCache}. + */ + public DnsCnameCache cnameCache() { + return cnameCache; + } + + /** + * Returns the cache used for authoritative DNS servers for a domain. + */ + public AuthoritativeDnsServerCache authoritativeDnsServerCache() { + return authoritativeDnsServerCache; + } + + /** + * Returns the timeout of each DNS query performed by this resolver (in milliseconds). + * The default value is 5 seconds. + */ + public long queryTimeoutMillis() { + return queryTimeoutMillis; + } + + /** + * Returns the {@link ResolvedAddressTypes} resolved by {@link #resolve(String)}. + * The default value depends on the value of the system property {@code "java.net.preferIPv6Addresses"}. + */ + public ResolvedAddressTypes resolvedAddressTypes() { + return resolvedAddressTypes; + } + + InternetProtocolFamily[] resolvedInternetProtocolFamiliesUnsafe() { + return resolvedInternetProtocolFamilies; + } + + final String[] searchDomains() { + return searchDomains; + } + + final int ndots() { + return ndots; + } + + final boolean supportsAAAARecords() { + return supportsAAAARecords; + } + + final boolean supportsARecords() { + return supportsARecords; + } + + final InternetProtocolFamily preferredAddressType() { + return preferredAddressType; + } + + final DnsRecordType[] resolveRecordTypes() { + return resolveRecordTypes; + } + + final boolean isDecodeIdn() { + return decodeIdn; + } + + /** + * 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}. + */ + public boolean isRecursionDesired() { + return recursionDesired; + } + + /** + * Returns the maximum allowed number of DNS queries to send when resolving a host name. + * The default value is {@code 8}. + */ + public int maxQueriesPerResolve() { + return maxQueriesPerResolve; + } + + /** + * Returns the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. + */ + public int maxPayloadSize() { + return maxPayloadSize; + } + + /** + * Returns the automatic inclusion of a optional records that tries to give the remote DNS server a hint about how + * much data the resolver can read per response is enabled. + */ + public boolean isOptResourceEnabled() { + return optResourceEnabled; + } + + /** + * Returns the component that tries to resolve hostnames against the hosts file prior to asking to + * remotes DNS servers. + */ + public HostsFileEntriesResolver hostsFileEntriesResolver() { + return hostsFileEntriesResolver; + } + + /** + * Closes the internal datagram channel used for sending and receiving DNS messages, and clears all DNS resource + * records from the cache. Attempting to send a DNS query or to resolve a domain name will fail once this method + * has been called. + */ + @Override + public void close() { + if (ch.isOpen()) { + ch.close(); + } + } + + @Override + protected EventLoop executor() { + return (EventLoop) super.executor(); + } + + private InetAddress resolveHostsFileEntry(String hostname) { + if (hostsFileEntriesResolver == null) { + return null; + } + InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes); + return address == null && isLocalWindowsHost(hostname) ? LOCALHOST_ADDRESS : address; + } + + private List resolveHostsFileEntries(String hostname) { + if (hostsFileEntriesResolver == null) { + return null; + } + List addresses; + if (hostsFileEntriesResolver instanceof DefaultHostsFileEntriesResolver) { + addresses = ((DefaultHostsFileEntriesResolver) hostsFileEntriesResolver) + .addresses(hostname, resolvedAddressTypes); + } else { + InetAddress address = hostsFileEntriesResolver.address(hostname, resolvedAddressTypes); + addresses = address != null ? Collections.singletonList(address) : null; + } + return addresses == null && isLocalWindowsHost(hostname) ? + Collections.singletonList(LOCALHOST_ADDRESS) : addresses; + } + + /** + * Checks whether the given hostname is the localhost/host (computer) name on Windows OS. + * Windows OS removed the localhost/host (computer) name information from the hosts file in the later versions + * and such hostname cannot be resolved from hosts file. + * See https://github.com/netty/netty/issues/5386 + * See https://github.com/netty/netty/issues/11142 + */ + private static boolean isLocalWindowsHost(String hostname) { + return PlatformDependent.isWindows() && + (LOCALHOST.equalsIgnoreCase(hostname) || + (WINDOWS_HOST_NAME != null && WINDOWS_HOST_NAME.equalsIgnoreCase(hostname))); + } + + /** + * Resolves the specified name into an address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * + * @return the address as the result of the resolution + */ + public final Future resolve(String inetHost, Iterable additionals) { + return resolve(inetHost, additionals, executor().newPromise()); + } + + /** + * Resolves the specified name into an address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the address as the result of the resolution + */ + public final Future resolve(String inetHost, Iterable additionals, + Promise promise) { + checkNotNull(promise, "promise"); + DnsRecord[] additionalsArray = toArray(additionals, true); + try { + doResolve(inetHost, additionalsArray, promise, resolveCache); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } + } + + /** + * Resolves the specified host name and port into a list of address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * + * @return the list of the address as the result of the resolution + */ + public final Future> resolveAll(String inetHost, Iterable additionals) { + return resolveAll(inetHost, additionals, executor().>newPromise()); + } + + /** + * Resolves the specified host name and port into a list of address. + * + * @param inetHost the name to resolve + * @param additionals additional records ({@code OPT}) + * @param promise the {@link Promise} which will be fulfilled when the name resolution is finished + * + * @return the list of the address as the result of the resolution + */ + public final Future> resolveAll(String inetHost, Iterable additionals, + Promise> promise) { + checkNotNull(promise, "promise"); + DnsRecord[] additionalsArray = toArray(additionals, true); + try { + doResolveAll(inetHost, additionalsArray, promise, resolveCache); + return promise; + } catch (Exception e) { + return promise.setFailure(e); + } + } + + @Override + protected void doResolve(String inetHost, Promise promise) throws Exception { + doResolve(inetHost, EMPTY_ADDITIONALS, promise, resolveCache); + } + + /** + * Resolves the {@link DnsRecord}s that are matched by the specified {@link DnsQuestion}. Unlike + * {@link #query(DnsQuestion)}, this method handles redirection, CNAMEs and multiple name servers. + * If the specified {@link DnsQuestion} is {@code A} or {@code AAAA}, this method looks up the configured + * {@link HostsFileEntries} before sending a query to the name servers. If a match is found in the + * {@link HostsFileEntries}, a synthetic {@code A} or {@code AAAA} record will be returned. + * + * @param question the question + * + * @return the list of the {@link DnsRecord}s as the result of the resolution + */ + public final Future> resolveAll(DnsQuestion question) { + return resolveAll(question, EMPTY_ADDITIONALS, executor().>newPromise()); + } + + /** + * Resolves the {@link DnsRecord}s that are matched by the specified {@link DnsQuestion}. Unlike + * {@link #query(DnsQuestion)}, this method handles redirection, CNAMEs and multiple name servers. + * If the specified {@link DnsQuestion} is {@code A} or {@code AAAA}, this method looks up the configured + * {@link HostsFileEntries} before sending a query to the name servers. If a match is found in the + * {@link HostsFileEntries}, a synthetic {@code A} or {@code AAAA} record will be returned. + * + * @param question the question + * @param additionals additional records ({@code OPT}) + * + * @return the list of the {@link DnsRecord}s as the result of the resolution + */ + public final Future> resolveAll(DnsQuestion question, Iterable additionals) { + return resolveAll(question, additionals, executor().>newPromise()); + } + + /** + * Resolves the {@link DnsRecord}s that are matched by the specified {@link DnsQuestion}. Unlike + * {@link #query(DnsQuestion)}, this method handles redirection, CNAMEs and multiple name servers. + * If the specified {@link DnsQuestion} is {@code A} or {@code AAAA}, this method looks up the configured + * {@link HostsFileEntries} before sending a query to the name servers. If a match is found in the + * {@link HostsFileEntries}, a synthetic {@code A} or {@code AAAA} record will be returned. + * + * @param question the question + * @param additionals additional records ({@code OPT}) + * @param promise the {@link Promise} which will be fulfilled when the resolution is finished + * + * @return the list of the {@link DnsRecord}s as the result of the resolution + */ + public final Future> resolveAll(DnsQuestion question, Iterable additionals, + Promise> promise) { + final DnsRecord[] additionalsArray = toArray(additionals, true); + return resolveAll(question, additionalsArray, promise); + } + + private Future> resolveAll(DnsQuestion question, DnsRecord[] additionals, + Promise> promise) { + checkNotNull(question, "question"); + checkNotNull(promise, "promise"); + + // Respect /etc/hosts as well if the record type is A or AAAA. + final DnsRecordType type = question.type(); + final String hostname = question.name(); + + if (type == DnsRecordType.A || type == DnsRecordType.AAAA) { + final List hostsFileEntries = resolveHostsFileEntries(hostname); + if (hostsFileEntries != null) { + List result = new ArrayList(); + for (InetAddress hostsFileEntry : hostsFileEntries) { + ByteBuf content = null; + if (hostsFileEntry instanceof Inet4Address) { + if (type == DnsRecordType.A) { + content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress()); + } + } else if (hostsFileEntry instanceof Inet6Address) { + if (type == DnsRecordType.AAAA) { + content = Unpooled.wrappedBuffer(hostsFileEntry.getAddress()); + } + } + if (content != null) { + // Our current implementation does not support reloading the hosts file, + // so use a fairly large TTL (1 day, i.e. 86400 seconds). + result.add(new DefaultDnsRawRecord(hostname, type, 86400, content)); + } + } + + if (!result.isEmpty()) { + if (!trySuccess(promise, result)) { + // We were not able to transfer ownership, release the records to prevent leaks. + for (DnsRecord r: result) { + ReferenceCountUtil.safeRelease(r); + } + } + return promise; + } + } + } + + // It was not A/AAAA question or there was no entry in /etc/hosts. + final DnsServerAddressStream nameServerAddrs = + dnsServerAddressStreamProvider.nameServerAddressStream(hostname); + new DnsRecordResolveContext(this, ch, promise, question, additionals, + nameServerAddrs, maxQueriesPerResolve).resolve(promise); + return promise; + } + + private static DnsRecord[] toArray(Iterable additionals, boolean validateType) { + checkNotNull(additionals, "additionals"); + if (additionals instanceof Collection) { + Collection records = (Collection) additionals; + for (DnsRecord r: additionals) { + validateAdditional(r, validateType); + } + return records.toArray(new DnsRecord[records.size()]); + } + + Iterator additionalsIt = additionals.iterator(); + if (!additionalsIt.hasNext()) { + return EMPTY_ADDITIONALS; + } + List records = new ArrayList(); + do { + DnsRecord r = additionalsIt.next(); + validateAdditional(r, validateType); + records.add(r); + } while (additionalsIt.hasNext()); + + return records.toArray(new DnsRecord[records.size()]); + } + + private static void validateAdditional(DnsRecord record, boolean validateType) { + checkNotNull(record, "record"); + if (validateType && record instanceof DnsRawRecord) { + throw new IllegalArgumentException("DnsRawRecord implementations not allowed: " + record); + } + } + + private InetAddress loopbackAddress() { + return preferredAddressType().localhost(); + } + + /** + * Hook designed for extensibility so one can pass a different cache on each resolution attempt + * instead of using the global one. + */ + protected void doResolve(String inetHost, + DnsRecord[] additionals, + Promise promise, + DnsCache resolveCache) throws Exception { + if (inetHost == null || inetHost.isEmpty()) { + // If an empty hostname is used we should use "localhost", just like InetAddress.getByName(...) does. + promise.setSuccess(loopbackAddress()); + return; + } + final InetAddress address = NetUtil.createInetAddressFromIpAddressString(inetHost); + if (address != null) { + // The inetHost is actually an ipaddress. + promise.setSuccess(address); + return; + } + + final String hostname = hostname(inetHost); + + InetAddress hostsFileEntry = resolveHostsFileEntry(hostname); + if (hostsFileEntry != null) { + promise.setSuccess(hostsFileEntry); + return; + } + + if (!doResolveCached(hostname, additionals, promise, resolveCache)) { + doResolveUncached(hostname, additionals, promise, resolveCache, completeOncePreferredResolved); + } + } + + private boolean doResolveCached(String hostname, + DnsRecord[] additionals, + Promise promise, + DnsCache resolveCache) { + final List cachedEntries = resolveCache.get(hostname, additionals); + if (cachedEntries == null || cachedEntries.isEmpty()) { + return false; + } + + Throwable cause = cachedEntries.get(0).cause(); + if (cause == null) { + final int numEntries = cachedEntries.size(); + // Find the first entry with the preferred address type. + for (InternetProtocolFamily f : resolvedInternetProtocolFamilies) { + for (int i = 0; i < numEntries; i++) { + final DnsCacheEntry e = cachedEntries.get(i); + if (f.addressType().isInstance(e.address())) { + trySuccess(promise, e.address()); + return true; + } + } + } + return false; + } else { + tryFailure(promise, cause); + return true; + } + } + + static boolean trySuccess(Promise promise, T result) { + final boolean notifiedRecords = promise.trySuccess(result); + if (!notifiedRecords) { + // There is nothing really wrong with not be able to notify the promise as we may have raced here because + // of multiple queries that have been executed. Log it with trace level anyway just in case the user + // wants to better understand what happened. + logger.trace("Failed to notify success ({}) to a promise: {}", result, promise); + } + return notifiedRecords; + } + + private static void tryFailure(Promise promise, Throwable cause) { + if (!promise.tryFailure(cause)) { + // There is nothing really wrong with not be able to notify the promise as we may have raced here because + // of multiple queries that have been executed. Log it with trace level anyway just in case the user + // wants to better understand what happened. + logger.trace("Failed to notify failure to a promise: {}", promise, cause); + } + } + + private void doResolveUncached(String hostname, + DnsRecord[] additionals, + final Promise promise, + DnsCache resolveCache, boolean completeEarlyIfPossible) { + final Promise> allPromise = executor().newPromise(); + doResolveAllUncached(hostname, additionals, promise, allPromise, resolveCache, completeEarlyIfPossible); + allPromise.addListener(new FutureListener>() { + @Override + public void operationComplete(Future> future) { + if (future.isSuccess()) { + trySuccess(promise, future.getNow().get(0)); + } else { + tryFailure(promise, future.cause()); + } + } + }); + } + + @Override + protected void doResolveAll(String inetHost, Promise> promise) throws Exception { + doResolveAll(inetHost, EMPTY_ADDITIONALS, promise, resolveCache); + } + + /** + * Hook designed for extensibility so one can pass a different cache on each resolution attempt + * instead of using the global one. + */ + protected void doResolveAll(String inetHost, + DnsRecord[] additionals, + Promise> promise, + DnsCache resolveCache) throws Exception { + if (inetHost == null || inetHost.isEmpty()) { + // If an empty hostname is used we should use "localhost", just like InetAddress.getAllByName(...) does. + promise.setSuccess(Collections.singletonList(loopbackAddress())); + return; + } + final InetAddress address = NetUtil.createInetAddressFromIpAddressString(inetHost); + if (address != null) { + // The unresolvedAddress was created via a String that contains an ipaddress. + promise.setSuccess(Collections.singletonList(address)); + return; + } + + final String hostname = hostname(inetHost); + + List hostsFileEntries = resolveHostsFileEntries(hostname); + if (hostsFileEntries != null) { + promise.setSuccess(hostsFileEntries); + return; + } + + if (!doResolveAllCached(hostname, additionals, promise, resolveCache, resolvedInternetProtocolFamilies)) { + doResolveAllUncached(hostname, additionals, promise, promise, + resolveCache, completeOncePreferredResolved); + } + } + + static boolean doResolveAllCached(String hostname, + DnsRecord[] additionals, + Promise> promise, + DnsCache resolveCache, + InternetProtocolFamily[] resolvedInternetProtocolFamilies) { + final List cachedEntries = resolveCache.get(hostname, additionals); + if (cachedEntries == null || cachedEntries.isEmpty()) { + return false; + } + + Throwable cause = cachedEntries.get(0).cause(); + if (cause == null) { + List result = null; + final int numEntries = cachedEntries.size(); + for (InternetProtocolFamily f : resolvedInternetProtocolFamilies) { + for (int i = 0; i < numEntries; i++) { + final DnsCacheEntry e = cachedEntries.get(i); + if (f.addressType().isInstance(e.address())) { + if (result == null) { + result = new ArrayList(numEntries); + } + result.add(e.address()); + } + } + } + if (result != null) { + trySuccess(promise, result); + return true; + } + return false; + } else { + tryFailure(promise, cause); + return true; + } + } + + private void doResolveAllUncached(final String hostname, + final DnsRecord[] additionals, + final Promise originalPromise, + final Promise> promise, + final DnsCache resolveCache, + final boolean completeEarlyIfPossible) { + // Call doResolveUncached0(...) in the EventLoop as we may need to submit multiple queries which would need + // to submit multiple Runnable at the end if we are not already on the EventLoop. + EventExecutor executor = executor(); + if (executor.inEventLoop()) { + doResolveAllUncached0(hostname, additionals, originalPromise, + promise, resolveCache, completeEarlyIfPossible); + } else { + executor.execute(new Runnable() { + @Override + public void run() { + doResolveAllUncached0(hostname, additionals, originalPromise, + promise, resolveCache, completeEarlyIfPossible); + } + }); + } + } + + private void doResolveAllUncached0(final String hostname, + final DnsRecord[] additionals, + final Promise originalPromise, + final Promise> promise, + final DnsCache resolveCache, + final boolean completeEarlyIfPossible) { + + assert executor().inEventLoop(); + + if (inflightLookups != null && (additionals == null || additionals.length == 0)) { + Future> inflightFuture = inflightLookups.get(hostname); + if (inflightFuture != null) { + inflightFuture.addListener(new GenericFutureListener>>() { + @SuppressWarnings("unchecked") + @Override + public void operationComplete(Future> future) { + if (future.isSuccess()) { + promise.setSuccess((List) future.getNow()); + } else { + Throwable cause = future.cause(); + if (isTimeoutError(cause)) { + // The failure was caused by a timeout. This might be happening as a result of + // the remote server be overloaded for some short amount of time or because + // UDP packets were dropped on the floor. In this case lets try to just do the + // query explicit and don't cascade this possible temporary failure. + resolveNow(hostname, additionals, originalPromise, promise, + resolveCache, completeEarlyIfPossible); + } else { + promise.setFailure(cause); + } + } + } + }); + return; + // Check if we have space left in the map. + } else if (inflightLookups.size() < maxNumConsolidation) { + inflightLookups.put(hostname, promise); + promise.addListener(new GenericFutureListener>>() { + @Override + public void operationComplete(Future> future) { + inflightLookups.remove(hostname); + } + }); + } + } + resolveNow(hostname, additionals, originalPromise, promise, resolveCache, completeEarlyIfPossible); + } + + private void resolveNow(final String hostname, + final DnsRecord[] additionals, + final Promise originalPromise, + final Promise> promise, + final DnsCache resolveCache, + final boolean completeEarlyIfPossible) { + final DnsServerAddressStream nameServerAddrs = + dnsServerAddressStreamProvider.nameServerAddressStream(hostname); + DnsAddressResolveContext ctx = new DnsAddressResolveContext(this, ch, originalPromise, hostname, + additionals, nameServerAddrs, maxQueriesPerResolve, resolveCache, + authoritativeDnsServerCache, completeEarlyIfPossible); + ctx.resolve(promise); + } + + private static String hostname(String inetHost) { + String hostname = IDN.toASCII(inetHost); + // Check for https://bugs.java.com/bugdatabase/view_bug.do?bug_id=6894622 + if (StringUtil.endsWith(inetHost, '.') && !StringUtil.endsWith(hostname, '.')) { + hostname += "."; + } + return hostname; + } + + /** + * Sends a DNS query with the specified question. + */ + public Future> query(DnsQuestion question) { + return query(nextNameServerAddress(), question); + } + + /** + * Sends a DNS query with the specified question with additional records. + */ + public Future> query( + DnsQuestion question, Iterable additionals) { + return query(nextNameServerAddress(), question, additionals); + } + + /** + * Sends a DNS query with the specified question. + */ + public Future> query( + DnsQuestion question, Promise> promise) { + return query(nextNameServerAddress(), question, Collections.emptyList(), promise); + } + + private InetSocketAddress nextNameServerAddress() { + return nameServerAddrStream.get().next(); + } + + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question) { + + return query0(nameServerAddr, question, NoopDnsQueryLifecycleObserver.INSTANCE, EMPTY_ADDITIONALS, true, + ch.eventLoop().>newPromise()); + } + + /** + * Sends a DNS query with the specified question with additional records using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question, Iterable additionals) { + + return query0(nameServerAddr, question, NoopDnsQueryLifecycleObserver.INSTANCE, + toArray(additionals, false), true, + ch.eventLoop().>newPromise()); + } + + /** + * Sends a DNS query with the specified question using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question, + Promise> promise) { + + return query0(nameServerAddr, question, NoopDnsQueryLifecycleObserver.INSTANCE, + EMPTY_ADDITIONALS, true, promise); + } + + /** + * Sends a DNS query with the specified question with additional records using the specified name server list. + */ + public Future> query( + InetSocketAddress nameServerAddr, DnsQuestion question, + Iterable additionals, + Promise> promise) { + + return query0(nameServerAddr, question, NoopDnsQueryLifecycleObserver.INSTANCE, + toArray(additionals, false), true, promise); + } + + /** + * Returns {@code true} if the {@link Throwable} was caused by an timeout or transport error. + * These methods can be used on the {@link Future#cause()} that is returned by the various methods exposed by this + * {@link DnsNameResolver}. + */ + public static boolean isTransportOrTimeoutError(Throwable cause) { + return cause != null && cause.getCause() instanceof DnsNameResolverException; + } + + /** + * Returns {@code true} if the {@link Throwable} was caused by an timeout. + * These methods can be used on the {@link Future#cause()} that is returned by the various methods exposed by this + * {@link DnsNameResolver}. + */ + public static boolean isTimeoutError(Throwable cause) { + return cause != null && cause.getCause() instanceof DnsNameResolverTimeoutException; + } + + final void flushQueries() { + ch.flush(); + } + + final Future> query0( + InetSocketAddress nameServerAddr, DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + DnsRecord[] additionals, + boolean flush, + Promise> promise) { + + final Promise> castPromise = cast( + checkNotNull(promise, "promise")); + final int payloadSize = isOptResourceEnabled() ? maxPayloadSize() : 0; + try { + DnsQueryContext queryContext = new DatagramDnsQueryContext(ch, channelReadyPromise, nameServerAddr, + queryContextManager, payloadSize, isRecursionDesired(), queryTimeoutMillis(), question, additionals, + castPromise, socketBootstrap, retryWithTcpOnTimeout); + ChannelFuture future = queryContext.writeQuery(flush); + queryLifecycleObserver.queryWritten(nameServerAddr, future); + return castPromise; + } catch (Exception e) { + return castPromise.setFailure(e); + } + } + + @SuppressWarnings("unchecked") + private static Promise> cast(Promise promise) { + return (Promise>) promise; + } + + final DnsServerAddressStream newNameServerAddressStream(String hostname) { + return dnsServerAddressStreamProvider.nameServerAddressStream(hostname); + } + + private final class DnsResponseHandler extends ChannelInboundHandlerAdapter { + + private final Promise channelActivePromise; + + DnsResponseHandler(Promise channelActivePromise) { + this.channelActivePromise = channelActivePromise; + } + + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + final Channel qCh = ctx.channel(); + final DatagramDnsResponse res = (DatagramDnsResponse) msg; + final int queryId = res.id(); + logger.debug("{} RECEIVED: UDP [{}: {}], {}", qCh, queryId, res.sender(), res); + + final DnsQueryContext qCtx = queryContextManager.get(res.sender(), queryId); + if (qCtx == null) { + logger.debug("{} Received a DNS response with an unknown ID: UDP [{}: {}]", + qCh, queryId, res.sender()); + res.release(); + return; + } else if (qCtx.isDone()) { + logger.debug("{} Received a DNS response for a query that was timed out or cancelled: UDP [{}: {}]", + qCh, queryId, res.sender()); + res.release(); + return; + } + + // The context will handle truncation itself. + qCtx.finishSuccess(res, res.isTruncated()); + } + + @Override + public void channelActive(ChannelHandlerContext ctx) throws Exception { + super.channelActive(ctx); + channelActivePromise.trySuccess(ctx.channel()); + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (cause instanceof CorruptedFrameException) { + logger.debug("{} Unable to decode DNS response: UDP", ctx.channel(), cause); + } else { + logger.warn("{} Unexpected exception: UDP", ctx.channel(), cause); + } + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java new file mode 100644 index 0000000..574bdd3 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverBuilder.java @@ -0,0 +1,661 @@ +/* + * Copyright 2015 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: + * + * https://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.ChannelFactory; +import io.netty.channel.EventLoop; +import io.netty.channel.ReflectiveChannelFactory; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.SocketChannel; +import io.netty.resolver.HostsFileEntriesResolver; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.util.concurrent.Future; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.SocketAddress; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.intValue; + +/** + * A {@link DnsNameResolver} builder. + */ +public final class DnsNameResolverBuilder { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolverBuilder.class); + + volatile EventLoop eventLoop; + private ChannelFactory channelFactory; + private ChannelFactory socketChannelFactory; + private boolean retryOnTimeout; + + private DnsCache resolveCache; + private DnsCnameCache cnameCache; + private AuthoritativeDnsServerCache authoritativeDnsServerCache; + private SocketAddress localAddress; + private Integer minTtl; + private Integer maxTtl; + private Integer negativeTtl; + private long queryTimeoutMillis = -1; + private ResolvedAddressTypes resolvedAddressTypes = DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + private boolean completeOncePreferredResolved; + private boolean recursionDesired = true; + private int maxQueriesPerResolve = -1; + private boolean traceEnabled; + private int maxPayloadSize = 4096; + private boolean optResourceEnabled = true; + private HostsFileEntriesResolver hostsFileEntriesResolver = HostsFileEntriesResolver.DEFAULT; + private DnsServerAddressStreamProvider dnsServerAddressStreamProvider = + DnsServerAddressStreamProviders.platformDefault(); + private DnsQueryLifecycleObserverFactory dnsQueryLifecycleObserverFactory = + NoopDnsQueryLifecycleObserverFactory.INSTANCE; + private String[] searchDomains; + private int ndots = -1; + private boolean decodeIdn = true; + + private int maxNumConsolidation; + + /** + * Creates a new builder. + */ + public DnsNameResolverBuilder() { + } + + /** + * Creates a new builder. + * + * @param eventLoop the {@link EventLoop} which will perform the communication with the DNS + * servers. + */ + public DnsNameResolverBuilder(EventLoop eventLoop) { + eventLoop(eventLoop); + } + + /** + * Sets the {@link EventLoop} which will perform the communication with the DNS servers. + * + * @param eventLoop the {@link EventLoop} + * @return {@code this} + */ + public DnsNameResolverBuilder eventLoop(EventLoop eventLoop) { + this.eventLoop = eventLoop; + return this; + } + + protected ChannelFactory channelFactory() { + return this.channelFactory; + } + + /** + * Sets the {@link ChannelFactory} that will create a {@link DatagramChannel}. + * If TCP fallback should be supported as well it is required + * to call the {@link #socketChannelFactory(ChannelFactory)} or {@link #socketChannelType(Class)} method. + * + * @param channelFactory the {@link ChannelFactory} + * @return {@code this} + */ + public DnsNameResolverBuilder channelFactory(ChannelFactory channelFactory) { + this.channelFactory = channelFactory; + return this; + } + + /** + * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type. + * Use as an alternative to {@link #channelFactory(ChannelFactory)}. + * If TCP fallback should be supported as well it is required + * to call the {@link #socketChannelFactory(ChannelFactory)} or {@link #socketChannelType(Class)} method. + * + * @param channelType the type + * @return {@code this} + */ + public DnsNameResolverBuilder channelType(Class channelType) { + return channelFactory(new ReflectiveChannelFactory(channelType)); + } + + /** + * Sets the {@link ChannelFactory} that will create a {@link SocketChannel} for + * TCP fallback if needed. + * + * TCP fallback is not enabled by default and must be enabled by providing a non-null + * {@link ChannelFactory} for this method. + * + * @param channelFactory the {@link ChannelFactory} or {@code null} + * if TCP fallback should not be supported. + * By default, TCP fallback is not enabled. + * @return {@code this} + */ + public DnsNameResolverBuilder socketChannelFactory(ChannelFactory channelFactory) { + return socketChannelFactory(channelFactory, false); + } + + /** + * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type for + * TCP fallback if needed. + * Use as an alternative to {@link #socketChannelFactory(ChannelFactory)}. + * + * TCP fallback is not enabled by default and must be enabled by providing a non-null + * {@code channelType} for this method. + * + * @param channelType the type or {@code null} if TCP fallback + * should not be supported. By default, TCP fallback is not enabled. + * @return {@code this} + */ + public DnsNameResolverBuilder socketChannelType(Class channelType) { + return socketChannelType(channelType, false); + } + + /** + * Sets the {@link ChannelFactory} that will create a {@link SocketChannel} for + * TCP fallback if needed. + * + * TCP fallback is not enabled by default and must be enabled by providing a non-null + * {@link ChannelFactory} for this method. + * + * @param channelFactory the {@link ChannelFactory} or {@code null} + * if TCP fallback should not be supported. + * By default, TCP fallback is not enabled. + * @param retryOnTimeout if {@code true} the {@link DnsNameResolver} will also fallback to TCP if a timeout + * was detected, if {@code false} it will only try to use TCP if the response was marked + * as truncated. + * @return {@code this} + */ + public DnsNameResolverBuilder socketChannelFactory( + ChannelFactory channelFactory, boolean retryOnTimeout) { + this.socketChannelFactory = channelFactory; + this.retryOnTimeout = retryOnTimeout; + return this; + } + + /** + * Sets the {@link ChannelFactory} as a {@link ReflectiveChannelFactory} of this type for + * TCP fallback if needed. + * Use as an alternative to {@link #socketChannelFactory(ChannelFactory)}. + * + * TCP fallback is not enabled by default and must be enabled by providing a non-null + * {@code channelType} for this method. + * + * @param channelType the type or {@code null} if TCP fallback + * should not be supported. By default, TCP fallback is not enabled. + * @param retryOnTimeout if {@code true} the {@link DnsNameResolver} will also fallback to TCP if a timeout + * was detected, if {@code false} it will only try to use TCP if the response was marked + * as truncated. + * @return {@code this} + */ + public DnsNameResolverBuilder socketChannelType( + Class channelType, boolean retryOnTimeout) { + if (channelType == null) { + return socketChannelFactory(null, retryOnTimeout); + } + return socketChannelFactory(new ReflectiveChannelFactory(channelType), retryOnTimeout); + } + + /** + * Sets the cache for resolution results. + * + * @param resolveCache the DNS resolution results cache + * @return {@code this} + */ + public DnsNameResolverBuilder resolveCache(DnsCache resolveCache) { + this.resolveCache = resolveCache; + return this; + } + + /** + * Sets the cache for {@code CNAME} mappings. + * + * @param cnameCache the cache used to cache {@code CNAME} mappings for a domain. + * @return {@code this} + */ + public DnsNameResolverBuilder cnameCache(DnsCnameCache cnameCache) { + this.cnameCache = cnameCache; + return this; + } + + /** + * Set the factory used to generate objects which can observe individual DNS queries. + * @param lifecycleObserverFactory the factory used to generate objects which can observe individual DNS queries. + * @return {@code this} + */ + public DnsNameResolverBuilder dnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory + lifecycleObserverFactory) { + this.dnsQueryLifecycleObserverFactory = checkNotNull(lifecycleObserverFactory, "lifecycleObserverFactory"); + return this; + } + + /** + * Sets the cache for authoritative NS servers + * + * @param authoritativeDnsServerCache the authoritative NS servers cache + * @return {@code this} + * @deprecated Use {@link #authoritativeDnsServerCache(AuthoritativeDnsServerCache)} + */ + @Deprecated + public DnsNameResolverBuilder authoritativeDnsServerCache(DnsCache authoritativeDnsServerCache) { + this.authoritativeDnsServerCache = new AuthoritativeDnsServerCacheAdapter(authoritativeDnsServerCache); + return this; + } + + /** + * Sets the cache for authoritative NS servers + * + * @param authoritativeDnsServerCache the authoritative NS servers cache + * @return {@code this} + */ + public DnsNameResolverBuilder authoritativeDnsServerCache(AuthoritativeDnsServerCache authoritativeDnsServerCache) { + this.authoritativeDnsServerCache = authoritativeDnsServerCache; + return this; + } + + /** + * Configure the address that will be used to bind too. If {@code null} the default will be used. + * @param localAddress the bind address + * @return {@code this} + */ + public DnsNameResolverBuilder localAddress(SocketAddress localAddress) { + this.localAddress = localAddress; + return this; + } + + /** + * Sets the minimum and maximum TTL of the cached DNS resource records (in seconds). If the TTL of the DNS + * resource record returned by the DNS server is less than the minimum TTL or greater than the maximum TTL, + * this resolver will ignore the TTL from the DNS server and use the minimum TTL or the maximum TTL instead + * respectively. + * The default value is {@code 0} and {@link Integer#MAX_VALUE}, which practically tells this resolver to + * respect the TTL from the DNS server. + * + * @param minTtl the minimum TTL + * @param maxTtl the maximum TTL + * @return {@code this} + */ + public DnsNameResolverBuilder ttl(int minTtl, int maxTtl) { + this.maxTtl = maxTtl; + this.minTtl = minTtl; + return this; + } + + /** + * Sets the TTL of the cache for the failed DNS queries (in seconds). + * + * @param negativeTtl the TTL for failed cached queries + * @return {@code this} + */ + public DnsNameResolverBuilder negativeTtl(int negativeTtl) { + this.negativeTtl = negativeTtl; + return this; + } + + /** + * Sets the timeout of each DNS query performed by this resolver (in milliseconds). + * {@code 0} disables the timeout. If not set or a negative number is set, the default timeout is used. + * + * @param queryTimeoutMillis the query timeout + * @return {@code this} + */ + public DnsNameResolverBuilder queryTimeoutMillis(long queryTimeoutMillis) { + this.queryTimeoutMillis = queryTimeoutMillis; + return this; + } + + /** + * Compute a {@link ResolvedAddressTypes} from some {@link InternetProtocolFamily}s. + * An empty input will return the default value, based on "java.net" System properties. + * Valid inputs are (), (IPv4), (IPv6), (Ipv4, IPv6) and (IPv6, IPv4). + * @param internetProtocolFamilies a valid sequence of {@link InternetProtocolFamily}s + * @return a {@link ResolvedAddressTypes} + */ + public static ResolvedAddressTypes computeResolvedAddressTypes(InternetProtocolFamily... internetProtocolFamilies) { + if (internetProtocolFamilies == null || internetProtocolFamilies.length == 0) { + return DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; + } + if (internetProtocolFamilies.length > 2) { + throw new IllegalArgumentException("No more than 2 InternetProtocolFamilies"); + } + + switch(internetProtocolFamilies[0]) { + case IPv4: + return (internetProtocolFamilies.length >= 2 + && internetProtocolFamilies[1] == InternetProtocolFamily.IPv6) ? + ResolvedAddressTypes.IPV4_PREFERRED: ResolvedAddressTypes.IPV4_ONLY; + case IPv6: + return (internetProtocolFamilies.length >= 2 + && internetProtocolFamilies[1] == InternetProtocolFamily.IPv4) ? + ResolvedAddressTypes.IPV6_PREFERRED: ResolvedAddressTypes.IPV6_ONLY; + default: + throw new IllegalArgumentException( + "Couldn't resolve ResolvedAddressTypes from InternetProtocolFamily array"); + } + } + + /** + * Sets the list of the protocol families of the address resolved. + * You can use {@link DnsNameResolverBuilder#computeResolvedAddressTypes(InternetProtocolFamily...)} + * to get a {@link ResolvedAddressTypes} out of some {@link InternetProtocolFamily}s. + * + * @param resolvedAddressTypes the address types + * @return {@code this} + */ + public DnsNameResolverBuilder resolvedAddressTypes(ResolvedAddressTypes resolvedAddressTypes) { + this.resolvedAddressTypes = resolvedAddressTypes; + return this; + } + + /** + * If {@code true} {@link DnsNameResolver#resolveAll(String)} will notify the returned {@link Future} as + * soon as all queries for the preferred address-type are complete. + * + * @param completeOncePreferredResolved {@code true} to enable, {@code false} to disable. + * @return {@code this} + */ + public DnsNameResolverBuilder completeOncePreferredResolved(boolean completeOncePreferredResolved) { + this.completeOncePreferredResolved = completeOncePreferredResolved; + return this; + } + + /** + * Sets if this resolver has to send a DNS query with the RD (recursion desired) flag set. + * + * @param recursionDesired true if recursion is desired + * @return {@code this} + */ + public DnsNameResolverBuilder recursionDesired(boolean recursionDesired) { + this.recursionDesired = recursionDesired; + return this; + } + + /** + * Sets the maximum allowed number of DNS queries to send when resolving a host name. + * + * @param maxQueriesPerResolve the max number of queries + * @return {@code this} + */ + public DnsNameResolverBuilder maxQueriesPerResolve(int maxQueriesPerResolve) { + this.maxQueriesPerResolve = maxQueriesPerResolve; + return this; + } + + /** + * Sets if this resolver should generate the detailed trace information in an exception message so that + * it is easier to understand the cause of resolution failure. + * + * @param traceEnabled true if trace is enabled + * @return {@code this} + * @deprecated Prefer to {@linkplain #dnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory) configure} + * a {@link LoggingDnsQueryLifeCycleObserverFactory} instead. + */ + @Deprecated + public DnsNameResolverBuilder traceEnabled(boolean traceEnabled) { + this.traceEnabled = traceEnabled; + return this; + } + + /** + * Sets the capacity of the datagram packet buffer (in bytes). The default value is {@code 4096} bytes. + * + * @param maxPayloadSize the capacity of the datagram packet buffer + * @return {@code this} + */ + public DnsNameResolverBuilder maxPayloadSize(int maxPayloadSize) { + this.maxPayloadSize = maxPayloadSize; + return this; + } + + /** + * Enable the automatic inclusion of a optional records that tries to give the remote DNS server a hint about + * how much data the resolver can read per response. Some DNSServer may not support this and so fail to answer + * queries. If you find problems you may want to disable this. + * + * @param optResourceEnabled if optional records inclusion is enabled + * @return {@code this} + */ + public DnsNameResolverBuilder optResourceEnabled(boolean optResourceEnabled) { + this.optResourceEnabled = optResourceEnabled; + return this; + } + + /** + * @param hostsFileEntriesResolver the {@link HostsFileEntriesResolver} used to first check + * if the hostname is locally aliased. + * @return {@code this} + */ + public DnsNameResolverBuilder hostsFileEntriesResolver(HostsFileEntriesResolver hostsFileEntriesResolver) { + this.hostsFileEntriesResolver = hostsFileEntriesResolver; + return this; + } + + protected DnsServerAddressStreamProvider nameServerProvider() { + return this.dnsServerAddressStreamProvider; + } + + /** + * Set the {@link DnsServerAddressStreamProvider} which is used to determine which DNS server is used to resolve + * each hostname. + * @return {@code this}. + */ + public DnsNameResolverBuilder nameServerProvider(DnsServerAddressStreamProvider dnsServerAddressStreamProvider) { + this.dnsServerAddressStreamProvider = + checkNotNull(dnsServerAddressStreamProvider, "dnsServerAddressStreamProvider"); + 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 = new 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(EmptyArrays.EMPTY_STRINGS); + return this; + } + + /** + * Set the number of dots which must appear in a name before an initial absolute query is made. + * The default value is {@code 1}. + * + * @param ndots the ndots value + * @return {@code this} + */ + public DnsNameResolverBuilder ndots(int ndots) { + this.ndots = ndots; + return this; + } + + private DnsCache newCache() { + return new DefaultDnsCache(intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE), intValue(negativeTtl, 0)); + } + + private AuthoritativeDnsServerCache newAuthoritativeDnsServerCache() { + return new DefaultAuthoritativeDnsServerCache( + intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE), + // Let us use the sane ordering as DnsNameResolver will be used when returning + // nameservers from the cache. + new NameServerComparator(DnsNameResolver.preferredAddressType(resolvedAddressTypes).addressType())); + } + + private DnsCnameCache newCnameCache() { + return new DefaultDnsCnameCache( + intValue(minTtl, 0), intValue(maxTtl, Integer.MAX_VALUE)); + } + + /** + * Set if domain / host names should be decoded to unicode when received. + * See rfc3492. + * + * @param decodeIdn if should get decoded + * @return {@code this} + */ + public DnsNameResolverBuilder decodeIdn(boolean decodeIdn) { + this.decodeIdn = decodeIdn; + return this; + } + + /** + * Set the maximum size of the cache that is used to consolidate lookups for different hostnames when in-flight. + * This means if multiple lookups are done for the same hostname and still in-flight only one actual query will + * be made and the result will be cascaded to the others. + * + * @param maxNumConsolidation the maximum lookups to consolidate (different hostnames), or {@code 0} if + * no consolidation should be performed. + * @return {@code this} + */ + public DnsNameResolverBuilder consolidateCacheSize(int maxNumConsolidation) { + this.maxNumConsolidation = ObjectUtil.checkPositiveOrZero(maxNumConsolidation, "maxNumConsolidation"); + return this; + } + + /** + * Returns a new {@link DnsNameResolver} instance. + * + * @return a {@link DnsNameResolver} + */ + public DnsNameResolver build() { + if (eventLoop == null) { + throw new IllegalStateException("eventLoop should be specified to build a DnsNameResolver."); + } + + if (resolveCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) { + logger.debug("resolveCache and TTLs are mutually exclusive. TTLs are ignored."); + } + + if (cnameCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) { + logger.debug("cnameCache and TTLs are mutually exclusive. TTLs are ignored."); + } + + if (authoritativeDnsServerCache != null && (minTtl != null || maxTtl != null || negativeTtl != null)) { + logger.debug("authoritativeDnsServerCache and TTLs are mutually exclusive. TTLs are ignored."); + } + + DnsCache resolveCache = this.resolveCache != null ? this.resolveCache : newCache(); + DnsCnameCache cnameCache = this.cnameCache != null ? this.cnameCache : newCnameCache(); + AuthoritativeDnsServerCache authoritativeDnsServerCache = this.authoritativeDnsServerCache != null ? + this.authoritativeDnsServerCache : newAuthoritativeDnsServerCache(); + return new DnsNameResolver( + eventLoop, + channelFactory, + socketChannelFactory, + retryOnTimeout, + resolveCache, + cnameCache, + authoritativeDnsServerCache, + localAddress, + dnsQueryLifecycleObserverFactory, + queryTimeoutMillis, + resolvedAddressTypes, + recursionDesired, + maxQueriesPerResolve, + traceEnabled, + maxPayloadSize, + optResourceEnabled, + hostsFileEntriesResolver, + dnsServerAddressStreamProvider, + searchDomains, + ndots, + decodeIdn, + completeOncePreferredResolved, + maxNumConsolidation); + } + + /** + * Creates a copy of this {@link DnsNameResolverBuilder} + * + * @return {@link DnsNameResolverBuilder} + */ + public DnsNameResolverBuilder copy() { + DnsNameResolverBuilder copiedBuilder = new DnsNameResolverBuilder(); + + if (eventLoop != null) { + copiedBuilder.eventLoop(eventLoop); + } + + if (channelFactory != null) { + copiedBuilder.channelFactory(channelFactory); + } + + copiedBuilder.socketChannelFactory(socketChannelFactory, retryOnTimeout); + + if (resolveCache != null) { + copiedBuilder.resolveCache(resolveCache); + } + + if (cnameCache != null) { + copiedBuilder.cnameCache(cnameCache); + } + if (maxTtl != null && minTtl != null) { + copiedBuilder.ttl(minTtl, maxTtl); + } + + if (negativeTtl != null) { + copiedBuilder.negativeTtl(negativeTtl); + } + + if (authoritativeDnsServerCache != null) { + copiedBuilder.authoritativeDnsServerCache(authoritativeDnsServerCache); + } + + if (dnsQueryLifecycleObserverFactory != null) { + copiedBuilder.dnsQueryLifecycleObserverFactory(dnsQueryLifecycleObserverFactory); + } + + copiedBuilder.queryTimeoutMillis(queryTimeoutMillis); + copiedBuilder.resolvedAddressTypes(resolvedAddressTypes); + copiedBuilder.recursionDesired(recursionDesired); + copiedBuilder.maxQueriesPerResolve(maxQueriesPerResolve); + copiedBuilder.traceEnabled(traceEnabled); + copiedBuilder.maxPayloadSize(maxPayloadSize); + copiedBuilder.optResourceEnabled(optResourceEnabled); + copiedBuilder.hostsFileEntriesResolver(hostsFileEntriesResolver); + + if (dnsServerAddressStreamProvider != null) { + copiedBuilder.nameServerProvider(dnsServerAddressStreamProvider); + } + + if (searchDomains != null) { + copiedBuilder.searchDomains(Arrays.asList(searchDomains)); + } + + copiedBuilder.ndots(ndots); + copiedBuilder.decodeIdn(decodeIdn); + copiedBuilder.completeOncePreferredResolved(completeOncePreferredResolved); + copiedBuilder.localAddress(localAddress); + copiedBuilder.consolidateCacheSize(maxNumConsolidation); + return copiedBuilder; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java new file mode 100644 index 0000000..ac4dfc6 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverException.java @@ -0,0 +1,75 @@ +/* + * Copyright 2015 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.util.internal.EmptyArrays; +import io.netty.util.internal.ObjectUtil; + +import java.net.InetSocketAddress; + +/** + * A {@link RuntimeException} raised when {@link DnsNameResolver} failed to perform a successful query. + */ +public class DnsNameResolverException extends RuntimeException { + + private static final long serialVersionUID = -8826717909627131850L; + + private final InetSocketAddress remoteAddress; + private final DnsQuestion question; + + public DnsNameResolverException(InetSocketAddress remoteAddress, DnsQuestion question, String message) { + super(message); + this.remoteAddress = validateRemoteAddress(remoteAddress); + this.question = validateQuestion(question); + } + + public DnsNameResolverException( + InetSocketAddress remoteAddress, DnsQuestion question, String message, Throwable cause) { + super(message, cause); + this.remoteAddress = validateRemoteAddress(remoteAddress); + this.question = validateQuestion(question); + } + + private static InetSocketAddress validateRemoteAddress(InetSocketAddress remoteAddress) { + return ObjectUtil.checkNotNull(remoteAddress, "remoteAddress"); + } + + private static DnsQuestion validateQuestion(DnsQuestion question) { + return ObjectUtil.checkNotNull(question, "question"); + } + + /** + * Returns the {@link InetSocketAddress} of the DNS query that has failed. + */ + public InetSocketAddress remoteAddress() { + return remoteAddress; + } + + /** + * Returns the {@link DnsQuestion} of the DNS query that has failed. + */ + public DnsQuestion question() { + return question; + } + + // Suppress a warning since the method doesn't need synchronization + @Override + public Throwable fillInStackTrace() { + setStackTrace(EmptyArrays.EMPTY_STACK_TRACE); + return this; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java new file mode 100644 index 0000000..0fca7f0 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsNameResolverTimeoutException.java @@ -0,0 +1,33 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsQuestion; + +import java.net.InetSocketAddress; + +/** + * A {@link DnsNameResolverException} raised when {@link DnsNameResolver} failed to perform a successful query because + * of an timeout. In this case you may want to retry the operation. + */ +public final class DnsNameResolverTimeoutException extends DnsNameResolverException { + private static final long serialVersionUID = -8826717969627131854L; + + public DnsNameResolverTimeoutException( + InetSocketAddress remoteAddress, DnsQuestion question, String message) { + super(remoteAddress, question, message); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java new file mode 100644 index 0000000..348d15b --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContext.java @@ -0,0 +1,598 @@ +/* + * Copyright 2014 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: + * + * https://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.bootstrap.Bootstrap; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.ChannelPromise; +import io.netty.handler.codec.dns.AbstractDnsOptPseudoRrRecord; +import io.netty.handler.codec.dns.DnsQuery; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsSection; +import io.netty.handler.codec.dns.TcpDnsQueryEncoder; +import io.netty.handler.codec.dns.TcpDnsResponseDecoder; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.GenericFutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.SystemPropertyUtil; +import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetSocketAddress; +import java.net.SocketAddress; +import java.util.concurrent.CancellationException; +import java.util.concurrent.TimeUnit; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +abstract class DnsQueryContext { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsQueryContext.class); + private static final long ID_REUSE_ON_TIMEOUT_DELAY_MILLIS; + + static { + ID_REUSE_ON_TIMEOUT_DELAY_MILLIS = + SystemPropertyUtil.getLong("io.netty.resolver.dns.idReuseOnTimeoutDelayMillis", 10000); + logger.debug("-Dio.netty.resolver.dns.idReuseOnTimeoutDelayMillis: {}", ID_REUSE_ON_TIMEOUT_DELAY_MILLIS); + } + + private static final TcpDnsQueryEncoder TCP_ENCODER = new TcpDnsQueryEncoder(); + + private final Future channelReadyFuture; + private final Channel channel; + private final InetSocketAddress nameServerAddr; + private final DnsQueryContextManager queryContextManager; + private final Promise> promise; + + private final DnsQuestion question; + private final DnsRecord[] additionals; + private final DnsRecord optResource; + + private final boolean recursionDesired; + + private final Bootstrap socketBootstrap; + + private final boolean retryWithTcpOnTimeout; + private final long queryTimeoutMillis; + + private volatile Future timeoutFuture; + + private int id = Integer.MIN_VALUE; + + DnsQueryContext(Channel channel, + Future channelReadyFuture, + InetSocketAddress nameServerAddr, + DnsQueryContextManager queryContextManager, + int maxPayLoadSize, + boolean recursionDesired, + long queryTimeoutMillis, + DnsQuestion question, + DnsRecord[] additionals, + Promise> promise, + Bootstrap socketBootstrap, + boolean retryWithTcpOnTimeout) { + this.channel = checkNotNull(channel, "channel"); + this.queryContextManager = checkNotNull(queryContextManager, "queryContextManager"); + this.channelReadyFuture = checkNotNull(channelReadyFuture, "channelReadyFuture"); + this.nameServerAddr = checkNotNull(nameServerAddr, "nameServerAddr"); + this.question = checkNotNull(question, "question"); + this.additionals = checkNotNull(additionals, "additionals"); + this.promise = checkNotNull(promise, "promise"); + this.recursionDesired = recursionDesired; + this.queryTimeoutMillis = queryTimeoutMillis; + this.socketBootstrap = socketBootstrap; + this.retryWithTcpOnTimeout = retryWithTcpOnTimeout; + + if (maxPayLoadSize > 0 && + // Only add the extra OPT record if there is not already one. This is required as only one is allowed + // as per RFC: + // - https://datatracker.ietf.org/doc/html/rfc6891#section-6.1.1 + !hasOptRecord(additionals)) { + optResource = new AbstractDnsOptPseudoRrRecord(maxPayLoadSize, 0, 0) { + // We may want to remove this in the future and let the user just specify the opt record in the query. + }; + } else { + optResource = null; + } + } + + private static boolean hasOptRecord(DnsRecord[] additionals) { + if (additionals != null && additionals.length > 0) { + for (DnsRecord additional: additionals) { + if (additional.type() == DnsRecordType.OPT) { + return true; + } + } + } + return false; + } + + /** + * Returns {@code true} if the query was completed already. + * + * @return {@code true} if done. + */ + final boolean isDone() { + return promise.isDone(); + } + + /** + * Returns the {@link DnsQuestion} that will be written as part of the {@link DnsQuery}. + * + * @return the question. + */ + final DnsQuestion question() { + return question; + } + + /** + * Creates and returns a new {@link DnsQuery}. + * + * @param id the transaction id to use. + * @param nameServerAddr the nameserver to which the query will be send. + * @return the new query. + */ + protected abstract DnsQuery newQuery(int id, InetSocketAddress nameServerAddr); + + /** + * Returns the protocol that is used for the query. + * + * @return the protocol. + */ + protected abstract String protocol(); + + /** + * Write the query and return the {@link ChannelFuture} that is completed once the write completes. + * + * @param flush {@code true} if {@link Channel#flush()} should be called as well. + * @return the {@link ChannelFuture} that is notified once once the write completes. + */ + final ChannelFuture writeQuery(boolean flush) { + assert id == Integer.MIN_VALUE : this.getClass().getSimpleName() + + ".writeQuery(...) can only be executed once."; + + if ((id = queryContextManager.add(nameServerAddr, this)) == -1) { + // We did exhaust the id space, fail the query + IllegalStateException e = new IllegalStateException("query ID space exhausted: " + question()); + finishFailure("failed to send a query via " + protocol(), e, false); + return channel.newFailedFuture(e); + } + + // Ensure we remove the id from the QueryContextManager once the query completes. + promise.addListener(new FutureListener>() { + @Override + public void operationComplete(Future> future) { + // Cancel the timeout task. + Future timeoutFuture = DnsQueryContext.this.timeoutFuture; + if (timeoutFuture != null) { + DnsQueryContext.this.timeoutFuture = null; + timeoutFuture.cancel(false); + } + + Throwable cause = future.cause(); + if (cause instanceof DnsNameResolverTimeoutException || cause instanceof CancellationException) { + // This query was failed due a timeout or cancellation. Let's delay the removal of the id to reduce + // the risk of reusing the same id again while the remote nameserver might send the response after + // the timeout. + channel.eventLoop().schedule(new Runnable() { + @Override + public void run() { + removeFromContextManager(nameServerAddr); + } + }, ID_REUSE_ON_TIMEOUT_DELAY_MILLIS, TimeUnit.MILLISECONDS); + } else { + // Remove the id from the manager as soon as the query completes. This may be because of success, + // failure or cancellation + removeFromContextManager(nameServerAddr); + } + } + }); + final DnsQuestion question = question(); + final DnsQuery query = newQuery(id, nameServerAddr); + + query.setRecursionDesired(recursionDesired); + + query.addRecord(DnsSection.QUESTION, question); + + for (DnsRecord record: additionals) { + query.addRecord(DnsSection.ADDITIONAL, record); + } + + if (optResource != null) { + query.addRecord(DnsSection.ADDITIONAL, optResource); + } + + if (logger.isDebugEnabled()) { + logger.debug("{} WRITE: {}, [{}: {}], {}", + channel, protocol(), id, nameServerAddr, question); + } + + return sendQuery(query, flush); + } + + private void removeFromContextManager(InetSocketAddress nameServerAddr) { + DnsQueryContext self = queryContextManager.remove(nameServerAddr, id); + + assert self == this : "Removed DnsQueryContext is not the correct instance"; + } + + private ChannelFuture sendQuery(final DnsQuery query, final boolean flush) { + final ChannelPromise writePromise = channel.newPromise(); + if (channelReadyFuture.isSuccess()) { + writeQuery(query, flush, writePromise); + } else { + Throwable cause = channelReadyFuture.cause(); + if (cause != null) { + // the promise failed before so we should also fail this query. + failQuery(query, cause, writePromise); + } else { + // The promise is not complete yet, let's delay the query. + channelReadyFuture.addListener(new GenericFutureListener>() { + @Override + public void operationComplete(Future future) { + if (future.isSuccess()) { + // If the query is done in a late fashion (as the channel was not ready yet) we always flush + // to ensure we did not race with a previous flush() that was done when the Channel was not + // ready yet. + writeQuery(query, true, writePromise); + } else { + Throwable cause = future.cause(); + failQuery(query, cause, writePromise); + } + } + }); + } + } + return writePromise; + } + + private void failQuery(DnsQuery query, Throwable cause, ChannelPromise writePromise) { + try { + promise.tryFailure(cause); + writePromise.tryFailure(cause); + } finally { + ReferenceCountUtil.release(query); + } + } + + private void writeQuery(final DnsQuery query, + final boolean flush, ChannelPromise promise) { + final ChannelFuture writeFuture = flush ? channel.writeAndFlush(query, promise) : + channel.write(query, promise); + if (writeFuture.isDone()) { + onQueryWriteCompletion(queryTimeoutMillis, writeFuture); + } else { + writeFuture.addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + onQueryWriteCompletion(queryTimeoutMillis, writeFuture); + } + }); + } + } + + private void onQueryWriteCompletion(final long queryTimeoutMillis, + ChannelFuture writeFuture) { + if (!writeFuture.isSuccess()) { + finishFailure("failed to send a query '" + id + "' via " + protocol(), writeFuture.cause(), false); + return; + } + + // Schedule a query timeout task if necessary. + if (queryTimeoutMillis > 0) { + timeoutFuture = channel.eventLoop().schedule(new Runnable() { + @Override + public void run() { + if (promise.isDone()) { + // Received a response before the query times out. + return; + } + + finishFailure("query '" + id + "' via " + protocol() + " timed out after " + + queryTimeoutMillis + " milliseconds", null, true); + } + }, queryTimeoutMillis, TimeUnit.MILLISECONDS); + } + } + + /** + * Notifies the original {@link Promise} that the response for the query was received. + * This method takes ownership of passed {@link AddressedEnvelope}. + */ + void finishSuccess(AddressedEnvelope envelope, boolean truncated) { + // Check if the response was not truncated or if a fallback to TCP is possible. + if (!truncated || !retryWithTcp(envelope)) { + final DnsResponse res = envelope.content(); + if (res.count(DnsSection.QUESTION) != 1) { + logger.warn("{} Received a DNS response with invalid number of questions. Expected: 1, found: {}", + channel, envelope); + } else if (!question().equals(res.recordAt(DnsSection.QUESTION))) { + logger.warn("{} Received a mismatching DNS response. Expected: [{}], found: {}", + channel, question(), envelope); + } else if (trySuccess(envelope)) { + return; // Ownership transferred, don't release + } + envelope.release(); + } + } + + @SuppressWarnings("unchecked") + private boolean trySuccess(AddressedEnvelope envelope) { + return promise.trySuccess((AddressedEnvelope) envelope); + } + + /** + * Notifies the original {@link Promise} that the query completes because of an failure. + */ + final boolean finishFailure(String message, Throwable cause, boolean timeout) { + if (promise.isDone()) { + return false; + } + final DnsQuestion question = question(); + + final StringBuilder buf = new StringBuilder(message.length() + 128); + buf.append('[') + .append(id) + .append(": ") + .append(nameServerAddr) + .append("] ") + .append(question) + .append(' ') + .append(message) + .append(" (no stack trace available)"); + + final DnsNameResolverException e; + if (timeout) { + // This was caused by a timeout so use DnsNameResolverTimeoutException to allow the user to + // handle it special (like retry the query). + e = new DnsNameResolverTimeoutException(nameServerAddr, question, buf.toString()); + if (retryWithTcpOnTimeout && retryWithTcp(e)) { + // We did successfully retry with TCP. + return false; + } + } else { + e = new DnsNameResolverException(nameServerAddr, question, buf.toString(), cause); + } + return promise.tryFailure(e); + } + + /** + * Retry the original query with TCP if possible. + * + * @param originalResult the result of the original {@link DnsQueryContext}. + * @return {@code true} if retry via TCP is supported and so the ownership of + * {@code originalResult} was transferred, {@code false} otherwise. + */ + private boolean retryWithTcp(final Object originalResult) { + if (socketBootstrap == null) { + return false; + } + + socketBootstrap.connect(nameServerAddr).addListener(new ChannelFutureListener() { + @Override + public void operationComplete(ChannelFuture future) { + if (!future.isSuccess()) { + logger.debug("{} Unable to fallback to TCP [{}: {}]", + future.channel(), id, nameServerAddr, future.cause()); + + // TCP fallback failed, just use the truncated response or error. + finishOriginal(originalResult, future); + return; + } + final Channel tcpCh = future.channel(); + Promise> promise = + tcpCh.eventLoop().newPromise(); + final TcpDnsQueryContext tcpCtx = new TcpDnsQueryContext(tcpCh, channelReadyFuture, + (InetSocketAddress) tcpCh.remoteAddress(), queryContextManager, 0, + recursionDesired, queryTimeoutMillis, question(), additionals, promise); + tcpCh.pipeline().addLast(TCP_ENCODER); + tcpCh.pipeline().addLast(new TcpDnsResponseDecoder()); + tcpCh.pipeline().addLast(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + Channel tcpCh = ctx.channel(); + DnsResponse response = (DnsResponse) msg; + int queryId = response.id(); + + if (logger.isDebugEnabled()) { + logger.debug("{} RECEIVED: TCP [{}: {}], {}", tcpCh, queryId, + tcpCh.remoteAddress(), response); + } + + DnsQueryContext foundCtx = queryContextManager.get(nameServerAddr, queryId); + if (foundCtx != null && foundCtx.isDone()) { + logger.debug("{} Received a DNS response for a query that was timed out or cancelled " + + ": TCP [{}: {}]", tcpCh, queryId, nameServerAddr); + response.release(); + } else if (foundCtx == tcpCtx) { + tcpCtx.finishSuccess(new AddressedEnvelopeAdapter( + (InetSocketAddress) ctx.channel().remoteAddress(), + (InetSocketAddress) ctx.channel().localAddress(), + response), false); + } else { + response.release(); + tcpCtx.finishFailure("Received TCP DNS response with unexpected ID", null, false); + if (logger.isDebugEnabled()) { + logger.debug("{} Received a DNS response with an unexpected ID: TCP [{}: {}]", + tcpCh, queryId, tcpCh.remoteAddress()); + } + } + } + + @Override + public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) { + if (tcpCtx.finishFailure( + "TCP fallback error", cause, false) && logger.isDebugEnabled()) { + logger.debug("{} Error during processing response: TCP [{}: {}]", + ctx.channel(), id, + ctx.channel().remoteAddress(), cause); + } + } + }); + + promise.addListener( + new FutureListener>() { + @Override + public void operationComplete( + Future> future) { + if (future.isSuccess()) { + finishSuccess(future.getNow(), false); + // Release the original result. + ReferenceCountUtil.release(originalResult); + } else { + // TCP fallback failed, just use the truncated response or error. + finishOriginal(originalResult, future); + } + tcpCh.close(); + } + }); + tcpCtx.writeQuery(true); + } + }); + return true; + } + + @SuppressWarnings("unchecked") + private void finishOriginal(Object originalResult, Future future) { + if (originalResult instanceof Throwable) { + Throwable error = (Throwable) originalResult; + ThrowableUtil.addSuppressed(error, future.cause()); + promise.tryFailure(error); + } else { + finishSuccess((AddressedEnvelope) originalResult, false); + } + } + + private static final class AddressedEnvelopeAdapter implements AddressedEnvelope { + private final InetSocketAddress sender; + private final InetSocketAddress recipient; + private final DnsResponse response; + + AddressedEnvelopeAdapter(InetSocketAddress sender, InetSocketAddress recipient, DnsResponse response) { + this.sender = sender; + this.recipient = recipient; + this.response = response; + } + + @Override + public DnsResponse content() { + return response; + } + + @Override + public InetSocketAddress sender() { + return sender; + } + + @Override + public InetSocketAddress recipient() { + return recipient; + } + + @Override + public AddressedEnvelope retain() { + response.retain(); + return this; + } + + @Override + public AddressedEnvelope retain(int increment) { + response.retain(increment); + return this; + } + + @Override + public AddressedEnvelope touch() { + response.touch(); + return this; + } + + @Override + public AddressedEnvelope touch(Object hint) { + response.touch(hint); + return this; + } + + @Override + public int refCnt() { + return response.refCnt(); + } + + @Override + public boolean release() { + return response.release(); + } + + @Override + public boolean release(int decrement) { + return response.release(decrement); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + + if (!(obj instanceof AddressedEnvelope)) { + return false; + } + + @SuppressWarnings("unchecked") + final AddressedEnvelope that = (AddressedEnvelope) obj; + if (sender() == null) { + if (that.sender() != null) { + return false; + } + } else if (!sender().equals(that.sender())) { + return false; + } + + if (recipient() == null) { + if (that.recipient() != null) { + return false; + } + } else if (!recipient().equals(that.recipient())) { + return false; + } + + return response.equals(obj); + } + + @Override + public int hashCode() { + int hashCode = response.hashCode(); + if (sender() != null) { + hashCode = hashCode * 31 + sender().hashCode(); + } + if (recipient() != null) { + hashCode = hashCode * 31 + recipient().hashCode(); + } + return hashCode; + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContextManager.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContextManager.java new file mode 100644 index 0000000..069e796 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryContextManager.java @@ -0,0 +1,178 @@ +/* + * Copyright 2015 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: + * + * https://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.collection.IntObjectHashMap; +import io.netty.util.collection.IntObjectMap; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.HashMap; +import java.util.Map; + +final class DnsQueryContextManager { + + /** + * A map whose key is the DNS server address and value is the map of the DNS query ID and its corresponding + * {@link DnsQueryContext}. + */ + private final Map map = + new HashMap(); + + /** + * Add {@link DnsQueryContext} to the context manager and return the ID that should be used for the query. + * This method will return {@code -1} if an ID could not be generated and the context was not stored. + * + * @param nameServerAddr The {@link InetSocketAddress} of the nameserver to query. + * @param qCtx The {@link {@link DnsQueryContext} to store. + * @return the ID that should be used or {@code -1} if none could be generated. + */ + int add(InetSocketAddress nameServerAddr, DnsQueryContext qCtx) { + final DnsQueryContextMap contexts = getOrCreateContextMap(nameServerAddr); + return contexts.add(qCtx); + } + + /** + * Return the {@link DnsQueryContext} for the given {@link InetSocketAddress} and id or {@code null} if + * none could be found. + * + * @param nameServerAddr The {@link InetSocketAddress} of the nameserver. + * @param id The id that identifies the {@link DnsQueryContext} and was used for the query. + * @return The context or {@code null} if none could be found. + */ + DnsQueryContext get(InetSocketAddress nameServerAddr, int id) { + final DnsQueryContextMap contexts = getContextMap(nameServerAddr); + if (contexts == null) { + return null; + } + return contexts.get(id); + } + + /** + * Remove the {@link DnsQueryContext} for the given {@link InetSocketAddress} and id or {@code null} if + * none could be found. + * + * @param nameServerAddr The {@link InetSocketAddress} of the nameserver. + * @param id The id that identifies the {@link DnsQueryContext} and was used for the query. + * @return The context or {@code null} if none could be removed. + */ + DnsQueryContext remove(InetSocketAddress nameServerAddr, int id) { + final DnsQueryContextMap contexts = getContextMap(nameServerAddr); + if (contexts == null) { + return null; + } + return contexts.remove(id); + } + + private DnsQueryContextMap getContextMap(InetSocketAddress nameServerAddr) { + synchronized (map) { + return map.get(nameServerAddr); + } + } + + private DnsQueryContextMap getOrCreateContextMap(InetSocketAddress nameServerAddr) { + synchronized (map) { + final DnsQueryContextMap contexts = map.get(nameServerAddr); + if (contexts != null) { + return contexts; + } + + final DnsQueryContextMap newContexts = new DnsQueryContextMap(); + final InetAddress a = nameServerAddr.getAddress(); + final int port = nameServerAddr.getPort(); + DnsQueryContextMap old = map.put(nameServerAddr, newContexts); + // Assert that we didn't replace an existing mapping. + assert old == null : "DnsQueryContextMap already exists for " + nameServerAddr; + + InetSocketAddress extraAddress = null; + if (a instanceof Inet4Address) { + // Also add the mapping for the IPv4-compatible IPv6 address. + final Inet4Address a4 = (Inet4Address) a; + if (a4.isLoopbackAddress()) { + extraAddress = new InetSocketAddress(NetUtil.LOCALHOST6, port); + } else { + extraAddress = new InetSocketAddress(toCompactAddress(a4), port); + } + } else if (a instanceof Inet6Address) { + // Also add the mapping for the IPv4 address if this IPv6 address is compatible. + final Inet6Address a6 = (Inet6Address) a; + if (a6.isLoopbackAddress()) { + extraAddress = new InetSocketAddress(NetUtil.LOCALHOST4, port); + } else if (a6.isIPv4CompatibleAddress()) { + extraAddress = new InetSocketAddress(toIPv4Address(a6), port); + } + } + if (extraAddress != null) { + old = map.put(extraAddress, newContexts); + // Assert that we didn't replace an existing mapping. + assert old == null : "DnsQueryContextMap already exists for " + extraAddress; + } + + return newContexts; + } + } + + private static Inet6Address toCompactAddress(Inet4Address a4) { + byte[] b4 = a4.getAddress(); + byte[] b6 = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, b4[0], b4[1], b4[2], b4[3] }; + try { + return (Inet6Address) InetAddress.getByAddress(b6); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + + private static Inet4Address toIPv4Address(Inet6Address a6) { + assert a6.isIPv4CompatibleAddress(); + + byte[] b6 = a6.getAddress(); + byte[] b4 = { b6[12], b6[13], b6[14], b6[15] }; + try { + return (Inet4Address) InetAddress.getByAddress(b4); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + + private static final class DnsQueryContextMap { + + private final DnsQueryIdSpace idSpace = new DnsQueryIdSpace(); + + // We increment on every usage so start with -1, this will ensure we start with 0 as first id. + private final IntObjectMap map = new IntObjectHashMap(); + + synchronized int add(DnsQueryContext ctx) { + int id = idSpace.nextId(); + DnsQueryContext oldCtx = map.put(id, ctx); + assert oldCtx == null; + return id; + } + + synchronized DnsQueryContext get(int id) { + return map.get(id); + } + + synchronized DnsQueryContext remove(int id) { + idSpace.pushId(id); + return map.remove(id); + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryIdSpace.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryIdSpace.java new file mode 100644 index 0000000..9dbb9b9 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryIdSpace.java @@ -0,0 +1,207 @@ +/* + * Copyright 2024 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: + * + * https://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.internal.MathUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.ThreadLocalRandom; + +import java.util.Random; + +/** + * Special data-structure that will allow to retrieve the next query id to use, while still guarantee some sort + * of randomness. + * The query id will be between 0 (inclusive) and 65535 (inclusive) as defined by the RFC. + */ +final class DnsQueryIdSpace { + private static final int MAX_ID = 65535; + private static final int BUCKETS = 4; + // Each bucket is 16kb of size. + private static final int BUCKET_SIZE = (MAX_ID + 1) / BUCKETS; + + // If there are other buckets left that have at least 500 usable ids we will drop an unused bucket. + private static final int BUCKET_DROP_THRESHOLD = 500; + private final DnsQueryIdRange[] idBuckets = new DnsQueryIdRange[BUCKETS]; + + DnsQueryIdSpace() { + assert idBuckets.length == MathUtil.findNextPositivePowerOfTwo(idBuckets.length); + // We start with 1 bucket. + idBuckets[0] = newBucket(0); + } + + private static DnsQueryIdRange newBucket(int idBucketsIdx) { + return new DnsQueryIdRange(BUCKET_SIZE, idBucketsIdx * BUCKET_SIZE); + } + + /** + * Returns the next ID to use for a query or {@code -1} if there is none left to use. + * + * @return next id to use. + */ + int nextId() { + int freeIdx = -1; + for (int bucketIdx = 0; bucketIdx < idBuckets.length; bucketIdx++) { + DnsQueryIdRange bucket = idBuckets[bucketIdx]; + if (bucket != null) { + int id = bucket.nextId(); + if (id != -1) { + return id; + } + } else if (freeIdx == -1 || + // Let's make it somehow random which free slot is used. + ThreadLocalRandom.current().nextBoolean()) { + // We have a slot that we can use to create a new bucket if we need to. + freeIdx = bucketIdx; + } + } + if (freeIdx == -1) { + // No ids left and no slot left to create a new bucket. + return -1; + } + + // We still have some slots free to store a new bucket. Let's do this now and use it to generate the next id. + DnsQueryIdRange bucket = newBucket(freeIdx); + idBuckets[freeIdx] = bucket; + int id = bucket.nextId(); + assert id >= 0; + return id; + } + + /** + * Push back the id, so it can be used again for the next query. + * + * @param id the id. + */ + void pushId(int id) { + int bucketIdx = id / BUCKET_SIZE; + if (bucketIdx >= idBuckets.length) { + throw new IllegalArgumentException("id too large: " + id); + } + DnsQueryIdRange bucket = idBuckets[bucketIdx]; + assert bucket != null; + bucket.pushId(id); + + if (bucket.usableIds() == bucket.maxUsableIds()) { + // All ids are usable in this bucket. Let's check if there are other buckets left that have still + // some space left and if so drop this bucket. + for (int idx = 0; idx < idBuckets.length; idx++) { + if (idx != bucketIdx) { + DnsQueryIdRange otherBucket = idBuckets[idx]; + if (otherBucket != null && otherBucket.usableIds() > BUCKET_DROP_THRESHOLD) { + // Drop bucket on the floor to reduce memory usage, there is another bucket left we can + // use that still has enough ids to use. + idBuckets[bucketIdx] = null; + return; + } + } + } + } + } + + /** + * Return how much more usable ids are left. + * + * @return the number of ids that are left for usage. + */ + int usableIds() { + int usableIds = 0; + for (DnsQueryIdRange bucket: idBuckets) { + // If there is nothing stored in the index yet we can assume the whole bucket is usable + usableIds += bucket == null ? BUCKET_SIZE : bucket.usableIds(); + } + return usableIds; + } + + /** + * Return the maximum number of ids that are supported. + * + * @return the maximum number of ids. + */ + int maxUsableIds() { + return BUCKET_SIZE * idBuckets.length; + } + + /** + * Provides a query if from a range of possible ids. + */ + private static final class DnsQueryIdRange { + + // Holds all possible ids which are stored as unsigned shorts + private final short[] ids; + private final int startId; + private int count; + + DnsQueryIdRange(int bucketSize, int startId) { + this.ids = new short[bucketSize]; + this.startId = startId; + for (int v = startId; v < bucketSize + startId; v++) { + pushId(v); + } + } + + /** + * Returns the next ID to use for a query or {@code -1} if there is none left to use. + * + * @return next id to use. + */ + int nextId() { + assert count >= 0; + if (count == 0) { + return -1; + } + short id = ids[count - 1]; + count--; + + return id & 0xFFFF; + } + + /** + * Push back the id, so it can be used again for the next query. + * + * @param id the id. + */ + void pushId(int id) { + if (count == ids.length) { + throw new IllegalStateException("overflow"); + } + assert id <= startId + ids.length && id >= startId; + // pick a slot for our index, and whatever was in that slot before will get moved to the tail. + Random random = PlatformDependent.threadLocalRandom(); + int insertionPosition = random.nextInt(count + 1); + ids[count] = ids[insertionPosition]; + ids[insertionPosition] = (short) id; + count++; + } + + /** + * Return how much more usable ids are left. + * + * @return the number of ids that are left for usage. + */ + int usableIds() { + return count; + } + + /** + * Return the maximum number of ids that are supported. + * + * @return the maximum number of ids. + */ + int maxUsableIds() { + return ids.length; + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java new file mode 100644 index 0000000..e1318ef --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserver.java @@ -0,0 +1,99 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.channel.ChannelFuture; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponseCode; + +import java.net.InetSocketAddress; +import java.util.List; + +/** + * This interface provides visibility into individual DNS queries. The lifecycle of an objects is as follows: + *

    + *
  1. Object creation
  2. + *
  3. {@link #queryCancelled(int)}
  4. + *
+ * OR + *
    + *
  1. Object creation
  2. + *
  3. {@link #queryWritten(InetSocketAddress, ChannelFuture)}
  4. + *
  5. {@link #queryRedirected(List)} or {@link #queryCNAMEd(DnsQuestion)} or + * {@link #queryNoAnswer(DnsResponseCode)} or {@link #queryCancelled(int)} or + * {@link #queryFailed(Throwable)} or {@link #querySucceed()}
  6. + *
+ *

+ * This interface can be used to track metrics for individual DNS servers. Methods which may lead to another DNS query + * return an object of type {@link DnsQueryLifecycleObserver}. Implementations may use this to build a query tree to + * understand the "sub queries" generated by a single query. + */ +public interface DnsQueryLifecycleObserver { + /** + * The query has been written. + * @param dnsServerAddress The DNS server address which the query was sent to. + * @param future The future which represents the status of the write operation for the DNS query. + */ + void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future); + + /** + * The query may have been written but it was cancelled at some point. + * @param queriesRemaining The number of queries remaining. + */ + void queryCancelled(int queriesRemaining); + + /** + * The query has been redirected to another list of DNS servers. + * @param nameServers The name servers the query has been redirected to. + * @return An observer for the new query which we may issue. + */ + DnsQueryLifecycleObserver queryRedirected(List nameServers); + + /** + * The query returned a CNAME which we may attempt to follow with a new query. + *

+ * Note that multiple queries may be encountering a CNAME. For example a if both {@link DnsRecordType#AAAA} and + * {@link DnsRecordType#A} are supported we may query for both. + * @param cnameQuestion the question we would use if we issue a new query. + * @return An observer for the new query which we may issue. + */ + DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion); + + /** + * The response to the query didn't provide the expected response code, but it didn't return + * {@link DnsResponseCode#NXDOMAIN} so we may try to query again. + * @param code the unexpected response code. + * @return An observer for the new query which we may issue. + */ + DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code); + + /** + * The following criteria are possible: + *

    + *
  • IO Error
  • + *
  • Server responded with an invalid DNS response
  • + *
  • Server responded with a valid DNS response, but it didn't progress the resolution
  • + *
+ * @param cause The cause which for the failure. + */ + void queryFailed(Throwable cause); + + /** + * The query received the expected results. + */ + void querySucceed(); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java new file mode 100644 index 0000000..e3d2776 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsQueryLifecycleObserverFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsQuestion; + +/** + * Used to generate new instances of {@link DnsQueryLifecycleObserver}. + */ +public interface DnsQueryLifecycleObserverFactory { + /** + * Create a new instance of a {@link DnsQueryLifecycleObserver}. This will be called at the start of a new query. + * @param question The question being asked. + * @return a new instance of a {@link DnsQueryLifecycleObserver}. + */ + DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java new file mode 100644 index 0000000..8d676a6 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsRecordResolveContext.java @@ -0,0 +1,95 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.UnknownHostException; +import java.util.List; + +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Promise; + +final class DnsRecordResolveContext extends DnsResolveContext { + + DnsRecordResolveContext(DnsNameResolver parent, Channel channel, Promise originalPromise, DnsQuestion question, + DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs, int allowedQueries) { + this(parent, channel, originalPromise, question.name(), question.dnsClass(), + new DnsRecordType[] { question.type() }, + additionals, nameServerAddrs, allowedQueries); + } + + private DnsRecordResolveContext(DnsNameResolver parent, Channel channel, Promise originalPromise, + String hostname, int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs, + int allowedQueries) { + super(parent, channel, originalPromise, hostname, dnsClass, expectedTypes, + additionals, nameServerAddrs, allowedQueries); + } + + @Override + DnsResolveContext newResolverContext(DnsNameResolver parent, Channel channel, Promise originalPromise, + String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs, + int allowedQueries) { + return new DnsRecordResolveContext(parent, channel, originalPromise, hostname, dnsClass, + expectedTypes, additionals, nameServerAddrs, allowedQueries); + } + + @Override + DnsRecord convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop) { + return ReferenceCountUtil.retain(record); + } + + @Override + List filterResults(List unfiltered) { + return unfiltered; + } + + @Override + boolean isCompleteEarly(DnsRecord resolved) { + return false; + } + + @Override + boolean isDuplicateAllowed() { + return true; + } + + @Override + void cache(String hostname, DnsRecord[] additionals, DnsRecord result, DnsRecord convertedResult) { + // Do not cache. + // XXX: When we implement cache, we would need to retain the reference count of the result record. + } + + @Override + void cache(String hostname, DnsRecord[] additionals, UnknownHostException cause) { + // Do not cache. + // XXX: When we implement cache, we would need to retain the reference count of the result record. + } + + @Override + DnsCnameCache cnameCache() { + // We don't use a cache here at all as we also don't cache if we end up using the DnsRecordResolverContext. + return NoopDnsCnameCache.INSTANCE; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java new file mode 100644 index 0000000..3361440 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsResolveContext.java @@ -0,0 +1,1470 @@ +/* + * Copyright 2014 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: + * + * https://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.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.channel.EventLoop; +import io.netty.handler.codec.CorruptedFrameException; +import io.netty.handler.codec.dns.DefaultDnsQuestion; +import io.netty.handler.codec.dns.DefaultDnsRecordDecoder; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRawRecord; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsResponseCode; +import io.netty.handler.codec.dns.DnsSection; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.ObjectUtil; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.StringUtil; +import io.netty.util.internal.SuppressJava6Requirement; +import io.netty.util.internal.ThrowableUtil; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.AbstractList; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Set; + +import static io.netty.handler.codec.dns.DnsResponseCode.NXDOMAIN; +import static io.netty.handler.codec.dns.DnsResponseCode.SERVFAIL; +import static io.netty.resolver.dns.DnsAddressDecoder.decodeAddress; +import static java.lang.Math.min; + +abstract class DnsResolveContext { + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsResolveContext.class); + + private static final RuntimeException NXDOMAIN_QUERY_FAILED_EXCEPTION = + DnsResolveContextException.newStatic("No answer found and NXDOMAIN response code returned", + DnsResolveContext.class, "onResponse(..)"); + private static final RuntimeException CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION = + DnsResolveContextException.newStatic("No matching CNAME record found", + DnsResolveContext.class, "onResponseCNAME(..)"); + private static final RuntimeException NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION = + DnsResolveContextException.newStatic("No matching record type found", + DnsResolveContext.class, "onResponseAorAAAA(..)"); + private static final RuntimeException UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION = + DnsResolveContextException.newStatic("Response type was unrecognized", + DnsResolveContext.class, "onResponse(..)"); + private static final RuntimeException NAME_SERVERS_EXHAUSTED_EXCEPTION = + DnsResolveContextException.newStatic("No name servers returned an answer", + DnsResolveContext.class, "tryToFinishResolve(..)"); + private static final RuntimeException SERVFAIL_QUERY_FAILED_EXCEPTION = + DnsErrorCauseException.newStatic("Query failed with SERVFAIL", SERVFAIL, + DnsResolveContext.class, "onResponse(..)"); + private static final RuntimeException NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION = + DnsErrorCauseException.newStatic("Query failed with NXDOMAIN", NXDOMAIN, + DnsResolveContext.class, "onResponse(..)"); + + final DnsNameResolver parent; + private final Channel channel; + private final Promise originalPromise; + private final DnsServerAddressStream nameServerAddrs; + private final String hostname; + private final int dnsClass; + private final DnsRecordType[] expectedTypes; + final DnsRecord[] additionals; + + private final Set>> queriesInProgress = + Collections.newSetFromMap( + new IdentityHashMap>, Boolean>()); + + private List finalResult; + private int allowedQueries; + private boolean triedCNAME; + private boolean completeEarly; + + DnsResolveContext(DnsNameResolver parent, Channel channel, Promise originalPromise, + String hostname, int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, DnsServerAddressStream nameServerAddrs, int allowedQueries) { + assert expectedTypes.length > 0; + + this.parent = parent; + this.channel = channel; + this.originalPromise = originalPromise; + this.hostname = hostname; + this.dnsClass = dnsClass; + this.expectedTypes = expectedTypes; + this.additionals = additionals; + + this.nameServerAddrs = ObjectUtil.checkNotNull(nameServerAddrs, "nameServerAddrs"); + this.allowedQueries = allowedQueries; + } + + static final class DnsResolveContextException extends RuntimeException { + + private static final long serialVersionUID = 1209303419266433003L; + + private DnsResolveContextException(String message) { + super(message); + } + + @SuppressJava6Requirement(reason = "uses Java 7+ Exception.(String, Throwable, boolean, boolean)" + + " but is guarded by version checks") + private DnsResolveContextException(String message, boolean shared) { + super(message, null, false, true); + assert shared; + } + + // Override fillInStackTrace() so we not populate the backtrace via a native call and so leak the + // Classloader. + @Override + public Throwable fillInStackTrace() { + return this; + } + + static DnsResolveContextException newStatic(String message, Class clazz, String method) { + final DnsResolveContextException exception; + if (PlatformDependent.javaVersion() >= 7) { + exception = new DnsResolveContextException(message, true); + } else { + exception = new DnsResolveContextException(message); + } + return ThrowableUtil.unknownStackTrace(exception, clazz, method); + } + } + + /** + * The {@link Channel} used. + */ + Channel channel() { + return channel; + } + + /** + * The {@link DnsCache} to use while resolving. + */ + DnsCache resolveCache() { + return parent.resolveCache(); + } + + /** + * The {@link DnsCnameCache} that is used for resolving. + */ + DnsCnameCache cnameCache() { + return parent.cnameCache(); + } + + /** + * The {@link AuthoritativeDnsServerCache} to use while resolving. + */ + AuthoritativeDnsServerCache authoritativeDnsServerCache() { + return parent.authoritativeDnsServerCache(); + } + + /** + * Creates a new context with the given parameters. + */ + abstract DnsResolveContext newResolverContext(DnsNameResolver parent, Channel channel, + Promise originalPromise, + String hostname, + int dnsClass, DnsRecordType[] expectedTypes, + DnsRecord[] additionals, + DnsServerAddressStream nameServerAddrs, int allowedQueries); + + /** + * Converts the given {@link DnsRecord} into {@code T}. + */ + abstract T convertRecord(DnsRecord record, String hostname, DnsRecord[] additionals, EventLoop eventLoop); + + /** + * Returns a filtered list of results which should be the final result of DNS resolution. This must take into + * account JDK semantics such as {@link NetUtil#isIpV6AddressesPreferred()}. + */ + abstract List filterResults(List unfiltered); + + abstract boolean isCompleteEarly(T resolved); + + /** + * Returns {@code true} if we should allow duplicates in the result or {@code false} if no duplicates should + * be included. + */ + abstract boolean isDuplicateAllowed(); + + /** + * Caches a successful resolution. + */ + abstract void cache(String hostname, DnsRecord[] additionals, + DnsRecord result, T convertedResult); + + /** + * Caches a failed resolution. + */ + abstract void cache(String hostname, DnsRecord[] additionals, + UnknownHostException cause); + + void resolve(final Promise> promise) { + final String[] searchDomains = parent.searchDomains(); + if (searchDomains.length == 0 || parent.ndots() == 0 || StringUtil.endsWith(hostname, '.')) { + internalResolve(hostname, promise); + } else { + final boolean startWithoutSearchDomain = hasNDots(); + final String initialHostname = startWithoutSearchDomain ? hostname : hostname + '.' + searchDomains[0]; + final int initialSearchDomainIdx = startWithoutSearchDomain ? 0 : 1; + + final Promise> searchDomainPromise = parent.executor().newPromise(); + searchDomainPromise.addListener(new FutureListener>() { + private int searchDomainIdx = initialSearchDomainIdx; + @Override + public void operationComplete(Future> future) { + Throwable cause = future.cause(); + if (cause == null) { + final List result = future.getNow(); + if (!promise.trySuccess(result)) { + for (T item : result) { + ReferenceCountUtil.safeRelease(item); + } + } + } else { + if (DnsNameResolver.isTransportOrTimeoutError(cause)) { + promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname, expectedTypes, + searchDomains)); + } else if (searchDomainIdx < searchDomains.length) { + Promise> newPromise = parent.executor().newPromise(); + newPromise.addListener(this); + doSearchDomainQuery(hostname + '.' + searchDomains[searchDomainIdx++], newPromise); + } else if (!startWithoutSearchDomain) { + internalResolve(hostname, promise); + } else { + promise.tryFailure(new SearchDomainUnknownHostException(cause, hostname, expectedTypes, + searchDomains)); + } + } + } + }); + doSearchDomainQuery(initialHostname, searchDomainPromise); + } + } + + private boolean hasNDots() { + for (int idx = hostname.length() - 1, dots = 0; idx >= 0; idx--) { + if (hostname.charAt(idx) == '.' && ++dots >= parent.ndots()) { + return true; + } + } + return false; + } + + private static final class SearchDomainUnknownHostException extends UnknownHostException { + private static final long serialVersionUID = -8573510133644997085L; + + SearchDomainUnknownHostException(Throwable cause, String originalHostname, + DnsRecordType[] queryTypes, String[] searchDomains) { + super("Failed to resolve '" + originalHostname + "' " + Arrays.toString(queryTypes) + + " and search domain query for configured domains failed as well: " + + Arrays.toString(searchDomains)); + setStackTrace(cause.getStackTrace()); + // Preserve the cause + initCause(cause.getCause()); + } + + // Suppress a warning since this method doesn't need synchronization + @Override + public Throwable fillInStackTrace() { + return this; + } + } + + void doSearchDomainQuery(String hostname, Promise> nextPromise) { + DnsResolveContext nextContext = newResolverContext(parent, channel, originalPromise, hostname, dnsClass, + expectedTypes, additionals, nameServerAddrs, + parent.maxQueriesPerResolve()); + nextContext.internalResolve(hostname, nextPromise); + } + + private static String hostnameWithDot(String name) { + if (StringUtil.endsWith(name, '.')) { + return name; + } + return name + '.'; + } + + // Resolve the final name from the CNAME cache until there is nothing to follow anymore. This also + // guards against loops in the cache but early return once a loop is detected. + // + // Visible for testing only + static String cnameResolveFromCache(DnsCnameCache cnameCache, String name) throws UnknownHostException { + String first = cnameCache.get(hostnameWithDot(name)); + if (first == null) { + // Nothing in the cache at all + return name; + } + + String second = cnameCache.get(hostnameWithDot(first)); + if (second == null) { + // Nothing else to follow, return first match. + return first; + } + + checkCnameLoop(name, first, second); + return cnameResolveFromCacheLoop(cnameCache, name, first, second); + } + + private static String cnameResolveFromCacheLoop( + DnsCnameCache cnameCache, String hostname, String first, String mapping) throws UnknownHostException { + // Detect loops by advance only every other iteration. + // See https://en.wikipedia.org/wiki/Cycle_detection#Floyd's_Tortoise_and_Hare + boolean advance = false; + + String name = mapping; + // Resolve from cnameCache() until there is no more cname entry cached. + while ((mapping = cnameCache.get(hostnameWithDot(name))) != null) { + checkCnameLoop(hostname, first, mapping); + name = mapping; + if (advance) { + first = cnameCache.get(first); + } + advance = !advance; + } + return name; + } + + private static void checkCnameLoop(String hostname, String first, String second) throws UnknownHostException { + if (first.equals(second)) { + // Follow CNAME from cache would loop. Lets throw and so fail the resolution. + throw new UnknownHostException("CNAME loop detected for '" + hostname + '\''); + } + } + private void internalResolve(String name, Promise> promise) { + try { + // Resolve from cnameCache() until there is no more cname entry cached. + name = cnameResolveFromCache(cnameCache(), name); + } catch (Throwable cause) { + promise.tryFailure(cause); + return; + } + + try { + DnsServerAddressStream nameServerAddressStream = getNameServers(name); + + final int end = expectedTypes.length - 1; + for (int i = 0; i < end; ++i) { + if (!query(name, expectedTypes[i], nameServerAddressStream.duplicate(), false, promise)) { + return; + } + } + query(name, expectedTypes[end], nameServerAddressStream, false, promise); + } finally { + // Now flush everything we submitted before. + parent.flushQueries(); + } + } + + /** + * Returns the {@link DnsServerAddressStream} that was cached for the given hostname or {@code null} if non + * could be found. + */ + private DnsServerAddressStream getNameServersFromCache(String hostname) { + int len = hostname.length(); + + if (len == 0) { + // We never cache for root servers. + return null; + } + + // We always store in the cache with a trailing '.'. + if (hostname.charAt(len - 1) != '.') { + hostname += "."; + } + + int idx = hostname.indexOf('.'); + if (idx == hostname.length() - 1) { + // We are not interested in handling '.' as we should never serve the root servers from cache. + return null; + } + + // We start from the closed match and then move down. + for (;;) { + // Skip '.' as well. + hostname = hostname.substring(idx + 1); + + int idx2 = hostname.indexOf('.'); + if (idx2 <= 0 || idx2 == hostname.length() - 1) { + // We are not interested in handling '.TLD.' as we should never serve the root servers from cache. + return null; + } + idx = idx2; + + DnsServerAddressStream entries = authoritativeDnsServerCache().get(hostname); + if (entries != null) { + // The returned List may contain unresolved InetSocketAddress instances that will be + // resolved on the fly in query(....). + return entries; + } + } + } + + private void query(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + final boolean flush, + final Promise> promise, + final Throwable cause) { + if (completeEarly || nameServerAddrStreamIndex >= nameServerAddrStream.size() || + allowedQueries == 0 || originalPromise.isCancelled() || promise.isCancelled()) { + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, queryLifecycleObserver, + promise, cause); + return; + } + + --allowedQueries; + + final InetSocketAddress nameServerAddr = nameServerAddrStream.next(); + if (nameServerAddr.isUnresolved()) { + queryUnresolvedNameServer(nameServerAddr, nameServerAddrStream, nameServerAddrStreamIndex, question, + queryLifecycleObserver, promise, cause); + return; + } + final Promise> queryPromise = + channel.eventLoop().newPromise(); + + final long queryStartTimeNanos; + final boolean isFeedbackAddressStream; + if (nameServerAddrStream instanceof DnsServerResponseFeedbackAddressStream) { + queryStartTimeNanos = System.nanoTime(); + isFeedbackAddressStream = true; + } else { + queryStartTimeNanos = -1; + isFeedbackAddressStream = false; + } + + final Future> f = + parent.query0(nameServerAddr, question, queryLifecycleObserver, additionals, flush, queryPromise); + + queriesInProgress.add(f); + + f.addListener(new FutureListener>() { + @Override + public void operationComplete(Future> future) { + queriesInProgress.remove(future); + + if (promise.isDone() || future.isCancelled()) { + queryLifecycleObserver.queryCancelled(allowedQueries); + + // Check if we need to release the envelope itself. If the query was cancelled the getNow() will + // return null as well as the Future will be failed with a CancellationException. + AddressedEnvelope result = future.getNow(); + if (result != null) { + result.release(); + } + return; + } + + final Throwable queryCause = future.cause(); + try { + if (queryCause == null) { + if (isFeedbackAddressStream) { + final DnsServerResponseFeedbackAddressStream feedbackNameServerAddrStream = + (DnsServerResponseFeedbackAddressStream) nameServerAddrStream; + feedbackNameServerAddrStream.feedbackSuccess(nameServerAddr, + System.nanoTime() - queryStartTimeNanos); + } + onResponse(nameServerAddrStream, nameServerAddrStreamIndex, question, future.getNow(), + queryLifecycleObserver, promise); + } else { + // Server did not respond or I/O error occurred; try again. + if (isFeedbackAddressStream) { + final DnsServerResponseFeedbackAddressStream feedbackNameServerAddrStream = + (DnsServerResponseFeedbackAddressStream) nameServerAddrStream; + feedbackNameServerAddrStream.feedbackFailure(nameServerAddr, queryCause, + System.nanoTime() - queryStartTimeNanos); + } + queryLifecycleObserver.queryFailed(queryCause); + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, + newDnsQueryLifecycleObserver(question), true, promise, queryCause); + } + } finally { + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, + // queryLifecycleObserver has already been terminated at this point so we must + // not allow it to be terminated again by tryToFinishResolve. + NoopDnsQueryLifecycleObserver.INSTANCE, + promise, queryCause); + } + } + }); + } + + private void queryUnresolvedNameServer(final InetSocketAddress nameServerAddr, + final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + final Promise> promise, + final Throwable cause) { + final String nameServerName = PlatformDependent.javaVersion() >= 7 ? + nameServerAddr.getHostString() : nameServerAddr.getHostName(); + assert nameServerName != null; + + // Placeholder so we will not try to finish the original query yet. + final Future> resolveFuture = parent.executor() + .newSucceededFuture(null); + queriesInProgress.add(resolveFuture); + + Promise> resolverPromise = parent.executor().newPromise(); + resolverPromise.addListener(new FutureListener>() { + @Override + public void operationComplete(final Future> future) { + // Remove placeholder. + queriesInProgress.remove(resolveFuture); + + if (future.isSuccess()) { + List resolvedAddresses = future.getNow(); + DnsServerAddressStream addressStream = new CombinedDnsServerAddressStream( + nameServerAddr, resolvedAddresses, nameServerAddrStream); + query(addressStream, nameServerAddrStreamIndex, question, + queryLifecycleObserver, true, promise, cause); + } else { + // Ignore the server and try the next one... + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, + question, queryLifecycleObserver, true, promise, cause); + } + } + }); + DnsCache resolveCache = resolveCache(); + if (!DnsNameResolver.doResolveAllCached(nameServerName, additionals, resolverPromise, resolveCache, + parent.resolvedInternetProtocolFamiliesUnsafe())) { + + new DnsAddressResolveContext(parent, channel, originalPromise, nameServerName, additionals, + parent.newNameServerAddressStream(nameServerName), + // Resolving the unresolved nameserver must be limited by allowedQueries + // so we eventually fail + allowedQueries, + resolveCache, + redirectAuthoritativeDnsServerCache(authoritativeDnsServerCache()), false) + .resolve(resolverPromise); + } + } + + private static AuthoritativeDnsServerCache redirectAuthoritativeDnsServerCache( + AuthoritativeDnsServerCache authoritativeDnsServerCache) { + // Don't wrap again to prevent the possibility of an StackOverflowError when wrapping another + // RedirectAuthoritativeDnsServerCache. + if (authoritativeDnsServerCache instanceof RedirectAuthoritativeDnsServerCache) { + return authoritativeDnsServerCache; + } + return new RedirectAuthoritativeDnsServerCache(authoritativeDnsServerCache); + } + + private static final class RedirectAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { + private final AuthoritativeDnsServerCache wrapped; + + RedirectAuthoritativeDnsServerCache(AuthoritativeDnsServerCache authoritativeDnsServerCache) { + this.wrapped = authoritativeDnsServerCache; + } + + @Override + public DnsServerAddressStream get(String hostname) { + // To not risk falling into any loop, we will not use the cache while following redirects but only + // on the initial query. + return null; + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + wrapped.cache(hostname, address, originalTtl, loop); + } + + @Override + public void clear() { + wrapped.clear(); + } + + @Override + public boolean clear(String hostname) { + return wrapped.clear(hostname); + } + } + + private void onResponse(final DnsServerAddressStream nameServerAddrStream, final int nameServerAddrStreamIndex, + final DnsQuestion question, AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise> promise) { + try { + final DnsResponse res = envelope.content(); + final DnsResponseCode code = res.code(); + if (code == DnsResponseCode.NOERROR) { + if (handleRedirect(question, envelope, queryLifecycleObserver, promise)) { + // Was a redirect so return here as everything else is handled in handleRedirect(...) + return; + } + final DnsRecordType type = question.type(); + + if (type == DnsRecordType.CNAME) { + onResponseCNAME(question, buildAliasMap(envelope.content(), cnameCache(), parent.executor()), + queryLifecycleObserver, promise); + return; + } + + for (DnsRecordType expectedType : expectedTypes) { + if (type == expectedType) { + onExpectedResponse(question, envelope, queryLifecycleObserver, promise); + return; + } + } + + queryLifecycleObserver.queryFailed(UNRECOGNIZED_TYPE_QUERY_FAILED_EXCEPTION); + return; + } + + // Retry with the next server if the server did not tell us that the domain does not exist. + if (code != DnsResponseCode.NXDOMAIN) { + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, + queryLifecycleObserver.queryNoAnswer(code), true, promise, cause(code)); + } else { + queryLifecycleObserver.queryFailed(NXDOMAIN_QUERY_FAILED_EXCEPTION); + + // Try with the next server if is not authoritative for the domain. + // + // From https://tools.ietf.org/html/rfc1035 : + // + // RCODE Response code - this 4 bit field is set as part of + // responses. The values have the following + // interpretation: + // + // .... + // .... + // + // 3 Name Error - Meaningful only for + // responses from an authoritative name + // server, this code signifies that the + // domain name referenced in the query does + // not exist. + // .... + // .... + if (!res.isAuthoritativeAnswer()) { + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, + newDnsQueryLifecycleObserver(question), true, promise, null); + } else { + // Failed with NX cause - distinction between an authoritative NXDOMAIN vs a timeout + tryToFinishResolve(nameServerAddrStream, nameServerAddrStreamIndex, question, + queryLifecycleObserver, promise, NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION); + } + } + } finally { + ReferenceCountUtil.safeRelease(envelope); + } + } + + /** + * Handles a redirect answer if needed and returns {@code true} if a redirect query has been made. + */ + private boolean handleRedirect( + DnsQuestion question, AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, Promise> promise) { + final DnsResponse res = envelope.content(); + + // Check if we have answers, if not this may be an non authority NS and so redirects must be handled. + if (res.count(DnsSection.ANSWER) == 0) { + AuthoritativeNameServerList serverNames = extractAuthoritativeNameServers(question.name(), res); + if (serverNames != null) { + int additionalCount = res.count(DnsSection.ADDITIONAL); + + AuthoritativeDnsServerCache authoritativeDnsServerCache = authoritativeDnsServerCache(); + for (int i = 0; i < additionalCount; i++) { + final DnsRecord r = res.recordAt(DnsSection.ADDITIONAL, i); + + if (r.type() == DnsRecordType.A && !parent.supportsARecords() || + r.type() == DnsRecordType.AAAA && !parent.supportsAAAARecords()) { + continue; + } + + // We may have multiple ADDITIONAL entries for the same nameserver name. For example one AAAA and + // one A record. + serverNames.handleWithAdditional(parent, r, authoritativeDnsServerCache); + } + + // Process all unresolved nameservers as well. + serverNames.handleWithoutAdditionals(parent, resolveCache(), authoritativeDnsServerCache); + + List addresses = serverNames.addressList(); + + // Give the user the chance to sort or filter the used servers for the query. + DnsServerAddressStream serverStream = parent.newRedirectDnsServerStream( + question.name(), addresses); + + if (serverStream != null) { + query(serverStream, 0, question, + queryLifecycleObserver.queryRedirected(new DnsAddressStreamList(serverStream)), + true, promise, null); + return true; + } + } + } + return false; + } + + private static Throwable cause(final DnsResponseCode code) { + assert code != null; + if (SERVFAIL.intValue() == code.intValue()) { + return SERVFAIL_QUERY_FAILED_EXCEPTION; + } else if (NXDOMAIN.intValue() == code.intValue()) { + return NXDOMAIN_CAUSE_QUERY_FAILED_EXCEPTION; + } + + return null; + } + + private static final class DnsAddressStreamList extends AbstractList { + + private final DnsServerAddressStream duplicate; + private List addresses; + + DnsAddressStreamList(DnsServerAddressStream stream) { + duplicate = stream.duplicate(); + } + + @Override + public InetSocketAddress get(int index) { + if (addresses == null) { + DnsServerAddressStream stream = duplicate.duplicate(); + addresses = new ArrayList(size()); + for (int i = 0; i < stream.size(); i++) { + addresses.add(stream.next()); + } + } + return addresses.get(index); + } + + @Override + public int size() { + return duplicate.size(); + } + + @Override + public Iterator iterator() { + return new Iterator() { + private final DnsServerAddressStream stream = duplicate.duplicate(); + private int i; + + @Override + public boolean hasNext() { + return i < stream.size(); + } + + @Override + public InetSocketAddress next() { + if (!hasNext()) { + throw new NoSuchElementException(); + } + i++; + return stream.next(); + } + + @Override + public void remove() { + throw new UnsupportedOperationException(); + } + }; + } + } + + /** + * Returns the {@code {@link AuthoritativeNameServerList} which were included in {@link DnsSection#AUTHORITY} + * or {@code null} if non are found. + */ + private static AuthoritativeNameServerList extractAuthoritativeNameServers(String questionName, DnsResponse res) { + int authorityCount = res.count(DnsSection.AUTHORITY); + if (authorityCount == 0) { + return null; + } + + AuthoritativeNameServerList serverNames = new AuthoritativeNameServerList(questionName); + for (int i = 0; i < authorityCount; i++) { + serverNames.add(res.recordAt(DnsSection.AUTHORITY, i)); + } + return serverNames.isEmpty() ? null : serverNames; + } + + private void onExpectedResponse( + DnsQuestion question, AddressedEnvelope envelope, + final DnsQueryLifecycleObserver queryLifecycleObserver, Promise> promise) { + + // We often get a bunch of CNAMES as well when we asked for A/AAAA. + final DnsResponse response = envelope.content(); + final Map cnames = buildAliasMap(response, cnameCache(), parent.executor()); + final int answerCount = response.count(DnsSection.ANSWER); + + boolean found = false; + boolean completeEarly = this.completeEarly; + boolean cnameNeedsFollow = !cnames.isEmpty(); + for (int i = 0; i < answerCount; i ++) { + final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); + final DnsRecordType type = r.type(); + boolean matches = false; + for (DnsRecordType expectedType : expectedTypes) { + if (type == expectedType) { + matches = true; + break; + } + } + + if (!matches) { + continue; + } + + final String questionName = question.name().toLowerCase(Locale.US); + final String recordName = r.name().toLowerCase(Locale.US); + + // Make sure the record is for the questioned domain. + if (!recordName.equals(questionName)) { + Map cnamesCopy = new HashMap(cnames); + // Even if the record's name is not exactly same, it might be an alias defined in the CNAME records. + String resolved = questionName; + do { + resolved = cnamesCopy.remove(resolved); + if (recordName.equals(resolved)) { + // We followed a CNAME chain that was part of the response without any extra queries. + cnameNeedsFollow = false; + break; + } + } while (resolved != null); + + if (resolved == null) { + assert questionName.isEmpty() || questionName.charAt(questionName.length() - 1) == '.'; + + for (String searchDomain : parent.searchDomains()) { + if (searchDomain.isEmpty()) { + continue; + } + + final String fqdn; + if (searchDomain.charAt(searchDomain.length() - 1) == '.') { + fqdn = questionName + searchDomain; + } else { + fqdn = questionName + searchDomain + '.'; + } + if (recordName.equals(fqdn)) { + resolved = recordName; + break; + } + } + if (resolved == null) { + if (logger.isDebugEnabled()) { + logger.debug("{} Ignoring record {} for [{}: {}] as it contains a different name than " + + "the question name [{}]. Cnames: {}, Search domains: {}", + channel, r.toString(), response.id(), envelope.sender(), questionName, cnames, + parent.searchDomains()); + } + continue; + } + } + } + + final T converted = convertRecord(r, hostname, additionals, parent.executor()); + if (converted == null) { + if (logger.isDebugEnabled()) { + logger.debug("{} Ignoring record {} for [{}: {}] as the converted record is null. " + + "Hostname [{}], Additionals: {}", + channel, r.toString(), response.id(), envelope.sender(), hostname, additionals); + } + continue; + } + + boolean shouldRelease = false; + // Check if we did determine we wanted to complete early before. If this is the case we want to not + // include the result + if (!completeEarly) { + completeEarly = isCompleteEarly(converted); + } + + // Check if the promise was done already, and only if not add things to the finalResult. Otherwise lets + // just release things after we cached it. + if (!promise.isDone()) { + // We want to ensure we do not have duplicates in finalResult as this may be unexpected. + // + // While using a LinkedHashSet or HashSet may sound like the perfect fit for this we will use an + // ArrayList here as duplicates should be found quite unfrequently in the wild and we dont want to pay + // for the extra memory copy and allocations in this cases later on. + if (finalResult == null) { + finalResult = new ArrayList(8); + finalResult.add(converted); + } else if (isDuplicateAllowed() || !finalResult.contains(converted)) { + finalResult.add(converted); + } else { + shouldRelease = true; + } + } else { + shouldRelease = true; + } + + cache(hostname, additionals, r, converted); + found = true; + + if (shouldRelease) { + ReferenceCountUtil.release(converted); + } + // Note that we do not break from the loop here, so we decode/cache all A/AAAA records. + } + + if (found && !cnameNeedsFollow) { + // If we found the correct result we can just stop here without following any extra CNAME records in the + // response. + if (completeEarly) { + this.completeEarly = true; + } + queryLifecycleObserver.querySucceed(); + } else if (cnames.isEmpty()) { + queryLifecycleObserver.queryFailed(NO_MATCHING_RECORD_QUERY_FAILED_EXCEPTION); + } else { + queryLifecycleObserver.querySucceed(); + // We also got a CNAME so we need to ensure we also query it. + onResponseCNAME(question, cnames, newDnsQueryLifecycleObserver(question), promise); + } + } + + private void onResponseCNAME( + DnsQuestion question, Map cnames, + final DnsQueryLifecycleObserver queryLifecycleObserver, + Promise> promise) { + + // Resolve the host name in the question into the real host name. + String resolved = question.name().toLowerCase(Locale.US); + boolean found = false; + while (!cnames.isEmpty()) { // Do not attempt to call Map.remove() when the Map is empty + // because it can be Collections.emptyMap() + // whose remove() throws a UnsupportedOperationException. + final String next = cnames.remove(resolved); + if (next != null) { + found = true; + resolved = next; + } else { + break; + } + } + + if (found) { + followCname(question, resolved, queryLifecycleObserver, promise); + } else { + queryLifecycleObserver.queryFailed(CNAME_NOT_FOUND_QUERY_FAILED_EXCEPTION); + } + } + + private static Map buildAliasMap(DnsResponse response, DnsCnameCache cache, EventLoop loop) { + final int answerCount = response.count(DnsSection.ANSWER); + Map cnames = null; + for (int i = 0; i < answerCount; i ++) { + final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); + final DnsRecordType type = r.type(); + if (type != DnsRecordType.CNAME) { + continue; + } + + if (!(r instanceof DnsRawRecord)) { + continue; + } + + final ByteBuf recordContent = ((ByteBufHolder) r).content(); + final String domainName = decodeDomainName(recordContent); + if (domainName == null) { + continue; + } + + if (cnames == null) { + cnames = new HashMap(min(8, answerCount)); + } + + String name = r.name().toLowerCase(Locale.US); + String mapping = domainName.toLowerCase(Locale.US); + + // Cache the CNAME as well. + String nameWithDot = hostnameWithDot(name); + String mappingWithDot = hostnameWithDot(mapping); + if (!nameWithDot.equalsIgnoreCase(mappingWithDot)) { + cache.cache(nameWithDot, mappingWithDot, r.timeToLive(), loop); + cnames.put(name, mapping); + } + } + + return cnames != null? cnames : Collections.emptyMap(); + } + + private void tryToFinishResolve(final DnsServerAddressStream nameServerAddrStream, + final int nameServerAddrStreamIndex, + final DnsQuestion question, + final DnsQueryLifecycleObserver queryLifecycleObserver, + final Promise> promise, + final Throwable cause) { + + // There are no queries left to try. + if (!completeEarly && !queriesInProgress.isEmpty()) { + queryLifecycleObserver.queryCancelled(allowedQueries); + + // There are still some queries in process, we will try to notify once the next one finishes until + // all are finished. + return; + } + + // There are no queries left to try. + if (finalResult == null) { + if (nameServerAddrStreamIndex < nameServerAddrStream.size()) { + if (queryLifecycleObserver == NoopDnsQueryLifecycleObserver.INSTANCE) { + // If the queryLifecycleObserver has already been terminated we should create a new one for this + // fresh query. + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, + newDnsQueryLifecycleObserver(question), true, promise, cause); + } else { + query(nameServerAddrStream, nameServerAddrStreamIndex + 1, question, queryLifecycleObserver, + true, promise, cause); + } + return; + } + + queryLifecycleObserver.queryFailed(NAME_SERVERS_EXHAUSTED_EXCEPTION); + + // .. and we could not find any expected records. + + // If cause != null we know this was caused by a timeout / cancel / transport exception. In this case we + // won't try to resolve the CNAME as we only should do this if we could not get the expected records + // because they do not exist and the DNS server did probably signal it. + if (cause == null && !triedCNAME && + (question.type() == DnsRecordType.A || question.type() == DnsRecordType.AAAA)) { + // As the last resort, try to query CNAME, just in case the name server has it. + triedCNAME = true; + + query(hostname, DnsRecordType.CNAME, getNameServers(hostname), true, promise); + return; + } + } else { + queryLifecycleObserver.queryCancelled(allowedQueries); + } + + // We have at least one resolved record or tried CNAME as the last resort. + finishResolve(promise, cause); + } + + private void finishResolve(Promise> promise, Throwable cause) { + // If completeEarly was true we still want to continue processing the queries to ensure we still put everything + // in the cache eventually. + if (!completeEarly && !queriesInProgress.isEmpty()) { + // If there are queries in progress, we should cancel it because we already finished the resolution. + for (Iterator>> i = queriesInProgress.iterator(); + i.hasNext();) { + Future> f = i.next(); + i.remove(); + + f.cancel(false); + } + } + + if (finalResult != null) { + if (!promise.isDone()) { + // Found at least one resolved record. + final List result = filterResults(finalResult); + // Lets replace the previous stored result. + finalResult = Collections.emptyList(); + if (!DnsNameResolver.trySuccess(promise, result)) { + for (T item : result) { + ReferenceCountUtil.safeRelease(item); + } + } + } else { + // This should always be the case as we replaced the list once notify the promise with an empty one + // and never add to it again. + assert finalResult.isEmpty(); + } + return; + } + + // No resolved address found. + final int maxAllowedQueries = parent.maxQueriesPerResolve(); + final int tries = maxAllowedQueries - allowedQueries; + final StringBuilder buf = new StringBuilder(64); + + buf.append("Failed to resolve '").append(hostname).append("' ").append(Arrays.toString(expectedTypes)); + if (tries > 1) { + if (tries < maxAllowedQueries) { + buf.append(" after ") + .append(tries) + .append(" queries "); + } else { + buf.append(". Exceeded max queries per resolve ") + .append(maxAllowedQueries) + .append(' '); + } + } + final UnknownHostException unknownHostException = new UnknownHostException(buf.toString()); + if (cause == null) { + // Only cache if the failure was not because of an IO error / timeout that was caused by the query + // itself. + cache(hostname, additionals, unknownHostException); + } else { + unknownHostException.initCause(cause); + } + promise.tryFailure(unknownHostException); + } + + static String decodeDomainName(ByteBuf in) { + in.markReaderIndex(); + try { + return DefaultDnsRecordDecoder.decodeName(in); + } catch (CorruptedFrameException e) { + // In this case we just return null. + return null; + } finally { + in.resetReaderIndex(); + } + } + + private DnsServerAddressStream getNameServers(String name) { + DnsServerAddressStream stream = getNameServersFromCache(name); + if (stream == null) { + // We need to obtain a new stream from the parent DnsNameResolver if the hostname is not the same as + // for the original query (for example we may follow CNAMEs). Otherwise let's just duplicate the + // original nameservers so we correctly update the internal index + if (name.equals(hostname)) { + return nameServerAddrs.duplicate(); + } + return parent.newNameServerAddressStream(name); + } + return stream; + } + + private void followCname(DnsQuestion question, String cname, DnsQueryLifecycleObserver queryLifecycleObserver, + Promise> promise) { + final DnsQuestion cnameQuestion; + final DnsServerAddressStream stream; + try { + cname = cnameResolveFromCache(cnameCache(), cname); + stream = getNameServers(cname); + cnameQuestion = new DefaultDnsQuestion(cname, question.type(), dnsClass); + } catch (Throwable cause) { + queryLifecycleObserver.queryFailed(cause); + PlatformDependent.throwException(cause); + return; + } + query(stream, 0, cnameQuestion, queryLifecycleObserver.queryCNAMEd(cnameQuestion), + true, promise, null); + } + + private boolean query(String hostname, DnsRecordType type, DnsServerAddressStream dnsServerAddressStream, + boolean flush, Promise> promise) { + final DnsQuestion question; + try { + question = new DefaultDnsQuestion(hostname, type, dnsClass); + } catch (Throwable cause) { + // Assume a single failure means that queries will succeed. If the hostname is invalid for one type + // there is no case where it is known to be valid for another type. + promise.tryFailure(new IllegalArgumentException("Unable to create DNS Question for: [" + hostname + ", " + + type + ']', cause)); + return false; + } + query(dnsServerAddressStream, 0, question, newDnsQueryLifecycleObserver(question), flush, promise, null); + return true; + } + + private DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) { + return parent.dnsQueryLifecycleObserverFactory().newDnsQueryLifecycleObserver(question); + } + + private final class CombinedDnsServerAddressStream implements DnsServerAddressStream { + private final InetSocketAddress replaced; + private final DnsServerAddressStream originalStream; + private final List resolvedAddresses; + private Iterator resolved; + + CombinedDnsServerAddressStream(InetSocketAddress replaced, List resolvedAddresses, + DnsServerAddressStream originalStream) { + this.replaced = replaced; + this.resolvedAddresses = resolvedAddresses; + this.originalStream = originalStream; + resolved = resolvedAddresses.iterator(); + } + + @Override + public InetSocketAddress next() { + if (resolved.hasNext()) { + return nextResolved0(); + } + InetSocketAddress address = originalStream.next(); + if (address.equals(replaced)) { + resolved = resolvedAddresses.iterator(); + return nextResolved0(); + } + return address; + } + + private InetSocketAddress nextResolved0() { + return parent.newRedirectServerAddress(resolved.next()); + } + + @Override + public int size() { + return originalStream.size() + resolvedAddresses.size() - 1; + } + + @Override + public DnsServerAddressStream duplicate() { + return new CombinedDnsServerAddressStream(replaced, resolvedAddresses, originalStream.duplicate()); + } + } + + /** + * Holds the closed DNS Servers for a domain. + */ + private static final class AuthoritativeNameServerList { + + private final String questionName; + + // We not expect the linked-list to be very long so a double-linked-list is overkill. + private AuthoritativeNameServer head; + + private int nameServerCount; + + AuthoritativeNameServerList(String questionName) { + this.questionName = questionName.toLowerCase(Locale.US); + } + + void add(DnsRecord r) { + if (r.type() != DnsRecordType.NS || !(r instanceof DnsRawRecord)) { + return; + } + + // Only include servers that serve the correct domain. + if (questionName.length() < r.name().length()) { + return; + } + + String recordName = r.name().toLowerCase(Locale.US); + + int dots = 0; + for (int a = recordName.length() - 1, b = questionName.length() - 1; a >= 0; a--, b--) { + char c = recordName.charAt(a); + if (questionName.charAt(b) != c) { + return; + } + if (c == '.') { + dots++; + } + } + + if (head != null && head.dots > dots) { + // We already have a closer match so ignore this one, no need to parse the domainName etc. + return; + } + + final ByteBuf recordContent = ((ByteBufHolder) r).content(); + final String domainName = decodeDomainName(recordContent); + if (domainName == null) { + // Could not be parsed, ignore. + return; + } + + // We are only interested in preserving the nameservers which are the closest to our qName, so ensure + // we drop servers that have a smaller dots count. + if (head == null || head.dots < dots) { + nameServerCount = 1; + head = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName); + } else if (head.dots == dots) { + AuthoritativeNameServer serverName = head; + while (serverName.next != null) { + serverName = serverName.next; + } + serverName.next = new AuthoritativeNameServer(dots, r.timeToLive(), recordName, domainName); + nameServerCount++; + } + } + + void handleWithAdditional( + DnsNameResolver parent, DnsRecord r, AuthoritativeDnsServerCache authoritativeCache) { + // Just walk the linked-list and mark the entry as handled when matched. + AuthoritativeNameServer serverName = head; + + String nsName = r.name(); + InetAddress resolved = decodeAddress(r, nsName, parent.isDecodeIdn()); + if (resolved == null) { + // Could not parse the address, just ignore. + return; + } + + while (serverName != null) { + if (serverName.nsName.equalsIgnoreCase(nsName)) { + if (serverName.address != null) { + // We received multiple ADDITIONAL records for the same name. + // Search for the last we insert before and then append a new one. + while (serverName.next != null && serverName.next.isCopy) { + serverName = serverName.next; + } + AuthoritativeNameServer server = new AuthoritativeNameServer(serverName); + server.next = serverName.next; + serverName.next = server; + serverName = server; + + nameServerCount++; + } + // We should replace the TTL if needed with the one of the ADDITIONAL record so we use + // the smallest for caching. + serverName.update(parent.newRedirectServerAddress(resolved), r.timeToLive()); + + // Cache the server now. + cache(serverName, authoritativeCache, parent.executor()); + return; + } + serverName = serverName.next; + } + } + + // Now handle all AuthoritativeNameServer for which we had no ADDITIONAL record + void handleWithoutAdditionals( + DnsNameResolver parent, DnsCache cache, AuthoritativeDnsServerCache authoritativeCache) { + AuthoritativeNameServer serverName = head; + + while (serverName != null) { + if (serverName.address == null) { + // These will be resolved on the fly if needed. + cacheUnresolved(serverName, authoritativeCache, parent.executor()); + + // Try to resolve via cache as we had no ADDITIONAL entry for the server. + + List entries = cache.get(serverName.nsName, null); + if (entries != null && !entries.isEmpty()) { + InetAddress address = entries.get(0).address(); + + // If address is null we have a resolution failure cached so just use an unresolved address. + if (address != null) { + serverName.update(parent.newRedirectServerAddress(address)); + + for (int i = 1; i < entries.size(); i++) { + address = entries.get(i).address(); + + assert address != null : + "Cache returned a cached failure, should never return anything else"; + + AuthoritativeNameServer server = new AuthoritativeNameServer(serverName); + server.next = serverName.next; + serverName.next = server; + serverName = server; + serverName.update(parent.newRedirectServerAddress(address)); + + nameServerCount++; + } + } + } + } + serverName = serverName.next; + } + } + + private static void cacheUnresolved( + AuthoritativeNameServer server, AuthoritativeDnsServerCache authoritativeCache, EventLoop loop) { + // We still want to cached the unresolved address + server.address = InetSocketAddress.createUnresolved( + server.nsName, DefaultDnsServerAddressStreamProvider.DNS_PORT); + + // Cache the server now. + cache(server, authoritativeCache, loop); + } + + private static void cache(AuthoritativeNameServer server, AuthoritativeDnsServerCache cache, EventLoop loop) { + // Cache NS record if not for a root server as we should never cache for root servers. + if (!server.isRootServer()) { + cache.cache(server.domainName, server.address, server.ttl, loop); + } + } + + /** + * Returns {@code true} if empty, {@code false} otherwise. + */ + boolean isEmpty() { + return nameServerCount == 0; + } + + /** + * Creates a new {@link List} which holds the {@link InetSocketAddress}es. + */ + List addressList() { + List addressList = new ArrayList(nameServerCount); + + AuthoritativeNameServer server = head; + while (server != null) { + if (server.address != null) { + addressList.add(server.address); + } + server = server.next; + } + return addressList; + } + } + + private static final class AuthoritativeNameServer { + private final int dots; + private final String domainName; + final boolean isCopy; + final String nsName; + + private long ttl; + private InetSocketAddress address; + + AuthoritativeNameServer next; + + AuthoritativeNameServer(int dots, long ttl, String domainName, String nsName) { + this.dots = dots; + this.ttl = ttl; + this.nsName = nsName; + this.domainName = domainName; + isCopy = false; + } + + AuthoritativeNameServer(AuthoritativeNameServer server) { + dots = server.dots; + ttl = server.ttl; + nsName = server.nsName; + domainName = server.domainName; + isCopy = true; + } + + /** + * Returns {@code true} if its a root server. + */ + boolean isRootServer() { + return dots == 1; + } + + /** + * Update the server with the given address and TTL if needed. + */ + void update(InetSocketAddress address, long ttl) { + assert this.address == null || this.address.isUnresolved(); + this.address = address; + this.ttl = min(this.ttl, ttl); + } + + void update(InetSocketAddress address) { + update(address, Long.MAX_VALUE); + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java new file mode 100644 index 0000000..e2ccc21 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStream.java @@ -0,0 +1,44 @@ +/* + * Copyright 2015 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; + +/** + * An infinite stream of DNS server addresses. + */ +public interface DnsServerAddressStream { + /** + * Retrieves the next DNS server address from the stream. + */ + InetSocketAddress next(); + + /** + * Get the number of times {@link #next()} will return a distinct element before repeating or terminating. + * @return the number of times {@link #next()} will return a distinct element before repeating or terminating. + */ + int size(); + + /** + * Duplicate this object. The result of this should be able to be independently iterated over via {@link #next()}. + *

+ * Note that clone() isn't used because it may make sense for some implementations to have the following + * relationship {@code x.duplicate() == x}. + * @return A duplicate of this object. + */ + DnsServerAddressStream duplicate(); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java new file mode 100644 index 0000000..27fee4e --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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; + +/** + * Provides an opportunity to override which {@link DnsServerAddressStream} is used to resolve a specific hostname. + *

+ * For example this can be used to represent /etc/resolv.conf and + * + * /etc/resolver. + */ +public interface DnsServerAddressStreamProvider { + /** + * Ask this provider for the name servers to query for {@code hostname}. + * @param hostname The hostname for which to lookup the DNS server addressed to use. + * If this is the final {@link DnsServerAddressStreamProvider} to be queried then generally empty + * string or {@code '.'} correspond to the default {@link DnsServerAddressStream}. + * @return The {@link DnsServerAddressStream} which should be used to resolve {@code hostname}. + */ + DnsServerAddressStream nameServerAddressStream(String hostname); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java new file mode 100644 index 0000000..6962b11 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddressStreamProviders.java @@ -0,0 +1,155 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.internal.PlatformDependent; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.security.AccessController; +import java.security.PrivilegedAction; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; + +/** + * Utility methods related to {@link DnsServerAddressStreamProvider}. + */ +public final class DnsServerAddressStreamProviders { + + private static final InternalLogger LOGGER = + InternalLoggerFactory.getInstance(DnsServerAddressStreamProviders.class); + private static final Constructor STREAM_PROVIDER_CONSTRUCTOR; + private static final String MACOS_PROVIDER_CLASS_NAME = + "io.netty.resolver.dns.macos.MacOSDnsServerAddressStreamProvider"; + + static { + Constructor constructor = null; + if (PlatformDependent.isOsx()) { + try { + // As MacOSDnsServerAddressStreamProvider is contained in another jar which depends on this jar + // we use reflection to use it if its on the classpath. + Object maybeProvider = AccessController.doPrivileged(new PrivilegedAction() { + @Override + public Object run() { + try { + return Class.forName( + MACOS_PROVIDER_CLASS_NAME, + true, + DnsServerAddressStreamProviders.class.getClassLoader()); + } catch (Throwable cause) { + return cause; + } + } + }); + if (maybeProvider instanceof Class) { + @SuppressWarnings("unchecked") + Class providerClass = + (Class) maybeProvider; + constructor = providerClass.getConstructor(); + constructor.newInstance(); // ctor ensures availability + LOGGER.debug("{}: available", MACOS_PROVIDER_CLASS_NAME); + } else { + throw (Throwable) maybeProvider; + } + } catch (ClassNotFoundException cause) { + LOGGER.warn("Can not find {} in the classpath, fallback to system defaults. This may result in " + + "incorrect DNS resolutions on MacOS. Check whether you have a dependency on " + + "'io.netty:netty-resolver-dns-native-macos'", MACOS_PROVIDER_CLASS_NAME); + } catch (Throwable cause) { + if (LOGGER.isDebugEnabled()) { + LOGGER.error("Unable to load {}, fallback to system defaults. This may result in " + + "incorrect DNS resolutions on MacOS. Check whether you have a dependency on " + + "'io.netty:netty-resolver-dns-native-macos'", MACOS_PROVIDER_CLASS_NAME, cause); + } else { + LOGGER.error("Unable to load {}, fallback to system defaults. This may result in " + + "incorrect DNS resolutions on MacOS. Check whether you have a dependency on " + + "'io.netty:netty-resolver-dns-native-macos'. Use DEBUG level to see the full stack: {}", + MACOS_PROVIDER_CLASS_NAME, + cause.getCause() != null ? cause.getCause().toString() : cause.toString()); + } + constructor = null; + } + } + STREAM_PROVIDER_CONSTRUCTOR = constructor; + } + + private DnsServerAddressStreamProviders() { + } + + /** + * A {@link DnsServerAddressStreamProvider} which inherits the DNS servers from your local host's configuration. + *

+ * Note that only macOS and Linux are currently supported. + * @return A {@link DnsServerAddressStreamProvider} which inherits the DNS servers from your local host's + * configuration. + */ + public static DnsServerAddressStreamProvider platformDefault() { + if (STREAM_PROVIDER_CONSTRUCTOR != null) { + try { + return STREAM_PROVIDER_CONSTRUCTOR.newInstance(); + } catch (IllegalAccessException e) { + // ignore + } catch (InstantiationException e) { + // ignore + } catch (InvocationTargetException e) { + // ignore + } + } + return unixDefault(); + } + + public static DnsServerAddressStreamProvider unixDefault() { + return DefaultProviderHolder.DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER; + } + + // We use a Holder class to only initialize DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER if we really + // need it. + private static final class DefaultProviderHolder { + // We use 5 minutes which is the same as what OpenJDK is using in sun.net.dns.ResolverConfigurationImpl. + private static final long REFRESH_INTERVAL = TimeUnit.MINUTES.toNanos(5); + + // TODO(scott): how is this done on Windows? This may require a JNI call to GetNetworkParams + // https://msdn.microsoft.com/en-us/library/aa365968(VS.85).aspx. + static final DnsServerAddressStreamProvider DEFAULT_DNS_SERVER_ADDRESS_STREAM_PROVIDER = + new DnsServerAddressStreamProvider() { + private volatile DnsServerAddressStreamProvider currentProvider = provider(); + private final AtomicLong lastRefresh = new AtomicLong(System.nanoTime()); + + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + long last = lastRefresh.get(); + DnsServerAddressStreamProvider current = currentProvider; + if (System.nanoTime() - last > REFRESH_INTERVAL) { + // This is slightly racy which means it will be possible still use the old configuration + // for a small amount of time, but that's ok. + if (lastRefresh.compareAndSet(last, System.nanoTime())) { + current = currentProvider = provider(); + } + } + return current.nameServerAddressStream(hostname); + } + + private DnsServerAddressStreamProvider provider() { + // If on windows just use the DefaultDnsServerAddressStreamProvider.INSTANCE as otherwise + // we will log some error which may be confusing. + return PlatformDependent.isWindows() ? DefaultDnsServerAddressStreamProvider.INSTANCE : + UnixResolverDnsServerAddressStreamProvider.parseSilently(); + } + }; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java new file mode 100644 index 0000000..9cc9c0b --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerAddresses.java @@ -0,0 +1,209 @@ +/* + * Copyright 2014 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.ObjectUtil.checkNonEmpty; + +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +/** + * Provides an infinite sequence of DNS server addresses to {@link DnsNameResolver}. + */ +@SuppressWarnings("IteratorNextCanNotThrowNoSuchElementException") +public abstract class DnsServerAddresses { + /** + * @deprecated Use {@link DefaultDnsServerAddressStreamProvider#defaultAddressList()}. + *

+ * Returns the list of the system DNS server addresses. If it failed to retrieve the list of the system DNS server + * addresses from the environment, it will return {@code "8.8.8.8"} and {@code "8.8.4.4"}, the addresses of the + * Google public DNS servers. + */ + @Deprecated + public static List defaultAddressList() { + return DefaultDnsServerAddressStreamProvider.defaultAddressList(); + } + + /** + * @deprecated Use {@link DefaultDnsServerAddressStreamProvider#defaultAddresses()}. + *

+ * Returns the {@link DnsServerAddresses} that yields the system DNS server addresses sequentially. If it failed to + * retrieve the list of the system DNS server addresses from the environment, it will use {@code "8.8.8.8"} and + * {@code "8.8.4.4"}, the addresses of the Google public DNS servers. + *

+ * This method has the same effect with the following code: + *

+     * DnsServerAddresses.sequential(DnsServerAddresses.defaultAddressList());
+     * 
+ *

+ */ + @Deprecated + public static DnsServerAddresses defaultAddresses() { + return DefaultDnsServerAddressStreamProvider.defaultAddresses(); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} sequentially. Once the + * last address is yielded, it will start again from the first address. + */ + public static DnsServerAddresses sequential(Iterable addresses) { + return sequential0(sanitize(addresses)); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} sequentially. Once the + * last address is yielded, it will start again from the first address. + */ + public static DnsServerAddresses sequential(InetSocketAddress... addresses) { + return sequential0(sanitize(addresses)); + } + + private static DnsServerAddresses sequential0(final List addresses) { + if (addresses.size() == 1) { + return singleton(addresses.get(0)); + } + + return new DefaultDnsServerAddresses("sequential", addresses) { + @Override + public DnsServerAddressStream stream() { + return new SequentialDnsServerAddressStream(addresses, 0); + } + }; + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code address} in a shuffled order. Once all + * addresses are yielded, the addresses are shuffled again. + */ + public static DnsServerAddresses shuffled(Iterable addresses) { + return shuffled0(sanitize(addresses)); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} in a shuffled order. Once all + * addresses are yielded, the addresses are shuffled again. + */ + public static DnsServerAddresses shuffled(InetSocketAddress... addresses) { + return shuffled0(sanitize(addresses)); + } + + private static DnsServerAddresses shuffled0(List addresses) { + if (addresses.size() == 1) { + return singleton(addresses.get(0)); + } + + return new DefaultDnsServerAddresses("shuffled", addresses) { + @Override + public DnsServerAddressStream stream() { + return new ShuffledDnsServerAddressStream(addresses); + } + }; + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} in a rotational sequential + * order. It is similar to {@link #sequential(Iterable)}, but each {@link DnsServerAddressStream} starts from + * a different starting point. For example, the first {@link #stream()} will start from the first address, the + * second one will start from the second address, and so on. + */ + public static DnsServerAddresses rotational(Iterable addresses) { + return rotational0(sanitize(addresses)); + } + + /** + * Returns the {@link DnsServerAddresses} that yields the specified {@code addresses} in a rotational sequential + * order. It is similar to {@link #sequential(Iterable)}, but each {@link DnsServerAddressStream} starts from + * a different starting point. For example, the first {@link #stream()} will start from the first address, the + * second one will start from the second address, and so on. + */ + public static DnsServerAddresses rotational(InetSocketAddress... addresses) { + return rotational0(sanitize(addresses)); + } + + private static DnsServerAddresses rotational0(List addresses) { + if (addresses.size() == 1) { + return singleton(addresses.get(0)); + } + + return new RotationalDnsServerAddresses(addresses); + } + + /** + * Returns the {@link DnsServerAddresses} that yields only a single {@code address}. + */ + public static DnsServerAddresses singleton(final InetSocketAddress address) { + checkNotNull(address, "address"); + if (address.isUnresolved()) { + throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + address); + } + + return new SingletonDnsServerAddresses(address); + } + + private static List sanitize(Iterable addresses) { + checkNotNull(addresses, "addresses"); + + final List list; + if (addresses instanceof Collection) { + list = new ArrayList(((Collection) addresses).size()); + } else { + list = new ArrayList(4); + } + + for (InetSocketAddress a : addresses) { + if (a == null) { + break; + } + if (a.isUnresolved()) { + throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + a); + } + list.add(a); + } + + return checkNonEmpty(list, "list"); + } + + private static List sanitize(InetSocketAddress[] addresses) { + checkNotNull(addresses, "addresses"); + + List list = new ArrayList(addresses.length); + for (InetSocketAddress a: addresses) { + if (a == null) { + break; + } + if (a.isUnresolved()) { + throw new IllegalArgumentException("cannot use an unresolved DNS server address: " + a); + } + list.add(a); + } + + if (list.isEmpty()) { + return DefaultDnsServerAddressStreamProvider.defaultAddressList(); + } + + return list; + } + + /** + * Starts a new infinite stream of DNS server addresses. This method is invoked by {@link DnsNameResolver} on every + * uncached {@link DnsNameResolver#resolve(String)}or {@link DnsNameResolver#resolveAll(String)}. + */ + public abstract DnsServerAddressStream stream(); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerResponseFeedbackAddressStream.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerResponseFeedbackAddressStream.java new file mode 100644 index 0000000..a4d24a3 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/DnsServerResponseFeedbackAddressStream.java @@ -0,0 +1,47 @@ +/* + * Copyright 2022 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; + +/** + * An infinite stream of DNS server addresses, that requests feedback to be returned to it. + * + * If query is successful timing information is provided, else a failure notification is given. + */ +public interface DnsServerResponseFeedbackAddressStream extends DnsServerAddressStream { + + /** + * A way to provide success feedback to {@link DnsServerAddressStream} so that {@link #next()} can be tuned + * to return the best performing DNS server address + * + * NOTE: This is called regardless of the RCode returned by the DNS server + * + * @param address The address returned by {@link #next()} that feedback needs to be applied to + * @param queryResponseTimeNanos The response time of a query against the given DNS server + */ + void feedbackSuccess(InetSocketAddress address, long queryResponseTimeNanos); + + /** + * A way to provide failure feedback to {@link DnsServerAddressStream} so that {@link #next()} cab be tuned + * to return the best performing DNS server address + * + * @param address The address returned by {@link #next()} that feedback needs to be applied to + * @param failureCause The reason the DNS query failed, can be used to penalize failures differently + * @param queryResponseTimeNanos The response time of a query against the given DNS server + */ + void feedbackFailure(InetSocketAddress address, Throwable failureCause, long queryResponseTimeNanos); +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/InflightNameResolver.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/InflightNameResolver.java new file mode 100644 index 0000000..0a8c2f4 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/InflightNameResolver.java @@ -0,0 +1,131 @@ +/* + * 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: + * + * https://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.resolver.NameResolver; +import io.netty.util.concurrent.EventExecutor; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.StringUtil; + +import java.util.List; +import java.util.concurrent.ConcurrentMap; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +// FIXME(trustin): Find a better name and move it to the 'resolver' module. +final class InflightNameResolver implements NameResolver { + + private final EventExecutor executor; + private final NameResolver delegate; + private final ConcurrentMap> resolvesInProgress; + private final ConcurrentMap>> resolveAllsInProgress; + + InflightNameResolver(EventExecutor executor, NameResolver delegate, + ConcurrentMap> resolvesInProgress, + ConcurrentMap>> resolveAllsInProgress) { + + this.executor = checkNotNull(executor, "executor"); + this.delegate = checkNotNull(delegate, "delegate"); + this.resolvesInProgress = checkNotNull(resolvesInProgress, "resolvesInProgress"); + this.resolveAllsInProgress = checkNotNull(resolveAllsInProgress, "resolveAllsInProgress"); + } + + @Override + public Future resolve(String inetHost) { + return resolve(inetHost, executor.newPromise()); + } + + @Override + public Future> resolveAll(String inetHost) { + return resolveAll(inetHost, executor.>newPromise()); + } + + @Override + public void close() { + delegate.close(); + } + + @Override + public Promise resolve(String inetHost, Promise promise) { + return resolve(resolvesInProgress, inetHost, promise, false); + } + + @Override + public Promise> resolveAll(String inetHost, Promise> promise) { + return resolve(resolveAllsInProgress, inetHost, promise, true); + } + + private Promise resolve( + final ConcurrentMap> resolveMap, + final String inetHost, final Promise promise, boolean resolveAll) { + + final Promise earlyPromise = resolveMap.putIfAbsent(inetHost, promise); + if (earlyPromise != null) { + // Name resolution for the specified inetHost is in progress already. + if (earlyPromise.isDone()) { + transferResult(earlyPromise, promise); + } else { + earlyPromise.addListener(new FutureListener() { + @Override + public void operationComplete(Future f) throws Exception { + transferResult(f, promise); + } + }); + } + } else { + try { + if (resolveAll) { + @SuppressWarnings("unchecked") + final Promise> castPromise = (Promise>) promise; // U is List + delegate.resolveAll(inetHost, castPromise); + } else { + @SuppressWarnings("unchecked") + final Promise castPromise = (Promise) promise; // U is T + delegate.resolve(inetHost, castPromise); + } + } finally { + if (promise.isDone()) { + resolveMap.remove(inetHost); + } else { + promise.addListener(new FutureListener() { + @Override + public void operationComplete(Future f) throws Exception { + resolveMap.remove(inetHost); + } + }); + } + } + } + + return promise; + } + + private static void transferResult(Future src, Promise dst) { + if (src.isSuccess()) { + dst.trySuccess(src.getNow()); + } else { + dst.tryFailure(src.cause()); + } + } + + @Override + public String toString() { + return StringUtil.simpleClassName(this) + '(' + delegate + ')'; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/LoggingDnsQueryLifeCycleObserverFactory.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/LoggingDnsQueryLifeCycleObserverFactory.java new file mode 100644 index 0000000..9212028 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/LoggingDnsQueryLifeCycleObserverFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright 2020 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.logging.LogLevel; +import io.netty.util.internal.logging.InternalLogLevel; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +/** + * A {@link DnsQueryLifecycleObserverFactory} that enables detailed logging in the {@link DnsNameResolver}. + *

+ * When {@linkplain DnsNameResolverBuilder#dnsQueryLifecycleObserverFactory(DnsQueryLifecycleObserverFactory) + * configured on the resolver}, detailed trace information will be generated so that it is easier to understand the + * cause of resolution failure. + */ +public final class LoggingDnsQueryLifeCycleObserverFactory implements DnsQueryLifecycleObserverFactory { + private static final InternalLogger DEFAULT_LOGGER = + InternalLoggerFactory.getInstance(LoggingDnsQueryLifeCycleObserverFactory.class); + private final InternalLogger logger; + private final InternalLogLevel level; + + /** + * Create {@link DnsQueryLifecycleObserver} instances that log events at the default {@link LogLevel#DEBUG} level. + */ + public LoggingDnsQueryLifeCycleObserverFactory() { + this(LogLevel.DEBUG); + } + + /** + * Create {@link DnsQueryLifecycleObserver} instances that log events at the given log level. + * @param level The log level to use for logging resolver events. + */ + public LoggingDnsQueryLifeCycleObserverFactory(LogLevel level) { + this.level = checkAndConvertLevel(level); + logger = DEFAULT_LOGGER; + } + + /** + * Create {@link DnsQueryLifecycleObserver} instances that log events to a logger with the given class context, + * at the given log level. + * @param classContext The class context for the logger to use. + * @param level The log level to use for logging resolver events. + */ + public LoggingDnsQueryLifeCycleObserverFactory(Class classContext, LogLevel level) { + this.level = checkAndConvertLevel(level); + logger = InternalLoggerFactory.getInstance(checkNotNull(classContext, "classContext")); + } + + /** + * Create {@link DnsQueryLifecycleObserver} instances that log events to a logger with the given name context, + * at the given log level. + * @param name The name for the logger to use. + * @param level The log level to use for logging resolver events. + */ + public LoggingDnsQueryLifeCycleObserverFactory(String name, LogLevel level) { + this.level = checkAndConvertLevel(level); + logger = InternalLoggerFactory.getInstance(checkNotNull(name, "name")); + } + + private static InternalLogLevel checkAndConvertLevel(LogLevel level) { + return checkNotNull(level, "level").toInternalLevel(); + } + + @Override + public DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) { + return new LoggingDnsQueryLifecycleObserver(question, logger, level); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/LoggingDnsQueryLifecycleObserver.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/LoggingDnsQueryLifecycleObserver.java new file mode 100644 index 0000000..33fd081 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/LoggingDnsQueryLifecycleObserver.java @@ -0,0 +1,87 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.channel.ChannelFuture; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResponseCode; +import io.netty.util.internal.logging.InternalLogLevel; +import io.netty.util.internal.logging.InternalLogger; + +import java.net.InetSocketAddress; +import java.util.List; + +import static io.netty.util.internal.ObjectUtil.checkNotNull; + +final class LoggingDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + private final InternalLogger logger; + private final InternalLogLevel level; + private final DnsQuestion question; + private InetSocketAddress dnsServerAddress; + + LoggingDnsQueryLifecycleObserver(DnsQuestion question, InternalLogger logger, InternalLogLevel level) { + this.question = checkNotNull(question, "question"); + this.logger = checkNotNull(logger, "logger"); + this.level = checkNotNull(level, "level"); + } + + @Override + public void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + this.dnsServerAddress = dnsServerAddress; + } + + @Override + public void queryCancelled(int queriesRemaining) { + if (dnsServerAddress != null) { + logger.log(level, "from {} : {} cancelled with {} queries remaining", dnsServerAddress, question, + queriesRemaining); + } else { + logger.log(level, "{} query never written and cancelled with {} queries remaining", question, + queriesRemaining); + } + } + + @Override + public DnsQueryLifecycleObserver queryRedirected(List nameServers) { + logger.log(level, "from {} : {} redirected", dnsServerAddress, question); + return this; + } + + @Override + public DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion) { + logger.log(level, "from {} : {} CNAME question {}", dnsServerAddress, question, cnameQuestion); + return this; + } + + @Override + public DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code) { + logger.log(level, "from {} : {} no answer {}", dnsServerAddress, question, code); + return this; + } + + @Override + public void queryFailed(Throwable cause) { + if (dnsServerAddress != null) { + logger.log(level, "from {} : {} failure", dnsServerAddress, question, cause); + } else { + logger.log(level, "{} query never written and failed", question, cause); + } + } + + @Override + public void querySucceed() { + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..5d16446 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/MultiDnsServerAddressStreamProvider.java @@ -0,0 +1,53 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.util.List; + +/** + * A {@link DnsServerAddressStreamProvider} which iterates through a collection of + * {@link DnsServerAddressStreamProvider} until the first non-{@code null} result is found. + */ +public final class MultiDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private final DnsServerAddressStreamProvider[] providers; + + /** + * Create a new instance. + * @param providers The providers to use for DNS resolution. They will be queried in order. + */ + public MultiDnsServerAddressStreamProvider(List providers) { + this.providers = providers.toArray(new DnsServerAddressStreamProvider[0]); + } + + /** + * Create a new instance. + * @param providers The providers to use for DNS resolution. They will be queried in order. + */ + public MultiDnsServerAddressStreamProvider(DnsServerAddressStreamProvider... providers) { + this.providers = providers.clone(); + } + + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + for (DnsServerAddressStreamProvider provider : providers) { + DnsServerAddressStream stream = provider.nameServerAddressStream(hostname); + if (stream != null) { + return stream; + } + } + return null; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NameServerComparator.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NameServerComparator.java new file mode 100644 index 0000000..351505f --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NameServerComparator.java @@ -0,0 +1,61 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.internal.ObjectUtil; + +import java.io.Serializable; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Comparator; +import java.util.List; + +/** + * Special {@link Comparator} implementation to sort the nameservers to use when follow redirects. + * + * This implementation follows all the semantics listed in the + * Comparator apidocs + * with the limitation that {@link InetSocketAddress#equals(Object)} will not result in the same return value as + * {@link #compare(InetSocketAddress, InetSocketAddress)}. This is completely fine as this should only be used + * to sort {@link List}s. + */ +public final class NameServerComparator implements Comparator, Serializable { + + private static final long serialVersionUID = 8372151874317596185L; + + private final Class preferredAddressType; + + public NameServerComparator(Class preferredAddressType) { + this.preferredAddressType = ObjectUtil.checkNotNull(preferredAddressType, "preferredAddressType"); + } + + @Override + public int compare(InetSocketAddress addr1, InetSocketAddress addr2) { + if (addr1.equals(addr2)) { + return 0; + } + if (!addr1.isUnresolved() && !addr2.isUnresolved()) { + if (addr1.getAddress().getClass() == addr2.getAddress().getClass()) { + return 0; + } + return preferredAddressType.isAssignableFrom(addr1.getAddress().getClass()) ? -1 : 1; + } + if (addr1.isUnresolved() && addr2.isUnresolved()) { + return 0; + } + return addr1.isUnresolved() ? 1 : -1; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java new file mode 100644 index 0000000..9266d62 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopAuthoritativeDnsServerCache.java @@ -0,0 +1,49 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; + +import java.net.InetSocketAddress; + +/** + * A noop {@link AuthoritativeDnsServerCache} that actually never caches anything. + */ +public final class NoopAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { + public static final NoopAuthoritativeDnsServerCache INSTANCE = new NoopAuthoritativeDnsServerCache(); + + private NoopAuthoritativeDnsServerCache() { } + + @Override + public DnsServerAddressStream get(String hostname) { + return null; + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + // NOOP + } + + @Override + public void clear() { + // NOOP + } + + @Override + public boolean clear(String hostname) { + return false; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java new file mode 100644 index 0000000..ec1d8dd --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCache.java @@ -0,0 +1,90 @@ +/* + * 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: + * + * https://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.EventLoop; +import io.netty.handler.codec.dns.DnsRecord; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.List; + +/** + * A noop DNS cache that actually never caches anything. + */ +public final class NoopDnsCache implements DnsCache { + + public static final NoopDnsCache INSTANCE = new NoopDnsCache(); + + /** + * Private singleton constructor. + */ + private NoopDnsCache() { + } + + @Override + public void clear() { + } + + @Override + public boolean clear(String hostname) { + return false; + } + + @Override + public List get(String hostname, DnsRecord[] additionals) { + return Collections.emptyList(); + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additional, + InetAddress address, long originalTtl, EventLoop loop) { + return new NoopDnsCacheEntry(address); + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additional, Throwable cause, EventLoop loop) { + return null; + } + + @Override + public String toString() { + return NoopDnsCache.class.getSimpleName(); + } + + private static final class NoopDnsCacheEntry implements DnsCacheEntry { + private final InetAddress address; + + NoopDnsCacheEntry(InetAddress address) { + this.address = address; + } + + @Override + public InetAddress address() { + return address; + } + + @Override + public Throwable cause() { + return null; + } + + @Override + public String toString() { + return address.toString(); + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java new file mode 100644 index 0000000..3efd084 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsCnameCache.java @@ -0,0 +1,45 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.EventLoop; + +public final class NoopDnsCnameCache implements DnsCnameCache { + + public static final NoopDnsCnameCache INSTANCE = new NoopDnsCnameCache(); + + private NoopDnsCnameCache() { } + + @Override + public String get(String hostname) { + return null; + } + + @Override + public void cache(String hostname, String cname, long originalTtl, EventLoop loop) { + // NOOP + } + + @Override + public void clear() { + // NOOP + } + + @Override + public boolean clear(String hostname) { + return false; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserver.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserver.java new file mode 100644 index 0000000..5dc9b6a --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserver.java @@ -0,0 +1,61 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.channel.ChannelFuture; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsResponseCode; + +import java.net.InetSocketAddress; +import java.util.List; + +final class NoopDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + static final NoopDnsQueryLifecycleObserver INSTANCE = new NoopDnsQueryLifecycleObserver(); + + private NoopDnsQueryLifecycleObserver() { + } + + @Override + public void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + } + + @Override + public void queryCancelled(int queriesRemaining) { + } + + @Override + public DnsQueryLifecycleObserver queryRedirected(List nameServers) { + return this; + } + + @Override + public DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion) { + return this; + } + + @Override + public DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code) { + return this; + } + + @Override + public void queryFailed(Throwable cause) { + } + + @Override + public void querySucceed() { + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java new file mode 100644 index 0000000..f09d698 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/NoopDnsQueryLifecycleObserverFactory.java @@ -0,0 +1,30 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import io.netty.handler.codec.dns.DnsQuestion; + +public final class NoopDnsQueryLifecycleObserverFactory implements DnsQueryLifecycleObserverFactory { + public static final NoopDnsQueryLifecycleObserverFactory INSTANCE = new NoopDnsQueryLifecycleObserverFactory(); + + private NoopDnsQueryLifecycleObserverFactory() { + } + + @Override + public DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) { + return NoopDnsQueryLifecycleObserver.INSTANCE; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java new file mode 100644 index 0000000..17dfa03 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/PreferredAddressTypeComparator.java @@ -0,0 +1,54 @@ +/* + * Copyright 2019 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: + * + * https://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.socket.InternetProtocolFamily; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.util.Comparator; + +final class PreferredAddressTypeComparator implements Comparator { + + private static final PreferredAddressTypeComparator IPv4 = new PreferredAddressTypeComparator(Inet4Address.class); + private static final PreferredAddressTypeComparator IPv6 = new PreferredAddressTypeComparator(Inet6Address.class); + + static PreferredAddressTypeComparator comparator(InternetProtocolFamily family) { + switch (family) { + case IPv4: + return IPv4; + case IPv6: + return IPv6; + default: + throw new IllegalArgumentException(); + } + } + + private final Class preferredAddressType; + + private PreferredAddressTypeComparator(Class preferredAddressType) { + this.preferredAddressType = preferredAddressType; + } + + @Override + public int compare(InetAddress o1, InetAddress o2) { + if (o1.getClass() == o2.getClass()) { + return 0; + } + return preferredAddressType.isAssignableFrom(o1.getClass()) ? -1 : 1; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/RotationalDnsServerAddresses.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/RotationalDnsServerAddresses.java new file mode 100644 index 0000000..ff85bf3 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/RotationalDnsServerAddresses.java @@ -0,0 +1,48 @@ +/* + * Copyright 2015 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; +import java.util.List; +import java.util.concurrent.atomic.AtomicIntegerFieldUpdater; + +final class RotationalDnsServerAddresses extends DefaultDnsServerAddresses { + + private static final AtomicIntegerFieldUpdater startIdxUpdater = + AtomicIntegerFieldUpdater.newUpdater(RotationalDnsServerAddresses.class, "startIdx"); + + @SuppressWarnings("UnusedDeclaration") + private volatile int startIdx; + + RotationalDnsServerAddresses(List addresses) { + super("rotational", addresses); + } + + @Override + public DnsServerAddressStream stream() { + for (;;) { + int curStartIdx = startIdx; + int nextStartIdx = curStartIdx + 1; + if (nextStartIdx >= addresses.size()) { + nextStartIdx = 0; + } + if (startIdxUpdater.compareAndSet(this, curStartIdx, nextStartIdx)) { + return new SequentialDnsServerAddressStream(addresses, curStartIdx); + } + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java new file mode 100644 index 0000000..d389af7 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/RoundRobinDnsAddressResolverGroup.java @@ -0,0 +1,66 @@ +/* + * 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: + * + * https://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.ChannelFactory; +import io.netty.channel.EventLoop; +import io.netty.channel.socket.DatagramChannel; +import io.netty.resolver.AddressResolver; +import io.netty.resolver.AddressResolverGroup; +import io.netty.resolver.NameResolver; +import io.netty.resolver.RoundRobinInetAddressResolver; + +import java.net.InetAddress; +import java.net.InetSocketAddress; + +/** + * A {@link AddressResolverGroup} of {@link DnsNameResolver}s that supports random selection of destination addresses if + * multiple are provided by the nameserver. This is ideal for use in applications that use a pool of connections, for + * which connecting to a single resolved address would be inefficient. + */ +public class RoundRobinDnsAddressResolverGroup extends DnsAddressResolverGroup { + + public RoundRobinDnsAddressResolverGroup(DnsNameResolverBuilder dnsResolverBuilder) { + super(dnsResolverBuilder); + } + + public RoundRobinDnsAddressResolverGroup( + Class channelType, + DnsServerAddressStreamProvider nameServerProvider) { + super(channelType, nameServerProvider); + } + + public RoundRobinDnsAddressResolverGroup( + ChannelFactory channelFactory, + DnsServerAddressStreamProvider nameServerProvider) { + super(channelFactory, nameServerProvider); + } + + /** + * We need to override this method, not + * {@link #newNameResolver(EventLoop, ChannelFactory, DnsServerAddressStreamProvider)}, + * because we need to eliminate possible caching of {@link io.netty.resolver.NameResolver#resolve} + * by {@link InflightNameResolver} created in + * {@link #newResolver(EventLoop, ChannelFactory, DnsServerAddressStreamProvider)}. + */ + @Override + protected final AddressResolver newAddressResolver(EventLoop eventLoop, + NameResolver resolver) + throws Exception { + return new RoundRobinInetAddressResolver(eventLoop, resolver).asAddressResolver(); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java new file mode 100644 index 0000000..5583073 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStream.java @@ -0,0 +1,73 @@ +/* + * Copyright 2015 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; +import java.util.Collection; +import java.util.List; + +final class SequentialDnsServerAddressStream implements DnsServerAddressStream { + + private final List addresses; + private int i; + + SequentialDnsServerAddressStream(List addresses, int startIdx) { + this.addresses = addresses; + i = startIdx; + } + + @Override + public InetSocketAddress next() { + int i = this.i; + InetSocketAddress next = addresses.get(i); + if (++ i < addresses.size()) { + this.i = i; + } else { + this.i = 0; + } + return next; + } + + @Override + public int size() { + return addresses.size(); + } + + @Override + public SequentialDnsServerAddressStream duplicate() { + return new SequentialDnsServerAddressStream(addresses, i); + } + + @Override + public String toString() { + return toString("sequential", i, addresses); + } + + static String toString(String type, int index, Collection addresses) { + final StringBuilder buf = new StringBuilder(type.length() + 2 + addresses.size() * 16); + buf.append(type).append("(index: ").append(index); + buf.append(", addrs: ("); + for (InetSocketAddress a: addresses) { + buf.append(a).append(", "); + } + + buf.setLength(buf.length() - 2); + buf.append("))"); + + return buf.toString(); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..ac76909 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SequentialDnsServerAddressStreamProvider.java @@ -0,0 +1,43 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; + +import static io.netty.resolver.dns.DnsServerAddresses.sequential; + +/** + * A {@link DnsServerAddressStreamProvider} which is backed by a sequential list of DNS servers. + */ +public final class SequentialDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider { + /** + * Create a new instance. + * @param addresses The addresses which will be be returned in sequential order via + * {@link #nameServerAddressStream(String)} + */ + public SequentialDnsServerAddressStreamProvider(InetSocketAddress... addresses) { + super(sequential(addresses)); + } + + /** + * Create a new instance. + * @param addresses The addresses which will be be returned in sequential order via + * {@link #nameServerAddressStream(String)} + */ + public SequentialDnsServerAddressStreamProvider(Iterable addresses) { + super(sequential(addresses)); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java new file mode 100644 index 0000000..25c03cb --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/ShuffledDnsServerAddressStream.java @@ -0,0 +1,77 @@ +/* + * Copyright 2015 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: + * + * https://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.internal.PlatformDependent; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.List; + +final class ShuffledDnsServerAddressStream implements DnsServerAddressStream { + + private final List addresses; + private int i; + + /** + * Create a new instance. + * @param addresses The addresses are not cloned. It is assumed the caller has cloned this array or otherwise will + * not modify the contents. + */ + ShuffledDnsServerAddressStream(List addresses) { + this.addresses = addresses; + + shuffle(); + } + + private ShuffledDnsServerAddressStream(List addresses, int startIdx) { + this.addresses = addresses; + i = startIdx; + } + + private void shuffle() { + Collections.shuffle(addresses, PlatformDependent.threadLocalRandom()); + } + + @Override + public InetSocketAddress next() { + int i = this.i; + InetSocketAddress next = addresses.get(i); + if (++ i < addresses.size()) { + this.i = i; + } else { + this.i = 0; + shuffle(); + } + return next; + } + + @Override + public int size() { + return addresses.size(); + } + + @Override + public ShuffledDnsServerAddressStream duplicate() { + return new ShuffledDnsServerAddressStream(addresses, i); + } + + @Override + public String toString() { + return SequentialDnsServerAddressStream.toString("shuffled", i, addresses); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..6bb7f3d --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddressStreamProvider.java @@ -0,0 +1,31 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; + +/** + * A {@link DnsServerAddressStreamProvider} which always uses a single DNS server for resolution. + */ +public final class SingletonDnsServerAddressStreamProvider extends UniSequentialDnsServerAddressStreamProvider { + /** + * Create a new instance. + * @param address The singleton address to use for every DNS resolution. + */ + public SingletonDnsServerAddressStreamProvider(final InetSocketAddress address) { + super(DnsServerAddresses.singleton(address)); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java new file mode 100644 index 0000000..92358d4 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/SingletonDnsServerAddresses.java @@ -0,0 +1,60 @@ +/* + * Copyright 2015 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: + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT + * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the + * License for the specific language governing permissions and limitations + * under the License. + */ + +package io.netty.resolver.dns; + +import java.net.InetSocketAddress; + +final class SingletonDnsServerAddresses extends DnsServerAddresses { + + private final InetSocketAddress address; + + private final DnsServerAddressStream stream = new DnsServerAddressStream() { + @Override + public InetSocketAddress next() { + return address; + } + + @Override + public int size() { + return 1; + } + + @Override + public DnsServerAddressStream duplicate() { + return this; + } + + @Override + public String toString() { + return SingletonDnsServerAddresses.this.toString(); + } + }; + + SingletonDnsServerAddresses(InetSocketAddress address) { + this.address = address; + } + + @Override + public DnsServerAddressStream stream() { + return stream; + } + + @Override + public String toString() { + return "singleton(" + address + ")"; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java new file mode 100644 index 0000000..f8022a8 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/TcpDnsQueryContext.java @@ -0,0 +1,53 @@ +/* + * Copyright 2019 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: + * + * https://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.AddressedEnvelope; +import io.netty.channel.Channel; +import io.netty.handler.codec.dns.DefaultDnsQuery; +import io.netty.handler.codec.dns.DnsQuery; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; + +import java.net.InetSocketAddress; + +final class TcpDnsQueryContext extends DnsQueryContext { + + TcpDnsQueryContext(Channel channel, Future channelReadyFuture, + InetSocketAddress nameServerAddr, + DnsQueryContextManager queryContextManager, + int maxPayLoadSize, boolean recursionDesired, + long queryTimeoutMillis, + DnsQuestion question, DnsRecord[] additionals, + Promise> promise) { + super(channel, channelReadyFuture, nameServerAddr, queryContextManager, maxPayLoadSize, recursionDesired, + // No retry via TCP. + queryTimeoutMillis, question, additionals, promise, null, false); + } + + @Override + protected DnsQuery newQuery(int id, InetSocketAddress nameServerAddr) { + return new DefaultDnsQuery(id); + } + + @Override + protected String protocol() { + return "TCP"; + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UniSequentialDnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UniSequentialDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..6c99747 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UniSequentialDnsServerAddressStreamProvider.java @@ -0,0 +1,34 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.internal.ObjectUtil; + +/** + * A {@link DnsServerAddressStreamProvider} which is backed by a single {@link DnsServerAddresses}. + */ +abstract class UniSequentialDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private final DnsServerAddresses addresses; + + UniSequentialDnsServerAddressStreamProvider(DnsServerAddresses addresses) { + this.addresses = ObjectUtil.checkNotNull(addresses, "addresses"); + } + + @Override + public final DnsServerAddressStream nameServerAddressStream(String hostname) { + return addresses.stream(); + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java new file mode 100644 index 0000000..f8eff17 --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProvider.java @@ -0,0 +1,410 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.SocketUtils; +import io.netty.util.internal.logging.InternalLogger; +import io.netty.util.internal.logging.InternalLoggerFactory; + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.DNS_PORT; +import static io.netty.util.internal.ObjectUtil.checkNotNull; +import static io.netty.util.internal.StringUtil.indexOfNonWhiteSpace; +import static io.netty.util.internal.StringUtil.indexOfWhiteSpace; + +/** + * Able to parse files such as /etc/resolv.conf and + * + * /etc/resolver to respect the system default domain servers. + */ +public final class UnixResolverDnsServerAddressStreamProvider implements DnsServerAddressStreamProvider { + private static final InternalLogger logger = + InternalLoggerFactory.getInstance(UnixResolverDnsServerAddressStreamProvider.class); + + private static final Pattern WHITESPACE_PATTERN = Pattern.compile("\\s+"); + private static final String RES_OPTIONS = System.getenv("RES_OPTIONS"); + + private static final String ETC_RESOLV_CONF_FILE = "/etc/resolv.conf"; + private static final String ETC_RESOLVER_DIR = "/etc/resolver"; + private static final String NAMESERVER_ROW_LABEL = "nameserver"; + private static final String SORTLIST_ROW_LABEL = "sortlist"; + private static final String OPTIONS_ROW_LABEL = "options "; + private static final String OPTIONS_ROTATE_FLAG = "rotate"; + private static final String DOMAIN_ROW_LABEL = "domain"; + private static final String SEARCH_ROW_LABEL = "search"; + private static final String PORT_ROW_LABEL = "port"; + + private final DnsServerAddresses defaultNameServerAddresses; + private final Map domainToNameServerStreamMap; + + /** + * Attempt to parse {@code /etc/resolv.conf} and files in the {@code /etc/resolver} directory by default. + * A failure to parse will return {@link DefaultDnsServerAddressStreamProvider}. + */ + static DnsServerAddressStreamProvider parseSilently() { + try { + UnixResolverDnsServerAddressStreamProvider nameServerCache = + new UnixResolverDnsServerAddressStreamProvider(ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR); + return nameServerCache.mayOverrideNameServers() ? nameServerCache + : DefaultDnsServerAddressStreamProvider.INSTANCE; + } catch (Exception e) { + if (logger.isDebugEnabled()) { + logger.debug("failed to parse {} and/or {}", ETC_RESOLV_CONF_FILE, ETC_RESOLVER_DIR, e); + } + return DefaultDnsServerAddressStreamProvider.INSTANCE; + } + } + + /** + * Parse a file of the format /etc/resolv.conf which may contain + * the default DNS server to use, and also overrides for individual domains. Also parse list of files of the format + * + * /etc/resolver which may contain multiple files to override the name servers used for multiple domains. + * @param etcResolvConf /etc/resolv.conf. + * @param etcResolverFiles List of files of the format defined in + * + * /etc/resolver. + * @throws IOException If an error occurs while parsing the input files. + */ + public UnixResolverDnsServerAddressStreamProvider(File etcResolvConf, File... etcResolverFiles) throws IOException { + Map etcResolvConfMap = parse(checkNotNull(etcResolvConf, "etcResolvConf")); + final boolean useEtcResolverFiles = etcResolverFiles != null && etcResolverFiles.length != 0; + domainToNameServerStreamMap = useEtcResolverFiles ? parse(etcResolverFiles) : etcResolvConfMap; + + DnsServerAddresses defaultNameServerAddresses + = etcResolvConfMap.get(etcResolvConf.getName()); + if (defaultNameServerAddresses == null) { + Collection values = etcResolvConfMap.values(); + if (values.isEmpty()) { + throw new IllegalArgumentException(etcResolvConf + " didn't provide any name servers"); + } + this.defaultNameServerAddresses = values.iterator().next(); + } else { + this.defaultNameServerAddresses = defaultNameServerAddresses; + } + + if (useEtcResolverFiles) { + domainToNameServerStreamMap.putAll(etcResolvConfMap); + } + } + + /** + * Parse a file of the format /etc/resolv.conf which may contain + * the default DNS server to use, and also overrides for individual domains. Also parse a directory of the format + * + * /etc/resolver which may contain multiple files to override the name servers used for multiple domains. + * @param etcResolvConf /etc/resolv.conf. + * @param etcResolverDir Directory containing files of the format defined in + * + * /etc/resolver. + * @throws IOException If an error occurs while parsing the input files. + */ + public UnixResolverDnsServerAddressStreamProvider(String etcResolvConf, String etcResolverDir) throws IOException { + this(etcResolvConf == null ? null : new File(etcResolvConf), + etcResolverDir == null ? null : new File(etcResolverDir).listFiles()); + } + + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + for (;;) { + int i = hostname.indexOf('.', 1); + if (i < 0 || i == hostname.length() - 1) { + return defaultNameServerAddresses.stream(); + } + + DnsServerAddresses addresses = domainToNameServerStreamMap.get(hostname); + if (addresses != null) { + return addresses.stream(); + } + + hostname = hostname.substring(i + 1); + } + } + + private boolean mayOverrideNameServers() { + return !domainToNameServerStreamMap.isEmpty() || defaultNameServerAddresses.stream().next() != null; + } + + private static Map parse(File... etcResolverFiles) throws IOException { + Map domainToNameServerStreamMap = + new HashMap(etcResolverFiles.length << 1); + boolean rotateGlobal = RES_OPTIONS != null && RES_OPTIONS.contains(OPTIONS_ROTATE_FLAG); + for (File etcResolverFile : etcResolverFiles) { + if (!etcResolverFile.isFile()) { + continue; + } + FileReader fr = new FileReader(etcResolverFile); + BufferedReader br = null; + try { + br = new BufferedReader(fr); + List addresses = new ArrayList(2); + String domainName = etcResolverFile.getName(); + boolean rotate = rotateGlobal; + int port = DNS_PORT; + String line; + while ((line = br.readLine()) != null) { + line = line.trim(); + try { + char c; + if (line.isEmpty() || (c = line.charAt(0)) == '#' || c == ';') { + continue; + } + if (!rotate && line.startsWith(OPTIONS_ROW_LABEL)) { + rotate = line.contains(OPTIONS_ROTATE_FLAG); + } else if (line.startsWith(NAMESERVER_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, NAMESERVER_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + + " in file " + etcResolverFile + ". value: " + line); + } + String maybeIP; + int x = indexOfWhiteSpace(line, i); + if (x == -1) { + maybeIP = line.substring(i); + } else { + // ignore comments + int idx = indexOfNonWhiteSpace(line, x); + if (idx == -1 || line.charAt(idx) != '#') { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + + " in file " + etcResolverFile + ". value: " + line); + } + maybeIP = line.substring(i, x); + } + + // There may be a port appended onto the IP address so we attempt to extract it. + if (!NetUtil.isValidIpV4Address(maybeIP) && !NetUtil.isValidIpV6Address(maybeIP)) { + i = maybeIP.lastIndexOf('.'); + if (i + 1 >= maybeIP.length()) { + throw new IllegalArgumentException("error parsing label " + NAMESERVER_ROW_LABEL + + " in file " + etcResolverFile + ". invalid IP value: " + line); + } + port = Integer.parseInt(maybeIP.substring(i + 1)); + maybeIP = maybeIP.substring(0, i); + } + InetSocketAddress addr = SocketUtils.socketAddress(maybeIP, port); + // Check if the address is resolved and only if this is the case use it. Otherwise just + // ignore it. This is needed to filter out invalid entries, as if for example an ipv6 + // address is used with a scope that represent a network interface that does not exists + // on the host. + if (!addr.isUnresolved()) { + addresses.add(addr); + } + } else if (line.startsWith(DOMAIN_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + DOMAIN_ROW_LABEL + + " in file " + etcResolverFile + " value: " + line); + } + domainName = line.substring(i); + if (!addresses.isEmpty()) { + putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate); + } + addresses = new ArrayList(2); + } else if (line.startsWith(PORT_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, PORT_ROW_LABEL.length()); + if (i < 0) { + throw new IllegalArgumentException("error parsing label " + PORT_ROW_LABEL + + " in file " + etcResolverFile + " value: " + line); + } + port = Integer.parseInt(line.substring(i)); + } else if (line.startsWith(SORTLIST_ROW_LABEL)) { + logger.info("row type {} not supported. Ignoring line: {}", SORTLIST_ROW_LABEL, line); + } + } catch (IllegalArgumentException e) { + logger.warn("Could not parse entry. Ignoring line: {}", line, e); + } + } + if (!addresses.isEmpty()) { + putIfAbsent(domainToNameServerStreamMap, domainName, addresses, rotate); + } + } finally { + if (br == null) { + fr.close(); + } else { + br.close(); + } + } + } + return domainToNameServerStreamMap; + } + + private static void putIfAbsent(Map domainToNameServerStreamMap, + String domainName, + List addresses, + boolean rotate) { + // TODO(scott): sortlist is being ignored. + DnsServerAddresses addrs = rotate + ? DnsServerAddresses.rotational(addresses) + : DnsServerAddresses.sequential(addresses); + putIfAbsent(domainToNameServerStreamMap, domainName, addrs); + } + + private static void putIfAbsent(Map domainToNameServerStreamMap, + String domainName, + DnsServerAddresses addresses) { + DnsServerAddresses existingAddresses = domainToNameServerStreamMap.put(domainName, addresses); + if (existingAddresses != null) { + domainToNameServerStreamMap.put(domainName, existingAddresses); + if (logger.isDebugEnabled()) { + logger.debug("Domain name {} already maps to addresses {} so new addresses {} will be discarded", + domainName, existingAddresses, addresses); + } + } + } + + /** + * Parse /etc/resolv.conf and return options of interest, namely: + * timeout, attempts and ndots. + * @return The options values provided by /etc/resolve.conf. + * @throws IOException If a failure occurs parsing the file. + */ + static UnixResolverOptions parseEtcResolverOptions() throws IOException { + return parseEtcResolverOptions(new File(ETC_RESOLV_CONF_FILE)); + } + + /** + * Parse a file of the format /etc/resolv.conf and return options + * of interest, namely: timeout, attempts and ndots. + * @param etcResolvConf a file of the format /etc/resolv.conf. + * @return The options values provided by /etc/resolve.conf. + * @throws IOException If a failure occurs parsing the file. + */ + static UnixResolverOptions parseEtcResolverOptions(File etcResolvConf) throws IOException { + UnixResolverOptions.Builder optionsBuilder = UnixResolverOptions.newBuilder(); + + FileReader fr = new FileReader(etcResolvConf); + BufferedReader br = null; + try { + br = new BufferedReader(fr); + String line; + while ((line = br.readLine()) != null) { + if (line.startsWith(OPTIONS_ROW_LABEL)) { + parseResOptions(line.substring(OPTIONS_ROW_LABEL.length()), optionsBuilder); + break; + } + } + } finally { + if (br == null) { + fr.close(); + } else { + br.close(); + } + } + + // amend options + if (RES_OPTIONS != null) { + parseResOptions(RES_OPTIONS, optionsBuilder); + } + + return optionsBuilder.build(); + } + + private static void parseResOptions(String line, UnixResolverOptions.Builder builder) { + String[] opts = WHITESPACE_PATTERN.split(line); + for (String opt : opts) { + try { + if (opt.startsWith("ndots:")) { + builder.setNdots(parseResIntOption(opt, "ndots:")); + } else if (opt.startsWith("attempts:")) { + builder.setAttempts(parseResIntOption(opt, "attempts:")); + } else if (opt.startsWith("timeout:")) { + builder.setTimeout(parseResIntOption(opt, "timeout:")); + } + } catch (NumberFormatException ignore) { + // skip bad int values from resolv.conf to keep value already set in UnixResolverOptions + } + } + } + + private static int parseResIntOption(String opt, String fullLabel) { + String optValue = opt.substring(fullLabel.length()); + return Integer.parseInt(optValue); + } + + /** + * Parse a file of the format /etc/resolv.conf and return the + * list of search domains found in it or an empty list if not found. + * @return List of search domains. + * @throws IOException If a failure occurs parsing the file. + */ + static List parseEtcResolverSearchDomains() throws IOException { + return parseEtcResolverSearchDomains(new File(ETC_RESOLV_CONF_FILE)); + } + + /** + * Parse a file of the format /etc/resolv.conf and return the + * list of search domains found in it or an empty list if not found. + * @param etcResolvConf a file of the format /etc/resolv.conf. + * @return List of search domains. + * @throws IOException If a failure occurs parsing the file. + */ + static List parseEtcResolverSearchDomains(File etcResolvConf) throws IOException { + String localDomain = null; + List searchDomains = new ArrayList(); + + FileReader fr = new FileReader(etcResolvConf); + BufferedReader br = null; + try { + br = new BufferedReader(fr); + String line; + while ((line = br.readLine()) != null) { + if (localDomain == null && line.startsWith(DOMAIN_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, DOMAIN_ROW_LABEL.length()); + if (i >= 0) { + localDomain = line.substring(i); + } + } else if (line.startsWith(SEARCH_ROW_LABEL)) { + int i = indexOfNonWhiteSpace(line, SEARCH_ROW_LABEL.length()); + if (i >= 0) { + // May contain more then one entry, either separated by whitespace or tab. + // See https://linux.die.net/man/5/resolver + String[] domains = WHITESPACE_PATTERN.split(line.substring(i)); + Collections.addAll(searchDomains, domains); + } + } + } + } finally { + if (br == null) { + fr.close(); + } else { + br.close(); + } + } + + // return what was on the 'domain' line only if there were no 'search' lines + return localDomain != null && searchDomains.isEmpty() + ? Collections.singletonList(localDomain) + : searchDomains; + } + +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverOptions.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverOptions.java new file mode 100644 index 0000000..195e56f --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/UnixResolverOptions.java @@ -0,0 +1,95 @@ +/* + * Copyright 2020 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: + * + * https://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; + +/** + * Represents options defined in a file of the format etc/resolv.conf. + */ +final class UnixResolverOptions { + + private final int ndots; + private final int timeout; + private final int attempts; + + UnixResolverOptions(int ndots, int timeout, int attempts) { + this.ndots = ndots; + this.timeout = timeout; + this.attempts = attempts; + } + + static UnixResolverOptions.Builder newBuilder() { + return new UnixResolverOptions.Builder(); + } + + /** + * The number of dots which must appear in a name before an initial absolute query is made. + * The default value is {@code 1}. + */ + int ndots() { + return ndots; + } + + /** + * The timeout of each DNS query performed by this resolver (in seconds). + * The default value is {@code 5}. + */ + int timeout() { + return timeout; + } + + /** + * The maximum allowed number of DNS queries to send when resolving a host name. + * The default value is {@code 16}. + */ + int attempts() { + return attempts; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "{ndots=" + ndots + + ", timeout=" + timeout + + ", attempts=" + attempts + + '}'; + } + + static final class Builder { + + private int ndots = 1; + private int timeout = 5; + private int attempts = 16; + + private Builder() { + } + + void setNdots(int ndots) { + this.ndots = ndots; + } + + void setTimeout(int timeout) { + this.timeout = timeout; + } + + void setAttempts(int attempts) { + this.attempts = attempts; + } + + UnixResolverOptions build() { + return new UnixResolverOptions(ndots, timeout, attempts); + } + } +} diff --git a/netty-resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java new file mode 100644 index 0000000..c4c42bf --- /dev/null +++ b/netty-resolver-dns/src/main/java/io/netty/resolver/dns/package-info.java @@ -0,0 +1,21 @@ +/* + * Copyright 2014 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: + * + * https://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. + */ + +/** + * An alternative to Java's built-in domain name lookup mechanism that resolves a domain name asynchronously, + * which supports the queries of an arbitrary DNS record type as well. + */ +package io.netty.resolver.dns; diff --git a/netty-resolver-dns/src/main/java/module-info.java b/netty-resolver-dns/src/main/java/module-info.java new file mode 100644 index 0000000..3db9592 --- /dev/null +++ b/netty-resolver-dns/src/main/java/module-info.java @@ -0,0 +1,11 @@ +module org.xbib.io.netty.resolver.dns { + exports io.netty.resolver.dns; + requires org.xbib.io.netty.buffer; + requires org.xbib.io.netty.channel; + requires org.xbib.io.netty.handler; + requires org.xbib.io.netty.handler.codec; + requires org.xbib.io.netty.handler.codec.dns; + requires org.xbib.io.netty.resolver; + requires org.xbib.io.netty.util; + requires java.naming; +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java new file mode 100644 index 0000000..3bc9534 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultAuthoritativeDnsServerCacheTest.java @@ -0,0 +1,198 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.util.NetUtil; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.Comparator; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DefaultAuthoritativeDnsServerCacheTest { + + @Test + public void testExpire() throws Throwable { + InetSocketAddress resolved1 = new InetSocketAddress( + InetAddress.getByAddress("ns1", new byte[] { 10, 0, 0, 1 }), 53); + InetSocketAddress resolved2 = new InetSocketAddress( + InetAddress.getByAddress("ns2", new byte[] { 10, 0, 0, 2 }), 53); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultAuthoritativeDnsServerCache cache = new DefaultAuthoritativeDnsServerCache(); + cache.cache("netty.io", resolved1, 1, loop); + cache.cache("netty.io", resolved2, 10000, loop); + + Throwable error = loop.schedule(new Callable() { + @Override + public Throwable call() { + try { + assertNull(cache.get("netty.io")); + return null; + } catch (Throwable cause) { + return cause; + } + } + }, 1, TimeUnit.SECONDS).get(); + if (error != null) { + throw error; + } + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testExpireWithDifferentTTLs() { + testExpireWithTTL0(1); + testExpireWithTTL0(1000); + testExpireWithTTL0(1000000); + } + + private static void testExpireWithTTL0(int days) { + EventLoopGroup group = new NioEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultAuthoritativeDnsServerCache cache = new DefaultAuthoritativeDnsServerCache(); + cache.cache("netty.io", new InetSocketAddress(NetUtil.LOCALHOST, 53), days, loop); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testAddMultipleDnsServerForSameHostname() throws Exception { + InetSocketAddress resolved1 = new InetSocketAddress( + InetAddress.getByAddress("ns1", new byte[] { 10, 0, 0, 1 }), 53); + InetSocketAddress resolved2 = new InetSocketAddress( + InetAddress.getByAddress("ns2", new byte[] { 10, 0, 0, 2 }), 53); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultAuthoritativeDnsServerCache cache = new DefaultAuthoritativeDnsServerCache(); + cache.cache("netty.io", resolved1, 100, loop); + cache.cache("netty.io", resolved2, 10000, loop); + + DnsServerAddressStream entries = cache.get("netty.io"); + assertEquals(2, entries.size()); + assertEquals(resolved1, entries.next()); + assertEquals(resolved2, entries.next()); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testUnresolvedReplacedByResolved() throws Exception { + InetSocketAddress unresolved = InetSocketAddress.createUnresolved("ns1", 53); + InetSocketAddress resolved1 = new InetSocketAddress( + InetAddress.getByAddress("ns2", new byte[] { 10, 0, 0, 2 }), 53); + InetSocketAddress resolved2 = new InetSocketAddress( + InetAddress.getByAddress("ns1", new byte[] { 10, 0, 0, 1 }), 53); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultAuthoritativeDnsServerCache cache = new DefaultAuthoritativeDnsServerCache(); + cache.cache("netty.io", unresolved, 100, loop); + cache.cache("netty.io", resolved1, 10000, loop); + + DnsServerAddressStream entries = cache.get("netty.io"); + assertEquals(2, entries.size()); + assertEquals(unresolved, entries.next()); + assertEquals(resolved1, entries.next()); + + cache.cache("netty.io", resolved2, 100, loop); + DnsServerAddressStream entries2 = cache.get("netty.io"); + + assertEquals(2, entries2.size()); + assertEquals(resolved2, entries2.next()); + assertEquals(resolved1, entries2.next()); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testUseNoComparator() throws Exception { + testUseComparator0(true); + } + + @Test + public void testUseComparator() throws Exception { + testUseComparator0(false); + } + + private static void testUseComparator0(boolean noComparator) throws Exception { + InetSocketAddress unresolved = InetSocketAddress.createUnresolved("ns1", 53); + InetSocketAddress resolved = new InetSocketAddress( + InetAddress.getByAddress("ns2", new byte[] { 10, 0, 0, 2 }), 53); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultAuthoritativeDnsServerCache cache; + + if (noComparator) { + cache = new DefaultAuthoritativeDnsServerCache(10000, 10000, null); + } else { + cache = new DefaultAuthoritativeDnsServerCache(10000, 10000, + new Comparator() { + @Override + public int compare(InetSocketAddress o1, InetSocketAddress o2) { + if (o1.equals(o2)) { + return 0; + } + if (o1.isUnresolved()) { + return 1; + } else { + return -1; + } + } + }); + } + cache.cache("netty.io", unresolved, 100, loop); + cache.cache("netty.io", resolved, 10000, loop); + + DnsServerAddressStream entries = cache.get("netty.io"); + assertEquals(2, entries.size()); + + if (noComparator) { + assertEquals(unresolved, entries.next()); + assertEquals(resolved, entries.next()); + } else { + assertEquals(resolved, entries.next()); + assertEquals(unresolved, entries.next()); + } + } finally { + group.shutdownGracefully(); + } + } + +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultDnsCacheTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultDnsCacheTest.java new file mode 100644 index 0000000..71f5328 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultDnsCacheTest.java @@ -0,0 +1,259 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; + +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.util.NetUtil; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.List; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertNull; + +public class DefaultDnsCacheTest { + + @Test + public void testExpire() throws Throwable { + InetAddress addr1 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 1 }); + InetAddress addr2 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(); + cache.cache("netty.io", null, addr1, 1, loop); + cache.cache("netty.io", null, addr2, 10000, loop); + + Throwable error = loop.schedule(new Callable() { + @Override + public Throwable call() { + try { + assertNull(cache.get("netty.io", null)); + return null; + } catch (Throwable cause) { + return cause; + } + } + }, 1, TimeUnit.SECONDS).get(); + if (error != null) { + throw error; + } + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testExpireWithDifferentTTLs() { + testExpireWithTTL0(1); + testExpireWithTTL0(1000); + testExpireWithTTL0(1000000); + } + + private static void testExpireWithTTL0(int days) { + EventLoopGroup group = new NioEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(); + assertNotNull(cache.cache("netty.io", null, NetUtil.LOCALHOST, days, loop)); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testExpireWithToBigMinTTL() { + EventLoopGroup group = new NioEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(Integer.MAX_VALUE, Integer.MAX_VALUE, Integer.MAX_VALUE); + assertNotNull(cache.cache("netty.io", null, NetUtil.LOCALHOST, 100, loop)); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testAddMultipleAddressesForSameHostname() throws Exception { + InetAddress addr1 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 1 }); + InetAddress addr2 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(); + cache.cache("netty.io", null, addr1, 1, loop); + cache.cache("netty.io", null, addr2, 10000, loop); + + List entries = cache.get("netty.io", null); + assertEquals(2, entries.size()); + assertEntry(entries.get(0), addr1); + assertEntry(entries.get(1), addr2); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testAddSameAddressForSameHostname() throws Exception { + InetAddress addr1 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 1 }); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(); + cache.cache("netty.io", null, addr1, 1, loop); + cache.cache("netty.io", null, addr1, 10000, loop); + + List entries = cache.get("netty.io", null); + assertEquals(1, entries.size()); + assertEntry(entries.get(0), addr1); + } finally { + group.shutdownGracefully(); + } + } + + private static void assertEntry(DnsCacheEntry entry, InetAddress address) { + assertEquals(address, entry.address()); + assertNull(entry.cause()); + } + + @Test + public void testCacheFailed() throws Exception { + InetAddress addr1 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 1 }); + InetAddress addr2 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(1, 100, 100); + cache.cache("netty.io", null, addr1, 10000, loop); + cache.cache("netty.io", null, addr2, 10000, loop); + + List entries = cache.get("netty.io", null); + assertEquals(2, entries.size()); + assertEntry(entries.get(0), addr1); + assertEntry(entries.get(1), addr2); + + Exception exception = new Exception(); + cache.cache("netty.io", null, exception, loop); + entries = cache.get("netty.io", null); + DnsCacheEntry entry = entries.get(0); + assertEquals(1, entries.size()); + assertThrowable(exception, entry.cause()); + assertNull(entry.address()); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testDotHandling() throws Exception { + InetAddress addr1 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 1 }); + InetAddress addr2 = InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }); + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(1, 100, 100); + cache.cache("netty.io", null, addr1, 10000, loop); + cache.cache("netty.io.", null, addr2, 10000, loop); + + List entries = cache.get("netty.io", null); + assertEquals(2, entries.size()); + assertEntry(entries.get(0), addr1); + assertEntry(entries.get(1), addr2); + + List entries2 = cache.get("netty.io.", null); + assertEquals(2, entries2.size()); + assertEntry(entries2.get(0), addr1); + assertEntry(entries2.get(1), addr2); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testCacheExceptionIsSafe() throws Exception { + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCache cache = new DefaultDnsCache(1, 100, 100); + + testSuppressed(cache, new UnknownHostException("test"), loop); + testSuppressed(cache, new Throwable("test"), loop); + } finally { + group.shutdownGracefully(); + } + } + + private static void testSuppressed(DnsCache cache, Throwable exception, EventLoop loop) { + String hostname = UUID.randomUUID().toString(); + cache.cache(hostname, null, exception, loop); + List entries = cache.get(hostname, null); + DnsCacheEntry entry = entries.get(0); + assertEquals(1, entries.size()); + assertNotSame(exception, entry.cause()); + + assertThrowable(exception, entry.cause()); + entry.cause().addSuppressed(new Throwable()); + + assertEquals(0, exception.getSuppressed().length); + assertEquals(1, entry.cause().getSuppressed().length); + assertNull(entry.address()); + } + + private static void assertThrowable(Throwable expected, Throwable actual) { + assertInstanceOf(expected.getClass(), actual); + assertEquals(expected.getMessage(), actual.getMessage()); + assertEquals(expected.getStackTrace().length, actual.getStackTrace().length); + ByteArrayOutputStream expectedOutputStream = new ByteArrayOutputStream(); + PrintWriter expectedWriter = new PrintWriter(expectedOutputStream); + ByteArrayOutputStream actualOutputStream = new ByteArrayOutputStream(); + PrintWriter actualWriter = new PrintWriter(actualOutputStream); + + try { + expected.printStackTrace(expectedWriter); + + expected.printStackTrace(actualWriter); + + assertArrayEquals(expectedOutputStream.toByteArray(), actualOutputStream.toByteArray()); + } finally { + expectedWriter.close(); + actualWriter.close(); + } + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultDnsCnameCacheTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultDnsCnameCacheTest.java new file mode 100644 index 0000000..fe495bf --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DefaultDnsCnameCacheTest.java @@ -0,0 +1,135 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; + +import org.junit.jupiter.api.Test; + +import java.util.concurrent.Callable; +import java.util.concurrent.TimeUnit; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DefaultDnsCnameCacheTest { + + @Test + public void testExpire() throws Throwable { + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCnameCache cache = new DefaultDnsCnameCache(); + cache.cache("netty.io", "mapping.netty.io", 1, loop); + + Throwable error = loop.schedule(new Callable() { + @Override + public Throwable call() { + try { + assertNull(cache.get("netty.io")); + return null; + } catch (Throwable cause) { + return cause; + } + } + }, 1, TimeUnit.SECONDS).get(); + if (error != null) { + throw error; + } + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testExpireWithDifferentTTLs() { + testExpireWithTTL0(1); + testExpireWithTTL0(1000); + testExpireWithTTL0(1000000); + } + + private static void testExpireWithTTL0(int days) { + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCnameCache cache = new DefaultDnsCnameCache(); + cache.cache("netty.io", "mapping.netty.io", TimeUnit.DAYS.toSeconds(days), loop); + assertEquals("mapping.netty.io", cache.get("netty.io")); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testMultipleCnamesForSameHostname() throws Exception { + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCnameCache cache = new DefaultDnsCnameCache(); + cache.cache("netty.io", "mapping1.netty.io", 10, loop); + cache.cache("netty.io", "mapping2.netty.io", 10000, loop); + + assertEquals("mapping2.netty.io", cache.get("netty.io")); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testAddSameCnameForSameHostname() throws Exception { + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCnameCache cache = new DefaultDnsCnameCache(); + cache.cache("netty.io", "mapping.netty.io", 10, loop); + cache.cache("netty.io", "mapping.netty.io", 10000, loop); + + assertEquals("mapping.netty.io", cache.get("netty.io")); + } finally { + group.shutdownGracefully(); + } + } + + @Test + public void testClear() throws Exception { + EventLoopGroup group = new DefaultEventLoopGroup(1); + + try { + EventLoop loop = group.next(); + final DefaultDnsCnameCache cache = new DefaultDnsCnameCache(); + cache.cache("x.netty.io", "mapping.netty.io", 100000, loop); + cache.cache("y.netty.io", "mapping.netty.io", 100000, loop); + + assertEquals("mapping.netty.io", cache.get("x.netty.io")); + assertEquals("mapping.netty.io", cache.get("y.netty.io")); + + assertTrue(cache.clear("x.netty.io")); + assertNull(cache.get("x.netty.io")); + assertEquals("mapping.netty.io", cache.get("y.netty.io")); + cache.clear(); + assertNull(cache.get("y.netty.io")); + } finally { + group.shutdownGracefully(); + } + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsAddressResolverGroupTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsAddressResolverGroupTest.java new file mode 100644 index 0000000..758bfcc --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsAddressResolverGroupTest.java @@ -0,0 +1,69 @@ +/* + * Copyright 2020 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: + * + * https://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.DefaultEventLoopGroup; +import io.netty.channel.EventLoop; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.resolver.AddressResolver; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.FutureListener; +import io.netty.util.concurrent.Promise; +import org.junit.jupiter.api.Test; + +import java.net.SocketAddress; +import java.nio.channels.UnsupportedAddressTypeException; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DnsAddressResolverGroupTest { + @Test + public void testUseConfiguredEventLoop() throws InterruptedException { + NioEventLoopGroup group = new NioEventLoopGroup(1); + final EventLoop loop = group.next(); + DefaultEventLoopGroup defaultEventLoopGroup = new DefaultEventLoopGroup(1); + DnsNameResolverBuilder builder = new DnsNameResolverBuilder() + .eventLoop(loop).channelType(NioDatagramChannel.class); + DnsAddressResolverGroup resolverGroup = new DnsAddressResolverGroup(builder); + try { + final Promise promise = loop.newPromise(); + AddressResolver resolver = resolverGroup.getResolver(defaultEventLoopGroup.next()); + resolver.resolve(new SocketAddress() { + private static final long serialVersionUID = 3169703458729818468L; + }).addListener(new FutureListener() { + @Override + public void operationComplete(Future future) { + try { + assertThat(future.cause(), + instanceOf(UnsupportedAddressTypeException.class)); + assertTrue(loop.inEventLoop()); + promise.setSuccess(null); + } catch (Throwable cause) { + promise.setFailure(cause); + } + } + }).await(); + promise.sync(); + } finally { + resolverGroup.close(); + group.shutdownGracefully(); + defaultEventLoopGroup.shutdownGracefully(); + } + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverBuilderTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverBuilderTest.java new file mode 100644 index 0000000..5d7cd32 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverBuilderTest.java @@ -0,0 +1,247 @@ +/* + * Copyright 2023 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: + * + * https://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.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.handler.codec.dns.DnsRecord; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.util.List; + +import static io.netty.resolver.dns.Cache.MAX_SUPPORTED_TTL_SECS; +import static org.assertj.core.api.Assertions.assertThat; + +class DnsNameResolverBuilderTest { + private static final EventLoopGroup GROUP = new NioEventLoopGroup(1); + + private DnsNameResolverBuilder builder; + private DnsNameResolver resolver; + + @BeforeEach + void setUp() { + builder = new DnsNameResolverBuilder(GROUP.next()).channelType(NioDatagramChannel.class); + } + + @AfterEach + void tearDown() { + if (resolver != null) { + resolver.close(); + } + } + + @AfterAll + static void shutdownEventLoopGroup() { + GROUP.shutdownGracefully(); + } + + @Test + void testDefaults() { + resolver = builder.build(); + + checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), MAX_SUPPORTED_TTL_SECS, 0, 0); + + checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), MAX_SUPPORTED_TTL_SECS, 0); + + checkDefaultAuthoritativeDnsServerCache( + (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), MAX_SUPPORTED_TTL_SECS, 0); + } + + @Test + void testCustomDnsCacheDefaultTtl() { + DnsCache testDnsCache = new TestDnsCache(); + resolver = builder.resolveCache(testDnsCache).build(); + + assertThat(resolver.resolveCache()).isSameAs(testDnsCache); + + checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), MAX_SUPPORTED_TTL_SECS, 0); + + checkDefaultAuthoritativeDnsServerCache( + (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), MAX_SUPPORTED_TTL_SECS, 0); + } + + @Test + void testCustomDnsCacheCustomTtl() { + DnsCache testDnsCache = new TestDnsCache(); + resolver = builder.resolveCache(testDnsCache).ttl(1, 2).negativeTtl(3).build(); + + assertThat(resolver.resolveCache()).isSameAs(testDnsCache); + + checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), 2, 1); + + checkDefaultAuthoritativeDnsServerCache( + (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), 2, 1); + } + + @Test + void testCustomDnsCnameCacheDefaultTtl() { + DnsCnameCache testDnsCnameCache = new TestDnsCnameCache(); + resolver = builder.cnameCache(testDnsCnameCache).build(); + + checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), MAX_SUPPORTED_TTL_SECS, 0, 0); + + assertThat(resolver.cnameCache()).isSameAs(testDnsCnameCache); + + checkDefaultAuthoritativeDnsServerCache( + (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), MAX_SUPPORTED_TTL_SECS, 0); + } + + @Test + void testCustomDnsCnameCacheCustomTtl() { + DnsCnameCache testDnsCnameCache = new TestDnsCnameCache(); + resolver = builder.cnameCache(testDnsCnameCache).ttl(1, 2).negativeTtl(3).build(); + + checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), 2, 1, 3); + + assertThat(resolver.cnameCache()).isSameAs(testDnsCnameCache); + + checkDefaultAuthoritativeDnsServerCache( + (DefaultAuthoritativeDnsServerCache) resolver.authoritativeDnsServerCache(), 2, 1); + } + + @Test + void testCustomAuthoritativeDnsServerCacheDefaultTtl() { + AuthoritativeDnsServerCache testAuthoritativeDnsServerCache = new TestAuthoritativeDnsServerCache(); + resolver = builder.authoritativeDnsServerCache(testAuthoritativeDnsServerCache).build(); + + checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), MAX_SUPPORTED_TTL_SECS, 0, 0); + + checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), MAX_SUPPORTED_TTL_SECS, 0); + + assertThat(resolver.authoritativeDnsServerCache()).isSameAs(testAuthoritativeDnsServerCache); + } + + @Test + void testCustomAuthoritativeDnsServerCacheCustomTtl() { + AuthoritativeDnsServerCache testAuthoritativeDnsServerCache = new TestAuthoritativeDnsServerCache(); + resolver = builder.authoritativeDnsServerCache(testAuthoritativeDnsServerCache) + .ttl(1, 2).negativeTtl(3).build(); + + checkDefaultDnsCache((DefaultDnsCache) resolver.resolveCache(), 2, 1, 3); + + checkDefaultDnsCnameCache((DefaultDnsCnameCache) resolver.cnameCache(), 2, 1); + + assertThat(resolver.authoritativeDnsServerCache()).isSameAs(testAuthoritativeDnsServerCache); + } + + @Test + void disableQueryTimeoutWithZero() { + resolver = builder.queryTimeoutMillis(0).build(); + assertThat(resolver.queryTimeoutMillis()).isEqualTo(0); + } + + private static void checkDefaultDnsCache(DefaultDnsCache dnsCache, + int expectedMaxTtl, int expectedMinTtl, int expectedNegativeTtl) { + assertThat(dnsCache.maxTtl()).isEqualTo(expectedMaxTtl); + assertThat(dnsCache.minTtl()).isEqualTo(expectedMinTtl); + assertThat(dnsCache.negativeTtl()).isEqualTo(expectedNegativeTtl); + } + + private static void checkDefaultDnsCnameCache(DefaultDnsCnameCache dnsCnameCache, + int expectedMaxTtl, int expectedMinTtl) { + assertThat(dnsCnameCache.maxTtl()).isEqualTo(expectedMaxTtl); + assertThat(dnsCnameCache.minTtl()).isEqualTo(expectedMinTtl); + } + + private static void checkDefaultAuthoritativeDnsServerCache( + DefaultAuthoritativeDnsServerCache authoritativeDnsServerCache, + int expectedMaxTtl, int expectedMinTtl) { + assertThat(authoritativeDnsServerCache.maxTtl()).isEqualTo(expectedMaxTtl); + assertThat(authoritativeDnsServerCache.minTtl()).isEqualTo(expectedMinTtl); + } + + private static final class TestDnsCache implements DnsCache { + + @Override + public void clear() { + //no-op + } + + @Override + public boolean clear(String hostname) { + return false; + } + + @Override + public List get(String hostname, DnsRecord[] additional) { + return null; + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additional, InetAddress address, + long originalTtl, EventLoop loop) { + return null; + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additional, Throwable cause, EventLoop loop) { + return null; + } + } + + private static final class TestDnsCnameCache implements DnsCnameCache { + + @Override + public String get(String hostname) { + return null; + } + + @Override + public void cache(String hostname, String cname, long originalTtl, EventLoop loop) { + //no-op + } + + @Override + public void clear() { + //no-op + } + + @Override + public boolean clear(String hostname) { + return false; + } + } + + private static final class TestAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { + + @Override + public DnsServerAddressStream get(String hostname) { + return null; + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + //no-op + } + + @Override + public void clear() { + //no-op + } + + @Override + public boolean clear(String hostname) { + return false; + } + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverClientSubnetTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverClientSubnetTest.java new file mode 100644 index 0000000..f707e6f --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverClientSubnetTest.java @@ -0,0 +1,69 @@ +/* + * 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: + * + * https://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.handler.codec.dns.DefaultDnsOptEcsRecord; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.util.internal.SocketUtils; +import io.netty.util.concurrent.Future; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.util.Collections; +import java.util.List; +import java.util.concurrent.TimeUnit; + +public class DnsNameResolverClientSubnetTest { + + // See https://www.gsic.uva.es/~jnisigl/dig-edns-client-subnet.html + // Ignore as this needs to query real DNS servers. + @Disabled + @Test + public void testSubnetQuery() throws Exception { + EventLoopGroup group = new NioEventLoopGroup(1); + DnsNameResolver resolver = newResolver(group).build(); + try { + // Same as: + // # /.bind-9.9.3-edns/bin/dig @ns1.google.com www.google.es +client=157.88.0.0/24 + Future> future = resolver.resolveAll("www.google.es", + Collections.singleton( + // Suggest max payload size of 1024 + // 157.88.0.0 / 24 + new DefaultDnsOptEcsRecord(1024, 24, + SocketUtils.addressByName("157.88.0.0").getAddress()))); + for (InetAddress address: future.syncUninterruptibly().getNow()) { + System.out.println(address); + } + } finally { + resolver.close(); + group.shutdownGracefully(0, 0, TimeUnit.SECONDS); + } + } + + private static DnsNameResolverBuilder newResolver(EventLoopGroup group) { + return new DnsNameResolverBuilder(group.next()) + .channelType(NioDatagramChannel.class) + .nameServerProvider( + new SingletonDnsServerAddressStreamProvider(SocketUtils.socketAddress("8.8.8.8", 53))) + .maxQueriesPerResolve(1) + .optResourceEnabled(false) + .ndots(1); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java new file mode 100644 index 0000000..b873a2f --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsNameResolverTest.java @@ -0,0 +1,4222 @@ +/* + * Copyright 2015 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: + * + * https://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.buffer.ByteBuf; +import io.netty.buffer.ByteBufHolder; +import io.netty.channel.AddressedEnvelope; +import io.netty.channel.ChannelFactory; +import io.netty.channel.ChannelFuture; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInboundHandlerAdapter; +import io.netty.channel.EventLoop; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.ReflectiveChannelFactory; +import io.netty.channel.nio.NioEventLoopGroup; +import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.DatagramPacket; +import io.netty.channel.socket.InternetProtocolFamily; +import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.nio.NioSocketChannel; +import io.netty.handler.codec.dns.DefaultDnsQuestion; +import io.netty.handler.codec.dns.DnsQuestion; +import io.netty.handler.codec.dns.DnsRawRecord; +import io.netty.handler.codec.dns.DnsRecord; +import io.netty.handler.codec.dns.DnsRecordType; +import io.netty.handler.codec.dns.DnsResponse; +import io.netty.handler.codec.dns.DnsResponseCode; +import io.netty.handler.codec.dns.DnsSection; +import io.netty.resolver.HostsFileEntriesProvider; +import io.netty.resolver.HostsFileEntriesResolver; +import io.netty.resolver.ResolvedAddressTypes; +import io.netty.util.CharsetUtil; +import io.netty.util.NetUtil; +import io.netty.util.ReferenceCountUtil; +import io.netty.util.concurrent.Future; +import io.netty.util.concurrent.Promise; +import io.netty.util.internal.PlatformDependent; +import io.netty.util.internal.SocketUtils; +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.DnsException; +import org.apache.directory.server.dns.io.encoder.DnsMessageEncoder; +import org.apache.directory.server.dns.messages.DnsMessage; +import org.apache.directory.server.dns.messages.DnsMessageModifier; +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.messages.ResponseCode; +import org.apache.directory.server.dns.store.DnsAttribute; +import org.apache.directory.server.dns.store.RecordStore; +import org.apache.mina.core.buffer.IoBuffer; +import org.hamcrest.Matchers; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.net.BindException; +import java.net.DatagramSocket; +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.net.Socket; +import java.net.UnknownHostException; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Queue; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedQueue; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; +import org.junit.jupiter.api.Timeout; +import org.junit.jupiter.api.function.Executable; + +import static io.netty.handler.codec.dns.DnsRecordType.A; +import static io.netty.handler.codec.dns.DnsRecordType.AAAA; +import static io.netty.handler.codec.dns.DnsRecordType.CNAME; +import static io.netty.handler.codec.dns.DnsRecordType.NAPTR; +import static io.netty.handler.codec.dns.DnsRecordType.SRV; +import static io.netty.resolver.dns.DnsNameResolver.DEFAULT_RESOLVE_ADDRESS_TYPES; +import static io.netty.resolver.dns.DnsServerAddresses.sequential; +import static io.netty.resolver.dns.TestDnsServer.newARecord; +import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static org.assertj.core.api.Assumptions.assumeThat; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.greaterThan; +import static org.hamcrest.Matchers.greaterThanOrEqualTo; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.is; + +import static org.hamcrest.Matchers.startsWith; +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +public class DnsNameResolverTest { + + private static final InternalLogger logger = InternalLoggerFactory.getInstance(DnsNameResolver.class); + private static final long DEFAULT_TEST_TIMEOUT_MS = 30000; + + // Using the top-100 web sites ranked in Alexa.com (Oct 2014) + // Please use the following series of shell commands to get this up-to-date: + // $ curl -O https://s3.amazonaws.com/alexa-static/top-1m.csv.zip + // $ unzip -o top-1m.csv.zip top-1m.csv + // $ head -100 top-1m.csv | cut -d, -f2 | cut -d/ -f1 | while read L; do echo '"'"$L"'",'; done > topsites.txt + private static final Set DOMAINS = Collections.unmodifiableSet(new HashSet(asList( + "google.com", + "youtube.com", + "facebook.com", + "baidu.com", + "wikipedia.org", + "yahoo.com", + "reddit.com", + "google.co.in", + "qq.com", + "amazon.com", + "taobao.com", + "tmall.com", + "twitter.com", + "vk.com", + "live.com", + "sohu.com", + "instagram.com", + "google.co.jp", + "sina.com.cn", + "jd.com", + "weibo.com", + "360.cn", + "google.de", + "google.co.uk", + "google.com.br", + "list.tmall.com", + "google.ru", + "google.fr", + "yandex.ru", + "netflix.com", + "google.it", + "google.com.hk", + "linkedin.com", + "pornhub.com", + "t.co", + "google.es", + "twitch.tv", + "alipay.com", + "xvideos.com", + "ebay.com", + "yahoo.co.jp", + "google.ca", + "google.com.mx", + "bing.com", + "ok.ru", + "imgur.com", + "microsoft.com", + "mail.ru", + "imdb.com", + "aliexpress.com", + "hao123.com", + "msn.com", + "tumblr.com", + "csdn.net", + "wikia.com", + "wordpress.com", + "office.com", + "google.com.tr", + "livejasmin.com", + "amazon.co.jp", + "deloton.com", + "apple.com", + "google.com.au", + "paypal.com", + "google.com.tw", + "bongacams.com", + "popads.net", + "whatsapp.com", + "blogspot.com", + "detail.tmall.com", + "google.pl", + "microsoftonline.com", + "xhamster.com", + "google.co.id", + "github.com", + "stackoverflow.com", + "pinterest.com", + "amazon.de", + "diply.com", + "amazon.co.uk", + "so.com", + "google.com.ar", + "coccoc.com", + "soso.com", + "espn.com", + "adobe.com", + "google.com.ua", + "tianya.cn", + "xnxx.com", + "googleusercontent.com", + "savefrom.net", + "google.com.pk", + "amazon.in", + "nicovideo.jp", + "google.co.th", + "dropbox.com", + "thepiratebay.org", + "google.com.sa", + "google.com.eg", + "pixnet.net", + "localhost"))); + + private static final Map DOMAINS_PUNYCODE = new HashMap(); + + static { + DOMAINS_PUNYCODE.put("büchner.de", "xn--bchner-3ya.de"); + DOMAINS_PUNYCODE.put("müller.de", "xn--mller-kva.de"); + } + + private static final Set DOMAINS_ALL; + + static { + Set all = new HashSet(DOMAINS.size() + DOMAINS_PUNYCODE.size()); + all.addAll(DOMAINS); + all.addAll(DOMAINS_PUNYCODE.values()); + DOMAINS_ALL = Collections.unmodifiableSet(all); + } + + /** + * The list of the domain names to exclude from {@link #testResolveAorAAAA()}. + */ + private static final Set EXCLUSIONS_RESOLVE_A = new HashSet(); + + static { + Collections.addAll( + EXCLUSIONS_RESOLVE_A, + "akamaihd.net", + "googleusercontent.com", + StringUtil.EMPTY_STRING); + } + + /** + * The list of the domain names to exclude from {@link #testResolveAAAA()}. + * Unfortunately, there are only handful of domain names with IPv6 addresses. + */ + private static final Set EXCLUSIONS_RESOLVE_AAAA = new HashSet(); + + static { + EXCLUSIONS_RESOLVE_AAAA.addAll(EXCLUSIONS_RESOLVE_A); + EXCLUSIONS_RESOLVE_AAAA.addAll(DOMAINS); + EXCLUSIONS_RESOLVE_AAAA.removeAll(asList( + "google.com", + "facebook.com", + "youtube.com", + "wikipedia.org", + "google.co.in", + "blogspot.com", + "vk.com", + "google.de", + "google.co.jp", + "google.co.uk", + "google.fr", + "google.com.br", + "google.ru", + "google.it", + "google.es", + "google.com.mx", + "xhamster.com", + "google.ca", + "google.co.id", + "blogger.com", + "flipkart.com", + "google.com.tr", + "google.com.au", + "google.pl", + "google.com.hk", + "blogspot.in" + )); + } + + /** + * The list of the domain names to exclude from {@link #testQueryMx()}. + */ + private static final Set EXCLUSIONS_QUERY_MX = new HashSet(); + + static { + Collections.addAll( + EXCLUSIONS_QUERY_MX, + "hao123.com", + "blogspot.com", + "t.co", + "espn.go.com", + "people.com.cn", + "googleusercontent.com", + "blogspot.in", + "localhost", + StringUtil.EMPTY_STRING); + } + + private static final String WINDOWS_HOST_NAME; + private static final boolean WINDOWS_HOSTS_FILE_LOCALHOST_ENTRY_EXISTS; + private static final boolean WINDOWS_HOSTS_FILE_HOST_NAME_ENTRY_EXISTS; + + static { + String windowsHostName; + boolean windowsHostsFileLocalhostEntryExists; + boolean windowsHostsFileHostNameEntryExists; + try { + if (PlatformDependent.isWindows()) { + windowsHostName = InetAddress.getLocalHost().getHostName(); + + HostsFileEntriesProvider provider = + HostsFileEntriesProvider.parser() + .parseSilently(Charset.defaultCharset(), CharsetUtil.UTF_16, CharsetUtil.UTF_8); + windowsHostsFileLocalhostEntryExists = + provider.ipv4Entries().get("localhost") != null || + provider.ipv6Entries().get("localhost") != null; + windowsHostsFileHostNameEntryExists = + provider.ipv4Entries().get(windowsHostName) != null || + provider.ipv6Entries().get(windowsHostName) != null; + } else { + windowsHostName = null; + windowsHostsFileLocalhostEntryExists = false; + windowsHostsFileHostNameEntryExists = false; + } + } catch (Exception ignore) { + windowsHostName = null; + windowsHostsFileLocalhostEntryExists = false; + windowsHostsFileHostNameEntryExists = false; + } + WINDOWS_HOST_NAME = windowsHostName; + WINDOWS_HOSTS_FILE_LOCALHOST_ENTRY_EXISTS = windowsHostsFileLocalhostEntryExists; + WINDOWS_HOSTS_FILE_HOST_NAME_ENTRY_EXISTS = windowsHostsFileHostNameEntryExists; + } + + private static final TestDnsServer dnsServer = new TestDnsServer(DOMAINS_ALL); + private static final EventLoopGroup group = new NioEventLoopGroup(1); + + private static DnsNameResolverBuilder newResolver(boolean decodeToUnicode) { + return newResolver(decodeToUnicode, null); + } + + private static DnsNameResolverBuilder newResolver(boolean decodeToUnicode, + DnsServerAddressStreamProvider dnsServerAddressStreamProvider) { + return newResolver(decodeToUnicode, dnsServerAddressStreamProvider, dnsServer); + } + + private static DnsNameResolverBuilder newResolver(boolean decodeToUnicode, + DnsServerAddressStreamProvider dnsServerAddressStreamProvider, + TestDnsServer dnsServer) { + DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next()) + .dnsQueryLifecycleObserverFactory(new TestRecursiveCacheDnsQueryLifecycleObserverFactory()) + .channelType(NioDatagramChannel.class) + .maxQueriesPerResolve(1) + .decodeIdn(decodeToUnicode) + .optResourceEnabled(false) + .ndots(1); + + if (dnsServerAddressStreamProvider == null) { + builder.nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress())); + } else { + builder.nameServerProvider(new MultiDnsServerAddressStreamProvider(dnsServerAddressStreamProvider, + new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()))); + } + + return builder; + } + + private static DnsNameResolverBuilder newResolver() { + return newResolver(true); + } + + private static DnsNameResolverBuilder newResolver(ResolvedAddressTypes resolvedAddressTypes) { + return newResolver() + .resolvedAddressTypes(resolvedAddressTypes); + } + + private static DnsNameResolverBuilder newNonCachedResolver(ResolvedAddressTypes resolvedAddressTypes) { + return newResolver() + .resolveCache(NoopDnsCache.INSTANCE) + .resolvedAddressTypes(resolvedAddressTypes); + } + + @BeforeAll + public static void init() throws Exception { + dnsServer.start(); + } + + @AfterAll + public static void destroy() { + dnsServer.stop(); + group.shutdownGracefully(); + } + + @Test + public void testResolveAorAAAA() throws Exception { + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_PREFERRED).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_A, AAAA); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveAAAAorA() throws Exception { + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV6_PREFERRED).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_A, A); + } finally { + resolver.close(); + } + } + + /** + * This test will start an second DNS test server which returns fixed results that can be easily verified as + * originating from the second DNS test server. The resolver will put {@link DnsServerAddressStreamProvider} under + * test to ensure that some hostnames can be directed toward both the primary and secondary DNS test servers + * simultaneously. + */ + @Test + public void testNameServerCache() throws IOException, InterruptedException { + final String overriddenIP = "12.34.12.34"; + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + switch (question.getRecordType()) { + case A: + Map attr = new HashMap(); + attr.put(DnsAttribute.IP_ADDRESS.toLowerCase(Locale.US), overriddenIP); + return Collections.singleton( + new TestDnsServer.TestResourceRecord( + question.getDomainName(), question.getRecordType(), attr)); + default: + return null; + } + } + }); + dnsServer2.start(); + try { + final Set overriddenHostnames = new HashSet(); + for (String name : DOMAINS) { + if (EXCLUSIONS_RESOLVE_A.contains(name)) { + continue; + } + if (PlatformDependent.threadLocalRandom().nextBoolean()) { + overriddenHostnames.add(name); + } + } + DnsNameResolver resolver = newResolver(false, new DnsServerAddressStreamProvider() { + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + return overriddenHostnames.contains(hostname) ? sequential(dnsServer2.localAddress()).stream() : + null; + } + }).build(); + try { + final Map resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A, AAAA); + for (Entry resolvedEntry : resultA.entrySet()) { + if (resolvedEntry.getValue().isLoopbackAddress()) { + continue; + } + if (overriddenHostnames.contains(resolvedEntry.getKey())) { + assertEquals(overriddenIP, resolvedEntry.getValue().getHostAddress(), + "failed to resolve " + resolvedEntry.getKey()); + } else { + assertNotEquals(overriddenIP, resolvedEntry.getValue().getHostAddress(), + "failed to resolve " + resolvedEntry.getKey()); + } + } + } finally { + resolver.close(); + } + } finally { + dnsServer2.stop(); + } + } + + @Test + public void testResolveA() throws Exception { + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY) + // Cache for eternity + .ttl(Integer.MAX_VALUE, Integer.MAX_VALUE) + .build(); + try { + final Map resultA = testResolve0(resolver, EXCLUSIONS_RESOLVE_A, null); + + // Now, try to resolve again to see if it's cached. + // This test works because the DNS servers usually randomizes the order of the records in a response. + // If cached, the resolved addresses must be always same, because we reuse the same response. + + final Map resultB = testResolve0(resolver, EXCLUSIONS_RESOLVE_A, null); + + // Ensure the result from the cache is identical from the uncached one. + assertThat(resultB.size(), is(resultA.size())); + for (Entry e : resultA.entrySet()) { + InetAddress expected = e.getValue(); + InetAddress actual = resultB.get(e.getKey()); + assertThat("Cache for " + e.getKey() + ": " + resolver.resolveAll(e.getKey()).getNow(), + actual, is(expected)); + } + } finally { + resolver.close(); + } + } + + @Test + public void testResolveAAAA() throws Exception { + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV6_ONLY).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_AAAA, null); + } finally { + resolver.close(); + } + } + + @Test + public void testNonCachedResolve() throws Exception { + DnsNameResolver resolver = newNonCachedResolver(ResolvedAddressTypes.IPV4_ONLY).build(); + try { + testResolve0(resolver, EXCLUSIONS_RESOLVE_A, null); + } finally { + resolver.close(); + } + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void testNonCachedResolveEmptyHostName() throws Exception { + testNonCachedResolveEmptyHostName(""); + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void testNonCachedResolveNullHostName() throws Exception { + testNonCachedResolveEmptyHostName(null); + } + + private static void testNonCachedResolveEmptyHostName(String inetHost) throws Exception { + DnsNameResolver resolver = newNonCachedResolver(ResolvedAddressTypes.IPV4_ONLY).build(); + try { + InetAddress addr = resolver.resolve(inetHost).syncUninterruptibly().getNow(); + assertEquals(SocketUtils.addressByName(inetHost), addr); + } finally { + resolver.close(); + } + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void testNonCachedResolveAllEmptyHostName() throws Exception { + testNonCachedResolveAllEmptyHostName(""); + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void testNonCachedResolveAllNullHostName() throws Exception { + testNonCachedResolveAllEmptyHostName(null); + } + + private static void testNonCachedResolveAllEmptyHostName(String inetHost) throws UnknownHostException { + DnsNameResolver resolver = newNonCachedResolver(ResolvedAddressTypes.IPV4_ONLY).build(); + try { + List addrs = resolver.resolveAll(inetHost).syncUninterruptibly().getNow(); + assertEquals(asList( + SocketUtils.allAddressesByName(inetHost)), addrs); + } finally { + resolver.close(); + } + } + + private static Map testResolve0(DnsNameResolver resolver, Set excludedDomains, + DnsRecordType cancelledType) + throws InterruptedException { + + assertThat(resolver.isRecursionDesired(), is(true)); + + final Map results = new HashMap(); + final Map> futures = + new LinkedHashMap>(); + + for (String name : DOMAINS) { + if (excludedDomains.contains(name)) { + continue; + } + + resolve(resolver, futures, name); + } + + for (Entry> e : futures.entrySet()) { + String unresolved = e.getKey(); + InetAddress resolved = e.getValue().sync().getNow(); + + logger.info("{}: {}", unresolved, resolved.getHostAddress()); + + assertThat(resolved.getHostName(), is(unresolved)); + + boolean typeMatches = false; + for (InternetProtocolFamily f : resolver.resolvedInternetProtocolFamiliesUnsafe()) { + Class resolvedType = resolved.getClass(); + if (f.addressType().isAssignableFrom(resolvedType)) { + typeMatches = true; + } + } + + assertThat(typeMatches, is(true)); + + results.put(resolved.getHostName(), resolved); + } + + assertQueryObserver(resolver, cancelledType); + + return results; + } + + @Test + public void testQueryMx() { + DnsNameResolver resolver = newResolver().build(); + try { + assertThat(resolver.isRecursionDesired(), is(true)); + + Map>> futures = + new LinkedHashMap>>(); + for (String name : DOMAINS) { + if (EXCLUSIONS_QUERY_MX.contains(name)) { + continue; + } + + queryMx(resolver, futures, name); + } + + for (Entry>> e : futures.entrySet()) { + String hostname = e.getKey(); + Future> f = e.getValue().awaitUninterruptibly(); + + DnsResponse response = f.getNow().content(); + assertThat(response.code(), is(DnsResponseCode.NOERROR)); + + final int answerCount = response.count(DnsSection.ANSWER); + final List mxList = new ArrayList(answerCount); + for (int i = 0; i < answerCount; i++) { + final DnsRecord r = response.recordAt(DnsSection.ANSWER, i); + if (r.type() == DnsRecordType.MX) { + mxList.add(r); + } + } + + assertThat(mxList.size(), is(greaterThan(0))); + StringBuilder buf = new StringBuilder(); + for (DnsRecord r : mxList) { + ByteBuf recordContent = ((ByteBufHolder) r).content(); + + buf.append(StringUtil.NEWLINE); + buf.append('\t'); + buf.append(r.name()); + buf.append(' '); + buf.append(r.type().name()); + buf.append(' '); + buf.append(recordContent.readUnsignedShort()); + buf.append(' '); + buf.append(DnsResolveContext.decodeDomainName(recordContent)); + } + + logger.info("{} has the following MX records:{}", hostname, buf); + response.release(); + + // We only track query lifecycle if it is managed by the DnsNameResolverContext, and not direct calls + // to query. + assertNoQueriesMade(resolver); + } + } finally { + resolver.close(); + } + } + + @Test + public void testNegativeTtl() throws Exception { + final DnsNameResolver resolver = newResolver().negativeTtl(10).build(); + try { + resolveNonExistentDomain(resolver); + + final int size = 10000; + final List exceptions = new ArrayList(); + + // If negative cache works, this thread should be done really quickly. + final Thread negativeLookupThread = new Thread() { + @Override + public void run() { + for (int i = 0; i < size; i++) { + exceptions.add(resolveNonExistentDomain(resolver)); + if (isInterrupted()) { + break; + } + } + } + }; + + negativeLookupThread.start(); + negativeLookupThread.join(DEFAULT_TEST_TIMEOUT_MS); + + if (negativeLookupThread.isAlive()) { + negativeLookupThread.interrupt(); + fail("Cached negative lookups did not finish quickly."); + } + + assertThat(exceptions, hasSize(size)); + } finally { + resolver.close(); + } + } + + private static UnknownHostException resolveNonExistentDomain(DnsNameResolver resolver) { + try { + resolver.resolve("non-existent.netty.io").sync(); + fail(); + return null; + } catch (Exception e) { + assertThat(e, is(instanceOf(UnknownHostException.class))); + + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + (TestRecursiveCacheDnsQueryLifecycleObserverFactory) resolver.dnsQueryLifecycleObserverFactory(); + TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll(); + if (observer != null) { + Object o = observer.events.poll(); + if (o instanceof QueryCancelledEvent) { + assertTrue(observer.question.type() == CNAME || observer.question.type() == AAAA, + "unexpected type: " + observer.question); + } else if (o instanceof QueryWrittenEvent) { + QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll(); + } else if (!(o instanceof QueryFailedEvent)) { + fail("unexpected event type: " + o); + } + assertTrue(observer.events.isEmpty()); + } + return (UnknownHostException) e; + } + } + + @Test + public void testResolveIp() { + DnsNameResolver resolver = newResolver().build(); + try { + InetAddress address = resolver.resolve("10.0.0.1").syncUninterruptibly().getNow(); + + assertEquals("10.0.0.1", address.getHostAddress()); + + // This address is already resolved, and so we shouldn't have to query for anything. + assertNoQueriesMade(resolver); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveEmptyIpv4() { + testResolve0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, StringUtil.EMPTY_STRING); + } + + @Test + public void testResolveEmptyIpv6() { + testResolve0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, StringUtil.EMPTY_STRING); + } + + @Test + public void testResolveLocalhostIpv4() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_LOCALHOST_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isNotEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolve0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, "localhost"); + } + + @Test + public void testResolveLocalhostIpv6() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_LOCALHOST_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolve0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, "localhost"); + } + + @Test + public void testResolveHostNameIpv4() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_HOST_NAME_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isNotEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolve0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, WINDOWS_HOST_NAME); + } + + @Test + public void testResolveHostNameIpv6() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_HOST_NAME_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolve0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, WINDOWS_HOST_NAME); + } + + @Test + public void testResolveNullIpv4() { + testResolve0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, null); + } + + @Test + public void testResolveNullIpv6() { + testResolve0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, null); + } + + private static void testResolve0(ResolvedAddressTypes addressTypes, InetAddress expectedAddr, String name) { + DnsNameResolver resolver = newResolver(addressTypes).build(); + try { + InetAddress address = resolver.resolve(name).syncUninterruptibly().getNow(); + assertEquals(expectedAddr, address); + + // We are resolving the local address, so we shouldn't make any queries. + assertNoQueriesMade(resolver); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveAllEmptyIpv4() { + testResolveAll0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, StringUtil.EMPTY_STRING); + } + + @Test + public void testResolveAllEmptyIpv6() { + testResolveAll0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, StringUtil.EMPTY_STRING); + } + + @Test + public void testResolveAllLocalhostIpv4() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_LOCALHOST_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isNotEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolveAll0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, "localhost"); + } + + @Test + public void testResolveAllLocalhostIpv6() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_LOCALHOST_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolveAll0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, "localhost"); + } + + @Test + public void testResolveAllHostNameIpv4() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_HOST_NAME_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isNotEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolveAll0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, WINDOWS_HOST_NAME); + } + + @Test + public void testResolveAllHostNameIpv6() { + assumeThat(PlatformDependent.isWindows()).isTrue(); + assumeThat(WINDOWS_HOSTS_FILE_HOST_NAME_ENTRY_EXISTS).isFalse(); + assumeThat(DEFAULT_RESOLVE_ADDRESS_TYPES).isEqualTo(ResolvedAddressTypes.IPV6_PREFERRED); + testResolveAll0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, WINDOWS_HOST_NAME); + } + + @Test + public void testCNAMEResolveAllIpv4() throws IOException { + testCNAMERecursiveResolve(true); + } + + @Test + public void testCNAMEResolveAllIpv6() throws IOException { + testCNAMERecursiveResolve(false); + } + + private static void testCNAMERecursiveResolve(boolean ipv4Preferred) throws IOException { + final String firstName = "firstname.com"; + final String secondName = "secondname.com"; + final String lastName = "lastname.com"; + final String ipv4Addr = "1.2.3.4"; + final String ipv6Addr = "::1"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + rm.setDnsType(RecordType.CNAME); + + if (question.getDomainName().equals(firstName)) { + rm.put(DnsAttribute.DOMAIN_NAME, secondName); + } else if (question.getDomainName().equals(secondName)) { + rm.put(DnsAttribute.DOMAIN_NAME, lastName); + } else if (question.getDomainName().equals(lastName)) { + rm.setDnsType(question.getRecordType()); + switch (question.getRecordType()) { + case A: + rm.put(DnsAttribute.IP_ADDRESS, ipv4Addr); + break; + case AAAA: + rm.put(DnsAttribute.IP_ADDRESS, ipv6Addr); + break; + default: + return null; + } + } else { + return null; + } + return Collections.singleton(rm.getEntry()); + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + if (ipv4Preferred) { + builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED); + } else { + builder.resolvedAddressTypes(ResolvedAddressTypes.IPV6_PREFERRED); + } + resolver = builder.build(); + InetAddress resolvedAddress = resolver.resolve(firstName).syncUninterruptibly().getNow(); + if (ipv4Preferred) { + assertEquals(ipv4Addr, resolvedAddress.getHostAddress()); + } else { + assertEquals(ipv6Addr, NetUtil.toAddressString(resolvedAddress)); + } + assertEquals(firstName, resolvedAddress.getHostName()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testCNAMERecursiveResolveMultipleNameServersIPv4() throws IOException { + testCNAMERecursiveResolveMultipleNameServers(true); + } + + @Test + public void testCNAMERecursiveResolveMultipleNameServersIPv6() throws IOException { + testCNAMERecursiveResolveMultipleNameServers(false); + } + + private static void testCNAMERecursiveResolveMultipleNameServers(boolean ipv4Preferred) throws IOException { + final String firstName = "firstname.nettyfoo.com"; + final String lastName = "lastname.nettybar.com"; + final String ipv4Addr = "1.2.3.4"; + final String ipv6Addr = "::1"; + final AtomicBoolean hitServer2 = new AtomicBoolean(); + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) throws DnsException { + hitServer2.set(true); + if (question.getDomainName().equals(firstName)) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + rm.setDnsType(RecordType.CNAME); + rm.put(DnsAttribute.DOMAIN_NAME, lastName); + return Collections.singleton(rm.getEntry()); + } else { + throw new DnsException(ResponseCode.REFUSED); + } + } + }); + final TestDnsServer dnsServer3 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) throws DnsException { + if (question.getDomainName().equals(lastName)) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + rm.setDnsType(question.getRecordType()); + switch (question.getRecordType()) { + case A: + rm.put(DnsAttribute.IP_ADDRESS, ipv4Addr); + break; + case AAAA: + rm.put(DnsAttribute.IP_ADDRESS, ipv6Addr); + break; + default: + return null; + } + + return Collections.singleton(rm.getEntry()); + } else { + throw new DnsException(ResponseCode.REFUSED); + } + } + }); + dnsServer2.start(); + dnsServer3.start(); + DnsNameResolver resolver = null; + try { + AuthoritativeDnsServerCache nsCache = new DefaultAuthoritativeDnsServerCache(); + // What we want to test is the following: + // 1. Do a DNS query. + // 2. CNAME is returned, we want to lookup that CNAME on multiple DNS servers + // 3. The first DNS server should fail + // 4. The second DNS server should succeed + // This verifies that we do in fact follow multiple DNS servers in the CNAME resolution. + // The DnsCache is used for the name server cache, but doesn't provide a InetSocketAddress (only InetAddress + // so no port), so we only specify the name server in the cache, and then specify both name servers in the + // fallback name server provider. + nsCache.cache("nettyfoo.com.", dnsServer2.localAddress(), 10000, group.next()); + resolver = new DnsNameResolver( + group.next(), new ReflectiveChannelFactory(NioDatagramChannel.class), + NoopDnsCache.INSTANCE, nsCache, NoopDnsQueryLifecycleObserverFactory.INSTANCE, 3000, + ipv4Preferred ? ResolvedAddressTypes.IPV4_ONLY : ResolvedAddressTypes.IPV6_ONLY, true, + 10, true, 4096, false, HostsFileEntriesResolver.DEFAULT, + new SequentialDnsServerAddressStreamProvider(dnsServer2.localAddress(), dnsServer3.localAddress()), + DnsNameResolver.DEFAULT_SEARCH_DOMAINS, 0, true) { + @Override + InetSocketAddress newRedirectServerAddress(InetAddress server) { + int port = hitServer2.get() ? dnsServer3.localAddress().getPort() : + dnsServer2.localAddress().getPort(); + return new InetSocketAddress(server, port); + } + }; + InetAddress resolvedAddress = resolver.resolve(firstName).syncUninterruptibly().getNow(); + if (ipv4Preferred) { + assertEquals(ipv4Addr, resolvedAddress.getHostAddress()); + } else { + assertEquals(ipv6Addr, NetUtil.toAddressString(resolvedAddress)); + } + assertEquals(firstName, resolvedAddress.getHostName()); + } finally { + dnsServer2.stop(); + dnsServer3.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testResolveAllNullIpv4() { + testResolveAll0(ResolvedAddressTypes.IPV4_ONLY, NetUtil.LOCALHOST4, null); + } + + @Test + public void testResolveAllNullIpv6() { + testResolveAll0(ResolvedAddressTypes.IPV6_ONLY, NetUtil.LOCALHOST6, null); + } + + private static void testResolveAll0(ResolvedAddressTypes addressTypes, InetAddress expectedAddr, String name) { + DnsNameResolver resolver = newResolver(addressTypes).build(); + try { + List addresses = resolver.resolveAll(name).syncUninterruptibly().getNow(); + assertEquals(1, addresses.size()); + assertEquals(expectedAddr, addresses.get(0)); + + // We are resolving the local address, so we shouldn't make any queries. + assertNoQueriesMade(resolver); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveAllMx() { + final DnsNameResolver resolver = newResolver().build(); + try { + assertThat(resolver.isRecursionDesired(), is(true)); + + final Map>> futures = new LinkedHashMap>>(); + for (String name : DOMAINS) { + if (EXCLUSIONS_QUERY_MX.contains(name)) { + continue; + } + + futures.put(name, resolver.resolveAll(new DefaultDnsQuestion(name, DnsRecordType.MX))); + } + + for (Entry>> e : futures.entrySet()) { + String hostname = e.getKey(); + Future> f = e.getValue().awaitUninterruptibly(); + + final List mxList = f.getNow(); + assertThat(mxList.size(), is(greaterThan(0))); + StringBuilder buf = new StringBuilder(); + for (DnsRecord r : mxList) { + ByteBuf recordContent = ((ByteBufHolder) r).content(); + + buf.append(StringUtil.NEWLINE); + buf.append('\t'); + buf.append(r.name()); + buf.append(' '); + buf.append(r.type().name()); + buf.append(' '); + buf.append(recordContent.readUnsignedShort()); + buf.append(' '); + buf.append(DnsResolveContext.decodeDomainName(recordContent)); + + ReferenceCountUtil.release(r); + } + + logger.info("{} has the following MX records:{}", hostname, buf); + } + } finally { + resolver.close(); + } + } + + @Test + public void testResolveAllHostsFile() { + final DnsNameResolver resolver = new DnsNameResolverBuilder(group.next()) + .channelType(NioDatagramChannel.class) + .hostsFileEntriesResolver(new HostsFileEntriesResolver() { + @Override + public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { + if ("foo.com.".equals(inetHost)) { + try { + return InetAddress.getByAddress("foo.com", new byte[] { 1, 2, 3, 4 }); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + return null; + } + }).build(); + + final List records = resolver.resolveAll(new DefaultDnsQuestion("foo.com.", A)) + .syncUninterruptibly().getNow(); + assertThat(records, Matchers.hasSize(1)); + assertThat(records.get(0), Matchers.instanceOf(DnsRawRecord.class)); + + final DnsRawRecord record = (DnsRawRecord) records.get(0); + final ByteBuf content = record.content(); + assertThat(record.name(), is("foo.com.")); + assertThat(record.dnsClass(), is(DnsRecord.CLASS_IN)); + assertThat(record.type(), is(A)); + assertThat(content.readableBytes(), is(4)); + assertThat(content.readInt(), is(0x01020304)); + record.release(); + } + + @Test + public void testResolveDecodeUnicode() { + testResolveUnicode(true); + } + + @Test + public void testResolveNotDecodeUnicode() { + testResolveUnicode(false); + } + + private static void testResolveUnicode(boolean decode) { + DnsNameResolver resolver = newResolver(decode).build(); + try { + for (Entry entries : DOMAINS_PUNYCODE.entrySet()) { + InetAddress address = resolver.resolve(entries.getKey()).syncUninterruptibly().getNow(); + assertEquals(decode ? entries.getKey() : entries.getValue(), address.getHostName()); + } + + assertQueryObserver(resolver, AAAA); + } finally { + resolver.close(); + } + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void secondDnsServerShouldBeUsedBeforeCNAMEFirstServerNotStarted() throws IOException { + secondDnsServerShouldBeUsedBeforeCNAME(false); + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void secondDnsServerShouldBeUsedBeforeCNAMEFirstServerFailResolve() throws IOException { + secondDnsServerShouldBeUsedBeforeCNAME(true); + } + + private static void secondDnsServerShouldBeUsedBeforeCNAME(boolean startDnsServer1) throws IOException { + final String knownHostName = "netty.io"; + final TestDnsServer dnsServer1 = new TestDnsServer(Collections.singleton("notnetty.com")); + final TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(knownHostName)); + DnsNameResolver resolver = null; + try { + final InetSocketAddress dnsServer1Address; + if (startDnsServer1) { + dnsServer1.start(); + dnsServer1Address = dnsServer1.localAddress(); + } else { + // Some address where a DNS server will not be running. + dnsServer1Address = new InetSocketAddress("127.0.0.1", 22); + } + dnsServer2.start(); + + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + new TestRecursiveCacheDnsQueryLifecycleObserverFactory(); + + DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next()) + .dnsQueryLifecycleObserverFactory(lifecycleObserverFactory) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .channelType(NioDatagramChannel.class) + .queryTimeoutMillis(1000) // We expect timeouts if startDnsServer1 is false + .optResourceEnabled(false) + .ndots(1); + + builder.nameServerProvider(new SequentialDnsServerAddressStreamProvider(dnsServer1Address, + dnsServer2.localAddress())); + resolver = builder.build(); + assertNotNull(resolver.resolve(knownHostName).syncUninterruptibly().getNow()); + + TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertEquals(1, lifecycleObserverFactory.observers.size()); + assertEquals(2, observer.events.size()); + QueryWrittenEvent writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer1Address, writtenEvent.dnsServerAddress); + QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll(); + + observer = lifecycleObserverFactory.observers.poll(); + assertEquals(2, observer.events.size()); + writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer2.localAddress(), writtenEvent.dnsServerAddress); + QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll(); + } finally { + if (resolver != null) { + resolver.close(); + } + dnsServer1.stop(); + dnsServer2.stop(); + } + } + + @Test + @Timeout(value = DEFAULT_TEST_TIMEOUT_MS, unit = TimeUnit.MILLISECONDS) + public void aAndAAAAQueryShouldTryFirstDnsServerBeforeSecond() throws IOException { + final String knownHostName = "netty.io"; + final TestDnsServer dnsServer1 = new TestDnsServer(Collections.singleton("notnetty.com")); + final TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(knownHostName)); + DnsNameResolver resolver = null; + try { + dnsServer1.start(); + dnsServer2.start(); + + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + new TestRecursiveCacheDnsQueryLifecycleObserverFactory(); + + DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next()) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .dnsQueryLifecycleObserverFactory(lifecycleObserverFactory) + .channelType(NioDatagramChannel.class) + .optResourceEnabled(false) + .ndots(1); + + builder.nameServerProvider(new SequentialDnsServerAddressStreamProvider(dnsServer1.localAddress(), + dnsServer2.localAddress())); + resolver = builder.build(); + assertNotNull(resolver.resolve(knownHostName).syncUninterruptibly().getNow()); + + TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertEquals(1, lifecycleObserverFactory.observers.size()); + assertEquals(2, observer.events.size()); + QueryWrittenEvent writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer1.localAddress(), writtenEvent.dnsServerAddress); + QueryFailedEvent failedEvent = (QueryFailedEvent) observer.events.poll(); + + observer = lifecycleObserverFactory.observers.poll(); + assertEquals(2, observer.events.size()); + writtenEvent = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer2.localAddress(), writtenEvent.dnsServerAddress); + QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll(); + } finally { + if (resolver != null) { + resolver.close(); + } + dnsServer1.stop(); + dnsServer2.stop(); + } + } + + @Test + public void testRecursiveResolveNoCache() throws Exception { + testRecursiveResolveCache(false); + } + + @Test + public void testRecursiveResolveCache() throws Exception { + testRecursiveResolveCache(true); + } + + @Test + public void testIpv4PreferredWhenIpv6First() throws Exception { + testResolvesPreferredWhenNonPreferredFirst0(ResolvedAddressTypes.IPV4_PREFERRED); + } + + @Test + public void testIpv6PreferredWhenIpv4First() throws Exception { + testResolvesPreferredWhenNonPreferredFirst0(ResolvedAddressTypes.IPV6_PREFERRED); + } + + private static void testResolvesPreferredWhenNonPreferredFirst0(ResolvedAddressTypes types) throws Exception { + final String name = "netty.com"; + // This store is non-compliant, returning records of the wrong type for a query. + // It works since we don't verify the type of the result when resolving to deal with + // non-compliant servers in the wild. + List> records = new ArrayList>(); + final String ipv6Address = "0:0:0:0:0:0:1:1"; + final String ipv4Address = "1.1.1.1"; + if (types == ResolvedAddressTypes.IPV4_PREFERRED) { + records.add(Collections.singleton(TestDnsServer.newAddressRecord(name, RecordType.AAAA, ipv6Address))); + records.add(Collections.singleton(TestDnsServer.newAddressRecord(name, RecordType.A, ipv4Address))); + } else { + records.add(Collections.singleton(TestDnsServer.newAddressRecord(name, RecordType.A, ipv4Address))); + records.add(Collections.singleton(TestDnsServer.newAddressRecord(name, RecordType.AAAA, ipv6Address))); + } + final Iterator> recordsIterator = records.iterator(); + RecordStore arbitrarilyOrderedStore = new RecordStore() { + @Override + public Set getRecords(QuestionRecord questionRecord) { + return recordsIterator.next(); + } + }; + TestDnsServer nonCompliantDnsServer = new TestDnsServer(arbitrarilyOrderedStore); + nonCompliantDnsServer.start(); + try { + DnsNameResolver resolver = newResolver(types) + .maxQueriesPerResolve(2) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider( + nonCompliantDnsServer.localAddress())) + .build(); + InetAddress resolved = resolver.resolve("netty.com").syncUninterruptibly().getNow(); + if (types == ResolvedAddressTypes.IPV4_PREFERRED) { + assertEquals(ipv4Address, resolved.getHostAddress()); + } else { + assertEquals(ipv6Address, resolved.getHostAddress()); + } + InetAddress ipv4InetAddress = InetAddress.getByAddress("netty.com", + InetAddress.getByName(ipv4Address).getAddress()); + InetAddress ipv6InetAddress = InetAddress.getByAddress("netty.com", + InetAddress.getByName(ipv6Address).getAddress()); + + List resolvedAll = resolver.resolveAll("netty.com").syncUninterruptibly().getNow(); + List expected = types == ResolvedAddressTypes.IPV4_PREFERRED ? + asList(ipv4InetAddress, ipv6InetAddress) : asList(ipv6InetAddress, ipv4InetAddress); + assertEquals(expected, resolvedAll); + } finally { + nonCompliantDnsServer.stop(); + } + } + + private static void testRecursiveResolveCache(boolean cache) + throws Exception { + final String hostname = "some.record.netty.io"; + final String hostname2 = "some2.record.netty.io"; + + final TestDnsServer dnsServerAuthority = new TestDnsServer(new HashSet( + asList(hostname, hostname2))); + dnsServerAuthority.start(); + + TestDnsServer dnsServer = new RedirectingTestDnsServer(hostname, + dnsServerAuthority.localAddress().getAddress().getHostAddress()); + dnsServer.start(); + + TestAuthoritativeDnsServerCache nsCache = new TestAuthoritativeDnsServerCache( + cache ? new DefaultAuthoritativeDnsServerCache() : NoopAuthoritativeDnsServerCache.INSTANCE); + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + new TestRecursiveCacheDnsQueryLifecycleObserverFactory(); + EventLoopGroup group = new NioEventLoopGroup(1); + final DnsNameResolver resolver = new DnsNameResolver( + group.next(), new ReflectiveChannelFactory(NioDatagramChannel.class), + NoopDnsCache.INSTANCE, nsCache, lifecycleObserverFactory, 3000, ResolvedAddressTypes.IPV4_ONLY, true, + 10, true, 4096, false, HostsFileEntriesResolver.DEFAULT, + new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress()), + DnsNameResolver.DEFAULT_SEARCH_DOMAINS, 0, true) { + @Override + InetSocketAddress newRedirectServerAddress(InetAddress server) { + if (server.equals(dnsServerAuthority.localAddress().getAddress())) { + return new InetSocketAddress(server, dnsServerAuthority.localAddress().getPort()); + } + return super.newRedirectServerAddress(server); + } + }; + + // Java7 will strip of the "." so we need to adjust the expected dnsname. Both are valid in terms of the RFC + // so its ok. + String expectedDnsName = PlatformDependent.javaVersion() == 7 ? + "dns4.some.record.netty.io" : "dns4.some.record.netty.io."; + + try { + resolver.resolveAll(hostname).syncUninterruptibly(); + + TestDnsQueryLifecycleObserver observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertTrue(lifecycleObserverFactory.observers.isEmpty()); + assertEquals(4, observer.events.size()); + QueryWrittenEvent writtenEvent1 = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServer.localAddress(), writtenEvent1.dnsServerAddress); + QueryRedirectedEvent redirectedEvent = (QueryRedirectedEvent) observer.events.poll(); + + assertEquals(expectedDnsName, redirectedEvent.nameServers.get(0).getHostName()); + assertEquals(dnsServerAuthority.localAddress(), redirectedEvent.nameServers.get(0)); + QueryWrittenEvent writtenEvent2 = (QueryWrittenEvent) observer.events.poll(); + assertEquals(dnsServerAuthority.localAddress(), writtenEvent2.dnsServerAddress); + QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll(); + + if (cache) { + assertNull(nsCache.cache.get("io.")); + assertNull(nsCache.cache.get("netty.io.")); + DnsServerAddressStream entries = nsCache.cache.get("record.netty.io."); + + // First address should be resolved (as we received a matching additional record), second is unresolved. + assertEquals(2, entries.size()); + assertFalse(entries.next().isUnresolved()); + assertTrue(entries.next().isUnresolved()); + + assertNull(nsCache.cache.get(hostname)); + + // Test again via cache. + resolver.resolveAll(hostname).syncUninterruptibly(); + + observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertTrue(lifecycleObserverFactory.observers.isEmpty()); + assertEquals(2, observer.events.size()); + writtenEvent1 = (QueryWrittenEvent) observer.events.poll(); + assertEquals(expectedDnsName, writtenEvent1.dnsServerAddress.getHostName()); + assertEquals(dnsServerAuthority.localAddress(), writtenEvent1.dnsServerAddress); + succeededEvent = (QuerySucceededEvent) observer.events.poll(); + + resolver.resolveAll(hostname2).syncUninterruptibly(); + + observer = lifecycleObserverFactory.observers.poll(); + assertNotNull(observer); + assertTrue(lifecycleObserverFactory.observers.isEmpty()); + assertEquals(2, observer.events.size()); + writtenEvent1 = (QueryWrittenEvent) observer.events.poll(); + assertEquals(expectedDnsName, writtenEvent1.dnsServerAddress.getHostName()); + assertEquals(dnsServerAuthority.localAddress(), writtenEvent1.dnsServerAddress); + succeededEvent = (QuerySucceededEvent) observer.events.poll(); + + // Check that it only queried the cache for record.netty.io. + assertNull(nsCache.cacheHits.get("io.")); + assertNull(nsCache.cacheHits.get("netty.io.")); + assertNotNull(nsCache.cacheHits.get("record.netty.io.")); + assertNull(nsCache.cacheHits.get("some.record.netty.io.")); + } + } finally { + resolver.close(); + group.shutdownGracefully(0, 0, TimeUnit.SECONDS); + dnsServer.stop(); + dnsServerAuthority.stop(); + } + } + + @Test + public void testFollowNsRedirectsNoopCaches() throws Exception { + testFollowNsRedirects(NoopDnsCache.INSTANCE, NoopAuthoritativeDnsServerCache.INSTANCE, false); + } + + @Test + public void testFollowNsRedirectsNoopDnsCache() throws Exception { + testFollowNsRedirects(NoopDnsCache.INSTANCE, new DefaultAuthoritativeDnsServerCache(), false); + } + + @Test + public void testFollowNsRedirectsNoopAuthoritativeDnsServerCache() throws Exception { + testFollowNsRedirects(new DefaultDnsCache(), NoopAuthoritativeDnsServerCache.INSTANCE, false); + } + + @Test + public void testFollowNsRedirectsDefaultCaches() throws Exception { + testFollowNsRedirects(new DefaultDnsCache(), new DefaultAuthoritativeDnsServerCache(), false); + } + + @Test + public void testFollowNsRedirectAndTrySecondNsOnTimeout() throws Exception { + testFollowNsRedirects(NoopDnsCache.INSTANCE, NoopAuthoritativeDnsServerCache.INSTANCE, true); + } + + @Test + public void testFollowNsRedirectAndTrySecondNsOnTimeoutDefaultCaches() throws Exception { + testFollowNsRedirects(new DefaultDnsCache(), new DefaultAuthoritativeDnsServerCache(), true); + } + + private void testFollowNsRedirects(DnsCache cache, AuthoritativeDnsServerCache authoritativeDnsServerCache, + final boolean invalidNsFirst) throws Exception { + final String domain = "netty.io"; + final String ns1Name = "ns1." + domain; + final String ns2Name = "ns2." + domain; + final InetAddress expected = InetAddress.getByAddress("some.record." + domain, new byte[] { 10, 10, 10, 10 }); + + // This is used to simulate a query timeout... + final DatagramSocket socket = new DatagramSocket(new InetSocketAddress(0)); + + final TestDnsServer dnsServerAuthority = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + if (question.getDomainName().equals(expected.getHostName())) { + return Collections.singleton(newARecord( + expected.getHostName(), expected.getHostAddress())); + } + return Collections.emptySet(); + } + }); + dnsServerAuthority.start(); + + TestDnsServer redirectServer = new TestDnsServer(new HashSet( + asList(expected.getHostName(), ns1Name, ns2Name))) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getDomainName().equals(expected.getHostName())) { + message.getAdditionalRecords().clear(); + message.getAnswerRecords().clear(); + if (invalidNsFirst) { + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns2Name)); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns1Name)); + } else { + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns1Name)); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns2Name)); + } + return message; + } + } + return message; + } + }; + redirectServer.start(); + EventLoopGroup group = new NioEventLoopGroup(1); + final DnsNameResolver resolver = new DnsNameResolver( + group.next(), new ReflectiveChannelFactory(NioDatagramChannel.class), + cache, authoritativeDnsServerCache, NoopDnsQueryLifecycleObserverFactory.INSTANCE, 2000, + ResolvedAddressTypes.IPV4_ONLY, true, 10, true, 4096, + false, HostsFileEntriesResolver.DEFAULT, + new SingletonDnsServerAddressStreamProvider(redirectServer.localAddress()), + DnsNameResolver.DEFAULT_SEARCH_DOMAINS, 0, true) { + + @Override + InetSocketAddress newRedirectServerAddress(InetAddress server) { + try { + if (server.getHostName().startsWith(ns1Name)) { + return new InetSocketAddress(InetAddress.getByAddress(ns1Name, + dnsServerAuthority.localAddress().getAddress().getAddress()), + dnsServerAuthority.localAddress().getPort()); + } + if (server.getHostName().startsWith(ns2Name)) { + return new InetSocketAddress(InetAddress.getByAddress(ns2Name, + NetUtil.LOCALHOST.getAddress()), socket.getLocalPort()); + } + } catch (UnknownHostException e) { + throw new IllegalStateException(e); + } + return super.newRedirectServerAddress(server); + } + }; + + try { + List resolved = resolver.resolveAll(expected.getHostName()).syncUninterruptibly().getNow(); + assertEquals(1, resolved.size()); + assertEquals(expected, resolved.get(0)); + + List resolved2 = resolver.resolveAll(expected.getHostName()).syncUninterruptibly().getNow(); + assertEquals(1, resolved2.size()); + assertEquals(expected, resolved2.get(0)); + + if (authoritativeDnsServerCache != NoopAuthoritativeDnsServerCache.INSTANCE) { + DnsServerAddressStream cached = authoritativeDnsServerCache.get(domain + '.'); + assertEquals(2, cached.size()); + InetSocketAddress ns1Address = InetSocketAddress.createUnresolved( + ns1Name + '.', DefaultDnsServerAddressStreamProvider.DNS_PORT); + InetSocketAddress ns2Address = InetSocketAddress.createUnresolved( + ns2Name + '.', DefaultDnsServerAddressStreamProvider.DNS_PORT); + + if (invalidNsFirst) { + assertEquals(ns2Address, cached.next()); + assertEquals(ns1Address, cached.next()); + } else { + assertEquals(ns1Address, cached.next()); + assertEquals(ns2Address, cached.next()); + } + } + if (cache != NoopDnsCache.INSTANCE) { + List ns1Cached = cache.get(ns1Name + '.', null); + assertEquals(1, ns1Cached.size()); + DnsCacheEntry nsEntry = ns1Cached.get(0); + assertNotNull(nsEntry.address()); + assertNull(nsEntry.cause()); + + List ns2Cached = cache.get(ns2Name + '.', null); + if (invalidNsFirst) { + assertEquals(1, ns2Cached.size()); + DnsCacheEntry ns2Entry = ns2Cached.get(0); + assertNotNull(ns2Entry.address()); + assertNull(ns2Entry.cause()); + } else { + // We should not even have tried to resolve the DNS name so this should be null. + assertNull(ns2Cached); + } + + List expectedCached = cache.get(expected.getHostName(), null); + assertEquals(1, expectedCached.size()); + DnsCacheEntry expectedEntry = expectedCached.get(0); + assertEquals(expected, expectedEntry.address()); + assertNull(expectedEntry.cause()); + } + } finally { + resolver.close(); + group.shutdownGracefully(0, 0, TimeUnit.SECONDS); + redirectServer.stop(); + dnsServerAuthority.stop(); + socket.close(); + } + } + + @Test + public void testMultipleAdditionalRecordsForSameNSRecord() throws Exception { + testMultipleAdditionalRecordsForSameNSRecord(false); + } + + @Test + public void testMultipleAdditionalRecordsForSameNSRecordReordered() throws Exception { + testMultipleAdditionalRecordsForSameNSRecord(true); + } + + private static void testMultipleAdditionalRecordsForSameNSRecord(final boolean reversed) throws Exception { + final String domain = "netty.io"; + final String hostname = "test.netty.io"; + final String ns1Name = "ns1." + domain; + final InetSocketAddress ns1Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 1 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns2Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 2 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns3Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 3 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns4Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 4 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + + TestDnsServer redirectServer = new TestDnsServer(new HashSet(asList(hostname, ns1Name))) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getDomainName().equals(hostname)) { + message.getAdditionalRecords().clear(); + message.getAnswerRecords().clear(); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns1Name)); + message.getAdditionalRecords().add(newARecord(ns1Address)); + message.getAdditionalRecords().add(newARecord(ns2Address)); + message.getAdditionalRecords().add(newARecord(ns3Address)); + message.getAdditionalRecords().add(newARecord(ns4Address)); + return message; + } + } + return message; + } + + private ResourceRecord newARecord(InetSocketAddress address) { + return newARecord(address.getHostName(), address.getAddress().getHostAddress()); + } + }; + redirectServer.start(); + EventLoopGroup group = new NioEventLoopGroup(1); + + final List cached = new CopyOnWriteArrayList(); + final AuthoritativeDnsServerCache authoritativeDnsServerCache = new AuthoritativeDnsServerCache() { + @Override + public DnsServerAddressStream get(String hostname) { + return null; + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + cached.add(address); + } + + @Override + public void clear() { + // NOOP + } + + @Override + public boolean clear(String hostname) { + return false; + } + }; + + final AtomicReference redirectedRef = new AtomicReference(); + final DnsNameResolver resolver = new DnsNameResolver( + group.next(), new ReflectiveChannelFactory(NioDatagramChannel.class), + NoopDnsCache.INSTANCE, authoritativeDnsServerCache, + NoopDnsQueryLifecycleObserverFactory.INSTANCE, 2000, ResolvedAddressTypes.IPV4_ONLY, + true, 10, true, 4096, + false, HostsFileEntriesResolver.DEFAULT, + new SingletonDnsServerAddressStreamProvider(redirectServer.localAddress()), + DnsNameResolver.DEFAULT_SEARCH_DOMAINS, 0, true) { + + @Override + protected DnsServerAddressStream newRedirectDnsServerStream( + String hostname, List nameservers) { + if (reversed) { + Collections.reverse(nameservers); + } + DnsServerAddressStream stream = new SequentialDnsServerAddressStream(nameservers, 0); + redirectedRef.set(stream); + return stream; + } + }; + + try { + Throwable cause = resolver.resolveAll(hostname).await().cause(); + assertTrue(cause instanceof UnknownHostException); + DnsServerAddressStream redirected = redirectedRef.get(); + assertNotNull(redirected); + assertEquals(4, redirected.size()); + assertEquals(4, cached.size()); + + if (reversed) { + assertEquals(ns4Address, redirected.next()); + assertEquals(ns3Address, redirected.next()); + assertEquals(ns2Address, redirected.next()); + assertEquals(ns1Address, redirected.next()); + } else { + assertEquals(ns1Address, redirected.next()); + assertEquals(ns2Address, redirected.next()); + assertEquals(ns3Address, redirected.next()); + assertEquals(ns4Address, redirected.next()); + } + + // We should always have the same order in the cache. + assertEquals(ns1Address, cached.get(0)); + assertEquals(ns2Address, cached.get(1)); + assertEquals(ns3Address, cached.get(2)); + assertEquals(ns4Address, cached.get(3)); + } finally { + resolver.close(); + group.shutdownGracefully(0, 0, TimeUnit.SECONDS); + redirectServer.stop(); + } + } + + @Test + public void testNSRecordsFromCache() throws Exception { + final String domain = "netty.io"; + final String hostname = "test.netty.io"; + final String ns0Name = "ns0." + domain + '.'; + final String ns1Name = "ns1." + domain + '.'; + final String ns2Name = "ns2." + domain + '.'; + + final InetSocketAddress ns0Address = new InetSocketAddress( + InetAddress.getByAddress(ns0Name, new byte[] { 10, 1, 0, 1 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns1Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 1 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns2Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 2 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns3Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 3 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns4Address = new InetSocketAddress( + InetAddress.getByAddress(ns1Name, new byte[] { 10, 0, 0, 4 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + final InetSocketAddress ns5Address = new InetSocketAddress( + InetAddress.getByAddress(ns2Name, new byte[] { 10, 0, 0, 5 }), + DefaultDnsServerAddressStreamProvider.DNS_PORT); + TestDnsServer redirectServer = new TestDnsServer(new HashSet(asList(hostname, ns1Name))) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getDomainName().equals(hostname)) { + message.getAdditionalRecords().clear(); + message.getAnswerRecords().clear(); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns0Name)); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns1Name)); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns2Name)); + + message.getAdditionalRecords().add(newARecord(ns0Address)); + message.getAdditionalRecords().add(newARecord(ns5Address)); + + return message; + } + } + return message; + } + + private ResourceRecord newARecord(InetSocketAddress address) { + return newARecord(address.getHostName(), address.getAddress().getHostAddress()); + } + }; + redirectServer.start(); + EventLoopGroup group = new NioEventLoopGroup(1); + + final List cached = new CopyOnWriteArrayList(); + final AuthoritativeDnsServerCache authoritativeDnsServerCache = new AuthoritativeDnsServerCache() { + @Override + public DnsServerAddressStream get(String hostname) { + return null; + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + cached.add(address); + } + + @Override + public void clear() { + // NOOP + } + + @Override + public boolean clear(String hostname) { + return false; + } + }; + + EventLoop loop = group.next(); + DefaultDnsCache cache = new DefaultDnsCache(); + cache.cache(ns1Name, null, ns1Address.getAddress(), 10000, loop); + cache.cache(ns1Name, null, ns2Address.getAddress(), 10000, loop); + cache.cache(ns1Name, null, ns3Address.getAddress(), 10000, loop); + cache.cache(ns1Name, null, ns4Address.getAddress(), 10000, loop); + + final AtomicReference redirectedRef = new AtomicReference(); + final DnsNameResolver resolver = new DnsNameResolver( + loop, new ReflectiveChannelFactory(NioDatagramChannel.class), + cache, authoritativeDnsServerCache, + NoopDnsQueryLifecycleObserverFactory.INSTANCE, 2000, ResolvedAddressTypes.IPV4_ONLY, + true, 10, true, 4096, + false, HostsFileEntriesResolver.DEFAULT, + new SingletonDnsServerAddressStreamProvider(redirectServer.localAddress()), + DnsNameResolver.DEFAULT_SEARCH_DOMAINS, 0, true) { + + @Override + protected DnsServerAddressStream newRedirectDnsServerStream( + String hostname, List nameservers) { + DnsServerAddressStream stream = new SequentialDnsServerAddressStream(nameservers, 0); + redirectedRef.set(stream); + return stream; + } + }; + + try { + Throwable cause = resolver.resolveAll(hostname).await().cause(); + assertTrue(cause instanceof UnknownHostException); + DnsServerAddressStream redirected = redirectedRef.get(); + assertNotNull(redirected); + assertEquals(6, redirected.size()); + assertEquals(3, cached.size()); + + // The redirected addresses should have been retrieven from the DnsCache if not resolved, so these are + // fully resolved. + assertEquals(ns0Address, redirected.next()); + assertEquals(ns1Address, redirected.next()); + assertEquals(ns2Address, redirected.next()); + assertEquals(ns3Address, redirected.next()); + assertEquals(ns4Address, redirected.next()); + assertEquals(ns5Address, redirected.next()); + + // As this address was supplied as ADDITIONAL we should put it resolved into the cache. + assertEquals(ns0Address, cached.get(0)); + assertEquals(ns5Address, cached.get(1)); + + // We should have put the unresolved address in the AuthoritativeDnsServerCache (but only 1 time) + assertEquals(unresolved(ns1Address), cached.get(2)); + } finally { + resolver.close(); + group.shutdownGracefully(0, 0, TimeUnit.SECONDS); + redirectServer.stop(); + } + } + + @Test + public void testNsLoopFailsResolveWithAuthoritativeDnsServerCache() throws Exception { + testNsLoopFailsResolve(new DefaultAuthoritativeDnsServerCache()); + } + + @Test + public void testNsLoopFailsResolveWithoutAuthoritativeDnsServerCache() throws Exception { + testNsLoopFailsResolve(NoopAuthoritativeDnsServerCache.INSTANCE); + } + + @Test + public void testRRNameContainsDifferentSearchDomainNoDomains() { + assertThrows(UnknownHostException.class, new Executable() { + @Override + public void execute() throws Throwable { + testRRNameContainsDifferentSearchDomain(Collections.emptyList(), "netty"); + } + }); + } + + @Test + public void testRRNameContainsDifferentSearchDomainEmptyExtraDomain() throws Exception { + testRRNameContainsDifferentSearchDomain(asList("io", ""), "netty"); + } + + @Test + public void testRRNameContainsDifferentSearchDomainSingleExtraDomain() throws Exception { + testRRNameContainsDifferentSearchDomain(asList("io", "foo.dom"), "netty"); + } + + @Test + public void testRRNameContainsDifferentSearchDomainMultiExtraDomains() throws Exception { + testRRNameContainsDifferentSearchDomain(asList("com", "foo.dom", "bar.dom"), "google"); + } + + private static void testRRNameContainsDifferentSearchDomain(final List searchDomains, String unresolved) + throws Exception { + final String ipAddrPrefix = "1.2.3."; + TestDnsServer searchDomainServer = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord questionRecord) { + Set records = new HashSet(searchDomains.size()); + final String qName = questionRecord.getDomainName(); + for (String searchDomain : searchDomains) { + if (qName.endsWith(searchDomain)) { + continue; + } + final ResourceRecord rr = newARecord(qName + '.' + searchDomain, + ipAddrPrefix + ThreadLocalRandom.current().nextInt(1, 10)); + logger.info("Adding A record: " + rr); + records.add(rr); + } + return records; + } + }); + searchDomainServer.start(); + + final DnsNameResolver resolver = newResolver(false, null, searchDomainServer) + .searchDomains(searchDomains) + .build(); + + try { + final List addresses = resolver.resolveAll(unresolved).sync().get(); + assertThat(addresses, Matchers.hasSize(greaterThan(0))); + for (InetAddress address : addresses) { + assertThat(address.getHostName(), startsWith(unresolved)); + assertThat(address.getHostAddress(), startsWith(ipAddrPrefix)); + } + } finally { + resolver.close(); + searchDomainServer.stop(); + } + } + + private void testNsLoopFailsResolve(AuthoritativeDnsServerCache authoritativeDnsServerCache) throws Exception { + final String domain = "netty.io"; + final String ns1Name = "ns1." + domain; + final String ns2Name = "ns2." + domain; + + TestDnsServer testDnsServer = new TestDnsServer(new HashSet( + Collections.singletonList(domain))) { + + @Override + protected DnsMessage filterMessage(DnsMessage message) { + // Just always return NS records only without any additional records (glue records). + // Because of this the resolver will never be able to resolve and so fail eventually at some + // point. + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getDomainName().equals(domain)) { + message.getAdditionalRecords().clear(); + message.getAnswerRecords().clear(); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns1Name)); + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(domain, ns2Name)); + } + } + return message; + } + }; + testDnsServer.start(); + DnsNameResolverBuilder builder = newResolver(); + + final DnsNameResolver resolver = builder.resolveCache(NoopDnsCache.INSTANCE) + .authoritativeDnsServerCache(authoritativeDnsServerCache) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(testDnsServer.localAddress())).build(); + + try { + assertThat(resolver.resolve(domain).await().cause(), + Matchers.instanceOf(UnknownHostException.class)); + assertThat(resolver.resolveAll(domain).await().cause(), + Matchers.instanceOf(UnknownHostException.class)); + } finally { + resolver.close(); + testDnsServer.stop(); + } + } + + private static InetSocketAddress unresolved(InetSocketAddress address) { + return InetSocketAddress.createUnresolved(address.getHostString(), address.getPort()); + } + + private static void resolve(DnsNameResolver resolver, Map> futures, String hostname) { + futures.put(hostname, resolver.resolve(hostname)); + } + + private static void queryMx( + DnsNameResolver resolver, + Map>> futures, + String hostname) { + futures.put(hostname, resolver.query(new DefaultDnsQuestion(hostname, DnsRecordType.MX))); + } + + private static void assertNoQueriesMade(DnsNameResolver resolver) { + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + (TestRecursiveCacheDnsQueryLifecycleObserverFactory) resolver.dnsQueryLifecycleObserverFactory(); + assertTrue(lifecycleObserverFactory.observers.isEmpty()); + } + + private static void assertQueryObserver(DnsNameResolver resolver, DnsRecordType cancelledType) { + TestRecursiveCacheDnsQueryLifecycleObserverFactory lifecycleObserverFactory = + (TestRecursiveCacheDnsQueryLifecycleObserverFactory) resolver.dnsQueryLifecycleObserverFactory(); + TestDnsQueryLifecycleObserver observer; + while ((observer = lifecycleObserverFactory.observers.poll()) != null) { + Object o = observer.events.poll(); + if (o instanceof QueryCancelledEvent) { + assertEquals(cancelledType, observer.question.type()); + } else if (o instanceof QueryWrittenEvent) { + QuerySucceededEvent succeededEvent = (QuerySucceededEvent) observer.events.poll(); + } else { + fail("unexpected event type: " + o); + } + assertTrue(observer.events.isEmpty()); + } + } + + private static final class TestRecursiveCacheDnsQueryLifecycleObserverFactory + implements DnsQueryLifecycleObserverFactory { + final Queue observers = + new ConcurrentLinkedQueue(); + @Override + public DnsQueryLifecycleObserver newDnsQueryLifecycleObserver(DnsQuestion question) { + TestDnsQueryLifecycleObserver observer = new TestDnsQueryLifecycleObserver(question); + observers.add(observer); + return observer; + } + } + + private static final class QueryWrittenEvent { + final InetSocketAddress dnsServerAddress; + + QueryWrittenEvent(InetSocketAddress dnsServerAddress) { + this.dnsServerAddress = dnsServerAddress; + } + } + + private static final class QueryCancelledEvent { + final int queriesRemaining; + + QueryCancelledEvent(int queriesRemaining) { + this.queriesRemaining = queriesRemaining; + } + } + + private static final class QueryRedirectedEvent { + final List nameServers; + + QueryRedirectedEvent(List nameServers) { + this.nameServers = nameServers; + } + } + + private static final class QueryCnamedEvent { + final DnsQuestion question; + + QueryCnamedEvent(DnsQuestion question) { + this.question = question; + } + } + + private static final class QueryNoAnswerEvent { + final DnsResponseCode code; + + QueryNoAnswerEvent(DnsResponseCode code) { + this.code = code; + } + } + + private static final class QueryFailedEvent { + final Throwable cause; + + QueryFailedEvent(Throwable cause) { + this.cause = cause; + } + } + + private static final class QuerySucceededEvent { + } + + private static final class TestDnsQueryLifecycleObserver implements DnsQueryLifecycleObserver { + final Queue events = new ArrayDeque(); + final DnsQuestion question; + + TestDnsQueryLifecycleObserver(DnsQuestion question) { + this.question = question; + } + + @Override + public void queryWritten(InetSocketAddress dnsServerAddress, ChannelFuture future) { + events.add(new QueryWrittenEvent(dnsServerAddress)); + } + + @Override + public void queryCancelled(int queriesRemaining) { + events.add(new QueryCancelledEvent(queriesRemaining)); + } + + @Override + public DnsQueryLifecycleObserver queryRedirected(List nameServers) { + events.add(new QueryRedirectedEvent(nameServers)); + return this; + } + + @Override + public DnsQueryLifecycleObserver queryCNAMEd(DnsQuestion cnameQuestion) { + events.add(new QueryCnamedEvent(cnameQuestion)); + return this; + } + + @Override + public DnsQueryLifecycleObserver queryNoAnswer(DnsResponseCode code) { + events.add(new QueryNoAnswerEvent(code)); + return this; + } + + @Override + public void queryFailed(Throwable cause) { + events.add(new QueryFailedEvent(cause)); + } + + @Override + public void querySucceed() { + events.add(new QuerySucceededEvent()); + } + } + + private static final class TestAuthoritativeDnsServerCache implements AuthoritativeDnsServerCache { + final AuthoritativeDnsServerCache cache; + final Map cacheHits = new HashMap(); + + TestAuthoritativeDnsServerCache(AuthoritativeDnsServerCache cache) { + this.cache = cache; + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + public boolean clear(String hostname) { + return cache.clear(hostname); + } + + @Override + public DnsServerAddressStream get(String hostname) { + DnsServerAddressStream cached = cache.get(hostname); + if (cached != null) { + cacheHits.put(hostname, cached.duplicate()); + } + return cached; + } + + @Override + public void cache(String hostname, InetSocketAddress address, long originalTtl, EventLoop loop) { + cache.cache(hostname, address, originalTtl, loop); + } + } + + private static final class TestDnsCache implements DnsCache { + final DnsCache cache; + final Map> cacheHits = + new HashMap>(); + + TestDnsCache(DnsCache cache) { + this.cache = cache; + } + + @Override + public void clear() { + cache.clear(); + } + + @Override + public boolean clear(String hostname) { + return cache.clear(hostname); + } + + @Override + public List get(String hostname, DnsRecord[] additionals) { + List cached = cache.get(hostname, additionals); + cacheHits.put(hostname, cached); + return cached; + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, InetAddress address, + long originalTtl, EventLoop loop) { + return cache.cache(hostname, additionals, address, originalTtl, loop); + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) { + return cache.cache(hostname, additionals, cause, loop); + } + } + + private static class RedirectingTestDnsServer extends TestDnsServer { + + private final String dnsAddress; + private final String domain; + + RedirectingTestDnsServer(String domain, String dnsAddress) { + super(Collections.singleton(domain)); + this.domain = domain; + this.dnsAddress = dnsAddress; + } + + @Override + protected DnsMessage filterMessage(DnsMessage message) { + // Clear the answers as we want to add our own stuff to test dns redirects. + message.getAnswerRecords().clear(); + message.getAuthorityRecords().clear(); + message.getAdditionalRecords().clear(); + + String name = domain; + for (int i = 0 ;; i++) { + int idx = name.indexOf('.'); + if (idx <= 0) { + break; + } + name = name.substring(idx + 1); // skip the '.' as well. + String dnsName = "dns" + idx + '.' + domain; + message.getAuthorityRecords().add(newNsRecord(name, dnsName)); + message.getAdditionalRecords().add(newARecord(dnsName, i == 0 ? dnsAddress : "1.2.3." + idx)); + + // Add an unresolved NS record (with no additionals as well) + message.getAuthorityRecords().add(newNsRecord(name, "unresolved." + dnsName)); + } + + return message; + } + } + + @Test + @Timeout(value = 3000, unit = TimeUnit.MILLISECONDS) + public void testTimeoutNotCached() { + DnsCache cache = new DnsCache() { + @Override + public void clear() { + // NOOP + } + + @Override + public boolean clear(String hostname) { + return false; + } + + @Override + public List get(String hostname, DnsRecord[] additionals) { + return Collections.emptyList(); + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, InetAddress address, + long originalTtl, EventLoop loop) { + fail("Should not be cached"); + return null; + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) { + fail("Should not be cached"); + return null; + } + }; + DnsNameResolverBuilder builder = newResolver(); + builder.queryTimeoutMillis(100) + .authoritativeDnsServerCache(cache) + .resolveCache(cache) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider( + new InetSocketAddress(NetUtil.LOCALHOST, 12345))); + DnsNameResolver resolver = builder.build(); + Future result = resolver.resolve("doesnotexist.netty.io").awaitUninterruptibly(); + Throwable cause = result.cause(); + assertThat(cause, Matchers.instanceOf(UnknownHostException.class)); + cause.getCause().printStackTrace(); + assertThat(cause.getCause(), Matchers.instanceOf(DnsNameResolverTimeoutException.class)); + assertTrue(DnsNameResolver.isTimeoutError(cause)); + assertTrue(DnsNameResolver.isTransportOrTimeoutError(cause)); + resolver.close(); + } + + @Test + public void testTimeoutIpv4PreferredA() throws IOException { + testTimeoutOneQuery(ResolvedAddressTypes.IPV4_PREFERRED, RecordType.A, RecordType.AAAA); + } + @Test + public void testTimeoutIpv4PreferredAAAA() throws IOException { + testTimeoutOneQuery(ResolvedAddressTypes.IPV4_PREFERRED, RecordType.AAAA, RecordType.A); + } + + @Test + public void testTimeoutIpv6PreferredA() throws IOException { + testTimeoutOneQuery(ResolvedAddressTypes.IPV6_PREFERRED, RecordType.A, RecordType.AAAA); + } + @Test + public void testTimeoutIpv6PreferredAAAA() throws IOException { + testTimeoutOneQuery(ResolvedAddressTypes.IPV6_PREFERRED, RecordType.AAAA, RecordType.A); + } + + private static void testTimeoutOneQuery(ResolvedAddressTypes type, final RecordType recordType, RecordType dropType) + throws IOException { + + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + + @Override + public Set getRecords(QuestionRecord question) { + Set records = new LinkedHashSet(2); + Map map1 = new HashMap(); + if (question.getRecordType() == RecordType.A) { + map1.put(DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.2"); + } else { + map1.put(DnsAttribute.IP_ADDRESS.toLowerCase(), "::1"); + } + records.add(new TestDnsServer.TestResourceRecord( + question.getDomainName(), recordType, map1)); + return records; + } + }); + dnsServer2.start(dropType); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .queryTimeoutMillis(500) + .resolvedAddressTypes(type) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List resolvedAddresses = + resolver.resolveAll("somehost.netty.io").syncUninterruptibly().getNow(); + assertEquals(1, resolvedAddresses.size()); + if (recordType == RecordType.A) { + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }))); + } else { + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress( + new byte[]{ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }))); + } + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testDnsNameResolverBuilderCopy() { + ChannelFactory channelFactory = + new ReflectiveChannelFactory(NioDatagramChannel.class); + DnsNameResolverBuilder builder = new DnsNameResolverBuilder(group.next()) + .channelFactory(channelFactory); + DnsNameResolverBuilder copiedBuilder = builder.copy(); + + // change channel factory does not propagate to previously made copy + ChannelFactory newChannelFactory = + new ReflectiveChannelFactory(NioDatagramChannel.class); + builder.channelFactory(newChannelFactory); + assertEquals(channelFactory, copiedBuilder.channelFactory()); + assertEquals(newChannelFactory, builder.channelFactory()); + } + + @Test + public void testFollowCNAMEEvenIfARecordIsPresent() throws IOException { + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + + @Override + public Set getRecords(QuestionRecord question) { + if (question.getDomainName().equals("cname.netty.io")) { + Map map1 = new HashMap(); + map1.put(DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.99"); + return Collections.singleton( + new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.A, map1)); + } else { + Set records = new LinkedHashSet(2); + Map map = new HashMap(); + map.put(DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"); + records.add(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.CNAME, map)); + + Map map1 = new HashMap(); + map1.put(DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.2"); + records.add(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.A, map1)); + return records; + } + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List resolvedAddresses = + resolver.resolveAll("somehost.netty.io").syncUninterruptibly().getNow(); + assertEquals(2, resolvedAddresses.size()); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 99 }))); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }))); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + // + // This should only result in one query. + // ;; ANSWER SECTION: + // somehost.netty.io. 594 IN CNAME cname.netty.io. + // cname.netty.io. 9042 IN CNAME cname2.netty.io. + // cname2.netty.io. 1312 IN CNAME cname3.netty.io.io. + // cname3.netty.io. 20 IN A 10.0.0.2 + @Test + public void testCNAMEFollowInResponseWithoutExtraQuery() throws IOException { + final AtomicInteger queryCount = new AtomicInteger(); + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + + @Override + public Set getRecords(QuestionRecord question) { + queryCount.incrementAndGet(); + if (question.getDomainName().equals("somehost.netty.io")) { + Set records = new LinkedHashSet(2); + Map map = new HashMap(); + map.put(DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"); + records.add(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.CNAME, map)); + + map = new HashMap(); + map.put(DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname2.netty.io"); + records.add(new TestDnsServer.TestResourceRecord( + "cname.netty.io", RecordType.CNAME, map)); + + map = new HashMap(); + map.put(DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname3.netty.io"); + records.add(new TestDnsServer.TestResourceRecord( + "cname2.netty.io", RecordType.CNAME, map)); + + Map map1 = new HashMap(); + map1.put(DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.2"); + records.add(new TestDnsServer.TestResourceRecord( + "cname3.netty.io", RecordType.A, map1)); + return records; + } + return null; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List resolvedAddresses = + resolver.resolveAll("somehost.netty.io").syncUninterruptibly().getNow(); + assertEquals(1, resolvedAddresses.size()); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 2 }))); + assertEquals(1, queryCount.get()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testFollowCNAMELoop() throws IOException { + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + + @Override + public Set getRecords(QuestionRecord question) { + Set records = new LinkedHashSet(4); + + records.add(new TestDnsServer.TestResourceRecord("x." + question.getDomainName(), + RecordType.A, Collections.singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.99"))); + records.add(new TestDnsServer.TestResourceRecord( + "cname2.netty.io", RecordType.CNAME, + Collections.singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"))); + records.add(new TestDnsServer.TestResourceRecord( + "cname.netty.io", RecordType.CNAME, + Collections.singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname2.netty.io"))); + records.add(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.CNAME, + Collections.singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"))); + return records; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + final DnsNameResolver finalResolver = resolver; + assertThrows(UnknownHostException.class, new Executable() { + @Override + public void execute() throws Throwable { + finalResolver.resolveAll("somehost.netty.io").syncUninterruptibly().getNow(); + } + }); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testCNAMELoopInCache() throws Throwable { + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress())); + + resolver = builder.build(); + // Add a CNAME loop into the cache + final String name = "somehost.netty.io."; + String name2 = "cname.netty.io."; + + resolver.cnameCache().cache(name, name2, Long.MAX_VALUE, resolver.executor()); + resolver.cnameCache().cache(name2, name, Long.MAX_VALUE, resolver.executor()); + final DnsNameResolver finalResolver = resolver; + assertThrows(UnknownHostException.class, new Executable() { + @Override + public void execute() throws Throwable { + finalResolver.resolve(name).syncUninterruptibly().getNow(); + } + }); + } finally { + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testSearchDomainQueryFailureForSingleAddressTypeCompletes() { + assertThrows(UnknownHostException.class, new Executable() { + @Override + public void execute() { + testSearchDomainQueryFailureCompletes(ResolvedAddressTypes.IPV4_ONLY); + } + }); + } + + @Test + public void testSearchDomainQueryFailureForMultipleAddressTypeCompletes() { + assertThrows(UnknownHostException.class, new Executable() { + @Override + public void execute() throws Throwable { + testSearchDomainQueryFailureCompletes(ResolvedAddressTypes.IPV4_PREFERRED); + } + }); + } + + private void testSearchDomainQueryFailureCompletes(ResolvedAddressTypes types) { + DnsNameResolver resolver = newResolver() + .resolvedAddressTypes(types) + .ndots(1) + .searchDomains(singletonList(".")).build(); + try { + resolver.resolve("invalid.com").syncUninterruptibly(); + } finally { + resolver.close(); + } + } + + @Test + @Timeout(value = 2000, unit = TimeUnit.MILLISECONDS) + public void testCachesClearedOnClose() throws Exception { + final CountDownLatch resolveLatch = new CountDownLatch(1); + final CountDownLatch authoritativeLatch = new CountDownLatch(1); + + DnsNameResolver resolver = newResolver().resolveCache(new DnsCache() { + @Override + public void clear() { + resolveLatch.countDown(); + } + + @Override + public boolean clear(String hostname) { + return false; + } + + @Override + public List get(String hostname, DnsRecord[] additionals) { + return null; + } + + @Override + public DnsCacheEntry cache( + String hostname, DnsRecord[] additionals, InetAddress address, long originalTtl, EventLoop loop) { + return null; + } + + @Override + public DnsCacheEntry cache( + String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) { + return null; + } + }).authoritativeDnsServerCache(new DnsCache() { + @Override + public void clear() { + authoritativeLatch.countDown(); + } + + @Override + public boolean clear(String hostname) { + return false; + } + + @Override + public List get(String hostname, DnsRecord[] additionals) { + return null; + } + + @Override + public DnsCacheEntry cache( + String hostname, DnsRecord[] additionals, InetAddress address, long originalTtl, EventLoop loop) { + return null; + } + + @Override + public DnsCacheEntry cache(String hostname, DnsRecord[] additionals, Throwable cause, EventLoop loop) { + return null; + } + }).build(); + + resolver.close(); + resolveLatch.await(); + authoritativeLatch.await(); + } + + @Test + public void testResolveACachedWithDot() { + final DnsCache cache = new DefaultDnsCache(); + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY) + .resolveCache(cache).build(); + + try { + String domain = DOMAINS.iterator().next(); + String domainWithDot = domain + '.'; + + resolver.resolve(domain).syncUninterruptibly(); + List cached = cache.get(domain, null); + List cached2 = cache.get(domainWithDot, null); + + assertEquals(1, cached.size()); + assertEquals(cached, cached2); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveACachedWithDotSearchDomain() throws Exception { + final TestDnsCache cache = new TestDnsCache(new DefaultDnsCache()); + TestDnsServer server = new TestDnsServer(Collections.singleton("test.netty.io")); + server.start(); + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY) + .searchDomains(Collections.singletonList("netty.io")) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(server.localAddress())) + .resolveCache(cache).build(); + try { + resolver.resolve("test").syncUninterruptibly(); + + assertNull(cache.cacheHits.get("test.netty.io")); + + List cached = cache.cache.get("test.netty.io", null); + List cached2 = cache.cache.get("test.netty.io.", null); + assertEquals(1, cached.size()); + assertEquals(cached, cached2); + + resolver.resolve("test").syncUninterruptibly(); + List entries = cache.cacheHits.get("test.netty.io"); + assertFalse(entries.isEmpty()); + } finally { + resolver.close(); + server.stop(); + } + } + + @Test + public void testChannelFactoryException() { + final IllegalStateException exception = new IllegalStateException(); + try { + newResolver().channelFactory(new ChannelFactory() { + @Override + public DatagramChannel newChannel() { + throw exception; + } + }).build(); + fail(); + } catch (Exception e) { + assertSame(exception, e); + } + } + + @Test + public void testCNameCached() throws Exception { + final Map cache = new ConcurrentHashMap(); + final AtomicInteger cnameQueries = new AtomicInteger(); + final AtomicInteger aQueries = new AtomicInteger(); + + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + + @Override + public Set getRecords(QuestionRecord question) { + if ("cname.netty.io".equals(question.getDomainName())) { + aQueries.incrementAndGet(); + + return Collections.singleton(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.A, + Collections.singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), "10.0.0.99"))); + } + if ("x.netty.io".equals(question.getDomainName())) { + cnameQueries.incrementAndGet(); + + return Collections.singleton(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.CNAME, + Collections.singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"))); + } + if ("y.netty.io".equals(question.getDomainName())) { + cnameQueries.incrementAndGet(); + + return Collections.singleton(new TestDnsServer.TestResourceRecord( + question.getDomainName(), RecordType.CNAME, + Collections.singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "x.netty.io"))); + } + return Collections.emptySet(); + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())) + .resolveCache(NoopDnsCache.INSTANCE) + .cnameCache(new DnsCnameCache() { + @Override + public String get(String hostname) { + assertTrue(hostname.endsWith("."), hostname); + return cache.get(hostname); + } + + @Override + public void cache(String hostname, String cname, long originalTtl, EventLoop loop) { + assertTrue(hostname.endsWith("."), hostname); + cache.put(hostname, cname); + } + + @Override + public void clear() { + // NOOP + } + + @Override + public boolean clear(String hostname) { + return false; + } + }); + resolver = builder.build(); + List resolvedAddresses = + resolver.resolveAll("x.netty.io").syncUninterruptibly().getNow(); + assertEquals(1, resolvedAddresses.size()); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 99 }))); + + assertEquals("cname.netty.io.", cache.get("x.netty.io.")); + assertEquals(1, cnameQueries.get()); + assertEquals(1, aQueries.get()); + + resolvedAddresses = + resolver.resolveAll("x.netty.io").syncUninterruptibly().getNow(); + assertEquals(1, resolvedAddresses.size()); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 99 }))); + + // Should not have queried for the CNAME again. + assertEquals(1, cnameQueries.get()); + assertEquals(2, aQueries.get()); + + resolvedAddresses = + resolver.resolveAll("y.netty.io").syncUninterruptibly().getNow(); + assertEquals(1, resolvedAddresses.size()); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 99 }))); + + assertEquals("x.netty.io.", cache.get("y.netty.io.")); + + // Will only query for one CNAME + assertEquals(2, cnameQueries.get()); + assertEquals(3, aQueries.get()); + + resolvedAddresses = + resolver.resolveAll("y.netty.io").syncUninterruptibly().getNow(); + assertEquals(1, resolvedAddresses.size()); + assertTrue(resolvedAddresses.contains(InetAddress.getByAddress(new byte[] { 10, 0, 0, 99 }))); + + // Should not have queried for the CNAME again. + assertEquals(2, cnameQueries.get()); + assertEquals(4, aQueries.get()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testInstanceWithNullPreferredAddressType() { + new DnsNameResolver( + group.next(), // eventLoop + new ReflectiveChannelFactory(NioDatagramChannel.class), // channelFactory + NoopDnsCache.INSTANCE, // resolveCache + NoopAuthoritativeDnsServerCache.INSTANCE, // authoritativeDnsServerCache + NoopDnsQueryLifecycleObserverFactory.INSTANCE, // dnsQueryLifecycleObserverFactory + 100, // queryTimeoutMillis + null, // resolvedAddressTypes, see https://github.com/netty/netty/pull/8445 + true, // recursionDesired + 1, // maxQueriesPerResolve + false, // traceEnabled + 4096, // maxPayloadSize + true, // optResourceEnabled + HostsFileEntriesResolver.DEFAULT, // hostsFileEntriesResolver + DnsServerAddressStreamProviders.platformDefault(), // dnsServerAddressStreamProvider + null, // searchDomains + 1, // ndots + true // decodeIdn + ).close(); + } + + @Test + public void testQueryTxt() throws Exception { + final String hostname = "txt.netty.io"; + final String txt1 = "some text"; + final String txt2 = "some more text"; + + TestDnsServer server = new TestDnsServer(new RecordStore() { + + @Override + public Set getRecords(QuestionRecord question) { + if (question.getDomainName().equals(hostname)) { + Map map1 = new HashMap(); + map1.put(DnsAttribute.CHARACTER_STRING.toLowerCase(), txt1); + + Map map2 = new HashMap(); + map2.put(DnsAttribute.CHARACTER_STRING.toLowerCase(), txt2); + + Set records = new HashSet(); + records.add(new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.TXT, map1)); + records.add(new TestDnsServer.TestResourceRecord(question.getDomainName(), RecordType.TXT, map2)); + return records; + } + return Collections.emptySet(); + } + }); + server.start(); + DnsNameResolver resolver = newResolver(ResolvedAddressTypes.IPV4_ONLY) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(server.localAddress())) + .build(); + try { + AddressedEnvelope envelope = resolver.query( + new DefaultDnsQuestion(hostname, DnsRecordType.TXT)).syncUninterruptibly().getNow(); + assertNotNull(envelope.sender()); + + DnsResponse response = envelope.content(); + assertNotNull(response); + + assertEquals(DnsResponseCode.NOERROR, response.code()); + int count = response.count(DnsSection.ANSWER); + + assertEquals(2, count); + List txts = new ArrayList(); + + for (int i = 0; i < 2; i++) { + txts.addAll(decodeTxt(response.recordAt(DnsSection.ANSWER, i))); + } + assertTrue(txts.contains(txt1)); + assertTrue(txts.contains(txt2)); + envelope.release(); + } finally { + resolver.close(); + server.stop(); + } + } + + private static List decodeTxt(DnsRecord record) { + if (!(record instanceof DnsRawRecord)) { + return Collections.emptyList(); + } + List list = new ArrayList(); + ByteBuf data = ((DnsRawRecord) record).content(); + int idx = data.readerIndex(); + int wIdx = data.writerIndex(); + while (idx < wIdx) { + int len = data.getUnsignedByte(idx++); + list.add(data.toString(idx, len, CharsetUtil.UTF_8)); + idx += len; + } + return list; + } + + @Test + public void testNotIncludeDuplicates() throws IOException { + final String name = "netty.io"; + final String ipv4Addr = "1.2.3.4"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + Set records = new LinkedHashSet(4); + String qName = question.getDomainName().toLowerCase(); + if (qName.equals(name)) { + records.add(new TestDnsServer.TestResourceRecord( + qName, RecordType.CNAME, + Collections.singletonMap( + DnsAttribute.DOMAIN_NAME.toLowerCase(), "cname.netty.io"))); + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + } else { + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + } + return records; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY); + + resolver = builder.build(); + List resolvedAddresses = resolver.resolveAll(name).syncUninterruptibly().getNow(); + assertEquals(Collections.singletonList(InetAddress.getByAddress(name, new byte[] { 1, 2, 3, 4 })), + resolvedAddresses); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testIncludeDuplicates() throws IOException { + final String name = "netty.io"; + final String ipv4Addr = "1.2.3.4"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + Set records = new LinkedHashSet(2); + String qName = question.getDomainName().toLowerCase(); + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + records.add(new TestDnsServer.TestResourceRecord(qName, + RecordType.A, Collections.singletonMap( + DnsAttribute.IP_ADDRESS.toLowerCase(), ipv4Addr))); + return records; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + builder.resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY); + + resolver = builder.build(); + List resolvedAddresses = resolver.resolveAll(new DefaultDnsQuestion(name, A)) + .syncUninterruptibly().getNow(); + assertEquals(2, resolvedAddresses.size()); + for (DnsRecord record: resolvedAddresses) { + ReferenceCountUtil.release(record); + } + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testDropAAAA() throws IOException { + String host = "somehost.netty.io"; + TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(host)); + dnsServer2.start(RecordType.AAAA); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(500) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List addressList = resolver.resolveAll(host).syncUninterruptibly().getNow(); + assertEquals(1, addressList.size()); + assertEquals(host, addressList.get(0).getHostName()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + @Timeout(value = 2000, unit = TimeUnit.MILLISECONDS) + public void testDropAAAAResolveFast() throws IOException { + String host = "somehost.netty.io"; + TestDnsServer dnsServer2 = new TestDnsServer(Collections.singleton(host)); + dnsServer2.start(RecordType.AAAA); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .completeOncePreferredResolved(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + InetAddress address = resolver.resolve(host).syncUninterruptibly().getNow(); + assertEquals(host, address.getHostName()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + @Timeout(value = 2000, unit = TimeUnit.MILLISECONDS) + public void testDropAAAAResolveAllFast() throws IOException { + final String host = "somehost.netty.io"; + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) throws DnsException { + String name = question.getDomainName(); + if (name.equals(host)) { + Set records = new HashSet(2); + records.add(new TestDnsServer.TestResourceRecord(name, RecordType.A, + Collections.singletonMap(DnsAttribute.IP_ADDRESS.toLowerCase(), + "10.0.0.1"))); + records.add(new TestDnsServer.TestResourceRecord(name, RecordType.A, + Collections.singletonMap(DnsAttribute.IP_ADDRESS.toLowerCase(), + "10.0.0.2"))); + return records; + } + return null; + } + }); + dnsServer2.start(RecordType.AAAA); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .completeOncePreferredResolved(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + List addresses = resolver.resolveAll(host).syncUninterruptibly().getNow(); + assertEquals(2, addresses.size()); + for (InetAddress address: addresses) { + assertThat(address, instanceOf(Inet4Address.class)); + assertEquals(host, address.getHostName()); + } + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testTruncatedWithoutTcpFallback() throws IOException { + testTruncated0(false, false); + } + + @Test + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testTruncatedWithTcpFallback() throws IOException { + testTruncated0(true, false); + } + + @Test + @Timeout(value = 5000, unit = TimeUnit.MILLISECONDS) + public void testTruncatedWithTcpFallbackBecauseOfMtu() throws IOException { + testTruncated0(true, true); + } + + private static DnsMessageModifier modifierFrom(DnsMessage message) { + DnsMessageModifier modifier = new DnsMessageModifier(); + modifier.setAcceptNonAuthenticatedData(message.isAcceptNonAuthenticatedData()); + modifier.setAdditionalRecords(message.getAdditionalRecords()); + modifier.setAnswerRecords(message.getAnswerRecords()); + modifier.setAuthoritativeAnswer(message.isAuthoritativeAnswer()); + modifier.setAuthorityRecords(message.getAuthorityRecords()); + modifier.setMessageType(message.getMessageType()); + modifier.setOpCode(message.getOpCode()); + modifier.setQuestionRecords(message.getQuestionRecords()); + modifier.setRecursionAvailable(message.isRecursionAvailable()); + modifier.setRecursionDesired(message.isRecursionDesired()); + modifier.setReserved(message.isReserved()); + modifier.setResponseCode(message.getResponseCode()); + modifier.setTransactionId(message.getTransactionId()); + modifier.setTruncated(message.isTruncated()); + return modifier; + } + + private static void testTruncated0(boolean tcpFallback, final boolean truncatedBecauseOfMtu) throws IOException { + ServerSocket serverSocket = null; + if (tcpFallback) { + // If we are configured to use TCP as a fallback also bind a TCP socket + serverSocket = new ServerSocket(); + serverSocket.setReuseAddress(true); + serverSocket.bind(new InetSocketAddress(NetUtil.LOCALHOST4, 0)); + } + + final String host = "somehost.netty.io"; + final String txt = "this is a txt record"; + final AtomicReference messageRef = new AtomicReference(); + + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + String name = question.getDomainName(); + if (name.equals(host)) { + return Collections.singleton( + new TestDnsServer.TestResourceRecord(name, RecordType.TXT, + Collections.singletonMap( + DnsAttribute.CHARACTER_STRING.toLowerCase(), txt))); + } + return null; + } + }) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + // Store a original message so we can replay it later on. + messageRef.set(message); + + if (!truncatedBecauseOfMtu) { + // Create a copy of the message but set the truncated flag. + DnsMessageModifier modifier = modifierFrom(message); + modifier.setTruncated(true); + return modifier.getDnsMessage(); + } + return message; + } + }; + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver(); + final DatagramChannel datagramChannel = new NioDatagramChannel(); + ChannelFactory channelFactory = new ChannelFactory() { + @Override + public DatagramChannel newChannel() { + return datagramChannel; + } + }; + builder.channelFactory(channelFactory); + if (tcpFallback) { + dnsServer2.start(null, (InetSocketAddress) serverSocket.getLocalSocketAddress()); + + // If we are configured to use TCP as a fallback also bind a TCP socket + builder.socketChannelType(NioSocketChannel.class); + } else { + dnsServer2.start(); + } + builder.queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + resolver = builder.build(); + if (truncatedBecauseOfMtu) { + datagramChannel.pipeline().addFirst(new ChannelInboundHandlerAdapter() { + @Override + public void channelRead(ChannelHandlerContext ctx, Object msg) { + if (msg instanceof DatagramPacket) { + // Truncate the packet by 1 byte. + DatagramPacket packet = (DatagramPacket) msg; + packet.content().writerIndex(packet.content().writerIndex() - 1); + } + ctx.fireChannelRead(msg); + } + }); + } + Future> envelopeFuture = resolver.query( + new DefaultDnsQuestion(host, DnsRecordType.TXT)); + + if (tcpFallback) { + // If we are configured to use TCP as a fallback lets replay the dns message over TCP + Socket socket = serverSocket.accept(); + responseViaSocket(socket, messageRef.get()); + + // Let's wait until we received the envelope before closing the socket. + envelopeFuture.syncUninterruptibly(); + + socket.close(); + serverSocket.close(); + } + + AddressedEnvelope envelope = envelopeFuture.syncUninterruptibly().getNow(); + assertNotNull(envelope.sender()); + + DnsResponse response = envelope.content(); + assertNotNull(response); + + assertEquals(DnsResponseCode.NOERROR, response.code()); + int count = response.count(DnsSection.ANSWER); + + assertEquals(1, count); + List texts = decodeTxt(response.recordAt(DnsSection.ANSWER, 0)); + assertEquals(1, texts.size()); + assertEquals(txt, texts.get(0)); + + if (tcpFallback) { + assertFalse(envelope.content().isTruncated()); + } else { + assertTrue(envelope.content().isTruncated()); + } + assertTrue(envelope.release()); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + private static void responseViaSocket(Socket socket, DnsMessage message) throws IOException { + InputStream in = socket.getInputStream(); + assertTrue((in.read() << 8 | (in.read() & 0xff)) > 2); // skip length field + int txnId = in.read() << 8 | (in.read() & 0xff); + + IoBuffer ioBuffer = IoBuffer.allocate(1024); + // Must replace the transactionId with the one from the TCP request + DnsMessageModifier modifier = modifierFrom(message); + modifier.setTransactionId(txnId); + new DnsMessageEncoder().encode(ioBuffer, modifier.getDnsMessage()); + ioBuffer.flip(); + + ByteBuffer lenBuffer = ByteBuffer.allocate(2); + lenBuffer.putShort((short) ioBuffer.remaining()); + lenBuffer.flip(); + + while (lenBuffer.hasRemaining()) { + socket.getOutputStream().write(lenBuffer.get()); + } + + while (ioBuffer.hasRemaining()) { + socket.getOutputStream().write(ioBuffer.get()); + } + socket.getOutputStream().flush(); + } + + @Test + public void testTcpFallbackWhenTimeout() throws IOException { + testTcpFallbackWhenTimeout(true); + } + + @Test + public void testTcpFallbackFailedWhenTimeout() throws IOException { + testTcpFallbackWhenTimeout(false); + } + + private void testTcpFallbackWhenTimeout(boolean tcpSuccess) throws IOException { + ServerSocket serverSocket = new ServerSocket(); + serverSocket.setReuseAddress(true); + serverSocket.bind(new InetSocketAddress(NetUtil.LOCALHOST4, 0)); + + final String host = "somehost.netty.io"; + final String txt = "this is a txt record"; + final AtomicReference messageRef = new AtomicReference(); + + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + String name = question.getDomainName(); + if (name.equals(host)) { + return Collections.singleton( + new TestDnsServer.TestResourceRecord(name, RecordType.TXT, + Collections.singletonMap( + DnsAttribute.CHARACTER_STRING.toLowerCase(), txt))); + } + return null; + } + }) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + // Store a original message so we can replay it later on. + messageRef.set(message); + return null; + } + }; + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver(); + final DatagramChannel datagramChannel = new NioDatagramChannel(); + ChannelFactory channelFactory = new ChannelFactory() { + @Override + public DatagramChannel newChannel() { + return datagramChannel; + } + }; + builder.channelFactory(channelFactory); + dnsServer2.start(null, (InetSocketAddress) serverSocket.getLocalSocketAddress()); + // If we are configured to use TCP as a fallback also bind a TCP socket + builder.socketChannelType(NioSocketChannel.class, true); + + builder.queryTimeoutMillis(1000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + resolver = builder.build(); + Future> envelopeFuture = resolver.query( + new DefaultDnsQuestion(host, DnsRecordType.TXT)); + + // If we are configured to use TCP as a fallback lets replay the dns message over TCP + Socket socket = serverSocket.accept(); + + if (tcpSuccess) { + responseViaSocket(socket, messageRef.get()); + + // Let's wait until we received the envelope before closing the socket. + envelopeFuture.syncUninterruptibly(); + socket.close(); + + AddressedEnvelope envelope = + envelopeFuture.syncUninterruptibly().getNow(); + assertNotNull(envelope.sender()); + + DnsResponse response = envelope.content(); + assertNotNull(response); + + assertEquals(DnsResponseCode.NOERROR, response.code()); + int count = response.count(DnsSection.ANSWER); + + assertEquals(1, count); + List texts = decodeTxt(response.recordAt(DnsSection.ANSWER, 0)); + assertEquals(1, texts.size()); + assertEquals(txt, texts.get(0)); + + assertFalse(envelope.content().isTruncated()); + assertTrue(envelope.release()); + } else { + // Just close the socket. This should cause the original exception to be used. + socket.close(); + Throwable error = envelopeFuture.awaitUninterruptibly().cause(); + assertThat(error, instanceOf(DnsNameResolverTimeoutException.class)); + assertThat(error.getSuppressed().length, greaterThanOrEqualTo(1)); + } + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + serverSocket.close(); + } + } + + @Test + public void testCancelPromise() throws Exception { + final EventLoop eventLoop = group.next(); + final Promise promise = eventLoop.newPromise(); + final TestDnsServer dnsServer1 = new TestDnsServer(Collections.emptySet()) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + promise.cancel(true); + return message; + } + }; + dnsServer1.start(); + final AtomicBoolean isQuerySentToSecondServer = new AtomicBoolean(); + final TestDnsServer dnsServer2 = new TestDnsServer(Collections.emptySet()) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + isQuerySentToSecondServer.set(true); + return message; + } + }; + dnsServer2.start(); + DnsServerAddressStreamProvider nameServerProvider = + new SequentialDnsServerAddressStreamProvider(dnsServer1.localAddress(), + dnsServer2.localAddress()); + final DnsNameResolver resolver = new DnsNameResolverBuilder(group.next()) + .dnsQueryLifecycleObserverFactory(new TestRecursiveCacheDnsQueryLifecycleObserverFactory()) + .channelType(NioDatagramChannel.class) + .optResourceEnabled(false) + .nameServerProvider(nameServerProvider) + .build(); + + try { + resolver.resolve("non-existent.netty.io", promise).sync(); + fail(); + } catch (Exception e) { + assertThat(e, is(instanceOf(CancellationException.class))); + } + assertThat(isQuerySentToSecondServer.get(), is(false)); + } + + @Test + public void testCNAMERecursiveResolveDifferentNameServersForDomains() throws IOException { + final String firstName = "firstname.com"; + final String secondName = "secondname.com"; + final String lastName = "lastname.com"; + final String ipv4Addr = "1.2.3.4"; + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + + if (question.getDomainName().equals(firstName)) { + rm.setDnsType(RecordType.CNAME); + rm.put(DnsAttribute.DOMAIN_NAME, secondName); + } else if (question.getDomainName().equals(lastName)) { + rm.setDnsType(question.getRecordType()); + rm.put(DnsAttribute.IP_ADDRESS, ipv4Addr); + } else { + return null; + } + return Collections.singleton(rm.getEntry()); + } + }); + dnsServer2.start(); + final TestDnsServer dnsServer3 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + if (question.getDomainName().equals(secondName)) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + rm.setDnsType(RecordType.CNAME); + rm.put(DnsAttribute.DOMAIN_NAME, lastName); + return Collections.singleton(rm.getEntry()); + } + return null; + } + }); + dnsServer3.start(); + DnsNameResolver resolver = null; + try { + resolver = newResolver() + .resolveCache(NoopDnsCache.INSTANCE) + .cnameCache(NoopDnsCnameCache.INSTANCE) + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new DnsServerAddressStreamProvider() { + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + if (hostname.equals(secondName + '.')) { + return DnsServerAddresses.singleton(dnsServer3.localAddress()).stream(); + } + return DnsServerAddresses.singleton(dnsServer2.localAddress()).stream(); + } + }) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED).build(); + + assertResolvedAddress(resolver.resolve(firstName).syncUninterruptibly().getNow(), ipv4Addr, firstName); + } finally { + dnsServer2.stop(); + dnsServer3.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + private static void assertResolvedAddress(InetAddress resolvedAddress, String ipAddr, String hostname) { + assertEquals(ipAddr, resolvedAddress.getHostAddress()); + assertEquals(hostname, resolvedAddress.getHostName()); + } + + @Test + public void testAllNameServers() throws IOException { + final String domain = "netty.io"; + final String ipv4Addr = "1.2.3.4"; + final AtomicInteger server2Counter = new AtomicInteger(); + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + server2Counter.incrementAndGet(); + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + + rm.setDnsType(question.getRecordType()); + rm.put(DnsAttribute.IP_ADDRESS, ipv4Addr); + return Collections.singleton(rm.getEntry()); + } + }); + dnsServer2.start(); + + final AtomicInteger server3Counter = new AtomicInteger(); + final TestDnsServer dnsServer3 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + server3Counter.incrementAndGet(); + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + + rm.setDnsType(question.getRecordType()); + rm.put(DnsAttribute.IP_ADDRESS, ipv4Addr); + return Collections.singleton(rm.getEntry()); + } + }); + dnsServer3.start(); + DnsNameResolver resolver = null; + try { + resolver = newResolver() + .resolveCache(NoopDnsCache.INSTANCE) + .cnameCache(NoopDnsCnameCache.INSTANCE) + .recursionDesired(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new DnsServerAddressStreamProvider() { + private final DnsServerAddresses addresses = + DnsServerAddresses.rotational(dnsServer2.localAddress(), dnsServer3.localAddress()); + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + return addresses.stream(); + } + }) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY).build(); + + assertResolvedAddress(resolver.resolve(domain).syncUninterruptibly().getNow(), ipv4Addr, domain); + assertEquals(1, server2Counter.get()); + assertEquals(0, server3Counter.get()); + assertResolvedAddress(resolver.resolve(domain).syncUninterruptibly().getNow(), ipv4Addr, domain); + assertEquals(1, server2Counter.get()); + assertEquals(1, server3Counter.get()); + assertResolvedAddress(resolver.resolve(domain).syncUninterruptibly().getNow(), ipv4Addr, domain); + assertEquals(2, server2Counter.get()); + assertEquals(1, server3Counter.get()); + assertResolvedAddress(resolver.resolve(domain).syncUninterruptibly().getNow(), ipv4Addr, domain); + assertEquals(2, server2Counter.get()); + assertEquals(2, server3Counter.get()); + } finally { + dnsServer2.stop(); + dnsServer3.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + @Timeout(value = 2000, unit = TimeUnit.MILLISECONDS) + public void testSrvWithCnameNotCached() throws Exception { + final AtomicBoolean alias = new AtomicBoolean(); + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + String name = question.getDomainName(); + if (name.equals("service.netty.io")) { + Set records = new HashSet(2); + + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(name); + rm.setDnsTtl(10); + rm.setDnsType(RecordType.CNAME); + rm.put(DnsAttribute.DOMAIN_NAME, "alias.service.netty.io"); + records.add(rm.getEntry()); + + rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(name); + rm.setDnsTtl(10); + rm.setDnsType(RecordType.SRV); + rm.put(DnsAttribute.DOMAIN_NAME, "foo.service.netty.io"); + rm.put(DnsAttribute.SERVICE_PORT, "8080"); + rm.put(DnsAttribute.SERVICE_PRIORITY, "10"); + rm.put(DnsAttribute.SERVICE_WEIGHT, "1"); + records.add(rm.getEntry()); + return records; + } + if (name.equals("foo.service.netty.io")) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(name); + rm.setDnsTtl(10); + rm.setDnsType(RecordType.A); + rm.put(DnsAttribute.IP_ADDRESS, "10.0.0.1"); + return Collections.singleton(rm.getEntry()); + } + if (alias.get()) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(name); + rm.setDnsTtl(10); + rm.setDnsType(RecordType.SRV); + rm.put(DnsAttribute.DOMAIN_NAME, "foo.service.netty.io"); + rm.put(DnsAttribute.SERVICE_PORT, "8080"); + rm.put(DnsAttribute.SERVICE_PRIORITY, "10"); + rm.put(DnsAttribute.SERVICE_WEIGHT, "1"); + return Collections.singleton(rm.getEntry()); + } + return null; + } + }); + dnsServer2.start(); + DnsNameResolver resolver = null; + try { + DnsNameResolverBuilder builder = newResolver() + .recursionDesired(false) + .queryTimeoutMillis(10000) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_PREFERRED) + .completeOncePreferredResolved(true) + .maxQueriesPerResolve(16) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())); + + resolver = builder.build(); + assertNotEmptyAndRelease(resolver.resolveAll(new DefaultDnsQuestion("service.netty.io", SRV))); + alias.set(true); + assertNotEmptyAndRelease(resolver.resolveAll(new DefaultDnsQuestion("service.netty.io", SRV))); + alias.set(false); + assertNotEmptyAndRelease(resolver.resolveAll(new DefaultDnsQuestion("service.netty.io", SRV))); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testCNAMEOnlyTriedOnAddressLookups() throws Exception { + + final AtomicInteger cnameQueries = new AtomicInteger(); + + TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord questionRecord) { + if (questionRecord.getRecordType() == RecordType.CNAME) { + cnameQueries.incrementAndGet(); + } + + return Collections.emptySet(); + } + }); + + dnsServer2.start(); + + DnsNameResolver resolver = null; + try { + resolver = newNonCachedResolver(ResolvedAddressTypes.IPV4_PREFERRED) + .maxQueriesPerResolve(4) + .searchDomains(Collections.emptyList()) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())) + .build(); + + // We expect these resolves to fail with UnknownHostException, + // and then check that no unexpected CNAME queries were performed. + assertThat(resolver.resolveAll(new DefaultDnsQuestion("lookup-srv.netty.io", SRV)).await().cause(), + instanceOf(UnknownHostException.class)); + assertEquals(0, cnameQueries.get()); + + assertThat(resolver.resolveAll(new DefaultDnsQuestion("lookup-naptr.netty.io", NAPTR)).await().cause(), + instanceOf(UnknownHostException.class)); + assertEquals(0, cnameQueries.get()); + + assertThat(resolver.resolveAll(new DefaultDnsQuestion("lookup-cname.netty.io", CNAME)).await().cause(), + instanceOf(UnknownHostException.class)); + assertEquals(1, cnameQueries.getAndSet(0)); + + assertThat(resolver.resolveAll(new DefaultDnsQuestion("lookup-a.netty.io", A)).await().cause(), + instanceOf(UnknownHostException.class)); + assertEquals(1, cnameQueries.getAndSet(0)); + + assertThat(resolver.resolveAll(new DefaultDnsQuestion("lookup-aaaa.netty.io", AAAA)).await().cause(), + instanceOf(UnknownHostException.class)); + assertEquals(1, cnameQueries.getAndSet(0)); + + assertThat(resolver.resolveAll("lookup-address.netty.io").await().cause(), + instanceOf(UnknownHostException.class)); + assertEquals(1, cnameQueries.getAndSet(0)); + } finally { + dnsServer2.stop(); + if (resolver != null) { + resolver.close(); + } + } + } + + private static void assertNotEmptyAndRelease(Future> recordsFuture) throws Exception { + List records = recordsFuture.get(); + assertFalse(records.isEmpty()); + for (DnsRecord record : records) { + ReferenceCountUtil.release(record); + } + } + + @Test + public void testResolveIpv6WithScopeId() throws Exception { + testResolveIpv6WithScopeId0(false); + } + + @Test + public void testResolveAllIpv6WithScopeId() throws Exception { + testResolveIpv6WithScopeId0(true); + } + + private void testResolveIpv6WithScopeId0(boolean resolveAll) throws Exception { + DnsNameResolver resolver = newResolver().build(); + String address = "fe80:0:0:0:1c31:d1d1:4824:72a9"; + int scopeId = 15; + String addressString = address + '%' + scopeId; + byte[] bytes = NetUtil.createByteArrayFromIpAddressString(address); + Inet6Address inet6Address = Inet6Address.getByAddress(null, bytes, scopeId); + try { + final InetAddress addr; + if (resolveAll) { + List addressList = resolver.resolveAll(addressString).getNow(); + assertEquals(1, addressList.size()); + addr = addressList.get(0); + } else { + addr = resolver.resolve(addressString).getNow(); + } + assertEquals(inet6Address, addr); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveIpv6WithoutScopeId() throws Exception { + testResolveIpv6WithoutScopeId0(false); + } + + @Test + public void testResolveAllIpv6WithoutScopeId() throws Exception { + testResolveIpv6WithoutScopeId0(true); + } + + private void testResolveIpv6WithoutScopeId0(boolean resolveAll) throws Exception { + DnsNameResolver resolver = newResolver().build(); + String addressString = "fe80:0:0:0:1c31:d1d1:4824:72a9"; + byte[] bytes = NetUtil.createByteArrayFromIpAddressString(addressString); + Inet6Address inet6Address = (Inet6Address) InetAddress.getByAddress(bytes); + try { + final InetAddress addr; + if (resolveAll) { + List addressList = resolver.resolveAll(addressString).getNow(); + assertEquals(1, addressList.size()); + addr = addressList.get(0); + } else { + addr = resolver.resolve(addressString).getNow(); + } + assertEquals(inet6Address, addr); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveIp4() throws Exception { + testResolveIp4(false); + } + + @Test + public void testResolveAllIp4() throws Exception { + testResolveIp4(true); + } + + private void testResolveIp4(boolean resolveAll) throws Exception { + DnsNameResolver resolver = newResolver().build(); + String addressString = "10.0.0.1"; + byte[] bytes = NetUtil.createByteArrayFromIpAddressString(addressString); + InetAddress inetAddress = InetAddress.getByAddress(bytes); + try { + final InetAddress addr; + if (resolveAll) { + List addressList = resolver.resolveAll(addressString).getNow(); + assertEquals(1, addressList.size()); + addr = addressList.get(0); + } else { + addr = resolver.resolve(addressString).getNow(); + } + assertEquals(inetAddress, addr); + } finally { + resolver.close(); + } + } + + @Test + public void testResolveSearchDomainStopOnFirstSuccess() throws Exception { + final String addressString = "10.0.0.1"; + final Queue names = new ConcurrentLinkedQueue(); + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + private int called; + @Override + public Set getRecords(QuestionRecord question) { + names.offer(question.getDomainName()); + if (++called == 2) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + + rm.setDnsType(question.getRecordType()); + rm.put(DnsAttribute.IP_ADDRESS, addressString); + return Collections.singleton(rm.getEntry()); + } + return null; + } + }); + dnsServer2.start(); + + DnsNameResolver resolver = newResolver().searchDomains( + Arrays.asList("search1.netty.io", "search2.netty.io", "search3.netty.io")) + .ndots(2).nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .build(); + + byte[] bytes = NetUtil.createByteArrayFromIpAddressString(addressString); + InetAddress inetAddress = InetAddress.getByAddress(bytes); + try { + final InetAddress addr = resolver.resolve("netty.io").sync().getNow(); + assertEquals(inetAddress, addr); + } finally { + resolver.close(); + dnsServer2.stop(); + assertEquals("netty.io.search1.netty.io", names.poll()); + assertEquals("netty.io.search2.netty.io", names.poll()); + assertTrue(names.isEmpty()); + } + } + + @Test + public void testResolveTryWithoutSearchDomainFirst() throws Exception { + testResolveTryWithoutSearchDomainFirst(true); + } + + @Test + public void testResolveTryWithoutSearchDomainFirstButContinue() throws Exception { + testResolveTryWithoutSearchDomainFirst(false); + } + + private static void testResolveTryWithoutSearchDomainFirst(final boolean absoluteSuccess) throws Exception { + final String addressString = "10.0.0.1"; + final Queue names = new ConcurrentLinkedQueue(); + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + private int called; + @Override + public Set getRecords(QuestionRecord question) { + names.offer(question.getDomainName()); + ++called; + if ((absoluteSuccess && called == 1) || called == 3) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + + rm.setDnsType(question.getRecordType()); + rm.put(DnsAttribute.IP_ADDRESS, addressString); + return Collections.singleton(rm.getEntry()); + } + return null; + } + }); + dnsServer2.start(); + + DnsNameResolver resolver = newResolver().searchDomains( + Arrays.asList("search1.netty.io", "search2.netty.io", "search3.netty.io")) + .ndots(1).nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .build(); + + byte[] bytes = NetUtil.createByteArrayFromIpAddressString(addressString); + InetAddress inetAddress = InetAddress.getByAddress(bytes); + try { + final InetAddress addr = resolver.resolve("netty.io").sync().getNow(); + assertEquals(inetAddress, addr); + } finally { + resolver.close(); + dnsServer2.stop(); + assertEquals("netty.io", names.poll()); + if (!absoluteSuccess) { + assertEquals("netty.io.search1.netty.io", names.poll()); + assertEquals("netty.io.search2.netty.io", names.poll()); + } + assertTrue(names.isEmpty()); + } + } + + @Test + public void testInflightQueries() throws Exception { + final String addressString = "10.0.0.1"; + final AtomicInteger called = new AtomicInteger(); + final CountDownLatch latch = new CountDownLatch(1); + final TestDnsServer dnsServer2 = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord question) { + called.incrementAndGet(); + try { + latch.await(); + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(question.getDomainName()); + rm.setDnsTtl(100); + + rm.setDnsType(question.getRecordType()); + rm.put(DnsAttribute.IP_ADDRESS, addressString); + return Collections.singleton(rm.getEntry()); + } catch (InterruptedException e) { + throw new IllegalStateException(e); + } + } + }); + dnsServer2.start(); + + DnsNameResolver resolver = newResolver() + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer2.localAddress())) + .resolvedAddressTypes(ResolvedAddressTypes.IPV4_ONLY) + .consolidateCacheSize(2) + .build(); + + byte[] bytes = NetUtil.createByteArrayFromIpAddressString(addressString); + InetAddress inetAddress = InetAddress.getByAddress(bytes); + try { + Future f = resolver.resolve("netty.io"); + Future f2 = resolver.resolve("netty.io"); + assertFalse(f.isDone()); + assertFalse(f2.isDone()); + + // Now unblock so we receive the response back for our query. + latch.countDown(); + + assertEquals(inetAddress, f.sync().getNow()); + assertEquals(inetAddress, f2.sync().getNow()); + } finally { + resolver.close(); + dnsServer2.stop(); + assertEquals(1, called.get()); + } + } + + @Test + public void testAddressAlreadyInUse() throws Exception { + DatagramSocket datagramSocket = new DatagramSocket(); + try { + assertTrue(datagramSocket.isBound()); + try { + final DnsNameResolver resolver = newResolver() + .localAddress(datagramSocket.getLocalSocketAddress()).build(); + try { + Throwable cause = assertThrows(UnknownHostException.class, new Executable() { + @Override + public void execute() throws Throwable { + resolver.resolve("netty.io").sync(); + } + }); + assertThat(cause.getCause(), Matchers.instanceOf(BindException.class)); + } finally { + resolver.close(); + } + } catch (IllegalStateException cause) { + // We might also throw directly here... in this case let's verify that we use the correct exception. + assertThat(cause.getCause(), Matchers.instanceOf(BindException.class)); + } + } finally { + datagramSocket.close(); + } + } + + @Test + public void testResponseFeedbackStream() { + final AtomicBoolean successCalled = new AtomicBoolean(); + final AtomicBoolean failureCalled = new AtomicBoolean(); + final AtomicBoolean returnSuccess = new AtomicBoolean(false); + final DnsNameResolver resolver = newResolver(true, new DnsServerAddressStreamProvider() { + @Override + public DnsServerAddressStream nameServerAddressStream(String hostname) { + return new DnsServerResponseFeedbackAddressStream() { + @Override + public void feedbackSuccess(InetSocketAddress address, long queryResponseTimeNanos) { + assertThat(queryResponseTimeNanos, greaterThanOrEqualTo(0L)); + successCalled.set(true); + } + + @Override + public void feedbackFailure(InetSocketAddress address, + Throwable failureCause, + long queryResponseTimeNanos) { + assertThat(queryResponseTimeNanos, greaterThanOrEqualTo(0L)); + assertNotNull(failureCause); + failureCalled.set(true); + } + + @Override + public InetSocketAddress next() { + if (returnSuccess.get()) { + return dnsServer.localAddress(); + } + try { + return new InetSocketAddress(InetAddress.getByAddress("foo.com", + new byte[] {(byte) 169, (byte) 254, 12, 34 }), 53); + } catch (UnknownHostException e) { + throw new Error(e); + } + } + + @Override + public int size() { + return 1; + } + + @Override + public DnsServerAddressStream duplicate() { + return this; + } + }; + } + }).build(); + try { + // setup call to be successful and verify + returnSuccess.set(true); + resolver.resolve("google.com").syncUninterruptibly().getNow(); + assertTrue(successCalled.get()); + assertFalse(failureCalled.get()); + + // reset state for next query + successCalled.set(false); + failureCalled.set(false); + + // setup call to fail and verify + returnSuccess.set(false); + try { + resolver.resolve("yahoo.com").syncUninterruptibly().getNow(); + fail(); + } catch (Exception e) { + // expected + assertThat(e, is(instanceOf(UnknownHostException.class))); + } finally { + assertFalse(successCalled.get()); + assertTrue(failureCalled.get()); + } + } finally { + if (resolver != null) { + resolver.close(); + } + } + } + + @Test + public void testCnameWithAAndAdditionalsAndAuthorities() throws Exception { + final String hostname = "test.netty.io"; + final String cname = "cname.netty.io"; + + final List nameServers = new ArrayList(); + + for (int i = 0; i < 13; i++) { + nameServers.add("ns" + i + ".foo.bar"); + } + + TestDnsServer server = new TestDnsServer(new RecordStore() { + @Override + public Set getRecords(QuestionRecord questionRecord) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(hostname); + rm.setDnsTtl(10000); + rm.setDnsType(RecordType.CNAME); + rm.put(DnsAttribute.DOMAIN_NAME, cname); + + Set records = new LinkedHashSet(); + records.add(rm.getEntry()); + records.add(newARecord(cname, "10.0.0.2")); + return records; + } + }) { + @Override + protected DnsMessage filterMessage(DnsMessage message) { + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getDomainName().equals(hostname)) { + // Let's add some extra records. + message.getAuthorityRecords().clear(); + message.getAdditionalRecords().clear(); + + for (String nameserver: nameServers) { + message.getAuthorityRecords().add(TestDnsServer.newNsRecord(".", nameserver)); + message.getAdditionalRecords().add(newAddressRecord(nameserver, RecordType.A, "10.0.0.1")); + message.getAdditionalRecords().add(newAddressRecord(nameserver, RecordType.AAAA, "::1")); + } + + return message; + } + } + return message; + } + }; + server.start(); + EventLoopGroup group = new NioEventLoopGroup(1); + + final DnsNameResolver resolver = new DnsNameResolver( + group.next(), new ReflectiveChannelFactory(NioDatagramChannel.class), + NoopDnsCache.INSTANCE, NoopAuthoritativeDnsServerCache.INSTANCE, + NoopDnsQueryLifecycleObserverFactory.INSTANCE, 2000, ResolvedAddressTypes.IPV4_ONLY, + true, 8, true, 4096, + false, HostsFileEntriesResolver.DEFAULT, + new SingletonDnsServerAddressStreamProvider(server.localAddress()), + new String [] { "k8se-apps.svc.cluster.local, svc.cluster.local, cluster.local" }, 1, true); + try { + InetAddress address = resolver.resolve(hostname).sync().getNow(); + assertArrayEquals(new byte[] { 10, 0, 0, 2 }, address.getAddress()); + } finally { + resolver.close(); + group.shutdownGracefully(0, 0, TimeUnit.SECONDS); + server.stop(); + } + } + +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsQueryIdSpaceTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsQueryIdSpaceTest.java new file mode 100644 index 0000000..bbd3bb7 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsQueryIdSpaceTest.java @@ -0,0 +1,81 @@ +/* + * Copyright 2024 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: + * + * https://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 org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; + +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class DnsQueryIdSpaceTest { + + @Test + public void testOverflow() { + final DnsQueryIdSpace ids = new DnsQueryIdSpace(); + assertThrows(IllegalStateException.class, new Executable() { + @Override + public void execute() { + ids.pushId(1); + } + }); + } + + @Test + public void testConsumeAndProduceAll() { + final DnsQueryIdSpace ids = new DnsQueryIdSpace(); + assertEquals(ids.maxUsableIds(), ids.usableIds()); + Set producedIdRound1 = new LinkedHashSet(); + Set producedIdRound2 = new LinkedHashSet(); + + for (int i = ids.maxUsableIds(); i > 0; i--) { + int id = ids.nextId(); + assertTrue(id >= 0); + assertTrue(producedIdRound1.add(id)); + } + assertEquals(0, ids.usableIds()); + assertEquals(-1, ids.nextId()); + + for (Integer v : producedIdRound1) { + ids.pushId(v); + } + + for (int i = ids.maxUsableIds(); i > 0; i--) { + int id = ids.nextId(); + assertTrue(id >= 0); + assertTrue(producedIdRound2.add(id)); + } + + assertEquals(producedIdRound1.size(), producedIdRound2.size()); + + Iterator producedIdRoundIt = producedIdRound1.iterator(); + Iterator producedIdRound2It = producedIdRound2.iterator(); + + boolean notSame = false; + while (producedIdRoundIt.hasNext()) { + if (producedIdRoundIt.next().intValue() != producedIdRound2It.next().intValue()) { + notSame = true; + break; + } + } + assertTrue(notSame); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsResolveContextTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsResolveContextTest.java new file mode 100644 index 0000000..f6b19f9 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsResolveContextTest.java @@ -0,0 +1,57 @@ +/* + * Copyright 2020 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: + * + * https://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.embedded.EmbeddedChannel; +import org.junit.jupiter.api.Test; + +import java.net.UnknownHostException; + +import static org.junit.jupiter.api.Assertions.fail; + +public class DnsResolveContextTest { + + private static final String HOSTNAME = "netty.io."; + + @Test + public void testCnameLoop() { + for (int i = 1; i < 128; i++) { + try { + DnsResolveContext.cnameResolveFromCache(buildCache(i), HOSTNAME); + fail(); + } catch (UnknownHostException expected) { + // expected + } + } + } + + private static DnsCnameCache buildCache(int chainLength) { + EmbeddedChannel channel = new EmbeddedChannel(); + DnsCnameCache cache = new DefaultDnsCnameCache(); + if (chainLength == 1) { + cache.cache(HOSTNAME, HOSTNAME, Long.MAX_VALUE, channel.eventLoop()); + } else { + String lastName = HOSTNAME; + for (int i = 1; i < chainLength; i++) { + String nextName = i + "." + lastName; + cache.cache(lastName, nextName, Long.MAX_VALUE, channel.eventLoop()); + lastName = nextName; + } + cache.cache(lastName, HOSTNAME, Long.MAX_VALUE, channel.eventLoop()); + } + return cache; + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java new file mode 100644 index 0000000..f2bfcc1 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressStreamProvidersTest.java @@ -0,0 +1,29 @@ +/* + * Copyright 2020 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: + * + * https://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 org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertSame; + +public class DnsServerAddressStreamProvidersTest { + + @Test + public void testUseCorrectProvider() { + assertSame(DnsServerAddressStreamProviders.unixDefault(), + DnsServerAddressStreamProviders.platformDefault()); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java new file mode 100644 index 0000000..8b19f57 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/DnsServerAddressesTest.java @@ -0,0 +1,127 @@ +/* + * Copyright 2014 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: + * + * https://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 org.junit.jupiter.api.Test; + +import java.net.InetSocketAddress; +import java.util.Collections; +import java.util.IdentityHashMap; +import java.util.Set; + +import static io.netty.resolver.dns.DefaultDnsServerAddressStreamProvider.defaultAddressList; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class DnsServerAddressesTest { + + private static final InetSocketAddress ADDR1 = new InetSocketAddress(NetUtil.LOCALHOST, 1); + private static final InetSocketAddress ADDR2 = new InetSocketAddress(NetUtil.LOCALHOST, 2); + private static final InetSocketAddress ADDR3 = new InetSocketAddress(NetUtil.LOCALHOST, 3); + + @Test + public void testDefaultAddresses() { + assertThat(defaultAddressList().size(), is(greaterThan(0))); + } + + @Test + public void testSequential() { + DnsServerAddresses seq = DnsServerAddresses.sequential(ADDR1, ADDR2, ADDR3); + assertThat(seq.stream(), is(not(sameInstance(seq.stream())))); + + for (int j = 0; j < 2; j ++) { + DnsServerAddressStream i = seq.stream(); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + } + } + + @Test + public void testRotational() { + DnsServerAddresses seq = DnsServerAddresses.rotational(ADDR1, ADDR2, ADDR3); + + DnsServerAddressStream i = seq.stream(); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + + i = seq.stream(); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + + i = seq.stream(); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + + i = seq.stream(); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + assertNext(i, ADDR1); + assertNext(i, ADDR2); + assertNext(i, ADDR3); + } + + @Test + public void testShuffled() { + DnsServerAddresses seq = DnsServerAddresses.shuffled(ADDR1, ADDR2, ADDR3); + + // Ensure that all three addresses are returned by the iterator. + // In theory, this test can fail at extremely low chance, but we don't really care. + Set set = Collections.newSetFromMap(new IdentityHashMap()); + DnsServerAddressStream i = seq.stream(); + for (int j = 0; j < 1048576; j ++) { + set.add(i.next()); + } + + assertThat(set.size(), is(3)); + assertThat(seq.stream(), is(not(sameInstance(seq.stream())))); + } + + @Test + public void testSingleton() { + DnsServerAddresses seq = DnsServerAddresses.singleton(ADDR1); + + // Should return the same iterator instance for least possible footprint. + assertThat(seq.stream(), is(sameInstance(seq.stream()))); + + DnsServerAddressStream i = seq.stream(); + assertNext(i, ADDR1); + assertNext(i, ADDR1); + assertNext(i, ADDR1); + } + + private static void assertNext(DnsServerAddressStream i, InetSocketAddress addr) { + assertThat(i.next(), is(sameInstance(addr))); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/NameServerComparatorTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/NameServerComparatorTest.java new file mode 100644 index 0000000..fdb10be --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/NameServerComparatorTest.java @@ -0,0 +1,171 @@ +/* + * Copyright 2018 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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 org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.net.Inet4Address; +import java.net.Inet6Address; +import java.net.InetAddress; +import java.net.InetSocketAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class NameServerComparatorTest { + + private static InetSocketAddress IPV4ADDRESS1; + private static InetSocketAddress IPV4ADDRESS2; + private static InetSocketAddress IPV4ADDRESS3; + + private static InetSocketAddress IPV6ADDRESS1; + private static InetSocketAddress IPV6ADDRESS2; + + private static InetSocketAddress UNRESOLVED1; + private static InetSocketAddress UNRESOLVED2; + private static InetSocketAddress UNRESOLVED3; + + @BeforeAll + public static void before() throws UnknownHostException { + IPV4ADDRESS1 = new InetSocketAddress(InetAddress.getByAddress("ns1", new byte[] { 10, 0, 0, 1 }), 53); + IPV4ADDRESS2 = new InetSocketAddress(InetAddress.getByAddress("ns2", new byte[] { 10, 0, 0, 2 }), 53); + IPV4ADDRESS3 = new InetSocketAddress(InetAddress.getByAddress("ns3", new byte[] { 10, 0, 0, 3 }), 53); + + IPV6ADDRESS1 = new InetSocketAddress(InetAddress.getByAddress( + "ns1", new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1 }), 53); + IPV6ADDRESS2 = new InetSocketAddress(InetAddress.getByAddress( + "ns2", new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2 }), 53); + + UNRESOLVED1 = InetSocketAddress.createUnresolved("ns3", 53); + UNRESOLVED2 = InetSocketAddress.createUnresolved("ns4", 53); + UNRESOLVED3 = InetSocketAddress.createUnresolved("ns5", 53); + } + + @Test + public void testCompareResolvedOnly() { + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + int x = comparator.compare(IPV4ADDRESS1, IPV6ADDRESS1); + int y = comparator.compare(IPV6ADDRESS1, IPV4ADDRESS1); + + assertEquals(-1, x); + assertEquals(x, -y); + + assertEquals(0, comparator.compare(IPV4ADDRESS1, IPV4ADDRESS1)); + assertEquals(0, comparator.compare(IPV6ADDRESS1, IPV6ADDRESS1)); + } + + @Test + public void testCompareUnresolvedSimple() { + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + int x = comparator.compare(IPV4ADDRESS1, UNRESOLVED1); + int y = comparator.compare(UNRESOLVED1, IPV4ADDRESS1); + + assertEquals(-1, x); + assertEquals(x, -y); + assertEquals(0, comparator.compare(IPV4ADDRESS1, IPV4ADDRESS1)); + assertEquals(0, comparator.compare(UNRESOLVED1, UNRESOLVED1)); + } + + @Test + public void testCompareUnresolvedOnly() { + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + int x = comparator.compare(UNRESOLVED1, UNRESOLVED2); + int y = comparator.compare(UNRESOLVED2, UNRESOLVED1); + + assertEquals(0, x); + assertEquals(x, -y); + + assertEquals(0, comparator.compare(UNRESOLVED1, UNRESOLVED1)); + assertEquals(0, comparator.compare(UNRESOLVED2, UNRESOLVED2)); + } + + @Test + public void testSortAlreadySortedPreferred() { + List expected = Arrays.asList(IPV4ADDRESS1, IPV4ADDRESS2, IPV4ADDRESS3); + List addresses = new ArrayList(expected); + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + + Collections.sort(addresses, comparator); + + assertEquals(expected, addresses); + } + + @Test + public void testSortAlreadySortedNotPreferred() { + List expected = Arrays.asList(IPV4ADDRESS1, IPV4ADDRESS2, IPV4ADDRESS3); + List addresses = new ArrayList(expected); + NameServerComparator comparator = new NameServerComparator(Inet6Address.class); + + Collections.sort(addresses, comparator); + + assertEquals(expected, addresses); + } + + @Test + public void testSortAlreadySortedUnresolved() { + List expected = Arrays.asList(UNRESOLVED1, UNRESOLVED2, UNRESOLVED3); + List addresses = new ArrayList(expected); + NameServerComparator comparator = new NameServerComparator(Inet6Address.class); + + Collections.sort(addresses, comparator); + + assertEquals(expected, addresses); + } + + @Test + public void testSortAlreadySortedMixed() { + List expected = Arrays.asList( + IPV4ADDRESS1, IPV4ADDRESS2, IPV6ADDRESS1, IPV6ADDRESS2, UNRESOLVED1, UNRESOLVED2); + + List addresses = new ArrayList(expected); + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + + Collections.sort(addresses, comparator); + + assertEquals(expected, addresses); + } + + @Test + public void testSort1() { + List expected = Arrays.asList( + IPV4ADDRESS1, IPV4ADDRESS2, IPV6ADDRESS1, IPV6ADDRESS2, UNRESOLVED1, UNRESOLVED2); + List addresses = new ArrayList( + Arrays.asList(IPV6ADDRESS1, IPV4ADDRESS1, IPV6ADDRESS2, UNRESOLVED1, UNRESOLVED2, IPV4ADDRESS2)); + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + + Collections.sort(addresses, comparator); + + assertEquals(expected, addresses); + } + + @Test + public void testSort2() { + List expected = Arrays.asList( + IPV4ADDRESS1, IPV4ADDRESS2, IPV6ADDRESS1, IPV6ADDRESS2, UNRESOLVED1, UNRESOLVED2); + List addresses = new ArrayList( + Arrays.asList(IPV4ADDRESS1, IPV6ADDRESS1, IPV6ADDRESS2, UNRESOLVED1, IPV4ADDRESS2, UNRESOLVED2)); + NameServerComparator comparator = new NameServerComparator(Inet4Address.class); + + Collections.sort(addresses, comparator); + + assertEquals(expected, addresses); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java new file mode 100644 index 0000000..4278553 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/PreferredAddressTypeComparatorTest.java @@ -0,0 +1,71 @@ +/* + * Copyright 2019 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: + * + * https://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.socket.InternetProtocolFamily; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class PreferredAddressTypeComparatorTest { + + @Test + public void testIpv4() throws UnknownHostException { + InetAddress ipv4Address1 = InetAddress.getByName("10.0.0.1"); + InetAddress ipv4Address2 = InetAddress.getByName("10.0.0.2"); + InetAddress ipv4Address3 = InetAddress.getByName("10.0.0.3"); + InetAddress ipv6Address1 = InetAddress.getByName("::1"); + InetAddress ipv6Address2 = InetAddress.getByName("::2"); + InetAddress ipv6Address3 = InetAddress.getByName("::3"); + + PreferredAddressTypeComparator ipv4 = PreferredAddressTypeComparator.comparator(InternetProtocolFamily.IPv4); + + List addressList = new ArrayList(); + Collections.addAll(addressList, ipv4Address1, ipv4Address2, ipv6Address1, + ipv6Address2, ipv4Address3, ipv6Address3); + Collections.sort(addressList, ipv4); + + assertEquals(Arrays.asList(ipv4Address1, ipv4Address2, ipv4Address3, ipv6Address1, + ipv6Address2, ipv6Address3), addressList); + } + + @Test + public void testIpv6() throws UnknownHostException { + InetAddress ipv4Address1 = InetAddress.getByName("10.0.0.1"); + InetAddress ipv4Address2 = InetAddress.getByName("10.0.0.2"); + InetAddress ipv4Address3 = InetAddress.getByName("10.0.0.3"); + InetAddress ipv6Address1 = InetAddress.getByName("::1"); + InetAddress ipv6Address2 = InetAddress.getByName("::2"); + InetAddress ipv6Address3 = InetAddress.getByName("::3"); + + PreferredAddressTypeComparator ipv4 = PreferredAddressTypeComparator.comparator(InternetProtocolFamily.IPv6); + + List addressList = new ArrayList(); + Collections.addAll(addressList, ipv4Address1, ipv4Address2, ipv6Address1, + ipv6Address2, ipv4Address3, ipv6Address3); + Collections.sort(addressList, ipv4); + + assertEquals(Arrays.asList(ipv6Address1, + ipv6Address2, ipv6Address3, ipv4Address1, ipv4Address2, ipv4Address3), addressList); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java new file mode 100644 index 0000000..98b0a68 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/SearchDomainTest.java @@ -0,0 +1,315 @@ +/* + * 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: + * + * https://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.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.net.InetAddress; +import java.net.UnknownHostException; +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.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.instanceOf; +import static org.hamcrest.Matchers.not; +import static org.hamcrest.core.StringContains.containsString; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class SearchDomainTest { + + private DnsNameResolverBuilder newResolver() { + return new DnsNameResolverBuilder(group.next()) + .channelType(NioDatagramChannel.class) + .nameServerProvider(new SingletonDnsServerAddressStreamProvider(dnsServer.localAddress())) + .maxQueriesPerResolve(1) + .optResourceEnabled(false) + .ndots(1); + } + + private TestDnsServer dnsServer; + private EventLoopGroup group; + private DnsNameResolver resolver; + + @BeforeEach + public void before() { + group = new NioEventLoopGroup(1); + } + + @AfterEach + public void destroy() { + if (dnsServer != null) { + dnsServer.stop(); + dnsServer = null; + } + if (resolver != null) { + resolver.close(); + } + 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(); + + resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).ndots(2).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 nor it's absolute but it should still be resolved after search list have + // been checked + resolved = assertResolve(resolver, "host3"); + assertEquals(store.getAddress("host3"), resolved); + + // "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" would have been directly resolved but since it has less than ndots the "foo.com" search domain + // is used. + resolved = assertResolve(resolver, "host5.sub"); + assertEquals(store.getAddress("host5.sub.foo.com"), 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(); + + resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).ndots(2).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 nor it's absolute but it should still be resolved after search list have + // been checked + resolved = assertResolveAll(resolver, "host3"); + assertEquals(store.getAddresses("host3"), resolved); + + // "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" would have been directly resolved but since it has less than ndots the "foo.com" search domain + // is used. + resolved = assertResolveAll(resolver, "host5.sub"); + assertEquals(store.getAddresses("host5.sub.foo.com"), 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(); + + 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 "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(); + + 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); + } + + @Test + public void testSearchDomainWithNdots0() throws Exception { + Set domains = new HashSet(); + domains.add("host1"); + domains.add("host1.foo.com"); + domains.add("host2.foo.com"); + + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(domains); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + resolver = newResolver().searchDomains(Collections.singleton("foo.com")).ndots(0).build(); + + // "host1" resolves directly as ndots = 0 + String resolved = assertResolve(resolver, "host1"); + assertEquals(store.getAddress("host1"), resolved); + + // "host1.foo.com" resolves to host1.foo + resolved = assertResolve(resolver, "host1.foo.com"); + assertEquals(store.getAddress("host1.foo.com"), resolved); + + // "host2" shouldn't resolve because it is not in the known domain names, and "host2" has 0 dots which is not + // less ndots (which is also 0). + assertNotResolve(resolver, "host2"); + } + + private static void assertNotResolve(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future fut = resolver.resolve(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + assertFalse(fut.isSuccess()); + } + + private static void assertNotResolveAll(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future> fut = resolver.resolveAll(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + assertFalse(fut.isSuccess()); + } + + private static String assertResolve(DnsNameResolver resolver, String inetHost) throws InterruptedException { + Future fut = resolver.resolve(inetHost); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + return fut.getNow().getHostAddress(); + } + + private static 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; + } + + @Test + public void testExceptionMsgContainsSearchDomain() throws Exception { + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(Collections.emptySet()); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).ndots(1).build(); + + Future fut = resolver.resolve("unknown.hostname"); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + assertFalse(fut.isSuccess()); + final Throwable cause = fut.cause(); + assertThat(cause, instanceOf(UnknownHostException.class)); + assertThat("search domain is included in UnknownHostException", cause.getMessage(), + containsString("foo.com")); + } + + @Test + public void testExceptionMsgDoesNotContainSearchDomainIfNdotsIsNotReached() throws Exception { + TestDnsServer.MapRecordStoreA store = new TestDnsServer.MapRecordStoreA(Collections.emptySet()); + dnsServer = new TestDnsServer(store); + dnsServer.start(); + + resolver = newResolver().searchDomains(Collections.singletonList("foo.com")).ndots(2).build(); + + Future fut = resolver.resolve("unknown.hostname"); + assertTrue(fut.await(10, TimeUnit.SECONDS)); + assertFalse(fut.isSuccess()); + final Throwable cause = fut.cause(); + assertThat(cause, instanceOf(UnknownHostException.class)); + assertThat("search domain is included in UnknownHostException", cause.getMessage(), + not(containsString("foo.com"))); + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java new file mode 100644 index 0000000..dad1c68 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/TestDnsServer.java @@ -0,0 +1,371 @@ +/* + * 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: + * + * https://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.PlatformDependent; +import org.apache.directory.server.dns.DnsServer; +import org.apache.directory.server.dns.io.decoder.DnsMessageDecoder; +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.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.ProtocolDecoderAdapter; +import org.apache.mina.filter.codec.ProtocolDecoderOutput; +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.Locale; +import java.util.Map; +import java.util.Set; + +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[0]); + } + + private final RecordStore store; + + TestDnsServer(Set domains) { + this(new TestRecordStore(domains)); + } + + TestDnsServer(RecordStore store) { + this.store = store; + } + + @Override + public void start() throws IOException { + start(null); + } + + /** + * Start the {@link TestDnsServer} but drop all {@link RecordType} queries + * and not send any response to these at all. + */ + public void start(final RecordType dropRecordType) throws IOException { + start(dropRecordType, new InetSocketAddress(NetUtil.LOCALHOST4, 0)); + } + + public void start(final RecordType dropRecordType, InetSocketAddress address) throws IOException { + 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) { + // USe our own codec to support AAAA testing + session.getFilterChain() + .addFirst("codec", new ProtocolCodecFilter( + new TestDnsProtocolUdpCodecFactory(dropRecordType))); + } + }); + + ((DatagramSessionConfig) acceptor.getSessionConfig()).setReuseAddress(true); + + // Start the listener + acceptor.bind(); + } + + public InetSocketAddress localAddress() { + return (InetSocketAddress) getTransports()[0].getAcceptor().getLocalAddress(); + } + + protected DnsMessage filterMessage(DnsMessage message) { + return message; + } + + protected static ResourceRecord newARecord(String name, String ipAddress) { + return newAddressRecord(name, RecordType.A, ipAddress); + } + + protected static ResourceRecord newNsRecord(String dnsname, String domainName) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(dnsname); + rm.setDnsTtl(100); + rm.setDnsType(RecordType.NS); + rm.put(DnsAttribute.DOMAIN_NAME, domainName); + return rm.getEntry(); + } + + protected static ResourceRecord newAddressRecord(String name, RecordType type, String address) { + ResourceRecordModifier rm = new ResourceRecordModifier(); + rm.setDnsClass(RecordClass.IN); + rm.setDnsName(name); + rm.setDnsTtl(100); + rm.setDnsType(type); + rm.put(DnsAttribute.IP_ADDRESS, address); + return rm.getEntry(); + } + + /** + * {@link ProtocolCodecFactory} which allows to test AAAA resolution. + */ + private final class TestDnsProtocolUdpCodecFactory implements ProtocolCodecFactory { + private final DnsMessageEncoder encoder = new DnsMessageEncoder(); + private final TestAAAARecordEncoder recordEncoder = new TestAAAARecordEncoder(); + private final RecordType dropRecordType; + + TestDnsProtocolUdpCodecFactory(RecordType dropRecordType) { + this.dropRecordType = dropRecordType; + } + + @Override + public ProtocolEncoder getEncoder(IoSession session) { + return new DnsUdpEncoder() { + + @Override + public void encode(IoSession session, Object message, ProtocolEncoderOutput out) { + IoBuffer buf = IoBuffer.allocate(4096); + DnsMessage dnsMessage = filterMessage((DnsMessage) message); + if (dnsMessage != null) { + encoder.encode(buf, dnsMessage); + + encodeAAAA(dnsMessage.getAnswerRecords(), buf); + encodeAAAA(dnsMessage.getAuthorityRecords(), buf); + encodeAAAA(dnsMessage.getAdditionalRecords(), buf); + buf.flip(); + + out.write(buf); + } + } + }; + } + + private void encodeAAAA(List records, IoBuffer out) { + for (ResourceRecord record : records) { + // 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(out, record); + } catch (IOException e) { + // Should never happen + throw new IllegalStateException(e); + } + } + } + } + + @Override + public ProtocolDecoder getDecoder(IoSession session) { + return new ProtocolDecoderAdapter() { + private final DnsMessageDecoder decoder = new DnsMessageDecoder(); + + @Override + public void decode(IoSession session, IoBuffer in, ProtocolDecoderOutput out) throws IOException { + DnsMessage message = decoder.decode(in); + if (dropRecordType != null) { + for (QuestionRecord record: message.getQuestionRecords()) { + if (record.getRecordType() == dropRecordType) { + return; + } + } + } + out.write(message); + } + }; + } + + private 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(resourceRecord.get(DnsAttribute.IP_ADDRESS)); + } + // encode the ::1 + ioBuffer.put(bytes); + } + } + } + + static final class MapRecordStoreA implements RecordStore { + + private final Map> domainMap; + + 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); + } + } + + 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) { + String name = questionRecord.getDomainName(); + List addresses = domainMap.get(name); + if (addresses != null && questionRecord.getRecordType() == RecordType.A) { + Set records = new LinkedHashSet(); + for (String address : addresses) { + Map attributes = new HashMap(); + attributes.put(DnsAttribute.IP_ADDRESS.toLowerCase(), address); + records.add(new TestResourceRecord(name, questionRecord.getRecordType(), attributes)); + } + 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(PlatformDependent.threadLocalRandom().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; + + private TestRecordStore(Set domains) { + this.domains = domains; + } + + @Override + public Set getRecords(QuestionRecord questionRecord) { + String name = questionRecord.getDomainName(); + if (domains.contains(name)) { + Map attr = new HashMap(); + switch (questionRecord.getRecordType()) { + case A: + do { + attr.put(DnsAttribute.IP_ADDRESS.toLowerCase(Locale.US), nextIp()); + } while (PlatformDependent.threadLocalRandom().nextBoolean()); + break; + case AAAA: + do { + attr.put(DnsAttribute.IP_ADDRESS.toLowerCase(Locale.US), nextIp6()); + } while (PlatformDependent.threadLocalRandom().nextBoolean()); + break; + case MX: + int priority = 0; + do { + attr.put(DnsAttribute.DOMAIN_NAME.toLowerCase(Locale.US), nextDomain()); + attr.put(DnsAttribute.MX_PREFERENCE.toLowerCase(Locale.US), String.valueOf(++priority)); + } while (PlatformDependent.threadLocalRandom().nextBoolean()); + break; + default: + return null; + } + return Collections.singleton( + new TestResourceRecord(name, questionRecord.getRecordType(), attr)); + } + return null; + } + } + + static final class TestResourceRecord extends ResourceRecordImpl { + + TestResourceRecord(String domainName, RecordType recordType, Map attributes) { + super(domainName, recordType, RecordClass.IN, 100, attributes); + } + + @Override + public int hashCode() { + return System.identityHashCode(this); + } + + @Override + public boolean equals(Object o) { + return o == this; + } + } +} diff --git a/netty-resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java new file mode 100644 index 0000000..f000359 --- /dev/null +++ b/netty-resolver-dns/src/test/java/io/netty/resolver/dns/UnixResolverDnsServerAddressStreamProviderTest.java @@ -0,0 +1,306 @@ +/* + * Copyright 2017 The Netty Project + * + * The Netty Project licenses this file to you under the Apache License, + * version 2.0 (the "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at: + * + * https://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.CharsetUtil; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.File; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.UUID; + +import static io.netty.resolver.dns.UnixResolverDnsServerAddressStreamProvider.parseEtcResolverOptions; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class UnixResolverDnsServerAddressStreamProviderTest { + @Test + public void defaultLookupShouldReturnResultsIfOnlySingleFileSpecified(@TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2\n" + + "nameserver 127.0.0.3\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + } + + @Test + public void nameServerAddressStreamShouldBeRotationalWhenRotationOptionsIsPresent( + @TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "options rotate\n" + + "domain linecorp.local\n" + + "nameserver 127.0.0.2\n" + + "nameserver 127.0.0.3\n" + + "nameserver 127.0.0.4\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + assertHostNameEquals("127.0.0.4", stream.next()); + + stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.3", stream.next()); + assertHostNameEquals("127.0.0.4", stream.next()); + assertHostNameEquals("127.0.0.2", stream.next()); + + stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.4", stream.next()); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + + stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + assertHostNameEquals("127.0.0.4", stream.next()); + } + + @Test + public void nameServerAddressStreamShouldAlwaysStartFromTheTopWhenRotationOptionsIsAbsent( + @TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2\n" + + "nameserver 127.0.0.3\n" + + "nameserver 127.0.0.4\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + assertHostNameEquals("127.0.0.4", stream.next()); + + stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + assertHostNameEquals("127.0.0.4", stream.next()); + + stream = p.nameServerAddressStream(""); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + assertHostNameEquals("127.0.0.4", stream.next()); + } + + @Test + public void defaultReturnedWhenNoBetterMatch(@TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2\n" + + "nameserver 127.0.0.3\n"); + File f2 = buildFile(tempDir, "domain squarecorp.local\n" + + "nameserver 127.0.0.4\n" + + "nameserver 127.0.0.5\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, f2); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertHostNameEquals("127.0.0.2", stream.next()); + assertHostNameEquals("127.0.0.3", stream.next()); + } + + @Test + public void moreRefinedSelectionReturnedWhenMatch(@TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2\n" + + "nameserver 127.0.0.3\n"); + File f2 = buildFile(tempDir, "domain dc1.linecorp.local\n" + + "nameserver 127.0.0.4\n" + + "nameserver 127.0.0.5\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, f2); + + DnsServerAddressStream stream = p.nameServerAddressStream("myhost.dc1.linecorp.local"); + assertHostNameEquals("127.0.0.4", stream.next()); + assertHostNameEquals("127.0.0.5", stream.next()); + } + + @Test + public void ndotsOptionIsParsedIfPresent(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n" + + "options ndots:0\n"); + assertEquals(0, parseEtcResolverOptions(f).ndots()); + + f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n" + + "options ndots:123 foo:goo\n"); + assertEquals(123, parseEtcResolverOptions(f).ndots()); + } + + @Test + public void defaultValueReturnedIfNdotsOptionsNotPresent(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n"); + assertEquals(1, parseEtcResolverOptions(f).ndots()); + } + + @Test + public void timeoutOptionIsParsedIfPresent(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n" + + "options timeout:0\n"); + assertEquals(0, parseEtcResolverOptions(f).timeout()); + + f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n" + + "options foo:bar timeout:124\n"); + assertEquals(124, parseEtcResolverOptions(f).timeout()); + } + + @Test + public void defaultValueReturnedIfTimeoutOptionsIsNotPresent(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n"); + assertEquals(5, parseEtcResolverOptions(f).timeout()); + } + + @Test + public void attemptsOptionIsParsedIfPresent(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n" + + "options attempts:0\n"); + assertEquals(0, parseEtcResolverOptions(f).attempts()); + + f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n" + + "options foo:bar attempts:12\n"); + assertEquals(12, parseEtcResolverOptions(f).attempts()); + } + + @Test + public void defaultValueReturnedIfAttemptsOptionsIsNotPresent(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 127.0.0.11\n"); + assertEquals(16, parseEtcResolverOptions(f).attempts()); + } + + @Test + public void emptyEtcResolverDirectoryDoesNotThrow(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2\n" + + "nameserver 127.0.0.3\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, tempDir.resolve("netty-empty").toFile().listFiles()); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertHostNameEquals("127.0.0.2", stream.next()); + } + + @Test + public void searchDomainsWithOnlyDomain(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2\n"); + List domains = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(f); + assertEquals(Collections.singletonList("linecorp.local"), domains); + } + + @Test + public void searchDomainsWithOnlySearch(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search linecorp.local\n" + + "nameserver 127.0.0.2\n"); + List domains = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(f); + assertEquals(Collections.singletonList("linecorp.local"), domains); + } + + @Test + public void searchDomainsWithMultipleSearch(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search linecorp.local\n" + + "search squarecorp.local\n" + + "nameserver 127.0.0.2\n"); + List domains = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(f); + assertEquals(Arrays.asList("linecorp.local", "squarecorp.local"), domains); + } + + @Test + public void searchDomainsWithMultipleSearchSeperatedByWhitespace(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search linecorp.local squarecorp.local\n" + + "nameserver 127.0.0.2\n"); + List domains = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(f); + assertEquals(Arrays.asList("linecorp.local", "squarecorp.local"), domains); + } + + @Test + public void searchDomainsWithMultipleSearchSeperatedByTab(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "search linecorp.local\tsquarecorp.local\n" + + "nameserver 127.0.0.2\n"); + List domains = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(f); + assertEquals(Arrays.asList("linecorp.local", "squarecorp.local"), domains); + } + + @Test + public void searchDomainsPrecedence(@TempDir Path tempDir) throws IOException { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "search squarecorp.local\n" + + "nameserver 127.0.0.2\n"); + List domains = UnixResolverDnsServerAddressStreamProvider.parseEtcResolverSearchDomains(f); + assertEquals(Collections.singletonList("squarecorp.local"), domains); + } + + @Test + public void ignoreInvalidEntries(@TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "domain netty.local\n" + + "nameserver nil\n" + + "nameserver 127.0.0.3\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertEquals(1, stream.size()); + assertHostNameEquals("127.0.0.3", stream.next()); + } + + private File buildFile(Path tempDir, String contents) throws IOException { + Path path = tempDir.resolve("netty-dns-" + UUID.randomUUID().toString().substring(24) + ".txt"); + Files.write(path, contents.getBytes(CharsetUtil.UTF_8)); + return path.toFile(); + } + + @Test + public void ignoreComments(@TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "domain linecorp.local\n" + + "nameserver 127.0.0.2 #somecomment\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertHostNameEquals("127.0.0.2", stream.next()); + } + + @Test + public void ipv6Nameserver(@TempDir Path tempDir) throws Exception { + File f = buildFile(tempDir, "search localdomain\n" + + "nameserver 10.211.55.1\n" + + "nameserver fe80::21c:42ff:fe00:18%nonexisting\n"); + UnixResolverDnsServerAddressStreamProvider p = + new UnixResolverDnsServerAddressStreamProvider(f, null); + + DnsServerAddressStream stream = p.nameServerAddressStream("somehost"); + assertHostNameEquals("10.211.55.1", stream.next()); + } + + private static void assertHostNameEquals(String expectedHostname, InetSocketAddress next) { + assertEquals(expectedHostname, next.getHostString(), "unexpected hostname: " + next); + } +} diff --git a/netty-resolver-dns/src/test/resources/logging.properties b/netty-resolver-dns/src/test/resources/logging.properties new file mode 100644 index 0000000..3cd7309 --- /dev/null +++ b/netty-resolver-dns/src/test/resources/logging.properties @@ -0,0 +1,7 @@ +handlers=java.util.logging.ConsoleHandler +.level=ALL +java.util.logging.SimpleFormatter.format=%1$tY-%1$tm-%1$td %1$tH:%1$tM:%1$tS.%1$tL %4$-7s [%3$s] %5$s %6$s%n +java.util.logging.ConsoleHandler.level=ALL +java.util.logging.ConsoleHandler.formatter=java.util.logging.SimpleFormatter +jdk.event.security.level=INFO +org.junit.jupiter.engine.execution.ConditionEvaluator.level=OFF diff --git a/netty-util/src/main/java/module-info.java b/netty-util/src/main/java/module-info.java index 7590c6c..f615ac3 100644 --- a/netty-util/src/main/java/module-info.java +++ b/netty-util/src/main/java/module-info.java @@ -11,6 +11,7 @@ module org.xbib.io.netty.util { org.xbib.io.netty.handler, org.xbib.io.netty.handler.codec, org.xbib.io.netty.handler.codec.compression, + org.xbib.io.netty.handler.codec.dns, org.xbib.io.netty.handler.codec.http, org.xbib.io.netty.handler.codec.httptwo, org.xbib.io.netty.handler.codec.httpthree, @@ -21,6 +22,7 @@ module org.xbib.io.netty.util { org.xbib.io.netty.handler.ssl, org.xbib.io.netty.handler.ssl.bouncycastle, org.xbib.io.netty.resolver, + org.xbib.io.netty.resolver.dns, org.xbib.io.netty.testsuite; exports io.netty.util.internal.logging to org.xbib.io.netty.buffer, @@ -31,6 +33,7 @@ module org.xbib.io.netty.util { org.xbib.io.netty.handler, org.xbib.io.netty.handler.codec, org.xbib.io.netty.handler.codec.compression, + org.xbib.io.netty.handler.codec.dns, org.xbib.io.netty.handler.codec.http, org.xbib.io.netty.handler.codec.httptwo, org.xbib.io.netty.handler.codec.httpthree, @@ -41,6 +44,7 @@ module org.xbib.io.netty.util { org.xbib.io.netty.handler.ssl, org.xbib.io.netty.handler.ssl.bouncycastle, org.xbib.io.netty.resolver, + org.xbib.io.netty.resolver.dns, org.xbib.io.netty.testsuite; requires org.xbib.io.netty.jctools; requires java.logging; diff --git a/settings.gradle b/settings.gradle index 85840a6..fad8554 100644 --- a/settings.gradle +++ b/settings.gradle @@ -51,6 +51,7 @@ dependencyResolutionManagement { library('commons-compress', 'org.apache.commons', 'commons-compress').version('1.25.0') library('xz-tools', 'org.tukaani', 'xz').version('1.9') library('rerunner-jupiter', 'io.github.artsok', 'rerunner-jupiter').version('2.1.6') + library('apache-ds-dns', 'org.apache.directory.server', 'apacheds-protocol-dns').version('2.0.0.AM27') } } } @@ -66,6 +67,7 @@ include 'netty-channel-unix-native' include 'netty-handler' include 'netty-handler-codec' include 'netty-handler-codec-compression' +include 'netty-handler-codec-dns' include 'netty-handler-codec-http' include 'netty-handler-codec-http2' include 'netty-handler-codec-http3' @@ -79,6 +81,7 @@ include 'netty-handler-ssl-bouncycastle' include 'netty-internal-tcnative' include 'netty-jctools' include 'netty-resolver' +include 'netty-resolver-dns' include 'netty-tcnative-boringssl-static-native' include 'netty-testsuite' include 'netty-util'