DNS requests in Java


This is the first article in a series of articles focusing on the DNS protocol over UDP and its implementation in java.

You can also find the original article I wrote on my medium.com.

The next article focuses on the response of a DNS server and how to parse it.

If you’re more interested in the code without all the explanation skip to the end of the article.

Recently I wanted to write a DNS Resolver in Java. Soon I learned that it’s not as easy as you’d expect(to the surprise of no one but myself I suppose).

What did perplex me the most however was the fact that there just aren’t any good articles(that I could find) on how to get your toes wet. The only up-to-date implementation of a dns server in java I could find was dnsjava, which, while very good is also very feature-rich and hard to follow. I just wanted something quick and dirty to get me going.


What tools I’ll be using:

  • IntelliJ IDEA Community Edition however any sort of medium where one can write code works fine. Eclipse, Notepad++, Vim, etc.
  • Wireshark so that I can intercept the requests I’ll be sending(and the responses for that matter). It’s not mandatory by any means, however I feel I get a better understanding of what’s happening.
  • Java 17. Of course other versions of java work just fine.

The end goal of this series is to be able to send a request and receive a response for A record type and maybe even a AAAA record.


Without rambling on let’s see what’s what.

First and foremost we should understand the anatomy of a DNS request/response.

And what better way to do that but to read RFC1035. Don’t feel like reading the whole thing? I’ll give you a brief rundown.

Disclaimer time: RFC1035 is, I believe, the oldest RFC of its kind related to the DNS protocol. There are other, newer, RFCs that have built upon this RFC. Some things in it are outdate, although the bulk is still accurate. Taking into account all RFCs related to the DNS protocol as a single person is, in my opinion, a fool’s errand, there are way to many. Instead, I’ll just focus on this particular RFC since it’s fairly easy too understand.

I’m aiming at doing a DNS request over UPD. There are many DNS protocols but the vast majority of calls are over UPD(this may have changed in recent years).

Anatomy of a DNS request

Taken from RFC1035:

All communications inside of the domain protocol are carried in a single format called a message.   

    +---------------------+
    |        Header       |
    +---------------------+
    |       Question      | the question for the name server
    +---------------------+
    |        Answer       | RRs answering the question
    +---------------------+
    |      Authority      | RRs pointing toward an authority
    +---------------------+
    |      Additional     | RRs holding additional information
    +---------------------+

Lets start with the Header section.

Section 4.1.1 of RFC1035 shows in great detail how the Header section must look like.

     0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Before we can properly read this section(or any other following section for that matter) we must understand how data is transmitted.

Section 2.3.2 tells us that: The order of transmission of the header and data described in this document is resolved to the octet level. Whenever a diagram shows a group of octets, the order of transmission of those octets is the normal order in which they are read in English.

Lets break this down:

  • An octet is equal to 8 bits. Specifically in Java that would be a byte. Here’s a something to freshen your memory of primitive types in java.
  • The order of bits is big-endian always. This means requests as well as responses always follow this convention. In other words, as the section above tells us, we read it like plain English.

Java uses a big-endian bit order to store data by default.

Now going back to the Header section. Lets choose the first entry, specifically the ID. If we read the diagram above we can tell that the ID has 16 bits or 2 bytes of data. In simpler terms it’s equivalent to a short in Java. So any number between -32,768 and 32,767(inclusive).

The RFC definition of ID is: A 16 bit identifier assigned by the program that
 generates any kind of query. This identifier is copied
 the corresponding reply and can be used by the requester
 to match up replies to outstanding queries.

Lets put this into more concrete Java terms

Random random = new Random();
short ID = (short)random.nextInt(32767);

The ID can be signed as well so don’t worry about a negative number.

Great we have the ID as a short. Lets move on to the next section.

The next section is a bit more tricky. In fact it has a special name, the Flags section.

You may have noticed that it’s not a single byte(the smallest unit we can easily represent in java). In fact the first byte includes in fact 5 distinct flags(QR, OpCode, AA, TC, and RD).

After testing things around a bit I came to the conclusion that the easiest way to represent the flags section in java is to use a string. Yes, that’s right, a string. I could have used a BitSet but it’s not immediately obvious what flag is what so I dropped that idea pretty fast.

Lets represent the Flags section. In total the length of the Flags is equal to 16 bits or 1 short.

QR — A one bit field that specifies whether this message is a
 query (0), or a response (1).

In this case, since we’re doing a request, the QR is 0.

String flags = "0"; // QR

