Introduction

The full name of DNS is domain name system. Since it is a system, there are clients and servers. Generally speaking, we do not need to perceive the existence of this DNS client, because when we access a certain domain name in the browser, the browser has already realized this work as a client.

But sometimes we don't use a browser, such as in a netty environment, how to construct a DNS request?

Introduction to DNS Transport Protocol

In the RFC specification, there are many kinds of DNS transport protocols, as follows:

  • DNS-over-UDP/53, referred to as "Do53", is a protocol that uses UDP for DNS query transmission.
  • DNS-over-TCP/53, referred to as "Do53/TCP", is a protocol that uses TCP for DNS query transmission.
  • DNSCrypt, a method for encrypting the DNS transfer protocol.
  • DNS-over-TLS is referred to as "DoT", which uses TLS for DNS protocol transmission.
  • DNS-over-HTTPS is referred to as "DoH", which uses HTTPS for DNS protocol transmission.
  • DNS-over-TOR, use VPN or tunnels to connect DNS.

These protocols have corresponding implementation methods. Let's first look at Do53/TCP, which is to use TCP for DNS protocol transmission.

DNS IP address

Let's first consider how to use the Do53/TCP protocol in netty to perform DNS queries.

Because DNS is a client and server model, what we need to do is to build a DNS client to query the known DNS server.

What are the known DNS server addresses?

In addition to the 13 root DNS IP addresses, there are also many free public DNS server addresses, such as our commonly used Ali DNS, which also provides IPv4/IPv6 DNS and DoT/DoH services.

 IPv4: 
223.5.5.5

223.6.6.6

IPv6: 
2400:3200::1

2400:3200:baba::1

DoH 地址: 
https://dns.alidns.com/dns-query

DoT 地址: 
dns.alidns.com

Another example is Baidu DNS, which provides a set of IPv4 and IPv6 addresses:

 IPv4: 
180.76.76.76

IPv6: 
2400:da00::6666

And 114DNS:

 114.114.114.114
114.114.115.115

Of course, there are many other public free DNS, here I choose to use Ali's IPv4: 223.5.5.5 as an example.

With the IP address, we also need to specify the connection port number of netty, which is 53 by default.

Then there is the domain name we want to query. Here we take www.flydean.com as an example.

You can also use the DNS resolution address configured in your system. Taking mac as an example, you can view the local DNS address through nslookup:

 nslookup  www.flydean.com
Server:        8.8.8.8
Address:    8.8.8.8#53

Non-authoritative answer:
www.flydean.com    canonical name = flydean.com.
Name:    flydean.com
Address: 47.107.98.187

The use of Do53/TCP in netty

With the IP address of the DNS Server, the next thing we need to do is to build a netty client, and then send a DNS query message to the DNS server.

Build DNS netty client

Because we are making a TCP connection, it can be achieved with the help of NIO operations in netty, which means that we need to use NioEventLoopGroup and NioSocketChannel to build a netty client:

 final String dnsServer = "223.5.5.5";
        final int dnsPort = 53;

EventLoopGroup group = new NioEventLoopGroup();
            Bootstrap b = new Bootstrap();
            b.group(group)
                    .channel(NioSocketChannel.class)
                    .handler(new Do53ChannelInitializer());

            final Channel ch = b.connect(dnsServer, dnsPort).sync().channel();

The bottom layer of the NIO Socket in netty uses the TCP protocol, so we only need to build the client like the common netty client service.

Then call the connect method of Bootstrap to connect to the DNS server, and the channel connection is established.

Here we pass in a custom Do53ChannelInitializer in the handler, we know that the role of the handler is to encode, decode and read the message. Because we don't know the message format of the client query at present, we will explain the implementation of Do53ChannelInitializer in detail later.

Send DNS query message

Netty provides the encapsulation of DNS messages. All DNS messages, including queries and responses, are subclasses of DnsMessage.

Each DnsMessage has a uniquely tagged ID and a DnsOpCode representing the type of message.

For DNS, opCode has the following types:

 public static final DnsOpCode QUERY = new DnsOpCode(0, "QUERY");
    public static final DnsOpCode IQUERY = new DnsOpCode(1, "IQUERY");
    public static final DnsOpCode STATUS = new DnsOpCode(2, "STATUS");
    public static final DnsOpCode NOTIFY = new DnsOpCode(4, "NOTIFY");
    public static final DnsOpCode UPDATE = new DnsOpCode(5, "UPDATE");

