Note: this page is part of the 'hello-dns' documentation effort.
(Top)
1 teaching DNS: Library, Authoritative, Resolver
2 Objects in tdns
2.1 DNSLabel
2.2 DNSName
2.3 DNSType, RCode, DNSSection
2.4 tdig
3 Parsing and generating DNS Messages
3.1 RRGens: dealing with all the record types
3.2 DNSMessageReader
3.3 DNSMessageWriter
3.3.1 Compression
3.4 EDNS and truncation
4 Internals
5 Compiling and running tdns
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:
Non-goals are:
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.
tdnsThese 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 DNSLabels, 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 6Similar enums are defined for RCodes (response codes, RCode::Nxdomain for example) and DNS Sections (Question, Answer, Nameserver/Authority, Additional). These too can be printed.
tdig
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.
RRGens: dealing with all the record types
DNS knows many record types, so we need a unified interface that can pass
all of them. For this purpose, tdns uses RRGen instances. RRGens 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.
RRGens 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);
RRGens 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:
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:
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"
For more detauls, read on about tauth, tres
or the C API.