OpCode — A four bit field that specifies kind of query in this
 message. This value is set by the originator of a query
 and copied into the response. The values are:

0 a standard query (QUERY)

1 an inverse query (IQUERY)

2 a server status request (STATUS)

3–15 reserved for future use

In this case we’re doing a standard query, so OpCode is 0. However, we need to represent this as a 4 bit long series of bits.

flags += "0000"; // OpCode

AA — Authoritative Answer — this bit is valid in responses,
 and specifies that the responding name server is an
 authority for the domain name in question section.

AA should be 0 as we need something there when we send the request.

flags += "0"; // AA

TC — TrunCation — specifies that this message was truncated
 due to length greater than that permitted on the
 transmission channel.

A side note for this particular flag. Section 2.3.4 tells that a UDP message is limited to 512 bytes or less. For the most part this doesn’t apply anymore, however this flag isn’t entirely redundant as some servers may still be limited by UDP package size.

In this case the message is not truncated. Mind you truncation specifies if the message was truncated(aka a boolean value) not by how much a message was truncated in absolute terms. So truncation is either true or false.

flags += "0"; // TC

RD — Recursion Desired — this bit may be set in a query and
 is copied into the response. If RD is set, it directs
 the name server to pursue the query recursively.
 Recursive query support is optional.

This is in fact the only bit set to true in the entire Flags section of this request. I have yet to find a case where recursion isn’t desired. More on what recursion in DNS is.

flags += "1"; // RD

RA — Recursion Available — this be is set or cleared in a
 response, and denotes whether recursive query support is
 available in the name server.

This will be set by the responding server. Set it to false in the request.

flags += "0"; // RA

Z-Reserved for future use. Must be zero in all queries
 and responses.

Self explanatory.

flags += "000"; // Z

RDCODE — Response code — this 4 bit field is set as part of
 responses.

A complete list of RDCODEs can be found here. For the purpose of a request the RDCODE is 0.

flags += "0000"; // RDCODE

At the end of it all the Flags section looks like so

"0000000100000000"

This is all fine and dandy for a quick visual inspection of the flags but utterly useless when it comes to actually sending the flags header.

short requestFlags = Short.parseShort("0000000100000000", 2);
ByteBuffer flagsByteBuffer = ByteBuffer.allocate(2).putShort(requestFlags);
byte[] flagsByteArray= flagsByteBuffer.array();

Parse the string representation as a short in base 2, and create a bytebuffer of said short with the total length of 2(2 bytes = 1 short), and finally transform the bytebuffer into an array of bytes.

Next sections are represented each by 1 short. There’s a total of 4 sections:

  • QDCOUNT — an unsigned 16 bit integer specifying the number of
     entries in the question section.
  • ANCOUNT — an unsigned 16 bit integer specifying the number of
     resource records in the answer section.
  • NSCOUNT — an unsigned 16 bit integer specifying the number of name
     server resource records in the authority records
     section.
  • ARCOUNT — an unsigned 16 bit integer specifying the number of
     resource records in the additional records section.

The only one of interest is the QDCOUNT section, since the other sections are going to be set by the responding server. Mind you we still have to represent them as 0 in the request.

In this particular case we have exactly 1 question.

A small but very important side note here. Technically nobody is stopping anybody from having more than 1 question per query, however, I have yet to see that happening. Quite frankly I don’t even know what’s going to happen if one was to have more than 1 question per query. I doubt a server is able to answer. Have a look at this stackoverflow question for a more detailed answer.

With that said…

short QDCOUNT = 1;
short ANCOUNT = 0;
short NSCOUNT = 0;
short ARCOUNT = 0;

Putting it all together:

Random random = new Random();
short ID = (short)random.nextInt(32767);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);

short requestFlags = Short.parseShort("0000000100000000", 2);
ByteBuffer flagsByteBuffer = ByteBuffer.allocate(2).putShort(requestFlags);
byte[] flagsByteArray = flagsByteBuffer.array();

short QDCOUNT = 1;
short ANCOUNT = 0;
short NSCOUNT = 0;
short ARCOUNT = 0;

dataOutputStream.writeShort(ID);
dataOutputStream.write(flagsByteArray);
dataOutputStream.writeShort(QDCOUNT);
dataOutputStream.writeShort(ANCOUNT);
dataOutputStream.writeShort(NSCOUNT);
dataOutputStream.writeShort(ARCOUNT);

We now have the complete Header section of the request.

Lets move on to the Question section.


The question section is in fact one of the most straight forward sections.

Section 4.1.2. states:

The question section is used to carry the "question" in most queries,
i.e., the parameters that define what is being asked.  The section
contains QDCOUNT (usually 1) entries, each of the following format:

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

QNAME — a domain name represented as a sequence of labels, where
 each label consists of a length octet followed by that
 number of octets. The domain name terminates with the
 zero length octet for the null label of the root. Note
 that this field may be an odd number of octets; no
 padding is used.

Small side note here. Normally we can(and should) use message compression for this section, however, since it’s not mandatory for requests I won’t be doing that. When the server responds the domain will be compressed though. So soon it will have to be dealt with.

This section is very straight forward. Take a domain name(without prefixes such as scheme(http or https), or www), split it into parts, and put it into an array of bytes.

String domain = "coderambling.com";
String[] domainParts = domain.split("\\.");

for (int i = 0; i < domainParts.length; i++) {
    byte[] domainBytes = domainParts[i].getBytes(StandardCharsets.UTF_8);
    dataOutputStream.writeByte(domainBytes.length);
    dataOutputStream.write(domainBytes);
}
dataOutputStream.writeByte(0);// no more parts

QTYPE — a two octet code which specifies the type of the query.
 The values for this field include all codes valid for a
 TYPE field, together with some more general codes which
 can match more than one type of RR.

AAAA for example is a type of record. You can request more than one record per query, however, for this example I’ll stick to a single record. Specifically A record.

Here’s a list of all records available.

// Type 1 = A (Host Request)
dataOutputStream.writeShort(1);

QCLASS — a two octet code that specifies the class of the query.
 For example, the QCLASS field is IN for the Internet.

A complete list of classes can be found here.

For this example I’ll be using IN class.

// Class 1 = IN
dataOutputStream.writeShort(1);

That’s it for the question section. Easy.


The next 3 sections(Answer, Authority, and Additional) will not be set in a request. The responding server will set those, so we can ignore them for the purpose of this example.

Java uses DatagramSockets and DatagramPackets to send/receive data via UDP.

Here’s the complete code to send such a request(in includes all of the above code).

InetAddress ipAddress = InetAddress.getByName("1.1.1.1");// cloudflare

Random random = new Random();
short ID = (short)random.nextInt(32767);
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
DataOutputStream dataOutputStream = new DataOutputStream(byteArrayOutputStream);

short requestFlags = Short.parseShort("0000000100000000", 2);
ByteBuffer flagsByteBuffer = ByteBuffer.allocate(2).putShort(requestFlags);
byte[] flagsByteArray = flagsByteBuffer.array();

short QDCOUNT = 1;
short ANCOUNT = 0;
short NSCOUNT = 0;
short ARCOUNT = 0;

dataOutputStream.writeShort(ID);
dataOutputStream.write(flagsByteArray);
dataOutputStream.writeShort(QDCOUNT);
dataOutputStream.writeShort(ANCOUNT);
dataOutputStream.writeShort(NSCOUNT);
dataOutputStream.writeShort(ARCOUNT);
String domain = "coderambling.com";
String[] domainParts = domain.split("\\.");

for (int i = 0; i < domainParts.length; i++) {
    byte[] domainBytes = domainParts[i].getBytes(StandardCharsets.UTF_8);
    dataOutputStream.writeByte(domainBytes.length);
    dataOutputStream.write(domainBytes);
}
// No more parts
dataOutputStream.writeByte(0);
// Type 1 = A (Host Request)
dataOutputStream.writeShort(1);
// Class 1 = IN
dataOutputStream.writeShort(1);

byte[] dnsFrame = byteArrayOutputStream.toByteArray();

System.out.println("Sending: " + dnsFrame.length + " bytes");
for (int i = 0; i < dnsFrame.length; i++) {
    System.out.print(String.format("%s", dnsFrame[i]) + " ");
}

DatagramSocket socket = new DatagramSocket();
DatagramPacket dnsReqPacket = new DatagramPacket(dnsFrame, dnsFrame.length, ipAddress, DNS_SERVER_PORT);
socket.send(dnsReqPacket);

Now, lets actually send a request and see what happens.

To capture the request use Wireshark on port 53(default UDP port).

wireshark port 53

Start listening to said port then send the request using your preferred way.

I used the default IntelliJ compiler to send the request.

After sending the request, a response should come up in Wireshark

Here we can see the flag set above(RD — recursion desired). As well as the response, which includes 1 A records for the chosen domain.

The next article will focus on the response.


Posted

in

by

Tags: