1ff8469ef7
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
434 lines
16 KiB
Python
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
|