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

DNS.NET Resolver (C#)

4.99/5 (96 votes)
11 Mar 2013CPOL6 min read 6   18.7K  
A full implementation of a reusable DNS resolver component and a Dig.Net example application.
Image 1

Introduction

One of the things I needed to complete my self-written spam-stopper, which acts like an SMTP proxy, is resolving DNS queries, reading SPF1 records, doing reversed lookups, etc. On the .NET (2.0) framework, there is a simple implementation of a DNS component. But it is far from complete. The other project on DNS resolving on CodeProject, C# .NET DNS query component, by Rob Philpott is old, buggy, incomplete, and not supported anymore. It was time to take the official RFCs on DNS and build the application from ground up. I must admit, the influence of Rob's project is there, but the code is definitely not the same.

Background

As mentioned before, the basics of DNS is explained in RFCs. (Request For Comments). These are the RFCs I used for the initial project:

  • rfc1034.txt (Domain names - Concepts and facilities, year 1987)
  • rfc1035.txt (Domain names - Implementation and specification, year 1987)
  • rfc1886.txt (DNS Extensions to support IP version 6, year 1995)
  • rfc2915.txt (The Naming Authority Pointer, year 2000)

A copy of these RFC text documents is included in the source of this project.

For a total set of background information, start at this IANA page.

Using the Code

The main core of DNS resolving is the 'resolver'. This class wraps queries into DNS packets and sends them to any DNS server. The response is then decoded into some useful information (at least for programmers, or for me in special Wink | ;-) ). All my DNS stuff uses the namespace Heijden.DNS. Don't change it in your applications, it gives me some comfort knowing my name is stored in some nice other projects. Don't forget to let me know.

Because I don't want to go immediately into details in this article, I only show the usage of the resolver component to warm you up. As the best way to do this, I have built a Dig class which acts like the good-old-Unix-style dig. Its acts like dig, but it is not a complete dig implementation. It does, however, do everything you want it to, presenting the output in more or less the same format.

This is the outline of the Dig class:

C#
using Heijden.DNS;

class Dig
{
  public Resolver resolver;

  public Dig()
  {
    resolver = new Resolver();
    resolver.OnVerbose += new Resolver.VerboseEventHandler(resolver_OnVerbose);
  }

  private void resolver_OnVerbose(object sender, Resolver.VerboseEventArgs e)
  {
    Console.WriteLine(e.Message);
  }

  // some more useful code goes here, please read on
}

The resolver uses the default DNS servers which are used on your Windows machine. A good alternative is to use Resolve.DefaultDnsServers which are the two servers from www.opendns.com and are free to use. The resolver accepts any DNS server, or servers. You can add as many as you need, specifying any IP and/or port number.

The main method to do queries to DNS servers is Query. In the dig example, I used a stopwatch to measure the total roundtrip time.

C#
public void DigIt(string name, QType qtype, QClass qclass)
{
  Console.WriteLine("; <<>> Dig.Net 0.0.1 <<>> @{0} {1} {2}",
       resolver.DnsServer, qtype, name);
  Console.WriteLine(";; global options: printcmd");

  Stopwatch sw = new Stopwatch();

  sw.Start();
  Response response = resolver.Query(name, qtype, qclass);
  sw.Stop();

  if(response.Error != "")
  {
    Console.WriteLine(";; " + response.Error);
    return;
  }

  // some more excellent code goes here, please read on
}

When not in error, Dig outputs the header information:

C#
// stay with me

Console.WriteLine(";; Got answer:");

Console.WriteLine(";; ->>HEADER<<- opcode: {0}, status: {1}, id: {2}",
  response.header.OPCODE,
  response.header.RCODE,
  response.header.ID);
Console.WriteLine(";; flags: {0}{1}{2}{3}; QUERY: {4}, ANSWER:
    {5}, AUTHORITY: {6}, ADDITIONAL: {7}",
  response.header.QR ? " qr" : "",
  response.header.AA ? " aa" : "",
  response.header.RD ? " rd" : "",
  response.header.RA ? " ra" : "",
  response.header.QDCOUNT,
  response.header.ANCOUNT,
  response.header.NSCOUNT,
  response.header.ARCOUNT);
Console.WriteLine("");

And more importantly, it shows the records in response to the query:

