«  4.5. TCP Socket Programming: HTTP   ::   Contents   ::   4.7. Application-Layer Broadcasting: DHCP  »

4.6. UDP Socket Programming: DNS

The DNS name space is organized as a hierarchy of zones of authority

Figure 4.6.1: The DNS name space is organized as a hierarchy of zones of authority

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.

Decorative note icon

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.

Decorative bug warning

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.

4.6.1. Resolving DNS Queries

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.

Iterative sequence of DNS requests

Figure 4.6.4: Iterative sequence of DNS requests

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.

4.6.2. DNS Resource Record Structure

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.

4.6.3. DNS Protocol Messages

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.

4.6.4. Constructing DNS Queries with Sockets

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.

Decorative bug warning

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));

4.6.5. Processing DNS Query Responses

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;
Decorative bug warning

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:

DNS structures are packed in a way that does not preserve word alignment

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:

Failing to declare a DNS record ``struct`` as packed can mislabel bytes due to word alignment

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.
«  4.5. TCP Socket Programming: HTTP   ::   Contents   ::   4.7. Application-Layer Broadcasting: DHCP  »

Contact Us License