Ticket #5668: opt-record-5668-examples.patch

File opt-record-5668-examples.patch, 30.7 KB (added by rwall, 3 years ago)

Some examples of _OPTHeader usage.

  • 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
     12EDNSMessage 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
     26EDNSMessage 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 reactor
     21from twisted.names import dns
     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 = DNSMessageManglingProtocol(
     176            controller=None, mangler=addOptRecord)
     177        self.connectProtocol(proto)
     178
     179        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     180
     181        def checkForOpt(message):
     182            self.assertTrue(
     183                hasAdditionalOptRecord(message),
     184                'Message did not contain an OPT record '
     185                + 'in its additional section. '
     186                + 'rCode: %s, ' % (message.rCode,)
     187                + 'answers: %s, ' % (message.answers,)
     188                + 'authority: %s, ' % (message.authority,)
     189                + 'additional: %s ' % (message.additional,))
     190        d.addCallback(checkForOpt)
     191
     192        return d
     193
     194
     195    def test_611_formErrOnMultipleOptRecords(self):
     196        """
     197        When an OPT RR is included within any DNS message, it MUST be
     198        the only OPT RR in that message.  If a query message with more
     199        than one OPT RR is received, a FORMERR (RCODE=1) MUST be
     200        returned.
     201
     202        https://tools.ietf.org/html/rfc6891#section-6.1.1
     203        """
     204        def addMultipleOptRecord(message):
     205            message.additional.extend([dns._OPTHeader(), dns._OPTHeader()])
     206            return message
     207
     208        proto = DNSMessageManglingProtocol(
     209            controller=None, mangler=addMultipleOptRecord)
     210        self.connectProtocol(proto)
     211
     212        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     213
     214        d.addCallback(
     215            lambda message: self.assertEqual(message.rCode, dns.EFORMAT))
     216
     217        return d
     218
     219
     220    def test_7_nonEdnsResponseToNonEdnsRequest(self):
     221        """
     222        Lack of presence of an OPT record in a request MUST be taken as an
     223        indication that the requestor does not implement any part of this
     224        specification and that the responder MUST NOT include an OPT record
     225        in its response.
     226
     227        https://tools.ietf.org/html/rfc6891#section-7
     228        """
     229
     230        proto = dns.DNSDatagramProtocol(controller=None)
     231        self.connectProtocol(proto)
     232
     233        d = proto.query(self.server, [dns.Query('.', dns.NS, dns.IN)])
     234
     235        def checkForOpt(message):
     236            self.assertFalse(
     237                hasAdditionalOptRecord(message),
     238                'Message contained an OPT record '
     239                + 'in its additional section. '
     240                + 'rCode: %s, ' % (message.rCode,)
     241                + 'answers: %s, ' % (message.answers,)
     242                + 'authority: %s, ' % (message.authority,)
     243                + 'additional: %s ' % (message.additional,))
     244        d.addCallback(checkForOpt)
     245
     246        return d
     247
     248
     249
     250globals().update(
     251    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
     18This example relies on _EDNSMessage, which I've temporarily copied
     19from ticket:5675 into twisted.names.edns.
     20
     21I've also hacked together some supporting classes in that module which
     22demonstrate how _EDNSMessage can be integrated with the existing
     23protocol and factory classes with some subclasses. More comments in
     24edns.py.
     25"""
     26
     27from functools import partial
     28import re
     29import sys
     30
     31from twisted.internet import task
     32from twisted.names import dns
     33from twisted.names.edns import EDNSResolver
     34from twisted.python import usage
     35
     36
     37
     38ALL_QUERY_TYPES = dict(dns.QUERY_TYPES.items() + dns.EXT_QUERIES.items())
     39
     40
     41
     42class Options(usage.Options):
     43    """
     44    Options based on dig.
     45    """
     46
     47    synopsis = 'Usage: txdig [OPTIONS] DOMAIN_NAME QUERY_TYPE'
     48
     49    optFlags = [
     50        ["tcp", None, "Use TCP when querying name servers."],
     51        ["noedns", None, "Disable EDNS."],
     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,
     172        tcp=False, timeout=5, tries=3,
     173        server='127.0.0.1', port=53, **kwargs):
     174    """
     175    Query a DNS server.
     176    """
     177    r = EDNSResolver(servers=[(server, port)],
     178                     reactor=reactor,
     179                     ednsVersion=edns,
     180                     maxSize=bufsize,
     181                     dnssecOK=dnssec)
     182
     183    if tcp:
     184        queryMethod = partial(r.queryTCP, timeout=timeout)
     185    else:
     186        queryMethod = partial(r.queryUDP, timeout=(timeout,) * tries)
     187
     188    d = queryMethod(queries=[dns.Query(queryName, queryType, queryClass)])
     189
     190    d.addCallback(printMessage)
     191
     192    return d
     193
     194
     195
     196def main(reactor):
     197    return dig(reactor, **parseOptions())
     198
     199
     200
     201if __name__ == "__main__":
     202    task.react(main)
  • twisted/names/edns.py

     
     1# Copyright (c) Twisted Matrix Laboratories.
     2# See LICENSE for details.
     3
     4"""
     5_EDNSMessage copied from #5675.
     6
     7Plus subclasses of dns.DNSDatagramProtocol, dns.DNSProtocol and
     8client.Resolver which integrate EDNSMessage.
     9"""
     10
     11from twisted.internet import error
     12from twisted.names import client, dns
     13from twisted.names.dns import EFORMAT, Message, OPT, _OPTHeader, OP_QUERY
     14from twisted.python import util as tputil
     15
     16
     17
     18class EDNSDatagramProtocol(dns.DNSDatagramProtocol):
     19    """
     20    This hack is necessary because dns.DNSDatagramProtocol is
     21    hardcoded to use dns.Message for building outbound query datagrams
     22    and for decoding incoming datagrams.
     23
     24    It would be easier to integrate new EDNS components if DNS
     25    protocols had a convenient way of specifying an alternative
     26    message factory.
     27    """
     28    def __init__(self, *args, **kwargs):
     29        """
     30        This seems ugly too. If I could provide a messageFactory
     31        function, these EDNSMessage arguments needn't be passed
     32        explicitly to the DNS protocols. Instead just pass
     33        partial(EDNSMessage, ednsVersion=x, maxSize=y).
     34        """
     35        self.ednsVersion = kwargs.pop('ednsVersion', 0)
     36        self.maxSize = kwargs.pop('maxSize', 4096)
     37        self.dnssecOK = kwargs.pop('dnssecOK', False)
     38
     39        dns.DNSDatagramProtocol.__init__(self, *args, **kwargs)
     40
     41
     42    def writeMessage(self, message, address):
     43        """
     44        Again, this is a hack, but it demonstrates the usefulness of
     45        _EDNSMessage.fromMessage for wrapping dns.Message.
     46
     47        It might be convenient if I could provide EDNS specific
     48        keyword arguments to fromMessage - ednsVersion, maxSize, etc.
     49        """
     50        message = _EDNSMessage.fromMessage(message)
     51
     52        message.ednsVersion = self.ednsVersion
     53        message.maxSize = self.maxSize
     54        message.dnssecOK = self.dnssecOK
     55
     56        return dns.DNSDatagramProtocol.writeMessage(self, message, address)
     57
     58
     59    def _query(self, *args, **kwargs):
     60        d = dns.DNSDatagramProtocol._query(self, *args, **kwargs)
     61
     62        return d.addCallback(_EDNSMessage.fromMessage)
     63
     64
     65
     66class EDNSStreamProtocol(dns.DNSProtocol):
     67    """
     68    See comments for EDNSDatagramProtocol.
     69
     70    It's a shame we have to duplicate the same hacks for the TCP DNS
     71    protocol.
     72
     73    If DNSDatagramProtocol used connected UDP instead, there would be
     74    less difference between the UDP and TCP protocols eg writeMessage
     75    would have a consistent signature and maybe this duplication
     76    wouldn't be necessary.
     77    """
     78    def __init__(self, *args, **kwargs):
     79        self.ednsVersion = kwargs.pop('ednsVersion', 0)
     80        self.maxSize = kwargs.pop('maxSize', 4096)
     81        self.dnssecOK = kwargs.pop('dnssecOK', False)
     82
     83        dns.DNSProtocol.__init__(self, *args, **kwargs)
     84
     85
     86    def writeMessage(self, message):
     87        message = _EDNSMessage.fromMessage(message)
     88        message.ednsVersion = self.controller.ednsVersion
     89        message.maxSize = self.controller.maxSize
     90        message.dnssecOK = self.controller.dnssecOK
     91
     92        return dns.DNSProtocol.writeMessage(self, message)
     93
     94
     95    def _query(self, *args, **kwargs):
     96        d = dns.DNSProtocol._query(self, *args, **kwargs)
     97        d.addCallback(_EDNSMessage.fromMessage)
     98        return d
     99
     100
     101
     102class EDNSClientFactory(client.DNSClientFactory):
     103    def buildProtocol(self, addr):
     104        p = EDNSStreamProtocol(controller=self.controller)
     105        p.factory = self
     106        return p
     107
     108
     109
     110class EDNSResolver(client.Resolver):
     111    """
     112    client.Resolver is hardcoded to use dns.DNSDatagramProtcol and
     113    dns.DNSProtocol (via client.DNSClientFactory).
     114
     115    It would be nice if I could specify dnsDatagramProtocolFactory and
     116    dnsStreamProtocolFactory as arguments to client.Resolver.
     117
     118    Also need to consider whether client.Resolver is a suitable place
     119    to do EDNS buffer size detection.
     120
     121    The IResolver methods of client.Resolver currently respond to
     122    truncated UDP messages by issuing a follow up TCP query.
     123
     124    In addition they could respond to timeouts by re-issue a UDP query
     125    with a smaller advertised EDNS buffersize.
     126
     127    See
     128     * https://tools.ietf.org/html/rfc6891#section-6.2.2
     129     * https://www.dns-oarc.net/oarc/services/replysizetest
     130    """
     131    def __init__(self, *args, **kwargs):
     132        self.ednsVersion = kwargs.pop('ednsVersion', 0)
     133        self.maxSize = kwargs.pop('maxSize', 4096)
     134        self.dnssecOK = kwargs.pop('dnssecOK', False)
     135
     136        client.Resolver.__init__(self, *args, **kwargs)
     137
     138        self.factory = EDNSClientFactory(self, self.timeout)
     139
     140
     141    def _connectedProtocol(self):
     142        proto = EDNSDatagramProtocol(
     143            ednsVersion=self.ednsVersion,
     144            maxSize=self.maxSize,
     145            dnssecOK=self.dnssecOK,
     146            controller=self,
     147            reactor=self._reactor)
     148
     149        while True:
     150            try:
     151                self._reactor.listenUDP(dns.randomSource(), proto)
     152            except error.CannotListenError:
     153                pass
     154            else:
     155                return proto
     156
     157
     158
     159class _EDNSMessage(tputil.FancyStrMixin, tputil.FancyEqMixin, object):
     160    """
     161    An C{EDNS} message.
     162
     163    Designed for compatibility with L{Message} but with a narrower
     164    public interface.
     165
     166    Most importantly, L{_EDNSMessage.fromStr} will interpret and
     167    remove OPT records that are present in the additional records
     168    section.
     169
     170    The OPT records are used to populate certain EDNS specific
     171    attributes.
     172
     173    L{_EDNSMessage.toStr} will add suitable OPT records to the
     174    additional section to represent the extended EDNS information.
     175
     176    @see: U{https://tools.ietf.org/html/rfc6891}
     177
     178    @ivar id: A 16 bit identifier assigned by the program that
     179        generates any kind of query.  This identifier is copied the
     180        corresponding reply and can be used by the requester to match
     181        up replies to outstanding queries.
     182
     183    @ivar answer: A one bit field that specifies whether this message
     184        is a query (0), or a response (1).
     185
     186    @ivar opCode: A four bit field that specifies kind of query in
     187        this message.  This value is set by the originator of a query
     188        and copied into the response.  The values are:
     189                0               a standard query (QUERY)
     190                1               an inverse query (IQUERY)
     191                2               a server status request (STATUS)
     192                3-15            reserved for future use
     193
     194    @ivar auth: Authoritative Answer - this bit is valid in responses,
     195        and specifies that the responding name server is an authority
     196        for the domain name in question section.
     197
     198    @ivar trunc: TrunCation - specifies that this message was
     199        truncated due to length greater than that permitted on the
     200        transmission channel.
     201
     202    @ivar recDes: Recursion Desired - this bit may be set in a query
     203        and is copied into the response.  If RD is set, it directs the
     204        name server to pursue the query recursively.  Recursive query
     205        support is optional.
     206
     207    @ivar recAv: Recursion Available - this be is set or cleared in a
     208        response, and denotes whether recursive query support is
     209        available in the name server.
     210
     211    @ivar rCode: Response code - this 4 bit field is set as part of
     212        responses.  The values have the following interpretation:
     213                0               No error condition
     214
     215                1               Format error - The name server was
     216                                unable to interpret the query.
     217                2               Server failure - The name server was
     218                                unable to process this query due to a
     219                                problem with the name server.
     220
     221                3               Name Error - Meaningful only for
     222                                responses from an authoritative name
     223                                server, this code signifies that the
     224                                domain name referenced in the query does
     225                                not exist.
     226
     227                4               Not Implemented - The name server does
     228                                not support the requested kind of query.
     229
     230                5               Refused - The name server refuses to
     231                                perform the specified operation for
     232                                policy reasons.  For example, a name
     233                                server may not wish to provide the
     234                                information to the particular requester,
     235                                or a name server may not wish to perform
     236                                a particular operation (e.g., zone
     237                                transfer) for particular data.
     238
     239    @ivar ednsVersion: Indicates the EDNS implementation level. Set to
     240        C{None} to prevent any EDNS attributes and options being added
     241        to the encoded byte string.
     242
     243    @ivar queries: A L{list} of L{Query} instances.
     244
     245    @ivar answers: A L{list} of L{RRHeader} instances.
     246
     247    @ivar authority: A L{list} of L{RRHeader} instances.
     248
     249    @ivar additional: A L{list} of L{RRHeader} instances.
     250    """
     251
     252    showAttributes = (
     253        'id', 'answer', 'opCode', 'auth', 'trunc',
     254        'recDes', 'recAv', 'rCode', 'ednsVersion', 'dnssecOK',
     255        'maxSize',
     256        'queries', 'answers', 'authority', 'additional')
     257
     258    compareAttributes = showAttributes
     259
     260    def __init__(self, id=0, answer=0,
     261                 opCode=OP_QUERY, auth=0,
     262                 trunc=0, recDes=0,
     263                 recAv=0, rCode=0, ednsVersion=0, dnssecOK=False, maxSize=512,
     264                 queries=None, answers=None, authority=None, additional=None):
     265        """
     266        All arguments are stored as attributes with the same names.
     267
     268        @see: L{_EDNSMessage} for an explanation of the meaning of
     269            each attribute.
     270
     271        @type id: C{int}
     272        @type answer: C{int}
     273        @type opCode: C{int}
     274        @type auth: C{int}
     275        @type trunc: C{int}
     276        @type recDes: C{int}
     277        @type recAv: C{int}
     278        @type rCode: C{int}
     279        @type ednsVersion: C{int} or C{None}
     280        @type queries: C{list} of L{Query}
     281        @type answers: C{list} of L{RRHeader}
     282        @type authority: C{list} of L{RRHeader}
     283        @type additional: C{list} of L{RRHeader}
     284        """
     285        self.id = id
     286        self.answer = answer
     287        self.opCode = opCode
     288
     289        # XXX: AA bit can be determined by checking for an
     290        # authoritative answer record whose name matches the query
     291        # name - perhaps in a higher level EDNSResponse class?
     292        self.auth = auth
     293
     294        # XXX: TC bit can be determined during encoding based on EDNS max
     295        # packet size.
     296        self.trunc = trunc
     297
     298        self.recDes = recDes
     299        self.recAv = recAv
     300        self.rCode = rCode
     301        self.ednsVersion = ednsVersion
     302        self.dnssecOK = dnssecOK
     303        self.maxSize = maxSize
     304
     305        self.queries = queries or []
     306        self.answers = answers or []
     307        self.authority = authority or []
     308        self.additional = additional or []
     309
     310        self._decodingErrors = []
     311
     312
     313    def toStr(self):
     314        """
     315        Encode to wire format.
     316
     317        If C{ednsVersion} is not None, an L{_OPTHeader} instance
     318        containing all the I{EDNS} specific attributes and options
     319        will be appended to the list of C{additional} records and this
     320        will be encoded into the byte string as an C{OPT} record byte
     321        string.
     322
     323        @return: A L{bytes} string.
     324        """
     325        m = Message(
     326            id=self.id,
     327            answer=self.answer,
     328            opCode=self.opCode,
     329            auth=self.auth,
     330            trunc=self.trunc,
     331            recDes=self.recDes,
     332            recAv=self.recAv,
     333            rCode=self.rCode,
     334            maxSize=self.maxSize)
     335
     336        m.queries = list(self.queries)
     337        m.answers = list(self.answers)
     338        m.authority = list(self.authority)
     339        m.additional = list(self.additional)
     340
     341        if self.ednsVersion is not None:
     342            o = _OPTHeader(version=self.ednsVersion,
     343                           udpPayloadSize=self.maxSize,
     344                           dnssecOK=self.dnssecOK)
     345            m.additional.append(o)
     346
     347        return m.toStr()
     348
     349
     350    @classmethod
     351    def fromMessage(cls, message):
     352        """
     353        Construct and return a new L(_EDNSMessage} whose attributes
     354        and records are derived from the attributes and records of
     355        C{message} (a L{Message} instance)
     356
     357        If present, an I{OPT} record will be extracted from the
     358        C{additional} section and its attributes and options will be
     359        used to set the EDNS specific attributes C{extendedRCODE},
     360        c{ednsVersion}, c{dnssecOK}, c{ednsOptions}.
     361
     362        The C{extendedRCODE} will be combined with C{message.rCode}
     363        and assigned to C{self.rCode}.
     364
     365        If multiple I{OPT} records are found, this is considered an
     366        error and no EDNS specific attributes will be
     367        set. Additionally, an L{EFORMAT} error will be appended to
     368        C{_decodingErrors}.
     369        """
     370        additional = []
     371        optRecords = []
     372        for r in message.additional:
     373            if r.type == OPT:
     374                optRecords.append(_OPTHeader.fromRRHeader(r))
     375            else:
     376                additional.append(r)
     377
     378        newMessage = cls(
     379            id=message.id,
     380            answer=message.answer,
     381            opCode=message.opCode,
     382            auth=message.auth,
     383            trunc=message.trunc,
     384            recDes=message.recDes,
     385            recAv=message.recAv,
     386            rCode=message.rCode,
     387            # Default to None, it will be updated later when the OPT
     388            # records are parsed.
     389            ednsVersion=None,
     390            queries=list(message.queries),
     391            answers=list(message.answers),
     392            authority=list(message.authority),
     393            additional=additional,
     394            )
     395
     396        if optRecords:
     397            if len(optRecords) > 1:
     398                newMessage._decodingErrors.append(EFORMAT)
     399            else:
     400                opt = optRecords[0]
     401                newMessage.ednsVersion = opt.version
     402                newMessage.maxSize = opt.udpPayloadSize
     403                newMessage.dnssecOK = opt.dnssecOK
     404
     405        return newMessage
     406
     407
     408    def fromStr(self, bytes):
     409        """
     410        Decode from wire format, saving flags, values and records to
     411        this L{_EDNSMessage} instance in place.
     412
     413        @type bytes: L{bytes}
     414        @param bytes: The full byte string to be decoded.
     415        """
     416        m = Message()
     417        m.fromStr(bytes)
     418
     419        ednsMessage = self.fromMessage(m)
     420        self.__dict__ = ednsMessage.__dict__