Commit 30838487 authored by Stephen Morris's avatar Stephen Morris
Browse files

First cut at code to classify a server response

The code analyses the response for errors as well as determining whether
it is an answer or a referral.
parent 332f90f4
......@@ -568,7 +568,7 @@ WARN_LOGFILE =
# directories like "/usr/src/myproject". Separate the files or directories
# with spaces.
INPUT = ../src/lib/cc ../src/lib/config ../src/lib/dns ../src/lib/exceptions ../src/lib/datasrc ../src/bin/auth ../src/lib/bench ../src/lib/log ../src/lib/asiolink/ ../src/lib/nsas
INPUT = ../src/lib/cc ../src/lib/config ../src/lib/dns ../src/lib/exceptions ../src/lib/datasrc ../src/bin/auth ../src/bin/resolver ../src/lib/bench ../src/lib/log ../src/lib/asiolink/ ../src/lib/nsas
# This tag can be used to specify the character encoding of the source files
# that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is
......
......@@ -37,6 +37,7 @@ spec_config.h: spec_config.h.pre
BUILT_SOURCES = spec_config.h
pkglibexec_PROGRAMS = b10-resolver
b10_resolver_SOURCES = resolver.cc resolver.h
b10_resolver_SOURCES += response_classifier.cc response_classifier.h
b10_resolver_SOURCES += $(top_builddir)/src/bin/auth/change_user.h
b10_resolver_SOURCES += $(top_builddir)/src/bin/auth/common.h
b10_resolver_SOURCES += main.cc
......
// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// $Id$
#include <cstddef>
#include <vector>
#include <resolver/response_classifier.h>
#include <dns/name.h>
#include <dns/opcode.h>
#include <dns/rcode.h>
#include <dns/rrset.h>
using namespace isc::dns;
using namespace std;
// Classify the response in the "message" object.
ResponseClassifier::Category ResponseClassifier::classify(
const Question& question, const MessagePtr& message, bool tcignore)
{
// Check header bits
if (!message->getHeaderFlag(Message::HEADERFLAG_QR)) {
return (NOTRESPONSE); // Query-response bit not set in the response
}
// We only recognise responses to queries here
if (message->getOpcode() != Opcode::QUERY()) {
return (OPCODE);
}
// Apparently have a response. There must be a single question in it...
const vector<QuestionPtr> msgquestion(message->beginQuestion(),
message->endQuestion());
if (msgquestion.size() != 1) {
return (NOTONEQUEST); // Not one question in response question section
}
// ... and the question should be equal to the question given.
// XXX: This means that "question" may not be the question sent by the
// client. In the case of a CNAME response, the qname of subsequent
// questions needs to be altered.
if (question != *(msgquestion[0])) {
return (MISMATQUEST);
}
// Check for Rcode-related errors.
const Rcode& rcode = message->getRcode();
if (rcode != Rcode::NOERROR()) {
if (rcode == Rcode::NXDOMAIN()) {
// No such domain. According to RFC2308, the domain referred to by
// the QNAME does not exist, although there may be a CNAME in the
// answer section and there may be an SOA and/or NS RRs in the
// authority section (ignoring any DNSSEC RRs for now).
//
// Note the "may". There may not be anything. Also, note that if
// there is a CNAME in the answer section, the authoritative server
// has verified that the name given in the CNAME's RDATA field does
// not exist. And that if a CNAME is returned in the answer, then
// the QNAME of the RRs in the authority section will refer to the
// authority for the CNAME's RDATA and not to the original question.
//
// Without doing further classification, it is sufficient to say
// that if an NXDOMAIN is received, there was no translation of the
// QNAME available.
return (NXDOMAIN); // Received NXDOMAIN from parent.
} else {
// Not NXDOMAIN but not NOERROR either. Must be an RCODE-related
// error.
return (RCODE);
}
}
// All seems OK and we can start looking at the content. However, one
// more header check remains - was the response truncated. If so, we'll
// probably want to re-query over TCP. However, in some circumstances we
// might want to go with what we have. So give the caller the option of
// ignoring the TC bit.
if (!tcignore && message->getHeaderFlag(Message::HEADERFLAG_TC)) {
return (TRUNCATED);
}
// By the time we get here, we're assured that the packet format is correct.
// We now need to decide as to whether it is an answer, a CNAME, or a
// referral. For this, we need to inspect the contents of the answer
// and authority sections.
const vector<RRsetPtr> answer(
message->beginSection(Message::SECTION_ANSWER),
message->endSection(Message::SECTION_ANSWER)
);
const vector<RRsetPtr> authority(
message->beginSection(Message::SECTION_AUTHORITY),
message->endSection(Message::SECTION_AUTHORITY)
);
// If there is nothing in the answer section, it is a referral - unless
// there is nothing in the authority section
if (answer.empty()) {
return (authority.empty() ? EMPTY : REFERRAL);
}
// Look at two cases - one RRset in the answer and multiple RRsets in
// the answer.
if (answer.size() == 1) {
// Does the name and class of the answer match that of the question?
if ((answer[0]->getName() == question.getName()) &&
(answer[0]->getClass() == question.getClass())) {
// It does. How about the type of the response? The response
// is an answer if the type matches that of the question, or if the
// question was for type ANY. It is a CNAME reply if the answer
// type is Referral. And it is an error for anything else.
if ((answer[0]->getType() == question.getType()) ||
(question.getType() == RRType::ANY())) {
return (ANSWER);
} else if (answer[0]->getType() == RRType::CNAME()) {
return (CNAME);
} else {
return (INVTYPE);
}
}
else {
return (INVNAMCLASS);
}
}
// Finally, there could be multiple RRsets in the answer. They should all
// have the same QCLASS, else there is some error in the response.
for (int i = 1; i < answer.size(); ++i) {
if (answer[0]->getClass() != answer[i]->getClass()) {
return (MULTICLASS);
}
}
// If they all have the same QNAME and the request type was ANY, we have
// an answer.
if (question.getType() == RRType::ANY()) {
bool all_same = true;
for (int i = 1; (i < answer.size()) && all_same; ++i) {
all_same = (answer[0]->getName() == answer[i]->getName());
}
if (all_same) {
return (ANSWER);
}
}
// Multiple RRs in the answer, and not all the same QNAME. This
// is either an answer, a CNAME (in either case, there could be multiple
// CNAMEs in the chain) or an error.
//
// So we need to follow the CNAME chain to resolve this. For this to work:
//
// a) There must be one RR that matches the name, class and type of
// the question, and this is a CNAME.
// b) The CNAME chain is followed until the end of the chain does not
// exist (answer is a CNAME) or it is not of type CNAME (ANSWER).
//
// In the latter case, if there are additional RRs, it must be an error.
vector<RRsetPtr> ansrrset(answer);
vector<int> present(ansrrset.size(), 1);
return cnameChase(question.getName(), question.getType(), ansrrset, present,
ansrrset.size());
}
// Search the CNAME chain.
ResponseClassifier::Category ResponseClassifier::cnameChase(
const Name& qname, const RRType& qtype, vector<RRsetPtr>& ansrrset,
vector<int>& present, size_t size)
{
// Search through the vector of RRset pointers until we find one with the
// right QNAME.
for (int i = 0; i < ansrrset.size(); ++i) {
if (present[i]) {
// This entry has not been logically removed, so look at it.
if (ansrrset[i]->getName() == qname) {
// QNAME match. If this RRset is a CNAME, remove it from
// further consideration. If nothing is left, the end of the
// chain is a CNAME so this is a referral. Otherwise replace
// the name with the RDATA of the CNAME and call ourself
// recursively.
if (ansrrset[i]->getType() == RRType::CNAME()) {
// Don't consider it in the next iteration (although we
// can still access it for now).
present[i] = 0;
--size;
if (size == 0) {
return (CNAME);
}
else {
if (ansrrset[i]->getRdataCount() != 1) {
// Multiple RDATA for a CNAME? This is invalid.
return (NOTSINGLE);
}
RdataIteratorPtr it = ansrrset[i]->getRdataIterator();
Name newname(it->getCurrent().toText());
return cnameChase(newname, qtype, ansrrset, present,
size);
}
} else {
// We've got here because the element is not a CNAME. If
// this is the last element and the type is the one we are
// after, we've found the answer, or it is an error. If
// there is more than one RRset left in the list we are
// searching, we have extra data in the answer.
if (ansrrset[i]->getType() == qtype) {
return ((size == 1) ? ANSWER : EXTRADATA);
}
return (INVTYPE);
}
}
}
}
// We get here if we've dropped off the end of the list without finding the
// QNAME we are looking for. This means that the CNAME chain has ended
// but there are additional RRsets in the data.
return (EXTRADATA);
}
// Copyright (C) 2011 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
// $Id$
#ifndef __RESPONSE_CLASSIFIER_H
#define __RESPONSE_CLASSIFIER_H
#include <cstddef>
#include <dns/question.h>
#include <dns/message.h>
#include <dns/question.h>
/// \brief Classify Server Response
///
/// This class is used in the recursive server. It is passed an answer received
/// from an upstream server and categorises it.
///
/// TODO: It is unlikely that the code can be used in this form. Some adaption
/// of it will be required to put it in the server.
///
/// TODO: The code here does not take into account any EDNS0 fields.
class ResponseClassifier {
public:
/// \brief Category of Answer
///
/// In the valid answers, not the distinction between REFERRAL and CNAME.
/// A REFERRAL answer means that the answer section of the message is
/// empty, but there is something in the authority section. A CNAME means
/// that the answer section contains one or more CNAMES in a chain that
/// do not end with a non-CNAME RRset.
enum Category {
// Packet is valid
ANSWER, ///< Response contains the answer
CNAME, ///< Response was a CNAME
NXDOMAIN, ///< Response was an NXDOMAIN
REFERRAL, ///< Response contains a referral
// Packet is invalid
EMPTY, ///< No answer or authority sections
EXTRADATA, ///< Answer section contains more RRsets than needed
INVNAMCLASS, ///< Invalid name or class in answer
INVTYPE, ///< Name/class of answer correct, type is wrong
MISMATQUEST, ///< Response question section != question
MULTICLASS, ///< Multiple classes in multi-RR answer
NOTONEQUEST, ///< Not one question in response question section
NOTRESPONSE, ///< Response has the Query/Response bit clear
NOTSINGLE, ///< CNAME has multiple RDATA elements.
OPCODE, ///< Opcode field does not indicate a query
RCODE, ///< RCODE indicated an error
TRUNCATED ///< Response was truncated
};
/// \brief Classify
///
/// Classify the response in the "message" object.
///
/// \param question Question that was sent to the server
/// \param message Pointer to the associated response from the server.
/// \param tcignore If set, the TC bit in a response packet is
/// ignored. Otherwise the error code TRUNCATED will be returned. The
/// only time this is likely to be used is in development where we are not
/// going to fail over to TCP and will want to use what is returned, even
/// if some of the response was lost.
static Category classify(const isc::dns::Question& question,
const isc::dns::MessagePtr& message, bool tcignore = false);
private:
/// \brief Follow CNAMEs
///
/// Given a QNAME and an answer section that contains CNAMEs, assume that
/// they form a CNAME chain and search through them. Possible outcomes
/// are:
///
/// a) All CNAMES and they form a chain. The result is a referral.
/// b) All but one are CNAMES and they form a chain. The other is pointed
/// to by the last element of the chain and is the correct QTYPE. The
/// result is an answer.
/// c) Having followed the CNAME chain as far as we can, there is one
/// remaining RRset that is of the wrong type, or there are multiple
/// RRsets remaining. return the EXTRADATA code.
///
/// \param qname Question name we are searching for
/// \param qtype Question type we are search for. (This is assumed not
/// to be "ANY".)
/// \param ansrrset Vector of RRsetPtr pointing to the RRsets we are
/// considering.
/// \param present Array of "int" the same size of ansrrset, with each
/// element set to "1" to allow the corresponding element of ansrrset to
/// be checked, and "0" to skip it. This might be premature optimisation,
/// but the algorithm would otherwise involve duplicating the RRset
/// vector then removing elements from random positions one by one. As
/// each removal involves the destruction of an "xxxPtr" element (which
/// presently is implemented by boost::shared_ptr), the overhad of memory
/// management seemed high. This solution imposes some additional loop
/// cycles, but that should be minimal compared with the overhead of the
/// memory management.
/// \param size Number of elements to check. See description of \c present
/// for details.
static Category cnameChase(const isc::dns::Name& qname,
const isc::dns::RRType& qtype,
std::vector<isc::dns::RRsetPtr>& ansrrset, std::vector<int>& present,
size_t size);
};
#endif // __RESPONSE_CLASSIFIER_H
......@@ -20,8 +20,10 @@ TESTS += run_unittests
run_unittests_SOURCES = $(top_srcdir)/src/lib/dns/tests/unittest_util.h
run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.cc
run_unittests_SOURCES += ../resolver.h ../resolver.cc
run_unittests_SOURCES += ../response_classifier.h ../response_classifier.cc
run_unittests_SOURCES += resolver_unittest.cc
run_unittests_SOURCES += resolver_config_unittest.cc
run_unittests_SOURCES += response_classifier_unittest.cc
run_unittests_SOURCES += run_unittests.cc
run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
......
// Copyright (C) 2010 Internet Systems Consortium, Inc. ("ISC")
//
// Permission to use, copy, modify, and/or distribute this software for any
// purpose with or without fee is hereby granted, provided that the above
// copyright notice and this permission notice appear in all copies.
//
// THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES WITH
// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
// AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR ANY SPECIAL, DIRECT,
// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE
// OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
// PERFORMANCE OF THIS SOFTWARE.
#include <iostream>
#include <gtest/gtest.h>
#include <dns/tests/unittest_util.h>
#include <resolver/response_classifier.h>
#include <dns/name.h>
#include <dns/opcode.h>
#include <dns/question.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
#include <dns/rcode.h>
#include <dns/rrclass.h>
#include <dns/rrset.h>
#include <dns/rrtype.h>
#include <dns/rrttl.h>
using namespace std;
using namespace isc::dns;
using namespace rdata;
using namespace isc::dns::rdata::generic;
using namespace isc::dns::rdata::in;
namespace {
class ResponseClassifierTest : public ::testing::Test {
public:
/// \brief Constructor
///
/// The naming convention is:
///
/// <category>_<class>_<type>_<name>
///
/// <category> is "qu" (question), "rrs" (rrset),
/// <qclass> is self-explanatory
/// <qtype> is self-explanatory
/// <name> is the first part of the domain name (all expected to be in
/// example.com)
///
/// Message variables
///
/// msg_<qtype> Where <qtype> is the type of query. These are only used
/// in the early tests where simple messages are required.
ResponseClassifierTest() :
msg_a(new Message(Message::RENDER)),
msg_any(new Message(Message::RENDER)),
qu_ch_a_www(Name("www.example.com"), RRClass::CH(), RRType::A()),
qu_in_any_www(Name("www.example.com"), RRClass::IN(), RRType::ANY()),
qu_in_a_www2(Name("www2.example.com"), RRClass::IN(), RRType::A()),
qu_in_a_www(Name("www.example.com"), RRClass::IN(), RRType::A()),
qu_in_cname_www1(Name("www1.example.com"), RRClass::IN(), RRType::A()),
qu_in_ns_(Name("example.com"), RRClass::IN(), RRType::NS()),
qu_in_txt_www(Name("www.example.com"), RRClass::IN(), RRType::TXT()),
rrs_hs_txt_www(new RRset(Name("www.example.com"), RRClass::HS(),
RRType::TXT(), RRTTL(300))),
rrs_in_a_mail(new RRset(Name("mail.example.com"), RRClass::IN(),
RRType::A(), RRTTL(300))),
rrs_in_a_www(new RRset(Name("www.example.com"), RRClass::IN(),
RRType::A(), RRTTL(300))),
rrs_in_cname_www1(new RRset(Name("www1.example.com"), RRClass::IN(),
RRType::CNAME(), RRTTL(300))),
rrs_in_cname_www2(new RRset(Name("www2.example.com"), RRClass::IN(),
RRType::CNAME(), RRTTL(300))),
rrs_in_ns_(new RRset(Name("example.com"), RRClass::IN(),
RRType::NS(), RRTTL(300))),
rrs_in_txt_www(new RRset(Name("www.example.com"), RRClass::IN(),
RRType::TXT(), RRTTL(300)))
{
// Set up the message to indicate a successful response to the question
// "www.example.com A", but don't add in any response sections.
msg_a->setHeaderFlag(Message::HEADERFLAG_QR);
msg_a->setOpcode(Opcode::QUERY());
msg_a->setRcode(Rcode::NOERROR());
msg_a->addQuestion(qu_in_a_www);
// ditto for the query "www.example.com ANY"
msg_any->setHeaderFlag(Message::HEADERFLAG_QR);
msg_any->setOpcode(Opcode::QUERY());
msg_any->setRcode(Rcode::NOERROR());
msg_any->addQuestion(qu_in_any_www);
// The next set of assignments set up the following zone records
//
// example.com NS ns0.isc.org
// NS ns0.example.org
//
// www.example.com A 1.2.3.4
// TXT "An example text string"
//
// mail.example.com A 4.5.6.7
//
// www1.example.com CNAME www.example.com
//
// www2.example.com CNAME www1.example.com
// Set up an imaginary NS RRset for an authority section
rrs_in_ns_->addRdata(ConstRdataPtr(new NS(Name("ns0.isc.org"))));
rrs_in_ns_->addRdata(ConstRdataPtr(new NS(Name("ns0.example.org"))));
// Set up the records for the www host
rrs_in_a_www->addRdata(ConstRdataPtr(new A("1.2.3.4")));
rrs_in_txt_www->addRdata(ConstRdataPtr(
new TXT("An example text string")));
// ... for the mail host
rrs_in_a_mail->addRdata(ConstRdataPtr(new A("5.6.7.8")));
// ... the CNAME records
rrs_in_cname_www1->addRdata(ConstRdataPtr(
new CNAME("www.example.com")));
rrs_in_cname_www2->addRdata(ConstRdataPtr(
new CNAME("www1.example.com")));
}
MessagePtr msg_a; // Pointer to message in RENDER state
MessagePtr msg_any; // Pointer to message in RENDER state
Question qu_ch_a_www; // www.example.com CH A
Question qu_in_any_www; // www.example.com IN ANY
Question qu_in_a_www2; // www.example.com IN ANY
Question qu_in_a_www; // www.example.com IN A
Question qu_in_cname_www1; // www1.example.com IN CNAME
Question qu_in_ns_; // example.com IN NS
Question qu_in_txt_www; // www.example.com IN TXT
RRsetPtr rrs_hs_txt_www; // www.example.com HS TXT
RRsetPtr rrs_in_a_mail; // mail.example.com IN A
RRsetPtr rrs_in_a_www; // www.example.com IN A
RRsetPtr rrs_in_cname_www1; // www1.example.com IN CNAME
RRsetPtr rrs_in_cname_www2; // www2.example.com IN CNAME
RRsetPtr rrs_in_ns_; // example.com IN NS
RRsetPtr rrs_in_txt_www; // www.example.com IN TXT
};
// Test that the system will reject a message which is a query.
TEST_F(ResponseClassifierTest, Query) {
// Set up message to indicate a query (QR flag = 0, one question). By
// default the opcode will be 0 (query)
msg_a->setHeaderFlag(Message::HEADERFLAG_QR, false);
// Should be rejected as it is a query, not a response
EXPECT_EQ(ResponseClassifier::NOTRESPONSE,
ResponseClassifier::classify(qu_in_a_www, msg_a));
}
// Check that we get an OPCODE error on all but QUERY opcodes.
TEST_F(ResponseClassifierTest, Opcode) {
uint8_t query = static_cast<uint8_t>(Opcode::QUERY().getCode());
for (uint8_t i = 0; i < (1 << 4); ++i) {
msg_a->setOpcode(Opcode(i));
if (i == query) {
EXPECT_NE(ResponseClassifier::OPCODE,
ResponseClassifier::classify(qu_in_a_www, msg_a));
} else {
EXPECT_EQ(ResponseClassifier::OPCODE,
ResponseClassifier::classify(qu_in_a_www, msg_a));
}
}
}
// Test that the system will reject a response with anything other than one
// question.
TEST_F(ResponseClassifierTest, MultipleQuestions) {
// Create a message object for this test that has no question section.
MessagePtr message(new Message(Message::RENDER));
message->setHeaderFlag(Message::HEADERFLAG_QR);
message->setOpcode(Opcode::QUERY());
message->setRcode(Rcode::NOERROR());
// Zero questions
EXPECT_EQ(ResponseClassifier::NOTONEQUEST,
ResponseClassifier::classify(qu_in_a_www, message));
// One question
message->addQuestion(qu_in_a_www);
EXPECT_NE(ResponseClassifier::NOTONEQUEST,
ResponseClassifier::classify(qu_in_a_www, message));
// Two questions
message->addQuestion(qu_in_ns_);
EXPECT_EQ(ResponseClassifier::NOTONEQUEST,
ResponseClassifier::classify(qu_in_a_www, message));
// And finish the check with three questions
message->addQuestion(qu_in_txt_www);