Browse Source

Merge branch 'master' of ssh://bind10.isc.org/var/bind10/git/bind10

Jelte Jansen 14 years ago
parent
commit
d85c19ad4f

+ 5 - 0
ChangeLog

@@ -1,5 +1,10 @@
 bind10-devel-20110120 released on January 20, 2011
 bind10-devel-20110120 released on January 20, 2011
 
 
+  156.	[func]		stephen
+	Added ResponseClassifier class to examine response from
+	a server and classify it into one of several categories.
+	(Trac #487, git 18491370576e7438c7893f8551bbb8647001be9c)
+
   155.	[doc]		jreed
   155.	[doc]		jreed
 	Miscellaneous documentation improvements for man pages and
 	Miscellaneous documentation improvements for man pages and
 	the guide, including auth, resolver, stats, xfrout, and
 	the guide, including auth, resolver, stats, xfrout, and

+ 1 - 1
doc/Doxyfile

@@ -568,7 +568,7 @@ WARN_LOGFILE           =
 # directories like "/usr/src/myproject". Separate the files or directories
 # directories like "/usr/src/myproject". Separate the files or directories
 # with spaces.
 # 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 ../src/lib/testutils
+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 ../src/lib/testutils
 
 
 # This tag can be used to specify the character encoding of the source files
 # 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
 # that doxygen parses. Internally doxygen uses the UTF-8 encoding, which is

+ 1 - 0
src/bin/resolver/Makefile.am

@@ -37,6 +37,7 @@ spec_config.h: spec_config.h.pre
 BUILT_SOURCES = spec_config.h 
 BUILT_SOURCES = spec_config.h 
 pkglibexec_PROGRAMS = b10-resolver
 pkglibexec_PROGRAMS = b10-resolver
 b10_resolver_SOURCES = resolver.cc resolver.h
 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/change_user.h
 b10_resolver_SOURCES += $(top_builddir)/src/bin/auth/common.h
 b10_resolver_SOURCES += $(top_builddir)/src/bin/auth/common.h
 b10_resolver_SOURCES += main.cc
 b10_resolver_SOURCES += main.cc

+ 259 - 0
src/bin/resolver/response_classifier.cc

@@ -0,0 +1,259 @@
+// 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 (message->getHeaderFlag(Message::HEADERFLAG_TC) && (!tcignore)) {
+        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()) {
+        if (authority.empty()) {
+            return (EMPTY);
+        } else {
+            return (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 CNAME.  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 {
+
+            // Either the name and/or class of the reply don't match that of
+            // the question.
+            return (INVNAMCLASS);
+        }
+    }
+
+    // There are 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 the request type was ANY and they all have the same QNAME, we have
+    // an answer.  But if they don't have the same QNAME, we must have an error;
+    // the only way we could get different QNAMES in an answer is if one were a
+    // CNAME - in which case there should no other record types at that QNAME.
+    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);
+        } else {
+            return (EXTRADATA);
+        }
+    }
+
+    // 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 CNAME.  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) {
+                        if (size == 1) {
+                            return (ANSWERCNAME);
+                        } else {
+                            return (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);
+}

+ 138 - 0
src/bin/resolver/response_classifier.h

@@ -0,0 +1,138 @@
+// 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 {
+
+        // Codes indicating that a message is valid.
+
+        ANSWER,             ///< Response contains the answer
+        ANSWERCNAME,        ///< Response was a CNAME chain ending in an answer
+        CNAME,              ///< Response was a CNAME
+        NXDOMAIN,           ///< Response was an NXDOMAIN
+        REFERRAL,           ///< Response contains a referral
+
+        // Codes indicating that a message is invalid.  Note that the error()
+        // method relies on these appearing after the "message valid" codes.
+
+        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 Check Error
+    ///
+    /// An inline routine to quickly classify whether the return category is
+    /// an error or not.  This makes use of internal knowledge of the order of
+    /// codes in the Category enum.
+    ///
+    /// \param code Return category from classify()
+    ///
+    /// \return true if the category is an error, false if not.
+    static bool error(Category code) {
+        return (code > REFERRAL);
+    }
+
+    /// \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

+ 2 - 0
src/bin/resolver/tests/Makefile.am

@@ -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.h
 run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.cc
 run_unittests_SOURCES += $(top_srcdir)/src/lib/dns/tests/unittest_util.cc
 run_unittests_SOURCES += ../resolver.h ../resolver.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_unittest.cc
 run_unittests_SOURCES += resolver_config_unittest.cc
 run_unittests_SOURCES += resolver_config_unittest.cc
