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
|
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 [[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
|
# 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
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):
|
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
|
||||||
|
@ -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()
|
||||||
|
@ -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
|
||||||
|
@ -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'),
|
||||||
]
|
]
|
||||||
|
@ -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
|
||||||
|
@ -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
|
||||||
|
@ -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))
|
||||||
|
Loading…
Reference in New Issue
Block a user