hello-dns/tdns
2018-04-12 23:53:01 +02:00
..
ext explain RPZ 2018-04-07 12:02:50 +02:00
.gitignore and gitignore 2018-04-02 13:25:51 +02:00
contents.cc update documentation and content population 2018-04-12 23:53:01 +02:00
dns-storage.cc further big rename 2018-04-12 16:31:43 +02:00
dns-storage.hh further big rename 2018-04-12 16:31:43 +02:00
dns.hh rebase 2018-04-01 18:31:41 +02:00
dnsmessages.cc further big rename 2018-04-12 16:31:43 +02:00
dnsmessages.hh further big rename 2018-04-12 16:31:43 +02:00
Makefile the big rename 2018-04-12 16:22:35 +02:00
nenum.hh lots of work, starting to look useful 2018-04-09 23:04:13 +02:00
README.md update documentation and content population 2018-04-12 23:53:01 +02:00
README.md.html improve README, make it markdeep 2018-04-11 17:32:46 +02:00
record-types.cc the big rename 2018-04-12 16:22:35 +02:00
record-types.hh further big rename 2018-04-12 16:31:43 +02:00
safearray.hh make DNSMessageWriter variable length 2018-04-11 22:12:57 +02:00
tdns.cc further big rename 2018-04-12 16:31:43 +02:00

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

teaching DNS

Welcome to tdns, the teaching authoritative server, implementing all of basic DNS in 1000 1100 lines of code.

The goals of tdns are:

  • Protocol correctness
  • Suitable for educational purposes
  • Display best practices

Non-goals are:

  • Performance
  • Implementing more features

Current status

Features are complete:

  • A, AAAA, NS, MX, CNAME, TXT, SOA
  • UDP & TCP
  • AXFR
  • Wildcards
  • Delegations
  • Glue records
  • Truncation
  • EDNS (buffer size, no options)

Missing:

  • Compression (may not fit in the 1200 lines!)

Known broken:

  • Embedded 0s in DNS labels don't yet work
  • Case-insensitive comparison isn't 100% correct
  • RCode after one CNAME chase
  • On output (to screen) we do not escape DNS names correctly
  • TCP/IP does not follow recommended timeouts

The code is not yet in a teachable state, and the layout is somewhat confusing: some stuff is in the wrong files.

Layout

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 are a fundamental type composed out of DNS Labels with the correct case-insensitive equivalence and identity rules, lots of problems can never happen.

The core or tdns therefore is the tree of nodes as intended in 1034, containing DNS native objects like DNS Labels and DNS Names.

Objects

DNS Objects

DNSLabel

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 of DNS Labels.

DNSName

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

DNSType, RCode, DNSSection

This is an enum that contains the names and numerical values of the DNS types. 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.

The DNS Tree

The DNS Tree is of fundamental importance, and is used a number of times within tdns.

When storing the contents of the org zone, it may look like this:


  •                                                                                           *
    
  •                               .---.                                                       *
    
  • 1 +---------+ +--------+ *
  •                   /           '-+-'          \                                            *
    
  •                  /              |             \                                           *
    
  •               .-+-.           .-+-.          .-+-.                                        *
    
  • 2 + ietf+ | ietg+ | ... + *
  •               '-+-'           '-+-'          '---'                                        *
    
  •                / \              |                                                         *
    
  •               /   \             |                                                         *
    
  •           .--+.    +---.      .-+-.                                                       *
    
  • 3 + ord | | fra + | ... + *
  •           '-+-'    '-+-'      '---'                                                       *
    
  •             |        |                                                                    *
    
  •           .-+-.    .-+-.                                                                  *                   
    
  • 4 + ns1 | | ns2 + *
  •           '-+-'    '---'                                                                  *                   
    
  •                                                                                           *
    

This tree has a depth of four. The top node has an empty name, and is relative to the name of the zone, in this case org.

On layer 4, we find the names ns1.ord.ietf.org and ns2.fra.ietf.org. Key to looking up anything in DNS is to follow the tree downwards and to observe what nodes are passed.

For example, a lookup for www.ietf.org starts as a lookup for www.ietf in the org zone (if loaded, of course). Layer 1 is where we start (and find the Start of Authority record), and we look if there is a child node called ietf. And there is.

As we look at that node, we could see NS records attached to it (ietf.org NS ns1.ord.ietf.org) for example. This means our lookup is done: we've found a zonecut. The authoritative server should now respond with a delegation by returning those NS records in the Nameserver section.

To complete the packet, we need to look up the IPv4 and IPv6 addresses of ns1.ord.ietf.org and ns2.fra.ietf.org. To do this, we traverse the tree downward again, starting at the apex with ns1.ord.ietf and going to the ietf, ord and finally ns1 labels. There we find attached the IP(v6) addresses.

Objects

