hello-dns/tdns/README.md

305 lines
12 KiB
Markdown
Raw Normal View History

2018-04-11 22:32:46 +07:00
<meta charset="utf-8" emacsmode="-*- markdown -*-">
**A warm welcome to DNS**
<link rel="stylesheet" href="https://casual-effects.com/markdeep/latest/apidoc.css?">
2018-04-10 05:05:41 +07:00
# teaching DNS
Welcome to tdns, the teaching authoritative server, implementing all of
2018-04-12 03:12:57 +07:00
basic DNS in ~~1000~~ 1100 lines of code.
2018-04-10 05:05:41 +07:00
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
2018-04-12 03:12:57 +07:00
* Truncation
* EDNS (buffer size, no options)
2018-04-10 05:05:41 +07:00
Missing:
* Compression (may not fit in the 1200 lines!)
2018-04-10 05:05:41 +07:00
Known broken:
* ~~Embedded 0s in DNS labels don't yet work~~
* ~~Case-insensitive comparison isn't 100% correct~~
2018-04-10 05:05:41 +07:00
* RCode after one CNAME chase
* On output (to screen) we do not escape DNS names correctly
2018-04-11 22:32:46 +07:00
* TCP/IP does not follow recommended timeouts
2018-04-10 05:05:41 +07:00
The code is not yet in a teachable state, and the layout is somewhat
confusing: some stuff is in the wrong files.
# Layout
2018-04-11 22:32:46 +07:00
Key to a good DNS implementation is having a faithful DNS storage model,
with the correct kind of objects in them.
2018-04-10 05:05:41 +07:00
Over the decades, many many nameservers have started out with an incorrect
2018-04-11 22:32:46 +07:00
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.
2018-04-10 05:05:41 +07:00
When storing DNS as a tree, as described in RFC 1034, a lot of things go
2018-04-11 22:32:46 +07:00
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
2018-04-11 22:32:46 +07:00
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:
2018-04-11 22:32:46 +07:00
*************************************************************************************************
* *
* .---. *
2018-04-11 22:36:12 +07:00
* 1 +---------+ +--------+ *
2018-04-11 22:32:46 +07:00
* / '-+-' \ *
* / | \ *
* .-+-. .-+-. .-+-. *
* 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
2018-04-11 22:32:46 +07:00
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.
2018-04-11 22:32:46 +07:00
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 `DNSName`s 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, `RRSet`s 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 }
```
2018-04-11 22:32:46 +07:00
Line 3 stores the priority and server name of this MX record (as defined in
lines 10 and 11).
2018-04-10 05:05:41 +07:00
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.
2018-04-10 05:05:41 +07:00
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.
2018-04-10 05:05:41 +07:00
13 to 17 show the construction of the actual DNS resource record in a
packet: the 16 bit priority, followed by the name.
2018-04-11 22:36:12 +07:00
<script>
window.markdeepOptions={};
window.markdeepOptions.tocStyle = "long";
</script>
2018-04-11 22:36:12 +07:00
<!-- Markdeep: --><style class="fallback">body{visibility:hidden;white-space:pre;font-family:monospace}</style><script src="../ext/markdeep.min.js"></script><script>window.alreadyProcessedMarkdeep||(document.body.style.visibility="visible")</script>