+run_unittests_SOURCES += response_classifier_unittest.cc
 run_unittests_SOURCES += run_unittests.cc
 run_unittests_SOURCES += run_unittests.cc
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_CPPFLAGS = $(AM_CPPFLAGS) $(GTEST_INCLUDES)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)
 run_unittests_LDFLAGS = $(AM_LDFLAGS) $(GTEST_LDFLAGS)

+ 494 - 0
src/bin/resolver/tests/response_classifier_unittest.cc

@@ -0,0 +1,494 @@
+// 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 error() function categorises the codes correctly.
+
+TEST_F(ResponseClassifierTest, StatusCodes) {
+    EXPECT_FALSE(ResponseClassifier::error(ResponseClassifier::ANSWER));
+    EXPECT_FALSE(ResponseClassifier::error(ResponseClassifier::ANSWERCNAME));
+    EXPECT_FALSE(ResponseClassifier::error(ResponseClassifier::CNAME));
+    EXPECT_FALSE(ResponseClassifier::error(ResponseClassifier::NXDOMAIN));
+    EXPECT_FALSE(ResponseClassifier::error(ResponseClassifier::REFERRAL));
+
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::EMPTY));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::EXTRADATA));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::INVNAMCLASS));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::INVTYPE));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::MISMATQUEST));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::MULTICLASS));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::NOTONEQUEST));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::NOTRESPONSE));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::NOTSINGLE));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::OPCODE));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::RCODE));
+    EXPECT_TRUE(ResponseClassifier::error(ResponseClassifier::TRUNCATED));
+}
+
+// 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);
+    EXPECT_EQ(ResponseClassifier::NOTONEQUEST,
+        ResponseClassifier::classify(qu_in_a_www, message));
+}
+
+// Test that the question in the question section in the message response
+// is equal to the question supplied.
+
+TEST_F(ResponseClassifierTest, SameQuestion) {
+
+    EXPECT_EQ(ResponseClassifier::MISMATQUEST,
+        ResponseClassifier::classify(qu_in_ns_, msg_a));
+    EXPECT_NE(ResponseClassifier::MISMATQUEST,
+        ResponseClassifier::classify(qu_in_a_www, msg_a));
+}
+
+// Should get an NXDOMAIN response only on an NXDOMAIN RCODE.
+
+TEST_F(ResponseClassifierTest, NXDOMAIN) {
+
+    uint16_t nxdomain = static_cast<uint16_t>(Rcode::NXDOMAIN().getCode());
+
+    for (uint8_t i = 0; i < (1 << 4); ++i) {
+        msg_a->setRcode(Rcode(i));
+        if (i == nxdomain) {
+            EXPECT_EQ(ResponseClassifier::NXDOMAIN,
+                ResponseClassifier::classify(qu_in_a_www, msg_a));
+        } else {
+            EXPECT_NE(ResponseClassifier::NXDOMAIN,
+                ResponseClassifier::classify(qu_in_a_www, msg_a));
+        }
+    }
+}
+
+// Check that we get an RCODE error on all but NXDOMAIN and NOERROR responses.
+
+TEST_F(ResponseClassifierTest, RCODE) {
+
+    uint16_t nxdomain = static_cast<uint16_t>(Rcode::NXDOMAIN().getCode());
+    uint16_t noerror = static_cast<uint16_t>(Rcode::NOERROR().getCode());
+
+    for (uint8_t i = 0; i < (1 << 4); ++i) {
+        msg_a->setRcode(Rcode(i));
+        if ((i == nxdomain) || (i == noerror)) {
+            EXPECT_NE(ResponseClassifier::RCODE,
+                ResponseClassifier::classify(qu_in_a_www, msg_a));
+        } else {
+            EXPECT_EQ(ResponseClassifier::RCODE,
+                ResponseClassifier::classify(qu_in_a_www, msg_a));
+        }
+    }
+}
+
+// Test that the code will detect a truncated message.  Even if nothing else
+// is wrong, we'll want to retry the query if we receive a truncated code.
+// However, we give the option to the user of the code aws to whether they
+// want to take into account the truncated bit.
+
+TEST_F(ResponseClassifierTest, Truncated) {
+
+    // Don't expect the truncated code whatever option we ask for if the TC
+    // bit is not set.
+    msg_a->setHeaderFlag(Message::HEADERFLAG_TC, false);
+    EXPECT_NE(ResponseClassifier::TRUNCATED,
+        ResponseClassifier::classify(qu_in_a_www, msg_a, true));
+    EXPECT_NE(ResponseClassifier::TRUNCATED,
+        ResponseClassifier::classify(qu_in_a_www, msg_a, false));
+
+    // Expect the truncated code if the TC bit is set, only if we don't ignore
+    // it.
+    msg_a->setHeaderFlag(Message::HEADERFLAG_TC, true);
+    EXPECT_NE(ResponseClassifier::TRUNCATED,
+        ResponseClassifier::classify(qu_in_a_www, msg_a, true));
+    EXPECT_EQ(ResponseClassifier::TRUNCATED,
+        ResponseClassifier::classify(qu_in_a_www, msg_a, false));
+}
+
+// Check for an empty packet (i.e. no error, but with the answer and additional
+// sections empty).
+
+TEST_F(ResponseClassifierTest, Empty) {
+
+    EXPECT_EQ(ResponseClassifier::EMPTY,
+        ResponseClassifier::classify(qu_in_a_www, msg_a));
+}
+
+// Anything where we have an empty answer section but something in the
+// authority section is a referral (if the status is NOERROR).
+
+TEST_F(ResponseClassifierTest, EmptyAnswerReferral) {
+
+    msg_a->addRRset(Message::SECTION_AUTHORITY, rrs_in_ns_);
+    EXPECT_EQ(ResponseClassifier::REFERRAL,
+        ResponseClassifier::classify(qu_in_a_www, msg_a));
+
+}
+
+// Check the case where we have a simple answer in the answer section.  This
+// occurs when the QNAME/QTYPE/QCLASS matches one of the RRsets in the
+// answer section - expect when the QTYPE is ANY, in which case the match
+// must be on the QNAME/QCLASS alone.
+
+TEST_F(ResponseClassifierTest, SingleAnswer) {
+
+    // Check a question that matches the answer
+    msg_a->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    EXPECT_EQ(ResponseClassifier::ANSWER,
+        ResponseClassifier::classify(qu_in_a_www, msg_a));
+
+    // Check an ANY question that matches the answer
+    msg_any->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    EXPECT_EQ(ResponseClassifier::ANSWER,
+        ResponseClassifier::classify(qu_in_any_www, msg_any));
+
+    // Check a CNAME response that matches the QNAME.
+    MessagePtr message_a(new Message(Message::RENDER));
+    message_a->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_a->setOpcode(Opcode::QUERY());
+    message_a->setRcode(Rcode::NOERROR());
+    message_a->addQuestion(qu_in_cname_www1);
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_cname_www1);
+    EXPECT_EQ(ResponseClassifier::CNAME,
+        ResponseClassifier::classify(qu_in_cname_www1, message_a));
+
+    // Check if the answer QNAME does not match the question
+    // Q: www.example.com  IN A
+    // A: mail.example.com IN A
+    MessagePtr message_b(new Message(Message::RENDER));
+    message_b->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_b->setOpcode(Opcode::QUERY());
+    message_b->setRcode(Rcode::NOERROR());
+    message_b->addQuestion(qu_in_a_www);
+    message_b->addRRset(Message::SECTION_ANSWER, rrs_in_a_mail);
+    EXPECT_EQ(ResponseClassifier::INVNAMCLASS,
+        ResponseClassifier::classify(qu_in_a_www, message_b));
+
+    // Check if the answer class does not match the question
+    // Q: www.example.com CH A
+    // A: www.example.com IN A
+    MessagePtr message_c(new Message(Message::RENDER));
+    message_c->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_c->setOpcode(Opcode::QUERY());
+    message_c->setRcode(Rcode::NOERROR());
+    message_c->addQuestion(qu_ch_a_www);
+    message_c->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    EXPECT_EQ(ResponseClassifier::INVNAMCLASS,
+        ResponseClassifier::classify(qu_ch_a_www, message_c));
+
+    // Check if the answer type does not match the question
+    // Q: www.example.com IN A
+    // A: www.example.com IN TXT
+    MessagePtr message_d(new Message(Message::RENDER));
+    message_d->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_d->setOpcode(Opcode::QUERY());
+    message_d->setRcode(Rcode::NOERROR());
+    message_d->addQuestion(qu_in_a_www);
+    message_d->addRRset(Message::SECTION_ANSWER, rrs_in_txt_www);
+    EXPECT_EQ(ResponseClassifier::INVTYPE,
+        ResponseClassifier::classify(qu_in_a_www, message_d));
+}
+
+// Check what happens if we have multiple RRsets in the answer.
+
+TEST_F(ResponseClassifierTest, MultipleAnswerRRsets) {
+
+    // All the same QNAME but different types is only valid on an ANY query.
+    MessagePtr message_a(new Message(Message::RENDER));
+    message_a->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_a->setOpcode(Opcode::QUERY());
+    message_a->setRcode(Rcode::NOERROR());
+    message_a->addQuestion(qu_in_any_www);
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_txt_www);
+    EXPECT_EQ(ResponseClassifier::ANSWER,
+        ResponseClassifier::classify(qu_in_any_www, message_a));
+
+    // On another type of query, it results in an EXTRADATA error
+    MessagePtr message_b(new Message(Message::RENDER));
+    message_b->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_b->setOpcode(Opcode::QUERY());
+    message_b->setRcode(Rcode::NOERROR());
+    message_b->addQuestion(qu_in_a_www);
+    message_b->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    message_b->addRRset(Message::SECTION_ANSWER, rrs_in_txt_www);
+    EXPECT_EQ(ResponseClassifier::EXTRADATA,
+        ResponseClassifier::classify(qu_in_a_www, message_b));
+
+    // Same QNAME on an ANY query is not valid with mixed classes
+    MessagePtr message_c(new Message(Message::RENDER));
+    message_c->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_c->setOpcode(Opcode::QUERY());
+    message_c->setRcode(Rcode::NOERROR());
+    message_c->addQuestion(qu_in_any_www);
+    message_c->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    message_c->addRRset(Message::SECTION_ANSWER, rrs_hs_txt_www);
+    EXPECT_EQ(ResponseClassifier::MULTICLASS,
+        ResponseClassifier::classify(qu_in_any_www, message_c));
+
+    // Mixed QNAME is not valid unless QNAME requested is a CNAME.
+    MessagePtr message_d(new Message(Message::RENDER));
+    message_d->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_d->setOpcode(Opcode::QUERY());
+    message_d->setRcode(Rcode::NOERROR());
+    message_d->addQuestion(qu_in_a_www);
+    message_d->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    message_d->addRRset(Message::SECTION_ANSWER, rrs_in_a_mail);
+    EXPECT_EQ(ResponseClassifier::EXTRADATA,
+        ResponseClassifier::classify(qu_in_a_www, message_d));
+
+    // Mixed QNAME is not valid when the query is an ANY.
+    MessagePtr message_e(new Message(Message::RENDER));
+    message_e->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_e->setOpcode(Opcode::QUERY());
+    message_e->setRcode(Rcode::NOERROR());
+    message_e->addQuestion(qu_in_any_www);
+    message_e->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    message_e->addRRset(Message::SECTION_ANSWER, rrs_in_txt_www);
+    message_e->addRRset(Message::SECTION_ANSWER, rrs_in_a_mail);
+    EXPECT_EQ(ResponseClassifier::EXTRADATA,
+        ResponseClassifier::classify(qu_in_any_www, message_e));
+}
+
+// CNAME chain is CNAME if it terminates in a CNAME, answer if it
+// does not, and error if there are RRs left over.
+TEST_F(ResponseClassifierTest, CNAMEChain) {
+
+    // Answer contains a single CNAME
+    MessagePtr message_a(new Message(Message::RENDER));
+    message_a->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_a->setOpcode(Opcode::QUERY());
+    message_a->setRcode(Rcode::NOERROR());
+    message_a->addQuestion(qu_in_a_www2);
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_cname_www2);
+    EXPECT_EQ(ResponseClassifier::CNAME,
+        ResponseClassifier::classify(qu_in_a_www2, message_a));
+
+    // Add a CNAME for www1, and it should still return a CNAME
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_cname_www1);
+    EXPECT_EQ(ResponseClassifier::CNAME,
+        ResponseClassifier::classify(qu_in_a_www2, message_a));
+
+    // Add the A record for www and it should be an answer
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_a_www);
+    EXPECT_EQ(ResponseClassifier::ANSWERCNAME,
+        ResponseClassifier::classify(qu_in_a_www2, message_a));
+
+    // Adding an unrelated TXT record should result in EXTRADATA
+    message_a->addRRset(Message::SECTION_ANSWER, rrs_in_txt_www);
+    EXPECT_EQ(ResponseClassifier::EXTRADATA,
+        ResponseClassifier::classify(qu_in_a_www2, message_a));
+
+    // Recreate the chain, but this time end with a TXT RR and not the A
+    // record.  This should return INVTYPE.
+    MessagePtr message_b(new Message(Message::RENDER));
+    message_b->setHeaderFlag(Message::HEADERFLAG_QR);
+    message_b->setOpcode(Opcode::QUERY());
+    message_b->setRcode(Rcode::NOERROR());
+    message_b->addQuestion(qu_in_a_www2);
+    message_b->addRRset(Message::SECTION_ANSWER, rrs_in_cname_www2);
+    message_b->addRRset(Message::SECTION_ANSWER, rrs_in_cname_www1);
+    message_b->addRRset(Message::SECTION_ANSWER, rrs_in_txt_www);
+
+    EXPECT_EQ(ResponseClassifier::INVTYPE,
+        ResponseClassifier::classify(qu_in_a_www2, message_b));
+}
+
+} // Anonymous namespace

