Implement TSIG Support in mDNS

Implements support for scoped TSIG Keys in MiniDNS. When a non-tsig
signed request is received, we assume the default pool should be
used.

Change-Id: I0b5ab727fba526724e44894bb7b84855e3ec0351
Implements: blueprint mdns-designate-mdns-tsig
This commit is contained in:
Kiall Mac Innes 2015-03-02 22:36:58 +00:00
parent f10d10a724
commit 24b5762e28
9 changed files with 426 additions and 81 deletions

View File

@ -67,6 +67,24 @@ if is_service_enabled designate && [[ -r $DESIGNATE_PLUGINS/backend-$DESIGNATE_B
source $DESIGNATE_PLUGINS/backend-$DESIGNATE_BACKEND_DRIVER source $DESIGNATE_PLUGINS/backend-$DESIGNATE_BACKEND_DRIVER
fi fi
# Helper Functions
# ----------------
function setup_colorized_logging_designate {
local conf_file=$1
local conf_section=$2
local project_var=${3:-"project_name"}
local user_var=${4:-"user_name"}
setup_colorized_logging $conf_file $conf_section $project_var $user_var
# Override the logging_context_format_string value chosen by
# setup_colorized_logging.
iniset $conf_file $conf_section logging_context_format_string "%(asctime)s.%(msecs)03d %(color)s%(levelname)s %(name)s [%(request_id)s %(user_identity)s%(color)s] %(instance)s%(color)s%(message)s"
}
# DevStack Plugin
# ---------------
# cleanup_designate - Remove residual data files, anything left over from previous # cleanup_designate - Remove residual data files, anything left over from previous
# runs that a clean run would need to clean up # runs that a clean run would need to clean up
function cleanup_designate { function cleanup_designate {
@ -115,7 +133,7 @@ function configure_designate {
# Format logging # Format logging
if [ "$LOG_COLOR" == "True" ] && [ "$SYSLOG" == "False" ]; then if [ "$LOG_COLOR" == "True" ] && [ "$SYSLOG" == "False" ]; then
setup_colorized_logging $DESIGNATE_CONF DEFAULT "tenant" "user" setup_colorized_logging_designate $DESIGNATE_CONF DEFAULT "tenant" "user"
fi fi
if is_service_enabled key; then if is_service_enabled key; then

29
contrib/dns_dump_raw.py Executable file
View File

@ -0,0 +1,29 @@
#!/usr/bin/env python
# Copyright 2014 Hewlett-Packard Development Company, L.P.
#
# Author: Kiall Mac Innes <kiall@hp.com>
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import sys
import binascii
import dns
import dns.message
wire = sys.argv[1]
# Prepare the Message
message = dns.message.from_wire(binascii.a2b_hex(wire))
# Print the test representation of the message
print(message.to_text())

View File

@ -43,7 +43,6 @@ class Service(service.DNSService, service.Service):
def _dns_application(self): def _dns_application(self):
# Create an instance of the RequestHandler class # Create an instance of the RequestHandler class
application = handler.RequestHandler() application = handler.RequestHandler()
application = dnsutils.ContextMiddleware(application)
application = dnsutils.SerializationMiddleware(application) application = dnsutils.SerializationMiddleware(application)
return application return application

View File

@ -34,12 +34,10 @@ class DesignateContext(context.RequestContext):
user_domain=None, project_domain=None, is_admin=False, user_domain=None, project_domain=None, is_admin=False,
read_only=False, show_deleted=False, request_id=None, read_only=False, show_deleted=False, request_id=None,
resource_uuid=None, overwrite=True, roles=None, resource_uuid=None, overwrite=True, roles=None,
service_catalog=None, all_tenants=False, user_identity=None, service_catalog=None, all_tenants=False, abandon=None,
abandon=None): tsigkey_id=None, user_identity=None):
# NOTE: user_identity may be passed in, but will be silently dropped as # NOTE: user_identity may be passed in, but will be silently dropped as
# it is a generated field based on several others. # it is a generated field based on several others.
roles = roles or []
super(DesignateContext, self).__init__( super(DesignateContext, self).__init__(
auth_token=auth_token, auth_token=auth_token,
user=user, user=user,
@ -54,8 +52,9 @@ class DesignateContext(context.RequestContext):
resource_uuid=resource_uuid, resource_uuid=resource_uuid,
overwrite=overwrite) overwrite=overwrite)
self.roles = roles self.roles = roles or []
self.service_catalog = service_catalog self.service_catalog = service_catalog
self.tsigkey_id = tsigkey_id
self.all_tenants = all_tenants self.all_tenants = all_tenants
self.abandon = abandon self.abandon = abandon
@ -68,11 +67,29 @@ class DesignateContext(context.RequestContext):
def to_dict(self): def to_dict(self):
d = super(DesignateContext, self).to_dict() d = super(DesignateContext, self).to_dict()
# Override the user_identity field to account for TSIG. When a TSIG key
# is used as authentication e.g. via MiniDNS, it will act as a form
# of "user",
user = self.user or '-'
if self.tsigkey_id and not self.user:
user = 'TSIG:%s' % self.tsigkey_id
user_idt = (
self.user_idt_format.format(user=user,
tenant=self.tenant or '-',
domain=self.domain or '-',
user_domain=self.user_domain or '-',
p_domain=self.project_domain or '-'))
# Update the dict with Designate specific extensions and overrides
d.update({ d.update({
'user_identity': user_idt,
'roles': self.roles, 'roles': self.roles,
'service_catalog': self.service_catalog, 'service_catalog': self.service_catalog,
'all_tenants': self.all_tenants, 'all_tenants': self.all_tenants,
'abandon': self.abandon, 'abandon': self.abandon,
'tsigkey_id': self.tsigkey_id
}) })
return copy.deepcopy(d) return copy.deepcopy(d)
@ -136,3 +153,7 @@ class DesignateContext(context.RequestContext):
if value: if value:
policy.check('abandon_domain', self) policy.check('abandon_domain', self)
self._abandon = value self._abandon = value
def get_current():
return context.get_current()

View File

@ -14,6 +14,7 @@
# License for the specific language governing permissions and limitations # License for the specific language governing permissions and limitations
# under the License. # under the License.
import socket import socket
import base64
import dns import dns
import dns.zone import dns.zone
@ -29,40 +30,6 @@ from designate.i18n import _LI
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
class SerializationMiddleware(object):
"""DNS Middleware to serialize/deserialize DNS Packets"""
def __init__(self, application):
self.application = application
def __call__(self, request):
try:
message = dns.message.from_wire(request['payload'])
# Create + Attach the initial "environ" dict. This is similar to
# the environ dict used in typical WSGI middleware.
message.environ = {'addr': request['addr']}
except dns.exception.DNSException:
LOG.error(_LE("Failed to deserialize packet from %(host)s:"
"%(port)d") % {'host': request['addr'][0],
'port': request['addr'][1]})
# We failed to deserialize the request, generate a failure
# response using a made up request.
response = dns.message.make_response(
dns.message.make_query('unknown', dns.rdatatype.A))
response.set_rcode(dns.rcode.FORMERR)
else:
# Hand the Deserialized packet on
response = self.application(message)
# Serialize and return the response if present
if response is not None:
return response.to_wire()
class DNSMiddleware(object): class DNSMiddleware(object):
"""Base DNS Middleware class with some utility methods""" """Base DNS Middleware class with some utility methods"""
def __init__(self, application): def __init__(self, application):
@ -90,21 +57,119 @@ class DNSMiddleware(object):
response = self.application(request) response = self.application(request)
return self.process_response(response) return self.process_response(response)
def _build_error_response(self):
response = dns.message.make_response(
dns.message.make_query('unknown', dns.rdatatype.A))
response.set_rcode(dns.rcode.FORMERR)
class ContextMiddleware(DNSMiddleware): return response
"""Temporary ContextMiddleware which attaches an admin context to every
request
This will be replaced with a piece of middleware which generates, from
a TSIG signed request, an appropriate Request Context. class SerializationMiddleware(DNSMiddleware):
""" """DNS Middleware to serialize/deserialize DNS Packets"""
def process_request(self, request):
def __init__(self, application, tsig_keyring=None):
self.application = application
self.tsig_keyring = tsig_keyring
def __call__(self, request):
# Generate the initial context. This may be updated by other middleware
# as we learn more information about the Request.
ctxt = context.DesignateContext.get_admin_context(all_tenants=True) ctxt = context.DesignateContext.get_admin_context(all_tenants=True)
request.environ['context'] = ctxt
try:
message = dns.message.from_wire(request['payload'],
self.tsig_keyring)
if message.had_tsig:
LOG.debug('Request signed with TSIG key: %s', message.keyname)
# Create + Attach the initial "environ" dict. This is similar to
# the environ dict used in typical WSGI middleware.
message.environ = {
'context': ctxt,
'addr': request['addr'],
}
except dns.message.UnknownTSIGKey:
LOG.error(_LE("Unknown TSIG key from %(host)s:"
"%(port)d") % {'host': request['addr'][0],
'port': request['addr'][1]})
response = self._build_error_response()
except dns.tsig.BadSignature:
LOG.error(_LE("Invalid TSIG signature from %(host)s:"
"%(port)d") % {'host': request['addr'][0],
'port': request['addr'][1]})
response = self._build_error_response()
except dns.exception.DNSException:
LOG.error(_LE("Failed to deserialize packet from %(host)s:"
"%(port)d") % {'host': request['addr'][0],
'port': request['addr'][1]})
response = self._build_error_response()
else:
# Hand the Deserialized packet onto the Application
response = self.application(message)
# Serialize and return the response if present
if response is not None:
return response.to_wire()
class TsigInfoMiddleware(DNSMiddleware):
"""Middleware which looks up the information available for a TsigKey"""
def __init__(self, application, storage):
super(TsigInfoMiddleware, self).__init__(application)
self.storage = storage
def process_request(self, request):
if not request.had_tsig:
return None
try:
criterion = {'name': request.keyname.to_text(True)}
tsigkey = self.storage.find_tsigkey(
context.get_current(), criterion)
request.environ['tsigkey'] = tsigkey
request.environ['context'].tsigkey_id = tsigkey.id
except exceptions.TsigKeyNotFound:
# This should never happen, as we just validated the key.. Except
# for race conditions..
return self._build_error_response()
return None return None
class TsigKeyring(object):
"""Implements the DNSPython KeyRing API, backed by the Designate DB"""
def __init__(self, storage):
self.storage = storage
def __getitem__(self, key):
return self.get(key)
def get(self, key, default=None):
try:
criterion = {'name': key.to_text(True)}
tsigkey = self.storage.find_tsigkey(
context.get_current(), criterion)
return base64.decodestring(tsigkey.secret)
except exceptions.TsigKeyNotFound:
return default
def from_dnspython_zone(dnspython_zone): def from_dnspython_zone(dnspython_zone):
# dnspython never builds a zone with more than one SOA, even if we give # dnspython never builds a zone with more than one SOA, even if we give
# it a zonefile that contains more than one # it a zonefile that contains more than one

View File

@ -32,6 +32,9 @@ OPTS = [
help='mDNS TCP Receive Timeout'), help='mDNS TCP Receive Timeout'),
cfg.BoolOpt('all-tcp', default=False, cfg.BoolOpt('all-tcp', default=False,
help='Send all traffic over TCP'), help='Send all traffic over TCP'),
cfg.BoolOpt('query-enforce-tsig', default=False,
help='Enforce all incoming queries (including AXFR) are TSIG '
'signed'),
cfg.StrOpt('storage-driver', default='sqlalchemy', cfg.StrOpt('storage-driver', default='sqlalchemy',
help='The storage driver to use'), help='The storage driver to use'),
] ]

