update markdeep, split up tdns, tauth, tres docs
This commit is contained in:
parent
05314a06c3
commit
b27988b643
@ -52,9 +52,10 @@ The content is spread out over several documents:
|
||||
* [The core of DNS](basic.md.html)
|
||||
* [Relevant to stub resolvers and applications](stub.md.html)
|
||||
* [Relevant to authoritative servers](auth.md.html)
|
||||
* [tdns: a 'from scratch' teaching authoritative server, implementing all of basic DNS in 1100 lines of code](tdns/README.md.html)
|
||||
* [Relevant to resolvers](resolver.md.html)
|
||||
* [tres: a 'from scratch' teaching resolver, implementing all relevant parts of DNS in 500 lines of code](tdns/tres.md.html)
|
||||
* [tdns: a 'from scratch' DNS library](tdns/README.md.html)
|
||||
* [tauth: a minimal but feature complete authoritative server](tdns/tauth.md.html)
|
||||
* [tres: a minimal but feature complete DNS resolver](tdns/tres.md.html)
|
||||
* Optional elements: [EDNS, TSIG, Dynamic Updates, DNAME, DNS Cookies](optional.md.html)
|
||||
* [Privacy related](privacy.md.html): QName minimization, DNS-over-TLS, DNS-over-HTTPS, EDNS Padding
|
||||
* [DNSSEC](dnssec.md.html)
|
||||
|
10
ext/markdeep.min.js
vendored
10
ext/markdeep.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,14 +1,14 @@
|
||||
CXXFLAGS:=-std=gnu++14 -Wall -O2 -MMD -MP -ggdb -Iext/simplesocket -Iext/simplesocket/ext/fmt-5.2.1/include -pthread
|
||||
CFLAGS:= -Wall -O2 -MMD -MP -ggdb
|
||||
|
||||
PROGRAMS = tdns tdig tres tdns-c-test
|
||||
PROGRAMS = tauth tdig tres tdns-c-test
|
||||
|
||||
all: $(PROGRAMS)
|
||||
|
||||
clean:
|
||||
rm -f *~ *.o *.d test $(PROGRAMS)
|
||||
|
||||
check: testrunner tdns tdig
|
||||
check: testrunner tauth tdig
|
||||
./testrunner
|
||||
cd tests ; ./basic
|
||||
|
||||
@ -16,7 +16,7 @@ check: testrunner tdns tdig
|
||||
|
||||
SIMPLESOCKET = ext/simplesocket/comboaddress.o ext/simplesocket/sclasses.o ext/simplesocket/swrappers.o ext/simplesocket/ext/fmt-5.2.1/src/format.o
|
||||
|
||||
tdns: tdns.o record-types.o dns-storage.o dnsmessages.o contents.o tdnssec.o $(SIMPLESOCKET)
|
||||
tauth: tauth.o record-types.o dns-storage.o dnsmessages.o contents.o tdnssec.o $(SIMPLESOCKET)
|
||||
g++ -std=gnu++14 $^ -o $@ -pthread
|
||||
|
||||
tdig: tdig.o record-types.o dns-storage.o dnsmessages.o $(SIMPLESOCKET)
|
||||
|
583
tdns/README.md
583
tdns/README.md
@ -5,14 +5,14 @@ Note: this page is part of the
|
||||
'[hello-dns](https://powerdns.org/hello-dns/)' documentation effort.
|
||||
|
||||
# teaching DNS
|
||||
Welcome to tdns, a 'from scratch' teaching authoritative server,
|
||||
implementing all of [basic DNS](../basic.md.html) in ~~1000~~ ~~1100~~ 1200
|
||||
lines of code. Code is
|
||||
Welcome to tdns, a 'from scratch' teaching DNS library. Based on `tdns`,
|
||||
[`tauth`](tauth.md.html) and [`tres`](tres.md.html) implement all of [basic
|
||||
DNS](../basic.md.html) in ~~1000~~ ~~1100~~ 1200 lines of code. Code is
|
||||
[here](https://github.com/ahupowerdns/hello-dns/tree/master/tdns). To
|
||||
compile, see the end of this document.
|
||||
|
||||
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
|
||||
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
|
||||
@ -22,7 +22,7 @@ Please contact bert.hubert@powerdns.com or
|
||||
[@PowerDNS_Bert](https://twitter.com/PowerDNS_Bert) if you have plans or
|
||||
feedback.
|
||||
|
||||
The goals of tdns are:
|
||||
The goals of `tdns`, `tauth` & `tres` are:
|
||||
|
||||
* Showing the DNS algorithms 'in code'
|
||||
* Protocol correctness, except where the protocol needs updating
|
||||
@ -32,56 +32,14 @@ The goals of tdns are:
|
||||
|
||||
Non-goals are:
|
||||
|
||||
* Performance (beyond 100kqps)
|
||||
* Performance
|
||||
* Implementing more features (unless very educational)
|
||||
* DNSSEC (for now)
|
||||
* DNSSEC signing
|
||||
|
||||
A more narrative explanation of what `tdns` is and what we hope it will
|
||||
achieve can be found [here](intro.md.html).
|
||||
|
||||
# Current status
|
||||
All 'basic DNS' items are implemented:
|
||||
|
||||
* A, AAAA, CNAME, MX, NS, PTR, SOA, NAPTR, SRV, TXT, "Unknown"
|
||||
* UDP & TCP
|
||||
* AXFR (incoming and outgoing)
|
||||
* Wildcards
|
||||
* Delegations
|
||||
* Glue records
|
||||
* Truncation
|
||||
* Compression
|
||||
|
||||
As a bonus:
|
||||
* EDNS (buffer size, no options)
|
||||
|
||||
Missing:
|
||||
* SRV and NAPTR would be nice
|
||||
|
||||
Known broken:
|
||||
* TCP/IP does not follow recommended timeouts
|
||||
|
||||
The code is not quite in a teachable state yet and still contains ugly bits.
|
||||
But well worth [a
|
||||
read](https://github.com/ahupowerdns/hello-dns/tree/master/tdns).
|
||||
|
||||
# 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
|
||||
# Objects in `tdns`
|
||||
These are found in [dns-storage.hh](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/dns-storage.hh)
|
||||
and
|
||||
[dns-storage.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/dns-storage.hh).
|
||||
@ -93,7 +51,7 @@ 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";
|
||||
if(a==b) cout << "The same\n";
|
||||
```
|
||||
Will print 'the same'.
|
||||
|
||||
@ -131,6 +89,9 @@ the code. Instead, use this:
|
||||
|
||||
```
|
||||
|
||||
Note: for convenience, when parsing human-generated input, `makeDNSName()`
|
||||
is available to make a DNSName from a string.
|
||||
|
||||
Since a `DNSName` consists of `DNSLabel`s, it gets the same escaping. To
|
||||
again emphasise how we interpret the input as binary, ponder:
|
||||
|
||||
@ -171,17 +132,17 @@ queries and parsing responses. For this purpose, the `tdig` tool is
|
||||
provided, somewhat modelled after the famous `dig` program created by ISC.
|
||||
|
||||
The code:
|
||||
```
|
||||
1 int main(int argc, char** argv)
|
||||
2 {
|
||||
3 /* ... */
|
||||
4 DNSName dn = DNSNameFromString(argv[1]);
|
||||
5 DNSType dt = makeDNSType(argv[2]);
|
||||
6 ComboAddress server(argv[3]);
|
||||
7
|
||||
8 DNSMessageWriter dmw(dn, dt);
|
||||
9 dmw.dh.rd = true;
|
||||
10 dmw.setEDNS(4000, false);
|
||||
``` C++ linenumbers
|
||||
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
|
||||
@ -239,494 +200,7 @@ On line 10 we print what we found. Note that the `RRGen` object helpfully
|
||||
has a `toString()` method for human friendly output.
|
||||
|
||||
|
||||
# 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 three places: 1) to quickly find the right zone for
|
||||
a query 2) within that zone, to traverse the names 3) DNS name compression.
|
||||
|
||||
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"})`.
|
||||
|
||||
Within `tdns`, the sample `powerdns.org` zone is populated within
|
||||
[contents.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/contents.cc).
|
||||
|
||||
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.
|
||||
|
||||
This code can be found in
|
||||
[record-types.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/record-types.cc)
|
||||
and
|
||||
[record-types.hh](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/record-types.cc).
|
||||
|
||||
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.
|
||||
|
||||
## A bit of fun: dynamic record contents
|
||||
Although names can not easily be dynamic within the DNS tree (either they
|
||||
exist or they don't), contents can be changed at will.
|
||||
|
||||
`tdns` defines a `time.tdns.powerdns.org` node which has a `ClockTXTGen`:
|
||||
|
||||
```
|
||||
newzone->add({"time"})->addRRs(ClockTXTGen::make("The time is %a, %d %b %Y %T %z"));
|
||||
```
|
||||
|
||||
The code behind this generator:
|
||||
|
||||
```
|
||||
void ClockTXTGen::toMessage(DNSMessageWriter& dmw)
|
||||
{
|
||||
struct tm tm;
|
||||
time_t now = time(0);
|
||||
localtime_r(&now, &tm);
|
||||
|
||||
std::string txt("overflow");
|
||||
char buffer[160];
|
||||
if(strftime(buffer, sizeof(buffer), d_format.c_str(), &tm))
|
||||
txt=buffer;
|
||||
|
||||
TXTGen gen(txt);
|
||||
gen.toMessage(dmw);
|
||||
}
|
||||
```
|
||||
Note that this generator uses the existing TXT code to encode itself.
|
||||
# The RFC 1034 algorithm
|
||||
As noted in the [basic DNS](../basic.md.html) and
|
||||
[authoritative](../auth.md.html) pages, the RFC 1034
|
||||
algorithm can be simplified for a pure authoritative server.
|
||||
|
||||
## Finding the right zone and node
|
||||
In [tdns.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/tdns.cc) , processing starts like this:
|
||||
|
||||
```
|
||||
1 DNSName zonename;
|
||||
2 auto fnd = zones.find(qname, zonename);
|
||||
3 ...
|
||||
4 response.dh.aa = 1;
|
||||
5
|
||||
6 auto bestzone = fnd->zone;
|
||||
7 DNSName searchname(qname), lastnode, zonecutname;
|
||||
8 const DNSNode* passedZonecut=0;
|
||||
9 auto node = bestzone->find(searchname, lastnode, &passedZonecut, &zonecutname);
|
||||
```
|
||||
|
||||
In line 1 we declare the DNSName where we will store the name of the
|
||||
matching zone. On line 2 we look up the query name, and get the node
|
||||
containing the zone, plus its name.
|
||||
|
||||
Line 3 elides error response if no zone was found. In line 4 we declare we
|
||||
have authority. Line 6 saves some typing later on.
|
||||
|
||||
Lines 7 and 8 declare what we are looking for, and reserves names for where
|
||||
we store what we found.
|
||||
|
||||
Line 9 finally calls `find` to find the best node within our zone. As noted
|
||||
above, `find` not only finds the best node, but also lets us know if we
|
||||
passed any NS records along the way.
|
||||
|
||||
## If we passed a zone cut
|
||||
|
||||
```
|
||||
1 if(passedZonecut) {
|
||||
2 response.dh.aa = false;
|
||||
3 cout<<"This is a delegation, zonecutname: '" << zonecutname << "'" << endl;
|
||||
4 auto iter = passedZonecut->rrsets.find(DNSType::NS);
|
||||
5 if(iter != passedZonecut->rrsets.end()) {
|
||||
6 const auto& rrset = iter->second;
|
||||
7 vector< DNSName > toresolve;
|
||||
8 for(const auto& rr : rrset.contents) {
|
||||
9 response.putRR(DNSSection::Authority, zonecutname+zonename, DNSType::NS, rrset.ttl, rr);
|
||||
10 toresolve.push_back(dynamic_cast< NSGen* >(rr.get())->d_name);
|
||||
11 }
|
||||
12 addAdditional(bestzone, zonename, toresolve, response);
|
||||
13 }
|
||||
14 }
|
||||
```
|
||||
|
||||
This is the first thing we check: did we pass a zone cut? If so, on line 2
|
||||
we drop the aa bit, since we clearly are not providing an authoritative
|
||||
answer.
|
||||
|
||||
Lines 4 and 5 lookup and verify if there is actually an NS record at the
|
||||
zone cut. This should always be true.
|
||||
|
||||
In line 7 we store room for the NS server names we will need to look up
|
||||
glue for. In line 8 we iterate over the NS records, which we put in the
|
||||
`DNSMessageWriter` on line 9. On line 10 we store glue record names.
|
||||
|
||||
Finally on line 12, we call `addAdditional` which will look up the glue
|
||||
names for us. This completes the response in case of a delegation.
|
||||
|
||||
Note that contrary to RFC 1034, `addAdditional` **only** looks for glue
|
||||
within the `bestzone` itself.
|
||||
|
||||
## NXDOMAIN
|
||||
|
||||
```
|
||||
1 else if(!searchname.empty()) {
|
||||
2 if(!CNAMELoopCount) // RFC 1034, 4.3.2, step 3.c
|
||||
3 response.dh.rcode = (int)RCode::Nxdomain;
|
||||
4 const auto& rrset = bestzone->rrsets[DNSType::SOA];
|
||||
5
|
||||
6 response.putRR(DNSSection::Authority, zonename, DNSType::SOA, rrset.ttl, rrset.contents[0]);
|
||||
7 }
|
||||
```
|
||||
|
||||
If `find` returned with a non-empty `searchname`, it meant there were parts
|
||||
of the query name that could not be matched to a node. We checked for a
|
||||
zonecut earlier (in the previous section), there was none. So this name
|
||||
really does not exist.
|
||||
|
||||
In line 3 we set the response status to NXDOMAIN, unless we've looped
|
||||
through a CNAME already.
|
||||
|
||||
In line 4 we look up the SOA record of our `bestzone` and in line 6 we put
|
||||
it in the message.
|
||||
|
||||
## Node exists
|
||||
At this stage we know a node exists for this name, although it may actually
|
||||
be a wildcard node. We do not actually care if it is. Here is what we have
|
||||
to do first though.
|
||||
|
||||
### Check for a CNAME
|
||||
|
||||
```
|
||||
1 auto iter = node->rrsets.cbegin();
|
||||
2 if(iter = node->rrsets.find(DNSType::CNAME), iter != node->rrsets.end()) {
|
||||
5 const auto& rrset = iter->second;
|
||||
6 response.putRR(DNSSection::Answer, lastnode+zonename, DNSType::CNAME, rrset.ttl, rrset.contents[0]);
|
||||
7 DNSName target=dynamic_cast<CNAMEGen*>(rrset.contents[0].get())->d_name;
|
||||
8 if(target.makeRelative(zonename)) {
|
||||
9 searchname = target;
|
||||
10 if(CNAMELoopCount++ < 10) {
|
||||
11 lastnode.clear();
|
||||
12 zonecutname.clear();
|
||||
13 goto loopCNAME;
|
||||
14 }
|
||||
15 }
|
||||
16 else
|
||||
17 cout<<" CNAME points to record " << target << " in other zone, good luck" << endl;
|
||||
18 }
|
||||
```
|
||||
|
||||
Line 1 defines an iterator for our subsequent lookup in line 2: is there a
|
||||
CNAME at this node? If so, in line 6 we put it in the DNSMessage. In line 7
|
||||
we extract the target of the CNAME.
|
||||
|
||||
In line 8 we again violate the RFC 1034 algorithm by checking if the CNAME
|
||||
points to somewhere within our own zone. If it points to another zone, we
|
||||
are not going to chase this CNAME.
|
||||
|
||||
On line 9 we redirect ourselves if within the same zone. We also check if we
|
||||
haven't looped 'too much' already. It appears everyone has picked the number
|
||||
10 for this. We do some cleanup on lines 11 and 12 and finally on line 13 we
|
||||
restart our algorithm. With a goto.
|
||||
|
||||
### Name exists, no CNAME, matching types
|
||||
```
|
||||
1 if(iter = node->rrsets.find(qtype), iter != node->rrsets.end() || (!node->rrsets.empty() && qtype==DNSType::ANY)) {
|
||||
2 auto range = make_pair(iter, iter);
|
||||
3 if(qtype == DNSType::ANY)
|
||||
4 range = make_pair(node->rrsets.begin(), node->rrsets.end());
|
||||
5 else
|
||||
6 ++range.second;
|
||||
7 for(auto i2 = range.first; i2 != range.second; ++i2) {
|
||||
8 const auto& rrset = i2->second;
|
||||
9 for(const auto& rr : rrset.contents) {
|
||||
10 response.putRR(DNSSection::Answer, lastnode+zonename, i2->first, rrset.ttl, rr);
|
||||
11 if(i2->first == DNSType::MX)
|
||||
12 additional.push_back(dynamic_cast< MXGen* >(rr.get())->d_name);
|
||||
13 }
|
||||
14 }
|
||||
15 }
|
||||
```
|
||||
|
||||
On line 1 is a somewhat tricky lookup that tries to find the query type in
|
||||
the RRSET, and if it could not be found, if the query maybe was for ANY and
|
||||
there are records that could be matched.
|
||||
|
||||
On lines 2 to 6 we either pick the matching RRSet to put in the DNSMessage,
|
||||
or we set it up so we iterate over all types, which we then do on lines 8 to
|
||||
14.
|
||||
|
||||
Note that again we gather up the server name of the MX record for additional
|
||||
processing. If we supported SRV records, we would do the same for them.
|
||||
|
||||
### The name exists, but no types or no types match
|
||||
Finally one of the most vexing parts of DNS: a name that exists, but there
|
||||
are no types or at least no matching types. This could be an 'empty
|
||||
non-terminal', created out of thin air by 'some.long.name.powerdns.org'.
|
||||
This DNS Name populates nodes all along its length, even if no RRSets are
|
||||
attached to 'long.name.powerdns.org' for example.
|
||||
|
||||
In many servers this is tricky, but since we followed a DNS tree based
|
||||
design with nodes, our code is trivial:
|
||||
|
||||
```
|
||||
1 else {
|
||||
2 const auto& rrset = bestzone->rrsets[DNSType::SOA];
|
||||
3 response.putRR(DNSSection::Authority, zonename, DNSType::SOA, rrset.ttl, rrset.contents[0]);
|
||||
4 }
|
||||
```
|
||||
|
||||
All we have to do is 'else' off the previous case, and add the SOA record.
|
||||
|
||||
# AXFR
|
||||
AXFR over TCP/IP consists of a series of DNS messages, each prefixed by a 16
|
||||
bit length field. The first and last RRSet contained within these DNS
|
||||
message(s) must be the SOA record of a zone. Code:
|
||||
|
||||
```
|
||||
1 DNSMessageWriter response(std::numeric_limits< uint16_t >::max()-sizeof(dnsheader));
|
||||
2 DNSName zone;
|
||||
3 auto fnd = zones->find(name, zone);
|
||||
4 if(!fnd || !fnd->zone || !name.empty() || !fnd->zone->rrsets.count(DNSType::SOA)) {
|
||||
5 cout<< " This was not a zone, or zone had no SOA" << endl;
|
||||
6 return;
|
||||
7 }
|
||||
8 response.dh = dm.dh;
|
||||
9 response.dh.ad = response.dh.ra = response.dh.aa = 0;
|
||||
10 response.dh.qr = 1;
|
||||
11 response.setQuestion(zone, type);
|
||||
12
|
||||
13 auto node = fnd->zone;
|
||||
14
|
||||
15 // send SOA
|
||||
16 response.putRR(DNSSection::Answer, zone, DNSType::SOA, node->rrsets[DNSType::SOA].ttl, node->rrsets[DNSType::SOA].contents[0]);
|
||||
17
|
||||
18 writeTCPResponse(sock, response);
|
||||
```
|
||||
|
||||
In line 1 we allocate a `DNSMessageWriter` of maximum size. Lines 2-7 find
|
||||
the best zone, as in the RFC 1034 algorithm. Of specific note is that 'empty
|
||||
non-terminal zones' could be found by this tree walking function, so we check for this.
|
||||
|
||||
The response is then prepared, copying in the original dnsheader (with the
|
||||
transaction id), and setting the flags, qname and qtype correctly.
|
||||
|
||||
Line 13 is again a convenience to save some typing. Line 16 adds the initial
|
||||
SOA record, and the response gets sent out on line 18.
|
||||
|
||||
Note that it is possible to use this first DNSMessage for the initial SOA
|
||||
record and subsequent records too. To keep things simple, we don't do this
|
||||
here.
|
||||
|
||||
Next up is the loop to pass the rest of the zone contents:
|
||||
|
||||
```
|
||||
1 response.setQuestion(zone, type);
|
||||
2
|
||||
3 node->visit([&response,&sock,&name,&type,&zone](const DNSName& nname, const DNSNode* n) {
|
||||
4 for(const auto& p : n->rrsets) {
|
||||
5 if(p.first == DNSType::SOA)
|
||||
6 continue;
|
||||
7 for(const auto& rr : p.second.contents) {
|
||||
8 retry:
|
||||
9 try {
|
||||
10 response.putRR(DNSSection::Answer, nname, p.first, p.second.ttl, rr);
|
||||
11 }
|
||||
12 catch(std::out_of_range& e) { // exceeded packet size
|
||||
13 writeTCPResponse(sock, response);
|
||||
14 response.setQuestion(zone, type);
|
||||
15 goto retry;
|
||||
16 }
|
||||
17 }
|
||||
18 }
|
||||
19 }, zone);
|
||||
20
|
||||
21 writeTCPResponse(sock, response);
|
||||
```
|
||||
|
||||
In line 1, the DNS message is emptied of RRSets. Line 3 launches a visitor
|
||||
that walks the DNS Tree and calls putRR on all RRSets it finds, except the
|
||||
SOA record, which was sent already., so we skip it on line 5.
|
||||
|
||||
Lines 9 to 11 attempt to put this resource record in the message. If the
|
||||
record does not fit, `putRR` rolls back the addition, and throws an
|
||||
exception which we catch on line 12. There we write out the message to TCP,
|
||||
reset the packet, and try again.
|
||||
|
||||
Finally in line 21 we write out the last `DNSMessageWriter` we filled.
|
||||
|
||||
To terminate the AXFR, we now need to resend the SOA record, which we do as
|
||||
follows:
|
||||
|
||||
```
|
||||
response.putRR(DNSSection::Answer, zone, DNSType::SOA, node->rrsets[DNSType::SOA].ttl, node->rrsets[DNSType::SOA].contents[0]);
|
||||
writeTCPResponse(sock, response);
|
||||
```
|
||||
|
||||
Note: this code, in `tcpClientThread` of
|
||||
[tdns.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/record-types.cc)
|
||||
does not yet implement best TCP practices on timeouts and keeping open
|
||||
connections.
|
||||
|
||||
# Parsing and generating DNS Messages
|
||||
This code is in [dnsmessages.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/dnsmessages.cc)
|
||||
@ -739,9 +213,6 @@ This class reads a DNS message, and makes available:
|
||||
* The dnsheader containing the flags
|
||||
* EDNS buffer size and value of DNSSEC Ok flag
|
||||
|
||||
~This is not a general purpose DNS Message reader. It can't parse resource
|
||||
records for example. It is meant for parsing queries.~
|
||||
|
||||
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.
|
||||
@ -763,8 +234,8 @@ back and a std::out_of_range() exception is thrown. This allows the caller
|
||||
to either truncate or decide this data was optional anyhow.
|
||||
|
||||
### Compression
|
||||
DNS compression is unreasonably difficult to get right. In what I am not
|
||||
sure is a coincidence, it turns out the DNS Tree can also be used to perform
|
||||
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
|
||||
@ -803,11 +274,13 @@ $ cd hello-dns/tdns
|
||||
$ git submodule init
|
||||
$ git submodule update
|
||||
$ make
|
||||
$ ./tdns [::1]:5300 &
|
||||
$ ./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"
|
||||
```
|
||||
|
||||
Next up, read about [`tauth`](tauth.md.html) and [`tres`](tres.md.html).
|
||||
|
||||
<script>
|
||||
window.markdeepOptions={};
|
||||
window.markdeepOptions.tocStyle = "long";
|
||||
|
@ -42,7 +42,7 @@ void DNSMessageReader::xfrName(DNSName& res, uint16_t* pos)
|
||||
uint16_t labellen2 = getUInt8(pos);
|
||||
uint16_t newpos = ((labellen & ~0xc0) << 8) | labellen2;
|
||||
newpos -= sizeof(dnsheader); // includes struct dnsheader
|
||||
// cout<< res<<" "<<(*pos -2) << " -> "<<newpos<<endl;
|
||||
|
||||
if(newpos < *pos) {
|
||||
res=res+getName(&newpos);
|
||||
return;
|
||||
|
544
tdns/tauth.md
Normal file
544
tdns/tauth.md
Normal file
@ -0,0 +1,544 @@
|
||||
<meta charset="utf-8" emacsmode="-*- markdown -*-">
|
||||
**A warm welcome to DNS**
|
||||
<!--<link rel="stylesheet" href="https://casual-effects.com/markdeep/latest/apidoc.css?">-->
|
||||
Note: this page is part of the
|
||||
'[hello-dns](https://powerdns.org/hello-dns/)' documentation effort.
|
||||
|
||||
# tauth: the teaching authoritative server
|
||||
`tauth` is based on the [`tdns`](README.md.html) teachable DNS library.
|
||||
`tauth` is a relatively full-featured authoritative server.
|
||||
|
||||
# Current status
|
||||
All 'basic DNS' items are implemented:
|
||||
|
||||
* UDP & TCP
|
||||
* AXFR (incoming and outgoing)
|
||||
* Wildcards
|
||||
* Delegations
|
||||
* Glue records
|
||||
* Truncation
|
||||
* Compression
|
||||
|
||||
As a bonus:
|
||||
* EDNS (buffer size, no options)
|
||||
* Serving of DNSSEC signed zones
|
||||
|
||||
Known broken:
|
||||
* TCP/IP does not follow recommended timeouts
|
||||
|
||||
The code is not quite in a teachable state yet and still contains ugly bits.
|
||||
But well worth [a
|
||||
read](https://github.com/ahupowerdns/hello-dns/tree/master/tdns).
|
||||
|
||||
# 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 `tauth` therefore is the tree of nodes as intended in 1034,
|
||||
containing DNS native objects like DNS Labels and DNS Names.
|
||||
|
||||
# The DNS Tree
|
||||
The DNS Tree is of fundamental importance, and is used a number of times
|
||||
within `tauth`.
|
||||
|
||||
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 three places: 1) to quickly find the right zone for
|
||||
a query 2) within that zone, to traverse the names 3) DNS name compression.
|
||||
|
||||
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"})`.
|
||||
|
||||
Within `tdns`, the sample `powerdns.org` zone is populated within
|
||||
[contents.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/contents.cc).
|
||||
|
||||
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.
|
||||
|
||||
This code can be found in
|
||||
[record-types.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/record-types.cc)
|
||||
and
|
||||
[record-types.hh](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/record-types.cc).
|
||||
|
||||
Since there are many record types, it is imperative that adding a new one
|
||||
needs to happen in only one place. Within `tauth`, 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.
|
||||
|
||||
|
||||
## A bit of fun: dynamic record contents
|
||||
Although names can not easily be dynamic within the DNS tree (either they
|
||||
exist or they don't), contents can be changed at will.
|
||||
|
||||
`tdns` defines a `time.tdns.powerdns.org` node which has a `ClockTXTGen`:
|
||||
|
||||
```
|
||||
newzone->add({"time"})->addRRs(ClockTXTGen::make("The time is %a, %d %b %Y %T %z"));
|
||||
```
|
||||
|
||||
The code behind this generator:
|
||||
|
||||
```
|
||||
void ClockTXTGen::toMessage(DNSMessageWriter& dmw)
|
||||
{
|
||||
struct tm tm;
|
||||
time_t now = time(0);
|
||||
localtime_r(&now, &tm);
|
||||
|
||||
std::string txt("overflow");
|
||||
char buffer[160];
|
||||
if(strftime(buffer, sizeof(buffer), d_format.c_str(), &tm))
|
||||
txt=buffer;
|
||||
|
||||
TXTGen gen(txt);
|
||||
gen.toMessage(dmw);
|
||||
}
|
||||
```
|
||||
Note that this generator uses the existing TXT code to encode itself.
|
||||
# The RFC 1034 algorithm
|
||||
As noted in the [basic DNS](../basic.md.html) and
|
||||
[authoritative](../auth.md.html) pages, the RFC 1034
|
||||
algorithm can be simplified for a pure authoritative server.
|
||||
|
||||
## Finding the right zone and node
|
||||
In [tdns.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/tdns.cc) , processing starts like this:
|
||||
|
||||
```
|
||||
1 DNSName zonename;
|
||||
2 auto fnd = zones.find(qname, zonename);
|
||||
3 ...
|
||||
4 response.dh.aa = 1;
|
||||
5
|
||||
6 auto bestzone = fnd->zone;
|
||||
7 DNSName searchname(qname), lastnode, zonecutname;
|
||||
8 const DNSNode* passedZonecut=0;
|
||||
9 auto node = bestzone->find(searchname, lastnode, &passedZonecut, &zonecutname);
|
||||
```
|
||||
|
||||
In line 1 we declare the DNSName where we will store the name of the
|
||||
matching zone. On line 2 we look up the query name, and get the node
|
||||
containing the zone, plus its name.
|
||||
|
||||
Line 3 elides error response if no zone was found. In line 4 we declare we
|
||||
have authority. Line 6 saves some typing later on.
|
||||
|
||||
Lines 7 and 8 declare what we are looking for, and reserves names for where
|
||||
we store what we found.
|
||||
|
||||
Line 9 finally calls `find` to find the best node within our zone. As noted
|
||||
above, `find` not only finds the best node, but also lets us know if we
|
||||
passed any NS records along the way.
|
||||
|
||||
## If we passed a zone cut
|
||||
|
||||
```
|
||||
1 if(passedZonecut) {
|
||||
2 response.dh.aa = false;
|
||||
3 cout<<"This is a delegation, zonecutname: '" << zonecutname << "'" << endl;
|
||||
4 auto iter = passedZonecut->rrsets.find(DNSType::NS);
|
||||
5 if(iter != passedZonecut->rrsets.end()) {
|
||||
6 const auto& rrset = iter->second;
|
||||
7 vector< DNSName > toresolve;
|
||||
8 for(const auto& rr : rrset.contents) {
|
||||
9 response.putRR(DNSSection::Authority, zonecutname+zonename, DNSType::NS, rrset.ttl, rr);
|
||||
10 toresolve.push_back(dynamic_cast< NSGen* >(rr.get())->d_name);
|
||||
11 }
|
||||
12 addAdditional(bestzone, zonename, toresolve, response);
|
||||
13 }
|
||||
14 }
|
||||
```
|
||||
|
||||
This is the first thing we check: did we pass a zone cut? If so, on line 2
|
||||
we drop the aa bit, since we clearly are not providing an authoritative
|
||||
answer.
|
||||
|
||||
Lines 4 and 5 lookup and verify if there is actually an NS record at the
|
||||
zone cut. This should always be true.
|
||||
|
||||
In line 7 we store room for the NS server names we will need to look up
|
||||
glue for. In line 8 we iterate over the NS records, which we put in the
|
||||
`DNSMessageWriter` on line 9. On line 10 we store glue record names.
|
||||
|
||||
Finally on line 12, we call `addAdditional` which will look up the glue
|
||||
names for us. This completes the response in case of a delegation.
|
||||
|
||||
Note that contrary to RFC 1034, `addAdditional` **only** looks for glue
|
||||
within the `bestzone` itself.
|
||||
|
||||
## NXDOMAIN
|
||||
|
||||
```
|
||||
1 else if(!searchname.empty()) {
|
||||
2 if(!CNAMELoopCount) // RFC 1034, 4.3.2, step 3.c
|
||||
3 response.dh.rcode = (int)RCode::Nxdomain;
|
||||
4 const auto& rrset = bestzone->rrsets[DNSType::SOA];
|
||||
5
|
||||
6 response.putRR(DNSSection::Authority, zonename, DNSType::SOA, rrset.ttl, rrset.contents[0]);
|
||||
7 }
|
||||
```
|
||||
|
||||
If `find` returned with a non-empty `searchname`, it meant there were parts
|
||||
of the query name that could not be matched to a node. We checked for a
|
||||
zonecut earlier (in the previous section), there was none. So this name
|
||||
really does not exist.
|
||||
|
||||
In line 3 we set the response status to NXDOMAIN, unless we've looped
|
||||
through a CNAME already.
|
||||
|
||||
In line 4 we look up the SOA record of our `bestzone` and in line 6 we put
|
||||
it in the message.
|
||||
|
||||
## Node exists
|
||||
At this stage we know a node exists for this name, although it may actually
|
||||
be a wildcard node. We do not actually care if it is. Here is what we have
|
||||
to do first though.
|
||||
|
||||
### Check for a CNAME
|
||||
|
||||
```
|
||||
1 auto iter = node->rrsets.cbegin();
|
||||
2 if(iter = node->rrsets.find(DNSType::CNAME), iter != node->rrsets.end()) {
|
||||
5 const auto& rrset = iter->second;
|
||||
6 response.putRR(DNSSection::Answer, lastnode+zonename, DNSType::CNAME, rrset.ttl, rrset.contents[0]);
|
||||
7 DNSName target=dynamic_cast<CNAMEGen*>(rrset.contents[0].get())->d_name;
|
||||
8 if(target.makeRelative(zonename)) {
|
||||
9 searchname = target;
|
||||
10 if(CNAMELoopCount++ < 10) {
|
||||
11 lastnode.clear();
|
||||
12 zonecutname.clear();
|
||||
13 goto loopCNAME;
|
||||
14 }
|
||||
15 }
|
||||
16 else
|
||||
17 cout<<" CNAME points to record " << target << " in other zone, good luck" << endl;
|
||||
18 }
|
||||
```
|
||||
|
||||
Line 1 defines an iterator for our subsequent lookup in line 2: is there a
|
||||
CNAME at this node? If so, in line 6 we put it in the DNSMessage. In line 7
|
||||
we extract the target of the CNAME.
|
||||
|
||||
In line 8 we again violate the RFC 1034 algorithm by checking if the CNAME
|
||||
points to somewhere within our own zone. If it points to another zone, we
|
||||
are not going to chase this CNAME.
|
||||
|
||||
On line 9 we redirect ourselves if within the same zone. We also check if we
|
||||
haven't looped 'too much' already. It appears everyone has picked the number
|
||||
10 for this. We do some cleanup on lines 11 and 12 and finally on line 13 we
|
||||
restart our algorithm. With a goto.
|
||||
|
||||
### Name exists, no CNAME, matching types
|
||||
```
|
||||
1 if(iter = node->rrsets.find(qtype), iter != node->rrsets.end() || (!node->rrsets.empty() && qtype==DNSType::ANY)) {
|
||||
2 auto range = make_pair(iter, iter);
|
||||
3 if(qtype == DNSType::ANY)
|
||||
4 range = make_pair(node->rrsets.begin(), node->rrsets.end());
|
||||
5 else
|
||||
6 ++range.second;
|
||||
7 for(auto i2 = range.first; i2 != range.second; ++i2) {
|
||||
8 const auto& rrset = i2->second;
|
||||
9 for(const auto& rr : rrset.contents) {
|
||||
10 response.putRR(DNSSection::Answer, lastnode+zonename, i2->first, rrset.ttl, rr);
|
||||
11 if(i2->first == DNSType::MX)
|
||||
12 additional.push_back(dynamic_cast< MXGen* >(rr.get())->d_name);
|
||||
13 }
|
||||
14 }
|
||||
15 }
|
||||
```
|
||||
|
||||
On line 1 is a somewhat tricky lookup that tries to find the query type in
|
||||
the RRSET, and if it could not be found, if the query maybe was for ANY and
|
||||
there are records that could be matched.
|
||||
|
||||
On lines 2 to 6 we either pick the matching RRSet to put in the DNSMessage,
|
||||
or we set it up so we iterate over all types, which we then do on lines 8 to
|
||||
14.
|
||||
|
||||
Note that again we gather up the server name of the MX record for additional
|
||||
processing. If we supported SRV records, we would do the same for them.
|
||||
|
||||
### The name exists, but no types or no types match
|
||||
Finally one of the most vexing parts of DNS: a name that exists, but there
|
||||
are no types or at least no matching types. This could be an 'empty
|
||||
non-terminal', created out of thin air by 'some.long.name.powerdns.org'.
|
||||
This DNS Name populates nodes all along its length, even if no RRSets are
|
||||
attached to 'long.name.powerdns.org' for example.
|
||||
|
||||
In many servers this is tricky, but since we followed a DNS tree based
|
||||
design with nodes, our code is trivial:
|
||||
|
||||
```
|
||||
1 else {
|
||||
2 const auto& rrset = bestzone->rrsets[DNSType::SOA];
|
||||
3 response.putRR(DNSSection::Authority, zonename, DNSType::SOA, rrset.ttl, rrset.contents[0]);
|
||||
4 }
|
||||
```
|
||||
|
||||
All we have to do is 'else' off the previous case, and add the SOA record.
|
||||
|
||||
# AXFR
|
||||
AXFR over TCP/IP consists of a series of DNS messages, each prefixed by a 16
|
||||
bit length field. The first and last RRSet contained within these DNS
|
||||
message(s) must be the SOA record of a zone. Code:
|
||||
|
||||
```
|
||||
1 DNSMessageWriter response(std::numeric_limits< uint16_t >::max()-sizeof(dnsheader));
|
||||
2 DNSName zone;
|
||||
3 auto fnd = zones->find(name, zone);
|
||||
4 if(!fnd || !fnd->zone || !name.empty() || !fnd->zone->rrsets.count(DNSType::SOA)) {
|
||||
5 cout<< " This was not a zone, or zone had no SOA" << endl;
|
||||
6 return;
|
||||
7 }
|
||||
8 response.dh = dm.dh;
|
||||
9 response.dh.ad = response.dh.ra = response.dh.aa = 0;
|
||||
10 response.dh.qr = 1;
|
||||
11 response.setQuestion(zone, type);
|
||||
12
|
||||
13 auto node = fnd->zone;
|
||||
14
|
||||
15 // send SOA
|
||||
16 response.putRR(DNSSection::Answer, zone, DNSType::SOA, node->rrsets[DNSType::SOA].ttl, node->rrsets[DNSType::SOA].contents[0]);
|
||||
17
|
||||
18 writeTCPResponse(sock, response);
|
||||
```
|
||||
|
||||
In line 1 we allocate a `DNSMessageWriter` of maximum size. Lines 2-7 find
|
||||
the best zone, as in the RFC 1034 algorithm. Of specific note is that 'empty
|
||||
non-terminal zones' could be found by this tree walking function, so we check for this.
|
||||
|
||||
The response is then prepared, copying in the original dnsheader (with the
|
||||
transaction id), and setting the flags, qname and qtype correctly.
|
||||
|
||||
Line 13 is again a convenience to save some typing. Line 16 adds the initial
|
||||
SOA record, and the response gets sent out on line 18.
|
||||
|
||||
Note that it is possible to use this first DNSMessage for the initial SOA
|
||||
record and subsequent records too. To keep things simple, we don't do this
|
||||
here.
|
||||
|
||||
Next up is the loop to pass the rest of the zone contents:
|
||||
|
||||
```
|
||||
1 response.setQuestion(zone, type);
|
||||
2
|
||||
3 node->visit([&response,&sock,&name,&type,&zone](const DNSName& nname, const DNSNode* n) {
|
||||
4 for(const auto& p : n->rrsets) {
|
||||
5 if(p.first == DNSType::SOA)
|
||||
6 continue;
|
||||
7 for(const auto& rr : p.second.contents) {
|
||||
8 retry:
|
||||
9 try {
|
||||
10 response.putRR(DNSSection::Answer, nname, p.first, p.second.ttl, rr);
|
||||
11 }
|
||||
12 catch(std::out_of_range& e) { // exceeded packet size
|
||||
13 writeTCPResponse(sock, response);
|
||||
14 response.setQuestion(zone, type);
|
||||
15 goto retry;
|
||||
16 }
|
||||
17 }
|
||||
18 }
|
||||
19 }, zone);
|
||||
20
|
||||
21 writeTCPResponse(sock, response);
|
||||
```
|
||||
|
||||
In line 1, the DNS message is emptied of RRSets. Line 3 launches a visitor
|
||||
that walks the DNS Tree and calls putRR on all RRSets it finds, except the
|
||||
SOA record, which was sent already., so we skip it on line 5.
|
||||
|
||||
Lines 9 to 11 attempt to put this resource record in the message. If the
|
||||
record does not fit, `putRR` rolls back the addition, and throws an
|
||||
exception which we catch on line 12. There we write out the message to TCP,
|
||||
reset the packet, and try again.
|
||||
|
||||
Finally in line 21 we write out the last `DNSMessageWriter` we filled.
|
||||
|
||||
To terminate the AXFR, we now need to resend the SOA record, which we do as
|
||||
follows:
|
||||
|
||||
```
|
||||
response.putRR(DNSSection::Answer, zone, DNSType::SOA, node->rrsets[DNSType::SOA].ttl, node->rrsets[DNSType::SOA].contents[0]);
|
||||
writeTCPResponse(sock, response);
|
||||
```
|
||||
|
||||
Note: this code, in `tcpClientThread` of
|
||||
[tdns.cc](https://github.com/ahupowerdns/hello-dns/blob/master/tdns/record-types.cc)
|
||||
does not yet implement best TCP practices on timeouts and keeping open
|
||||
connections.
|
||||
|
||||
<script>
|
||||
window.markdeepOptions={};
|
||||
window.markdeepOptions.tocStyle = "long";
|
||||
</script>
|
||||
<!-- 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>
|
1
tdns/tauth.md.html
Symbolic link
1
tdns/tauth.md.html
Symbolic link
@ -0,0 +1 @@
|
||||
tauth.md
|
Loading…
Reference in New Issue
Block a user