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
|
2018-04-12 05:04:59 +07:00
|
|
|
* EDNS (buffer size, no options)
|
2018-04-10 05:05:41 +07:00
|
|
|
|
|
|
|
Missing:
|
2018-04-12 05:04:59 +07:00
|
|
|
* Compression (may not fit in the 1200 lines!)
|
2018-04-10 05:05:41 +07:00
|
|
|
|
|
|
|
Known broken:
|
2018-04-12 05:04:59 +07:00
|
|
|
* ~~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.
|
|
|
|
|
2018-04-13 04:53:01 +07:00
|
|
|
# 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`.
|
|
|
|
|
2018-04-13 04:53:01 +07:00
|
|
|
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 + *
|
|
|
|
* '-+-' '---' *
|
|
|
|
* *
|
|
|
|
*************************************************************************************************
|
|
|
|
|
2018-04-13 04:53:01 +07:00
|
|
|
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`
|
2018-04-13 04:53:01 +07:00
|
|
|
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.
|
|
|
|
|
2018-04-13 04:53:01 +07:00
|
|
|
## 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
|
|
|
|
2018-04-13 04:53:01 +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
|
|
|
|
2018-04-13 04:53:01 +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
|
|
|
|
2018-04-13 04:53:01 +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
|
|
|
|
2018-04-13 04:53:01 +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
|
|
|
|
2018-04-13 04:53:01 +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>
|