<meta charset="utf-8" emacsmode="-*- markdown -*-">
**A warm welcome to DNS**
Note: this page is part of the 'hello-dns' documentation effort.
Welcome to tdns, a 'from scratch' teaching DNS library. Based on tdns
,
tauth
and tres
implement all of basic
DNS and large parts of DNSSEC in 2000 3000 3100
lines of code. Code is
here. To
compile, see the end of this document.
Even though the 'hello-dns' documents describe how basic DNS works, and how
servers should function, nothing quite says how to do things
like actual running code. tdns
is small enough to read in one sitting and
shows how DNS packets are parsed and generated. tdns
is currently written
in C++ 2014, and is MIT licensed. Reimplementations in other languages are
highly welcome, as these may be more accessible to other programmers.
Please contact bert.hubert@powerdns.com or @PowerDNS_Bert if you have plans or feedback.
The goals of tdns
, tauth
& tres
are:
- Showing the DNS algorithms 'in code'
- Protocol correctness, except where the protocol needs updating
- Suitable for educational purposes
- Display best practices, both in DNS and security
- Be a living warning for how hard it is to write a nameserver correctly
Non-goals are:
- Performance
- Implementing more features (unless very educational)
- DNSSEC signing, validation
A more narrative explanation of what tdns
is and what we hope it will
achieve can be found here.
The code for tdns
can be found on GitHub and is also documented
using Doxygen.
These are found in dns-storage.hh and dns-storage.cc.
The most basic object in tdns
is DNSLabel. www.powerdns.com
consists of
three labels, www
, powerdns
and com
. DNS is fundamentally case
insensitive (in its own unique way), and so is DNSLabel. So for example:
DNSLabel a("www"), b("WWW");
if(a==b) cout << "The same\n";
Will print 'the same'.
In DNS a label consists of between 1 and 63 characters, and these characters
can be any 8 bit value, including 0x0
. By making our fundamental data type
DNSLabel
behave like this, all the rest of tdns
automatically gets all
of this right.
When DNS labels contain spaces or other non-ascii characters, and a label needs to be converted for screen display or entry, escaping rules apply. The only place in a nameserver where these escaping rules should be enabled is in the parsing or printing of DNS Labels.
The input to a DNSLabel
is an unescaped binary string. The escaping
example from RFC 4343 thus works like this:
DNSLabel dl("Donald E. Eastlake 3rd");
cout << dl << endl; // prints: Donald\032E\.\032Eastlake\0323rd
A sequence of DNS Labels makes a DNS name. We store such a sequence as a
DNSName
. To make this safe, even in the face of embedded dots, spaces and
other things, within tdns
we make no effort to parse www.powerdns.com
in
the code. Instead, use this:
DNSName sample({"www", "powerdns", "com"});
cout << sample <<"\n"; // prints www.powerdns.com.
sample.pop_back();
cout << sample << ", size: " << sample.size() << sample.size() << '\n';
// prints www.powerdns., size 2
Note: for convenience, when parsing human-generated input, makeDNSName()
is available to make a DNSName from a string.
Since a DNSName
consists of DNSLabel
s, it gets the same escaping. To
again emphasise how we interpret the input as binary, ponder:
DNSName test({"powerdns", "com."});
cout << test << endl; // prints: powerdns.com\..
const char zero[]="p\x0werdns";
DNSName test2({std::string(zero, sizeof(zero)-1), "com"});
cout << test2 << endl; // prints: p\000werdns.com.
These is an enums that contains the names and numerical values of the DNS
types and error codes. This means for example that DNSType::A
corresponds
to 1 and DNSType::SOA
to 6.
To make life a little bit easier, an operator has been defined which allows
the printing of DNSTypes
as symbolic names. Sample:
DNSType a = DNSType::CNAME;
cout << a << "\n"; // prints: CNAME
a = (DNSType) 6;
cout << a <<" is "<< (int)a << "\n"; // prints: SOA is 6
Similar enums are defined for RCodes (response codes, RCode::Nxdomain for example) and DNS Sections (Question, Answer, Nameserver/Authority, Additional). These too can be printed.
To discover how tdns
works, let's start with the basics: sending DNS
queries and parsing responses. For this purpose, the tdig
tool is
provided, somewhat modelled after the famous dig
program created by ISC.
The code:
int main(int argc, char** argv)
{
/* ... */
DNSName dn = makeDNSName(argv[1]);
DNSType dt = makeDNSType(argv[2]);
ComboAddress server(argv[3]);
DNSMessageWriter dmw(dn, dt);
dmw.dh.rd = true;
dmw.setEDNS(4000, false);
This starts out with the basics: it reads a DNSName
from the first
argument to tdns
, a DNSType
from the second and finally a server IP
address from the third argument.
With this knowledge, in line 8 we create a DNSMessageWriter
to make a
question for query name dn
and query type dt
. In addition, we set the
'recursion desired' flag.
Finally on line 10, we indicate our support for up to 4000 byte responses, but we set the 'DNSSEC Ok' flag to false.
Next, mechanics:
1 Socket sock(server.sin4.sin_family, SOCK_DGRAM);
2 SConnect(sock, server);
3 SWrite(sock, dmw.serialize());
4 string resp = SRecvfrom(sock, 65535, server);
5
6 DNSMessageReader dmr(resp);
In line 1 we create a datagram socket appropriate for the protocol of
server
. This is based on a small set of socket wrappers called
simplesockets. On line 2 we
connect and on line 3 we serialize our DNSMessageWriter and send the
resulting packet. On line 4 we receive a response.
Finally on line 6 we parse that response into a DNSMessageReader
.
1 DNSSection rrsection;
2 uint32_t ttl;
3
4 dmr.getQuestion(dn, dt);
5
6 cout<<"Received " << resp.size() << " byte response with RCode ";
7 cout << (RCode)dmr.dh.rcode << ", qname " << dn << ", qtype " << dt << endl;
8 std::unique_ptr< RRGen > rr;
9 while(dmr.getRR(rrsection, dn, dt, ttl, rr)) {
10 cout << dn<< " IN " << dt << " " << ttl << " " << rr->toString() << endl;
11 }
On lines 1 and 2 we declare some variable we'll need later to actually retrieve the resource records. On line 4 we retrieved the name and type we received an answer for, and on line 6 this all is displayed.
Line 8 declares 'rr' ready to receive our Resource Records, which are then
retrieved using the getRR
method from the DNSMessageReader
on line 9.
On line 10 we print what we found. Note that the RRGen
object helpfully
has a toString()
method for human friendly output.
This code is in dnsmessages.cc and dnsmessages.hh.
DNS knows many record types, so we need a unified interface that can pass
all of them. For this purpose, tdns
uses RRGen
instances. RRGen
s are
classes, one for each record type, all deriving from the RRGen
base.
Each RRGen
has a method called toString()
which emits the record's
contents in familiar 'zonefile' format.
RRGen
s can be created using their specific instance types, for example
like this:
ComboAddress ip("203.0.113.1");
auto agen = AGen::make(ip);
cout << agen->toString() << endl; // prints 203.0.113.1
auto soagen = SOAGen::make({"ns1", "powerdns", "com"},
{"bert.hubert", "powerdns", "com"}, 2018102301);
RRGen
s also know how to serialize themselves from a DNSMessageReader
, or how to
write themselves out to a DNSMessageWriter
.
When reading DNS Messages (see below), DNSMessageReader::getRR()
will
return RRGen
instances to you, if you want to do more than print their
contents, you need to cast them to the specific type, for example:
ComboAddress ret;
ret.sin4.sin_family = 0;
if(auto ptr = dynamic_cast<AGen*>(rr.get()))
ret=ptr->getIP();
else if(auto ptr = dynamic_cast<AAAAGen*>(rr.get()))
ret=ptr->getIP();
This code from tres
checks if a record is an IP or IPv6 address and
extracts the IP address - all without using ASCII.
This class reads a DNS message, and makes available:
- The query name (qname) and type (qtype)
- The dnsheader containing the flags
- EDNS buffer size and value of DNSSEC Ok flag
Of specific security note, this is one area where we might potentially have
to do pointer arithmetic. For security purposes, DNSMessageReader
uses
bounds checking access methods exclusively.
Somewhat unexpectedly, parsing a packet does not immediately give the user
access to the query and type of the query (or response). The reason for this
is that there are packets that have no query defined. So to get the query,
call getQuery()
.
Getting resource records from a DNSMessageReader
happens via getRR
which
returns record details and a smart pointer to an RRGen
instance (as
described above).
A good example of how DNSMessageReader
works can be found in
tdig.cc
.
This class creates DNS messages, and in its constructor it needs to know the name and type it is creating a message for.
Packets are only written in order. So it is not possible to
change the qname
after adding a resource record. Resource records must
also be added together as RRSets, and in 'section order'.
Internally DNSMessageWriter
again only uses bounds checked methods for
modifying its state.
A DNSMessageWriter
has a maximum length (set via its constructor). If new
resource record, as written by putRR
, would exceed this maximum length,
that record is rolled back and a std::out_of_range() exception is thrown.
This allows the caller to either truncate or decide this data was optional
anyhow.
Writing actual records to DNSMessageWriter proceeds via putRR()
which
serializes RRGen
instances to the message.
Samples of how to do this can be found in tres.cc and tauth.cc.
DNS compression is unreasonably difficult to get right. In what happens to be a coincidence, it turns out the DNS Tree can also be used to perform DNS name compression.
For every invocation of putName()
in DNSMessageWriter()
we check the DNS
tree if it has a match on the full name, and if not, we add the name
and its components of the name to a DNS tree.
This effectively gets us the desired compression behaviour, except special care has to be taken to not do wildcard processing.
EDNS tells us that a larger buffer size is available. However, even with such a larger buffer size, a packet may exceed the available space. In that case, the standard tells us to truncate the packet, and then still put an EDNS record in the response.
The DNSMessageWriter, in somewhat of a layering violation, takes care of
this in serialize()
.
tdns
uses several small pieces of code not core to dns:
- nenum this is a simple 'named ENUM' construct that enables the printing of DNSName::A
- Simplesocket a small set of convenience functions for making sockets, parsing IP addresses etc.
- Catch2 a unit test framework
This requires a recent compiler version that supports C++ 2014. If you encounter problems, please let me know (see above for address details).
$ git clone https://github.com/ahupowerdns/hello-dns.git
$ cd hello-dns/tdns
$ git submodule init
$ git submodule update
$ make
$ ./tauth [::1]:5300 &
$ dig -t any time.powerdns.org @::1 -p 5300 +short
time.powerdns.org. 3600 IN TXT "The time is Fri, 13 Apr 2018 12:55:54 +0200"