Changelog:

  • 19 Oct 2024: don’t require generating request with multiple questions, since that’s apparently prohibited by RFC 9619
  • 21 Oct 2024: specify that you may assume –process-response’s answer only includes responsive records
  • 23 Oct 2024: have create-request responses prefixed with size for consistency with response handling; change text2pcap advice accordingly
  • 23 Oct 2024: add link to dns samples; –send-request testing instructions
  • 25 Oct 2024: add nxdomain to dns samples + seperate link
  • 26 Oct 2024: allow trailing . or not in json domain name output since the dns-samples are not consistent about this
  • 26 Oct 2024: update dns-samples to fix cname-to-aaaa and cname-to-aaaa2 being duplicates
  • 26 Oct 2024: mention argparse in hints
  • 28 Oct 2024: provide examples of using sys.stdout.buffer and sys.stdin.buffer + correct after first version had typo
  • 29 Oct 2024: always show –ipv4 or –ipv6 being passed when –create-request is used
  • 30 Oct 2024: make it extra clear that you cannot call tshark in your dns.py, it must do its own parsing
  • 30 Oct 2024; explicit mention where to submit code
  • 1 Nov 2024: note potential issues using powershell

1 Your Task

  1. In dns.py, write a program where:

    • python3 dns.py --create-request HOSTNAME --ipv4 creates a DNS query message that requests the IPv4 addresses of HOSTNAME with recursion disabled, and writes it to stdout (as in using sys.stdout.buffer in Python3) prefixed by the request size as a 2-byte big endian number (like would be used when sending a DNS request over TCP).

      python3 dns,py --create-request HOSTNAME --ipv6 should do the same, but request the IPv6 address.

    • python3 dns.py --process-response reads a DNS query response (for a query asking for an IPv4 and/or IPv6 address for a hostname) from stdin (as in using sys.stdin.buffer in Python3) and parses it. (You may not use a DNS library or call out to another program to do the parsing or request generation.) The input DNS query response will be prefixed by its size in bytes, as happens when it is sent by a DNS server.

      Then, it outputs JSON in the format specified in the output format section below identifying:

      • if it’s an error;
      • the IPv4 and IPv6 addresses identified in the answer part of the response, if any;
      • if no IPv4 and IPv6 address is identified in the answer part of the response, then:
        • if a CNAME needs to be followed to find the IPv4 or IPv6 address, the name to lookup in the recursive query
        • the names and IPv4 and/or IPv6 addresses of potential DNS servers to contact to resolve that name as part of a recursive query

      To keep the assignment simpler, you do not have to implement DNS name compression in your responses.

    • python3 dns.py --send-request HOSTNAME --server SERVER --port PORT --ipv4 or python3 dns.py --send-request HOSTNAME --server SERVER --port PORT --ipv6 makes a DNS request using TCP to the DNS server with name SERVER on port number PORT. The request should be the same request that would be generated by the --create-request option. After making the request, the program should retrieve the response and process the result as in --process-response, outputting the corresponding JSON to stdout.

  2. Submit your code by uploading dns.py to the submission site.

2 output format

When processing a response, output a JSON dictionary with the entries with the following keys and corresponding values:

In addition, you may include extra entries in the dictionary at your discretion. You may assume that any returned records in the answer or authority are responsive to the query. (For example, we won’t include an A record or CNAME record for an unrelated domain in the answer section.)

In this JSON output:

3 some DNS references

  1. Official specifications:

    • term definitions: RFC 9499
    • concepts and facilities: RFC1034
    • implementation and specification: RFC1035
    • DNS extensions to support IP Version 6: RFC3596
  2. TCP/IP Illustrated, Volume 2, Chapter 11 (available via UVa library through O’Reilly; login/create an account with your UVa email)

