diff options
Diffstat (limited to 'project2')
-rw-r--r-- | project2/proj2_s4498062/.gitignore | 2 | ||||
-rw-r--r-- | project2/proj2_s4498062/README.md | 45 | ||||
-rw-r--r-- | project2/proj2_s4498062/cloogle.zone | 13 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/__init__.py | 1 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/cache.py | 119 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/classes.py | 54 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/domainname.py | 125 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/message.py | 288 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/rcodes.py | 82 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/regexes.py | 46 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/resolver.py | 153 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/resource.py | 248 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/server.py | 133 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/types.py | 69 | ||||
-rw-r--r-- | project2/proj2_s4498062/dns/zone.py | 54 | ||||
-rwxr-xr-x | project2/proj2_s4498062/dns_client.py | 35 | ||||
-rwxr-xr-x | project2/proj2_s4498062/dns_server.py | 38 | ||||
-rwxr-xr-x | project2/proj2_s4498062/dns_tests.py | 88 |
18 files changed, 1593 insertions, 0 deletions
diff --git a/project2/proj2_s4498062/.gitignore b/project2/proj2_s4498062/.gitignore new file mode 100644 index 0000000..0191c0c --- /dev/null +++ b/project2/proj2_s4498062/.gitignore @@ -0,0 +1,2 @@ +*.pyc +.dns.cache diff --git a/project2/proj2_s4498062/README.md b/project2/proj2_s4498062/README.md new file mode 100644 index 0000000..ce89073 --- /dev/null +++ b/project2/proj2_s4498062/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/project2/proj2_s4498062/cloogle.zone b/project2/proj2_s4498062/cloogle.zone new file mode 100644 index 0000000..807b3f8 --- /dev/null +++ b/project2/proj2_s4498062/cloogle.zone @@ -0,0 +1,13 @@ +; vim: ft=bindzone: +; The Cloogle zone file + +cloogle.org. 3600000 NS ns1.p01.antagonist.nl +cloogle.org. 3600000 NS ns2.p01.antagonist.net +cloogle.org. 3600000 NS ns3.p01.antagonist.de + +cloogle.org. 3600000 A 84.22.111.158 +cloogle.org. 3600000 AAAA 2a02:2770:17:0:21a:4aff:fe1d:9a23 + +www.cloogle.org. 3600000 CNAME cloogle.org + +; End of file diff --git a/project2/proj2_s4498062/dns/__init__.py b/project2/proj2_s4498062/dns/__init__.py new file mode 100644 index 0000000..992b090 --- /dev/null +++ b/project2/proj2_s4498062/dns/__init__.py @@ -0,0 +1 @@ +"""DNS tools""" diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py new file mode 100644 index 0000000..a8b62be --- /dev/null +++ b/project2/proj2_s4498062/dns/cache.py @@ -0,0 +1,119 @@ +"""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 +import time + +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, + "timestamp": obj.timestamp + } + 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"]) + timestamp = dct["timestamp"] + return ResourceRecord(name, type_, class_, ttl, rdata, timestamp) + + +class RecordCache(object): + """ Cache for ResourceRecords """ + + FILE = '.dns.cache' + + def __init__(self, ttl): + """ Initialize the RecordCache + + Args: + ttl (int): TTL of cached entries (if > 0) + """ + self.records = [] + self.read_cache_file() + self.ttl = ttl + + def __del__(self): + self.write_cache_file() + + def remove_old(self): + """Remove entries for which the TTL has expired""" + now = int(time.time()) + for record in reversed(self.records): + if min(self.ttl, record.ttl) + record.timestamp < now: + self.records.remove(record) + + 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 + """ + self.remove_old() + return [ + r for r in self.records + if r.match(name=dname, type_=type_, class_=class_)] + + def add_records_from(self, msg): + for record in msg.answers + msg.authorities + msg.additionals: + if record.type_ in [Type.A, Type.AAAA, Type.CNAME, Type.NS] and \ + record.class_ == Class.IN: + self.add_record(record) + + def add_record(self, record): + """ Add a new Record to the cache + + Args: + record (ResourceRecord): the record added to the cache + """ + self.records.append(record) + + def read_cache_file(self): + """ Read the cache file from disk """ + try: + with open(self.FILE, 'r') as jsonfile: + self.records = json.load( + jsonfile, object_hook=resource_from_json) + except IOError: + pass + + def write_cache_file(self): + """ Write the cache file to disk """ + self.remove_old() + with open(self.FILE, 'w') as jsonfile: + json.dump(self.records, jsonfile, cls=ResourceEncoder, indent=4) diff --git a/project2/proj2_s4498062/dns/classes.py b/project2/proj2_s4498062/dns/classes.py new file mode 100644 index 0000000..b6123cd --- /dev/null +++ b/project2/proj2_s4498062/dns/classes.py @@ -0,0 +1,54 @@ +""" 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 + """ + + # pylint: disable=invalid-name + 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/project2/proj2_s4498062/dns/domainname.py b/project2/proj2_s4498062/dns/domainname.py new file mode 100644 index 0000000..81b5f4c --- /dev/null +++ b/project2/proj2_s4498062/dns/domainname.py @@ -0,0 +1,125 @@ +""" 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/project2/proj2_s4498062/dns/message.py b/project2/proj2_s4498062/dns/message.py new file mode 100644 index 0000000..fd858b2 --- /dev/null +++ b/project2/proj2_s4498062/dns/message.py @@ -0,0 +1,288 @@ +""" 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 +from dns.types import Type +from dns.classes import Class + + +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): + auth, offset = ResourceRecord.from_bytes(packet, offset, parser) + authorities.append(auth) + + # Parse additionals + additionals = [] + for _ in range(header.ar_count): + addit, offset = ResourceRecord.from_bytes(packet, offset, parser) + additionals.append(addit) + + 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. + """ + + # pylint: disable=missing-docstring, invalid-name + + 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 + + def __repr__(self): + return '{} {} {}'.format( + self.qname, + Type.to_string(self.qtype), + Class.to_string(self.qclass)) diff --git a/project2/proj2_s4498062/dns/rcodes.py b/project2/proj2_s4498062/dns/rcodes.py new file mode 100644 index 0000000..4f40621 --- /dev/null +++ b/project2/proj2_s4498062/dns/rcodes.py @@ -0,0 +1,82 @@ +#!/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/project2/proj2_s4498062/dns/regexes.py b/project2/proj2_s4498062/dns/regexes.py new file mode 100644 index 0000000..ffa1770 --- /dev/null +++ b/project2/proj2_s4498062/dns/regexes.py @@ -0,0 +1,46 @@ +"""Regexes used in the DNS protocol""" + + +def grpm(regex): + """Make a matching group""" + return grp(regex, matching=True) + + +def grp(regex, matching=False): + """Make a group""" + return r'(' + (r'' if matching else r'?:') + regex + r')' + + +def opt(regex): + """Make an optional group""" + return grp(grp(regex) + r'?') + + +def regex_opt_r(*regexes): + """Make a group that matches one of the given regexes""" + return grp(r'|'.join(regexes)) + + +DIGIT = r'\d' +LETTER = r'[a-zA-Z]' +LETDIG = grp(regex_opt_r(DIGIT, LETTER)) +LETDIGHYP = grp(regex_opt_r(LETDIG, r'-')) +LDHSTR = grp(LETDIGHYP + r'+') +LABEL = grp(LETTER + opt(opt(LDHSTR) + LETDIG)) +SUBDOMAIN = grp(grpm(grp(LABEL + r'\.') + r'*') + grpm(LABEL)) +DOMAIN = regex_opt_r(SUBDOMAIN, r' ') + +# Fast, non-matching domain +_DOMAIN = r'(?:(?:[a-zA-Z](?:[a-zA-Z\d\-]*[a-zA-Z\d])?\.)*)?' + +IP = r'(?:(?:\d{1,3}\.){3}\d{1,3})' + +CLASS = regex_opt_r(r'IN', r'CH') +TYPE = regex_opt_r(r'A', r'CNAME', r'HINFO', r'MX', r'NS', r'PTR', r'SOA') +TTL = r'\d+' +RDATA = r'.*?' +RR = regex_opt_r( + grp(grpm(TTL) + r'\s+' + opt(grpm(CLASS))), + grp(grpm(CLASS) + r'\s+' + opt(grpm(TTL))) + ) + r'\s+' + grpm(TYPE) + r'\s+' + grpm(RDATA) + r'\s*(?:(?<!\\);)?\s*$' +ZONE_LINE_DOMAIN = r'^' + grpm(_DOMAIN) + r'\s+' + RR diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py new file mode 100644 index 0000000..4c09681 --- /dev/null +++ b/project2/proj2_s4498062/dns/resolver.py @@ -0,0 +1,153 @@ +#!/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. +""" + +from random import randint +import socket + +from dns.classes import Class +from dns.types import Type + +from dns.cache import RecordCache +from dns.message import Message, Question, Header + + +class Resolver(object): + """ DNS resolver """ + + ROOT_SERVERS = [ + '198.41.0.4', + '192.228.79.201', + '192.33.4.12', + '199.7.91.13', + '192.203.230.10', + '192.5.5.241', + '192.112.36.4', + '198.97.190.53', + '192.36.148.17', + '192.58.128.30', + '193.0.14.129', + '199.7.83.42', + '202.12.27.33' + ] + + def __init__(self, caching, ttl, timeout=3): + """ 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 + self.timeout = timeout + + if self.caching: + self.cache = RecordCache(ttl) + + def do_query(self, hint, hostname, type_, class_=Class.IN, caching=True): + """Do a query to a hint""" + if self.caching and caching: + records = self.cache.lookup(hostname, type_, class_) + if records != []: + return records + + ident = randint(0, 65535) + header = Header(ident, 0, 1, 0, 0, 0) + header.qr = 0 + header.opcode = 0 + header.rd = 1 + req = Message(header, [Question(hostname, type_, class_)]) + + sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + sock.settimeout(self.timeout) + sock.sendto(req.to_bytes(), (hint, 53)) + + try: + data = sock.recv(512) + resp = Message.from_bytes(data) + if resp.header.ident == ident: + if self.caching and caching: + self.cache.add_records_from(resp) + return resp.answers + resp.authorities + resp.additionals + except socket.timeout: + pass + return [] + + def do_query_to_multiple( + self, hints, hostname, type_, class_=Class.IN, caching=True): + """Do a query to multiple hints, return the remaining hints""" + while hints != []: + hint = hints.pop() + response = self.do_query(hint, hostname, type_, class_, caching) + if response is not None: + return hints, response + return [], [] + + def gethostbyname(self, hostname): + """ Translate a host name to IPv4 address. + + Args: + hostname (str): the hostname to resolve + + Returns: + (str, [str], [str]): (hostname, aliaslist, ipaddrlist) + """ + if self.caching: + addrs = self.cache.lookup(hostname, Type.A, Class.IN) + cnames = self.cache.lookup(hostname, Type.CNAME, Class.IN) + if addrs != []: + return hostname, \ + [r.rdata.data for r in cnames], \ + [r.rdata.data for r in addrs] + for cname in cnames: + cname, aliases, addrs = self.gethostbyname(cname.rdata.data) + if addrs != []: + return str(cname), aliases, addrs + + if hostname == '': + return hostname, [], [] + + hints = self.ROOT_SERVERS[:] + aliases = [] + while hints != []: + hints, info = self.do_query_to_multiple(hints, hostname, Type.A) + + aliases += [ + r.rdata.data for r in info + if r.match(type_=Type.CNAME, class_=Class.IN, name=hostname)] + + # Case 1: answer + ips = [ + r.rdata.data for r in info + if r.match(type_=Type.A, class_=Class.IN, name=hostname)] + if ips != []: + return hostname, aliases, ips + + # Case 2: name servers + auths = [ + r.rdata.data for r in info + if r.match(type_=Type.NS, class_=Class.IN)] + ips = [ + add.rdata.data for ns in auths for add in info + if add.match(name=ns, type_=Type.A)] + if ips != []: + hints += ips + continue + if auths != []: + auths = [h for a in auths for h in self.gethostbyname(a)[2]] + hints += auths + continue + + # Case 3: aliases + for alias in aliases: + _, extra_aliases, alias_addresses = self.gethostbyname(alias) + if alias_addresses != []: + return hostname, aliases + extra_aliases, alias_addresses + + return hostname, aliases, [] diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py new file mode 100644 index 0000000..e552c22 --- /dev/null +++ b/project2/proj2_s4498062/dns/resource.py @@ -0,0 +1,248 @@ +""" 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 +import time + +from dns.types import Type + + +class ResourceRecord(object): + """ DNS resource record """ + def __init__(self, name, type_, class_, ttl, rdata, timestamp=None): + """ 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 + self.timestamp = int(timestamp or time.time()) + + def match(self, name=None, type_=None, class_=None, ttl=None): + """Check if the record matches properties""" + return (name is None or self.name == name) and \ + (type_ is None or self.type_ == type_) and \ + (class_ is None or self.class_ == class_) and \ + (ttl is None or self.ttl == ttl) + + 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, rdlen = struct.unpack_from("!HHIH", packet, offset) + offset += 10 + rdata = RecordData.from_bytes(type_, packet, offset, rdlen, parser) + offset += rdlen + 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): + """Data of an A record""" + + 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): + """Data of a CNAME record""" + + 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): + """Data of an NS record""" + + 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): + """Data of an AAAA record""" + + 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): + """Data of a generic record""" + + 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/project2/proj2_s4498062/dns/server.py b/project2/proj2_s4498062/dns/server.py new file mode 100644 index 0000000..10cad8b --- /dev/null +++ b/project2/proj2_s4498062/dns/server.py @@ -0,0 +1,133 @@ +""" 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. +""" +import re +import socket +from threading import Thread + +import dns.regexes as rgx +from dns.classes import Class +from dns.types import Type +from dns.message import Header, Message +from dns.resolver import Resolver +from dns.resource import \ + ResourceRecord, ARecordData, NSRecordData, CNAMERecordData + + +class RequestHandler(Thread): + """ A handler for requests to the DNS server """ + + def __init__(self, skt, ttl, data, addr, zone): + # pylint: disable=too-many-arguments + """ Initialize the handler thread """ + super(RequestHandler, self).__init__() + self.daemon = True + self.skt = skt + self.ttl = ttl + self.data = data + self.addr = addr + self.zone = zone + + def run(self): + """ Run the handler thread """ + resolver = Resolver(True, self.ttl) + + request = Message.from_bytes(self.data) + answs, adds, auths = [], [], [] + + for req in request.questions: + rrs = [ + r for r in self.zone + if r.match(type_=req.qtype, class_=req.qclass, name=req.qname)] + if rrs != []: + auths += rrs + elif req.qtype in [Type.A, Type.CNAME] and req.qclass == Class.IN: + name, cnames, addrs = resolver.gethostbyname(req.qname) + if name != req.qname: + answs.append(ResourceRecord( + str(req.qname), Type.CNAME, Class.IN, self.ttl, + CNAMERecordData(str(name)))) + # pylint: disable=bad-continuation + addrs = [ResourceRecord( + name, Type.A, Class.IN, self.ttl, + ARecordData(data)) + for data in addrs] + cnames = [ResourceRecord( + name, Type.CNAME, Class.IN, self.ttl, + CNAMERecordData(data)) + for data in cnames] + if req.qtype == Type.A: + answs += addrs + cnames + if req.qtype == Type.CNAME: + answs += cnames + + header = Header( + request.header.ident, 0, 0, len(answs), len(auths), len(adds)) + response = Message(header, None, answs, auths, adds) + + self.skt.sendto(response.to_bytes(), self.addr) + + +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 + self.zone = [] + + self.skt = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + + def serve(self): + """ Start serving request """ + self.skt.bind(('localhost', self.port)) + while not self.done: + data, addr = self.skt.recvfrom(512) + reqh = RequestHandler( + self.skt, self.ttl, data, addr, zone=self.zone) + reqh.start() + + def shutdown(self): + """ Shutdown the server """ + self.skt.close() + self.done = True + + def parse_zone_file(self, fname): + """Parse a zone file + + Will crash if the zone file has incorrect syntax. + """ + with open(fname) as zonef: + zone = zonef.read() + ttl, class_ = 3600000, Class.IN + for match in re.finditer(rgx.ZONE_LINE_DOMAIN, zone, re.MULTILINE): + match = match.groups() + name = match[0][:-1] + ttl = int(match[1] or match[4] or ttl * 1000) / 1000 + class_ = Class.from_string( + match[2] or match[3] or Class.to_string(class_)) + type_ = Type.from_string(match[5]) + data = match[6] + + if type_ == Type.A: + cls = ARecordData + elif type_ == Type.NS: + cls = NSRecordData + elif type_ == Type.CNAME: + cls = CNAMERecordData + else: + continue + + record = ResourceRecord(name, type_, class_, ttl, cls(data)) + self.zone.append(record) diff --git a/project2/proj2_s4498062/dns/types.py b/project2/proj2_s4498062/dns/types.py new file mode 100644 index 0000000..494232c --- /dev/null +++ b/project2/proj2_s4498062/dns/types.py @@ -0,0 +1,69 @@ +#!/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 + """ + # pylint: disable=invalid-name + 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/project2/proj2_s4498062/dns/zone.py b/project2/proj2_s4498062/dns/zone.py new file mode 100644 index 0000000..8ada3db --- /dev/null +++ b/project2/proj2_s4498062/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/project2/proj2_s4498062/dns_client.py b/project2/proj2_s4498062/dns_client.py new file mode 100755 index 0000000..0f5e50f --- /dev/null +++ b/project2/proj2_s4498062/dns_client.py @@ -0,0 +1,35 @@ +#!/usr/bin/env python2 + +""" Simple DNS client + +A simple example of a client using the DNS resolver. +""" + +import dns.resolver + + +def main(): + """DNS client""" + # 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 + +if __name__ == "__main__": + main() diff --git a/project2/proj2_s4498062/dns_server.py b/project2/proj2_s4498062/dns_server.py new file mode 100755 index 0000000..3bdd04d --- /dev/null +++ b/project2/proj2_s4498062/dns_server.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python2 + +""" DNS server + +This script contains the code for starting a DNS server. +""" + +import dns.server + + +def main(): + """DNS server""" + # 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=5300, + help="Port which server listens on") + args = parser.parse_args() + + # Start server + server = dns.server.Server(args.port, args.caching, args.ttl) + server.parse_zone_file('cloogle.zone') + + try: + server.serve() + except KeyboardInterrupt: + server.shutdown() + print + +if __name__ == "__main__": + main() diff --git a/project2/proj2_s4498062/dns_tests.py b/project2/proj2_s4498062/dns_tests.py new file mode 100755 index 0000000..4a054b6 --- /dev/null +++ b/project2/proj2_s4498062/dns_tests.py @@ -0,0 +1,88 @@ +#!/usr/bin/env python2 +""" Tests for the DNS resolver and server """ + +import sys +import time +import unittest + +from dns.cache import RecordCache +from dns.classes import Class +from dns.resolver import Resolver +from dns.resource import ResourceRecord, CNAMERecordData +from dns.types import Type + +portnr = 5300 +server = '127.0.0.1' + +class TestResolver(unittest.TestCase): + """Test cases for the resolver with caching disabled""" + + def setUp(self): + self.resolv = Resolver(False, 0) + + def test_solve(self): + """Test solving some FQDN""" + host, aliases, addrs = self.resolv.gethostbyname('camilstaps.nl') + self.assertEqual(host, 'camilstaps.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, ['84.22.111.158']) + + def test_nonexistant(self): + """Test solving a nonexistant FQDN""" + host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl') + self.assertEqual(host, 'nothing.ru.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, []) + + +class TestResolverCache(unittest.TestCase): + """Test cases for the resolver with caching enabled""" + + TTL = 3 + + def setup_resolver(self): + """Setup a resolver with an invalid cache""" + self.resolv = Resolver(True, self.TTL) + self.cache = RecordCache(self.TTL) + self.cache.add_record(ResourceRecord( + 'nothing.ru.nl', Type.CNAME, Class.IN, self.TTL, + CNAMERecordData('camilstaps.nl'))) + self.resolv.cache = self.cache + + def test_solve_invalid(self): + """Test solving an invalid cached FQDN""" + self.setup_resolver() + host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl') + self.assertEqual(host, 'camilstaps.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, ['84.22.111.158']) + + def test_solve_invalid_after_expiration(self): + """Test solving an invalid cached FQDN after TTL expiration""" + self.setup_resolver() + time.sleep(self.TTL + 1) + host, aliases, addrs = self.resolv.gethostbyname('nothing.ru.nl') + self.assertEqual(host, 'nothing.ru.nl') + self.assertEqual(aliases, []) + self.assertEqual(addrs, []) + + +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=5300) + 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() |