| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734 |
- # Copyright (c) Twisted Matrix Laboratories.
- # See LICENSE for details.
- """
- Test cases for Twisted.names' root resolver.
- """
- from zope.interface import implementer
- from zope.interface.verify import verifyClass
- from twisted.python.log import msg
- from twisted.trial import util
- from twisted.trial.unittest import SynchronousTestCase, TestCase
- from twisted.internet.defer import Deferred, succeed, gatherResults, TimeoutError
- from twisted.internet.interfaces import IResolverSimple
- from twisted.names import client, root
- from twisted.names.root import Resolver
- from twisted.names.dns import (
- IN, HS, A, NS, CNAME, OK, ENAME, Record_CNAME,
- Name, Query, Message, RRHeader, Record_A, Record_NS)
- from twisted.names.error import DNSNameError, ResolverError
- from twisted.names.test.test_util import MemoryReactor
- def getOnePayload(results):
- """
- From the result of a L{Deferred} returned by L{IResolver.lookupAddress},
- return the payload of the first record in the answer section.
- """
- ans, auth, add = results
- return ans[0].payload
- def getOneAddress(results):
- """
- From the result of a L{Deferred} returned by L{IResolver.lookupAddress},
- return the first IPv4 address from the answer section.
- """
- return getOnePayload(results).dottedQuad()
- class RootResolverTests(TestCase):
- """
- Tests for L{twisted.names.root.Resolver}.
- """
- def _queryTest(self, filter):
- """
- Invoke L{Resolver._query} and verify that it sends the correct DNS
- query. Deliver a canned response to the query and return whatever the
- L{Deferred} returned by L{Resolver._query} fires with.
- @param filter: The value to pass for the C{filter} parameter to
- L{Resolver._query}.
- """
- reactor = MemoryReactor()
- resolver = Resolver([], reactor=reactor)
- d = resolver._query(
- Query(b'foo.example.com', A, IN), [('1.1.2.3', 1053)], (30,),
- filter)
- # A UDP port should have been started.
- portNumber, transport = reactor.udpPorts.popitem()
- # And a DNS packet sent.
- [(packet, address)] = transport._sentPackets
- message = Message()
- message.fromStr(packet)
- # It should be a query with the parameters used above.
- self.assertEqual(message.queries, [Query(b'foo.example.com', A, IN)])
- self.assertEqual(message.answers, [])
- self.assertEqual(message.authority, [])
- self.assertEqual(message.additional, [])
- response = []
- d.addCallback(response.append)
- self.assertEqual(response, [])
- # Once a reply is received, the Deferred should fire.
- del message.queries[:]
- message.answer = 1
- message.answers.append(RRHeader(
- b'foo.example.com', payload=Record_A('5.8.13.21')))
- transport._protocol.datagramReceived(
- message.toStr(), ('1.1.2.3', 1053))
- return response[0]
- def test_filteredQuery(self):
- """
- L{Resolver._query} accepts a L{Query} instance and an address, issues
- the query, and returns a L{Deferred} which fires with the response to
- the query. If a true value is passed for the C{filter} parameter, the
- result is a three-tuple of lists of records.
- """
- answer, authority, additional = self._queryTest(True)
- self.assertEqual(
- answer,
- [RRHeader(b'foo.example.com', payload=Record_A('5.8.13.21', ttl=0))])
- self.assertEqual(authority, [])
- self.assertEqual(additional, [])
- def test_unfilteredQuery(self):
- """
- Similar to L{test_filteredQuery}, but for the case where a false value
- is passed for the C{filter} parameter. In this case, the result is a
- L{Message} instance.
- """
- message = self._queryTest(False)
- self.assertIsInstance(message, Message)
- self.assertEqual(message.queries, [])
- self.assertEqual(
- message.answers,
- [RRHeader(b'foo.example.com', payload=Record_A('5.8.13.21', ttl=0))])
- self.assertEqual(message.authority, [])
- self.assertEqual(message.additional, [])
- def _respond(self, answers=[], authority=[], additional=[], rCode=OK):
- """
- Create a L{Message} suitable for use as a response to a query.
- @param answers: A C{list} of two-tuples giving data for the answers
- section of the message. The first element of each tuple is a name
- for the L{RRHeader}. The second element is the payload.
- @param authority: A C{list} like C{answers}, but for the authority
- section of the response.
- @param additional: A C{list} like C{answers}, but for the
- additional section of the response.
- @param rCode: The response code the message will be created with.
- @return: A new L{Message} initialized with the given values.
- """
- response = Message(rCode=rCode)
- for (section, data) in [(response.answers, answers),
- (response.authority, authority),
- (response.additional, additional)]:
- section.extend([
- RRHeader(name, record.TYPE, getattr(record, 'CLASS', IN),
- payload=record)
- for (name, record) in data])
- return response
- def _getResolver(self, serverResponses, maximumQueries=10):
- """
- Create and return a new L{root.Resolver} modified to resolve queries
- against the record data represented by C{servers}.
- @param serverResponses: A mapping from dns server addresses to
- mappings. The inner mappings are from query two-tuples (name,
- type) to dictionaries suitable for use as **arguments to
- L{_respond}. See that method for details.
- """
- roots = ['1.1.2.3']
- resolver = Resolver(roots, maximumQueries)
- def query(query, serverAddresses, timeout, filter):
- msg("Query for QNAME %s at %r" % (query.name, serverAddresses))
- for addr in serverAddresses:
- try:
- server = serverResponses[addr]
- except KeyError:
- continue
- records = server[query.name, query.type]
- return succeed(self._respond(**records))
- resolver._query = query
- return resolver
- def test_lookupAddress(self):
- """
- L{root.Resolver.lookupAddress} looks up the I{A} records for the
- specified hostname by first querying one of the root servers the
- resolver was created with and then following the authority delegations
- until a result is received.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'foo.example.com', A): {
- 'authority': [(b'foo.example.com', Record_NS(b'ns1.example.com'))],
- 'additional': [(b'ns1.example.com', Record_A('34.55.89.144'))],
- },
- },
- ('34.55.89.144', 53): {
- (b'foo.example.com', A): {
- 'answers': [(b'foo.example.com', Record_A('10.0.0.1'))],
- }
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'foo.example.com')
- d.addCallback(getOneAddress)
- d.addCallback(self.assertEqual, '10.0.0.1')
- return d
- def test_lookupChecksClass(self):
- """
- If a response includes a record with a class different from the one
- in the query, it is ignored and lookup continues until a record with
- the right class is found.
- """
- badClass = Record_A('10.0.0.1')
- badClass.CLASS = HS
- servers = {
- ('1.1.2.3', 53): {
- (b'foo.example.com', A): {
- 'answers': [(b'foo.example.com', badClass)],
- 'authority': [(b'foo.example.com', Record_NS(b'ns1.example.com'))],
- 'additional': [(b'ns1.example.com', Record_A('10.0.0.2'))],
- },
- },
- ('10.0.0.2', 53): {
- (b'foo.example.com', A): {
- 'answers': [(b'foo.example.com', Record_A('10.0.0.3'))],
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'foo.example.com')
- d.addCallback(getOnePayload)
- d.addCallback(self.assertEqual, Record_A('10.0.0.3'))
- return d
- def test_missingGlue(self):
- """
- If an intermediate response includes no glue records for the
- authorities, separate queries are made to find those addresses.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'foo.example.com', A): {
- 'authority': [(b'foo.example.com', Record_NS(b'ns1.example.org'))],
- # Conspicuous lack of an additional section naming ns1.example.com
- },
- (b'ns1.example.org', A): {
- 'answers': [(b'ns1.example.org', Record_A('10.0.0.1'))],
- },
- },
- ('10.0.0.1', 53): {
- (b'foo.example.com', A): {
- 'answers': [(b'foo.example.com', Record_A('10.0.0.2'))],
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'foo.example.com')
- d.addCallback(getOneAddress)
- d.addCallback(self.assertEqual, '10.0.0.2')
- return d
- def test_missingName(self):
- """
- If a name is missing, L{Resolver.lookupAddress} returns a L{Deferred}
- which fails with L{DNSNameError}.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'foo.example.com', A): {
- 'rCode': ENAME,
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'foo.example.com')
- return self.assertFailure(d, DNSNameError)
- def test_answerless(self):
- """
- If a query is responded to with no answers or nameserver records, the
- L{Deferred} returned by L{Resolver.lookupAddress} fires with
- L{ResolverError}.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'example.com')
- return self.assertFailure(d, ResolverError)
- def test_delegationLookupError(self):
- """
- If there is an error resolving the nameserver in a delegation response,
- the L{Deferred} returned by L{Resolver.lookupAddress} fires with that
- error.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- 'authority': [(b'example.com', Record_NS(b'ns1.example.com'))],
- },
- (b'ns1.example.com', A): {
- 'rCode': ENAME,
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'example.com')
- return self.assertFailure(d, DNSNameError)
- def test_delegationLookupEmpty(self):
- """
- If there are no records in the response to a lookup of a delegation
- nameserver, the L{Deferred} returned by L{Resolver.lookupAddress} fires
- with L{ResolverError}.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- 'authority': [(b'example.com', Record_NS(b'ns1.example.com'))],
- },
- (b'ns1.example.com', A): {
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'example.com')
- return self.assertFailure(d, ResolverError)
- def test_lookupNameservers(self):
- """
- L{Resolver.lookupNameservers} is like L{Resolver.lookupAddress}, except
- it queries for I{NS} records instead of I{A} records.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- 'rCode': ENAME,
- },
- (b'example.com', NS): {
- 'answers': [(b'example.com', Record_NS(b'ns1.example.com'))],
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupNameservers(b'example.com')
- def getOneName(results):
- ans, auth, add = results
- return ans[0].payload.name
- d.addCallback(getOneName)
- d.addCallback(self.assertEqual, Name(b'ns1.example.com'))
- return d
- def test_returnCanonicalName(self):
- """
- If a I{CNAME} record is encountered as the answer to a query for
- another record type, that record is returned as the answer.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- 'answers': [(b'example.com', Record_CNAME(b'example.net')),
- (b'example.net', Record_A('10.0.0.7'))],
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'example.com')
- d.addCallback(lambda results: results[0]) # Get the answer section
- d.addCallback(
- self.assertEqual,
- [RRHeader(b'example.com', CNAME, payload=Record_CNAME(b'example.net')),
- RRHeader(b'example.net', A, payload=Record_A('10.0.0.7'))])
- return d
- def test_followCanonicalName(self):
- """
- If no record of the requested type is included in a response, but a
- I{CNAME} record for the query name is included, queries are made to
- resolve the value of the I{CNAME}.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- 'answers': [(b'example.com', Record_CNAME(b'example.net'))],
- },
- (b'example.net', A): {
- 'answers': [(b'example.net', Record_A('10.0.0.5'))],
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'example.com')
- d.addCallback(lambda results: results[0]) # Get the answer section
- d.addCallback(
- self.assertEqual,
- [RRHeader(b'example.com', CNAME, payload=Record_CNAME(b'example.net')),
- RRHeader(b'example.net', A, payload=Record_A('10.0.0.5'))])
- return d
- def test_detectCanonicalNameLoop(self):
- """
- If there is a cycle between I{CNAME} records in a response, this is
- detected and the L{Deferred} returned by the lookup method fails
- with L{ResolverError}.
- """
- servers = {
- ('1.1.2.3', 53): {
- (b'example.com', A): {
- 'answers': [(b'example.com', Record_CNAME(b'example.net')),
- (b'example.net', Record_CNAME(b'example.com'))],
- },
- },
- }
- resolver = self._getResolver(servers)
- d = resolver.lookupAddress(b'example.com')
- return self.assertFailure(d, ResolverError)
- def test_boundedQueries(self):
- """
- L{Resolver.lookupAddress} won't issue more queries following
- delegations than the limit passed to its initializer.
- """
- servers = {
- ('1.1.2.3', 53): {
- # First query - force it to start over with a name lookup of
- # ns1.example.com
- (b'example.com', A): {
- 'authority': [(b'example.com', Record_NS(b'ns1.example.com'))],
- },
- # Second query - let it resume the original lookup with the
- # address of the nameserver handling the delegation.
- (b'ns1.example.com', A): {
- 'answers': [(b'ns1.example.com', Record_A('10.0.0.2'))],
- },
- },
- ('10.0.0.2', 53): {
- # Third query - let it jump straight to asking the
- # delegation server by including its address here (different
- # case from the first query).
- (b'example.com', A): {
- 'authority': [(b'example.com', Record_NS(b'ns2.example.com'))],
- 'additional': [(b'ns2.example.com', Record_A('10.0.0.3'))],
- },
- },
- ('10.0.0.3', 53): {
- # Fourth query - give it the answer, we're done.
- (b'example.com', A): {
- 'answers': [(b'example.com', Record_A('10.0.0.4'))],
- },
- },
- }
- # Make two resolvers. One which is allowed to make 3 queries
- # maximum, and so will fail, and on which may make 4, and so should
- # succeed.
- failer = self._getResolver(servers, 3)
- failD = self.assertFailure(
- failer.lookupAddress(b'example.com'), ResolverError)
- succeeder = self._getResolver(servers, 4)
- succeedD = succeeder.lookupAddress(b'example.com')
- succeedD.addCallback(getOnePayload)
- succeedD.addCallback(self.assertEqual, Record_A('10.0.0.4'))
- return gatherResults([failD, succeedD])
- class ResolverFactoryArguments(Exception):
- """
- Raised by L{raisingResolverFactory} with the *args and **kwargs passed to
- that function.
- """
- def __init__(self, args, kwargs):
- """
- Store the supplied args and kwargs as attributes.
- @param args: Positional arguments.
- @param kwargs: Keyword arguments.
- """
- self.args = args
- self.kwargs = kwargs
- def raisingResolverFactory(*args, **kwargs):
- """
- Raise a L{ResolverFactoryArguments} exception containing the
- positional and keyword arguments passed to resolverFactory.
- @param args: A L{list} of all the positional arguments supplied by
- the caller.
- @param kwargs: A L{list} of all the keyword arguments supplied by
- the caller.
- """
- raise ResolverFactoryArguments(args, kwargs)
- class RootResolverResolverFactoryTests(TestCase):
- """
- Tests for L{root.Resolver._resolverFactory}.
- """
- def test_resolverFactoryArgumentPresent(self):
- """
- L{root.Resolver.__init__} accepts a C{resolverFactory}
- argument and assigns it to C{self._resolverFactory}.
- """
- r = Resolver(hints=[None], resolverFactory=raisingResolverFactory)
- self.assertIs(r._resolverFactory, raisingResolverFactory)
- def test_resolverFactoryArgumentAbsent(self):
- """
- L{root.Resolver.__init__} sets L{client.Resolver} as the
- C{_resolverFactory} if a C{resolverFactory} argument is not
- supplied.
- """
- r = Resolver(hints=[None])
- self.assertIs(r._resolverFactory, client.Resolver)
- def test_resolverFactoryOnlyExpectedArguments(self):
- """
- L{root.Resolver._resolverFactory} is supplied with C{reactor} and
- C{servers} keyword arguments.
- """
- dummyReactor = object()
- r = Resolver(hints=['192.0.2.101'],
- resolverFactory=raisingResolverFactory,
- reactor=dummyReactor)
- e = self.assertRaises(ResolverFactoryArguments,
- r.lookupAddress, 'example.com')
- self.assertEqual(
- ((), {'reactor': dummyReactor, 'servers': [('192.0.2.101', 53)]}),
- (e.args, e.kwargs)
- )
- ROOT_SERVERS = [
- 'a.root-servers.net',
- 'b.root-servers.net',
- 'c.root-servers.net',
- 'd.root-servers.net',
- 'e.root-servers.net',
- 'f.root-servers.net',
- 'g.root-servers.net',
- 'h.root-servers.net',
- 'i.root-servers.net',
- 'j.root-servers.net',
- 'k.root-servers.net',
- 'l.root-servers.net',
- 'm.root-servers.net']
- @implementer(IResolverSimple)
- class StubResolver(object):
- """
- An L{IResolverSimple} implementer which traces all getHostByName
- calls and their deferred results. The deferred results can be
- accessed and fired synchronously.
- """
- def __init__(self):
- """
- @type calls: L{list} of L{tuple} containing C{args} and
- C{kwargs} supplied to C{getHostByName} calls.
- @type pendingResults: L{list} of L{Deferred} returned by
- C{getHostByName}.
- """
- self.calls = []
- self.pendingResults = []
- def getHostByName(self, *args, **kwargs):
- """
- A fake implementation of L{IResolverSimple.getHostByName}
- @param args: A L{list} of all the positional arguments supplied by
- the caller.
- @param kwargs: A L{list} of all the keyword arguments supplied by
- the caller.
- @return: A L{Deferred} which may be fired later from the test
- fixture.
- """
- self.calls.append((args, kwargs))
- d = Deferred()
- self.pendingResults.append(d)
- return d
- verifyClass(IResolverSimple, StubResolver)
- class BootstrapTests(SynchronousTestCase):
- """
- Tests for L{root.bootstrap}
- """
- def test_returnsDeferredResolver(self):
- """
- L{root.bootstrap} returns an object which is initially a
- L{root.DeferredResolver}.
- """
- deferredResolver = root.bootstrap(StubResolver())
- self.assertIsInstance(deferredResolver, root.DeferredResolver)
- def test_resolves13RootServers(self):
- """
- The L{IResolverSimple} supplied to L{root.bootstrap} is used to lookup
- the IP addresses of the 13 root name servers.
- """
- stubResolver = StubResolver()
- root.bootstrap(stubResolver)
- self.assertEqual(
- stubResolver.calls,
- [((s,), {}) for s in ROOT_SERVERS])
- def test_becomesResolver(self):
- """
- The L{root.DeferredResolver} initially returned by L{root.bootstrap}
- becomes a L{root.Resolver} when the supplied resolver has successfully
- looked up all root hints.
- """
- stubResolver = StubResolver()
- deferredResolver = root.bootstrap(stubResolver)
- for d in stubResolver.pendingResults:
- d.callback('192.0.2.101')
- self.assertIsInstance(deferredResolver, Resolver)
- def test_resolverReceivesRootHints(self):
- """
- The L{root.Resolver} which eventually replaces L{root.DeferredResolver}
- is supplied with the IP addresses of the 13 root servers.
- """
- stubResolver = StubResolver()
- deferredResolver = root.bootstrap(stubResolver)
- for d in stubResolver.pendingResults:
- d.callback('192.0.2.101')
- self.assertEqual(deferredResolver.hints, ['192.0.2.101'] * 13)
- def test_continuesWhenSomeRootHintsFail(self):
- """
- The L{root.Resolver} is eventually created, even if some of the root
- hint lookups fail. Only the working root hint IP addresses are supplied
- to the L{root.Resolver}.
- """
- stubResolver = StubResolver()
- deferredResolver = root.bootstrap(stubResolver)
- results = iter(stubResolver.pendingResults)
- d1 = next(results)
- for d in results:
- d.callback('192.0.2.101')
- d1.errback(TimeoutError())
- def checkHints(res):
- self.assertEqual(deferredResolver.hints, ['192.0.2.101'] * 12)
- d1.addBoth(checkHints)
- def test_continuesWhenAllRootHintsFail(self):
- """
- The L{root.Resolver} is eventually created, even if all of the root hint
- lookups fail. Pending and new lookups will then fail with
- AttributeError.
- """
- stubResolver = StubResolver()
- deferredResolver = root.bootstrap(stubResolver)
- results = iter(stubResolver.pendingResults)
- d1 = next(results)
- for d in results:
- d.errback(TimeoutError())
- d1.errback(TimeoutError())
- def checkHints(res):
- self.assertEqual(deferredResolver.hints, [])
- d1.addBoth(checkHints)
- self.addCleanup(self.flushLoggedErrors, TimeoutError)
- def test_passesResolverFactory(self):
- """
- L{root.bootstrap} accepts a C{resolverFactory} argument which is passed
- as an argument to L{root.Resolver} when it has successfully looked up
- root hints.
- """
- stubResolver = StubResolver()
- deferredResolver = root.bootstrap(
- stubResolver, resolverFactory=raisingResolverFactory)
- for d in stubResolver.pendingResults:
- d.callback('192.0.2.101')
- self.assertIs(
- deferredResolver._resolverFactory, raisingResolverFactory)
- class StubDNSDatagramProtocol:
- """
- A do-nothing stand-in for L{DNSDatagramProtocol} which can be used to avoid
- network traffic in tests where that kind of thing doesn't matter.
- """
- def query(self, *a, **kw):
- return Deferred()
- _retrySuppression = util.suppress(
- category=DeprecationWarning,
- message=(
- 'twisted.names.root.retry is deprecated since Twisted 10.0. Use a '
- 'Resolver object for retry logic.'))
|