Advertisment

Building a DNS Server

author-image
PCQ Bureau
New Update

In the previous part, we saw what we could do with our own DNS implementation. This time, we shall understand the code behind its key elements. In order to take full advantage of our programming platform, we use various classes. In our project, we also have a few utility classes, which encapsulate other functionalities-like socket interfaces and error handling. Using these classes, we can split our code into easily manageable and maintainable pieces. As we also decided the last time, our DNS will work on both UDP and TCP protocols, and it will also use port 53 on both. Let's now get into the key elements of the DNS server. 

Advertisment

Our data structures

The DNS server is full of resource records of various types. We shall categorize each into its own data structure, and generalize it in our
dnsRESOURCERECORD structure as shown below.

Public Structure dnsRESOURCERECORD

Dim RRName As String

Dim RRType As String

Dim RRData As String

Dim TTL As Int32

' functions to do various tasks

End Structure

Direct Hit!
Applies to: Advanced system programmers
USP: Classes implemented in VB.NET to make it easier to understand
Links:
http://www.ietf.org/rfc/rfc1035.txt 
Advertisment

In addition to these properties, the SOA record has several fields. So, we define another structure for this, not shown in this article (but provided later in our source code). We also need variables to hold our server's properties. If you check the DNS documentation on MSDN you will find the DNS server's properties encapsulated in a DNS_Server WMI class. We don't need this really, since they would act as global variables in our service. Hence, we simply declare them as Public variables in a module.

Socket classes

As we said already, there are two protocols - UDP and TCP. But, we can implement both in a single class. We call this class 'DNSListener' where each variable used is duplicated for both protocols. Thus,

Private _udpSocket, _tcpSocket As Socket

Private _udpListener, _tcpListener As Thread

Advertisment

When the class is instantiated, we bind each to their sockets on port 53 and then start them off listening for incoming queries since DNS only responds to queries. Of course, they also initiate connections to other DNS servers for synchronization as well as forwarded querying, but we shall add these later. Following code sets the ball rolling.

_udpSocket = New Socket(AddressFamily.InterNetwork, _

SocketType.Dgram, _

ProtocolType.Udp)

_udpSocket.Bind(New IPEndPoint(IPAddress.Any, _myPort))

_tcpSocket = New Socket(AddressFamily.InterNetwork, _

SocketType.Stream, _

ProtocolType.Tcp)

_tcpSocket.Bind(New IPEndPoint(IPAddress.Any, _myPort))

Advertisment

_udpListener = New Thread(AddressOf BlockForDataUdp)

_udpListener.Start()

_tcpListener = New Thread(AddressOf
BlockForDataTcp)

_tcpListener.Start()

Call flow within our DNS project, how it branches off to handle forwarded queries and dual-protocol processing

Advertisment

The two functions BlockForDataUdp and BlockforDataTcp simply listen in an endless loop. When data comes, the code in the loop automatically breaks in and we branch off for the processing required. Then the respective variables are cleared and listening starts again as an interminable operation as:

Private Sub BlockForDataUdp()

Dim _data() As Byte

Do While (1)

_udpSocket.Listen(0)

_status = dnsListenerStatus.LISTENING

_udpSocket.Receive(_data)

_status = dnsListenerStatus.QUERY_IN_PROGRESS

'PERFORM THE QUERY

Erase _data

_udpSocket.Close()

Loop

End Sub

Demuxing data

When our data travels over the network, it will be in the form of padded strings and numeric values. Within our program, we need that data within manageable structures and other variables. We shall allocate the task of translating between the two forms to our demuxing functions-EncodeDataForTransmission and DecodeDataFromSocket. Since the data for each result or query will be unique to the query or system state that preceded it, we must use different functions (although of the same name) to do this for each step. That is, the data we send for an 'A' query will be far less and different from the data we send for an SOA query. This means that the results for the A query will have to be done by something that knows and recognizes 'A' data and so for the SOA query too.

Advertisment

Data bubbling

Now, the problem becomes bi-directional when we consider that our incoming data is not always a query, but can be a returned result from another DNS server! This means, this data can contain resource records and other maintenance information. To simplify this task, we invent a concept called Data Bubbling, where each step examines the data collected and determines what it can do with that data and processes it accordingly. It also adds its results to a queue to be processed fully later. Thus, the innermost callee of our call-stack is the only one that ever knows exactly how to deal with something and exactly what and how it should reply.

While the code for this is in several pieces, we should point out here that each implementation of these two functions have the same name and signature. So, there will be a copy of EncodeDataForTransmission in each of dnsRESOURCERECORD and in dnsARECORD. But while the one in dnsRESOURCERECORD will know only how to deal with a generalized RR data, the one in dnsARECORD will know how to deal with only an 'A' record. Yes, the ones at the base level will act as a fall back in case no implementation exists for a given RR.

In our last part of this series next month, we shall put all of this together and run our DNS server. We shall also see how we can make our DNS system coexist with other DNS servers running on the same machine. 

Advertisment

Sujay V Sarma

Advertisment

Stay connected with us through our social media channels for the latest updates and news!

Follow us: