MaraDNS Data Structures

# Summary

This document describes some of the data structures that the MaraDNS program contains; in particular, the data structures which contain the DNS resource records.

# DNS records

The cached and authoritative DNS records are separate data structures in MaraDNS. This is to minimize the interfearance between the cached data and the authoritative data; it should be impossible for any changes to data in the recursive DNS cache to change authoritative DNS data.

Despite these, the two data structures are very similar in form. In fact, the `udpsuccess()` function is used by both the authoritative and recursive code to send out a UDP packet with the answer to the DNS question they have answered.

There are, however, some differences between the two data structures for the following reasons:

- The authoritative data structure has no need to store "this record does not exist" records; the authoritative data structure is a, well, authoritative copy of the data in question; if the data is not there, the record does not exist.

  The caching data structure, however, needs to store "this record does not exist" records; the cache is, by definition, an incomplete image of the DNS space which we only set up so that multiple queries for the same name are faster after the first query.

- The authoritative data structure stores DNS referrals ("I do not have this information, but the information can be found here") in a different format than the recursive data structure. Again, this is because the authoritative data is guaranteed to be complete, and the cache is essentially guaranteed only to be a snaphot of a few records in the DNS space.

  If recursive DNS referrals were stored in the same format as authoritative DNS referrals, MaraDNS would be vulernable to a security attack known as DNS cache poisoning.

- The authoritative dns structure can "cross-link" between different DNS records. For example, if there is a CNAME in the authoritative DNS space, and an IP which corresponds to that CNAME elsewhere in that space, MaraDNS can have a pointer in the CNAME record which points directly to the memory address for the IP corresponding to the CNAME. This is because authoritative data is static in MaraDNS; the data never changes until MaraDNS is restarted.

  With recursive data, on the other hand, the data is very dynamic. Memory pointers are in a constant state of flux. As a result, it is impossible to have one resource record directly point to the memory address of another resource record; direct memory pointers, while extremely fast, can do very nasty things if the expected data is not there. As a result, each record is self-contained; MaraDNS literally copies the IP for a CNAME in to the CNAME resource record; if the IP resource record is removed later on, but the CNAME lingers, the IP in the CNAME will still point to valid data.

Now that I have summarized the differences between the two data strucutres, I will describe, in great technical detail, the DNS data strucutre.

# The DNS hash

The data strucutre that MaraDNS uses is a hash; the program looks for an element in the array by string reference. The process is, roughly speaking, as follows:

- The string, or hash index is converted in to a small number. This is the hashing process. This process allows the array to be relatively small, while allowing fast searches for the data element that corresponds to, say "Awww.maradns.org.".
- If more than one string exists which has the same hash value (turns in to the same number), the array index which the hash value points to will be a list of elements (a linked list, as it turns out), where each element points to the actual string we are trying to find a reference for, and the data which corresponds to that string.
- Once we find the hash element for the string we are looking for data for, we return a pointer to the memory address which has the data we are looking for.

A hash is very efficient for quickly accessing a data element when we know that exact name for what we are looking for.

The strings that MaraDNS' hash strucutre uses are not ordinary strings; instead they are special JsStr objects. JsStr strings are strings which are generated by a special library, which I wrote myself (this is, in fact, the oldest code in the MaraDNS code base; I wrote most of the JsStr code back in the summer of 2000), and has the following features:

- The strings are resistant to buffer overflows.
- It is possible to put ASCII nulls or any other characters in the strings.
- It is possible to have different encodings for different strings, such as binary, ASCII, or ISO 8859-1.

The main feature we take advantage of is the fact that we can put ASCII nulls in the strings; raw "over the wire" DNS queries use binary data which always contains ASCII nulls.

The strings which MaraDNS uses as hash indexes are raw binary strings, based on the raw format of "Domains names" specified in RFC 1035 §3.1. A domain name is a series of "labels", where each label is a one-byte (or "octet") length field followed by a string of characters length long. The domain name is terminated by a label with a length field of zero.

For example, what is in human readable form as "www.maradns.org" is in the following form in DNS "over the wire" packets:

[binary 3] www [binary 7] maradns [binary 3] org [binary 0]

Where [binary number] is a single byte with a value of number. If one were to look at a DNS packet requesting "www.maradns.org" with a hex dump utility, one would see something like this:

    03 77 77 77 07 6D 61 72 61 64 6E 73 03 6F 72 67 00
     ?  w  w  w  ?  m  a  r  a  d  n  s  ?  o  r  g  ?

In addition to have the domain label for www.maradns.org, the hash index string also has a two-byte indicator what what kind of resource records one wishes.

# What is a resource record?

A resource record is the kind of information one wishes to have for a given name, such as www.maradns.org. Usually, one simply wishes to know the IP for www.maradns.org so that one's web browser can contact the web page. This type of request is handled by the A record.

However, it is also possible that one wishes to send email to maradns.org; perhaps one wishes to post to the maradns mailing list. In this case, one would request the mail exchanger for maradns.org; the name server then replies with a list of machines which accept mail for maradns.org. This type of request is handled by the MX record.

In many cases, notably aol.com, the machines which accept incoming mail are different machines than the machines which a person would connect to with a web browser. As a result, programs which send mail send out DNS requests for the mail exchanger (or MX record) for a given domain name label, and most other programs (including web browsers) simply as for the IP (or A record) for the machine in question.

DNS was designed to be extensible. As a result, DNS can potentially have over 65000 different record types; less than 100 different record types for DNS have been proposed in the 22 years DNS has been around. Commonly, only six different record types are used: A, MX, SOA, NS, PTR, and CNAME. A and MX are described above. SOA and NS records are used for, respectively, "this record does not exist" and "Please go here to find the answer to your question" replies. CNAME records, while in common use, are never used by responsible DNS administrators. Should ipv6 ever become a reality, AAAA records will be used by responsible DNS administrators; irresponsible DNS administrators may, unfortunatly, be foolish enough to use A6 or DNAME records.

Each resource record type has a numeric value assosciated with it. "A" records have a numeric value of 1; "MX" records have a numeric value of 15.

# The hash index

In a raw DNS query, a resource record type is a big endian two byte value which is appended to the domain label for the query. MaraDNS' hash uses, as a hash index, a raw binary domain label followed by a 2-byte resource record type. As a result, an A query for www.maradns.org is indexed thusly (using our hypothetical hex dump program again):

    03 77 77 77 07 6D 61 72 61 64 6E 73 03 6F 72 67 00 00 01
     ?  w  w  w  ?  m  a  r  a  d  n  s  ?  o  r  g  ?  ?  ?

A MX request for maradns.org, which a mailing program would send in order to find out which mail server to send email with a suffix of @maradns.org to, is indexed thusly:

    07 6D 61 72 61 64 6E 73 03 6F 72 67 00 00 0F
     ?  m  a  r  a  d  n  s  ?  o  r  g  ?  ?  ?

# The hash data

MaraDNS' hash structure, in addition to having a pointer to the memory address with the hash data in question, has an interger which indicates the kind of data this hash elements has. The authoritative hash (yes, the authoritative and cache data use separate hashes) only uses one datatype for its data: MARA_DNSRR, or a DNS resource record which uses an `rr` data structure.

The rr data structure is as follows (from MaraDns.h):

    /* Structure which contains the data for a DNS record pointed to from
       the hash */
    typedef struct rr {
        qual_timestamp expire; /* When this RR expires (0=authoritative/never) */
        uint32 ttl; /* The TTL for the record in question */
        uint32 authoritative; /* Are we authoritative for this zone */
        struct rr *next; /* Either the first NS for this zone _or_ the next copy
                            of the same record for the same data type */
        struct rr *ip; /* If this is a PTR, MX, or NS record, pointer to the
                          ip the rr domain name points to (if applicable) */
        js_string *ptr; /* If this is a PTR request pointing to a CNAME,
                           we want to give them the PTR record along
                           with the CNAME record. */
        uint16 rr_type; /* Type of resource record this entry is */
        js_string *query; /* Pointer to the query one asks to get this answer */
        js_string *data; /* The actual raw binary data for this RR */
        fila *zap; /* Pointer to a structure used for deleting elements from
                      the cache when the cache starts to fill up */
        char seen; /* This is used by udpsuccess() to insure that a given
                      element in the hash is only visited once */
        perm_t perms; /* What IPs can view this element; making this zero
                         means that all IPs can view the element in question */
        struct rr_list *list; /* rr_list of all records this CNAME refers to */
        char rcode; /* This is used to store the rcode for a given DNS answer */
        } rr;

As can be seen, this can point to other records in two places: From the "next" element of the structure, and from the "ip" element of the structure.

This structure stores a single resource records; a single DNS name can have multiple resource records attached to it. For example, a single computer may not be able to handle all of the traffic our web site is receiving; so we have eight different computers, with different IPs, which distribute the traffic, hopefully evenly, between the eight computers.

DNS can distribute the load among the eight computers; an A query for, say, www.example.com, will return a list of eight IPs. The client chooses one of the IPs at random, and each computer does not need to process as many queries.

The "next" part of this data structure is a pointer to the next DNS resource record in cases where a single name has multiple resource records.

In addition, many DNS records, for better or for worse, point to DNS names instead of, say, IPs (while this makes DNS more flexible, it also make DNS harder to implement). Since a person really wants an IP instead of a name, DNS servers usually supply the IP which corresponds to the name in question. This is why the rr structure as an "ip" pointer; so that MaraDNS, when run in authoritative mode, can give out the IP (if it has it) for any names it makes available in NS or MX records.

DNS can provide various types of data in the answer to a DNS request, depending on the request one sends to a DNS server. All of these replies share one thing in common: The data is a RR header, as described in RFC 1035 §3.2.1, followed by a binary stream of data (called RDDATA in RFC1035). That "data" js_string object is a binary string containing the raw DNS RDDATA for the data in question; MaraDNS creates the RR header from the hash index (which becomes "name" and "type" in the header); this index is pointed to in the "query" element of the RR structure, the CLASS is always 1 (internet), the TTL field of the RR data structure is the TTL in the RR header, and the RDLENGTH in the RR header is obtained from the length value for the js_string object.

In addition, authoritative DNS replies include a listing of the name servers which are the name servers for the query in question. In other words, a DNS question for "where is www.example.com." will get a reply which would translate in to English as "www.example.com is at 10.1.2.3; and the name servers which give the right answers for this question are named ns1.example.com, which has an IP of 10.1.2.4, and ns2.example.com, which has an IP of 10.1.2.5".

The way MaraDNS handles this is somewhat kludgy. After the last answer for a question, the "next" element points to the memory address for the first name server record for the record list in question. When MaraDNS is formulating a reply, it knows that, as soon as the record type changes from a non-NS record type to a NS record type, that we have gone from giving out direct answers to their question to giving out the list of name servers.

# The structure of cached data

Due to the complexities added by caching data, the structure for cache data is somewhat more complicated; there are several cases which we need to deal with:

- The case of a RR being the direct answer to our query.
- The case of a RR being a CNAME answer; in this case, our query contains both the existance of a CNAME; and the A record which the CNAME points to (if it exists).
- The case where we are caching a "this record does not" exist entry.
- The case were we are caching a "the name servers which handle *.example.com. are located at 10.1.2.3 and 10.1.2.4"
- The case where we are caching a "the name servers which handle *.example.com. have the names ns1.example.af.mil. and ns2.example.af.mil."

In all cases, unlike the authoritative data, the "ip" and "next" pointers can not point to other RRs; each RR needs to be a self-contained unit. (The reasons for this are explained previously in this document).

In the first case (a straight RR), the data is the same as the data for an authoritative answer. MaraDNS creates an RR structure in the same format as an RR structure for an authoritative record. The only difference is that cache records do not include a section where the name servers for the record in question are listed; and that the data never points to IPs (e.g. A cache MX record does not have the IPs which correspond to the MX record; this is a non-issue since most mailer programs are smart enough to do separate queries, one to get the list of MX records, and other queries to get the A records which the MX records point to).

This data is indexed in the hash as being an answer for the query asked.

In the second case (the RR being a CNAME answer), the data is the same format for the first, case, with one addition. The one addition is this: the IP data structure element points to a RR structure (especially made for this data) which contains the IP for this CNAME record. The reason we need to do this is because stub resolvers will not perform a second query to find out what the A record which corresponds to a given CNAME record is; if the data is not in the DNS packet which contains the CNAME, the host name in quesiton will not resolve.

