import initial framework
commit
f05c34a352
@ -0,0 +1 @@
|
||||
*.pyc
|
@ -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.
|
||||
|
@ -0,0 +1 @@
|
||||
#!/usr/bin/env python2
|
@ -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
|
@ -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]
|
@ -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
|
@ -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
|
@ -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]
|
@ -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
|
@ -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)
|
@ -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
|
@ -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]
|
@ -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
|
@ -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)
|
@ -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()
|
@ -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()
|
Loading…
Reference in New Issue