The Domain Name System (DNS) is a distributed database that resolves human-readable URLs
(such as stuff.com
or k12.county.edu
) into IP addresses. The basic structure and operation
of DNS is defined in RFCs 1034 and 1035. DNS defines a hierarchical name space, illustrated in
Figure 4.6.1, that are controlled by multiple name servers. At the
highest level is the root name server, which is denoted with a dot ("."
). The
Internet Corporation for Assigned Names and Numbers (ICANN) is a nonprofit organization that
governs and maintains the root structures of the DNS hierarchy. The level just below the root is the
set of top-level domains (TLDs), which provide structure based on the
type of service that the registered organization provides. Each TLD is governed and maintained by a
separate company or organization, such as Verisign. These organizations coordinate their work with
ICANN to maintain the core of the DNS hierarchy.
Levels in the DNS hierarchy are indicated by dots within the domain name. For instance, an education
institution (such as a university or school district) would register their domain names under the
.edu
TLD, establishing ownership of a domain name such as university.edu
. Commercial
enterprises and other businesses use the .com
TLD, reserving domain names like business.com
.
Non-profit organizations register domain names under the .org
TLD, establishing names like
charity.org
.
Note
The names university.edu
, business.com
, and charity.org
are intended to illustrate the
types of entities that might use these TLDs. These domains are actually registered to real
organizations (CapStone University, a private registrant, and Global Impact, respectively). None of
the addresses or descriptions in this section are intended to refer to these specific entities.
Organizations themselves can extend the DNS hierarchy based on their own needs and services. The
organizations manage this by setting up and running their own authoritative name servers. For
instance, charity.org
might use the separate domain names mail.charity.org
and
www.charity.org
to distinguish their email server from the server for their web page. These
names are considered subdomains of the charity.org
domain name.
Bug Warning
Once an organization has registered a domain name with the appropriate TLD, that organization has
established control of all subdomains within their zone of authority. That is, the organization
that has set up the authoritative name server for charity.org
has administrative control over
every domain name that ends with those fields. Note, though, that this control does not extend to
similar-looking domain names. The key distinction is the presence of the "."
immediately
preceding charity.org
in a domain name. That is, the owners of charity.org
would have the
authority for the domain names mail.charity.org
and www.charity.org
; they would not,
however, have control over mailcharity.org
or wwwcharity.org
. Registering similar-looking
domain names is a common technique for criminal or other malicious groups that are attempting to
take advantage of users who make a mistake typing the URL or those who might overlook the missing
"."
as part of a spam email message in a phishing attack.
DNS is designed to be a resilient system for resolving addresses. As such, there is not actually a
single root DNS server. As of this writing, there are currently 13 root servers operating
world-wide. These 13 root servers communicate with each other to maintain a consistent database of
IP addresses for the TLD servers. Again, as of this writing, there are currently over 1500 TLD
extensions. These extensions include the original seven TLDs (.com
, .edu
, .gov
,
.int
, .mil
, .net
, and .org
). Other TLD extensions indicate country codes (such as
.uk
for the United Kingdom or .ca
for Canada), though many companies repurposed country
codes to make their domain names more readable. Some examples of this practice include
del.icio.us
and bit.ly
, which used the country codes for the United State and Libya to
create readable domain names; in the former case, a company registered the domain name icio.us
with the US domain registrar, then created the del.icio.us
entry within its own authoritative
name server. In more recent years, ICANN has expanded the TLD extensions to include topical names,
such as .car
, .hospital
, or .restaurant
.
To translate a domain name into an IP address, a user program (such as a web browser) contacts a
local process known as a resolver. The resolver maintains a master file that
contains a local database of pre-defined addresses along with a cache of recently translated
addresses. If the master file contains the address for the requested domain name, the resolver can
return the answer immediately. For addresses that are not in the master file, the resolver would
then consult the larger DNS structure. The pre-defined addresses in the master file include the
addresses of the 13 root servers. For instance, the root name server a.root-servers.net
has a
persistent IP address of 198.41.0.4. These addresses serve as the entry point to the Internet’s DNS
database.
DNS defines two strategies that resolvers can adopt. In the iterative strategy, a DNS resolver will send repeated queries to different servers until it can resolve the request. For instance, a home user’s laptop may send a DNS request to their ISP or a public DNS service like OpenDNS. This DNS resolver had only the root server addresses, it would contact a root server to get the address of the TLD server; this same DNS resolver would then send a request to the TLD requesting the address of the authoritative name server, and so on. In the recursive strategy, the DNS resolver would simply forward the request to a different resolver that would take control. This approach is illustrated by the same scenario, because the laptop itself has a DNS resolver; instead of iteratively contacting the name servers to resolve the request, the laptop sent a single request to the ISP DNS resolver or OpenDNS. The DNS specification requires all resolvers implement an iterative solution, while the recursive strategy is optional.
Figure 4.6.4 illustrates the iterative sequence of messages sent when a web browser
tries to resolve www.charity.org
. [1] The resolver finds the root server address and sends a
query to 198.41.0.4 to look up the .org
TLD address. The root name server responds with
199.19.56.1 as the address for the .org
TLD name server. The resolver then contacts that server
to get the address of the authoritative name server for .charity.org
, which is at address
12.34.56.78. Finally, the resolver contacts the authoritative name server that indicates
www.charity.org
can be found at 12.34.56.80.
If every DNS query required the steps in Figure 4.6.4, the system would suffer from
terrible performance. Every time someone accessed a web page, sent an email, or streamed a piece of
music, the client would have to contact one of the 13 root servers; these servers would quickly
crash from the strain of handling these requests. Instead, all levels of the DNS system employ an
extensive amount of caching. As such, except under rare circumstances, the master file on the local
machine already has the IP address for the .org
TLD when the original request is received.
Similarly, if the request for www.charity.org
is followed by requests for www-1.charity.org
,
mail.charity.org
, or any other subdomain, the resolver would not contact either the root or
.org
TLD name servers, as the resolver’s cache would already have the address of the needed
authoritative name server. In addition to caching, the TLD and authoritative name servers also
employ replication across multiple IP addresses. That is, 199.19.56.1 is one of several IP
addresses that correspond to the .org
TLD. The resolver can contact any of these addresses and
is likely to get the same results.
The translation information for DNS queries are stored and sent in structures known as
resource records. Table 4.6 shows the generic structure of
every resource record. The NAME
and RDATA
fields (indicated with the wavy lines in the
table) are variable length; all other fields are exactly 16 bits wide. The NAME
field (also
called the owner) is the human-readable domain name of the record. The TYPE
provides information
about the resource under consideration, as described below. The CLASS
designates the protocol
stack in use, which is IN
to indicate Internet. The TTL
field indicates how many seconds the
record should be considered valid in the local host’s cache; once the record expires, the resolver
should repeat the query to check if the record has been updated. Finally, the RDATA
field
includes the actual data of the record, which is tied to the type, and the RDLENGTH
indicates
the length of RDATA
.
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
NAME | |||||||||||||||
TYPE | |||||||||||||||
CLASS | |||||||||||||||
TTL | |||||||||||||||
RDLENGTH | |||||||||||||||
RDATA |
Table 4.6: Generic structure of a DNS resource record
There are several common types of resources records. The A
type denotes a host address, so the
RDATA
field would contain the IP address for the domain name. The CNAME
type denotes a
canonical name record that maps an alias to the definitive domain name. For instance, a
company might create the subdomains ftp.example.com
and www.example.com
, though both of
these addresses are handled by the server identified by the name example.com
. Resource records
for example.com would have an A
type with the server’s IP address, while resource records for both
ftp.example.com
and www.example.com
would have CNAME
type, with example.com
in the
RDATA
field. A third common resource record type is NS
, which indicates the authoritative name
server for the domain. For instance, the RDATA
field for the NS
record for example.com
would contain the domain name of the authoritative server for the example.com
domain. Finally,
an MX
record type is used to store information about mail exchange servers that are responsible
for delivering email.
To illustrate the example, consider again the fictional scenario in Figure 4.6.4. When
the resolver contacted the .org
TLD name server at 199.19.56.1, this server might first reply
with an NS resource record for the charity.org
domain. This authoritative name server might have
a domain name like ns.charity.org
, which would be the contents of the RDATA
field for the NS
resource record. To resolve this address, a second response would contain a resource record with the
A type containing the address of the name server 12.34.56.78. Complicating matters further, this
authoritative name server might initially respond with a CNAME
resource record to indicate that
www.charity.org
is an alias for a0.web.charity.org
. A second response from the
charity.org
authoritative name server would then return an A resource record to indicate that
a0.web.charity.org
can be accessed at 12.34.56.80.
Like HTTP/1.0, the DNS protocol is a simple request-response protocol with no persistent state
between messages, but DNS uses UDP instead of TCP. That is, a DNS client can construct the datagram
format specified by the RFC and send it to an arbitrary server as a UDP message with no prior
connection. The server would then respond to the IP address in the UDP datagram header with the
response. The DNS message itself contains five fields. The header
field indicates if the message
contains a query, a response, or another type of message. The question field contains the domain
name that is being queried (the QNAME
), along with the class (QCLASS
) and type (QTYPE
)
of resource record requested. For a DNS response, the answer
field would contain the resource
record. The authority
and additional
fields provide additional information.
To illustrate the structure of a DNS query and response, consider a request to resolve the domain
name example.com
. [2] Table 4.7 shows the interpretation of the bytes of the
request. (Note that the exact structure of the UDP datagram consists of just the bytes shown,
concatenated in order: 123401000001...
) The header starts with a 16-bit randomly chosen
identifier denoted as XID (1234
in our example), followed by a 16-bit value that serves as a bit
mask. The structure of the bit mask is shown in Table 4.8. The rest of the header after the
bit-mask indicates how many entries are in each of the other fields, each as a 16-bit value.
Header | 1234 |
XID=0x1234 [random identifier] |
0100 |
OPCODE=SQUERY |
|
0001 0000 0000 0000 |
1 question field |
|
Question | 0765 7861 6d70 6c65 0363 6f6d 00 |
QNAME=EXAMPLE.COM., |
0001 0001 |
QCLASS=IN, QTYPE=A |
|
Answer | <empty> |
|
Authority | <empty> |
|
Additional | <empty> |
Table 4.7: Sequence of bytes and their interpretation to query DNS for example.com
Table 4.8 illustrates the structure of the 16-bit flag field that follows the XID field of
a DNS header. In the message shown in Table 4.7, the only bit set is the Recursion Desired
(RD
) bit. The Opcode
field indicates that this is a standard query (SQUERY = 0000
). In
the response shown in Table 4.9, the flag value is 0x8180
, which means that the Query
Response (QR
) bit has been set to indicate the message is a response, as well as the Recursion
Available
(RA
) bit. The RCODE
field is used to indicate if an error occurs, and all 0 bits
there indicates there was no error processing the query. Information on the other fields is
available in RFC 1035.
Bit Index | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Meaning | QR |
Opcode |
AA |
TC |
RD |
RA |
Z |
RCODE |
||||||||
Value | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
Hex Value | 0 | 1 | 0 | 0 |
Table 4.8: Structure of the flags field in a DNS query header
The question field of the request starts with the fields of the domain name, with each field
preceded by the number of bytes in the field. That is, the domain name in the question does not
contain the standard dotted format for the name. In this example, the domain name (QNAME
[3]) starts with 7 characters (65 78 61 6d 70 6c 65 = "EXAMPLE"
) followed by a field with 3
characters (63 6f 6d = "COM"
). The QNAME continues until the first 0-byte (00
) is
encountered, as this indicates a 0-length field. The remainder of the question indicate the class
(0001 = IN
) and the type of resource record sought (0001 = A
).
Table 4.9 shows the response for the query from Table 4.7. In the response, the
header
is almost identical to that of the request. The randomly chosen identifier XID
should
match the original request; if the resolver has sent multiple requests, the XID
field allows the
resolver to determine which request is being answered. The bit mask has been modified to denote that
this message is a response and the recursive resolution strategy is available. The header
also
indicates that a single answer has been provided. The question
field is identical to the
original request.
Header | 1234 |
XID=0x1234 [random identifier] |
8180 |
OPCODE=SQUERY, RESPONSE, RA |
|
0001 0001 0000 0000 |
1 question and 1 answer |
|
Question | 0765 7861 6d70 6c65 0363 6f6d 00 |
QNAME=EXAMPLE.COM., |
0001 0001 |
QCLASS=IN, QTYPE=A |
|
Answer | <empty> |
|
c00c |
QNAME=EXAMPLE.COM. [compressed] |
|
0001 |
QTYPE=A |
|
0001 |
QCLASS=IN |
|
0000 e949 |
TTL = 0xe949 = 59721 |
|
04 |
RDLENGTH = 4 |
|
5db8 d822 |
RDATA = 0x5db8d822 [93.184.216.34] |
|
Authority | <empty> |
|
Additional | <empty> |
Table 4.9: Sequence of bytes and their interpretation for the example.com DNS response
The answer field contains the resource record, which adheres to the general structure defined in
Table 4.6. The record is an A
-type Internet (IN
class) with a time-to-live of
59,721 seconds. The RDLENGTH
indicates a length of four bytes for the RDATA
, which contains
an IPv4 address. Note that the address is simply a 32-bit number 0x4db8d822
; by interpreting
each byte as a separate number, this denotes the dotted decimal address 93.184.216.34.
The QNAME
field of the resource record is employing a compression technique to keep the message
as small as possible. That is, since the question field already contains the domain name, there is
no need to repeat the string in the answer. DNS indicates the compression is being used by setting
the first two bits of the answer field to 11
(hence the first byte is 0xc
). Ignoring those
two bits, the next 14 bits (0x000c
after clearing out the two “11
” bits) indicate the
location of the name as a byte offset within the message. That is, the answer is pointing to the
byte offset 12 (0xc
) within the datagram, which is where the 07657861...
starts.
To illustrate how to work with DNS in code, we start by declaring the following types for a DNS
header and question. The dns_header_t
and dns_question_t
type definitions are those used in
the macOS DNS implementation and are present in the dns_util.h
header file. However, these are
not part of the POSIX standard, so they do not exist on other systems. [4] We use them here for
convenience to construct the query.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | typedef struct {
uint16_t xid; /* Randomly chosen identifier */
uint16_t flags; /* Bit-mask to indicate request/response */
uint16_t qdcount; /* Number of questions */
uint16_t ancount; /* Number of answers */
uint16_t nscount; /* Number of authority records */
uint16_t arcount; /* Number of additional records */
} dns_header_t;
typedef struct {
char *name; /* Pointer to the domain name in memory */
uint16_t dnstype; /* The QTYPE (1 = A) */
uint16_t dnsclass; /* The QCLASS (1 = IN) */
} dns_question_t;
|
Code Listing 4.17 illustrates how to start creating a DNS query using the OpenDNS service. This same request could be sent to any DNS server, such as the DNS server operated by the reader’s ISP. [5] As with HTTP before, the code starts by creating a socket, but this socket uses the SOCK_DGRAM
type to create a UDP socket. OpenDNS’s DNS server IPv4 address is available at 208.67.222.222, which is the hexadecimal value 0xd043dede
. DNS servers listen on port 53, so that value is also set. For the DNS header, we can randomly assign any value to the XID
field, which has no inherent meaning to the server itself. The flag field is set to declare the message is a request (Q=0
) and to indicate that recursion is desired (RD=1
). Finally, we declare that we will be sending a single question in this request. Note that all of the numeric values are set using the htons()
and htonl()
standard C functions to ensure that the values in the datagram will be in the correct byte order.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | /* Code Listing 4.17:
Creating a DNS header and question to send to OpenDNS
*/
int socketfd = socket (AF_INET, SOCK_DGRAM, 0);
struct sockaddr_in address;
address.sin_family = AF_INET;
/* OpenDNS is currently at 208.67.222.222 (0xd043dede) */
address.sin_addr.s_addr = htonl (0xd043dede);
/* DNS runs on port 53 */
address.sin_port = htons (53);
/* Set up the DNS header */
dns_header_t header;
memset (&header, 0, sizeof (dns_header_t));
header.xid= htons (0x1234); /* Randomly chosen ID */
header.flags = htons (0x0100); /* Q=0, RD=1 */
header.qdcount = htons (1); /* Sending 1 question */
|
Code Listing 4.18 illustrates the initial steps for setting up the question field. The
length of this field is not fixed, as it depends on the length of the domain name being translated.
As such, the dns_question_t
type does not contain the full contents of the question itself,
using a pointer to the name field within the program instead. For this scenario, we are only
requesting an Internet address record, so we set the QTYPE
to 1 (A
) and QCLASS
to 1 (IN
).
1 2 3 4 5 6 7 8 9 10 11 12 | /* Code Listing 4.18:
Creating a DNS header and question to send to OpenDNS
*/
/* Set up the DNS question */
dns_question_t question;
question.dnstype = htons (1); /* QTYPE 1=A */
question.dnsclass = htons (1); /* QCLASS 1=IN */
/* DNS name format requires two bytes more than the length of the
domain name as a string */
question.name = calloc (strlen (hostname) + 2, sizeof (char));
|
Recall the domain name formatting in Table 4.7 and Table 4.9. Given a human
readable domain name, such as www.charity.org
, the string is broken apart into distinct fields
based on the dot; in this case, the three fields are "www"
, "charity"
, and "org"
. Within
the DNS question, each field is preceded by a one-byte value that indicates the length of the field.
The name is considered terminated once the null-byte is used to indicate a zero-length field. Code
Listing 4.19 shows an algorithm to convert a human readable name into the DNS question
format. The code starts by copying the hostname into the second byte of the space allocated for the
name; the reason for this is to leave one byte of space for the length of the first field (which
will be 3 for "www"
), which will be determined later. Throughout the rest of the algorithm the
prev pointer is used to keep track of the location of the byte where the current field’s length will
be stored. As such, prev is initialized to the first byte of the space for the name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | /* Code Listing 4.19:
Algorithm for converting a hostname string to DNS question fields
*/
/* Leave the first byte blank for the first field length */
memcpy (question.name + 1, hostname, strlen (hostname));
uint8_t *prev = (uint8_t *) question.name;
uint8_t count = 0; /* Used to count the bytes in a field */
/* Traverse through the name, looking for the . locations */
for (size_t i = 0; i < strlen (hostname); i++)
{
/* A . indicates the end of a field */
if (hostname[i] == '.')
{
/* Copy the length to the byte before this field, then
update prev to the location of the . */
*prev = count;
prev = question.name + i + 1;
count = 0;
}
else
count++;
}
*prev = count;
|
When the hostname is copied into the space for the question, the string still contains the dot characters. In the DNS question format, these dots are replaced by the lengths of the field that follows. Returning to the example of www.charity.org
, the first dot should be replaced by 7, indicating the length of the field "charity"
. The for-loop in Code Listing 4.19 replaces the dots with the field name, by keeping prev pointing to the location of the dot preceding the current field. As such, once another dot is encountered, the code can update the byte where the previous dot is stored with the length of the field that just ended. The count variable is then reset to 0 (starting to count the length of a new field), and prev is updated to point to the new dot. When the loop ends, prev is still pointing to the location of the last dot, so its value can be modified with the length of the last field.
Bug Warning
The correctness of Code Listing 4.19 relies on correct handling of two common mistakes
with pointers. First, it is important to distinguish between updating where in memory the prev
pointer is pointing (prev = query + i + 1)
and updating the value stored at that memory
location (*prev = count)
. Typos involving the *
are notorious sources of bugs with
pointers. The second critical dependency is the use of calloc()
in Code Listing 4.18 instead of malloc()
. Using calloc()
initializes the space that question.name
points to with all zeroes. Consequently, we do not need to explicitly null-terminate the string,
because there is already a zero there. Since malloc()
does not guarantee initialization of the
allocated memory space, the byte that indicates the zero-length field might not actually store 0.
This could lead to incorrect behavior in the DNS processing, including buffer overflows at either
the client or server.
Once the header and question fields have been constructed, all that remains is to assemble these
bytes into a packet and send the request through the UDP socket. Code Listing 4.20
illustrates this procedure. First, the total packet length needs to be determined. DNS headers are
fixed size, but the questions are not. The length of the question is based on the extended length of
the hostname (including the byte for the first field’s length and the final null-terminating byte).
The question also contains two 16-bit values to indicate the QTYPE
and QCLASS
. Once the size
is determined and the space is dynamically allocated, the code concatenates all fields as necessary.
The header is copied in first, followed immediately by the QNAME
, with the QTYPE
and
QCLASS
at the end. Since DNS is based on UDP for transport, the code must use sendto()
to
deliver the message to the socket.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /* Code Listing 4.20:
Assembling the DNS header and question to send via a UDP packet
*/
/* Copy all fields into a single, concatenated packet */
size_t packetlen = sizeof (header) + strlen (hostname) + 2 +
sizeof (question.dnstype) + sizeof (question.dnsclass);
uint8_t *packet = calloc (packetlen, sizeof (uint8_t));
uint8_t *p = (uint8_t *)packet;
/* Copy the header first */
memcpy (p, &header, sizeof (header));
p += sizeof (header);
/* Copy the question name, QTYPE, and QCLASS fields */
memcpy (p, question.name, strlen (hostname) + 1);
p += strlen (hostname) + 2; /* includes 0 octet for end */
memcpy (p, &question.dnstype, sizeof (question.dnstype));
p += sizeof (question.dnstype);
memcpy (p, &question.dnsclass, sizeof (question.dnsclass));
/* Send the packet to OpenDNS, then request the response */
sendto (socketfd, packet, packetlen, 0, (struct sockaddr *) &addr,
(socklen_t) sizeof (addr));
|
To receive the response from the DNS server, Code Listing 4.21 starts by allocating and
clearing the contents of a 512-byte buffer in memory. The length of this buffer can be hard-coded in
this way, as the DNS specification mandates a maximum of 512 bytes for all messages. The actual
length of the received data is set when recvfrom()
retrieves the response from the socket.
1 2 3 4 5 6 7 8 9 10 | /* Code Listing 4.21:
Receiving a DNS header and confirming there were no errors
*/
socklen_t length = 0;
uint8_t response[512];
memset (&response, 0, 512);
/* Receive the response from OpenDNS into a local buffer */
ssize_t bytes = recvfrom (socketfd, response, 512, 0, (struct sockaddr *) &addr, &length);
|
The response from the server (assuming the request is successfully processed) would consist of the
fixed-size header, a question field identical to that sent in the request, and an answer containing
the information from a resource record. The structure of the answer depends on several factors,
including the IP version (IPv4 or IPv6) and the type of record requested. That is, the responses for
address (A
), namespace (NS
), or canonical name (CNAME
) records vary in structure. In
this scenario, we are requesting an IPv4 address, so the bytes in the request would match the
following struct
definition.
1 2 3 4 5 6 7 8 9 | /* Structure of the bytes for an IPv4 answer */
typedef struct {
uint16_t compression;
uint16_t type;
uint16_t class;
uint32_t ttl;
uint16_t length;
struct in_addr addr;
} __attribute__((packed)) dns_record_a_t;
|
Bug Warning
The use of __attribute__((packed))
in this struct
declaration is critical to tell the
compiler not to re-order the fields of the struct
within the program. When reading data from
the network, the bytes will occur in a particular order. When we use a struct
to impose a
logical meaning on those bytes in a program, we would expect the interpretation to look like
this:
However, compilers routinely re-order the fields in a struct
to preserve word alignment,
trying to group the bytes into chunks of 32 bits as much as possible. In this case, many compilers
would swap the ttl
and length
fields, which would impose the wrong structure on the
sequence of bytes received from the network:
Code Listing 4.22 shows how the client can take the received response and interpret it
correctly for an IPv4 A record. By casting the response as a dns_header_t *
variable, the code
can refer to the fields within the header based on the struct declaration. By applying the 0xf
bit mask, we can examine just the RCODE
field of the flag to detect if an error occurs. If the
RCODE
is 0, then the request was processed correctly. Next, we need to traverse through the
question field, which begins with the variable-length QNAME
. The start_of_name
pointer is
created to keep track of where the name starts. Each iteration of the loop determines where the next
field length byte will occur and replaces it with a dot, while calculating the total length of the
name.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* Code Listing 4.22:
Checking the header and question name of the DNS response
*/
dns_header_t *response_header = (dns_header_t *)response;
assert ((ntohs (response_header->flags) & 0xf) == 0);
/* Get a pointer to the start of the question name, and
reconstruct it from the fields */
uint8_t *start_of_name = (uint8_t *) (response + sizeof (dns_header_t));
uint8_t total = 0;
uint8_t *field_length = start_of_name;
while (*field_length != 0)
{
/* Restore the dot in the name and advance to next length */
total += *field_length + 1;
*field_length = '.';
field_length = start_of_name + total;
}
|
Once we have determined the total length of the domain name in the question field, we can skip
directly to the resource records in the answer. Immediately after the while loop in Code Listing
4.22, the field_length
pointer will be pointing to the null byte at the end of the
name. The records begin five bytes later, after the null byte, the QTYPE
, and the QCLASS
fields. Casting the remaining bytes as a dns_record_a_t *
allows Code Listing 4.23
to treat this data as an array of records. The fields of these records can then be cast using the
struct
definition from above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* Code Listing 4.23:
Printing the DNS resource records returned
*/
/* Skip null byte, qtype, and qclass to get to first answer */
dns_record_a_t *records = (dns_record_a_t *) (field_length + 5);
for (int i = 0; i < ntohs (response_header->ancount); i++)
{
printf ("TYPE: %" PRId16 "\n", ntohs (records[i].type));
printf ("CLASS: %" PRId16 "\n", ntohs (records[i].class));
printf ("TTL: %" PRIx32 "\n", ntohl (records[i].ttl));
printf ("IPv4: %08" PRIx32 "\n", ntohl (records[i].addr));
printf ("IPv4: %s\n", inet_ntoa (records[i].addr));
}
|
The Extended Example for Chapter 5 combines all of the preceding code segments, along with some
additional statements for printing, into a single program to run as a basic DNS client. If this
program is compiled into the current directory as an executable called dns
, the output would
look like the following when querying the address example.com
. Note that this example only works
with some domain names, as our basic client only supports a limited subset of the required
functionality as defined in RFC 1034 and RFC 1035.
$ ./dns example.com
Lookup example.com
1234 0100 0001 0000 0000 0000 0765 7861
6d70 6c65 0363 6f6d 0000 0100 01
Received 45 bytes from 208.67.222.222:
1234 8180 0001 0001 0000 0000 0765 7861
6d70 6c65 0363 6f6d 0000 0100 01c0 0c00
0100 0100 00e9 4900 045d b8d8 22
TYPE: 1
CLASS: 1
TTL: e949
IPv4: 5db8d822
IPv4: 93.184.216.34
[1] | The domain name and IP address for www.charity.org are fictional and provided for
illustrative purposes only. The addresses 198.41.0.4 and 199.19.56.1 are the real addresses for the
root and .org TLD name servers, however. |
[2] | The Internet Assigned Name Authority (IANA) is one portion of ICANN. IANA maintains
example.com specifically as a public resource for illustrating DNS functioning. |
[3] | The DNS specification prepends a "Q" to the beginning of field names to indicate that
the field is for a query, even though the distinction has no practical impact for basic queries.
Hence, the reader should treat NAME and QNAME `` as the same, likewise for ``CLASS and
QCLASS , and so on. |
[4] | Linux contains similar structs in <arpa/nameser.h> and <arpa/nameser_compat.h> ,
but they are more complex than shown here. For instance, the Linux version contains names to access
the individual bits of the flags field. |
[5] | By using OpenDNS in this scenario, we can illustrate the full process of the network
request, including setting up the UDP socket with an IP address that is functional as of this
writing. OpenDNS also provides a number of other benefits, such as increased privacy and security
services. For more information, consult their site at https://www.opendns.com . |