This data is indexed in the hash with the same domain name as the question, but where the record type has been changed to CNAME.

In the third case, where we cache a "this record does not exist" answer, the data is almost identical to a postivie RR. The only difference is that the data is a SOA record instead of a reply for the quesiton that they asked; and that the "datatype" for the hash element is a "MARA_DNS_NEG" datatype instead of a standard "MARA_DNSRR" datatype.

The hash index for "this does not exit" replies is the same as the query being asked; the only thing different is the hash datatype.

# How referrals are stored

A referral is a record in the form "all *.example.com records can be found at the name servers with the IPs 10.1.2.3 and 10.1.2.4" or "all *.example.net records can be found at the name servers with the names ns1.example.org and ns2.example.org".

In the case of caching NS referrals, we use a different data structure for storing the data, as follows:

    typedef struct closer {
        unsigned int num_elements; /* Number of elements in the linked list */
        time_t ttd; /* Time for this record to die */
        int datatype; /* This is the RR type of the data (RR_A [in which
                         case, the data is a pointer to a uint32] or RR_NS
                         [which makes the data point to a js_string object])
                       */
        void *data;
        fila *zap; /* So that we can determine in which order to zap RRs */
        struct closer *masked; /* If another record with a ttd after the
                                  ttd for this record exists, we want to
                                  access that data when this record expires */
        struct closer *next; } closer;

The only difference is whether the server which gave us the referral gave us glue records.

If we have glue records, we convert the glue records in to the IPs which are glued to them, and store the data in the hash so that the "closer" structure has a datatype of RR_A, and data points to a unsigned 32-bit integer (an IP).

If we were not given glue record, then the closer struct has a datatype of RR_NS, and data points to a js_string which is a domain label with a 2-byte "type" suffix; the suffix always has a value of 1.

In both cases, the data is indexed in the hash with a domain label followed by a bogus "type" of 252; this is the data type used for an AXFR request, which means that this kind of data will never be cached as an ordinary RR. The data type for the hash element is a MARA_DNS_NS element.

# Removing elements from the cache

The cache, to be effective, only needs to be a small subset of all of the DNS information available on the internet. In order to minimize the memory resources that MaraDNS needs, after the cache expens to a given size, MaraDNS will remove the elements from the cache.

Elements need to be removed from the cache in such a way that:

- Elements removed from the cache have not been recently accessed
- There is not needless overhead for detemining what elements to remove from the cache.
- We do not attempt to remove already-removed elements from the cache

The way MaraDNS accomplishes this is by having a special fila data strcuture. The fila code was developed when I was down in México; fila is the Spanish word for what American English speakers call a "line" and what the rest of the English-speaking world more accurately calls a "queue".

The fila data structure is a circular linked list, is works as follows:

- Every time we create a new resource record in the cache, it is placed at the top of the list.
- Every time we access a resource record (or a nameserver referral), we move that record to the top of the list.
- When we remove records once the cache starts to fill up, we remove the records at the bottom of the list.

The reason why the resource records in the cache need a pointer to the corresponding fila element is because, when a record's TTL expires, we need to remove the element from the cache, and the corresponding fila element.

# Some other information about the rr data structure

## The seen element

The seen element in the rr data structure is used by the `udpsuccess()` routine. `udpsuccess()` is the routine which takes a resource record, and sends it over UDP to a client which wants a given resource record.

Since a resource record has several parts, each of which is its own data structure, it is important to guarantee that a given resource record is only displayed once; the seen element is given a value of one when it is shown.

udpsuccess() also clears out all of the values of seen back to zero after showing all of the elements in question.

## An ip of 255.255.255.255 or 0.0.0.0

Some of MaraDNS' routines allow a query to be performed, and to return an IP based on the query performed. In some cases, we could not get an IP; perhaps there is no such resource record (and we received a "no such record" response when asking for the IP); or perhaps there was an error condition when trying to process the IP. The bogus IP of 0.0.0.0 indicates an error condition; the bogus IP of 255.255.255.255 indicates that a RR for the IP, in fact, does not exist.