4 sample DNS responses

  1. Here is an archive of DNS responses suitable for passing to python3 --process-response. The .dat files are the DNS responses, which you can test with something like

    python3 --process-response <file.dat

    (Note that there are reports that these command do not work without modificaiton in Powershell (probably because it treats file.dat as a text instead of binary file). I know they will work on portal.cs.virginia.edu.)

    the .txt files show a human-readable version which is created by running a command like

    od -Ax -tx1 -v file.dat | text2pcap -T 53,53 - - | tshark -T text -r - -V >file.txt

    to have tshark (command-line version of wireshark) process the DNS response (from file.dat into `file.txt), then extracting the relevant part of the output.

    The .reference.json files are the responses our reference implementation generated.

  2. The original version of the DNS samples archive did not contain a sample for name not found; it now does, or you can download the extra dat file here.

    It also contained duplicate records for cname-to-aaaa and cname-to-aaaa2; this is now corrected; you can download an actually different cname-to-aaaa2 dat and txt file.

5 testing –send-request

  1. You can test send-request using the nc utility, which can act as a simple TCP server.

    For example, you can run

    nc -l -p 0 -s 127.0.0.1 -v <input.dat > output.dat

    in one terminal. (The -l option says to listen for (act as a server) instead of making connections (act as a client). The -p option selects the input port number to bind to; binding to port 0 has the OS select a port number. The -s 127.0.0.1 selects an IP address of 127.0.0.1, which is an IP address for localhost — it only contacts the current machine.)

    This commad will output a message like

    Listening on 127.0.0.1 46245

    Pay special attention to the port number listed after the IP address. Then in another terminal on the same machine you can run a command like

    python3 dns.py --send-request www.cs.virginia.edu --server 127.0.0.1 --port 46245

    (If you are connecting to portal.cs.virginia.edu, see note below regarding getting two terminals on the same machine.)

    When your dns.py connects to nc, it will receive the contents of input.dat and whatever it sends will be written to output.dat

    Regarding portal.cs.virginia.edu: ssh user@portal.cs.virginia.edu connects you to one of several machines, names portal01 through portal11. You can tell which of those machines you are logged in using your command prompt and/or by running the hostname command. Once logged into one portal machine or sufficiently on the UVa network, you can log into a particular portal machine using ssh portalXX.cs.virginia.edu where portallXX is the name of the machine with the number. With some SSH clients (probably including Linux/OS X’s), you can have the SSH client login to portal.cs.virginia.edu and then SSH from that machine to portalXX.cs.virginia.edu with a command like ssh -J user@portal.cs.virginia.edu user@portalXX.cs.virginia.edu. (See also OpenSSH’s manapage’s section on the -J option.)

6 Hints

6.1 Python argument parsing

  1. If found Python’s standard argparse library helpful.

6.2 Checking requests in wireshark

  1. For debugging, you can convert the output of

    python3 dns.py --create-request HOSTNAME ...

    to a PCAP file you can open with wireshark using the text2pcap utility.

    One way to do this for a request for www.cs.virginia.edu:

    python3 dns.py --create-request www.cs.virginia.edu --ipv4 > request.dat
    od -Ax -tx1 -v < request.dat > request.od
    text2pcap -T 53,53 request.od request.pcapng

    (Note that there are reports that these command do not work without modificaiton in Powershell (probably because it treats request.dat as a text instead of binary file). I know they will work on portal.cs.virginia.edu.)

    The first command runs python3 dns.py --create-request www.cs.virginia.edu, but sends its output to the file request.dat. The second command converts request.dat into a text-format (with hexadecimal) file that is saved in request.od. The third takes request.od and converts it to a file that wireshark can open by adding an ethernet and TCP header using source and destination port 53. (If you used -u 53,53, it would wrap it in a UDP header instead.)

    You can open up that file in a graphical version of wireshark. Also, on the command line on portal, you can get a dump of the contents with all information with a command like

    tshark -T text -V -r request.pcapng > request-as-text.txt

    which will output a meachine-reaable text representation as request-as-text.txt

    You can combine all of these into one command with something like:

    python3 dns.py --create-request www.cs.virginia.edu | od -Ax -tx1 -v - | text2pcap -T 53,53 - - | tshark -T text -V -r - > request-as-text.txt

6.3 Encoding/decoding big-endian values

  1. The standard Python struct library is convenient for converting two and from big-endian integers.

6.4 Using sys.stdout.buffer and sys.stdin.buffer

  1. sys.stdout.buffer and sys.stdin.buffer provide support for input and output for bytes (instead of strs) in Python.

    You can use them write or read bytes from stdin and stdout with something like

    sys.stdout.buffer.write(bytes([1,2,3,4]))

    or

    the_4_bytes = sys.stdin.buffer.read(4)

    .

6.5 Opening/using TCP connections

  1. You can use the socket library to create TCP connections. Most simple is using create_connection followed by sendall and recv:

    import socket
    ...
    sock = socket.create_connection(('host', port))
    sock.sendall(b'The bytes to send')
    data = sock.recv(size)

    (sendall is like send, but in case not all bytes can be sent at once, it calls send repeatedly until they all are or there is an error.)

    Note that you may need to call recv multiple times in case only some of the bytes you request are available initially.


  1. RFC 4291 only gives examples with upper-case A-F, but we will allow both upper and lower case.↩︎