232 lines
9.9 KiB
Python
232 lines
9.9 KiB
Python
#!/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
|
|
|
|
import socket
|
|
|
|
import dns.message
|
|
import dns.zone
|
|
|
|
from dns.classes import Class
|
|
from dns.types import Type
|
|
|
|
from resolver import Resolver
|
|
|
|
class RequestHandler(Thread):
|
|
""" A handler for requests to the DNS server """
|
|
|
|
def __init__(self, addr, packet, parent):
|
|
""" Initialize the handler thread """
|
|
super(RequestHandler, self).__init__()
|
|
self.daemon = True
|
|
self.packet = packet
|
|
self.addr = addr
|
|
self.parent = parent
|
|
|
|
def generate_response(self, QNAME, QTYPE, packet, rec=False, answers=None,auth=None,additional=None, response_flags = None):
|
|
"""
|
|
Generate the response records and flags for a query.
|
|
This follows the algorithm as outlined in the RFC, and copied in the comments below.
|
|
"""
|
|
if answers is None: answers = []
|
|
if auth is None: auth = []
|
|
if additional is None: additional = []
|
|
|
|
print "generating result for ", QNAME, QTYPE
|
|
|
|
# The actual algorithm used by the name server will depend on the local OS
|
|
# and data structures used to store RRs. The following algorithm assumes
|
|
# that the RRs are organized in several tree structures, one for each
|
|
# zone, and another for the cache:
|
|
if response_flags is None: response_flags = {}
|
|
|
|
# 1. Set or clear the value of recursion available in the response
|
|
# depending on whether the name server is willing to provide
|
|
# recursive service. If recursive service is available and
|
|
# requested via the RD bit in the query, go to step 5,
|
|
# otherwise step 2.
|
|
response_flags['ra'] = 1
|
|
|
|
done = False
|
|
|
|
if not packet.header.rd:
|
|
|
|
# 2. Search the available zones for the zone which is the nearest
|
|
# ancestor to QNAME. If such a zone is found, go to step 3,
|
|
# otherwise step 4.
|
|
|
|
zone = self.parent.catalog.find_nearest(QNAME)
|
|
|
|
if zone:
|
|
(matchtype, lookedup) = zone.lookup(QNAME)
|
|
|
|
|
|
# 3. Start matching down, label by label, in the zone. The
|
|
# matching process can terminate several ways:
|
|
|
|
if matchtype == 'full':
|
|
# a. If the whole of QNAME is matched, we have found the
|
|
# node.
|
|
|
|
# If the data at the node is a CNAME, and QTYPE doesn't
|
|
# match CNAME, copy the CNAME RR into the answer section
|
|
# of the response, change QNAME to the canonical name in
|
|
# the CNAME RR, and go back to step 1.
|
|
cnames = [n for n in lookedup if n.type_ == Type.CNAME]
|
|
response_flags['aa'] = True
|
|
if cnames:
|
|
#print "following cname", cnames[0].rdata.data
|
|
answers.append(cnames[0])
|
|
QNAME = cnames[0].rdata.data
|
|
return self.generate_response(cnames[0].rdata.data, QTYPE, packet, True, answers, auth, additional, response_flags)
|
|
|
|
else:
|
|
# Otherwise, copy all RRs which match QTYPE into the
|
|
# answer section and go to step 6.
|
|
answers += [n for n in lookedup if n.type_ == QTYPE]
|
|
response_flags['aa'] = True
|
|
done = True
|
|
|
|
elif matchtype == 'auth':
|
|
# b. If a match would take us out of the authoritative data,
|
|
# we have a referral. This happens when we encounter a
|
|
# node with NS RRs marking cuts along the bottom of a
|
|
# zone.
|
|
|
|
# Copy the NS RRs for the subzone into the authority
|
|
# section of the reply. Put whatever addresses are
|
|
# available into the additional section, using glue RRs
|
|
# if the addresses are not available from authoritative
|
|
# data or the cache. Go to step 4.
|
|
nameservs = [n for n in lookedup if n.type_ == Type.NS]
|
|
response_flags['aa'] = True
|
|
auth += nameservs
|
|
for serv in nameservs:
|
|
# try to find them in the zone
|
|
(match2, look2) = zone.lookup(serv.rdata.data)
|
|
if match2 == 'full':
|
|
additional += look2
|
|
# try to find them in the cache
|
|
else:
|
|
look2 = self.parent.resolver.cache.lookup(serv.rdata.data, Types.A, Class.IN)
|
|
additional += look2
|
|
|
|
|
|
# c. If at some label, a match is impossible (i.e., the
|
|
# corresponding label does not exist), look to see if a
|
|
# the "*" label exists.
|
|
|
|
elif matchtype == 'no':
|
|
# If the "*" label does not exist, check whether the name
|
|
# we are looking for is the original QNAME in the query
|
|
# or a name we have followed due to a CNAME. If the name
|
|
# is original, set an authoritative name error in the
|
|
# response and exit. Otherwise just exit.
|
|
if not rec:
|
|
response_flags['rcode'] = dns.rcodes.RCode.NXDomain
|
|
done = True
|
|
|
|
elif matchtype == 'star':
|
|
# If the "*" label does exist, match RRs at that node
|
|
# against QTYPE. If any match, copy them into the answer
|
|
# section, but set the owner of the RR to be QNAME, and
|
|
# not the node with the "*" label. Go to step 6.
|
|
new = [n.copy() for n in lookedup if n.type_ == QTYPE]
|
|
for x in new:
|
|
x.name = QNAME
|
|
answers += new
|
|
done = True
|
|
if not done:
|
|
# 4. Start matching down in the cache. If QNAME is found in the
|
|
# cache, copy all RRs attached to it that match QTYPE into the
|
|
# answer section. If there was no delegation from
|
|
# authoritative data, look for the best one from the cache, and
|
|
# put it in the authority section. Go to step 6.
|
|
lookedup = self.parent.resolver.cache.lookup(QNAME, QTYPE, Class.IN)
|
|
answers += lookedup
|
|
if zone and matchtype == 'auth' and len(auth) == 0:
|
|
(addit, ns) = self.parent.resolver.best_ns_from_cache(QNAME)
|
|
auth += ns
|
|
additional += addit
|
|
else:
|
|
# 5. Using the local resolver or a copy of its algorithm (see
|
|
# resolver section of this memo) to answer the query. Store
|
|
# the results, including any intermediate CNAMEs, in the answer
|
|
# section of the response.
|
|
|
|
hostname, alias, ips = self.parent.resolver.lookup(QTYPE, QNAME)
|
|
answers += alias + ips
|
|
|
|
# 6. Using local data only, attempt to add other RRs which may be
|
|
# useful to the additional section of the query. Exit.
|
|
pass # This happens in step 4 and 3b
|
|
|
|
response_flags['rq'] = 1
|
|
response_flags['opcode'] = 0
|
|
return response_flags,answers,auth,additional
|
|
|
|
|
|
def run(self):
|
|
""" Run the handler thread """
|
|
# parse the packet
|
|
packet = dns.message.Message.from_bytes(self.packet)
|
|
assert len(packet.questions) == 1
|
|
|
|
q = packet.questions[0]
|
|
|
|
answers = []
|
|
auth = []
|
|
additional = []
|
|
|
|
response_flags,answers,auth,additional = self.generate_response(q.qname, q.qtype, packet)
|
|
|
|
print "sending", answers, auth, additional
|
|
message = dns.message.makepacket(response_flags, packet.questions, answers, auth, additional, ident=packet.header.ident)
|
|
|
|
self.parent.sock.sendto(message.to_bytes(), self.addr)
|
|
|
|
|
|
|
|
class Server(object):
|
|
""" A recursive DNS server """
|
|
|
|
def __init__(self, port, caching, ttl):
|
|
""" Initialize the server
|
|
|
|
Args:
|
|
port (int): port that server is listening on
|
|
caching (bool): server uses resolver with caching if true
|
|
ttl (int): ttl for records (if > 0) of cache
|
|
"""
|
|
self.caching = caching
|
|
self.ttl = ttl
|
|
self.port = port
|
|
self.done = False
|
|
self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
|
self.resolver = Resolver(caching, ttl)
|
|
self.resolver.cache.read_cache_file()
|
|
self.catalog = dns.zone.Catalog()
|
|
|
|
def add_zone(self, name, file):
|
|
z = dns.zone.Zone()
|
|
z.read_master_file(file)
|
|
self.catalog.add_zone(name, z)
|
|
|
|
def serve(self):
|
|
""" Start serving request """
|
|
self.sock.bind(('', self.port))
|
|
while not self.done:
|
|
(packet, addr) = self.sock.recvfrom(4096)
|
|
RequestHandler(addr, packet, self).start()
|
|
def shutdown(self):
|
|
""" Shutdown the server """
|
|
self.done = True
|
|
self.sock.close()
|
|
self.resolver.cache.write_cache_file()
|