Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

C# .NET DNS query component

4.89/5 (83 votes)
25 Oct 2005CPOL9 min read 6   12.3K  
A reusable component for performing DNS queries.

Test Application

Introduction

This article demonstrates the form of DNS query messages and how to submit a request to a DNS server and interpret the result. I have wrapped this functionality up into a small easy-to-use C# assembly which you can easily deploy in your own applications to make ANAME, MX, NS and SOA queries. The assembly is entirely safe managed code, and it works just as well on Linux with the Mono CLR as it does on Windows. This article and the code supplied with it has been built from the information in RFC1035 - Domain names - implementation and specification.

Background

Until recently, spam was something which annoyed other people; I never got any. Somehow though, my email address has ended up on a mailing list and spam started showing up in my inbox. At first this was just a minor nuisance, but I expect my email address was sold on with thousands of others to other spammers and now I receive about 100 junk emails a day - a major annoyance. Why are they so convinced I need Viagra? I'm not that old...

Outlook to its credit has proven effective at detecting and destroying this junk, but I still didn't like the fact that it had to be picked up only to be discarded. The slightly protracted Send and Receive that normally indicates the arrival of a new attention-worthy email became more and more disappointing, and I wondered what someone would do if they were still stuck with a dial-up connection. It would be intolerable.

My solution was to create my own SMTP/POP3 server combination which could detect junk and destroy it, long before it could trouble Outlook, and this would present a good chance for me to get my .NET socket programming up to speed having used it very little in the past. I could use one of my Linux servers in Red Bus in London which I use for online game-hosting to host the mail server (via the glorious gift of Mono), but to do this would mean having to use completely managed C# without any P/Invoke calls. I got to work.

SMTP and POP3 are pretty simple text based standards to implement, but I hit a problem. In order for my SMTP server to relay my messages to other servers, an MX lookup would be required. An MX lookup is the act of retrieving the hostname(s) of one or more mail servers which handle a domain's email. A quick look at System.Net.Dns showed that the framework didn't support this, and as already mentioned I couldn't go down the Interop path. The only other alternatives were expensive commercial components which were out of the question because I certainly wasn't going to end up out of pocket on account of some spammers. My project had just got bigger.

So you want to ask a DNS server a question...

On the surface of it, DNS seems pretty straightforward, simply converting names to numbers, or retrieving other information about a domain. It's actually a huge and complicated subject and books on it tend to be quite wide. Thankfully, for the purpose of what we're doing we don't need to understand very much at all - just how to create a query, send it to a server, and interpret the response. The most common query that a DNS server deals with is the ANAME query, which maps domain names to IP addresses (codeproject.com to 209.171.52.99, for example). System.Net.Dns.GetHostByName performs ANAME lookups. Probably the next most common type of query is the MX query.

Unlike many of the internet protocols which are text based, DNS is a binary protocol. DNS servers are some of the busiest computers on the internet, and the overhead of string-parsing would make such a protocol prohibitive. To keep things fast and lean, UDP is the transport of choice being lightweight, connectionless and fast in comparison to TCP. To communicate with a DNS server, you simply throw a single UDP packet at it and it throws one back. Oh, and these packets cannot exceed 512 bytes in length. (Incidentally, many firewalls block UDP packets larger than 512 bytes in length.)

The diagram below shows the binary request I sent to my DNS server to look up the MX records for the domain microsoft.com and the corresponding response I received. To do this, I sent a 31 byte UDP packet to port 53 of my DNS server as shown below. It replied with a 97 byte response again on UDP port 53.

Both request and response share the same format, which starts with a 12 byte header block. This starts with a two byte message identifier. This can be any 16 bit value and is echoed in the first two bytes of the response, and is useful as it allows us to match up requests and responses as UDP makes no guarantees about the order in which things arrive. After that follows a two byte status field which in our request has just one single bit set, the recursion desired bit. Next comes a two byte value denoting how many questions there are in the request, in this case just 1. There then follows three more two byte values denoting the number of answers, name server records and additional records. As this is a request, all these are zero.

The rest of the request is our single question. A question consists of a variable length domain name, a two byte QTYPE and a two byte QCLASS in that order. Domain names are treated as a series of labels, labels being the words between dots. In our example microsoft.com consists of two labels, microsoft and com. Each label is preceded by a single byte specifying its length. The QTYPE denotes the type of record to retrieve, in this example, MX. QCLASS is Internet.

