import initial framework

master
Yorick van Pelt 2016-05-27 19:38:58 +02:00
commit f05c34a352
16 changed files with 1254 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
*.pyc

45
README.md Normal file
View File

@ -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.

1
dns/__init__.py Normal file
View File

@ -0,0 +1 @@
#!/usr/bin/env python2

89
dns/cache.py Normal file
View File

@ -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

55
dns/classes.py Normal file
View File

@ -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]

127
dns/domainname.py Normal file
View File

@ -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

278
dns/message.py Normal file
View File

@ -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

81
dns/rcodes.py Normal file
View File

@ -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]

72
dns/resolver.py Normal file
View File

@ -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

241
dns/resource.py Normal file
View File

@ -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)

53
dns/server.py Normal file
View File

@ -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

67
dns/types.py Normal file
View File

@ -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]

54
dns/zone.py Normal file
View File

@ -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

28
dns_client.py Normal file
View File

@ -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)

28
dns_server.py Normal file
View File

@ -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()

34
dns_tests.py Normal file
View File

@ -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()