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.
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.
QR | Opcode | AA | TC | RD | RA | Z | RCODE |
0 | 0000 | 0 | 0 | 1 | 0 | 000 | 0000 |
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.