View File

@ -24,27 +24,28 @@ from oslo.config import cfg
from oslo_log import log as logging from oslo_log import log as logging
from designate import exceptions from designate import exceptions
from designate import storage from designate.i18n import _LW
from designate.i18n import _LE
LOG = logging.getLogger(__name__) LOG = logging.getLogger(__name__)
CONF = cfg.CONF CONF = cfg.CONF
CONF.import_opt('default_pool_id', 'designate.central',
group='service:central')
class RequestHandler(object): class RequestHandler(object):
def __init__(self): """MiniDNS Request Handler"""
# Get a storage connection # TODO(kiall): This class is getting a little unwieldy, we should rework
storage_driver = cfg.CONF['service:mdns'].storage_driver # with a little more structure.
self.storage = storage.get_storage(storage_driver) def __init__(self, storage):
self.storage = storage
def __call__(self, request): def __call__(self, request):
""" """
:param request: DNS Request Message :param request: DNS Request Message
:return: DNS Response Message :return: DNS Response Message
""" """
context = request.environ['context']
if request.opcode() == dns.opcode.QUERY: if request.opcode() == dns.opcode.QUERY:
# Currently we expect exactly 1 question in the section # Currently we expect exactly 1 question in the section
# TSIG places the pseudo records into the additional section. # TSIG places the pseudo records into the additional section.
@ -58,9 +59,9 @@ class RequestHandler(object):
# receiving an IXFR request. # receiving an IXFR request.
# TODO(Ron): send IXFR response when receiving IXFR request. # TODO(Ron): send IXFR response when receiving IXFR request.
if q_rrset.rdtype in (dns.rdatatype.AXFR, dns.rdatatype.IXFR): if q_rrset.rdtype in (dns.rdatatype.AXFR, dns.rdatatype.IXFR):
response = self._handle_axfr(context, request) response = self._handle_axfr(request)
else: else:
response = self._handle_record_query(context, request) response = self._handle_record_query(request)
else: else:
# Unhandled OpCode's include STATUS, IQUERY, NOTIFY, UPDATE # Unhandled OpCode's include STATUS, IQUERY, NOTIFY, UPDATE
response = self._handle_query_error(request, dns.rcode.REFUSED) response = self._handle_query_error(request, dns.rcode.REFUSED)
@ -79,18 +80,39 @@ class RequestHandler(object):
return response return response
def _convert_to_rrset(self, context, recordset, domain=None): def _domain_criterion_from_request(self, request, criterion=None):
"""Builds a bare criterion dict based on the request attributes"""
criterion = criterion or {}
tsigkey = request.environ.get('tsigkey')
if tsigkey is None and CONF['service:mdns'].query_enforce_tsig:
raise exceptions.Forbidden('Request is not TSIG signed')
elif tsigkey is None:
# Default to using the default_pool_id when no TSIG key is
# available
criterion['pool_id'] = CONF['service:central'].default_pool_id
else:
if tsigkey.scope == 'POOL':
criterion['pool_id'] = tsigkey.resource_id
elif tsigkey.scope == 'ZONE':
criterion['id'] = tsigkey.resource_id
else:
raise NotImplementedError("Support for %s scoped TSIG Keys is "
"not implemented")
return criterion
def _convert_to_rrset(self, domain, recordset):
# Fetch the domain or the config ttl if the recordset ttl is null # Fetch the domain or the config ttl if the recordset ttl is null
if recordset.ttl: if recordset.ttl:
ttl = recordset.ttl ttl = recordset.ttl
elif domain is not None:
ttl = domain.ttl
else: else:
domain = self.storage.get_domain(context, recordset.domain_id)
if domain.ttl:
ttl = domain.ttl ttl = domain.ttl
else:
ttl = CONF.default_ttl
# construct rdata from all the records # construct rdata from all the records
rdata = [] rdata = []
@ -113,18 +135,28 @@ class RequestHandler(object):
return r_rrset return r_rrset
def _handle_axfr(self, context, request): def _handle_axfr(self, request):
context = request.environ['context']
response = dns.message.make_response(request) response = dns.message.make_response(request)
q_rrset = request.question[0] q_rrset = request.question[0]
# First check if there is an existing zone # First check if there is an existing zone
# TODO(vinod) once validation is separated from the api, # TODO(vinod) once validation is separated from the api,
# validate the parameters # validate the parameters
criterion = {'name': q_rrset.name.to_text()}
try: try:
criterion = self._domain_criterion_from_request(
request, {'name': q_rrset.name.to_text()})
domain = self.storage.find_domain(context, criterion) domain = self.storage.find_domain(context, criterion)
except exceptions.DomainNotFound: except exceptions.DomainNotFound:
LOG.exception(_LE("got exception while handling axfr request. " LOG.warning(_LW("DomainNotFound while handling axfr request. "
"Question is %(qr)s") % {'qr': q_rrset}) "Question was %(qr)s") % {'qr': q_rrset})
return self._handle_query_error(request, dns.rcode.REFUSED)
except exceptions.Forbidden:
LOG.warning(_LW("Forbidden while handling axfr request. "
"Question was %(qr)s") % {'qr': q_rrset})
return self._handle_query_error(request, dns.rcode.REFUSED) return self._handle_query_error(request, dns.rcode.REFUSED)
@ -135,20 +167,20 @@ class RequestHandler(object):
soa_recordsets = self.storage.find_recordsets(context, criterion) soa_recordsets = self.storage.find_recordsets(context, criterion)
for recordset in soa_recordsets: for recordset in soa_recordsets:
r_rrsets.append(self._convert_to_rrset(context, recordset, domain)) r_rrsets.append(self._convert_to_rrset(domain, recordset))
# Get all the recordsets other than SOA # Get all the recordsets other than SOA
criterion = {'domain_id': domain.id, 'type': '!SOA'} criterion = {'domain_id': domain.id, 'type': '!SOA'}
recordsets = self.storage.find_recordsets(context, criterion) recordsets = self.storage.find_recordsets(context, criterion)
for recordset in recordsets: for recordset in recordsets:
r_rrset = self._convert_to_rrset(context, recordset, domain) r_rrset = self._convert_to_rrset(domain, recordset)
if r_rrset: if r_rrset:
r_rrsets.append(r_rrset) r_rrsets.append(r_rrset)
# Append the SOA recordset at the end # Append the SOA recordset at the end
for recordset in soa_recordsets: for recordset in soa_recordsets:
r_rrsets.append(self._convert_to_rrset(context, recordset, domain)) r_rrsets.append(self._convert_to_rrset(domain, recordset))
response.set_rcode(dns.rcode.NOERROR) response.set_rcode(dns.rcode.NOERROR)
# TODO(vinod) check if we dnspython has an upper limit on the number # TODO(vinod) check if we dnspython has an upper limit on the number
@ -159,9 +191,11 @@ class RequestHandler(object):
return response return response
def _handle_record_query(self, context, request): def _handle_record_query(self, request):
"""Handle a DNS QUERY request for a record""" """Handle a DNS QUERY request for a record"""
context = request.environ['context']
response = dns.message.make_response(request) response = dns.message.make_response(request)
try: try:
q_rrset = request.question[0] q_rrset = request.question[0]
# TODO(vinod) once validation is separated from the api, # TODO(vinod) once validation is separated from the api,
@ -172,11 +206,30 @@ class RequestHandler(object):
'domains_deleted': False 'domains_deleted': False
} }
recordset = self.storage.find_recordset(context, criterion) recordset = self.storage.find_recordset(context, criterion)
r_rrset = self._convert_to_rrset(context, recordset)
try:
criterion = self._domain_criterion_from_request(
request, {'id': recordset.domain_id})
domain = self.storage.find_domain(context, criterion)
except exceptions.DomainNotFound:
LOG.warning(_LW("DomainNotFound while handling query request"
". Question was %(qr)s") % {'qr': q_rrset})
return self._handle_query_error(request, dns.rcode.REFUSED)
except exceptions.Forbidden:
LOG.warning(_LW("Forbidden while handling query request. "
"Question was %(qr)s") % {'qr': q_rrset})
return self._handle_query_error(request, dns.rcode.REFUSED)
r_rrset = self._convert_to_rrset(domain, recordset)
response.set_rcode(dns.rcode.NOERROR) response.set_rcode(dns.rcode.NOERROR)
response.answer = [r_rrset] response.answer = [r_rrset]
# For all the data stored in designate mdns is Authoritative # For all the data stored in designate mdns is Authoritative
response.flags |= dns.flags.AA response.flags |= dns.flags.AA
except exceptions.NotFound: except exceptions.NotFound:
# If an FQDN exists, like www.rackspace.com, but the specific # If an FQDN exists, like www.rackspace.com, but the specific
# record type doesn't exist, like type SPF, then the return code # record type doesn't exist, like type SPF, then the return code
@ -196,4 +249,7 @@ class RequestHandler(object):
# If zone transfers needs different errors, we could revisit this. # If zone transfers needs different errors, we could revisit this.
response.set_rcode(dns.rcode.REFUSED) response.set_rcode(dns.rcode.REFUSED)
except exceptions.Forbidden:
response.set_rcode(dns.rcode.REFUSED)
return response return response

View File

@ -18,6 +18,7 @@ from oslo_log import log as logging
from designate import utils from designate import utils
from designate import service from designate import service
from designate import storage
from designate import dnsutils from designate import dnsutils
from designate.mdns import handler from designate.mdns import handler
from designate.mdns import notify from designate.mdns import notify
@ -30,6 +31,9 @@ class Service(service.DNSService, service.RPCService, service.Service):
def __init__(self, threads=None): def __init__(self, threads=None):
super(Service, self).__init__(threads=threads) super(Service, self).__init__(threads=threads)
# Get a storage connection
self.storage = storage.get_storage(CONF['service:mdns'].storage_driver)
@property @property
def service_name(self): def service_name(self):
return 'mdns' return 'mdns'
@ -42,9 +46,11 @@ class Service(service.DNSService, service.RPCService, service.Service):
@property @property
@utils.cache_result @utils.cache_result
def _dns_application(self): def _dns_application(self):
# Create an instance of the RequestHandler class # Create an instance of the RequestHandler class and wrap with
application = handler.RequestHandler() # necessary middleware.
application = dnsutils.ContextMiddleware(application) application = handler.RequestHandler(self.storage)
application = dnsutils.SerializationMiddleware(application) application = dnsutils.TsigInfoMiddleware(application, self.storage)
application = dnsutils.SerializationMiddleware(
application, dnsutils.TsigKeyring(self.storage))
return application return application