Test Application

The response we get back tells us that there are three inbound mail servers for the domain microsoft.com, maila.microsoft.com, mailb.microsoft.com and mailc.microsoft.com. All three have the same preference of 10. When sending mail to a domain, the mail server with the lowest preference should be tried first, then the next lowest etc. In this case, there is no preference difference and any of the three may be used. Let's look a bit more closely at the response.

You may have noticed that the first 31 bytes of the response are very similar to the request, the only difference being in the status field (bytes 2 & 3) and the answer count (bytes 6 & 7). The answer count tells us that three answers follow in the response. I refer those who are interested in the make up of the status field to the above RFC section 4.1.1, as I will not cover that here. You'll also notice that the question is echoed in the response, something which seems rather inefficient to me, but that's the standard. The first answer starts at byte 31 (0x1F).

The first part of any answer embeds the question in it so if you ask more than one question you know to which question the answer refers. A shortened form is used - rather than repeating the domain microsoft.com explicitly here which is wasteful when we've only got 512 bytes to play with. We reference the existing domain definition at byte 12 (0x0C). This requires just two bytes instead of 15 in our example. When examining the label length byte which precedes a label, if the two most significant bits are set, this denotes a reference to a previously defined domain name and the label does not follow here. The next byte tells you the position in the message of the existing domain name. Again the QTYPE and QCLASS follow, and then we start to see the part which is the answer.

The next four bytes represent the TTL (time to live) of the record. When a DNS server can't answer a question explicitly, it knows (or can find out) another server which can and asks that. It will then cache this answer for a certain period to improve efficiency. Every record in the cache has a TTL after which it will be destroyed and re-fetched from elsewhere if needed.

The next two bytes tell the size of the record, the next two the MX preference, and then follows the variable length domain name. Here we only specify the mailc part of the domain, and then again reference the rest of the domain name at byte 12 (to produce mailc.microsoft.com). Two almost identical records follow for maila.microsoft.com and mailb.microsoft.com.

Using the component

Now that everything is as clear as opaque crystal, let's look at the way you can use the supplied component to perform the domain look up for you. You will need to reference the assembly and import the Bdev.Net.Dns namespace. The following code illustrates the example above:

C#
// Shameful hardcoding of my DNS server
IPAddress dnsServerAddress = IPAddress.Parse("194.74.65.68");

// Retrieve the MX records for the domain microsoft.com
MXRecord[] records = Resolver.MXLookup("microsoft.com", 
                                       dnsServerAddress);

// iterate through all the records and display the output
foreach (MXRecord record in records)
{
    Console.WriteLine("{0}, preference {1}", 
            record.HostName, record.Preference);
}

This uses a simplified form of the interface which is for MX records. You could also do the same query, with the code which follows:

C#
// Further shameful hardcoding of my DNS server
IPAddress dnsServerAddress = IPAddress.Parse("194.74.65.68");

// create a request
Request request = new Request();

// add the question
request.AddQuestion(new Question("microsoft.com", 
                       DnsType.MX, DnsClass.IN));

// send the query and collect the response
Response response = Resolver.Lookup(request, dnsServerAddress);

// iterate through all the answers and display the output
foreach (Answer answer in Answers)
{
    MXRecord record = (MXRecord)answer.Record;
    Console.WriteLine("{0}, preference {1}", 
                      record.HostName, record.Preference);
}

Odds and ends

One annoyance with this component is that you have to explicitly tell the resolver the IP address of the DNS server to query, every time you do a look up. Ideally, it would use one of the default DNS servers so that you could leave this parameter out, but I have been unable to find a way of getting this information programmatically. If you know a way, then please let me know. It's haunting me.

Two things to note about DNS servers. Firstly, some support a TCP connection on port 53 in addition to UDP, and this can be used to get round the 512 byte limitation. Many do not, and I have not provided a TCP implementation. Secondly, although the protocol allows more than one question per request, many servers don't, probably to try and keep things within the 512 byte limit. I recommend that you only ever use a single question per request. If the response doesn't fit into 512 bytes, there is a truncation bit which is set in the status field.

Please report back to me any bugs/enhancements.

History

  • 2005-10-07:

    Initial release.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)