From 7500f59ce0dbd8d376c008001dd638ad2844ab2b Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 23 May 2016 22:01:31 +0200 Subject: Framework 2 --- project2/proj2_s4498062/dns/resource.py | 241 ++++++++++++++++++++++++++++++++ 1 file changed, 241 insertions(+) create mode 100644 project2/proj2_s4498062/dns/resource.py (limited to 'project2/proj2_s4498062/dns/resource.py') diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py new file mode 100644 index 0000000..fdf51de --- /dev/null +++ b/project2/proj2_s4498062/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) -- cgit v1.2.3 From 73ceb0d501b63dadc33e21e9ec367bf0e2f26a6a Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 23 May 2016 22:09:33 +0200 Subject: Framework 2: most basic pylint stuff --- project2/proj2_s4498062/dns/__init__.py | 2 +- project2/proj2_s4498062/dns/cache.py | 2 -- project2/proj2_s4498062/dns/classes.py | 3 +-- project2/proj2_s4498062/dns/domainname.py | 4 +--- project2/proj2_s4498062/dns/message.py | 16 +++++++++------- project2/proj2_s4498062/dns/rcodes.py | 1 + project2/proj2_s4498062/dns/resolver.py | 1 + project2/proj2_s4498062/dns/resource.py | 3 ++- project2/proj2_s4498062/dns/types.py | 2 ++ project2/proj2_s4498062/dns_client.py | 27 +++++++++++++++++---------- project2/proj2_s4498062/dns_server.py | 22 +++++++++++++++------- 11 files changed, 50 insertions(+), 33 deletions(-) (limited to 'project2/proj2_s4498062/dns/resource.py') diff --git a/project2/proj2_s4498062/dns/__init__.py b/project2/proj2_s4498062/dns/__init__.py index 18ff536..992b090 100644 --- a/project2/proj2_s4498062/dns/__init__.py +++ b/project2/proj2_s4498062/dns/__init__.py @@ -1 +1 @@ -#!/usr/bin/env python2 +"""DNS tools""" diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py index e148d3a..3ef14b3 100644 --- a/project2/proj2_s4498062/dns/cache.py +++ b/project2/proj2_s4498062/dns/cache.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """A cache for resource records This module contains a class which implements a cache for DNS resource records, diff --git a/project2/proj2_s4498062/dns/classes.py b/project2/proj2_s4498062/dns/classes.py index 2a6a67d..b6123cd 100644 --- a/project2/proj2_s4498062/dns/classes.py +++ b/project2/proj2_s4498062/dns/classes.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ DNS CLASS and QCLASS values This module contains an Enum of CLASS and QCLASS values. The Enum also contains @@ -18,6 +16,7 @@ class Class(object): 255 """ + # pylint: disable=invalid-name IN = 1 CS = 2 CH = 3 diff --git a/project2/proj2_s4498062/dns/domainname.py b/project2/proj2_s4498062/dns/domainname.py index 8876b48..81b5f4c 100644 --- a/project2/proj2_s4498062/dns/domainname.py +++ b/project2/proj2_s4498062/dns/domainname.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ Parsing and composing domain names This module contains two classes for converting domain names to and from bytes. @@ -18,7 +16,7 @@ class Composer(object): self.offsets = dict() def to_bytes(self, offset, dnames): - # Convert each domain name in to bytes + """Convert each domain name in to bytes""" result = b"" for dname in dnames: # Split domain name into labels diff --git a/project2/proj2_s4498062/dns/message.py b/project2/proj2_s4498062/dns/message.py index 54933b7..baaba17 100644 --- a/project2/proj2_s4498062/dns/message.py +++ b/project2/proj2_s4498062/dns/message.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ DNS messages This module contains classes for DNS messages, their header section and @@ -15,7 +13,9 @@ from dns.resource import ResourceRecord class Message(object): """ DNS message """ - def __init__(self, header, questions=None, answers=None, authorities=None, additionals=None): + def __init__( + self, header, questions=None, answers=None, authorities=None, + additionals=None): """ Create a new DNS message Args: @@ -101,14 +101,14 @@ class Message(object): # Parse authorities authorities = [] for _ in range(header.ns_count): - authority, offset = ResourceRecord.from_bytes(packet, offset, parser) - authorities.append(authority) + auth, offset = ResourceRecord.from_bytes(packet, offset, parser) + authorities.append(auth) # Parse additionals additionals = [] for _ in range(header.ar_count): - additional, offset = ResourceRecord.from_bytes(packet, offset, parser) - additionals.append(additional) + addit, offset = ResourceRecord.from_bytes(packet, offset, parser) + additionals.append(addit) return cls(header, questions, answers, authorities, additionals) @@ -122,6 +122,8 @@ class Header(object): 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 diff --git a/project2/proj2_s4498062/dns/rcodes.py b/project2/proj2_s4498062/dns/rcodes.py index 4b1d33d..4f40621 100644 --- a/project2/proj2_s4498062/dns/rcodes.py +++ b/project2/proj2_s4498062/dns/rcodes.py @@ -6,6 +6,7 @@ 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 diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index ba27f74..29451bf 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -16,6 +16,7 @@ import dns.cache import dns.message import dns.rcodes + class Resolver(object): """ DNS resolver """ diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index fdf51de..19a09bb 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -44,7 +44,8 @@ class ResourceRecord(object): """ 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) + type_, class_, ttl, rdlength = struct.unpack_from( + "!HHIH", packet, offset) offset += 10 rdata = RecordData.from_bytes(type_, packet, offset, rdlength, parser) offset += rdlength diff --git a/project2/proj2_s4498062/dns/types.py b/project2/proj2_s4498062/dns/types.py index f918050..494232c 100644 --- a/project2/proj2_s4498062/dns/types.py +++ b/project2/proj2_s4498062/dns/types.py @@ -7,6 +7,7 @@ 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 @@ -16,6 +17,7 @@ class Type(object): >>> Type.CNAME 5 """ + # pylint: disable=invalid-name A = 1 NS = 2 CNAME = 5 diff --git a/project2/proj2_s4498062/dns_client.py b/project2/proj2_s4498062/dns_client.py index b15d566..0f5e50f 100644 --- a/project2/proj2_s4498062/dns_client.py +++ b/project2/proj2_s4498062/dns_client.py @@ -7,22 +7,29 @@ A simple example of a client using the DNS resolver. import dns.resolver -if __name__ == "__main__": + +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") + 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) + 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 index 07e0d9a..4ac2ec4 100644 --- a/project2/proj2_s4498062/dns_server.py +++ b/project2/proj2_s4498062/dns_server.py @@ -7,16 +7,21 @@ This script contains the code for starting a DNS server. import dns.server -if __name__ == "__main__": + +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=5353, - help="Port which server listens on") + 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 @@ -26,3 +31,6 @@ if __name__ == "__main__": except KeyboardInterrupt: server.shutdown() print() + +if __name__ == "__main__": + main() -- cgit v1.2.3 From d946765f5a96dd4cfae2d5139dc6f4bd3cf85e60 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Tue, 24 May 2016 17:57:35 +0200 Subject: Working resolver --- project2/proj2_s4498062/dns/resolver.py | 140 ++++++++++++++++++++++++++------ project2/proj2_s4498062/dns/resource.py | 10 +++ 2 files changed, 123 insertions(+), 27 deletions(-) (limited to 'project2/proj2_s4498062/dns/resource.py') diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index 29451bf..ffb7c16 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -7,20 +7,37 @@ 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 import dns.cache -import dns.message +from dns.message import Message, Question, Header import dns.rcodes class Resolver(object): """ DNS resolver """ - def __init__(self, caching, ttl): + 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: @@ -29,6 +46,38 @@ class Resolver(object): """ self.caching = caching self.ttl = ttl + self.timeout = timeout + + def do_query(self, hint, hostname, type_, class_=Class.IN): + """Do a query to a hint""" + 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: + return resp + except socket.timeout: + pass + return None + + def do_query_to_multiple(self, hints, hostname, type_, class_=Class.IN): + """Do a query to multiple hints, return the remaining hints""" + while hints != []: + hint = hints.pop() + response = self.do_query(hint, hostname, type_, class_) + if response is not None: + return hints, response + return [], None def gethostbyname(self, hostname): """ Translate a host name to IPv4 address. @@ -43,31 +92,68 @@ class Resolver(object): 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)) + domains = hostname.split('.') + hints = self.ROOT_SERVERS - # Receive response - data = sock.recv(512) - response = dns.message.Message.from_bytes(data) + if domains == []: + return hostname, [], [] - # Get data + domain = domains.pop(-1) 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 + while hints != []: + hints, resp = self.do_query_to_multiple(hints, domain, Type.A) + if resp == None: + continue + + info = resp.answers + resp.authorities + resp.additionals + + aliases += [ + r.rdata.data for r in info + if r.match(type_=Type.CNAME, class_=Class.IN, name=domain)] + + # Case 1: answer + ips = [ + r.rdata.data for r in info + if r.match(type_=Type.A, class_=Class.IN, name=domain)] + 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, name=domain)] + ips = [ + add.rdata.data for ns in auths for add in info + if add.match(name=ns, type_=Type.A)] + if ips != []: + hints += ips + if domain != hostname: + domain = domains.pop(-1) + '.' + domain + continue + if auths != []: + auths = [h for a in auths for h in self.gethostbyname(a)[2]] + hints += auths + if domain != hostname: + domain = domains.pop(-1) + '.' + domain + continue + # Case 3: delegation to other name servers + parent = '.'.join(domain.split('.')[1:]) + refs = [ + r.rdata.data for r in info + if r.match(type_=Type.NS, class_=Class.IN, name=parent)] + ips = [ + add.rdata.data for ns in refs for add in info + if add.match(name=ns, type_=Type.A)] + if ips != []: + hints += ips + continue + if refs != []: + refs = [h for r in refs for h in self.gethostbyname(r)[2]] + hints += refs + continue + # Case 4: 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 index 19a09bb..89201ec 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -30,6 +30,13 @@ class ResourceRecord(object): self.ttl = ttl self.rdata = rdata + 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]) @@ -51,6 +58,9 @@ class ResourceRecord(object): offset += rdlength return cls(name, type_, class_, ttl, rdata), offset + def __repr__(self): + return ' '.join(map(str, [self.name, self.type_, self.rdata.data])) + class RecordData(object): """ Record Data """ -- cgit v1.2.3 From bc1d79113ad3fdfcf3319b3cc36f1a0253e64f9d Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Tue, 24 May 2016 18:16:17 +0200 Subject: Caching (mostly from da97de6) --- project2/proj2_s4498062/.gitignore | 3 +- project2/proj2_s4498062/dns/cache.py | 55 ++++++++++++++++++++++++++------- project2/proj2_s4498062/dns/resolver.py | 48 ++++++++++++++++------------ project2/proj2_s4498062/dns/resource.py | 26 +++++++--------- 4 files changed, 85 insertions(+), 47 deletions(-) (limited to 'project2/proj2_s4498062/dns/resource.py') diff --git a/project2/proj2_s4498062/.gitignore b/project2/proj2_s4498062/.gitignore index 94487b9..0191c0c 100644 --- a/project2/proj2_s4498062/.gitignore +++ b/project2/proj2_s4498062/.gitignore @@ -1 +1,2 @@ -*.pyc +*.pyc +.dns.cache diff --git a/project2/proj2_s4498062/dns/cache.py b/project2/proj2_s4498062/dns/cache.py index 3ef14b3..9cde66f 100644 --- a/project2/proj2_s4498062/dns/cache.py +++ b/project2/proj2_s4498062/dns/cache.py @@ -7,6 +7,7 @@ It is highly recommended to use these. """ import json +import time from dns.resource import ResourceRecord, RecordData from dns.types import Type @@ -22,11 +23,12 @@ class ResourceEncoder(json.JSONEncoder): 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 + "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) @@ -42,20 +44,33 @@ def resource_from_json(dct): class_ = Class.from_string(dct["class"]) ttl = dct["ttl"] rdata = RecordData.create(type_, dct["rdata"]) - return ResourceRecord(name, type_, class_, ttl, rdata) + timestamp = dct["timestamp"] + return ResourceRecord(name, type_, class_, ttl, rdata, timestamp) class RecordCache(object): """ Cache for ResourceRecords """ - def __init__(self, ttl): + FILE = '.dns.cache' + + def __init__(self): """ Initialize the RecordCache Args: ttl (int): TTL of cached entries (if > 0) """ self.records = [] - self.ttl = ttl + self.read_cache_file() + + def __del__(self): + self.write_cache_file() + + def remove_old(self): + """Remove entries for which the TTL has expired""" + now = int(time.clock()) + for record in reversed(self.records): + if record.ttl + record.timestamp < now: + self.records.remove(record) def lookup(self, dname, type_, class_): """ Lookup resource records in cache @@ -68,7 +83,16 @@ class RecordCache(object): type_ (Type): type class_ (Class): class """ - pass + 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 @@ -76,12 +100,19 @@ class RecordCache(object): Args: record (ResourceRecord): the record added to the cache """ - pass + self.records.append(record) def read_cache_file(self): """ Read the cache file from disk """ - pass + 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 """ - pass + 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/resolver.py b/project2/proj2_s4498062/dns/resolver.py index ffb7c16..fe46492 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -13,7 +13,7 @@ import socket from dns.classes import Class from dns.types import Type -import dns.cache +from dns.cache import RecordCache from dns.message import Message, Question, Header import dns.rcodes @@ -48,8 +48,16 @@ class Resolver(object): self.ttl = ttl self.timeout = timeout - def do_query(self, hint, hostname, type_, class_=Class.IN): + if self.caching: + self.cache = RecordCache() + + 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 @@ -65,19 +73,22 @@ class Resolver(object): data = sock.recv(512) resp = Message.from_bytes(data) if resp.header.ident == ident: - return resp + if self.caching and caching: + self.cache.add_records_from(resp) + return resp.answers + resp.authorities + resp.additionals except socket.timeout: pass - return None + return [] - def do_query_to_multiple(self, hints, hostname, type_, class_=Class.IN): + 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_) + response = self.do_query(hint, hostname, type_, class_, caching) if response is not None: return hints, response - return [], None + return [], [] def gethostbyname(self, hostname): """ Translate a host name to IPv4 address. @@ -101,11 +112,7 @@ class Resolver(object): domain = domains.pop(-1) aliases = [] while hints != []: - hints, resp = self.do_query_to_multiple(hints, domain, Type.A) - if resp == None: - continue - - info = resp.answers + resp.authorities + resp.additionals + hints, info = self.do_query_to_multiple(hints, domain, Type.A) aliases += [ r.rdata.data for r in info @@ -117,7 +124,8 @@ class Resolver(object): if r.match(type_=Type.A, class_=Class.IN, name=domain)] if ips != []: return hostname, aliases, ips - # Case 2: name servers + + # Case 2: name servers for this domain auths = [ r.rdata.data for r in info if r.match(type_=Type.NS, class_=Class.IN, name=domain)] @@ -135,21 +143,23 @@ class Resolver(object): if domain != hostname: domain = domains.pop(-1) + '.' + domain continue - # Case 3: delegation to other name servers + + # Case 3: name servers for the same domain parent = '.'.join(domain.split('.')[1:]) - refs = [ + auths = [ r.rdata.data for r in info if r.match(type_=Type.NS, class_=Class.IN, name=parent)] ips = [ - add.rdata.data for ns in refs for add in info + 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 refs != []: - refs = [h for r in refs for h in self.gethostbyname(r)[2]] - hints += refs + if auths != []: + auths = [h for r in auths for h in self.gethostbyname(r)[2]] + hints += auths continue + # Case 4: aliases for alias in aliases: _, extra_aliases, alias_addresses = self.gethostbyname(alias) diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index 89201ec..b1c8ae4 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -1,5 +1,3 @@ -#!/usr/bin/env python2 - """ A DNS resource record This class contains classes for DNS resource records and record data. This @@ -9,13 +7,14 @@ 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): + def __init__(self, name, type_, class_, ttl, rdata, timestamp=time.time()): """ Create a new resource record Args: @@ -29,6 +28,7 @@ class ResourceRecord(object): self.class_ = class_ self.ttl = ttl self.rdata = rdata + self.timestamp = timestamp def match(self, name=None, type_=None, class_=None, ttl=None): """Check if the record matches properties""" @@ -51,16 +51,12 @@ class ResourceRecord(object): """ 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) + type_, class_, ttl, rdlen = struct.unpack_from("!HHIH", packet, offset) offset += 10 - rdata = RecordData.from_bytes(type_, packet, offset, rdlength, parser) - offset += rdlength + rdata = RecordData.from_bytes(type_, packet, offset, rdlen, parser) + offset += rdlen return cls(name, type_, class_, ttl, rdata), offset - def __repr__(self): - return ' '.join(map(str, [self.name, self.type_, self.rdata.data])) - class RecordData(object): """ Record Data """ @@ -121,7 +117,7 @@ class RecordData(object): class ARecordData(RecordData): - """ Record data for A type """ + """Data of an A record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -147,7 +143,7 @@ class ARecordData(RecordData): class CNAMERecordData(RecordData): - """ Record data for CNAME type """ + """Data of a CNAME record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -174,7 +170,7 @@ class CNAMERecordData(RecordData): class NSRecordData(RecordData): - """ Record data for NS type """ + """Data of an NS record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -201,7 +197,7 @@ class NSRecordData(RecordData): class AAAARecordData(RecordData): - """ Record data for AAAA type """ + """Data of an AAAA record""" def to_bytes(self, offset, composer): """ Convert to bytes @@ -227,7 +223,7 @@ class AAAARecordData(RecordData): class GenericRecordData(RecordData): - """ Generic Record Data (for other types) """ + """Data of a generic record""" def to_bytes(self, offset, composer): """ Convert to bytes -- cgit v1.2.3 From 61c1214dce59fd19e86bd9362207fb5e658d9a88 Mon Sep 17 00:00:00 2001 From: Camil Staps Date: Mon, 30 May 2016 15:21:17 +0200 Subject: Fixes for resolver & server; tests for resolver; cloogle zonefile --- project2/proj2_s4498062/cloogle.zone | 13 +++++ project2/proj2_s4498062/dns/cache.py | 7 +-- project2/proj2_s4498062/dns/resolver.py | 42 ++++----------- project2/proj2_s4498062/dns/resource.py | 4 +- project2/proj2_s4498062/dns/server.py | 18 +++++-- project2/proj2_s4498062/dns_server.py | 4 +- project2/proj2_s4498062/dns_tests.py | 68 ++++++++++++++++++++++--- project2/proj2_s4498062/named.root | 90 --------------------------------- 8 files changed, 105 insertions(+), 141 deletions(-) create mode 100644 project2/proj2_s4498062/cloogle.zone delete mode 100644 project2/proj2_s4498062/named.root (limited to 'project2/proj2_s4498062/dns/resource.py') 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/cache.py b/project2/proj2_s4498062/dns/cache.py index 9cde66f..a8b62be 100644 --- a/project2/proj2_s4498062/dns/cache.py +++ b/project2/proj2_s4498062/dns/cache.py @@ -53,7 +53,7 @@ class RecordCache(object): FILE = '.dns.cache' - def __init__(self): + def __init__(self, ttl): """ Initialize the RecordCache Args: @@ -61,15 +61,16 @@ class RecordCache(object): """ 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.clock()) + now = int(time.time()) for record in reversed(self.records): - if record.ttl + record.timestamp < now: + if min(self.ttl, record.ttl) + record.timestamp < now: self.records.remove(record) def lookup(self, dname, type_, class_): diff --git a/project2/proj2_s4498062/dns/resolver.py b/project2/proj2_s4498062/dns/resolver.py index 2c22adb..4c09681 100644 --- a/project2/proj2_s4498062/dns/resolver.py +++ b/project2/proj2_s4498062/dns/resolver.py @@ -48,7 +48,7 @@ class Resolver(object): self.timeout = timeout if self.caching: - self.cache = RecordCache() + self.cache = RecordCache(ttl) def do_query(self, hint, hostname, type_, class_=Class.IN, caching=True): """Do a query to a hint""" @@ -98,9 +98,6 @@ class Resolver(object): Returns: (str, [str], [str]): (hostname, aliaslist, ipaddrlist) """ - domains = hostname.split('.') - hints = self.ROOT_SERVERS - if self.caching: addrs = self.cache.lookup(hostname, Type.A, Class.IN) cnames = self.cache.lookup(hostname, Type.CNAME, Class.IN) @@ -109,66 +106,45 @@ class Resolver(object): [r.rdata.data for r in cnames], \ [r.rdata.data for r in addrs] for cname in cnames: - print 'trying', cname.rdata.data cname, aliases, addrs = self.gethostbyname(cname.rdata.data) if addrs != []: return str(cname), aliases, addrs - if domains == []: + if hostname == '': return hostname, [], [] - domain = domains.pop(-1) + hints = self.ROOT_SERVERS[:] aliases = [] while hints != []: - hints, info = self.do_query_to_multiple(hints, domain, Type.A) + 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=domain)] + 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=domain)] + if r.match(type_=Type.A, class_=Class.IN, name=hostname)] if ips != []: return hostname, aliases, ips - # Case 2: name servers for this domain + # Case 2: name servers auths = [ r.rdata.data for r in info - if r.match(type_=Type.NS, class_=Class.IN, name=domain)] + 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 - if domain != hostname: - domain = domains.pop(-1) + '.' + domain continue if auths != []: auths = [h for a in auths for h in self.gethostbyname(a)[2]] hints += auths - if domain != hostname: - domain = domains.pop(-1) + '.' + domain - continue - - # Case 3: name servers for the same domain - parent = '.'.join(domain.split('.')[1:]) - auths = [ - r.rdata.data for r in info - if r.match(type_=Type.NS, class_=Class.IN, name=parent)] - 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 r in auths for h in self.gethostbyname(r)[2]] - hints += auths continue - # Case 4: aliases + # Case 3: aliases for alias in aliases: _, extra_aliases, alias_addresses = self.gethostbyname(alias) if alias_addresses != []: diff --git a/project2/proj2_s4498062/dns/resource.py b/project2/proj2_s4498062/dns/resource.py index b1c8ae4..e552c22 100644 --- a/project2/proj2_s4498062/dns/resource.py +++ b/project2/proj2_s4498062/dns/resource.py @@ -14,7 +14,7 @@ from dns.types import Type class ResourceRecord(object): """ DNS resource record """ - def __init__(self, name, type_, class_, ttl, rdata, timestamp=time.time()): + def __init__(self, name, type_, class_, ttl, rdata, timestamp=None): """ Create a new resource record Args: @@ -28,7 +28,7 @@ class ResourceRecord(object): self.class_ = class_ self.ttl = ttl self.rdata = rdata - self.timestamp = timestamp + self.timestamp = int(timestamp or time.time()) def match(self, name=None, type_=None, class_=None, ttl=None): """Check if the record matches properties""" diff --git a/project2/proj2_s4498062/dns/server.py b/project2/proj2_s4498062/dns/server.py index f830651..10cad8b 100644 --- a/project2/proj2_s4498062/dns/server.py +++ b/project2/proj2_s4498062/dns/server.py @@ -12,7 +12,8 @@ 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, CNAMERecordData +from dns.resource import \ + ResourceRecord, ARecordData, NSRecordData, CNAMERecordData class RequestHandler(Thread): @@ -112,12 +113,21 @@ class Server(object): ttl, class_ = 3600000, Class.IN for match in re.finditer(rgx.ZONE_LINE_DOMAIN, zone, re.MULTILINE): match = match.groups() - name = match[0] - ttl = int(match[1] or match[4] or ttl) + 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] - record = ResourceRecord(name, type_, class_, ttl, data) + 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_server.py b/project2/proj2_s4498062/dns_server.py index 41d837a..3bdd04d 100755 --- a/project2/proj2_s4498062/dns_server.py +++ b/project2/proj2_s4498062/dns_server.py @@ -20,13 +20,13 @@ def main(): "-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, + "-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('named.root') + server.parse_zone_file('cloogle.zone') try: server.serve() diff --git a/project2/proj2_s4498062/dns_tests.py b/project2/proj2_s4498062/dns_tests.py index 26bc00b..4a054b6 100755 --- a/project2/proj2_s4498062/dns_tests.py +++ b/project2/proj2_s4498062/dns_tests.py @@ -1,16 +1,70 @@ #!/usr/bin/env python2 +""" Tests for the DNS resolver and server """ -""" Tests for your DNS resolver and server """ +import sys +import time +import unittest -portnr = 5353 -server = "localhost" +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): - pass + """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): - pass + """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): @@ -22,11 +76,11 @@ if __name__ == "__main__": 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) + 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 diff --git a/project2/proj2_s4498062/named.root b/project2/proj2_s4498062/named.root deleted file mode 100644 index 3c82146..0000000 --- a/project2/proj2_s4498062/named.root +++ /dev/null @@ -1,90 +0,0 @@ -; This file holds the information on root name servers needed to -; initialize cache of Internet domain name servers -; (e.g. reference this file in the "cache . " -; configuration file of BIND domain name servers). -; -; This file is made available by InterNIC -; under anonymous FTP as -; file /domain/named.cache -; on server FTP.INTERNIC.NET -; -OR- RS.INTERNIC.NET -; -; last update: March 23, 2016 -; related version of root zone: 2016032301 -; -; formerly NS.INTERNIC.NET -; -. 3600000 NS A.ROOT-SERVERS.NET. -A.ROOT-SERVERS.NET. 3600000 A 198.41.0.4 -A.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:ba3e::2:30 -; -; FORMERLY NS1.ISI.EDU -; -. 3600000 NS B.ROOT-SERVERS.NET. -B.ROOT-SERVERS.NET. 3600000 A 192.228.79.201 -B.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:84::b -; -; FORMERLY C.PSI.NET -; -. 3600000 NS C.ROOT-SERVERS.NET. -C.ROOT-SERVERS.NET. 3600000 A 192.33.4.12 -C.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2::c -; -; FORMERLY TERP.UMD.EDU -; -. 3600000 NS D.ROOT-SERVERS.NET. -D.ROOT-SERVERS.NET. 3600000 A 199.7.91.13 -D.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2d::d -; -; FORMERLY NS.NASA.GOV -; -. 3600000 NS E.ROOT-SERVERS.NET. -E.ROOT-SERVERS.NET. 3600000 A 192.203.230.10 -; -; FORMERLY NS.ISC.ORG -; -. 3600000 NS F.ROOT-SERVERS.NET. -F.ROOT-SERVERS.NET. 3600000 A 192.5.5.241 -F.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:2f::f -; -; FORMERLY NS.NIC.DDN.MIL -; -. 3600000 NS G.ROOT-SERVERS.NET. -G.ROOT-SERVERS.NET. 3600000 A 192.112.36.4 -; -; FORMERLY AOS.ARL.ARMY.MIL -; -. 3600000 NS H.ROOT-SERVERS.NET. -H.ROOT-SERVERS.NET. 3600000 A 198.97.190.53 -H.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:1::53 -; -; FORMERLY NIC.NORDU.NET -; -. 3600000 NS I.ROOT-SERVERS.NET. -I.ROOT-SERVERS.NET. 3600000 A 192.36.148.17 -I.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fe::53 -; -; OPERATED BY VERISIGN, INC. -; -. 3600000 NS J.ROOT-SERVERS.NET. -J.ROOT-SERVERS.NET. 3600000 A 192.58.128.30 -J.ROOT-SERVERS.NET. 3600000 AAAA 2001:503:c27::2:30 -; -; OPERATED BY RIPE NCC -; -. 3600000 NS K.ROOT-SERVERS.NET. -K.ROOT-SERVERS.NET. 3600000 A 193.0.14.129 -K.ROOT-SERVERS.NET. 3600000 AAAA 2001:7fd::1 -; -; OPERATED BY ICANN -; -. 3600000 NS L.ROOT-SERVERS.NET. -L.ROOT-SERVERS.NET. 3600000 A 199.7.83.42 -L.ROOT-SERVERS.NET. 3600000 AAAA 2001:500:9f::42 -; -; OPERATED BY WIDE -; -. 3600000 NS M.ROOT-SERVERS.NET. -M.ROOT-SERVERS.NET. 3600000 A 202.12.27.33 -M.ROOT-SERVERS.NET. 3600000 AAAA 2001:dc3::35 -; End of file -- cgit v1.2.3