net-dnsserver/dns/server.py

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