Skip to content

Commit

Permalink
Add TcpDnsQueryDecoder and TcpDnsResponseEncoder (netty#11415)
Browse files Browse the repository at this point in the history
Motivation:
There is no decoder and encoder for TCP based DNS.

Result:
- Added decoder and encoder
- Added tests
- Added example

Result:

Be able to decode and encode TCP based dns 

Co-authored-by: Norman Maurer <norman_maurer@apple.com>
  • Loading branch information
RockyLOMO and normanmaurer committed Jul 28, 2021
1 parent c83c5b7 commit ccef8fe
Show file tree
Hide file tree
Showing 7 changed files with 490 additions and 60 deletions.
Expand Up @@ -51,9 +51,16 @@ public DatagramDnsQueryDecoder(DnsRecordDecoder recordDecoder) {
}

@Override
protected void decode(ChannelHandlerContext ctx, DatagramPacket packet, List<Object> out) throws Exception {
protected void decode(ChannelHandlerContext ctx, final DatagramPacket packet, List<Object> out) throws Exception {
final ByteBuf buf = packet.content();

DnsMessageUtil.decodeDnsQuery(recordDecoder, buf, new DnsMessageUtil.DnsQueryFactory() {
@Override
public DnsQuery newQuery(int id, DnsOpCode dnsOpCode) {
return new DatagramDnsQuery(packet.sender(), packet.recipient(), id, dnsOpCode);
}
});

final DnsQuery query = newQuery(packet, buf);
boolean success = false;
try {
Expand Down
Expand Up @@ -61,19 +61,7 @@ protected void encode(ChannelHandlerContext ctx,
final DnsResponse response = in.content();
final ByteBuf buf = allocateBuffer(ctx, in);

boolean success = false;
try {
encodeHeader(response, buf);
encodeQuestions(response, buf);
encodeRecords(response, DnsSection.ANSWER, buf);
encodeRecords(response, DnsSection.AUTHORITY, buf);
encodeRecords(response, DnsSection.ADDITIONAL, buf);
success = true;
} finally {
if (!success) {
buf.release();
}
}
DnsMessageUtil.encodeDnsResponse(recordEncoder, response, buf);

out.add(new DatagramPacket(buf, recipient, null));
}
Expand All @@ -87,49 +75,4 @@ protected ByteBuf allocateBuffer(
@SuppressWarnings("unused") AddressedEnvelope<DnsResponse, InetSocketAddress> msg) throws Exception {
return ctx.alloc().ioBuffer(1024);
}

/**
* 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 void encodeQuestions(DnsResponse response, ByteBuf buf) throws Exception {
final int count = response.count(DnsSection.QUESTION);
for (int i = 0; i < count; i++) {
recordEncoder.encodeQuestion((DnsQuestion) response.recordAt(DnsSection.QUESTION, i), buf);
}
}

private void encodeRecords(DnsResponse response, DnsSection section, ByteBuf buf) throws Exception {
final int count = response.count(section);
for (int i = 0; i < count; i++) {
recordEncoder.encodeRecord(response.recordAt(section, i), buf);
}
}
}
Expand Up @@ -15,7 +15,9 @@
*/
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;
Expand Down Expand Up @@ -177,5 +179,124 @@ private static void appendRecords(StringBuilder buf, DnsMessage message, DnsSect
}
}

private DnsMessageUtil() { }
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.<DnsQuestion>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() {
}
}
@@ -0,0 +1,55 @@
/*
* 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;

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);
}
});
}
}
@@ -0,0 +1,54 @@
/*
* 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 java.util.List;

@ChannelHandler.Sharable
public final class TcpDnsResponseEncoder extends MessageToMessageEncoder<DnsResponse> {
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<Object> 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);
}
}

0 comments on commit ccef8fe

Please sign in to comment.