Post

Understanding DNS Compression

Understanding DNS Compression

Story

I wanted to learn more about DNS packet formats, so I decided to use Wireshark and implement a DNS resolver. One issue that I had trouble with was parsing the DNS records of nameservers, because of DNS compression. In this post, I will go over what the DNS record format is and how to parse the record.

The DNS Response of the query “nvidia.com”

Here is a DNS response in wireshark, along with the corresponding hexadecimal.

Desktop View

1
2
3
4
47 cb 81 80 00 01 00 01 00 00 00 01 06 6e 76 69
64 69 61 03 63 6f 6d 00 00 01 00 01 c0 0c 00 01
00 01 00 00 04 16 00 04 22 c2 61 8a 00 00 29 04
c4 00 00 00 00 00 00

DNS Record

We will be looking at DNS Records to understand DNS compression.

Desktop View

1
c0 0c 00 01 00 01 00 00 04 16 00 04 22 c2 61 8a

The Record format has the following byte allocations in order:

  • Name: <= 255 bytes
  • Type: 2
  • Class: 2
  • Time to live: 4
  • Data Length: 2
  • Address 4

Our DNS record seems kind of small… Let’s map out the bytes to their corresponding fields:

  • Name: c0 0c
  • Type: 00 01
  • Class: 00 01
  • Time to live: 00 00 04 16
  • Data Length: 00 04
  • Address 22 c2 61 8a

Notice how the name is only 2 bytes long? This means that DNS compression was used! (kinda)

DNS Compression

Translate the name into binary:

11000000 00001100

When the first 2 bits of a name field are 1’s, this means that DNS compression has been used. The last 14 remaining bits represent an offset that points to the string to be used. When we ignore the first 2 bits, we get a pointer to byte 12.

00000000 00001100 = 12

Below is the 12th byte in the response (0 index) until encountering the null character 0x00

1
06 6e 76 69 64 69 61 03 63 6f 6d 00

This translates to “6nvidia3com”, which is the query “nvidia.com” we entered.

Code

The process to code this is fairly simple. We get the first byte, bitwise AND to get rid of the first two 1’s. Then we left shift by 8, so that the second_byte integers are maintained during a bitwise OR.

In simple terms, this is converting a 16 bit integer to a 14 bit integer after removing the first 2 bits on the left.

Then we seek the position that the 14 bit integer told us to go to and decode the name normally, which in this case was “6nvidia3com”

We return the pointer back to where it was to continue parsing.

1
2
3
4
5
6
7
8
def decode_compressed_name(first_byte: int, reader: BytesIO) -> bytes:
    second_byte = reader.read(1)[0]
    pointer = ((first_byte & 0b0011_1111) << 8) | second_byte
    current_pos = reader.tell()
    reader.seek(pointer)
    result = decode_name(reader)
    reader.seek(current_pos)
    return result

Note

Take a look here for project details.

This post is licensed under CC BY 4.0 by the author.