View File

@ -16,20 +16,42 @@
import binascii import binascii
import dns import dns
from oslo.config import cfg
from designate import context from designate import context
from designate.tests.test_mdns import MdnsTestCase from designate.tests.test_mdns import MdnsTestCase
from designate.mdns import handler from designate.mdns import handler
CONF = cfg.CONF
default_pool_id = CONF['service:central'].default_pool_id
class MdnsRequestHandlerTest(MdnsTestCase): class MdnsRequestHandlerTest(MdnsTestCase):
def setUp(self): def setUp(self):
super(MdnsRequestHandlerTest, self).setUp() super(MdnsRequestHandlerTest, self).setUp()
self.handler = handler.RequestHandler() self.handler = handler.RequestHandler(self.storage)
self.addr = ["0.0.0.0", 5556] self.addr = ["0.0.0.0", 5556]
self.context = context.DesignateContext.get_admin_context( self.context = context.DesignateContext.get_admin_context(
all_tenants=True) all_tenants=True)
# Create a TSIG Key for the default pool, and another for some other
# pool.
self.tsigkey_pool_default = self.create_tsigkey(
name='default-pool',
scope='POOL',
resource_id=default_pool_id)
self.tsigkey_pool_unknown = self.create_tsigkey(
name='unknown-pool',
scope='POOL',
resource_id='628e55a0-c724-4767-8c59-0a61c15d3444')
self.tsigkey_zone_unknown = self.create_tsigkey(
name='unknown-zone',
scope='ZONE',
resource_id='82fd08be-9eb7-4d94-8267-a26f8348671d')
def test_dispatch_opcode_iquery(self): def test_dispatch_opcode_iquery(self):
# DNS packet with IQUERY opcode # DNS packet with IQUERY opcode
payload = "271109000001000000000000076578616d706c6503636f6d0000010001" payload = "271109000001000000000000076578616d706c6503636f6d0000010001"
@ -284,3 +306,129 @@ class MdnsRequestHandlerTest(MdnsTestCase):
response = self.handler(request).to_wire() response = self.handler(request).to_wire()
self.assertEqual(expected_response, binascii.b2a_hex(response)) self.assertEqual(expected_response, binascii.b2a_hex(response))
def test_dispatch_opcode_query_tsig_scope_pool(self):
# Create a domain/recordset/record to query
domain = self.create_domain(name='example.com.')
recordset = self.create_recordset(
domain, name='example.com.', type='A')
self.create_record(
domain, recordset, data='192.0.2.5')
# DNS packet with QUERY opcode for A example.com.
payload = ("c28901200001000000000001076578616d706c6503636f6d0000010001"
"0000291000000000000000")
request = dns.message.from_wire(binascii.a2b_hex(payload))
request.environ = {
'addr': self.addr,
'context': self.context,
'tsigkey': self.tsigkey_pool_default,
}
# Ensure the Query, with the correct pool's TSIG, gives a NOERROR.
# id 49801
# opcode QUERY
# rcode NOERROR
# flags QR AA RD
# edns 0
# payload 8192
# ;QUESTION
# example.com. IN A
# ;ANSWER
# example.com. 3600 IN A 192.0.2.5
# ;AUTHORITY
# ;ADDITIONAL
expected_response = ("c28985000001000100000001076578616d706c6503636f6d"
"0000010001c00c0001000100000e100004c0000205000029"
"2000000000000000")
response = self.handler(request).to_wire()
self.assertEqual(expected_response, binascii.b2a_hex(response))
# Ensure the Query, with the incorrect pool's TSIG, gives a REFUSED
request.environ['tsigkey'] = self.tsigkey_pool_unknown
# id 49801
# opcode QUERY
# rcode REFUSED
# flags QR RD
# edns 0
# payload 8192
# ;QUESTION
# example.com. IN A
# ;ANSWER
# ;AUTHORITY
# ;ADDITIONAL
expected_response = ("c28981050001000000000001076578616d706c6503636f6d"
"00000100010000292000000000000000")
response = self.handler(request).to_wire()
self.assertEqual(expected_response, binascii.b2a_hex(response))
def test_dispatch_opcode_query_tsig_scope_zone(self):
# Create a domain/recordset/record to query
domain = self.create_domain(name='example.com.')
recordset = self.create_recordset(
domain, name='example.com.', type='A')
self.create_record(
domain, recordset, data='192.0.2.5')
# Create a TSIG Key Matching the zone
tsigkey_zone_known = self.create_tsigkey(
name='known-zone',
scope='ZONE',
resource_id=domain.id)
# DNS packet with QUERY opcode for A example.com.
payload = ("c28901200001000000000001076578616d706c6503636f6d0000010001"
"0000291000000000000000")
request = dns.message.from_wire(binascii.a2b_hex(payload))
request.environ = {
'addr': self.addr,
'context': self.context,
'tsigkey': tsigkey_zone_known,
}
# Ensure the Query, with the correct zone's TSIG, gives a NOERROR.
# id 49801
# opcode QUERY
# rcode NOERROR
# flags QR AA RD
# edns 0
# payload 8192
# ;QUESTION
# example.com. IN A
# ;ANSWER
# example.com. 3600 IN A 192.0.2.5
# ;AUTHORITY
# ;ADDITIONAL
expected_response = ("c28985000001000100000001076578616d706c6503636f6d"
"0000010001c00c0001000100000e100004c0000205000029"
"2000000000000000")
response = self.handler(request).to_wire()
self.assertEqual(expected_response, binascii.b2a_hex(response))
# Ensure the Query, with the incorrect zone's TSIG, gives a REFUSED
request.environ['tsigkey'] = self.tsigkey_zone_unknown
# id 49801
# opcode QUERY
# rcode REFUSED
# flags QR RD
# edns 0
# payload 8192
# ;QUESTION
# example.com. IN A
# ;ANSWER
# ;AUTHORITY
# ;ADDITIONAL
expected_response = ("c28981050001000000000001076578616d706c6503636f6d"
"00000100010000292000000000000000")
response = self.handler(request).to_wire()
self.assertEqual(expected_response, binascii.b2a_hex(response))