333 lines
12 KiB
Python
333 lines
12 KiB
Python
# -*- test-case-name: twisted.names.test.test_rootresolve -*-
|
|
# Copyright (c) Twisted Matrix Laboratories.
|
|
# See LICENSE for details.
|
|
|
|
"""
|
|
Resolver implementation for querying successive authoritative servers to
|
|
lookup a record, starting from the root nameservers.
|
|
|
|
@author: Jp Calderone
|
|
|
|
todo::
|
|
robustify it
|
|
documentation
|
|
"""
|
|
|
|
from twisted.python.failure import Failure
|
|
from twisted.internet import defer
|
|
from twisted.names import dns, common, error
|
|
|
|
|
|
|
|
class _DummyController:
|
|
"""
|
|
A do-nothing DNS controller. This is useful when all messages received
|
|
will be responses to previously issued queries. Anything else received
|
|
will be ignored.
|
|
"""
|
|
def messageReceived(self, *args):
|
|
pass
|
|
|
|
|
|
|
|
class Resolver(common.ResolverBase):
|
|
"""
|
|
L{Resolver} implements recursive lookup starting from a specified list of
|
|
root servers.
|
|
|
|
@ivar hints: See C{hints} parameter of L{__init__}
|
|
@ivar _maximumQueries: See C{maximumQueries} parameter of L{__init__}
|
|
@ivar _reactor: See C{reactor} parameter of L{__init__}
|
|
@ivar _resolverFactory: See C{resolverFactory} parameter of L{__init__}
|
|
"""
|
|
def __init__(self, hints, maximumQueries=10,
|
|
reactor=None, resolverFactory=None):
|
|
"""
|
|
@param hints: A L{list} of L{str} giving the dotted quad
|
|
representation of IP addresses of root servers at which to
|
|
begin resolving names.
|
|
@type hints: L{list} of L{str}
|
|
|
|
@param maximumQueries: An optional L{int} giving the maximum
|
|
number of queries which will be attempted to resolve a
|
|
single name.
|
|
@type maximumQueries: L{int}
|
|
|
|
@param reactor: An optional L{IReactorTime} and L{IReactorUDP}
|
|
provider to use to bind UDP ports and manage timeouts.
|
|
@type reactor: L{IReactorTime} and L{IReactorUDP} provider
|
|
|
|
@param resolverFactory: An optional callable which accepts C{reactor}
|
|
and C{servers} arguments and returns an instance that provides a
|
|
C{queryUDP} method. Defaults to L{twisted.names.client.Resolver}.
|
|
@type resolverFactory: callable
|
|
"""
|
|
common.ResolverBase.__init__(self)
|
|
self.hints = hints
|
|
self._maximumQueries = maximumQueries
|
|
self._reactor = reactor
|
|
if resolverFactory is None:
|
|
from twisted.names.client import Resolver as resolverFactory
|
|
self._resolverFactory = resolverFactory
|
|
|
|
|
|
def _roots(self):
|
|
"""
|
|
Return a list of two-tuples representing the addresses of the root
|
|
servers, as defined by C{self.hints}.
|
|
"""
|
|
return [(ip, dns.PORT) for ip in self.hints]
|
|
|
|
|
|
def _query(self, query, servers, timeout, filter):
|
|
"""
|
|
Issue one query and return a L{Deferred} which fires with its response.
|
|
|
|
@param query: The query to issue.
|
|
@type query: L{dns.Query}
|
|
|
|
@param servers: The servers which might have an answer for this
|
|
query.
|
|
@type servers: L{list} of L{tuple} of L{str} and L{int}
|
|
|
|
@param timeout: A timeout on how long to wait for the response.
|
|
@type timeout: L{tuple} of L{int}
|
|
|
|
@param filter: A flag indicating whether to filter the results. If
|
|
C{True}, the returned L{Deferred} will fire with a three-tuple of
|
|
lists of L{RRHeaders} (like the return value of the I{lookup*}
|
|
methods of L{IResolver}. IF C{False}, the result will be a
|
|
L{Message} instance.
|
|
@type filter: L{bool}
|
|
|
|
@return: A L{Deferred} which fires with the response or a timeout
|
|
error.
|
|
@rtype: L{Deferred}
|
|
"""
|
|
r = self._resolverFactory(servers=servers, reactor=self._reactor)
|
|
d = r.queryUDP([query], timeout)
|
|
if filter:
|
|
d.addCallback(r.filterAnswers)
|
|
return d
|
|
|
|
|
|
def _lookup(self, name, cls, type, timeout):
|
|
"""
|
|
Implement name lookup by recursively discovering the authoritative
|
|
server for the name and then asking it, starting at one of the servers
|
|
in C{self.hints}.
|
|
"""
|
|
if timeout is None:
|
|
# A series of timeouts for semi-exponential backoff, summing to an
|
|
# arbitrary total of 60 seconds.
|
|
timeout = (1, 3, 11, 45)
|
|
return self._discoverAuthority(
|
|
dns.Query(name, type, cls), self._roots(), timeout,
|
|
self._maximumQueries)
|
|
|
|
|
|
def _discoverAuthority(self, query, servers, timeout, queriesLeft):
|
|
"""
|
|
Issue a query to a server and follow a delegation if necessary.
|
|
|
|
@param query: The query to issue.
|
|
@type query: L{dns.Query}
|
|
|
|
@param servers: The servers which might have an answer for this
|
|
query.
|
|
@type servers: L{list} of L{tuple} of L{str} and L{int}
|
|
|
|
@param timeout: A C{tuple} of C{int} giving the timeout to use for this
|
|
query.
|
|
|
|
@param queriesLeft: A C{int} giving the number of queries which may
|
|
yet be attempted to answer this query before the attempt will be
|
|
abandoned.
|
|
|
|
@return: A L{Deferred} which fires with a three-tuple of lists of
|
|
L{RRHeaders} giving the response, or with a L{Failure} if there is
|
|
a timeout or response error.
|
|
"""
|
|
# Stop now if we've hit the query limit.
|
|
if queriesLeft <= 0:
|
|
return Failure(
|
|
error.ResolverError("Query limit reached without result"))
|
|
|
|
d = self._query(query, servers, timeout, False)
|
|
d.addCallback(
|
|
self._discoveredAuthority, query, timeout, queriesLeft - 1)
|
|
return d
|
|
|
|
|
|
def _discoveredAuthority(self, response, query, timeout, queriesLeft):
|
|
"""
|
|
Interpret the response to a query, checking for error codes and
|
|
following delegations if necessary.
|
|
|
|
@param response: The L{Message} received in response to issuing C{query}.
|
|
@type response: L{Message}
|
|
|
|
@param query: The L{dns.Query} which was issued.
|
|
@type query: L{dns.Query}.
|
|
|
|
@param timeout: The timeout to use if another query is indicated by
|
|
this response.
|
|
@type timeout: L{tuple} of L{int}
|
|
|
|
@param queriesLeft: A C{int} giving the number of queries which may
|
|
yet be attempted to answer this query before the attempt will be
|
|
abandoned.
|
|
|
|
@return: A L{Failure} indicating a response error, a three-tuple of
|
|
lists of L{RRHeaders} giving the response to C{query} or a
|
|
L{Deferred} which will fire with one of those.
|
|
"""
|
|
if response.rCode != dns.OK:
|
|
return Failure(self.exceptionForCode(response.rCode)(response))
|
|
|
|
# Turn the answers into a structure that's a little easier to work with.
|
|
records = {}
|
|
for answer in response.answers:
|
|
records.setdefault(answer.name, []).append(answer)
|
|
|
|
def findAnswerOrCName(name, type, cls):
|
|
cname = None
|
|
for record in records.get(name, []):
|
|
if record.cls == cls:
|
|
if record.type == type:
|
|
return record
|
|
elif record.type == dns.CNAME:
|
|
cname = record
|
|
# If there were any CNAME records, return the last one. There's
|
|
# only supposed to be zero or one, though.
|
|
return cname
|
|
|
|
seen = set()
|
|
name = query.name
|
|
record = None
|
|
while True:
|
|
seen.add(name)
|
|
previous = record
|
|
record = findAnswerOrCName(name, query.type, query.cls)
|
|
if record is None:
|
|
if name == query.name:
|
|
# If there's no answer for the original name, then this may
|
|
# be a delegation. Code below handles it.
|
|
break
|
|
else:
|
|
# Try to resolve the CNAME with another query.
|
|
d = self._discoverAuthority(
|
|
dns.Query(str(name), query.type, query.cls),
|
|
self._roots(), timeout, queriesLeft)
|
|
# We also want to include the CNAME in the ultimate result,
|
|
# otherwise this will be pretty confusing.
|
|
def cbResolved(results):
|
|
answers, authority, additional = results
|
|
answers.insert(0, previous)
|
|
return (answers, authority, additional)
|
|
d.addCallback(cbResolved)
|
|
return d
|
|
elif record.type == query.type:
|
|
return (
|
|
response.answers,
|
|
response.authority,
|
|
response.additional)
|
|
else:
|
|
# It's a CNAME record. Try to resolve it from the records
|
|
# in this response with another iteration around the loop.
|
|
if record.payload.name in seen:
|
|
raise error.ResolverError("Cycle in CNAME processing")
|
|
name = record.payload.name
|
|
|
|
|
|
# Build a map to use to convert NS names into IP addresses.
|
|
addresses = {}
|
|
for rr in response.additional:
|
|
if rr.type == dns.A:
|
|
addresses[rr.name.name] = rr.payload.dottedQuad()
|
|
|
|
hints = []
|
|
traps = []
|
|
for rr in response.authority:
|
|
if rr.type == dns.NS:
|
|
ns = rr.payload.name.name
|
|
if ns in addresses:
|
|
hints.append((addresses[ns], dns.PORT))
|
|
else:
|
|
traps.append(ns)
|
|
if hints:
|
|
return self._discoverAuthority(
|
|
query, hints, timeout, queriesLeft)
|
|
elif traps:
|
|
d = self.lookupAddress(traps[0], timeout)
|
|
def getOneAddress(results):
|
|
answers, authority, additional = results
|
|
return answers[0].payload.dottedQuad()
|
|
d.addCallback(getOneAddress)
|
|
d.addCallback(
|
|
lambda hint: self._discoverAuthority(
|
|
query, [(hint, dns.PORT)], timeout, queriesLeft - 1))
|
|
return d
|
|
else:
|
|
return Failure(error.ResolverError(
|
|
"Stuck at response without answers or delegation"))
|
|
|
|
|
|
|
|
def makePlaceholder(deferred, name):
|
|
def placeholder(*args, **kw):
|
|
deferred.addCallback(lambda r: getattr(r, name)(*args, **kw))
|
|
return deferred
|
|
return placeholder
|
|
|
|
class DeferredResolver:
|
|
def __init__(self, resolverDeferred):
|
|
self.waiting = []
|
|
resolverDeferred.addCallback(self.gotRealResolver)
|
|
|
|
def gotRealResolver(self, resolver):
|
|
w = self.waiting
|
|
self.__dict__ = resolver.__dict__
|
|
self.__class__ = resolver.__class__
|
|
for d in w:
|
|
d.callback(resolver)
|
|
|
|
def __getattr__(self, name):
|
|
if name.startswith('lookup') or name in ('getHostByName', 'query'):
|
|
self.waiting.append(defer.Deferred())
|
|
return makePlaceholder(self.waiting[-1], name)
|
|
raise AttributeError(name)
|
|
|
|
|
|
|
|
def bootstrap(resolver, resolverFactory=None):
|
|
"""
|
|
Lookup the root nameserver addresses using the given resolver
|
|
|
|
Return a Resolver which will eventually become a C{root.Resolver}
|
|
instance that has references to all the root servers that we were able
|
|
to look up.
|
|
|
|
@param resolver: The resolver instance which will be used to
|
|
lookup the root nameserver addresses.
|
|
@type resolver: L{twisted.internet.interfaces.IResolverSimple}
|
|
|
|
@param resolverFactory: An optional callable which returns a
|
|
resolver instance. It will passed as the C{resolverFactory}
|
|
argument to L{Resolver.__init__}.
|
|
@type resolverFactory: callable
|
|
|
|
@return: A L{DeferredResolver} which will be dynamically replaced
|
|
with L{Resolver} when the root nameservers have been looked up.
|
|
"""
|
|
domains = [chr(ord('a') + i) for i in range(13)]
|
|
L = [resolver.getHostByName('%s.root-servers.net' % d) for d in domains]
|
|
d = defer.DeferredList(L)
|
|
|
|
def buildResolver(res):
|
|
return Resolver(
|
|
hints=[e[1] for e in res if e[0]],
|
|
resolverFactory=resolverFactory)
|
|
d.addCallback(buildResolver)
|
|
|
|
return DeferredResolver(d)
|