CSc 423 Assignment 2

What's That Name Again?

Assigned
Due

Feb 24
80 pts
Mar 19
This project is to create a small CleanSocks DNS client. It takes a host name and a DNS server on the command line, and sends a request for a DNS A record (IP4 address lookup) to a specified server at the standard DNS port, and prints information from the result. Like this:
tom@laptop$ ./qa2 www.google.com 1.1.1.1 Request 46880 to 1.1.1.1:53 Received msg id=46880 from 1.1.1.1:53 Resp no error: Flags: recur-req recur-avail 1 questions, 1 answer RRs 0 nameserver RRs, 0 other RRs === Questions === www.google.com, type: 1, class 1 === Answer === www.google.com, type: 1, class: 1, TTL: 189: 172.217.2.36
Or maybe like this:
tom@laptop ./qa2 www.mc.edu 205.251.195.126 Request 32378 to 205.251.195.126:53 Received msg id=32378 from 205.251.195.126:53 Resp no error: Flags: authoritative recur-req 1 questions, 1 answer RRs 4 nameserver RRs, 0 other RRs === Questions === www.mc.edu, type: 1, class 1 === Answer === www.mc.edu, type: 1, class: 1, TTL: 3600: 104.247.79.10 === Nameserver === mc.edu, type: 2, class: 1, TTL: 172800: ns-1324.awsdns-37.org mc.edu, type: 2, class: 1, TTL: 172800: ns-1989.awsdns-56.co.uk mc.edu, type: 2, class: 1, TTL: 172800: ns-324.awsdns-40.com mc.edu, type: 2, class: 1, TTL: 172800: ns-894.awsdns-47.net
We will code to the original RFC 1035. There have been a number of extensions and improvements, but servers will understand the classic.

The Protocol

DNS is quite different from HTTP. It uses binary messages over UDP rather than text over TCP. For a UDP example, you might look at the UDP Send and UDP Echo examples. To simplify building and reading the protocol messages, you have a C++ class in binbuf.h and binbuf.cpp, with some documentation here. You will also find the stdint.h useful to represent exact sizes required by the standard. You may also find the C/C++ bitwise operators useful.

DNS, at least the part we're interested in, sends a UDP message to a server, and receives one back. The message format is described in RFC 1035 Section 4.1. The message is made of the five sections listed there, each of which is given in more detail later in the RFC document. The header and question sections have their own formats, and the answer, authority and additional sections are lists of resource records, each with its own type and format. Any or all of the three lists sections may be empty.

The client needs to build a simple request which has a header and one question, with the answer, authority and additional sections empty. It uses UDP to send this to the server, and receives a response. The response, like all DNS messages, has the same format. The client receives this message, parses it, and prints some of its contents.

Our use of the protocol will be simple. Send one message to one server and wait for a response. If the message or response is lost, the client will hang and you will need to kill it. A more sophisticated resolver would use response time-outs and resends to handle this much better.

Procedure

First, create a BinaryBuffer object and build the message to send. Begin by adding the header fields, as described Section 4.1.1. The first thing to insert is a two-byte (type uint16_t) id number. It's a random number used to match the response with the request. Use the C library rand() to generate this, then insert into the buffer.

The next thing to insert is a complicated line containing some one-bit and smaller fields, as shown. What you want to send is this:
QROpcodeAATCRDRAZRCODE
0000000100000000
Most of the flags are relevant to responses, so we have zeros. We are making a standard query and requesting recursion. These flags produce a two-byte value which can be written in C as 0x0100. The 0x indicates a hexadecimal constant. Insert that into the buffer. Next, insert the four two-byte counts, a one and three zeros.

Now, build the question section: insert the host name being queried, from the command line. Then insert the query type and class, which are each 1. That completes the query.

Choose a random port number in the range 2000 to 40000. Create a UDP socket and bind it the wild card address any at that random port. Consult the UDP Echo example for the bind call. You can use this socket to both send and receive messages. Build the server endpoint from the server IP address and the port number for the domain service. Send your message to the server endpoint with sendto as shown in the UDP Sending example. Then, receive the response in another (or the same) buffer object. Again, see the UDP Echo example for the receive.

Now, you get to parse the response. Back to Section 4.1. Extract the ID from the header, and make sure it agrees with the one you sent. If not, throw out the message, and read again. You should read messages until you get one with the right id. Once you do, extract the flags word. Print the flags which are set (any of AA, TC RD or RA). Also extract and print the RCODE, which is an error code. (Continue to parse the response, even if it shows an error.)

Read the four counts. Read and print the question section (it should match the one sent), and whatever resource records are present in each of the appropriate sections. The counts from the header tells how many RRs are in each section, frequently zero. I made a function to extract one RR, and called it in loops for each of the three sections. The general form of a resource record is given in Section 4.1.3. For each one, recover and print the name (which is a host or domain name), the type, class and TTL. Recover and store the RDLENGTH. Each RR type has its own RDATA format. The type values are given in Section 3.2.2. We are interested in types A, NS and CNAME, and their RDATA formats are given in Section 3.4.1, Section 3.3.11 and Section 3.3.1, respectively. (Spoiler alert: IP 4 address, hostname, hostname.). If the record is any of these types, extract and print the data. For others, use the RDLENGTH value to just ignore the RDATA section. (In fact, that's what it's there for: so clients that don't understand a particular RR type, or just want to, can skip it, but keep reading.)

When extracting the flags and RCODE from the received header, you must enter bit-jockey mode: Extract portions of a larger integer. This can be done with arithmetic: integer division and modulus, or with C/C++ bitwise operators, which work on individual bits. For instance, if you have extracted the flags word from the header into a uint16_t variable called flags, you can test for the AA flag by writing if(flags & 0x0400) .... The constant has a one bit only in the location of that flag, and the and operation turns all the other bits off. Then the if is true exactly when that bit is set.

Submission

When your program is working, nicely commented and properly indented, submit it using the form here. There is a place to upload the binary buffer, but there is no need to send thses unless you make some changes to the class.