deb-designate/designate/mdns/handler.py
Doug Hellmann 1ff8469ef7 Drop use of 'oslo' namespace package
The Oslo libraries have moved all of their code out of the 'oslo'
namespace package into per-library packages. The namespace package was
retained during kilo for backwards compatibility, but will be removed by
the liberty-2 milestone. This change removes the use of the namespace
package, replacing it with the new package names.

The patches in the libraries will be put on hold until application
patches have landed, or L2, whichever comes first. At that point, new
versions of the libraries without namespace packages will be released as
a major version update.

Please merge this patch, or an equivalent, before L2 to avoid problems
with those library releases.

Blueprint: remove-namespace-packages
https://blueprints.launchpad.net/oslo-incubator/+spec/remove-namespace-packages

Change-Id: Iae62b48993eef3b31420f8cc245a55f5e303c4fc
2015-04-28 18:32:15 +00:00

434 lines
16 KiB
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 dns
import dns.flags
import dns.opcode
import dns.rcode
import dns.rdataclass
import dns.rdatatype
import dns.message
from oslo_config import cfg
from oslo_log import log as logging
from designate import exceptions
from designate.mdns import xfr
from designate.central import rpcapi as central_api
from designate.i18n import _LI
from designate.i18n import _LW
LOG = logging.getLogger(__name__)
CONF = cfg.CONF
CONF.import_opt('default_pool_id', 'designate.central',
group='service:central')
# 10 Bytes of RR metadata, 64 bytes of TSIG RR data, variable length TSIG Key
# name (restricted in designate to 160 chars), 1 byte for trailing dot.
TSIG_RRSIZE = 10 + 64 + 160 + 1
class RequestHandler(xfr.XFRMixin):
def __init__(self, storage, tg):
# Get a storage connection
self.storage = storage
self.tg = tg
@property
def central_api(self):
return central_api.CentralAPI.get_instance()
def __call__(self, request):
"""
:param request: DNS Request Message
:return: DNS Response Message
"""
if request.opcode() == dns.opcode.QUERY:
# Currently we expect exactly 1 question in the section
# TSIG places the pseudo records into the additional section.
if (len(request.question) != 1 or
request.question[0].rdclass != dns.rdataclass.IN):
yield self._handle_query_error(request, dns.rcode.REFUSED)
raise StopIteration
q_rrset = request.question[0]
# Handle AXFR and IXFR requests with an AXFR responses for now.
# It is permissible for a server to send an AXFR response when
# receiving an IXFR request.
# TODO(Ron): send IXFR response when receiving IXFR request.
if q_rrset.rdtype in (dns.rdatatype.AXFR, dns.rdatatype.IXFR):
for response in self._handle_axfr(request):
yield response
raise StopIteration
else:
for response in self._handle_record_query(request):
yield response
raise StopIteration
elif request.opcode() == dns.opcode.NOTIFY:
for response in self._handle_notify(request):
yield response
raise StopIteration
else:
# Unhandled OpCode's include STATUS, IQUERY, NOTIFY, UPDATE
yield self._handle_query_error(request, dns.rcode.REFUSED)
raise StopIteration
def _handle_notify(self, request):
"""
Constructs the response to a NOTIFY and acts accordingly on it.
* Checks if the master sending the NOTIFY is in the Zone's masters,
if not it is ignored.
* Checks if SOA query response serial != local serial.
"""
context = request.environ['context']
response = dns.message.make_response(request)
if len(request.question) != 1:
response.set_rcode(dns.rcode.FORMERR)
yield response
raise StopIteration
else:
question = request.question[0]
criterion = {
'name': question.name.to_text(),
'type': 'SECONDARY',
'deleted': False
}
try:
domain = self.storage.find_domain(context, criterion)
except exceptions.DomainNotFound:
response.set_rcode(dns.rcode.NOTAUTH)
yield response
raise StopIteration
notify_addr = request.environ['addr'][0]
# We check if the src_master which is the assumed master for the zone
# that is sending this NOTIFY OP is actually the master. If it's not
# We'll reply but don't do anything with the NOTIFY.
master_addr = domain.get_master_by_ip(notify_addr)
if not master_addr:
msg = _LW("NOTIFY for %(name)s from non-master server "
"%(addr)s, ignoring.")
LOG.warn(msg % {"name": domain.name, "addr": notify_addr})
response.set_rcode(dns.rcode.REFUSED)
yield response
raise StopIteration
resolver = dns.resolver.Resolver()
# According to RFC we should query the server that sent the NOTIFY
resolver.nameservers = [notify_addr]
soa_answer = resolver.query(domain.name, 'SOA')
soa_serial = soa_answer[0].serial
if soa_serial == domain.serial:
msg = _LI("Serial %(serial)s is the same for master and us for "
"%(domain_id)s")
LOG.info(msg % {"serial": soa_serial, "domain_id": domain.id})
else:
msg = _LI("Scheduling AXFR for %(domain_id)s from %(master_addr)s")
info = {"domain_id": domain.id, "master_addr": master_addr}
LOG.info(msg % info)
self.tg.add_thread(self.domain_sync, context, domain,
[master_addr])
response.flags |= dns.flags.AA
yield response
raise StopIteration
def _handle_query_error(self, request, rcode):
"""
Construct an error response with the rcode passed in.
:param request: The decoded request from the wire.
:param rcode: The response code to send back.
:return: A dns response message with the response code set to rcode
"""
response = dns.message.make_response(request)
response.set_rcode(rcode)
return response
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
else:
ttl = domain.ttl
# construct rdata from all the records
rdata = []
for record in recordset.records:
# TODO(Ron): this should be handled in the Storage query where we
# find the recordsets.
if record.action != 'DELETE':
rdata.append(str(record.data))
# Now put the records into dnspython's RRsets
# answer section has 1 RR set. If the RR set has multiple
# records, DNSpython puts each record in a separate answer
# section.
# RRSet has name, ttl, class, type and rdata
# The rdata has one or more records
r_rrset = None
if rdata:
r_rrset = dns.rrset.from_text_list(
recordset.name, ttl, dns.rdataclass.IN, recordset.type, rdata)
return r_rrset
def _prep_rrsets(self, raw_records, domain_ttl):
rrsets = []
rrset_id = None
current_rrset = None
for record in raw_records:
# If we're looking at the first, or a new rrset
if record[0] != rrset_id:
if current_rrset is not None:
# If this isn't the first iteration
rrsets.append(current_rrset)
# Set up a new rrset
rrset_id = record[0]
rrtype = str(record[1])
# gross
ttl = int(record[2]) if record[2] is not None else domain_ttl
name = str(record[3])
rdata = str(record[4])
current_rrset = dns.rrset.from_text_list(
name, ttl, dns.rdataclass.IN, rrtype, [rdata])
else:
# We've already got an rrset, add the rdata
rrtype = str(record[1])
rdata = str(record[4])
rd = dns.rdata.from_text(dns.rdataclass.IN,
dns.rdatatype.from_text(rrtype), rdata)
current_rrset.add(rd)
# If the last record examined was a new rrset, or there is only 1 rrset
if rrsets == [] or (rrsets != [] and rrsets[-1] != current_rrset):
if current_rrset is not None:
rrsets.append(current_rrset)
return rrsets
def _handle_axfr(self, request):
context = request.environ['context']
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
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.warning(_LW("DomainNotFound while handling axfr request. "
"Question was %(qr)s") % {'qr': q_rrset})
yield self._handle_query_error(request, dns.rcode.REFUSED)
raise StopIteration
except exceptions.Forbidden:
LOG.warning(_LW("Forbidden while handling axfr request. "
"Question was %(qr)s") % {'qr': q_rrset})
yield self._handle_query_error(request, dns.rcode.REFUSED)
raise StopIteration
# The AXFR response needs to have a SOA at the beginning and end.
criterion = {'domain_id': domain.id, 'type': 'SOA'}
soa_records = self.storage.find_recordsets_axfr(context, criterion)
# Get all the records other than SOA
criterion = {'domain_id': domain.id, 'type': '!SOA'}
records = self.storage.find_recordsets_axfr(context, criterion)
# Place the SOA RRSet at the front and end of the RRSet list
records.insert(0, soa_records[0])
records.append(soa_records[0])
# Build the DNSPython RRSets from the Records
rrsets = self._prep_rrsets(records, domain.ttl)
# Build up a dummy response, we're stealing it's logic for building
# the Flags.
response = dns.message.make_response(request)
response.flags |= dns.flags.AA
response.set_rcode(dns.rcode.NOERROR)
max_message_size = CONF['service:mdns'].max_message_size
if max_message_size > 65535:
LOG.warning(_LW('MDNS max message size must not be greater than '
'65535'))
max_message_size = 65535
if request.had_tsig:
# Make some room for the TSIG RR to be appended at the end of the
# rendered message.
max_message_size = max_message_size - TSIG_RRSIZE
# Render the results, yielding a packet after each TooBig exception.
i, renderer = 0, None
while i < len(rrsets):
# No renderer? Build one
if renderer is None:
renderer = dns.renderer.Renderer(
response.id, response.flags, max_message_size)
for q in request.question:
renderer.add_question(q.name, q.rdtype, q.rdclass)
try:
renderer.add_rrset(dns.renderer.ANSWER, rrsets[i])
i += 1
except dns.exception.TooBig:
renderer.write_header()
if request.had_tsig:
# Make the space we reserved for TSIG available for use
renderer.max_size += TSIG_RRSIZE
renderer.add_tsig(
request.keyname,
request.keyring[request.keyname],
request.fudge,
request.original_id,
request.tsig_error,
request.other_data,
request.request_mac,
request.keyalgorithm)
yield renderer
renderer = None
if renderer is not None:
renderer.write_header()
if request.had_tsig:
# Make the space we reserved for TSIG available for use
renderer.max_size += TSIG_RRSIZE
renderer.add_tsig(
request.keyname,
request.keyring[request.keyname],
request.fudge,
request.original_id,
request.tsig_error,
request.other_data,
request.request_mac,
request.keyalgorithm)
yield renderer
raise StopIteration
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,
# validate the parameters
criterion = {
'name': q_rrset.name.to_text(),
'type': dns.rdatatype.to_text(q_rrset.rdtype),
'domains_deleted': False
}
recordset = self.storage.find_recordset(context, criterion)
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})
yield self._handle_query_error(request, dns.rcode.REFUSED)
raise StopIteration
except exceptions.Forbidden:
LOG.warning(_LW("Forbidden while handling query request. "
"Question was %(qr)s") % {'qr': q_rrset})
yield self._handle_query_error(request, dns.rcode.REFUSED)
raise StopIteration
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
# would be NOERROR and the SOA record is returned. This tells
# caching nameservers that the FQDN does exist, so don't negatively
# cache it, but the specific record doesn't exist.
#
# If an FQDN doesn't exist with any record type, that is NXDOMAIN.
# However, an authoritative nameserver shouldn't return NXDOMAIN
# for a zone it isn't authoritative for. It would be more
# appropriate for it to return REFUSED. It should still return
# NXDOMAIN if it is authoritative for a domain but the FQDN doesn't
# exist, like abcdef.rackspace.com. Of course, a wildcard within a
# domain would mean that NXDOMAIN isn't ever returned for a domain.
#
# To simply things currently this returns a REFUSED in all cases.
# 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)
yield response