tdns uses a DNS tree in two places: 1) to quickly find the right zone for a query 2) within that zone, to traverse the names.

The DNS tree within tdns consists of DNSNode objects, each of which can have:

  • Child nodes
  • Pointer to a zone
  • Attached RRSets, keyed on type

The child nodes are always used in the DNS tree. The pointer to a zone is only used when consulting the 'tree of zones'. The attached RRsets meanwhile are only consulted when the right zone is found, to provide actual DNS answers.

Manipulating the tree

To add nodes to the DNS tree, or to add things to existing nodes, use the add method like this:

	newzone->add({"www"})->addRRs(CNAMEGen::make({"server1","powerdns","org"}));
	newzone->add({"www"})->rrsets[DNSType::CNAME].ttl = 1200;

The first line creates the www node, and provisions a CNAME there. The second line updates the new node to set the ttl. Note that addRRs accepts multiple 'generator' parameters, more about which later.

add accepts DNSNames as parameter, so to populate www.fra.ietf.org, use newzone->add({"www", "fra", "ietf", "org"}).

Finding nodes in the tree uses a slightly more complicated method called find. Unlike add it will not modify the tree, even though it has in common that it will return a pointer to a node.

find however also returns some additional things: which parts of the DNSName did not match a node, if a DNS zonecut was encountered while traversing the tree, and what name it had.

The syntax:

	DNSName searchname({"www", "ietf", "org"}), lastname, zonecutname;
	DNSNode* passedZonecut;
	DNSNode* node = bestzone->find(searchname, lastname, &passedZonecut, &zonecutname);

When this operates on the org zone tree displayed above, after the call to find, searchname will be www, while lastname is {"ietf", "org"}. What this means was that the www label could not be matched in the tree, since it isn't there.

passedZonecut is set to the node that describes ietf.org, where NS records live that describe the delegation. zonecutname is therefore set to ietf.org.

To clarify this further, a lookup for ns1.ord.ietf.org would end up with:

  • searchname empty: all labels of ns1.ord.ietf.org were matched
  • lastname is then ns1.ord.ietf.org
  • passedZonecut again points to the {"ietf", "org"} node, which has the
    NS RRSet that describes the delegation
  • zonecutname is set to {"ietf", "org"}.

The DNS Tree is aware of * semantics, and when traversing nodes and not finding a match, it will look for a * node. The tree does not do any special processing for CNAMEs though.

Based on the find method, implementing the RFC 1034 DNS algorithm is very straightforward.

Record generators

As noted above, RRSets contain things like CNAMEGen::make. These are generators that are stored in a DNSNode and that know how to put their content into a DNSMessageWriter. Each implemented DNSType has at least one associated generator. A more complete example of populating a zone looks like this:

	newzone->addRRs(SOAGen::make({"ns1", "powerdns", "org"}, {"admin", "powerdns", "org"}, 1),
	                 NSGen::make({"ns1", "powerdns", "org"}), NSGen::make({"ns2", "powerdns", "org"}),
	                 MXGen::make(25, {"server1", "powerdns", "org"})
	               );
	newzone->add({"server1"})->addRRs(AGen::make("213.244.168.210"), AAAAGen::make("::1"));

This attaches SOA, NS and MX records to the apex of a zone, and defines a server1 node that is also referenced in the MX record.

Since there are many record types, it is imperative that adding a new one needs to happen in only one place. Within tdns, it actually requires two places: the DNSType enum needs to be updated with the numerical value of the type, and a 'XGen` struct needs to be written. Luckily this is simple enough. Here is the entire MX record implementation:

1	struct MXGen : RRGen
2	{
3	  MXGen(uint16_t prio, const DNSName& name) : d_prio(prio), d_name(name) {}
4	  static std::unique_ptr< RRGen > make(uint16_t prio, const DNSName& name)
5	  {
6	    return std::make_unique< MXGen >(prio, name);
7	  }
8	  void toMessage(DNSMessageWriter& dpw) override;
9	  DNSType getType() const override { return DNSType::MX; }
10	  uint16_t d_prio;
11	  DNSName d_name;
12	};

	...

13	void MXGen::toMessage(DNSMessageWriter& dmw) 
14	{
15	  dmw.putUInt16(d_prio);
16	  dmw.putName(d_name);
17	}

Line 3 stores the priority and server name of this MX record (as defined in lines 10 and 11).

Lines 4-7 are mechanics so we can make a smart pointer for an MXGen type using a call to make. This smart pointer is sort of reference counted in that its reference count is always 1. This means there is no overhead.

Line 8 defines the call that transposes this record into a DNSMessageWriter. Line 9 announces to anyone who wants to know what the DNSType of this generator is. This is used by addRRs as shown above to put the generator in the right RRSet place.

13 to 17 show the construction of the actual DNS resource record in a packet: the 16 bit priority, followed by the name.