C#
if (response.header.QDCOUNT > 0)
{
  Console.WriteLine(";; QUESTION SECTION:");
  foreach (Question question in response.Questions)
    Console.WriteLine(";{0}" , question);
  Console.WriteLine("");
}

if (response.header.ANCOUNT > 0)
{
  Console.WriteLine(";; ANSWER SECTION:");
  foreach (AnswerRR answerRR in response.Answers)
    Console.WriteLine(answerRR);
  Console.WriteLine("");
}

if (response.header.NSCOUNT > 0)
{
  Console.WriteLine(";; AUTHORITY SECTION:");
  foreach (AuthorityRR authorityRR in response.Authorities)
    Console.WriteLine(authorityRR);
  Console.WriteLine("");
}

if (response.header.ARCOUNT > 0)
{
  Console.WriteLine(";; ADDITIONAL SECTION:");
  foreach (AdditionalRR additionalRR in response.Additionals)
    Console.WriteLine(additionalRR);
  Console.WriteLine("");
}

Personally, I love to have the records 'foreach-ed'.

Most of the work is done; wrap it up showing some rudimental information:

C#
Console.WriteLine(";; Query time: {0} msec", sw.ElapsedMilliseconds);
Console.WriteLine(";; SERVER: {0}#{1}({2})" ,
    response.Server.Address,response.Server.Port,response.Server.Address);
Console.WriteLine(";; WHEN: " +
    response.TimeStamp.ToString("ddd MMM dd hh:mm:ss yyyy",
    new System.Globalization.CultureInfo("en-US")));
Console.WriteLine(";; MSG SIZE rcvd: " + response.MessageSize);

I used Console as an output stream. In your application, it can easily be redirected using a TextWriter class. In the example project, I used:

C#
Console.SetOut(new FeedbackWriter(this.textBox1));

This FeedbackWriter is a wrapper for a textbox in my project, but it can be tuned to your needs.

C#
class FeedbackWriter : TextWriter
{
  internal FeedbackWriter(TextBox textBox) : base()
  {
    _textBox = textBox;
  }

  private TextBox _textBox;

  public override Encoding Encoding
  {
    get { return Encoding.Default; }
  }

  private delegate void WriteDelegate(string value);
  public override void Write(string value)
  {
    if (_textBox.InvokeRequired)
    {
      _textBox.Invoke(new WriteDelegate(Write), new object[] { value });
    }
    else
    {
      _textBox.AppendText(value.Replace("\n", base.NewLine));
    }
  }

  public override void WriteLine(string value)
  {
    this.Write(value);
    this.Write(base.NewLine);
  }
}

Give Me More Details

Okay, Dig is good. But, the workhorse is the Resolver class. It has got so many secrets. I will try to reveal all of them.

The resolver uses two main classes to do its work. It uses a Query class and delivers a Response class. Querying a DNS server can be done by the TCP or UDP transport protocols. The main DNS methods can be used synchronously and asynchronously. This took me really much typing work. Some headaches and no sleep to do it right. All responses can be stored in a real-time response cache. It uses the Time-To-Live properties of the response records which can be viewed in the Dig application by doing the same queries over and over again (it counts down the TTL values). The caching of records speeds up applications tremendously.

Public constants, constructors, properties, and methods which can be used on the resolver:

C#
// Some usefull constants
public const string Version = "1.0.0.0";
public const int DefaultPort = 53;
public static readonly IPEndPoint[] DefaultDnsServers;

// Constructors
public Resolver(IPEndPoint[] DnsServers);
public Resolver(IPEndPoint DnsServer);
public Resolver(IPAddress ServerIpAddress, int ServerPortNumber);
public Resolver(string ServerIpAddress, int ServerPortNumber);
public Resolver(string ServerIpAddress);
public Resolver();

// Properties
public int TimeOut;
public int Retries;
public bool Recursion;
public TransportType TransportType;
public IPEndPoint[] DnsServers;
public string DnsServer;
public bool UseCache;

// Methods
public void ClearCache();
public Response Query(string name, QType qtype, QClass qclass);
public Response Query(string name, QType qtype);
public static IPEndPoint[] GetDnsServers();

public static string GetArpaFromIp(IPAddress ip);

