Skip to content

tklynsma/dns

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

58 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

DNS Resolver and Name Server

Description

An implementation of a (caching) DNS resolver and name server for a course on Computer Networks. A framework was provided for the project which already provided classes for manipulating DNS messages and converting them to and from bytes.

File structure

  • dns
    • cache.py: Contains a cache for the resolver.
    • classes.py: Enum of CLASSes and QCLASSes.
    • message.py: Classes for DNS messages.
    • name.py: Classes for reading and writing domain names as bytes.
    • rcodes.py: Enum of RCODEs.
    • resolver.py: Class for the DNS resolver.
    • resource.py: Classes for DNS resource records.
    • server.py: Contains the DNS server.
    • types.py: Enum of TYPEs and QTYPEs.
    • zone.py: Name space zones.
  • dns_client.py: A simple DNS client, which serves as an example user of the resolver.
  • dns_server.py: Code for starting the DNS server and parsing args.
  • dns_tests.py: Tests for the resolver, cache and server.

Usage

Usage is as described in the assignment. In addition a "verbose" output option has been added to both dns_client.py and dns_server.py. To use this add the parameter -v or --verbose.

The server, when running in its default settings (localhost using port 5353), was tested using the following dig command:

$ dig @localhost hostname -p 5353 +noedns [+norec]

nslookup with the correct settings for server and port should work as well.

Resolver

Name resolution roughly follows the algorithm described in Section 5.3.3 of RFC 1034. At creation, the list of root servers is initialized using the zone file root (see root hints). The cache is read from the json cachefile cache. The cache is shared between all resolver instances and written back to the cachefile at deletion.

Consulting the cache

When caching is enabled the resolver will first attempt to resolve the hostname using the cache:

  1. Check the cache for CNAME resource records matching the hostname. While there is still a valid CNAME record to be found in the cache: add the hostname to the aliaslist and change the current hostname to the canonical name found in rdata.

  2. Check the cache for A resource records matching the hostname. If any were found return the answer; otherwise continue at the next step.

  3. Check the cache for NS resource records. Start matching down the labels in hostname, starting at hostname and moving up to (but excluding) the root, until any matching NS resource records are found. If found; lookup matching A resource records, change the list of hints to the found name servers and start an iterative query for the hostname.

Building an iterative query

If the cache was unsuccesful in resolving the hostname or caching was disabled the resolver will build an iterative query:

  1. Select and remove the first name server in the list of hints and send a query for the hostname. The header's QR, OPCODE and RD bits are all set to zero. This tells the receiving name server that the message is a standard query and that no recursion is desired. If the list of hints is empty go to step 5.

  2. Check whether the response message (if any) is valid. The response is considered valid if the following conditions hold:

    • No unexpected error was encountered when sending or receiving datagrams. This includes timeout exceptions.
    • The response has a RCODE of zero, indicating that no errors occurred.
    • The response has its QR bit set to 1, indicating that it is a response message.
    • The identification number of the response corresponds to the identification number defined in the query.
    • The question section in the response is equal to the question section defined in the query.

    The first two conditions ensure that the datagram was received without errors. The last three conditions ensure that the response is an answer to the question defined in the query. This also guarantees that concurrent queries are handled correctly. If the response was invalid continue at the next name server in the list of hints (go back to step 1).

  3. If the response contains an answer: Loop over all resource records found in the answer section. If the resource record is of type CNAME add the hostname to aliaslist and change the current hostname to the domain name found in rdata. If the resource record is of type A add the IP address found in rdata to the list of IP addresses. If any A resource records were found return the answer. Otherwise, start a new query to the (new) hostname using any additional name servers found in the authority and additional section as additional initial hints.

  4. If the response contains no answers: Check the authority and additional sections for name server hints and for each server add its IP address (or if no corresponsing A resource record is found: its domain name) to the start of the list of hints. Name servers with a provided IP address in the additional section are preferred over name servers without an IP address. Go back to step 1.

  5. When the list of hints is exhausted and no answer is found: output the hostname and empty lists for the aliases and IP addresses.

Cache

For efficient lookup the cache is implemented as a dictionary; with domain names as keys and lists of resource records as values. The cachefile is read from and stored on disk using json format. The cache is also guarded against concurrent access.

To remove expired records a timestamp is associated with each resource record. Expired resource records are removed from the cache if the following condition is false:

self.timestamp + self.ttl > time.time()

When the cache file is first initialized all resource records for which this condition does not hold are filtered from the cache. Thereafter this condition is only checked when doing a lookup: all resource records for dname for which the condition does not hold are filtered from the cache before returning the result.

Name server

The name server roughly follows the algorithm described in Section 4.3.2 of RFC 1034, omitting step 3.b. At creation, it will initialize its zonefile zone and bind its UDP socket to localhost and the indicated port number. When starting the DNS server using dns_server.py the default port is set to 5353.

Server

The server listens for incoming datagrams and, if the datagram is a valid DNS query it will start a new thread to concurrently handle the request. A datagram is considered a valid DNS query if:

  • No errors occurred while parsing the message from bytes.
  • The message's OPCODE is equal to zero, indicating a standard query.
  • The message's QR bit is set to zero, indicating a query.
  • The message contains at least one resource record in its question section.

If any of these conditions fail the datagram is ignored.

Request handler

Each request handler runs in a separate thread, resolves the query and sends a response back to the datagram's source address. Concurrency is ensured by protecting the cache against concurrent access and by matching responses in the resolver to their DNS transaction ID. When sending a response the handler sets the header's QR and RA bits to 1, meaning the message is a response and recursion is available on the server. The header's RD bit is copied from the query and the AA bit is set in case of an authorative response.

  1. First, the handler checks whether the question's QTYPE is of type A. If not, an empty response is send back with RCODE 4 (not implemented). Otherwise, the handler will continue by consulting its zone.

  2. Check the zone for CNAME resource records matching the hostname. While there is still a valid CNAME record to be found in the zone: add the record to the list of cnames and change the current hostname to the canonical name found in rdata.

  3. Check the zone for A resource records matching the hostname and save these records in answers.

  4. Check the zone for NS resource records. Start matching down the labels in hostname, starting at hostname and moving up to (but excluding) the root, until any matching NS resource records are found. If found, look up matching A resource records in the zone.

  5. If answers is non-empty: return an authorative response to the datagram's source address containing all found records.

  6. If no recursion is desired and other records were found in the server's zone: send back an authorative response to the datagram's source address containing all found records.

  7. If no recursion is desired and no records were found, then the hostname points outside of the server's zone. An empty response is send back with RCODE 5 (refused).

  8. If recursion is desired and no answer was found using zone resolution, then the resolver is used to answer the query. When solving a recursive query the resolver will use the same transaction ID as the original query. If the resolver finds an answer it is send back to the datagram's source address. If not, an empty response is send back with RCODE 3 (name error).

About

DNS resolver and name server for a course on computer networks.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages