DNS

This post recounts my efforts in implmenting the DNS protocol. Both a client and a server. It is my third post in my "Simple Systems" series. As with all posts in this series remember: Emphasis was placed on making the process of the protocol understandable, not on the code being effecient. One can't effectively make a more effecient system until you fully understand the problem. So this post is aimed at some one who does not know how DNS works and would like an easy to follow example of a fully functioning DNS client and DNS server.

Objectives

  1. Create a DNS Client that can make requests of real DNS servers.
  2. Create a DNS Server that can field requests from real DNS Clients.

Introduction

DNS, Domain Name Service, is a protocol for translating domain names, such as studiosleepygiraffe.com, into IP addresses, such as 127.0.0.1. Strictly speaking it is not a necessary system for the internet to work, but niether is a phone book for the telephone system. But you can immediately see the value of it with such an analogy. DNS is basically a phone book. Not only does it allow you to remember a name of a website instead of it's IP address, it allows that IP address to change as the organizations and people behind websites change and upgrad their hardware.

Origionally described in 1983 in RFC 883, it has been empowering the use of the internet since. There have been over 50 RFCs with recommending changes and upgrades to allow for ever more complex cases to be managed by it. Most interesting in my opinion is RFC 1035 which introduced the TXT record type which is a generic record that simple hold plain text. To me this sounds like record type that will grow in popularity as APIs become more common. It can serve as a simple place to store configurations related to your API. It makes DNS more of a Data Negotioation Service and less strictly a Domain Name Service.

There are a lot of things that can make DNS complicated, with recursive calls to other servers and authoritative answers and the like. But when you trim away the 90% that is edge cases and focus on the 10% that is the core use it gets pretty simple. The simplest way to think of DNS is a key-value store. Key: A:studiosleepygiraffe.com, value: 96.2.162.67. Key: TXT:studiosleepygiraffe.com, value: "This is an example message." and so on. You can think of the different records as different kinds of keys, but all that really changes is a few bits idenifying the record type. Like short hand.

DNS Methods

I'm trying to avoid making files like these because of my emphasis on making things serialized as possible in execution. But practicality beats purity. The nine methods in this file made the DNS Client and DNS Server much easier to read and understand so I went with it. The file is well documented with comments, so I will let them speak for themselves. I just wanted to mention why this existed.

DNS Client

As usual the file is filled with comments documenting the process, so a bird's eye view of the process is all I'll give here.

  1. Create a header.
  2. Add questions to the header. Note that in a real system you'd have to do these steps inverted so that you knew how many questions there were when you made the header. Here we simplified and only have one question.
  3. Connect to a DNS server over UDP
  4. Send our request
  5. Receive the response
  6. Read out the number of questions and the number of answers.
  7. Process out the questions. They are variable length so we need to process them to find where they stop.
  8. Process the answers.
  9. Print.

DNS Server

I had to use a socket server to get the DNS Server to behave correctly. UDP is managed differently from TCP because it is effectively one way. I won't go into to much detail now because this is about DNS not UDP, but UDP essentially is party A leaving party B a voice message and then party B responding in kind. It is super light weight because it has no checking for succesful transfer, but also means it takes more effort on the application programmer's side to get it to work as one might expect. That being said, to keep things simple I used a socket server.

I also had to include an "ipv4tobytes" method. Just a reminder that IP addresses are also human readable representations of the actual machine address: a string of 32 bits.

The DNS Server functions rather simply: 1. Wait for request 2. Parse header to find out number of questions. 3. Parse questions. 4. Lookup keys in local dictionary. 5. Create Response. 6. Create Header. 7. Send response.

Lessons Learned

I almost didn't do DNS. After two weeks of trying to understand it I nearly gave up. But I am glad I didn't. This has been a very insightful process. It is where I came to understand that only about 10% of a protocol matters, the rest is edge cases. I mentioned above that DNS is basically a key-value store. And while that is true, appraoching it that way didn't help me understanding it, at first. The problem is that's not how the writers of the RFC seemed to imagine it. They talked firstly about how servers are setup, what the network looks like, etc. They had a whole RFC, 882, on DNS before the main one, 883, that didn't ever address the layout of the DNS request (so far as I can tell). In RFC 883 they also described how the records were to be stored on disk. They were master minds trying to give shape to the internet that would be. That was not my objective.

The result of this was confusion on my part. I ended up pulling up a python library for DNS to figure out how DNS requests were formed. It got me to RFC 1035 section 4.1.1: Message, Format, Header section format. Section 4 of the RFC is well written and the most important part of DNS. It explains what each bit does and how they are laid out. This is where I finally got traction with the protocol. I wrote the interceptor to capture a DNS request and managed to capture a packet and examine it. I followed the guide of RFC 1035 and wrote the methods that parsed it into usable bits. I then learned how to do this the other direction. By the time I finished the DNS client I felt quite powerful. It's as if the magic wand I had suddenly became a power drill. The magic and mystery was gone, leaving nothing but potential behind.

The record types in DNS had always confused me. As simple as they are I just wasn't getting them. Having to write something to manage each record type made me intimately familiar with the important ones, A, CNAME, POINTER, really fast. Which is one of the goals here: Understand how DNS works. By making it I learned it throughly.

Conclusion

This system validated my idea for this series of posts. By making it I learned it throughly. I would highly recommend this approach to anyone, including myself, who wants to understand how a system works. When you simply read documentation on the outside it can be hard to figure out why something is broken. But when you understand it internally, you can see obscure error messages and understand what they mean and know how to fix them.

Real Code

If you are looking to do real work with DNS I would recommend DNS Python or DNSLib. The latter was the one I read to get an idea of how DNS works. They are definately much better, and complete, implementations of DNS than what I have done here. If you want to setup your own DNS server try out PI-Hole.

Thank you for reading.