+ 22 - 2
src/lib/dns/question.h

@@ -228,10 +228,10 @@ public:
     //@}
     //@}
 
 
     ///
     ///
-    /// \name Comparison Operator
+    /// \name Comparison Operators
     ///
     ///
     //@{
     //@{
-    /// A comparison operator is needed for this class so it can
+    /// A "less than" operator is needed for this class so it can
     /// function as an index to std::map.
     /// function as an index to std::map.
     bool operator <(const Question& rhs) const {
     bool operator <(const Question& rhs) const {
         return (rrclass_ < rhs.rrclass_ ||
         return (rrclass_ < rhs.rrclass_ ||
@@ -239,6 +239,26 @@ public:
                  (rrtype_ < rhs.rrtype_ ||
                  (rrtype_ < rhs.rrtype_ ||
                   (rrtype_ == rhs.rrtype_ && (name_ < rhs.name_)))));
                   (rrtype_ == rhs.rrtype_ && (name_ < rhs.name_)))));
     }
     }
+
+    /// Equality operator.  Primarily used to compare the question section in
+    /// a response to that in the query.
+    ///
+    /// \param rhs Question to compare against
+    /// \return true if name, class and type are equal, false otherwise
+    bool operator==(const Question& rhs) const {
+        return ((rrclass_ == rhs.rrclass_) && (rrtype_ == rhs.rrtype_) &&
+                (name_ == rhs.name_));
+    }
+
+    /// Inequality operator.  Primarily used to compare the question section in
+    /// a response to that in the query.
+    ///
+    /// \param rhs Question to compare against
+    /// \return true if one or more of the name, class and type do not match,
+    /// false otherwise.
+    bool operator!=(const Question& rhs) const {
+        return (!operator==(rhs));
+    }
     //@}
     //@}
 
 
 private:
 private:

+ 33 - 0
src/lib/dns/tests/question_unittest.cc

@@ -139,6 +139,39 @@ TEST_F(QuestionTest, comparison) {
     EXPECT_FALSE(Question(a, ch, ns) < Question(a, ch, ns));
     EXPECT_FALSE(Question(a, ch, ns) < Question(a, ch, ns));
     EXPECT_FALSE(Question(b, in, ns) < Question(b, in, ns));
     EXPECT_FALSE(Question(b, in, ns) < Question(b, in, ns));
     EXPECT_FALSE(Question(b, in, aaaa) < Question(b, in, aaaa));
     EXPECT_FALSE(Question(b, in, aaaa) < Question(b, in, aaaa));
+
+    // Identical questions are equal
+
+    EXPECT_TRUE(Question(a, in, ns) == Question(a, in, ns));
+    EXPECT_FALSE(Question(a, in, ns) != Question(a, in, ns));
+
+    // Components differing by one component are unequal...
+
+    EXPECT_FALSE(Question(b, in, ns) == Question(a, in, ns));
+    EXPECT_TRUE(Question(b, in, ns) != Question(a, in, ns));
+
+    EXPECT_FALSE(Question(a, ch, ns) == Question(a, in, ns));
+    EXPECT_TRUE(Question(a, ch, ns) != Question(a, in, ns));
+
+    EXPECT_FALSE(Question(a, in, aaaa) == Question(a, in, ns));
+    EXPECT_TRUE(Question(a, in, aaaa) != Question(a, in, ns));
+
+    // ... as are those differing by two components
+
+    EXPECT_FALSE(Question(b, ch, ns) == Question(a, in, ns));
+    EXPECT_TRUE(Question(b, ch, ns) != Question(a, in, ns));
+
+    EXPECT_FALSE(Question(b, in, aaaa) == Question(a, in, ns));
+    EXPECT_TRUE(Question(b, in, aaaa) != Question(a, in, ns));
+
+    EXPECT_FALSE(Question(a, ch, aaaa) == Question(a, in, ns));
+    EXPECT_TRUE(Question(a, ch, aaaa) != Question(a, in, ns));
+
+    // ... and question differing by all three
+
+    EXPECT_FALSE(Question(b, ch, aaaa) == Question(a, in, ns));
+    EXPECT_TRUE(Question(b, ch, aaaa) != Question(a, in, ns));
+
 }
 }
 
 
 }
 }