// Deprecated methods in the original System.Net.DNS class
public IPAddress[] GetHostAddresses(string hostNameOrAddress);
public IAsyncResult BeginGetHostAddresses(string hostNameOrAddress, 
    AsyncCallback requestCallback, object stateObject);
public IPAddress[] EndGetHostAddresses(IAsyncResult AsyncResult);
public IPHostEntry GetHostByAddress(IPAddress ip);
public IPHostEntry GetHostByAddress(string address);
public IPHostEntry GetHostByName(string hostName);
public IAsyncResult BeginGetHostByName(string hostName, 
    AsyncCallback requestCallback, object stateObject);
public IPHostEntry EndGetHostByName(IAsyncResult AsyncResult);
public IPHostEntry Resolve(string hostName);
public IAsyncResult BeginResolve(string hostName, 
    AsyncCallback requestCallback, object stateObject);
public IPHostEntry EndResolve(IAsyncResult AsyncResult);

// Non-Deprecated methods
public IPHostEntry GetHostEntry(IPAddress ip);
public IPHostEntry GetHostEntry(string hostNameOrAddress);
public IAsyncResult BeginGetHostEntry(string hostNameOrAddress, 
    AsyncCallback requestCallback, object stateObject);
public IAsyncResult BeginGetHostEntry(IPAddress ip, 
    AsyncCallback requestCallback, object stateObject);
public IPHostEntry EndGetHostEntry(IAsyncResult AsyncResult);

The resolver does not use any query method from the System.Net.DNS class; it borrows some handy IP macros, however. These are used in the GetArpaFromIp method to handle IPv4 and IPv6 addresses.

The main method of the Resolver class is:

C#
public Response Query(string name, QType qtype, QClass qclass);

These are the complete enumerated lists of QType and QClass:

C#
public enum QType : ushort
{
  A = Type.A,   // a IPV4 host address
  NS = Type.NS, // an authoritative name server
  MD = Type.MD, // a mail destination (Obsolete - use MX)
  MF = Type.MF, // a mail forwarder (Obsolete - use MX)
  CNAME = Type.CNAME, // the canonical name for an alias
  SOA = Type.SOA, // marks the start of a zone of authority
  MB = Type.MB,   // a mailbox domain name (EXPERIMENTAL)
  MG = Type.MG,   // a mail group member (EXPERIMENTAL)
  MR = Type.MR,   // a mail rename domain name (EXPERIMENTAL)
  NULL = Type.NULL, // a null RR (EXPERIMENTAL)
  WKS = Type.WKS,   // a well known service description
  PTR = Type.PTR,   // a domain name pointer
  HINFO = Type.HINFO, // host information
  MINFO = Type.MINFO, // mailbox or mail list information
  MX = Type.MX,   // mail exchange
  TXT = Type.TXT, // text strings


  RP = Type.RP,       // The Responsible Person rfc1183
  AFSDB = Type.AFSDB, // AFS Data Base location
  X25 = Type.X25,     // X.25 address rfc1183
  ISDN = Type.ISDN,   // ISDN address rfc1183
  RT = Type.RT,       // The Route Through rfc1183

  NSAP = Type.NSAP,        // Network service access point address rfc1706
  NSAP_PTR = Type.NSAPPTR, // Obsolete, rfc1348

  SIG = Type.SIG, // Cryptographic public key signature rfc2931 / rfc2535
  KEY = Type.KEY, // Public key as used in DNSSEC rfc2535

  PX = Type.PX,   // Pointer to X.400/RFC822 mail mapping information rfc2163

  GPOS = Type.GPOS, // Geographical position rfc1712 (obsolete)

  AAAA = Type.AAAA, // a IPV6 host address

  LOC = Type.LOC, // Location information rfc1876

  NXT = Type.NXT, // Obsolete rfc2065 / rfc2535

  EID = Type.EID, // *** Endpoint Identifier (Patton)
  NIMLOC = Type.NIMLOC,// *** Nimrod Locator (Patton)

  SRV = Type.SRV,     // Location of services rfc2782
  ATMA = Type.ATMA,   // *** ATM Address (Dobrowski)
  NAPTR = Type.NAPTR, // The Naming Authority Pointer rfc3403
  KX = Type.KX,       // Key Exchange Delegation Record rfc2230
  CERT = Type.CERT,   // *** CERT RFC2538


  A6 = Type.A6, // IPv6 address rfc3363
  DNAME = Type.DNAME, // A way to provide aliases for a whole domain,
                      // not just a single domain name as with CNAME. rfc2672

  SINK = Type.SINK,   // *** SINK Eastlake
  OPT = Type.OPT,     // *** OPT RFC2671
  APL = Type.APL,     // *** APL [RFC3123]
  DS = Type.DS,       // Delegation Signer rfc3658

  SSHFP = Type.SSHFP,  // *** SSH Key Fingerprint RFC-ietf-secsh-dns
  IPSECKEY = Type.IPSECKEY, // rfc4025
  RRSIG = Type.RRSIG,  // *** RRSIG RFC-ietf-dnsext-dnssec-2535
  NSEC = Type.NSEC,    // *** NSEC RFC-ietf-dnsext-dnssec-2535
  DNSKEY = Type.DNSKEY,// *** DNSKEY RFC-ietf-dnsext-dnssec-2535
  DHCID = Type.DHCID,  // rfc4701

  NSEC3 = Type.NSEC3,  // RFC5155
  NSEC3PARAM = Type.NSEC3PARAM, // RFC5155

  HIP = Type.HIP,      // RFC-ietf-hip-dns-09.txt

  SPF = Type.SPF,      // RFC4408
  UINFO = Type.UINFO,  // *** IANA-Reserved
  UID = Type.UID,      // *** IANA-Reserved
  GID = Type.GID,      // *** IANA-Reserved
  UNSPEC = Type.UNSPEC,// *** IANA-Reserved

  TKEY = Type.TKEY,    // Transaction key rfc2930
  TSIG = Type.TSIG,    // Transaction signature rfc2845

  IXFR = 251,  // incremental transfer [RFC1995]
  AXFR = 252,  // transfer of an entire zone [RFC1035]
  MAILB = 253, // mailbox-related RRs (MB, MG or MR) [RFC1035]
  MAILA = 254, // mail agent RRs (Obsolete - see MX) [RFC1035]
  ANY = 255,   // A request for all records [RFC1035]

  TA = Type.TA,  // DNSSEC Trust Authorities [Weiler] 13 December 2005
  DLV = Type.DLV // DNSSEC Lookaside Validation [RFC4431]
}

public enum QClass
{
  IN,    // the Internet
  CS,    // the CSNET class (Obsolete)
  CH,    // the CHAOS class
  HS,    // Hesiod [Dyer 87]

  ANY    // any class
}

Responses consist of Resource Records (RR).

C#
public class RR
{
  public string NAME;
  public Type Type;
  public Class Class;

  public int TTL;
  public ushort RDLENGTH;
  public Record RECORD;

  public int TimeLived;

  public RR(RecordReader rr);
  // .....
}

Attention, the Record field is a self-reference which makes programming easy, by navigating from RR to Record and from Record to its bold RR data.

C#
public abstract class Record
{
  /// <summary />
  /// The Resource Record this RDATA record belongs to
  /// </summary />
  public RR RR;
}

As of writing, the following inherited Record classes (add a .cs extension for the source file) are defined:

RecordA
RecordNS
RecordMD
RecordMF
RecordCNAME
RecordSOA
RecordMB
RecordMG
RecordMR
RecordNULL
RecordWKS
RecordPTR
RecordHINFO
RecordMINFO
RecordMX
RecordTXT
RecordRP
RecordAFSDB
RecordX25
RecordISDN
RecordRT
RecordNSAP
RecordNSAPPTR
RecordSIG
RecordKEY
RecordPX
RecordGPOS
RecordAAAA
RecordLOC
RecordNXT
RecordEID
RecordNIMLOC
RecordSRV
RecordATMA
RecordNAPTR
RecordKX
RecordCERT
RecordA6
RecordDNAME
RecordSINK
RecordOPT
RecordAPL
RecordDS
RecordSSHFP
RecordIPSECKEY
RecordRRSIG
RecordNSEC
RecordDNSKEY
RecordDHCID
RecordNSEC3
RecordNSEC3PARAM
RecordHIP
RecordSPF
RecordUINFO
RecordUID
RecordGID
RecordUNSPEC
RecordTKEY
RecordTSIG

!!Keep in mind, not all records are implemented yet!!

That's about it. The classes in the project are more or less well documented. So, programming must not be so difficult.

