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 ). 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:
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);
}
}
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.
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;
}
}
When not in error, Dig
outputs the header information:
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:
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:
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:
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.
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:
public const string Version = "1.0.0.0";
public const int DefaultPort = 53;
public static readonly IPEndPoint[] DefaultDnsServers;
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();
public int TimeOut;
public int Retries;
public bool Recursion;
public TransportType TransportType;
public IPEndPoint[] DnsServers;
public string DnsServer;
public bool UseCache;
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);
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);
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:
public Response Query(string name, QType qtype, QClass qclass);
These are the complete enumerated lists of QType
and QClass
:
public enum QType : ushort
{
A = Type.A,
NS = Type.NS,
MD = Type.MD,
MF = Type.MF,
CNAME = Type.CNAME,
SOA = Type.SOA,
MB = Type.MB,
MG = Type.MG,
MR = Type.MR,
NULL = Type.NULL,
WKS = Type.WKS,
PTR = Type.PTR,
HINFO = Type.HINFO,
MINFO = Type.MINFO,
MX = Type.MX,
TXT = Type.TXT,
RP = Type.RP,
AFSDB = Type.AFSDB,
X25 = Type.X25,
ISDN = Type.ISDN,
RT = Type.RT,
NSAP = Type.NSAP,
NSAP_PTR = Type.NSAPPTR,
SIG = Type.SIG,
KEY = Type.KEY,
PX = Type.PX,
GPOS = Type.GPOS,
AAAA = Type.AAAA,
LOC = Type.LOC,
NXT = Type.NXT,
EID = Type.EID,
NIMLOC = Type.NIMLOC,
SRV = Type.SRV,
ATMA = Type.ATMA,
NAPTR = Type.NAPTR,
KX = Type.KX,
CERT = Type.CERT,
A6 = Type.A6,
DNAME = Type.DNAME,
SINK = Type.SINK,
OPT = Type.OPT,
APL = Type.APL,
DS = Type.DS,
SSHFP = Type.SSHFP,
IPSECKEY = Type.IPSECKEY,
RRSIG = Type.RRSIG,
NSEC = Type.NSEC,
DNSKEY = Type.DNSKEY,
DHCID = Type.DHCID,
NSEC3 = Type.NSEC3,
NSEC3PARAM = Type.NSEC3PARAM,
HIP = Type.HIP,
SPF = Type.SPF,
UINFO = Type.UINFO,
UID = Type.UID,
GID = Type.GID,
UNSPEC = Type.UNSPEC,
TKEY = Type.TKEY,
TSIG = Type.TSIG,
IXFR = 251,
AXFR = 252,
MAILB = 253,
MAILA = 254,
ANY = 255,
TA = Type.TA,
DLV = Type.DLV
}
public enum QClass
{
IN,
CS,
CH,
HS,
ANY
}
Responses consist of Resource Records (RR
).
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.
public abstract class Record
{
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:0087 .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:
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
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.