Because each DnsMessage may contain 4 sections, each section is represented by DnsSection. Because there are 4 sections, 4 section types are defined in DnsSection:

 QUESTION,
    ANSWER,
    AUTHORITY,
    ADDITIONAL;

Each section contains multiple DnsRecords. DnsRecord represents Resource record, referred to as RR. There is a CLASS field in RR. The following is the definition of the CLASS field in DnsRecord:

 int CLASS_IN = 1;
    int CLASS_CSNET = 2;
    int CLASS_CHAOS = 3;
    int CLASS_HESIOD = 4;
    int CLASS_NONE = 254;
    int CLASS_ANY = 255;

DnsMessage is a unified representation of DNS messages. For queries, netty provides a special query class called DefaultDnsQuery.

Let's first look at the definition and constructor of DefaultDnsQuery:

 public class DefaultDnsQuery extends AbstractDnsMessage implements DnsQuery {

        public DefaultDnsQuery(int id) {
        super(id);
    }

    public DefaultDnsQuery(int id, DnsOpCode opCode) {
        super(id, opCode);
    }

The constructor of DefaultDnsQuery needs to pass in id and opCode.

We can define a DNS query like this:

 int randomID = (int) (System.currentTimeMillis() / 1000);
            DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)

Since it is QEURY, you also need to set the query section in the 4 sections:

 query.setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));

Here, the setRecord method is called to insert RR data into the section.

The RR data here uses DefaultDnsQuestion. DefaultDnsQuestion has two constructors, one is the domain name to be queried, here is "www.flydean.com", and the other parameter is the type of dns record.

There are many types of dns records. There is a special class DnsRecordType in netty. There are many types defined in DnsRecordType, as shown below:

 public class DnsRecordType implements Comparable<DnsRecordType> {
    public static final DnsRecordType A = new DnsRecordType(1, "A");
    public static final DnsRecordType NS = new DnsRecordType(2, "NS");
    public static final DnsRecordType CNAME = new DnsRecordType(5, "CNAME");
    public static final DnsRecordType SOA = new DnsRecordType(6, "SOA");
    public static final DnsRecordType PTR = new DnsRecordType(12, "PTR");
    public static final DnsRecordType MX = new DnsRecordType(15, "MX");
    public static final DnsRecordType TXT = new DnsRecordType(16, "TXT");
    ...

Because there are many types, we choose a few commonly used ones to explain.

  • Type A, which is the abbreviation of address, is used to specify the IP address corresponding to the host name or domain name.
  • The NS type, short for name server, is the domain name server record, which is used to specify which DNS server the domain name is resolved by.
  • MX type, short for mail exchanger, is a mail exchange record used to locate the mail server according to the suffix of the mailbox.
  • The CNAME type, short for canonical name, can map multiple names to the same host.
  • TXT type, used to indicate the description information of the host or domain name.

The above are the dns record types we often use.

Here we choose to use A to query the host IP address corresponding to the domain name.

After building the query, we can use the netty client to send the query command to the dns server. The specific code is as follows:

 DnsQuery query = new DefaultDnsQuery(randomID, DnsOpCode.QUERY)
                    .setRecord(DnsSection.QUESTION, new DefaultDnsQuestion(queryDomain, DnsRecordType.A));
            ch.writeAndFlush(query).sync();

Message processing for DNS queries

We have sent the DNS query message, and the next step is to process and parse the message.

Remember our custom Do53ChannelInitializer? Take a look at its implementation:

 class Do53ChannelInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) {
        ChannelPipeline p = ch.pipeline();
        p.addLast(new TcpDnsQueryEncoder())
                .addLast(new TcpDnsResponseDecoder())
                .addLast(new Do53ChannelInboundHandler());
    }
}

We added two codecs TcpDnsQueryEncoder and TcpDnsResponseDecoder that come with netty to the pipline, and a custom Do53ChannelInboundHandler for message parsing.

