From f05c34a352fd0bfddee7e8795cf89f78ddb103cf Mon Sep 17 00:00:00 2001 From: Yorick van Pelt Date: Fri, 27 May 2016 19:38:58 +0200 Subject: [PATCH] import initial framework --- .gitignore | 1 + README.md | 45 ++++++++ dns/__init__.py | 1 + dns/cache.py | 89 +++++++++++++++ dns/classes.py | 55 +++++++++ dns/domainname.py | 127 +++++++++++++++++++++ dns/message.py | 278 ++++++++++++++++++++++++++++++++++++++++++++++ dns/rcodes.py | 81 ++++++++++++++ dns/resolver.py | 72 ++++++++++++ dns/resource.py | 241 ++++++++++++++++++++++++++++++++++++++++ dns/server.py | 53 +++++++++ dns/types.py | 67 +++++++++++ dns/zone.py | 54 +++++++++ dns_client.py | 28 +++++ dns_server.py | 28 +++++ dns_tests.py | 34 ++++++ 16 files changed, 1254 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 dns/__init__.py create mode 100644 dns/cache.py create mode 100644 dns/classes.py create mode 100644 dns/domainname.py create mode 100644 dns/message.py create mode 100644 dns/rcodes.py create mode 100644 dns/resolver.py create mode 100644 dns/resource.py create mode 100644 dns/server.py create mode 100644 dns/types.py create mode 100644 dns/zone.py create mode 100644 dns_client.py create mode 100644 dns_server.py create mode 100644 dns_tests.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..94487b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.pyc diff --git a/README.md b/README.md new file mode 100644 index 0000000..ce89073 --- /dev/null +++ b/README.md @@ -0,0 +1,45 @@ +# Project 2 Framework + +## Description + +This directory contains a framework for a DNS resolver and a recursive DNS server. +The framework provides classes for manipulating DNS messages (and converting them to bytes). +The framework also contains a few stubs which you need to implement. +Most files contain pointers to the relevant sections of RFC 1034 and RFC 1035. +These are not the only relevant sections though, and you might need to read more of the RFCs. + +It is probably a good idea to read RFC 1034 before proceeding. +This RFC explains an overview of DNS and introduces some of the naming which is also used in the framework. + +## File structure + +* proj1_sn1_sn2 + * dns + * cache.py: Contains a cache for the resolver. You have to implement this. + * classes.py: Enum of CLASSes and QCLASSes. + * domainname.py: Classes for reading and writing domain names as bytes. + * message.py: Classes for DNS messages. + * rcodes.py: Enum of RCODEs. + * resolver.py: Class for a DNS resolver. You have to implement this. + * resource.py: Classes for DNS resource records. + * server.py: Contains a DNS server. You have to implement this. + * types.py: Enum of TYPEs and QTYPEs. + * zone.py: name space zones. You have to implement this. + * 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 your resolver, cache and server. You have to implement this. + +## Implementation Hints and Tips + +You should start with implementing the resolver, which you need for the server. +You will need message.py, resource.py, types.py, classes.py and rcodes.py. +You can ignore the code for converting from and to bytes from these files if +you want, but it might be useful (especially for debugging). + +After finishing the resolver you need to implement caching and the DNS server. +You can implement these in any order that you like. +I suggest implementing the recursive part (the resolving) of your DNS server, before implementing the management of the servers zone. + +Wireshark and dns_client.py are useful tools for debugging your resolver. +Wireshark and nslookup are useful tools for debugging your server. + diff --git a/dns/__init__.py b/dns/__init__.py new file mode 100644 index 0000000..144e7c6 --- /dev/null +++ b/dns/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python2 diff --git a/dns/cache.py b/dns/cache.py new file mode 100644 index 0000000..57a821a --- /dev/null +++ b/dns/cache.py @@ -0,0 +1,89 @@ +#!/usr/bin/env python2 + +"""A cache for resource records + +This module contains a class which implements a cache for DNS resource records, +you still have to do most of the implementation. The module also provides a +class and a function for converting ResourceRecords from and to JSON strings. +It is highly recommended to use these. +""" + +import json + +from dns.resource import ResourceRecord, RecordData +from dns.types import Type +from dns.classes import Class + + +class ResourceEncoder(json.JSONEncoder): + """ Conver ResourceRecord to JSON + + Usage: + string = json.dumps(records, cls=ResourceEncoder, indent=4) + """ + def default(self, obj): + if isinstance(obj, ResourceRecord): + return { + "name": obj.name, + "type": Type.to_string(obj.type_), + "class": Class.to_string(obj.class_), + "ttl": obj.ttl, + "rdata": obj.rdata.data + } + return json.JSONEncoder.default(self, obj) + + +def resource_from_json(dct): + """ Convert JSON object to ResourceRecord + + Usage: + records = json.loads(string, object_hook=resource_from_json) + """ + name = dct["name"] + type_ = Type.from_string(dct["type"]) + class_ = Class.from_string(dct["class"]) + ttl = dct["ttl"] + rdata = RecordData.create(type_, dct["rdata"]) + return ResourceRecord(name, type_, class_, ttl, rdata) + + +class RecordCache(object): + """ Cache for ResourceRecords """ + + def __init__(self, ttl): + """ Initialize the RecordCache + + Args: + ttl (int): TTL of cached entries (if > 0) + """ + self.records = [] + self.ttl = ttl + + def lookup(self, dname, type_, class_): + """ Lookup resource records in cache + + Lookup for the resource records for a domain name with a specific type + and class. + + Args: + dname (str): domain name + type_ (Type): type + class_ (Class): class + """ + pass + + def add_record(self, record): + """ Add a new Record to the cache + + Args: + record (ResourceRecord): the record added to the cache + """ + pass + + def read_cache_file(self): + """ Read the cache file from disk """ + pass + + def write_cache_file(self): + """ Write the cache file to disk """ + pass diff --git a/dns/classes.py b/dns/classes.py new file mode 100644 index 0000000..aaaf30c --- /dev/null +++ b/dns/classes.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python2 + +""" DNS CLASS and QCLASS values + +This module contains an Enum of CLASS and QCLASS values. The Enum also contains +a method for converting values to strings. See sections 3.2.4 and 3.2.5 of RFC +1035 for more information. +""" + + +class Class(object): + """ Enum of CLASS and QCLASS values + + Usage: + >>> Class.IN + 1 + >>> Class.ANY + 255 + """ + + IN = 1 + CS = 2 + CH = 3 + HS = 4 + ANY = 255 + + by_string = { + "IN": IN, + "CS": CS, + "CH": CH, + "HS": HS, + "*": ANY + } + + by_value = dict([(y, x) for x, y in by_string.items()]) + + @staticmethod + def to_string(class_): + """ Convert a Class to a string + + Usage: + >>> Class.to_string(Class.IN) + 'IN' + """ + return Class.by_value[class_] + + @staticmethod + def from_string(string): + """ Convert a string to a Class + + Usage: + >>> Class.from_string('IN') + 1 + """ + return Class.by_string[string] diff --git a/dns/domainname.py b/dns/domainname.py new file mode 100644 index 0000000..315139c --- /dev/null +++ b/dns/domainname.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python2 + +""" Parsing and composing domain names + +This module contains two classes for converting domain names to and from bytes. +You won't have to use these classes. They're used internally in Message, +Question, ResourceRecord and RecordData. You can read section 4.1.4 of RFC 1035 +if you want more info. +""" + +import struct + + +class Composer(object): + """ Converts a string representation of a domain name to bytes """ + + def __init__(self): + self.offsets = dict() + + def to_bytes(self, offset, dnames): + # Convert each domain name in to bytes + result = b"" + for dname in dnames: + # Split domain name into labels + labels = dname.split(".") + + # Determine keys of subdomains in offset dict + keys = [] + for label in reversed(labels): + name = label + if keys: + name += "." + keys[-1] + keys.append(name) + keys.reverse() + + # Convert label to bytes + add_null = True + for j, label in enumerate(labels): + if keys[j] in self.offsets: + offset = self.offsets[keys[j]] + pointer = (3 << 14) + offset + result += struct.pack("!H", pointer) + add_null = False + offset += 2 + break + else: + self.offsets[keys[j]] = offset + result += struct.pack("!B{}s".format(len(label)), + len(label), + label) + offset += 1 + len(label) + + # Add null character at end + if add_null: + result += b"\x00" + offset += 1 + + return result + + +class Parser(object): + """ Convert byte representations of domain names to strings """ + + def __init__(self): + self.labels = dict() + + def from_bytes(self, packet, offset, num): + """ Convert domain name from bytes to string + + Args: + packet (bytes): packet containing the domain name + offset (int): offset of domain name in packet + num (int): number of domain names to decode + + Returns: + str, int + """ + + dnames = [] + + # Read the domain names + for _ in range(num): + # Read a new domain name + dname = "" + prev_offsets = [] + done = False + while done is False: + # Read length of next label + llength = struct.unpack_from("!B", packet, offset)[0] + + # Done reading domain when length is zero + if llength == 0: + offset += 1 + break + + # Compression label + elif (llength >> 6) == 3: + new_offset = offset + 2 + target = struct.unpack_from("!H", packet, offset)[0] + target -= 3 << 14 + label = self.labels[target] + done = True + + # Normal label + else: + new_offset = offset + llength + 1 + label = struct.unpack_from("{}s".format(llength), + packet, offset+1)[0] + + # Add label to dictionary + self.labels[offset] = label + for prev_offset in prev_offsets: + self.labels[prev_offset] += "." + label + prev_offsets.append(offset) + + # Update offset + offset = new_offset + + # Append label to domain name + if len(dname) > 0: + dname += "." + dname += label + + # Append domain name to list + dnames.append(dname) + + return dnames, offset diff --git a/dns/message.py b/dns/message.py new file mode 100644 index 0000000..3333f43 --- /dev/null +++ b/dns/message.py @@ -0,0 +1,278 @@ +#!/usr/bin/env python2 + +""" DNS messages + +This module contains classes for DNS messages, their header section and +question fields. See section 4 of RFC 1035 for more info. +""" + +import struct + +from dns.domainname import Parser, Composer +from dns.resource import ResourceRecord + + +class Message(object): + """ DNS message """ + + def __init__(self, header, questions=None, answers=None, authorities=None, additionals=None): + """ Create a new DNS message + + Args: + header (Header): the header section + questions ([Question]): the question section + answers ([ResourceRecord]): the answer section + authorities ([ResourceRecord]): the authority section + additionals ([ResourceRecord]): the additional section + """ + if questions is None: + questions = [] + if answers is None: + answers = [] + if authorities is None: + authorities = [] + if additionals is None: + additionals = [] + + self.header = header + self.questions = questions + self.answers = answers + self.authorities = authorities + self.additionals = additionals + + @property + def resources(self): + """ Getter for all resource records """ + return self.answers + self.authorities + self.additionals + + def to_bytes(self): + """ Convert Message to bytes """ + composer = Composer() + + # Add header + result = self.header.to_bytes() + + # Add questions + for question in self.questions: + offset = len(result) + result += question.to_bytes(offset, composer) + + # Add answers + for answer in self.answers: + offset = len(result) + result += answer.to_bytes(offset, composer) + + # Add authorities + for authority in self.authorities: + offset = len(result) + result += authority.to_bytes(offset, composer) + + # Add additionals + for additional in self.additionals: + offset = len(result) + result += additional.to_bytes(offset, composer) + + return result + + @classmethod + def from_bytes(cls, packet): + """ Create Message from bytes + + Args: + packet (bytes): byte representation of the message + """ + parser = Parser() + + # Parse header + header, offset = Header.from_bytes(packet), 12 + + # Parse questions + questions = [] + for _ in range(header.qd_count): + question, offset = Question.from_bytes(packet, offset, parser) + questions.append(question) + + # Parse answers + answers = [] + for _ in range(header.an_count): + answer, offset = ResourceRecord.from_bytes(packet, offset, parser) + answers.append(answer) + + # Parse authorities + authorities = [] + for _ in range(header.ns_count): + authority, offset = ResourceRecord.from_bytes(packet, offset, parser) + authorities.append(authority) + + # Parse additionals + additionals = [] + for _ in range(header.ar_count): + additional, offset = ResourceRecord.from_bytes(packet, offset, parser) + additionals.append(additional) + + return cls(header, questions, answers, authorities, additionals) + + +class Header(object): + """ The header section of a DNS message + + Contains a number of properties which are accessible as normal member + variables. + + See section 4.1.1 of RFC 1035 for their meaning. + """ + + def __init__(self, ident, flags, qd_count, an_count, ns_count, ar_count): + """ Create a new Header object + + Args: + ident (int): identifier + qd_count (int): number of entries in question section + an_count (int): number of entries in answer section + ns_count (int): number of entries in authority section + ar_count (int): number of entries in additional section + """ + self.ident = ident + self._flags = flags + self.qd_count = qd_count + self.an_count = an_count + self.ns_count = ns_count + self.ar_count = ar_count + + def to_bytes(self): + """ Convert header to bytes """ + return struct.pack("!6H", + self.ident, + self._flags, + self.qd_count, + self.an_count, + self.ns_count, + self.ar_count) + + @classmethod + def from_bytes(cls, packet): + """ Convert Header from bytes """ + if len(packet) < 12: + raise ValueError("header is too short") + return cls(*struct.unpack_from("!6H", packet)) + + @property + def flags(self): + return self._flags + @flags.setter + def flags(self, value): + if value >= (1 << 16): + raise ValueError("value too big for flags") + self._flags = value + + @property + def qr(self): + return self._flags & (1 << 15) + @qr.setter + def qr(self, value): + if value: + self._flags |= (1 << 15) + else: + self._flags &= ~(1 << 15) + + @property + def opcode(self): + return (self._flags & (((1 << 4) - 1) << 11)) >> 11 + @opcode.setter + def opcode(self, value): + if value > 0b1111: + raise ValueError("invalid opcode") + self._flags &= ~(((1 << 4) - 1) << 11) + self._flags |= value << 11 + + @property + def aa(self): + return self._flags & (1 << 10) + @aa.setter + def aa(self, value): + if value: + self._flags |= (1 << 10) + else: + self._flags &= ~(1 << 10) + + @property + def tc(self): + return self._flags & (1 << 9) + @tc.setter + def tc(self, value): + if value: + self._flags |= (1 << 9) + else: + self._flags &= ~(1 << 9) + + @property + def rd(self): + return self._flags & (1 << 8) + @rd.setter + def rd(self, value): + if value: + self._flags |= (1 << 8) + else: + self._flags &= ~(1 << 8) + + @property + def ra(self): + return self._flags & (1 << 7) + @ra.setter + def ra(self, value): + if value: + self._flags |= (1 << 7) + else: + self._flags &= ~(1 << 7) + + @property + def z(self): + return self._flags & (((1 << 3) - 1) << 4) >> 4 + @z.setter + def z(self, value): + if value: + raise ValueError("non-zero zero flag") + + @property + def rcode(self): + return self._flags & ((1 << 4) - 1) + @rcode.setter + def rcode(self, value): + if value > 0b1111: + raise ValueError("invalid return code") + self._flags &= ~((1 << 4) - 1) + self._flags |= value + + +class Question(object): + """ An entry in the question section. + + See section 4.1.2 of RFC 1035 for more info. + """ + + def __init__(self, qname, qtype, qclass): + """ Create a new entry in the question section + + Args: + qname (str): QNAME + qtype (Type): QTYPE + qclass (Class): QCLASS + """ + self.qname = qname + self.qtype = qtype + self.qclass = qclass + + def to_bytes(self, offset, composer): + """ Convert Question to bytes """ + bqname = composer.to_bytes(offset, [self.qname]) + bqtype = struct.pack("!H", self.qtype) + bqclass = struct.pack("!H", self.qclass) + return bqname + bqtype + bqclass + + @classmethod + def from_bytes(cls, packet, offset, parser): + """ Convert Question from bytes """ + qnames, offset = parser.from_bytes(packet, offset, 1) + qname = qnames[0] + qtype, qclass = struct.unpack_from("!2H", packet, offset) + return cls(qname, qtype, qclass), offset + 4 diff --git a/dns/rcodes.py b/dns/rcodes.py new file mode 100644 index 0000000..73d6470 --- /dev/null +++ b/dns/rcodes.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python2 + +""" DNS RCODE values + +This module contains an Enum of RCODE values. See section 4.1.4 of RFC 1035 for +more info. +""" + +class RCode(object): + """ Enum of RCODE values + + Usage: + >>> NoError + 0 + >>> NXDomain + 3 + """ + + NoError = 0 + FormErr = 1 + ServFail = 2 + NXDomain = 3 + NotImp = 4 + Refused = 5 + YXDomain = 6 + YXRRSet = 7 + NXRRSet = 8 + NotAuth = 9 + NotZone = 10 + BADVERS = 16 + BADSIG = 16 + BADKEY = 17 + BADTIME = 18 + BADMODE = 19 + BADNAME = 20 + BADALG = 21 + BADTRUNC = 22 + + by_string = { + "NoError": NoError, + "FormErr": FormErr, + "ServFail": ServFail, + "NXDomain": NXDomain, + "NotImp": NotImp, + "Refused": Refused, + "YXDomain": YXDomain, + "YXRRSet": YXRRSet, + "NXRRSet": NXRRSet, + "NotAuth": NotAuth, + "NotZone": NotZone, + "BADVERS": BADVERS, + "BADSIG": BADSIG, + "BADKEY": BADKEY, + "BADTIME": BADTIME, + "BADMODE": BADMODE, + "BADNAME": BADNAME, + "BADALG": BADALG, + "BADTRUNC": BADTRUNC + } + + by_value = dict([(y, x) for x, y in by_string.items()]) + + @staticmethod + def to_string(rcode): + """ Convert an RCode to a string + + Usage: + >>> RCode.to_string(RCode.NoError) + 'NoError' + """ + return RCode.by_value[rcode] + + @staticmethod + def from_string(string): + """ Convert a string to an RCode + + Usage: + >>> RCode.from_string('NoError') + 0 + """ + return RCode.by_string[string] diff --git a/dns/resolver.py b/dns/resolver.py new file mode 100644 index 0000000..3cc6426 --- /dev/null +++ b/dns/resolver.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python2 + +""" DNS Resolver + +This module contains a class for resolving hostnames. You will have to implement +things in this module. This resolver will be both used by the DNS client and the +DNS server, but with a different list of servers. +""" + +import socket + +from dns.classes import Class +from dns.types import Type + +import dns.cache +import dns.message +import dns.rcodes + +class Resolver(object): + """ DNS resolver """ + + def __init__(self, caching, ttl): + """ Initialize the resolver + + Args: + caching (bool): caching is enabled if True + ttl (int): ttl of cache entries (if > 0) + """ + self.caching = caching + self.ttl = ttl + + def gethostbyname(self, hostname): + """ Translate a host name to IPv4 address. + + Currently this method contains an example. You will have to replace + this example with example with the algorithm described in section + 5.3.3 in RFC 1034. + + Args: + hostname (str): the hostname to resolve + + Returns: + (str, [str], [str]): (hostname, aliaslist, ipaddrlist) + """ + timeout = 2 + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(timeout) + + # Create and send query + question = dns.message.Question(hostname, Type.A, Class.IN) + header = dns.message.Header(9001, 0, 1, 0, 0, 0) + header.qr = 0 + header.opcode = 0 + header.rd = 1 + query = dns.message.Message(header, [question]) + sock.sendto(query.to_bytes(), ("8.8.8.8", 53)) + + # Receive response + data = sock.recv(512) + response = dns.message.Message.from_bytes(data) + + # Get data + aliases = [] + for additional in response.additionals: + if additional.type_ == Type.CNAME: + aliases.append(additional.rdata.data) + addresses = [] + for answer in response.answers: + if answer.type_ == Type.A: + addresses.append(answer.rdata.data) + + return hostname, aliases, addresses diff --git a/dns/resource.py b/dns/resource.py new file mode 100644 index 0000000..9b17468 --- /dev/null +++ b/dns/resource.py @@ -0,0 +1,241 @@ +#!/usr/bin/env python2 + +""" A DNS resource record + +This class contains classes for DNS resource records and record data. This +module is fully implemented. You will have this module in the implementation +of your resolver and server. +""" + +import socket +import struct + +from dns.types import Type + + +class ResourceRecord(object): + """ DNS resource record """ + def __init__(self, name, type_, class_, ttl, rdata): + """ Create a new resource record + + Args: + name (str): domain name + type_ (Type): the type + class_ (Class): the class + rdata (RecordData): the record data + """ + self.name = name + self.type_ = type_ + self.class_ = class_ + self.ttl = ttl + self.rdata = rdata + + def to_bytes(self, offset, composer): + """ Convert ResourceRecord to bytes """ + record = composer.to_bytes(offset, [self.name]) + record += struct.pack("!HHI", self.type_, self.class_, self.ttl) + offset += len(record) + 2 + rdata = self.rdata.to_bytes(offset, composer) + record += struct.pack("!H", len(rdata)) + rdata + return record + + @classmethod + def from_bytes(cls, packet, offset, parser): + """ Convert ResourceRecord from bytes """ + names, offset = parser.from_bytes(packet, offset, 1) + name = names[0] + type_, class_, ttl, rdlength = struct.unpack_from("!HHIH", packet, offset) + offset += 10 + rdata = RecordData.from_bytes(type_, packet, offset, rdlength, parser) + offset += rdlength + return cls(name, type_, class_, ttl, rdata), offset + + +class RecordData(object): + """ Record Data """ + + def __init__(self, data): + """ Initialize the record data + + Args: + data (str): data + """ + self.data = data + + @staticmethod + def create(type_, data): + """ Create a RecordData object from bytes + + Args: + type_ (Type): type + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + classdict = { + Type.A: ARecordData, + Type.CNAME: CNAMERecordData, + Type.NS: NSRecordData, + Type.AAAA: AAAARecordData + } + if type_ in classdict: + return classdict[type_](data) + else: + return GenericRecordData(data) + + @staticmethod + def from_bytes(type_, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + type_ (Type): type + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + classdict = { + Type.A: ARecordData, + Type.CNAME: CNAMERecordData, + Type.NS: NSRecordData, + Type.AAAA: AAAARecordData + } + if type_ in classdict: + return classdict[type_].from_bytes( + packet, offset, rdlength, parser) + else: + return GenericRecordData.from_bytes( + packet, offset, rdlength, parser) + + +class ARecordData(RecordData): + """ Record data for A type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return socket.inet_aton(self.data) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + data = socket.inet_ntoa(packet[offset:offset+4]) + return cls(data) + + +class CNAMERecordData(RecordData): + """ Record data for CNAME type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return composer.to_bytes(offset, [self.data]) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + names, offset = parser.from_bytes(packet, offset, 1) + data = names[0] + return cls(data) + + +class NSRecordData(RecordData): + """ Record data for NS type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return composer.to_bytes(offset, [self.data]) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + names, offset = parser.from_bytes(packet, offset, 1) + data = names[0] + return cls(data) + + +class AAAARecordData(RecordData): + """ Record data for AAAA type """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return socket.inet_pton(socket.AF_INET6, self.data) + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + data = socket.inet_ntop(socket.AF_INET6, packet[offset:offset+16]) + return cls(data) + + +class GenericRecordData(RecordData): + """ Generic Record Data (for other types) """ + + def to_bytes(self, offset, composer): + """ Convert to bytes + + Args: + offset (int): offset in message + composer (Composer): domain name composer + """ + return self.data + + @classmethod + def from_bytes(cls, packet, offset, rdlength, parser): + """ Create a RecordData object from bytes + + Args: + packet (bytes): packet + offset (int): offset in message + rdlength (int): length of rdata + parser (int): domain name parser + """ + data = packet[offset:offset+rdlength] + return cls(data) diff --git a/dns/server.py b/dns/server.py new file mode 100644 index 0000000..f307db3 --- /dev/null +++ b/dns/server.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python2 + +""" A recursive DNS server + +This module provides a recursive DNS server. You will have to implement this +server using the algorithm described in section 4.3.2 of RFC 1034. +""" + +from threading import Thread + + +class RequestHandler(Thread): + """ A handler for requests to the DNS server """ + + def __init__(self): + """ Initialize the handler thread """ + super(RequestHandler, self).__init__() + self.daemon = True + + def run(self): + """ Run the handler thread """ + # TODO: Handle DNS request + pass + + +class Server(object): + """ A recursive DNS server """ + + def __init__(self, port, caching, ttl): + """ Initialize the server + + Args: + port (int): port that server is listening on + caching (bool): server uses resolver with caching if true + ttl (int): ttl for records (if > 0) of cache + """ + self.caching = caching + self.ttl = ttl + self.port = port + self.done = False + # TODO: create socket + + def serve(self): + """ Start serving request """ + # TODO: start listening + while not self.done: + # TODO: receive request and open handler + pass + + def shutdown(self): + """ Shutdown the server """ + self.done = True + # TODO: shutdown socket diff --git a/dns/types.py b/dns/types.py new file mode 100644 index 0000000..4711fc1 --- /dev/null +++ b/dns/types.py @@ -0,0 +1,67 @@ +#!/usr/bin/env python2 + +""" DNS TYPE and QTYPE values + +This module contains an Enum for TYPE and QTYPE values. This Enum also contains +a method for converting Enum values to strings. See sections 3.2.2 and 3.2.3 of +RFC 1035 for more information. +""" + +class Type(object): + """ DNS TYPE and QTYPE + + Usage: + >>> Type.A + 1 + >>> Type.CNAME + 5 + """ + A = 1 + NS = 2 + CNAME = 5 + SOA = 6 + WKS = 11 + PTR = 12 + HINFO = 13 + MINFO = 14 + MX = 15 + TXT = 16 + AAAA = 28 + ANY = 255 + + by_string = { + "A": A, + "NS": NS, + "CNAME": CNAME, + "SOA": SOA, + "WKS": WKS, + "PTR": PTR, + "HINFO": HINFO, + "MINFO": MINFO, + "MX": MX, + "TXT": TXT, + "AAAA": AAAA, + "*": ANY + } + + by_value = dict([(y, x) for x, y in by_string.items()]) + + @staticmethod + def to_string(type_): + """ Convert a Type to a string + + Usage: + >>> Type.to_string(Type.A) + 'A' + """ + return Type.by_value[type_] + + @staticmethod + def from_string(string): + """ Convert a string to a Type + + Usage: + >>> Type.from_string('CNAME') + 5 + """ + return Type.by_string[string] diff --git a/dns/zone.py b/dns/zone.py new file mode 100644 index 0000000..b7254b9 --- /dev/null +++ b/dns/zone.py @@ -0,0 +1,54 @@ +#!/usr/bin/env python2 + +""" Zones of domain name space + +See section 6.1.2 of RFC 1035 and section 4.2 of RFC 1034. +Instead of tree structures we simply use dictionaries from domain names to +zones or record sets. + +These classes are merely a suggestion, feel free to use something else. +""" + + +class Catalog(object): + """ A catalog of zones """ + + def __init__(self): + """ Initialize the catalog """ + self.zones = {} + + def add_zone(self, name, zone): + """ Add a new zone to the catalog + + Args: + name (str): root domain name + zone (Zone): zone + """ + self.zones[name] = zone + + +class Zone(object): + """ A zone in the domain name space """ + + def __init__(self): + """ Initialize the Zone """ + self.records = {} + + def add_node(self, name, record_set): + """ Add a record set to the zone + + Args: + name (str): domain name + record_set ([ResourceRecord]): resource records + """ + self.records[name] = record_set + + def read_master_file(self, filename): + """ Read the zone from a master file + + See section 5 of RFC 1035. + + Args: + filename (str): the filename of the master file + """ + pass diff --git a/dns_client.py b/dns_client.py new file mode 100644 index 0000000..710a785 --- /dev/null +++ b/dns_client.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python2 + +""" Simple DNS client + +A simple example of a client using the DNS resolver. +""" + +import dns.resolver + +if __name__ == "__main__": + # Parse arguments + import argparse + parser = argparse.ArgumentParser(description="DNS Client") + parser.add_argument("hostname", help="hostname to resolve") + parser.add_argument("-c", "--caching", action="store_true", + help="Enable caching") + parser.add_argument("-t", "--ttl", metavar="time", type=int, default=0, + help="TTL value of cached entries") + args = parser.parse_args() + + # Resolve hostname + resolver = dns.resolver.Resolver(args.caching, args.ttl) + hostname, aliases, addresses = resolver.gethostbyname(args.hostname) + + # Print output + print(hostname) + print(aliases) + print(addresses) diff --git a/dns_server.py b/dns_server.py new file mode 100644 index 0000000..f1d4b09 --- /dev/null +++ b/dns_server.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python2 + +""" DNS server + +This script contains the code for starting a DNS server. +""" + +import dns.server + +if __name__ == "__main__": + # Parse arguments + import argparse + parser = argparse.ArgumentParser(description="DNS Server") + parser.add_argument("-c", "--caching", action="store_true", + help="Enable caching") + parser.add_argument("-t", "--ttl", metavar="time", type=int, default=0, + help="TTL value of cached entries (if > 0)") + parser.add_argument("-p", "--port", type=int, default=5353, + help="Port which server listens on") + args = parser.parse_args() + + # Start server + server = dns.server.Server(args.port, args.caching, args.ttl) + try: + server.serve() + except KeyboardInterrupt: + server.shutdown() + print() diff --git a/dns_tests.py b/dns_tests.py new file mode 100644 index 0000000..66c04ed --- /dev/null +++ b/dns_tests.py @@ -0,0 +1,34 @@ +#!/usr/bin/env python2 + +""" Tests for your DNS resolver and server """ + +portnr = 5353 +server = "localhost" + +class TestResolver(unittest.TestCase): + pass + + +class TestResolverCache(unittest.TestCase): + pass + + +class TestServer(unittest.TestCase): + pass + + +if __name__ == "__main__": + # Parse command line arguments + import argparse + parser = argparse.ArgumentParser(description="HTTP Tests") + parser.add_argument("-s", "--server", type=str, default="localhost") + parser.add_argument("-p", "--port", type=int, default=5001) + args, extra = parser.parse_known_args() + portnr = args.port + server = args.server + + # Pass the extra arguments to unittest + sys.argv[1:] = extra + + # Start test suite + unittest.main()