net-dnsserver/dns/resolver.py

187 lines
6.8 KiB
Python

#!/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
from dns.rcodes import RCode
from collections import defaultdict
SBELT = {
"A.ROOT-SERVERS.NET": (True, "198.41.0.4"),
"B.ROOT-SERVERS.NET": (True, "192.228.79.201"),
"C.ROOT-SERVERS.NET": (True, "192.33.4.12"),
"D.ROOT-SERVERS.NET": (True, "199.7.91.13"),
"E.ROOT-SERVERS.NET": (True, "192.203.230.10"),
"F.ROOT-SERVERS.NET": (True, "192.5.5.241"),
"G.ROOT-SERVERS.NET": (True, "192.112.36.4"),
"H.ROOT-SERVERS.NET": (True, "128.63.2.53"),
"I.ROOT-SERVERS.NET": (True, "192.36.148.17"),
"J.ROOT-SERVERS.NET": (True, "192.58.128.30"),
"K.ROOT-SERVERS.NET": (True, "193.0.14.129"),
"L.ROOT-SERVERS.NET": (True, "199.7.83.42"),
"M.ROOT-SERVERS.NET": (True, "202.12.27.33")
}
def dictappend(seq):
s = defaultdict(list)
for (k,v) in seq:
s[k].append(v)
return s
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
@classmethod
def query(cls, query, dest):
timeout = 2
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock.settimeout(timeout)
sock.sendto(query.to_bytes(), dest)
# Receive response
data = sock.recv(65530) # max MTU is not this value
response = dns.message.Message.from_bytes(data)
sock.close()
return response
@classmethod
def makepacket(cls, flags, qs=[], ans=[], ns=[], ar=[], ident=9001):
header = dns.message.Header(ident, 0, len(qs), len(ans), len(ns), len(ar))
for (k,v) in flags.items():
setattr(header, k, v)
return dns.message.Message(header, qs, ans, ns, ar)
def gethostbyname(self, hostname, additionals=[]):
""" 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)
"""
# Create and send query
# 1. See if the answer is in local information, and if so return
# it to the client.
pass
# 2. Find the best servers to ask.
SLIST = SBELT.values()
SNAME = hostname
aliases = [] + additionals
while True:
SLIST.sort(key=lambda a: a[0], reverse=True)
if len(SLIST) == 0:
return [[], [], []]
# state 1
(isLookedUp, ip) = SLIST.pop(0)
if not isLookedUp:
# XXX: this can get into an infinite loop
#print "looking up name server"
ns_hostname, ns_aliases, ns_ips = self.gethostbyname(ip)
if len(ns_ips):
ip = ns_ips[0]
SLIST = ns_ips[1:] + SLIST
else:
continue # didn't find the server
#print "asking " + ip
# state 2
# 3. Send them queries until one returns a response.
question = dns.message.Question(hostname, Type.A, Class.IN)
query = Resolver.makepacket({"qr": 0, "opcode": 0, "rd": 0}, [question])
# state 3
try:
response = Resolver.query(query, (ip, 53))
# temp
if response.header.rcode == RCode.ServFail:
raise Exception("server failure")
except Exception as e:
print e
# d. if the response shows a servers failure or other
# bizarre contents, delete the server from the SLIST and
# go back to step 3.
continue # edge 3 -> 1
# 4. Analyze the response, either:
# a. if the response answers the question or contains a name
# error, cache the data as well as returning it back to
# the client.
if response.header.rcode == dns.rcodes.RCode.NXDomain:
print "NXDOMAIN"
return ([],[],[]) # edge 3 -> 6
if response.header.aa:
# state 5
answers = dictappend([(ans.name, ans.rdata.data) for ans in response.answers if ans.type_ == Type.A])
cnames = dictappend([(ans.name, ans.rdata.data) for ans in (response.answers + response.additionals) if ans.type_ == Type.CNAME])
while True:
if SNAME in cnames:
# state 7
# c. if the response shows a CNAME and that is not the
# answer itself, cache the CNAME, change the SNAME to the
# canonical name in the CNAME RR and go to step 1.
if cnames[SNAME][0] in aliases:
#print "not following cname loop"
return SNAME, aliases, []
aliases.append(cnames[SNAME][0])
SNAME = cnames[SNAME][0]
#print "following CNAME", SNAME
if SNAME in answers:
return SNAME, aliases, answers[SNAME]
break # 5 -> 5'
# recursing
# 5 -> 0
return self.gethostbyname(SNAME, aliases)
# b. if the response contains a better delegation to other
# servers, cache the delegation information, and go to
# step 2.
if response.authorities:
# state 4
# cache
cands = [auth.rdata.data for auth in response.authorities]
d = {}
for add in response.additionals:
if add.type_ == Type.A:
d[add.name] = add.rdata.data
SLIST = [(True, d[x]) if x in d else (False, x) for x in cands]
#print "got authorities", SLIST
continue # 4 -> 1
# NOT REACHED
#print "NOT REACHED"