Points of Interest

Nowadays, networks have started using the IPv6 range more and more. In this project, IPv6 is 100% supported. An example of this is when doing lookups for the AAAA record or PTR lookups, like this one (PTR on 2001:0610:0000:800a:b192:0087Blush | :O .5.0.152):

; <<>> Dig.Net 0.0.1 <<>> @192.168.1.254 
    PTR 8.9.0.0.5.0.0.0.7.8.0.0.2.9.1.b.a.0.0.8.0.0.0.0.0.1.6.0.1.0.0.2.ip6.arpa. 
;; global options: printcmd 
;; Got answer: 
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3565 
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 

;; QUESTION SECTION: 
;8.9.0.0.5.0.0.0.7.8.0.0.2.9.1.b.a.0.0.8.0.0.0.0.0.1.6.0.1.0.0.2.ip6.arpa. IN PTR 

;; ANSWER SECTION: 
8.9.0.0.5.0.0.0.7.8.0.0.2.9.1.b.a.0.0.8.0.0.0.0.0.1.6.0.1.0.0.2.ip6.arpa.7200 
    IN PTR www.surfnet.nl. 

;; Query time: 28 msec 
;; SERVER: 192.168.1.254#53(192.168.1.254) 
;; WHEN: Sat Feb 16 02:05:36 2008 
;; MSG SIZE rcvd: 118

Checking this by querying for the AAAA record on www.surfnet.nl:

; <<>> Dig.Net 0.0.1 <<>> @192.168.1.254 AAAA www.surfnet.nl 
;; global options: printcmd 
;; Got answer: 
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 3564 
;; flags: qr rd ra; QUERY: 1, ANSWER: 1, AUTHORITY: 0, ADDITIONAL: 0 

;; QUESTION SECTION: 
;www.surfnet.nl. IN AAAA 

;; ANSWER SECTION: 
www.surfnet.nl. 848 IN AAAA 2001:0610:0000:800a:b192:0087:0.5.0.152 

;; Query time: 3 msec 
;; SERVER: 192.168.1.254#53(192.168.1.254) 
;; WHEN: Sat Feb 16 02:05:26 2008 
;; MSG SIZE rcvd: 60

Yes, I know, these look ugly, but hey, we can address every atom in the universe using IPv6!!

I have got some questions about NAPTR lookups, therefore I have added some code (July 18, 2008) to make these lookups more simple. Here is some example:

C#
Resolver resolver = new Resolver();

string TelephoneNumber = "+1 800-555-5555";
resolver.DnsServer = "E164.org";

string strArpaNumber = Resolver.GetArpaFromEnum(TelephoneNumber);
Response response = resolver.Query(strArpaNumber, QType.NAPTR, QClass.IN);
foreach (RR rr in response.Answers)
  Console.WriteLine(rr.ToString()); 

Output:

;; ANSWER SECTION:
5.5.5.5.5.5.5.0.0.8.1.e164.arpa. 60 IN NAPTR 200 10 "u" "E2U+SIP" "...." .
5.5.5.5.5.5.5.0.0.8.1.e164.arpa. 60 IN NAPTR 200 10 "u" "E2U+SIP" "----" .
5.5.5.5.5.5.5.0.0.8.1.e164.arpa. 60 IN NAPTR 200 10 "u" "E2U+SIP" "++++" .

In the Dig example, the translation of the phone-number is done automatically when using NAPTR lookups (can be switched off by the checkbox). The example ANSWER output is edited to protect some data, shown as .... ---- and ++++ , but it works in real-life Wink | ;-)

History

As of writing, the version of this project is 1.0.0.0:

  • April 4, 2008: Thanks to Martin G C Davies for fixing the GetDNSServers routine to take only the 'OperationalStatus.Up' interfaces.
  • April 4, 2008: Thanks to 'gbonnet' for pointing me to the 'NAPTR' records, it is added to the project.
  • May 20, 2008: Jon Webster has fixed the duplicate entries in DNS servers.
  • July 18, 2008: Added some handy code to do NAPTR lookups.

Version: 1.0.0.1

  • May 20, 2008: The source code / demo code is much newer than this article. Almost any possible DNS record is added to the project. But implementation is not complete. Anyone is invited to implement the 'empty' DNS record types. Wink | ;-)

License

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