Because we write DnsQuery to the channel, we need an encoder to encode DnsQuery into ByteBuf. Here we use the TcpDnsQueryEncoder provided by netty:

 public final class TcpDnsQueryEncoder extends MessageToByteEncoder<DnsQuery>

TcpDnsQueryEncoder inherits from MessageToByteEncoder, which means to encode DnsQuery as ByteBuf.

Take a look at his encode method:

 protected void encode(ChannelHandlerContext ctx, DnsQuery msg, ByteBuf out) throws Exception {
        out.writerIndex(out.writerIndex() + 2);
        this.encoder.encode(msg, out);
        out.setShort(0, out.readableBytes() - 2);
    }

It can be seen that TcpDnsQueryEncoder stores the length information of msg before msg encoding, so it is a length-based object encoder.

The encoder here is a DnsQueryEncoder object.

Take a look at its encoder method:

 void encode(DnsQuery query, ByteBuf out) throws Exception {
        encodeHeader(query, out);
        this.encodeQuestions(query, out);
        this.encodeRecords(query, DnsSection.ADDITIONAL, out);
    }

DnsQueryEncoder will encode headers, questions and records in turn.

After completing the encoding, we also need to decode the DnsResponse from the return of the DNS server. Here we use the TcpDnsResponseDecoder that comes with netty:

 public final class TcpDnsResponseDecoder extends LengthFieldBasedFrameDecoder

TcpDnsResponseDecoder inherits from LengthFieldBasedFrameDecoder, indicating that the data is divided by field length, which is similar to the format of the encoder we just introduced.

Take a look at his decode method:

 protected Object decode(ChannelHandlerContext ctx, ByteBuf in) throws Exception {
        ByteBuf frame = (ByteBuf)super.decode(ctx, in);
        if (frame == null) {
            return null;
        } else {
            DnsResponse var4;
            try {
                var4 = this.responseDecoder.decode(ctx.channel().remoteAddress(), ctx.channel().localAddress(), frame.slice());
            } finally {
                frame.release();
            }
            return var4;
        }
    }

The decode method first calls the decode method of LengthFieldBasedFrameDecoder to extract the content to be decoded, then calls the decode method of responseDecoder, and finally returns DnsResponse.

The responseDecoder here is a DnsResponseDecoder. The details of the specific decoder will not be elaborated here. Interested students can consult the code documentation by themselves.

Finally, we get the DnsResponse object.

Next is the custom InboundHandler to parse the message:

 class Do53ChannelInboundHandler extends SimpleChannelInboundHandler<DefaultDnsResponse>

In its channelRead0 method, we call the readMsg method to process the message:

 private static void readMsg(DefaultDnsResponse msg) {
        if (msg.count(DnsSection.QUESTION) > 0) {
            DnsQuestion question = msg.recordAt(DnsSection.QUESTION, 0);
            log.info("question is :{}",question);
        }
        int i = 0, count = msg.count(DnsSection.ANSWER);
        while (i < count) {
            DnsRecord record = msg.recordAt(DnsSection.ANSWER, i);
            //A记录用来指定主机名或者域名对应的IP地址
            if (record.type() == DnsRecordType.A) {
                DnsRawRecord raw = (DnsRawRecord) record;
                log.info("ip address is: {}",NetUtil.bytesToIpAddress(ByteBufUtil.getBytes(raw.content())));
            }
            i++;
        }
    }

DefaultDnsResponse is an implementation of DnsResponse. First, determine whether the number of QUESTIONs in msg is greater than zero.

If it is greater than zero, print out the information of the question.

Then parse out the ANSWER in msg and print it out.

In the end, we might get output like this:

 INFO  c.f.dnstcp.Do53ChannelInboundHandler - question is :DefaultDnsQuestion(www.flydean.com. IN A)
INFO  c.f.dnstcp.Do53ChannelInboundHandler - ip address is: 47.107.98.187

Summarize

The above is the explanation of using netty to create a DNS client for TCP query.

The code of this article, you can refer to:

learn-netty4

For more information, please refer to http://www.flydean.com/54-netty-dns-over-tcp/

The most popular interpretation, the most profound dry goods, the most concise tutorial, many you do not know

Welcome to pay attention to my official account: "Program those things", understand technology, understand you better!


flydean
890 声望433 粉丝

欢迎访问我的个人网站:www.flydean.com