hello-dns/tdns/intro.md
bert hubert 4614e7e0c6 wip
2018-04-17 16:22:44 +02:00

9.0 KiB

            <meta charset="utf-8" emacsmode="-*- markdown -*-">
                        **A warm welcome to DNS**

Note: this page is part of the 'hello-dns' documentation effort.

teaching DNS

Welcome to tdns, a 'from scratch' teaching authoritative server, implementing all of basic DNS in 1000 1100 1200 lines of code. Code is here. To compile, see here.

tdns is part of the 'hello-dns' effort to provide a good entrypoint into DNS. This project was started after an IETF presentation in which it was discovered the DNS standards have now grown to 2500 pages, and we can no longer expect new entrants to the field to read all that. After 30 years, DNS deserves a fresh explanation and hello-dns is it.

Even though the 'hello-dns' documents describe how basic DNS works, and how an authoritative server 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.

The goals of tdns 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 nameserver correctly

Non-goals are:

  • Performance (beyond 100kqps)
  • Implementing more features (unless very educational)
  • DNSSEC (more about which later)

Besides being 'teachable', tdns could actually be useful if you need a 4-file dependency-light library that can look up DNS things for you.

Features

Despite being very small, tdns covers a lot of ground, with all 'basic DNS' items are implemented:

  • A, AAAA, CNAME, MX, NS, PTR, SOA, TXT, "Unknown"
  • UDP & TCP
  • AXFR (incoming and outgoing)
  • Wildcards
  • Delegations
  • Glue records
  • Truncation
  • Compression / Decompression

As a bonus:

  • EDNS (buffer size, no options)

What this means is that with tdns, you can actually host your domain name, or even slave it from another master server.

What makes tdig different?

There is no shortage of nameservers. In fact, there is an embarrassing richness of very good ones out there already. So why bother? The biggest problem with DNS today is not the great open source implementations. It is the absolutely dreary stuff we find in appliances, modems, load balancers, CDNs, CPEs and routers.

The DNS community frequently laments how much work our resolvers have to do to work around broken implementations. In fact, we are so fed up with this that ISC, NLNetLabs, CZNIC and PowerDNS together have announced that starting 2019 we will no longer work around certain classes of breakage.

In addition, with the advent of RFCs like 8020 sending incorrect answers will start wiping out your domain name.

However, we can't put the all of the blame for disappointing quality on the embedded and closed source implementation community. It was indeed frighteningly hard to out find how to write a correct authoritative nameserver.

Existing open source nameservers are all highly optimized and/or have decades of operational expertise worked into their code. What this means is that actually reading that code to learn about DNS is not easy. Achieving millions of queries per second does not leave the luxury of keeping code as straightforward as possible!

tdns addresses this gap by being a 1500 line long server that is well documented and described. Any competent programmer can read the entire source code a few hours and observe how things should be done.

That sounds like hubris

In a sense, this is by design. tdns attempts to do everything not only correctly but also in a best practice fashion. It wants to be an excellent nameserver that is fully compliant to all relevant standards and lore.

It is hoped that the DNS community will rally to this cause and pour over the tdns source code to spot everything that could potentially be wrong or could be done better.

In other words, were tdns is currently not right, we hope that with sufficient attention it soon will be.

How did all those features fit in 1200 lines?

Key to a good DNS implementation is having a faithful DNS storage model, with the correct kind of objects in them.

Over the decades, many many nameservers have started out with an incorrect storage model, leading to pain later on with empty non-terminals, case sensitivity, setting the 'AA' bit on glue (or not) and eventually DNSSEC ordering problems.

When storing DNS as a tree, as described in RFC 1034, a lot of things go right "automatically". When DNS Names and are a fundamental type composed out of DNS Labels with the correct case-insensitive equivalence and identity rules, lots of problems can never happen. Lots of conversion mechanics also does not need to be written.

The core or tdns therefore is the tree of nodes as intended in 1034, containing DNS native objects like DNSLabels and DNSNames. These get escaping, case sensitivity and binary correctness right 'automatically'.

The DNS Tree

Of specific note is the DNS Tree as described in RFC 1034. Because DNS is never shown to us as a tree, and in fact is presented as a flat 'zone file', it is easy to ignore the tree-like nature of DNS.

If this tree is embraced however, it turns out that a nameserver can use the very same DNS tree implementation three times:

  1. To find the most specific zone to be serving answers from
  2. To traverse that zone to find the correct answers or delegation
  3. To implement DNS name compression

By reusing the same logic three times, there is less code to type and less to explain.

Interestingly, when asked (via Paul Vixie), Paul Mockapetris indicated he was surprised that the DNS Tree could in fact be reused for DNS name compression. This 2018 discovery in a 1985 protocol turns out to work surprisingly well!

The fundamental symmetry of DNS

In what is likely not an accident, all known DNS record types are laid out exactly the same in the packet as in the zone file. So in other words the well known SOA record looks like this on our screen:

ripe.net. SOA	manus.authdns.ripe.net. dns.ripe.net. 1523965801 3600 600 864000 300
#                      mname               rname         serial  ref  ret  exp   min

And in the packet, this very same record looks like this:


  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • / MNAME / *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • / RNAME / *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • | SERIAL | *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • | REFRESH | *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • | RETRY | *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • | EXPIRE | *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *
  • | MINIMUM | *
  • +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+ *

DNS Records need to be: 1) parsed from a message 2) serialized to a message 3) parsed from zone file format 4) emitted in zone file format.

It turns out these four conversions exhibit complete symmetry for all regular DNS resource types.

This means we can define one conversion 'operator':

void SOAGen::doConv(auto& x) 
{
  x.xfrName(d_mname);     x.xfrName(d_rname);
  x.xfrUInt32(d_serial);  x.xfrUInt32(d_refresh);
  x.xfrUInt32(d_retry);   x.xfrUInt32(d_expire);
  x.xfrUInt32(d_minimum);
}

And actually reuse that for all four cases:

SOAGen::SOAGen(DNSMessageReader& dmr)         { doConv(dmr); }
void SOAGen::toMessage(DNSMessageWriter& dmw) { doConv(dmw); }
SOAGen::SOAGen(StringReader& sr)              { doConv(sr);  }

std::string SOAGen::toString() 
{
  StringBuilder sb;
  doConv(sb);
  return sb.d_string;
}