Ticket #5675: edns-message-5675-examples.patch

File edns-message-5675-examples.patch, 22.2 KB (added by rwall, 3 years ago)

Latest edns examples as a patch against edns-message-5675-4

  • doc/names/examples/edns_server.tac.py

     
     1# Copyright (c) Twisted Matrix Laboratories.
     2# See LICENSE for details.
     3
     4"""
     5A DNS server which replies NXDOMAIN to all queries.
     6
     7Usage: twistd -noy doc/names/examples/edns_auth_server.tac.py
     8
     9This server uses the protocol hacks from edns.py
     10
     11The important thing is that because messages are decoded using
     12dns._EDNSMessage rather than dns.Message, OPT records are extracted from
     13the additional section of EDNS query messages during decoding.
     14
     15This is one way of fixing #6645.
     16
     17Additionally we force ednsVersion=None so that the server doesn't
     18respond with any OPT records.
     19Although RFC6891-7 suggests that the correct response should be FORMERR.
     20 * https://tools.ietf.org/html/rfc6891#section-7
     21
     22Ultimately, DNSServerFactory will need modifying or replacing so that
     23it can dynamically respond using the correct EDNS settings and RCODE
     24based on the client request.
     25
     26dns._EDNSMessage will also need to be made aware of RRSets so that it can
     27correctly limit the size of (or truncate) responses based on the
     28chosen maxSize.
     29 * https://twistedmatrix.com/trac/wiki/EDNS0#Selectivetruncate
     30"""
     31
     32from functools import partial
     33
     34from twisted.application.internet import TCPServer, UDPServer
     35from twisted.application.service import Application, MultiService
     36
     37from twisted.names import edns, server
     38
     39
     40
     41PORT = 10053
     42EDNS_VERSION = None
     43
     44
     45def makeService():
     46    masterService = MultiService()
     47
     48    factory = server.DNSServerFactory(
     49        authorities=[],
     50        caches=[],
     51        clients=[])
     52
     53    factory.protocol = partial(edns.EDNSStreamProtocol, ednsVersion=EDNS_VERSION)
     54    proto = edns.EDNSDatagramProtocol(ednsVersion=EDNS_VERSION, controller=factory)
     55
     56    UDPServer(PORT, proto).setServiceParent(masterService)
     57    TCPServer(PORT, factory).setServiceParent(masterService)
     58
     59    return masterService
     60
     61
     62
     63application = Application("An EDNS aware noop DNS server")
     64
     65
     66
     67makeService().setServiceParent(application)
  • doc/names/examples/test_edns_compliance.py

     
     1# Copyright (c) Twisted Matrix Laboratories.
     2# See LICENSE for details.
     3
     4"""
     5An example trial test module which demonstrates how the low level
     6L{dns._OPTHeader} class can be used for testing DNS servers for
     7compliance with DNS RFCs.
     8
     9This example should be run using trial eg
     10
     11 trial doc/names/examples/test_edns_compliance.py
     12
     13 OR
     14
     15 TARGET=127.0.0.1 trial doc/names/examples/test_edns_compliance.py
     16"""
     17
     18import os
     19
     20from twisted.internet import defer, reactor
     21from twisted.names import dns, edns
     22from twisted.trial import unittest
     23
     24
     25
     26class DNSMessageManglingProtocol(dns.DNSDatagramProtocol):
     27    """
     28    A L{dns.DNSDatagramProtocol} subclass with hooks for mangling a
     29    L{dns.Message} before it is sent.
     30    """
     31
     32    def __init__(self, *args, **kwargs):
     33        """
     34        @param mangler: A callable which will be passed a message
     35            argument and must return a message which will then be
     36            encoded and sent.
     37        @type mangler: L{callable}
     38
     39        @see: L{dns.DNSDatagramProtocol.__init__} for inherited
     40            arguments.
     41        """
     42        self.mangler = kwargs.pop('mangler')
     43        dns.DNSDatagramProtocol.__init__(self, *args, **kwargs)
     44
     45
     46    def writeMessage(self, message, address):
     47        """
     48        Send a message holding DNS queries.
     49
     50        @type message: L{dns.Message}
     51        """
     52        message = self.mangler(message)
     53        return dns.DNSDatagramProtocol.writeMessage(self, message, address)
     54
     55
     56
     57def serversUnderTest(default):
     58    """
     59    Return a list of server information tuples found in the
     60    environment or C{default} if none are found.
     61
     62    @param default: A default list of servers to be tested if none
     63        were found among the environment variables.
     64    @type default: L{list} of 3-L{tuple}.
     65
     66    @return: L{list} of L{tuple} containing target server info
     67        (host, port, description)
     68    """
     69    targetServer = os.environ.get('TARGET')
     70    if targetServer is not None:
     71        parts = targetServer.split(',', 2)
     72        if len(parts) == 2:
     73            parts.append(parts[0])
     74        if len(parts) == 1:
     75            parts.extend([53, parts[0]])
     76        parts[1] = int(parts[1])
     77        return [tuple(parts)]
     78    else:
     79        return default
     80
     81
     82
     83# Default servers to be tested
     84SERVERS = [
     85    # GoogleDNS public recursive resolver
     86    ('8.8.8.8', 53, 'GoogleRecursiveDns'),
     87
     88    # OpenDNS public recursive resolver
     89    ('208.67.222.222', 53, 'OpenDNS'),
     90
     91    # Twisted 13.1 Authoritative DNS (ns1.twistedmatrix.com)
     92    ('66.35.39.66', 53, 'TwistedAuthoritativeDns'),
     93
     94    # Bind 9.9.3-S1-P1 (as reported by version.bind CH TXT) (ams.sns-pb.isc.org)
     95    ('199.6.1.30', 53, 'Bind9.9'),
     96
     97    # Power DNS (as reported by version.bind CH TXT) (dns-us1.powerdns.net)
     98    ('46.165.192.30', 53, 'PowerDNS'),
     99
     100    # NSD 4.0.0b5 (as reported by version.bind CH TXT) (open.nlnetlabs.nl)
     101    ('213.154.224.1', 53, 'NSD4'),
     102
     103    # DJBDNS (uz5dz39x8xk8wyq3dzn7vpt670qmvzx0zd9zg4ldwldkv6kx9ft090.ns.yp.to.)
     104    # ('131.155.71.143', 53, 'DJBDNS')
     105]
     106
     107
     108
     109class DNSComplianceTestBuilder(object):
     110    """
     111    Build a dictionary of L{unittest.TestCase} classes each of which
     112    runs a group of tests against a particular server.
     113    """
     114    @classmethod
     115    def makeTestCaseClasses(cls):
     116        """
     117        Create a L{unittest.TestCase} subclass which mixes in C{cls}
     118        for each server and return a dict mapping their names to them.
     119        """
     120        classes = {}
     121        for host, port, description in serversUnderTest(SERVERS):
     122            name = (cls.__name__ + "." + description).replace(".", "_")
     123            class testcase(cls, unittest.TestCase):
     124                __module__ = cls.__module__
     125                server = (host, port)
     126            testcase.__name__ = name
     127            classes[testcase.__name__] = testcase
     128        return classes
     129
     130
     131
     132def hasAdditionalOptRecord(message):
     133    """
     134    Test a message for an L{dns._OPTHeader} instance among its
     135    additional records.
     136    """
     137    for r in message.additional:
     138        if r.type == dns.OPT:
     139            return True
     140    return False
     141
     142
     143
     144class RFC6891Tests(DNSComplianceTestBuilder):
     145    """
     146    Tests for compliance with RFC6891.
     147
     148    https://tools.ietf.org/html/rfc6891#section-6.1.1
     149    """
     150    def connectProtocol(self, proto):
     151        """
     152        Connect C{proto} to a listening UDP port and add a cleanup to
     153        stop the port when the current test finishes.
     154
     155        @param proto: A L{twisted.internet.protocols.DatagramProtocol}
     156            instance.
     157        """
     158        port = reactor.listenUDP(0, proto)
     159        self.addCleanup(port.stopListening)
     160
     161
     162    def test_611_ednsResponseToEdnsRequest(self):
     163        """
     164        If an OPT record is present in a received request, compliant
     165        responders MUST include an OPT record in their respective
     166        responses.
     167
     168        https://tools.ietf.org/html/rfc6891#section-6.1.1
     169        """
     170
     171        def addOptRecord(message):
     172            message.additional.append(dns._OPTHeader(version=1))
     173            return message
     174
     175        proto = edns.EDNSDatagramProtocol(
     176            controller=None, ednsVersion=0)
     177        self.connectProtocol(proto)
     178
     179        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     180
     181        def checkEdnsVersion(message):
     182            self.assertEqual(message.ednsVersion, 0)
     183        d.addCallback(checkEdnsVersion)
     184
     185        return d
     186
     187
     188    def test_611_formErrOnMultipleOptRecords(self):
     189        """
     190        When an OPT RR is included within any DNS message, it MUST be
     191        the only OPT RR in that message.  If a query message with more
     192        than one OPT RR is received, a FORMERR (RCODE=1) MUST be
     193        returned.
     194
     195        https://tools.ietf.org/html/rfc6891#section-6.1.1
     196        """
     197        def addMultipleOptRecord(message):
     198            message.additional.extend([dns._OPTHeader(), dns._OPTHeader()])
     199            return message
     200
     201        proto = DNSMessageManglingProtocol(
     202            controller=None, mangler=addMultipleOptRecord)
     203        self.connectProtocol(proto)
     204
     205        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     206
     207        d.addCallback(
     208            lambda message: self.assertEqual(message.rCode, dns.EFORMAT))
     209
     210        return d
     211
     212
     213    def test_613_badVersion(self):
     214        """
     215        If a responder does not implement the VERSION level of the
     216        request, then it MUST respond with RCODE=BADVERS.
     217
     218        https://tools.ietf.org/html/rfc6891#section-6.1.3
     219        """
     220        proto = edns.EDNSDatagramProtocol(
     221            controller=None, ednsVersion=255)
     222
     223        self.connectProtocol(proto)
     224
     225        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     226
     227        d.addCallback(
     228            lambda message: self.assertEqual(message.rCode, dns.EBADVERSION))
     229
     230        return d
     231
     232
     233    def test_623_minPayloadSize(self):
     234        """
     235        Requestor's Payload Size
     236
     237        Values lower than 512 MUST be treated as equal to 512.
     238
     239        https://tools.ietf.org/html/rfc6891#section-6.2.3
     240        https://tools.ietf.org/html/rfc6891#section-6.2.5
     241        """
     242        proto512 = edns.EDNSDatagramProtocol(
     243            controller=None, maxSize=512)
     244        self.connectProtocol(proto512)
     245
     246        proto128 = edns.EDNSDatagramProtocol(
     247            controller=None, maxSize=128)
     248        self.connectProtocol(proto128)
     249
     250        d512 = proto512.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     251        d128 = proto128.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     252
     253        d = defer.gatherResults([d512, d128])
     254
     255        def compareMessages(messages):
     256            m1, m2 = messages
     257            self.assertEqual(len(m1.answers), len(m2.answers),
     258                             'Different answers.')
     259            self.assertEqual(len(m1.authority), len(m2.authority),
     260                             'Different authority')
     261            self.assertEqual(len(m1.additional), len(m2.additional),
     262                             'Different additional')
     263        d.addCallback(compareMessages)
     264
     265        return d
     266
     267
     268    def test_7_nonEdnsResponseToNonEdnsRequest(self):
     269        """
     270        Lack of presence of an OPT record in a request MUST be taken as an
     271        indication that the requestor does not implement any part of this
     272        specification and that the responder MUST NOT include an OPT record
     273        in its response.
     274
     275        https://tools.ietf.org/html/rfc6891#section-7
     276        """
     277
     278        proto = edns.EDNSDatagramProtocol(controller=None, ednsVersion=None)
     279        self.connectProtocol(proto)
     280
     281        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     282
     283        def checkForOpt(message):
     284            self.assertIdentical(message.ednsVersion, None)
     285        d.addCallback(checkForOpt)
     286
     287        return d
     288
     289
     290
     291globals().update(
     292    RFC6891Tests.makeTestCaseClasses())
  • doc/names/examples/txdig.py

     
     1# Copyright (c) Twisted Matrix Laboratories.
     2# See LICENSE for details.
     3
     4"""
     5A flexible tool for interrogating DNS name servers.
     6
     7Example usage:
     8 txdig -s 8.8.8.8 example.com NS
     9
     10This is a usecase for an API with the convenience of client.Resolver
     11while allowing fine control of the DNS query message.
     12
     13I use client.Resolver.queryUDP and queryTCP instead of IResolver
     14methods because I want to choose the transport protocol and because
     15these functions return a message instance instead of just the record
     16sections.
     17
     18I've hacked together some supporting classes in that module which
     19demonstrate how _EDNSMessage can be integrated with the existing
     20protocol and factory classes with some subclasses. More comments in
     21edns.py.
     22"""
     23
     24from functools import partial
     25import re
     26import sys
     27
     28from twisted.internet import task
     29from twisted.names import dns
     30from twisted.names.edns import EDNSResolver
     31from twisted.python import usage
     32
     33
     34
     35ALL_QUERY_TYPES = dict(dns.QUERY_TYPES.items() + dns.EXT_QUERIES.items())
     36
     37
     38
     39class Options(usage.Options):
     40    """
     41    Options based on dig.
     42    """
     43
     44    synopsis = 'Usage: txdig [OPTIONS] DOMAIN_NAME QUERY_TYPE'
     45
     46    optFlags = [
     47        ["tcp", None, "Use TCP when querying name servers."],
     48        ["noedns", None, "Disable EDNS."],
     49        ["noadflag", None, ("Do not set the AD "
     50                            "(authentic data) bit in the query.")],
     51        ["cdflag", None, "Set the CD (checking disabled) bit in the query."],
     52        ["dnssec", None, ("Requests DNSSEC records be sent "
     53                          "by setting the DNSSEC OK bit (DO) "
     54                          "in the OPT record in the additional section "
     55                          "of the query.")],
     56    ]
     57
     58    optParameters = [
     59            ["server", "s", '127.0.0.1',
     60             "The name or IP address of the name server to query.", str],
     61
     62            ["port", "p", 53,
     63             "The port number of the name server to query.", int],
     64
     65            ["timeout", "t", 5,
     66             "The timeout for a query in seconds.", float],
     67
     68            ["tries", "T", 3,
     69             "The number of times to try UDP queries to server.", int],
     70
     71            ["edns", None, 0,
     72             "Specify the EDNS version to query with.", int],
     73
     74            ["bufsize", None, 4096,
     75             "Set the UDP message buffer size advertised using EDNS0.", int],
     76        ]
     77
     78
     79    def parseArgs(self, queryName='', queryType='ALL_RECORDS'):
     80        self['queryName'] = queryName
     81        try:
     82            self['queryType'] = dns.REV_TYPES[queryType]
     83        except KeyError:
     84            raise usage.UsageError(
     85                'Unrecognised QUERY_TYPE %r. ' % (queryType,)
     86                + 'Must be one of %r' % (sorted(dns.REV_TYPES.keys()),))
     87
     88
     89    def postOptions(self):
     90        if self['noedns']:
     91            self['edns'] = None
     92
     93
     94
     95def parseOptions():
     96    """
     97    Parse command line options and print the full usage message to
     98    stderr if there are errors.
     99    """
     100    options = Options()
     101    try:
     102        options.parseOptions()
     103    except usage.UsageError as errortext:
     104        sys.stderr.write(str(options) + '\n')
     105        sys.stderr.write('ERROR: %s\n' % (errortext,))
     106        raise SystemExit(1)
     107    return options
     108
     109
     110
     111def formatRecord(record):
     112    """
     113    Format a record and its payload to match the dig long form.
     114    """
     115    line = []
     116
     117    if isinstance(record, dns.Query):
     118        line.append(';')
     119
     120    line.append(record.name.name.ljust(25))
     121
     122    if isinstance(record, dns.RRHeader):
     123        line.append(str(record.ttl).ljust(6))
     124
     125    line.append(
     126        dns.QUERY_CLASSES.get(
     127            record.cls, '(%s)' % (record.cls,)).ljust(5))
     128
     129    line.append(
     130        ALL_QUERY_TYPES.get(
     131            record.type, '(%s)' % (record.type,)).ljust(5))
     132
     133    if isinstance(record, dns.RRHeader):
     134        payload = str(record.payload)
     135        # Remove the <RECORD_NAME and > from the payload str
     136        line.append(payload[payload.find(' '):-1])
     137
     138    # Remove the ttl from the payload, its already printed from the RRHeader.
     139    line = re.sub('\s+ttl=\d+', '', ' '.join(line))
     140
     141    return line
     142
     143
     144
     145def printMessage(message):
     146    """
     147    Print the sections of a message in dig long form.
     148    """
     149    sections = ("queries", "answers", "authority", "additional")
     150    print ";; flags:",
     151    for a in message.showAttributes:
     152        if a in sections:
     153            continue
     154        print '%s: %s,' % (a, getattr(message, a)),
     155    print
     156
     157    for section in sections:
     158        records = getattr(message, section)
     159        print ";;", section.upper(), "SECTION:", len(records)
     160        for r in records:
     161            print formatRecord(r)
     162        print
     163
     164    print ";; MSG SIZE recvd:", len(message.toStr())
     165
     166    return message
     167
     168
     169
     170def dig(reactor, queryName='', queryType=dns.ALL_RECORDS, queryClass=dns.IN,
     171        edns=0, bufsize=4096, dnssec=False, noadflag=False, cdflag=False,
     172        tcp=False, timeout=5, tries=3,
     173        server='127.0.0.1', port=53, **kwargs):
     174    """
     175    Query a DNS server.
     176    """
     177    messageOptions = dict(
     178        ednsVersion=edns,
     179        maxSize=bufsize,
     180        dnssecOK=dnssec,
     181        authenticData=not noadflag,
     182        checkingDisabled=cdflag)
     183
     184    r = EDNSResolver(servers=[(server, port)],
     185                     reactor=reactor,
     186                     messageOptions=messageOptions)
     187
     188    if tcp:
     189        queryMethod = partial(r.queryTCP, timeout=timeout)
     190    else:
     191        queryMethod = partial(r.queryUDP, timeout=(timeout,) * tries)
     192
     193    d = queryMethod(queries=[dns.Query(queryName, queryType, queryClass)])
     194
     195    d.addCallback(printMessage)
     196
     197    return d
     198
     199
     200
     201def main(reactor):
     202    return dig(reactor, **parseOptions())
     203
     204
     205
     206if __name__ == "__main__":
     207    task.react(main)
  • twisted/names/edns.py

     
     1# Copyright (c) Twisted Matrix Laboratories.
     2# See LICENSE for details.
     3
     4"""
     5Subclasses of dns.DNSDatagramProtocol, dns.DNSProtocol and
     6client.Resolver which integrate EDNSMessage.
     7"""
     8
     9from twisted.internet import error
     10from twisted.names import client, dns
     11from twisted.names.dns import _EDNSMessage
     12
     13
     14
     15class EDNSDatagramProtocol(dns.DNSDatagramProtocol):
     16    """
     17    This hack is necessary because dns.DNSDatagramProtocol is
     18    hardcoded to use dns.Message for building outbound query datagrams
     19    and for decoding incoming datagrams.
     20
     21    It would be easier to integrate new EDNS components if DNS
     22    protocols had a convenient way of specifying an alternative
     23    message factory.
     24    """
     25    def __init__(self, *args, **kwargs):
     26        """
     27        This seems ugly too. If I could provide a messageFactory
     28        function, these EDNSMessage arguments needn't be passed
     29        explicitly to the DNS protocols. Instead just pass
     30        partial(EDNSMessage, ednsVersion=x, maxSize=y).
     31        """
     32        self._messageOptions = kwargs.pop('messageOptions', {})
     33        dns.DNSDatagramProtocol.__init__(self, *args, **kwargs)
     34
     35
     36    def writeMessage(self, message, address):
     37        """
     38        Again, this is a hack, but it demonstrates the usefulness of
     39        _EDNSMessage.fromMessage for wrapping dns.Message.
     40
     41        It might be convenient if I could provide EDNS specific
     42        keyword arguments to fromMessage - ednsVersion, maxSize, etc.
     43        """
     44        message = _EDNSMessage.fromMessage(message)
     45        for k, v in self._messageOptions.items():
     46            setattr(message, k, v)
     47        return dns.DNSDatagramProtocol.writeMessage(self, message, address)
     48
     49
     50    def _query(self, *args, **kwargs):
     51        d = dns.DNSDatagramProtocol._query(self, *args, **kwargs)
     52
     53        return d.addCallback(_EDNSMessage.fromMessage)
     54
     55
     56
     57class EDNSStreamProtocol(dns.DNSProtocol):
     58    """
     59    See comments for EDNSDatagramProtocol.
     60
     61    It's a shame we have to duplicate the same hacks for the TCP DNS
     62    protocol.
     63
     64    If DNSDatagramProtocol used connected UDP instead, there would be
     65    less difference between the UDP and TCP protocols eg writeMessage
     66    would have a consistent signature and maybe this duplication
     67    wouldn't be necessary.
     68    """
     69    def __init__(self, *args, **kwargs):
     70        self._messageOptions = kwargs.pop('messageOptions', {})
     71        dns.DNSProtocol.__init__(self, *args, **kwargs)
     72
     73
     74    def writeMessage(self, message):
     75        message = _EDNSMessage.fromMessage(message)
     76        for k, v in self._messageOptions.items():
     77            setattr(message, k, v)
     78        return dns.DNSProtocol.writeMessage(self, message)
     79
     80
     81    def _query(self, *args, **kwargs):
     82        d = dns.DNSProtocol._query(self, *args, **kwargs)
     83        d.addCallback(_EDNSMessage.fromMessage)
     84        return d
     85
     86
     87
     88class EDNSClientFactory(client.DNSClientFactory):
     89    def __init__(self, *args, **kwargs):
     90        self._messageOptions = kwargs.pop('messageOptions', {})
     91        client.DNSClientFactory.__init__(self, *args, **kwargs)
     92
     93    def buildProtocol(self, addr):
     94        p = EDNSStreamProtocol(controller=self.controller,
     95                               messageOptions=self._messageOptions)
     96        p.factory = self
     97        return p
     98
     99
     100
     101class EDNSResolver(client.Resolver):
     102    """
     103    client.Resolver is hardcoded to use dns.DNSDatagramProtcol and
     104    dns.DNSProtocol (via client.DNSClientFactory).
     105
     106    It would be nice if I could specify dnsDatagramProtocolFactory and
     107    dnsStreamProtocolFactory as arguments to client.Resolver.
     108
     109    Also need to consider whether client.Resolver is a suitable place
     110    to do EDNS buffer size detection.
     111
     112    The IResolver methods of client.Resolver currently respond to
     113    truncated UDP messages by issuing a follow up TCP query.
     114
     115    In addition they could respond to timeouts by re-issue a UDP query
     116    with a smaller advertised EDNS buffersize.
     117
     118    See
     119     * https://tools.ietf.org/html/rfc6891#section-6.2.2
     120     * https://www.dns-oarc.net/oarc/services/replysizetest
     121    """
     122    def __init__(self, *args, **kwargs):
     123        self._messageOptions = kwargs.pop('messageOptions', {})
     124        client.Resolver.__init__(self, *args, **kwargs)
     125        self.factory = EDNSClientFactory(self,
     126                                         timeout=self.timeout,
     127                                         messageOptions=self._messageOptions)
     128
     129
     130    def _connectedProtocol(self):
     131        proto = EDNSDatagramProtocol(controller=self,
     132                                     reactor=self._reactor,
     133                                     messageOptions=self._messageOptions)
     134        while True:
     135            try:
     136                self._reactor.listenUDP(dns.randomSource(), proto)
     137            except error.CannotListenError:
     138                pass
     139            else:
     140                return proto