Merge "Implement TSIG Support in mDNS"
This commit is contained in:
commit
963e4d0151
@ -67,6 +67,24 @@ if is_service_enabled designate && [[ -r $DESIGNATE_PLUGINS/backend-$DESIGNATE_B
|
||||
source $DESIGNATE_PLUGINS/backend-$DESIGNATE_BACKEND_DRIVER
|
||||
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 [[01;36m%(request_id)s [00;36m%(user_identity)s%(color)s] [01;35m%(instance)s%(color)s%(message)s[00m"
|
||||
}
|
||||
|
||||
# DevStack Plugin
|
||||
# ---------------
|
||||
|
||||
# cleanup_designate - Remove residual data files, anything left over from previous
|
||||
# runs that a clean run would need to clean up
|
||||
function cleanup_designate {
|
||||
@ -115,7 +133,7 @@ function configure_designate {
|
||||
|
||||
# Format logging
|
||||
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
|
||||
|
||||
if is_service_enabled key; then
|
||||
|
29
contrib/dns_dump_raw.py
Executable file
29
contrib/dns_dump_raw.py
Executable 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())
|
@ -43,7 +43,6 @@ class Service(service.DNSService, service.Service):
|
||||
def _dns_application(self):
|
||||
# Create an instance of the RequestHandler class
|
||||
application = handler.RequestHandler()
|
||||
application = dnsutils.ContextMiddleware(application)
|
||||
application = dnsutils.SerializationMiddleware(application)
|
||||
|
||||
return application
|
||||
|
@ -34,12 +34,10 @@ class DesignateContext(context.RequestContext):
|
||||
user_domain=None, project_domain=None, is_admin=False,
|
||||
read_only=False, show_deleted=False, request_id=None,
|
||||
resource_uuid=None, overwrite=True, roles=None,
|
||||
service_catalog=None, all_tenants=False, user_identity=None,
|
||||
abandon=None):
|
||||
service_catalog=None, all_tenants=False, abandon=None,
|
||||
tsigkey_id=None, user_identity=None):
|
||||
# NOTE: user_identity may be passed in, but will be silently dropped as
|
||||
# it is a generated field based on several others.
|
||||
|
||||
roles = roles or []
|
||||
super(DesignateContext, self).__init__(
|
||||
auth_token=auth_token,
|
||||
user=user,
|
||||
@ -54,8 +52,9 @@ class DesignateContext(context.RequestContext):
|
||||
resource_uuid=resource_uuid,
|
||||
overwrite=overwrite)
|
||||
|
||||
self.roles = roles
|
||||
self.roles = roles or []
|
||||
self.service_catalog = service_catalog
|
||||
self.tsigkey_id = tsigkey_id
|
||||
|
||||
self.all_tenants = all_tenants
|
||||
self.abandon = abandon
|
||||
@ -68,11 +67,29 @@ class DesignateContext(context.RequestContext):
|
||||
def to_dict(self):
|
||||
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({
|
||||
'user_identity': user_idt,
|
||||
'roles': self.roles,
|
||||
'service_catalog': self.service_catalog,
|
||||
'all_tenants': self.all_tenants,
|
||||
'abandon': self.abandon,
|
||||
'tsigkey_id': self.tsigkey_id
|
||||
})
|
||||
|
||||
return copy.deepcopy(d)
|
||||
@ -136,3 +153,7 @@ class DesignateContext(context.RequestContext):
|
||||
if value:
|
||||
policy.check('abandon_domain', self)
|
||||
self._abandon = value
|
||||
|
||||
|
||||
def get_current():
|
||||
return context.get_current()
|
||||
|
@ -14,6 +14,7 @@
|
||||
# License for the specific language governing permissions and limitations
|
||||
# under the License.
|
||||
import socket
|
||||
import base64
|
||||
|
||||
import dns
|
||||
import dns.zone
|
||||
@ -29,40 +30,6 @@ from designate.i18n import _LI
|
||||
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):
|
||||
"""Base DNS Middleware class with some utility methods"""
|
||||
def __init__(self, application):
|
||||
@ -90,21 +57,119 @@ class DNSMiddleware(object):
|
||||
response = self.application(request)
|
||||
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):
|
||||
"""Temporary ContextMiddleware which attaches an admin context to every
|
||||
request
|
||||
return response
|
||||
|
||||
This will be replaced with a piece of middleware which generates, from
|
||||
a TSIG signed request, an appropriate Request Context.
|
||||
"""
|
||||
def process_request(self, request):
|
||||
|
||||
class SerializationMiddleware(DNSMiddleware):
|
||||
"""DNS Middleware to serialize/deserialize DNS Packets"""
|
||||
|
||||
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)
|
||||
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
|
||||
|
||||
|
||||
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):
|
||||
# dnspython never builds a zone with more than one SOA, even if we give
|
||||
# it a zonefile that contains more than one
|
||||
|
@ -32,6 +32,9 @@ OPTS = [
|
||||
help='mDNS TCP Receive Timeout'),
|
||||
cfg.BoolOpt('all-tcp', default=False,
|
||||
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',
|
||||
help='The storage driver to use'),
|
||||
]
|
||||
|
@ -24,27 +24,28 @@ from oslo.config import cfg
|
||||
from oslo_log import log as logging
|
||||
|
||||
from designate import exceptions
|
||||
from designate import storage
|
||||
from designate.i18n import _LE
|
||||
from designate.i18n import _LW
|
||||
|
||||
|
||||
LOG = logging.getLogger(__name__)
|
||||
CONF = cfg.CONF
|
||||
|
||||
CONF.import_opt('default_pool_id', 'designate.central',
|
||||
group='service:central')
|
||||
|
||||
|
||||
class RequestHandler(object):
|
||||
def __init__(self):
|
||||
# Get a storage connection
|
||||
storage_driver = cfg.CONF['service:mdns'].storage_driver
|
||||
self.storage = storage.get_storage(storage_driver)
|
||||
"""MiniDNS Request Handler"""
|
||||
# TODO(kiall): This class is getting a little unwieldy, we should rework
|
||||
# with a little more structure.
|
||||
def __init__(self, storage):
|
||||
self.storage = storage
|
||||
|
||||
def __call__(self, request):
|
||||
"""
|
||||
:param request: DNS Request Message
|
||||
:return: DNS Response Message
|
||||
"""
|
||||
context = request.environ['context']
|
||||
|
||||
if request.opcode() == dns.opcode.QUERY:
|
||||
# Currently we expect exactly 1 question in the section
|
||||
# TSIG places the pseudo records into the additional section.
|
||||
@ -58,9 +59,9 @@ class RequestHandler(object):
|
||||
# receiving an IXFR request.
|
||||
# TODO(Ron): send IXFR response when receiving IXFR request.
|
||||
if q_rrset.rdtype in (dns.rdatatype.AXFR, dns.rdatatype.IXFR):
|
||||
response = self._handle_axfr(context, request)
|
||||
response = self._handle_axfr(request)
|
||||
else:
|
||||
response = self._handle_record_query(context, request)
|
||||
response = self._handle_record_query(request)
|
||||
else:
|
||||
# Unhandled OpCode's include STATUS, IQUERY, NOTIFY, UPDATE
|
||||
response = self._handle_query_error(request, dns.rcode.REFUSED)
|
||||
@ -79,18 +80,39 @@ class RequestHandler(object):
|
||||
|
||||
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
|
||||
if recordset.ttl:
|
||||
ttl = recordset.ttl
|
||||
elif domain is not None:
|
||||
ttl = domain.ttl
|
||||
else:
|
||||
domain = self.storage.get_domain(context, recordset.domain_id)
|
||||
if domain.ttl:
|
||||
ttl = domain.ttl
|
||||
else:
|
||||
ttl = CONF.default_ttl
|
||||
|
||||
# construct rdata from all the records
|
||||
rdata = []
|
||||
@ -113,18 +135,28 @@ class RequestHandler(object):
|
||||
|
||||
return r_rrset
|
||||
|
||||
def _handle_axfr(self, context, request):
|
||||
def _handle_axfr(self, request):
|
||||
context = request.environ['context']
|
||||
|
||||
response = dns.message.make_response(request)
|
||||
q_rrset = request.question[0]
|
||||
# First check if there is an existing zone
|
||||
# TODO(vinod) once validation is separated from the api,
|
||||
# validate the parameters
|
||||
criterion = {'name': q_rrset.name.to_text()}
|
||||
try:
|
||||
criterion = self._domain_criterion_from_request(
|
||||
request, {'name': q_rrset.name.to_text()})
|
||||
domain = self.storage.find_domain(context, criterion)
|
||||
|
||||
except exceptions.DomainNotFound:
|
||||
LOG.exception(_LE("got exception while handling axfr request. "
|
||||
"Question is %(qr)s") % {'qr': q_rrset})
|
||||
LOG.warning(_LW("DomainNotFound while handling axfr 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 axfr request. "
|
||||
"Question was %(qr)s") % {'qr': q_rrset})
|
||||
|
||||
return self._handle_query_error(request, dns.rcode.REFUSED)
|
||||
|
||||
@ -135,20 +167,20 @@ class RequestHandler(object):
|
||||
soa_recordsets = self.storage.find_recordsets(context, criterion)
|
||||
|
||||
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
|
||||
criterion = {'domain_id': domain.id, 'type': '!SOA'}
|
||||
recordsets = self.storage.find_recordsets(context, criterion)
|
||||
|
||||
for recordset in recordsets:
|
||||
r_rrset = self._convert_to_rrset(context, recordset, domain)
|
||||
r_rrset = self._convert_to_rrset(domain, recordset)
|
||||
if r_rrset:
|
||||
r_rrsets.append(r_rrset)
|
||||
|
||||
# Append the SOA recordset at the end
|
||||
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)
|
||||
# TODO(vinod) check if we dnspython has an upper limit on the number
|
||||
@ -159,9 +191,11 @@ class RequestHandler(object):
|
||||
|
||||
return response
|
||||
|
||||
def _handle_record_query(self, context, request):
|
||||
def _handle_record_query(self, request):
|
||||
"""Handle a DNS QUERY request for a record"""
|
||||
context = request.environ['context']
|
||||
response = dns.message.make_response(request)
|
||||
|
||||
try:
|
||||
q_rrset = request.question[0]
|
||||
# TODO(vinod) once validation is separated from the api,
|
||||
@ -172,11 +206,30 @@ class RequestHandler(object):
|
||||
'domains_deleted': False
|
||||
}
|
||||
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.answer = [r_rrset]
|
||||
# For all the data stored in designate mdns is Authoritative
|
||||
response.flags |= dns.flags.AA
|
||||
|
||||
except exceptions.NotFound:
|
||||
# If an FQDN exists, like www.rackspace.com, but the specific
|
||||
# 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.
|
||||
response.set_rcode(dns.rcode.REFUSED)
|
||||
|
||||
except exceptions.Forbidden:
|
||||
response.set_rcode(dns.rcode.REFUSED)
|
||||
|
||||
return response
|
||||
|
@ -18,6 +18,7 @@ from oslo_log import log as logging
|
||||
|
||||
from designate import utils
|
||||
from designate import service
|
||||
from designate import storage
|
||||
from designate import dnsutils
|
||||
from designate.mdns import handler
|
||||
from designate.mdns import notify
|
||||
@ -30,6 +31,9 @@ class Service(service.DNSService, service.RPCService, service.Service):
|
||||
def __init__(self, threads=None):
|
||||
super(Service, self).__init__(threads=threads)
|
||||
|
||||
# Get a storage connection
|
||||
self.storage = storage.get_storage(CONF['service:mdns'].storage_driver)
|
||||
|
||||
@property
|
||||
def service_name(self):
|
||||
return 'mdns'
|
||||
@ -42,9 +46,11 @@ class Service(service.DNSService, service.RPCService, service.Service):
|
||||
@property
|
||||
@utils.cache_result
|
||||
def _dns_application(self):
|
||||
# Create an instance of the RequestHandler class
|
||||
application = handler.RequestHandler()
|
||||
application = dnsutils.ContextMiddleware(application)
|
||||
application = dnsutils.SerializationMiddleware(application)
|
||||
# Create an instance of the RequestHandler class and wrap with
|
||||
# necessary middleware.
|
||||
application = handler.RequestHandler(self.storage)
|
||||
application = dnsutils.TsigInfoMiddleware(application, self.storage)
|
||||
application = dnsutils.SerializationMiddleware(
|
||||
application, dnsutils.TsigKeyring(self.storage))
|
||||
|
||||
return application
|
||||
|
@ -16,20 +16,42 @@
|
||||
import binascii
|
||||
|
||||
import dns
|
||||
from oslo.config import cfg
|
||||
|
||||
from designate import context
|
||||
from designate.tests.test_mdns import MdnsTestCase
|
||||
from designate.mdns import handler
|
||||
|
||||
CONF = cfg.CONF
|
||||
default_pool_id = CONF['service:central'].default_pool_id
|
||||
|
||||
|
||||
class MdnsRequestHandlerTest(MdnsTestCase):
|
||||
def setUp(self):
|
||||
super(MdnsRequestHandlerTest, self).setUp()
|
||||
self.handler = handler.RequestHandler()
|
||||
self.handler = handler.RequestHandler(self.storage)
|
||||
self.addr = ["0.0.0.0", 5556]
|
||||
|
||||
self.context = context.DesignateContext.get_admin_context(
|
||||
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):
|
||||
# DNS packet with IQUERY opcode
|
||||
payload = "271109000001000000000000076578616d706c6503636f6d0000010001"
|
||||
@ -284,3 +306,129 @@ class MdnsRequestHandlerTest(MdnsTestCase):
|
||||
response = self.handler(request).to_wire()
|
||||
|
||||
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))
|
||||
|
Loading…
Reference in New Issue
Block a user