Browse Source

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

Jeremy C. Reed 13 years ago
parent
commit
789a59358f
53 changed files with 1839 additions and 407 deletions
  1. 34 0
      ChangeLog
  2. 20 12
      configure.ac
  3. 22 27
      src/bin/auth/query.cc
  4. 275 32
      src/bin/auth/tests/query_unittest.cc
  5. 10 0
      src/bin/bind10/bind10_src.py.in
  6. 9 1
      src/bin/bind10/tests/bind10_test.py.in
  7. 107 6
      src/bin/ddns/ddns.py.in
  8. 24 0
      src/bin/ddns/ddns_messages.mes
  9. 1 0
      src/bin/ddns/tests/Makefile.am
  10. 272 1
      src/bin/ddns/tests/ddns_test.py
  11. 5 5
      src/bin/xfrin/tests/xfrin_test.py
  12. 1 1
      src/bin/xfrin/xfrin.py.in
  13. 4 4
      src/bin/xfrout/tests/xfrout_test.py.in
  14. 1 1
      src/bin/xfrout/xfrout.py.in
  15. 50 31
      src/lib/datasrc/database.cc
  16. 8 1
      src/lib/datasrc/database.h
  17. 79 3
      src/lib/datasrc/memory_datasrc.cc
  18. 15 0
      src/lib/datasrc/memory_datasrc.h
  19. 81 52
      src/lib/datasrc/tests/database_unittest.cc
  20. 154 11
      src/lib/datasrc/tests/memory_datasrc_unittest.cc
  21. 273 92
      src/lib/datasrc/zone.h
  22. 15 0
      src/lib/dns/masterload.cc
  23. 2 3
      src/lib/dns/masterload.h
  24. 13 2
      src/lib/dns/rrset.h
  25. 14 0
      src/lib/dns/tests/masterload_unittest.cc
  26. 9 6
      src/lib/python/isc/datasrc/datasrc.cc
  27. 118 19
      src/lib/python/isc/datasrc/finder_inc.cc
  28. 29 6
      src/lib/python/isc/datasrc/finder_python.cc
  29. 75 68
      src/lib/python/isc/datasrc/tests/datasrc_test.py
  30. 1 1
      src/lib/python/isc/datasrc/updater_python.cc
  31. 5 5
      src/lib/python/isc/notify/notify_out.py
  32. 1 1
      src/lib/python/isc/util/Makefile.am
  33. 2 2
      src/lib/python/isc/util/io/Makefile.am
  34. 0 0
      src/lib/python/isc/util/cio/__init__.py
  35. 1 1
      src/lib/python/isc/util/io/socketsession.py
  36. 0 0
      src/lib/python/isc/util/cio/socketsession_inc.cc
  37. 2 2
      src/lib/python/isc/util/io/socketsession_python.cc
  38. 0 0
      src/lib/python/isc/util/cio/socketsession_python.h
  39. 0 0
      src/lib/python/isc/util/cio/socketsessionforwarder_inc.cc
  40. 1 1
      src/lib/python/isc/util/io/socketsessionforwarder_python.cc
  41. 0 0
      src/lib/python/isc/util/cio/socketsessionforwarder_python.h
  42. 0 0
      src/lib/python/isc/util/cio/socketsessionreceiver_inc.cc
  43. 2 2
      src/lib/python/isc/util/io/socketsessionreceiver_python.cc
  44. 0 0
      src/lib/python/isc/util/cio/socketsessionreceiver_python.h
  45. 0 0
      src/lib/python/isc/util/cio/tests/Makefile.am
  46. 1 1
      src/lib/python/isc/util/io/tests/socketsession_test.py
  47. 12 0
      src/lib/server_common/socket_request.cc
  48. 53 5
      src/lib/server_common/socket_request.h
  49. 6 0
      src/lib/server_common/tests/socket_requestor_test.cc
  50. 1 0
      tests/lettuce/configurations/resolver/resolver_basic.config.orig
  51. 26 0
      tests/lettuce/features/resolver_basic.feature
  52. 1 1
      tests/lettuce/features/terrain/querying.py
  53. 4 1
      tests/lettuce/features/terrain/terrain.py

+ 34 - 0
ChangeLog

@@ -1,3 +1,37 @@
+369.	[func]		vorner
+	The SocketRequestor provides more information about what error happened
+	when it throws, by using subclasses of the original exception. This way
+	a user not interested in the difference can still use the original
+	exception, while it can be recognized if necessary.
+	(Trac #1542, git 2080e0316a339fa3cadea00e10b1ec4bc322ada0)
+
+368.	[func]*		jinmei
+	libdatasrc: the interface of ZoneFinder() was changed: WILDCARD
+	related result codes were deprecated and removed, and the
+	corresponding information is now provided via a separate accessor
+	method on FindResult.  Other separate FindResult methods will
+	also tell the caller whether the zone is signed with NSEC or NSEC3
+	(when necessary and applicable).
+	(Trac #1611, git c175c9c06034b4118e0dfdbccd532c2ebd4ba7e8)
+
+367.	[bug]		jinmei
+	libdatasrc: in-memory data source could incorrectly reject to load
+	zones containing RRSIG records.  For example, it didn't allow
+	RRSIG that covers a CNAME RR.  This fix also makes sure find()
+	will return RRsets with RRSIGs if they are signed.
+	(Trac #1614, git e8241ea5a4adea1b42a60ee7f2c5cfb87301734c)
+
+366.	[bug]		vorner
+	Fixed problem where a directory named "io" conflicted with the python3
+	standard module "io" and caused the installation to fail.  The
+	offending directory has been renamed to "cio".
+	(Trac #1561, git d81cf24b9e37773ba9a0d5061c779834ff7d62b9)
+
+365.	[bug]		jinmei
+	libdatasrc: in-memory datasource incorrectly returned delegation
+	for DS lookups.
+	(Trac #1571, git d22e90b5ef94880183cd652e112399b3efb9bd67)
+
 364.	[func]		jinmei
 	b10-auth experimentally supports statistics counters of incoming
 	requests per opcode.  The counters can be (e.g.) shown as

+ 20 - 12
configure.ac

@@ -48,16 +48,24 @@ AM_CONDITIONAL(USE_CLANGPP, test "X${CLANGPP}" = "Xyes")
 
 # Linker options
 
-# check -R rather than gcc specific -rpath to be as portable as possible.
+# check -R and -Wl,-R rather than gcc specific -rpath to be as portable
+# as possible.
 AC_MSG_CHECKING([whether -R flag is available in linker])
 LDFLAGS_SAVED="$LDFLAGS"
 LDFLAGS="$LDFLAGS -R/usr/lib"
 AC_TRY_LINK([],[],
-	[ AC_MSG_RESULT(yes)
-		rpath_available=yes
-	],[ AC_MSG_RESULT(no)
-	rpath_available=no
-	])
+    [ AC_MSG_RESULT(yes)
+        rpath_flag=-R
+    ],[ AC_MSG_RESULT(no)
+        AC_MSG_CHECKING([whether -Wl,-R flag is available in linker])
+        LDFLAGS="$LDFLAGS_SAVED -Wl,-R"
+        AC_TRY_LINK([], [],
+            [ AC_MSG_RESULT(yes)
+                rpath_flag=-Wl,-R
+            ],[ AC_MSG_RESULT(no)
+                 rpath_flag=no
+            ])
+    ])
 LDFLAGS=$LDFLAGS_SAVED
 
 # allow building programs with static link.  we need to make it selective
@@ -202,10 +210,10 @@ fi
 # modules, we embed the path to the modules when possible.  We do this even
 # when the path is known in the common operational environment (e.g. when
 # it's stored in a common "hint" file) for simplicity.
-if test $rpath_available = yes; then
+if test $rpath_flag != no; then
 	python_rpath=
 	for flag in ${PYTHON_LDFLAGS}; do
-		python_rpath="${python_rpath} `echo $flag | sed -ne 's/^\(\-L\)/-R/p'`"
+		python_rpath="${python_rpath} `echo $flag | sed -ne "s/^\(\-L\)/${rpath_flag}/p"`"
 	done
 	PYTHON_LDFLAGS="${PYTHON_LDFLAGS} ${python_rpath}"
 fi
@@ -534,10 +542,10 @@ for flag in ${BOTAN_LIBS}; do
 done
 
 # See python_rpath for some info on why we do this
-if test $rpath_available = yes; then
+if test $rpath_flag != no; then
     BOTAN_RPATH=
     for flag in ${BOTAN_LIBS}; do
-            BOTAN_RPATH="${BOTAN_RPATH} `echo $flag | sed -ne 's/^\(\-L\)/-R/p'`"
+            BOTAN_RPATH="${BOTAN_RPATH} `echo $flag | sed -ne "s/^\(\-L\)/${rpath_flag}/p"`"
     done
 AC_SUBST(BOTAN_RPATH)
 
@@ -971,8 +979,8 @@ AC_CONFIG_FILES([Makefile
                  src/lib/python/isc/acl/tests/Makefile
                  src/lib/python/isc/util/Makefile
                  src/lib/python/isc/util/tests/Makefile
-                 src/lib/python/isc/util/io/Makefile
-                 src/lib/python/isc/util/io/tests/Makefile
+                 src/lib/python/isc/util/cio/Makefile
+                 src/lib/python/isc/util/cio/tests/Makefile
                  src/lib/python/isc/datasrc/Makefile
                  src/lib/python/isc/datasrc/tests/Makefile
                  src/lib/python/isc/dns/Makefile

+ 22 - 27
src/bin/auth/query.cc

@@ -79,8 +79,8 @@ Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
 
     // Find AAAA rrset
     if (qname_ != qname || qtype_ != RRType::AAAA()) {
-        ZoneFinder::FindResult aaaa_result =
-            zone.find(qname, RRType::AAAA(), options | dnssec_opt_);
+        ZoneFinder::FindResult aaaa_result = zone.find(qname, RRType::AAAA(),
+                                                       options | dnssec_opt_);
         if (aaaa_result.code == ZoneFinder::SUCCESS) {
             response_.addRRset(Message::SECTION_ADDITIONAL,
                     boost::const_pointer_cast<RRset>(aaaa_result.rrset),
@@ -91,8 +91,9 @@ Query::addAdditionalAddrs(ZoneFinder& zone, const Name& qname,
 
 void
 Query::addSOA(ZoneFinder& finder) {
-    ZoneFinder::FindResult soa_result(finder.find(finder.getOrigin(),
-        RRType::SOA(), dnssec_opt_));
+    ZoneFinder::FindResult soa_result = finder.find(finder.getOrigin(),
+                                                    RRType::SOA(),
+                                                    dnssec_opt_);
     if (soa_result.code != ZoneFinder::SUCCESS) {
         isc_throw(NoSOA, "There's no SOA record in zone " <<
             finder.getOrigin().toText());
@@ -147,9 +148,8 @@ Query::addNXDOMAINProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
     // Confirm the wildcard doesn't exist (this should result in NXDOMAIN;
     // otherwise we shouldn't have got NXDOMAIN for the original query in
     // the first place).
-    const ZoneFinder::FindResult fresult = finder.find(wildname,
-                                                       RRType::NSEC(),
-                                                       dnssec_opt_);
+    const ZoneFinder::FindResult fresult =
+        finder.find(wildname, RRType::NSEC(), dnssec_opt_);
     if (fresult.code != ZoneFinder::NXDOMAIN || !fresult.rrset ||
         fresult.rrset->getRdataCount() == 0) {
         isc_throw(BadNSEC, "Unexpected result for wildcard NXDOMAIN proof");
@@ -191,9 +191,6 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
     if (nsec->getRdataCount() == 0) {
         isc_throw(BadNSEC, "NSEC for WILDCARD_NXRRSET is empty");
     }
-    // Add this NSEC RR to authority section.
-    response_.addRRset(Message::SECTION_AUTHORITY,
-                      boost::const_pointer_cast<RRset>(nsec), dnssec_);
     
     const ZoneFinder::FindResult fresult =
         finder.find(qname_, RRType::NSEC(),
@@ -214,8 +211,9 @@ Query::addWildcardNXRRSETProof(ZoneFinder& finder, ConstRRsetPtr nsec) {
 void
 Query::addAuthAdditional(ZoneFinder& finder) {
     // Fill in authority and addtional sections.
-    ZoneFinder::FindResult ns_result = finder.find(finder.getOrigin(),
-                                                   RRType::NS(), dnssec_opt_);
+    ZoneFinder::FindResult ns_result =
+        finder.find(finder.getOrigin(), RRType::NS(), dnssec_opt_);
+
     // zone origin name should have NS records
     if (ns_result.code != ZoneFinder::SUCCESS) {
         isc_throw(NoApexNS, "There's no apex NS records in zone " <<
@@ -306,7 +304,6 @@ Query::process() {
             break;
         }
         case ZoneFinder::CNAME:
-        case ZoneFinder::WILDCARD_CNAME:
             /*
              * We don't do chaining yet. Therefore handling a CNAME is
              * mostly the same as handling SUCCESS, but we didn't get
@@ -322,12 +319,11 @@ Query::process() {
 
             // If the answer is a result of wildcard substitution,
             // add a proof that there's no closer name.
-            if (dnssec_ && db_result.code == ZoneFinder::WILDCARD_CNAME) {
+            if (dnssec_ && db_result.isWildcard()) {
                 addWildcardProof(*result.zone_finder);
             }
             break;
         case ZoneFinder::SUCCESS:
-        case ZoneFinder::WILDCARD:
             if (qtype_is_any) {
                 // If quety type is ANY, insert all RRs under the domain
                 // into answer section.
@@ -357,7 +353,7 @@ Query::process() {
 
             // If the answer is a result of wildcard substitution,
             // add a proof that there's no closer name.
-            if (dnssec_ && db_result.code == ZoneFinder::WILDCARD) {
+            if (dnssec_ && db_result.isWildcard()) {
                 addWildcardProof(*result.zone_finder);
             }
             break;
@@ -377,17 +373,16 @@ Query::process() {
             break;
         case ZoneFinder::NXRRSET:
             addSOA(*result.zone_finder);
-            if (dnssec_ && db_result.rrset) {
-                response_.addRRset(Message::SECTION_AUTHORITY,
-                                   boost::const_pointer_cast<RRset>(
-                                       db_result.rrset),
-                                   dnssec_);
-            }
-            break;
-        case ZoneFinder::WILDCARD_NXRRSET:
-            addSOA(*result.zone_finder);
-            if (dnssec_ && db_result.rrset) {
-                addWildcardNXRRSETProof(zfinder, db_result.rrset);
+            if (dnssec_) {
+                if (db_result.isNSECSigned() && db_result.rrset) {
+                    response_.addRRset(Message::SECTION_AUTHORITY,
+                                       boost::const_pointer_cast<RRset>(
+                                           db_result.rrset),
+                                       dnssec_);
+                    if (db_result.isWildcard()) {
+                        addWildcardNXRRSETProof(zfinder, db_result.rrset);
+                    }
+                }
             }
             break;
         default:

+ 275 - 32
src/bin/auth/tests/query_unittest.cc

@@ -19,6 +19,8 @@
 #include <boost/bind.hpp>
 #include <boost/scoped_ptr.hpp>
 
+#include <exceptions/exceptions.h>
+
 #include <dns/masterload.h>
 #include <dns/message.h>
 #include <dns/name.h>
@@ -153,6 +155,14 @@ const char* const nsec_www_txt =
 // Authoritative data without NSEC
 const char* const nonsec_a_txt = "nonsec.example.com. 3600 IN A 192.0.2.0\n";
 
+// NSEC3 RRs.  You may also need to add mapping to MockZoneFinder::hash_map_.
+const char* const nsec3_apex_txt =
+    "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom.example.com. 3600 IN NSEC3 1 1 12 "
+    "aabbccdd 2t7b4g4vsa5smi47k61mv5bv1a22bojr NS SOA NSEC3PARAM RRSIG\n";
+const char* const nsec3_www_txt =
+    "q04jkcevqvmu85r014c7dkba38o0ji5r.example.com. 3600 IN NSEC3 1 1 12 "
+    "aabbccdd r53bq7cc2uvmubfu5ocmm6pers9tk9en A RRSIG\n";
+
 // A helper function that generates a textual representation of RRSIG RDATA
 // for the given covered type.  The resulting RRSIG may not necessarily make
 // sense in terms of the DNSSEC protocol, but for our testing purposes it's
@@ -185,6 +195,7 @@ public:
         has_apex_NS_(true),
         rrclass_(RRClass::IN()),
         include_rrsig_anyway_(false),
+        use_nsec3_(false),
         nsec_name_(origin_)
     {
         stringstream zone_stream;
@@ -197,7 +208,8 @@ public:
             wild_txt << nsec_wild_txt << cnamewild_txt << nsec_cnamewild_txt <<
             wild_txt_nxrrset << nsec_wild_txt_nxrrset << wild_txt_next <<
             nsec_wild_txt_next << empty_txt << nsec_empty_txt <<
-            empty_prev_txt << nsec_empty_prev_txt;
+            empty_prev_txt << nsec_empty_prev_txt <<
+            nsec3_apex_txt << nsec3_www_txt;
 
         masterLoad(zone_stream, origin_, rrclass_,
                    boost::bind(&MockZoneFinder::loadRRset, this, _1));
@@ -206,6 +218,21 @@ public:
                                                     RRClass::IN(),
                                                     RRType::NSEC(),
                                                     RRTTL(3600)));
+
+        // (Faked) NSEC3 hash map.  For convenience we use hardcoded built-in
+        // map instead of calculating and using actual hash.
+        // The used hash values are borrowed from RFC5155 examples.
+        hash_map_[Name("example.com")] = "0p9mhaveqvm6t7vbl5lop2u3t2rp3tom";
+        hash_map_[Name("nxdomain.example.com")] =
+            "v644ebqk9bibcna874givr6joj62mlhv";
+        hash_map_[Name("nx.domain.example.com")] =
+            "v644ebqk9bibcna874givr6joj62mlhv";
+        hash_map_[Name("domain.example.com")] =
+            "v644ebqk9bibcna874givr6joj62mlhv";
+        hash_map_[Name("nxdomain2.example.com")] =
+            "q00jkcevqvmu85r014c7dkba38o0ji5r";
+        hash_map_[Name("nxdomain3.example.com")] =
+            "009mhaveqvm6t7vbl5lop2u3t2rp3tom";
     }
     virtual isc::dns::Name getOrigin() const { return (origin_); }
     virtual isc::dns::RRClass getClass() const { return (rrclass_); }
@@ -216,6 +243,9 @@ public:
                                std::vector<ConstRRsetPtr>& target,
                                const FindOptions options = FIND_DEFAULT);
 
+    virtual ZoneFinder::FindNSEC3Result
+    findNSEC3(const Name& name, bool recursive);
+
     // If false is passed, it makes the zone broken as if it didn't have the
     // SOA.
     void setSOAFlag(bool on) { has_SOA_ = on; }
@@ -236,6 +266,10 @@ public:
         nsec_result_.reset(new ZoneFinder::FindResult(code, rrset));
     }
 
+    // If true is passed return an empty NSEC3 RRset for some negative
+    // answers when DNSSEC is required.
+    void setNSEC3Flag(bool on) { use_nsec3_ = on; }
+
     Name findPreviousName(const Name&) const {
         isc_throw(isc::NotImplemented, "Mock doesn't support previous name");
     }
@@ -249,7 +283,19 @@ private:
     typedef map<RRType, ConstRRsetPtr> RRsetStore;
     typedef map<Name, RRsetStore> Domains;
     Domains domains_;
+    Domains nsec3_domains_;
     void loadRRset(RRsetPtr rrset) {
+        if (rrset->getType() == RRType::NSEC3()) {
+            // NSEC3 should go to the dedicated table
+            nsec3_domains_[rrset->getName()][rrset->getType()] = rrset;
+
+            // By nature it should have RRSIG.  (We may want to selectively
+            // omit this to test pathological cases).
+            rrset->addRRsig(RdataPtr(new generic::RRSIG(
+                                         getCommonRRSIGText(rrset->getType().
+                                                            toText()))));
+            return;
+        }
         domains_[rrset->getName()][rrset->getType()] = rrset;
         if (rrset->getName() == delegation_name_ &&
             rrset->getType() == RRType::NS()) {
@@ -283,9 +329,11 @@ private:
     ConstRRsetPtr dname_rrset_;
     const RRClass rrclass_;
     bool include_rrsig_anyway_;
+    bool use_nsec3_;
     // The following two will be used for faked NSEC cases
     Name nsec_name_;
     boost::scoped_ptr<ZoneFinder::FindResult> nsec_result_;
+    map<Name, string> hash_map_;
 };
 
 // A helper function that generates a new RRset based on "wild_rrset",
@@ -327,6 +375,51 @@ MockZoneFinder::findAll(const Name& name, std::vector<ConstRRsetPtr>& target,
     return (result);
 }
 
+ZoneFinder::FindNSEC3Result
+MockZoneFinder::findNSEC3(const Name& name, bool recursive) {
+    ConstRRsetPtr covering_proof;
+    const int labels = name.getLabelCount();
+
+    // For brevity, we assume several things below: maps should have an
+    // expected entry when operator[] is used; maps are not empty.
+    for (int i = 0; i < labels; ++i) {
+        const string hlabel = hash_map_[name.split(i, labels - i)];
+        const Name hname = Name(hlabel + ".example.com");
+        // We don't use const_iterator so that we can use operator[] below
+        Domains::iterator found_domain = nsec3_domains_.lower_bound(hname);
+
+        // If the given hash is larger than the largest stored hash or
+        // the first label doesn't match the target, identify the "previous"
+        // hash value and remember it as the candidate next closer proof.
+        if (found_domain == nsec3_domains_.end() ||
+            found_domain->first.split(0, 1).toText(true) != hlabel) {
+            // If the given hash is larger or smaller than everything,
+            // the covering proof is the NSEC3 that has the largest hash.
+            if (found_domain == nsec3_domains_.end() ||
+                found_domain == nsec3_domains_.begin()) {
+                covering_proof =
+                    nsec3_domains_.rbegin()->second[RRType::NSEC3()];
+            } else {
+                // Otherwise, H(found_domain-1) < given_hash < H(found_domain)
+                // The covering proof is the first one.
+                covering_proof = (--found_domain)->second[RRType::NSEC3()];
+            }
+            if (!recursive) {   // in non recursive mode, we are done.
+                return (ZoneFinder::FindNSEC3Result(false,
+                                                    name.getLabelCount(),
+                                                    covering_proof,
+                                                    ConstRRsetPtr()));
+            }
+        } else {                // exact match
+            return (ZoneFinder::FindNSEC3Result(
+                        true, name.getLabelCount() - i,
+                        found_domain->second[RRType::NSEC3()],
+                        covering_proof));
+        }
+    }
+    isc_throw(isc::Unexpected, "findNSEC3() isn't expected to fail");
+}
+
 ZoneFinder::FindResult
 MockZoneFinder::find(const Name& name, const RRType& type,
                      const FindOptions options)
@@ -362,7 +455,7 @@ MockZoneFinder::find(const Name& name, const RRType& type,
             ConstRRsetPtr rrset;
             // Strip whatever signature there is in case DNSSEC is not required
             // Just to make sure the Query asks for it when it is needed
-            if (options & ZoneFinder::FIND_DNSSEC ||
+            if ((options & ZoneFinder::FIND_DNSSEC) != 0 ||
                 include_rrsig_anyway_ ||
                 !found_rrset->second->getRRsig()) {
                 rrset = found_rrset->second;
@@ -389,12 +482,16 @@ MockZoneFinder::find(const Name& name, const RRType& type,
 
         // Otherwise it's NXRRSET case.
         if ((options & FIND_DNSSEC) != 0) {
+            if (use_nsec3_) {
+                return (FindResult(NXRRSET, RRsetPtr(), RESULT_NSEC3_SIGNED));
+            }
             found_rrset = found_domain->second.find(RRType::NSEC());
             if (found_rrset != found_domain->second.end()) {
-                return (FindResult(NXRRSET, found_rrset->second));
+                return (FindResult(NXRRSET, found_rrset->second,
+                                   RESULT_NSEC_SIGNED));
             }
         }
-        return (FindResult(NXRRSET, RRsetPtr()));
+        return (FindResult(NXRRSET, RRsetPtr(), RESULT_NSEC_SIGNED));
     }
 
     // query name isn't found in our domains.
@@ -413,10 +510,14 @@ MockZoneFinder::find(const Name& name, const RRType& type,
         // the origin of the zone)
         --domain;               // reset domain to the "previous name"
         if ((options & FIND_DNSSEC) != 0) {
+            if (use_nsec3_) {
+                return (FindResult(NXRRSET, RRsetPtr(), RESULT_NSEC3_SIGNED));
+            }
             RRsetStore::const_iterator found_rrset =
                 (*domain).second.find(RRType::NSEC());
             if (found_rrset != (*domain).second.end()) {
-                return (FindResult(NXRRSET, found_rrset->second));
+                return (FindResult(NXRRSET, found_rrset->second,
+                                   RESULT_NSEC_SIGNED));
             }
         }
         return (FindResult(NXRRSET, RRsetPtr()));
@@ -436,36 +537,57 @@ MockZoneFinder::find(const Name& name, const RRType& type,
                 domain = domains_.find(Name("*").concatenate(wild_suffix));
                 // Matched the QNAME
                 if (domain != domains_.end()) {
-                   RRsetStore::const_iterator found_rrset =
-                       domain->second.find(type);
-                   // Matched the QTYPE
-                   if(found_rrset != domain->second.end()) {
-                    return (FindResult(WILDCARD,
-                            substituteWild(*found_rrset->second, name)));
-                   } else {
-                   // No matched QTYPE, this case is for WILDCARD_NXRRSET
-                     found_rrset = domain->second.find(RRType::NSEC());
-                     assert(found_rrset != domain->second.end());
-                     Name newName = Name("*").concatenate(wild_suffix);
-                     return (FindResult(WILDCARD_NXRRSET,
-                           substituteWild(*found_rrset->second,newName)));
-                   }
-                 } else {
+                    RRsetStore::const_iterator found_rrset =
+                        domain->second.find(type);
+                    // Matched the QTYPE
+                    if(found_rrset != domain->second.end()) {
+                        return (FindResult(SUCCESS,
+                                           substituteWild(
+                                               *found_rrset->second, name),
+                                           RESULT_WILDCARD |
+                                           (use_nsec3_ ?
+                                            RESULT_NSEC3_SIGNED :
+                                            RESULT_NSEC_SIGNED)));
+                    } else {
+                        // No matched QTYPE, this case is for WILDCARD_NXRRSET
+                        if (use_nsec3_) {
+                            return (FindResult(NXRRSET, RRsetPtr(),
+                                               RESULT_WILDCARD |
+                                               RESULT_NSEC3_SIGNED));
+                        }
+                        const Name new_name =
+                            Name("*").concatenate(wild_suffix);
+                        found_rrset = domain->second.find(RRType::NSEC());
+                        assert(found_rrset != domain->second.end());
+                        return (FindResult(NXRRSET,
+                                           substituteWild(
+                                               *found_rrset->second,
+                                               new_name),
+                                           RESULT_WILDCARD |
+                                           RESULT_NSEC_SIGNED));
+                    }
+                } else {
                     // This is empty non terminal name case on wildcard.
-                    Name emptyName = Name("*").concatenate(wild_suffix);
+                    const Name empty_name = Name("*").concatenate(wild_suffix);
+                    if (use_nsec3_) {
+                        return (FindResult(NXRRSET, RRsetPtr(),
+                                           RESULT_WILDCARD |
+                                           RESULT_NSEC3_SIGNED));
+                    }
                     for (Domains::reverse_iterator it = domains_.rbegin();
-                        it != domains_.rend();
-                        ++it) {
-                            RRsetStore::const_iterator nsec_it;
-                            if ((*it).first < emptyName &&
+                         it != domains_.rend();
+                         ++it) {
+                        RRsetStore::const_iterator nsec_it;
+                        if ((*it).first < empty_name &&
                             (nsec_it = (*it).second.find(RRType::NSEC()))
                             != (*it).second.end()) {
-                                return (FindResult(WILDCARD_NXRRSET,
-                                                   (*nsec_it).second));
-                            }
+                            return (FindResult(NXRRSET, (*nsec_it).second,
+                                               RESULT_WILDCARD |
+                                               RESULT_NSEC_SIGNED));
                         }
+                    }
                 }
-                return (FindResult(WILDCARD_NXRRSET,RRsetPtr()));
+                return (FindResult(NXRRSET, RRsetPtr(), RESULT_WILDCARD));
              }
         }
         const Name cnamewild_suffix("cnamewild.example.com");
@@ -476,8 +598,11 @@ MockZoneFinder::find(const Name& name, const RRType& type,
             RRsetStore::const_iterator found_rrset =
                 domain->second.find(RRType::CNAME());
             assert(found_rrset != domain->second.end());
-            return (FindResult(WILDCARD_CNAME,
-                               substituteWild(*found_rrset->second, name)));
+            return (FindResult(CNAME,
+                               substituteWild(*found_rrset->second, name),
+                               RESULT_WILDCARD |
+                               (use_nsec3_ ? RESULT_NSEC3_SIGNED :
+                                RESULT_NSEC_SIGNED)));
         }
     }
 
@@ -489,6 +614,10 @@ MockZoneFinder::find(const Name& name, const RRType& type,
     // we don't care about pathological cases such as the name is "smaller"
     // than the origin)
     if ((options & FIND_DNSSEC) != 0) {
+        if (use_nsec3_) {
+            return (FindResult(NXDOMAIN, RRsetPtr(), RESULT_NSEC3_SIGNED));
+        }
+
         // Emulate a broken DataSourceClient for some special names.
         if (nsec_result_ && nsec_name_ == name) {
             return (*nsec_result_);
@@ -504,7 +633,8 @@ MockZoneFinder::find(const Name& name, const RRType& type,
             if ((*it).first < name &&
                 (nsec_it = (*it).second.find(RRType::NSEC()))
                 != (*it).second.end()) {
-                return (FindResult(NXDOMAIN, (*nsec_it).second));
+                return (FindResult(NXDOMAIN, (*nsec_it).second,
+                                   RESULT_NSEC_SIGNED));
             }
         }
     }
@@ -1317,4 +1447,117 @@ TEST_F(QueryTest, MaxLenDNAME) {
     EXPECT_TRUE(ok) << "The synthetized CNAME not found";
 }
 
+// Test for this test module itself
+void
+nsec3Check(bool expected_matched, uint8_t expected_labels,
+           const string& expected_rrsets_txt,
+           const ZoneFinder::FindNSEC3Result& result)
+{
+    vector<ConstRRsetPtr> actual_rrsets;
+    EXPECT_EQ(expected_matched, result.matched);
+    // Convert to int so the error messages would be more readable:
+    EXPECT_EQ(static_cast<int>(expected_labels),
+              static_cast<int>(result.closest_labels));
+    if (result.closest_proof) {
+        actual_rrsets.push_back(result.closest_proof);
+    }
+    if (result.next_proof) {
+        actual_rrsets.push_back(result.next_proof);
+    }
+    rrsetsCheck(expected_rrsets_txt, actual_rrsets.begin(),
+                actual_rrsets.end());
+}
+
+TEST_F(QueryTest, findNSEC3) {
+    // In all test cases in the recursive mode, the closest encloser is the
+    // apex, and result's closest_labels should be the number of apex labels.
+    // (In non recursive mode closest_labels should be the # labels of the
+    // query name)
+    const uint8_t expected_closest_labels =
+        Name("example.com").getLabelCount();
+
+    // Apex name.  It should have a matching NSEC3
+    nsec3Check(true, expected_closest_labels, nsec3_apex_txt,
+               mock_finder->findNSEC3(Name("example.com"), false));
+
+    // Recursive mode doesn't change the result in this case.
+    nsec3Check(true, expected_closest_labels, nsec3_apex_txt,
+               mock_finder->findNSEC3(Name("example.com"), true)); 
+
+    // Non existent name.  Disabling recursion, a covering NSEC3 should be
+    // returned.
+    nsec3Check(false, 4, nsec3_www_txt,
+               mock_finder->findNSEC3(Name("nxdomain.example.com"), false));
+
+    // Non existent name.  The closest provable encloser is the apex,
+    // and next closer is the query name.
+    nsec3Check(true, expected_closest_labels,
+               string(nsec3_apex_txt) + string(nsec3_www_txt),
+               mock_finder->findNSEC3(Name("nxdomain.example.com"), true));
+
+    // Similar to the previous case, but next closer name is different
+    // (is the parent) of the non existent name.
+    nsec3Check(true, expected_closest_labels,
+               string(nsec3_apex_txt) + string(nsec3_www_txt),
+               mock_finder->findNSEC3(Name("nx.domain.example.com"), true));
+
+    // In the rest of test we check hash comparison for wrap around cases.
+    nsec3Check(false, 4, nsec3_apex_txt,
+               mock_finder->findNSEC3(Name("nxdomain2.example.com"), false));
+    nsec3Check(false, 4, nsec3_www_txt,
+               mock_finder->findNSEC3(Name("nxdomain3.example.com"), false));
+}
+
+// The following are tentative tests until we really add tests for the
+// query logic for these cases.  At that point it's probably better to
+// clean them up.
+TEST_F(QueryTest, nxdomainWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("nxdomain.example.com"), RRType::A(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXDOMAIN, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_FALSE(result.isWildcard());
+}
+
+TEST_F(QueryTest, nxrrsetWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("www.example.com"), RRType::TXT(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_FALSE(result.isWildcard());
+}
+
+TEST_F(QueryTest, emptyNameWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("no.example.com"), RRType::A(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_FALSE(result.isWildcard());
+}
+
+TEST_F(QueryTest, wildcardNxrrsetWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("www1.uwild.example.com"), RRType::TXT(),
+        ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_FALSE(result.rrset);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_TRUE(result.isWildcard());
+}
+
+TEST_F(QueryTest, wildcardEmptyWithNSEC3) {
+    mock_finder->setNSEC3Flag(true);
+    ZoneFinder::FindResult result = mock_finder->find(
+        Name("a.t.example.com"), RRType::A(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::NXRRSET, result.code);
+    EXPECT_TRUE(result.isNSEC3Signed());
+    EXPECT_TRUE(result.isWildcard());
+}
 }

+ 10 - 0
src/bin/bind10/bind10_src.py.in

@@ -87,6 +87,10 @@ DBG_COMMANDS = logger.DBGLVL_TRACE_DETAIL
 CREATOR_SOCKET_OK = b"1\n"
 CREATOR_SOCKET_UNAVAILABLE = b"0\n"
 
+# RCodes of known exceptions for the get_token command
+CREATOR_SOCKET_ERROR = 2
+CREATOR_SHARE_ERROR = 3
+
 # Assign this process some longer name
 isc.util.process.rename(sys.argv[0])
 
@@ -805,6 +809,12 @@ class BoB:
                 'token': token,
                 'path': self._socket_path
             })
+        except isc.bind10.socket_cache.SocketError as e:
+            return isc.config.ccsession.create_answer(CREATOR_SOCKET_ERROR,
+                                                      str(e))
+        except isc.bind10.socket_cache.ShareError as e:
+            return isc.config.ccsession.create_answer(CREATOR_SHARE_ERROR,
+                                                      str(e))
         except Exception as e:
             return isc.config.ccsession.create_answer(1, str(e))
 

+ 9 - 1
src/bin/bind10/tests/bind10_test.py.in

@@ -263,10 +263,11 @@ class TestCacheCommands(unittest.TestCase):
             """
             [rcode, ranswer] = self.__boss._get_socket(args)['result']
             self.assertEqual(code, rcode)
-            if code == 1:
+            if code != 0:
                 # This should be an error message. The exact formatting
                 # is unknown, but we check it is string at least
                 self.assertTrue(isinstance(ranswer, str))
+
         def mod_args(name, value):
             """
             Override a parameter in the args.
@@ -296,6 +297,13 @@ class TestCacheCommands(unittest.TestCase):
         # to an error, not propagated
         self.__raise_exception = Exception("Test exception")
         check_code(1, self.__socket_args)
+        # The special "expected" exceptions
+        self.__raise_exception = \
+            isc.bind10.socket_cache.ShareError("Not shared")
+        check_code(3, self.__socket_args)
+        self.__raise_exception = \
+            isc.bind10.socket_cache.SocketError("Not shared", 13)
+        check_code(2, self.__socket_args)
 
     def drop_socket(self, token):
         """

+ 107 - 6
src/bin/ddns/ddns.py.in

@@ -23,19 +23,32 @@ from isc.dns import *
 from isc.config.ccsession import *
 from isc.cc import SessionError, SessionTimeout
 import isc.util.process
+import isc.util.cio.socketsession
+import select
+import errno
 
 from isc.log_messages.ddns_messages import *
 
 from optparse import OptionParser, OptionValueError
 import os
+import os.path
 import signal
+import socket
 
 isc.log.init("b10-ddns")
 logger = isc.log.Logger("ddns")
+TRACE_BASIC = logger.DBGLVL_TRACE_BASIC
 
 DATA_PATH = bind10_config.DATA_PATH
+SOCKET_FILE = DATA_PATH + '/ddns_socket'
 if "B10_FROM_SOURCE" in os.environ:
     DATA_PATH = os.environ['B10_FROM_SOURCE'] + "/src/bin/ddns"
+if "B10_FROM_BUILD" in os.environ:
+    if "B10_FROM_SOURCE_LOCALSTATEDIR" in os.environ:
+        SOCKET_FILE = os.environ["B10_FROM_SOURCE_LOCALSTATEDIR"] + \
+            "/ddns_socket"
+    else:
+        SOCKET_FILE = os.environ["B10_FROM_BUILD"] + "/ddns_socket"
 SPECFILE_LOCATION = DATA_PATH + "/ddns.spec"
 
 
@@ -65,6 +78,13 @@ class DDNSSession:
         '''Initialize a DDNS Session'''
         pass
 
+def clear_socket():
+    '''
+    Removes the socket file, if it exists.
+    '''
+    if os.path.exists(SOCKET_FILE):
+        os.remove(SOCKET_FILE)
+
 class DDNSServer:
     def __init__(self, cc_session=None):
         '''
@@ -85,9 +105,17 @@ class DDNSServer:
         self._config_data = self._cc.get_full_config()
         self._cc.start()
         self._shutdown = False
+        # List of the session receivers where we get the requests
+        self._socksession_receivers = {}
+        clear_socket()
+        self._listen_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        self._listen_socket.bind(SOCKET_FILE)
+        self._listen_socket.listen(16)
 
     def config_handler(self, new_config):
         '''Update config data.'''
+        # TODO: Handle exceptions and turn them to an error response
+        # (once we have any configuration)
         answer = create_answer(0)
         return answer
 
@@ -96,6 +124,7 @@ class DDNSServer:
         Handle a CC session command, as sent from bindctl or other
         BIND 10 modules.
         '''
+        # TODO: Handle exceptions and turn them to an error response
         if cmd == "shutdown":
             logger.info(DDNS_RECEIVED_SHUTDOWN_COMMAND)
             self.trigger_shutdown()
@@ -125,19 +154,90 @@ class DDNSServer:
         '''
         pass
 
+    def accept(self):
+        """
+        Accept another connection and create the session receiver.
+        """
+        try:
+            sock = self._listen_socket.accept()
+            fileno = sock.fileno()
+            logger.debug(TRACE_BASIC, DDNS_NEW_CONN, fileno,
+                         sock.getpeername())
+            receiver = isc.util.cio.socketsession.SocketSessionReceiver(sock)
+            self._socksession_receivers[fileno] = (sock, receiver)
+        except (socket.error, isc.util.cio.socketsession.SocketSessionError) \
+            as e:
+            # These exceptions mean the connection didn't work, but we can
+            # continue with the rest
+            logger.error(DDNS_ACCEPT_FAILURE, e)
+
+    def handle_request(self, request):
+        """
+        This is the place where the actual DDNS processing is done. Other
+        methods are either subroutines of this method or methods doing the
+        uninteresting "accounting" stuff, like accepting socket,
+        initialization, etc.
+
+        It is called with the request being session as received from
+        SocketSessionReceiver, i.e. tuple
+        (socket, local_address, remote_address, data).
+        """
+        # TODO: Implement the magic
+
+        # TODO: Don't propagate most of the exceptions (like datasrc errors),
+        # just drop the packet.
+        pass
+
+    def handle_session(self, fileno):
+        """
+        Handle incoming session on the socket with given fileno.
+        """
+        logger.debug(TRACE_BASIC, DDNS_SESSION, fileno)
+        (socket, receiver) = self._socksession_receivers[fileno]
+        try:
+            self.handle_request(receiver.pop())
+        except isc.util.cio.socketsession.SocketSessionError as se:
+            # No matter why this failed, the connection is in unknown, possibly
+            # broken state. So, we close the socket and remove the receiver.
+            del self._socksession_receivers[fileno]
+            socket.close()
+            logger.warn(DDNS_DROP_CONN, fileno, se)
+
     def run(self):
         '''
         Get and process all commands sent from cfgmgr or other modules.
         This loops waiting for events until self.shutdown() has been called.
         '''
         logger.info(DDNS_RUNNING)
+        cc_fileno = self._cc.get_socket().fileno()
+        listen_fileno = self._listen_socket.fileno()
         while not self._shutdown:
-            # We do not catch any exceptions here right now, but this would
-            # be a good place to catch any exceptions that b10-ddns can
-            # recover from. We currently have no exception hierarchy to
-            # make such a distinction easily, but once we do, this would
-            # be the place to catch.
-            self._cc.check_command(False)
+            # In this event loop, we propagate most of exceptions, which will
+            # subsequently kill the process. We expect the handling functions
+            # to catch their own exceptions which they can recover from
+            # (malformed packets, lost connections, etc). The rationale behind
+            # this is they know best which exceptions are recoverable there
+            # and an exception may be recoverable somewhere, but not elsewhere.
+
+            try:
+                (reads, writes, exceptions) = \
+                    select.select([cc_fileno, listen_fileno] +
+                                  list(self._socksession_receivers.keys()), [],
+                                  [])
+            except select.error as se:
+                # In case it is just interrupted, we continue like nothing
+                # happened
+                if se.args[0] == errno.EINTR:
+                    (reads, writes, exceptions) = ([], [], [])
+                else:
+                    raise
+            for fileno in reads:
+                if fileno == cc_fileno:
+                    self._cc.check_command(True)
+                elif fileno == listen_fileno:
+                    self.accept()
+                else:
+                    self.handle_session(fileno)
         self.shutdown_cleanup()
         logger.info(DDNS_STOPPED)
 
@@ -204,6 +304,7 @@ def main(ddns_server=None):
         logger.error(DDNS_CC_SESSION_TIMEOUT_ERROR)
     except Exception as e:
         logger.error(DDNS_UNCAUGHT_EXCEPTION, type(e).__name__, str(e))
+    clear_socket()
 
 if '__main__' == __name__:
     main()

+ 24 - 0
src/bin/ddns/ddns_messages.mes

@@ -19,6 +19,12 @@
 # <topsrcdir>/tools/reorder_message_file.py to make sure the
 # messages are in the correct order.
 
+% DDNS_ACCEPT_FAILURE error accepting a connection: %1
+There was a low-level error when we tried to accept an incoming connection
+(probably coming from b10-auth). We continue serving on whatever other
+connections we already have, but this connection is dropped. The reason
+is logged.
+
 % DDNS_CC_SESSION_ERROR error reading from cc channel: %1
 There was a problem reading from the command and control channel. The
 most likely cause is that the msgq process is not running.
@@ -32,6 +38,13 @@ configuration manager b10-cfgmgr is not running.
 The ddns process encountered an error when installing the configuration at
 startup time.  Details of the error are included in the log message.
 
+% DDNS_DROP_CONN dropping connection on file descriptor %1 because of error %2
+There was an error on a connection with the b10-auth server (or whatever
+connects to the ddns daemon). This might be OK, for example when the
+authoritative server shuts down, the connection would get closed. It also
+can mean the system is busy and can't keep up or that the other side got
+confused and sent bad data.
+
 % DDNS_MODULECC_SESSION_ERROR error encountered by configuration/command module: %1
 There was a problem in the lower level module handling configuration and
 control commands.  This could happen for various reasons, but the most likely
@@ -39,6 +52,12 @@ cause is that the configuration database contains a syntax error and ddns
 failed to start at initialization.  A detailed error message from the module
 will also be displayed.
 
+% DDNS_NEW_CONN new connection on file descriptor %1 from %2
+Debug message. We received a connection and we are going to start handling
+requests from it. The file descriptor number and the address where the request
+comes from is logged. The connection is over a unix domain socket and is likely
+coming from a b10-auth process.
+
 % DDNS_RECEIVED_SHUTDOWN_COMMAND shutdown command received
 The ddns process received a shutdown command from the command channel
 and will now shut down.
@@ -47,6 +66,11 @@ and will now shut down.
 The ddns process has successfully started and is now ready to receive commands
 and updates.
 
+% DDNS_SESSION session arrived on file descriptor %1
+A debug message, informing there's some activity on the given file descriptor.
+It will be either a request or the file descriptor will be closed. See
+following log messages to see what of it.
+
 % DDNS_SHUTDOWN ddns server shutting down
 The ddns process is shutting down. It will no longer listen for new commands
 or updates. Any command or update that is being addressed at this moment will

+ 1 - 0
src/bin/ddns/tests/Makefile.am

@@ -21,6 +21,7 @@ endif
 	for pytest in $(PYTESTS) ; do \
 	echo Running test: $$pytest ; \
 	B10_FROM_SOURCE=$(abs_top_srcdir) \
+	B10_FROM_BUILD=$(abs_top_builddir) \
 	$(LIBRARY_PATH_PLACEHOLDER) \
 	PYTHONPATH=$(COMMON_PYTHON_PATH):$(abs_top_builddir)/src/bin/ddns:$(abs_top_builddir)/src/lib/dns/python/.libs:$(abs_top_builddir)/src/lib/util/io/.libs \
 	TESTDATASRCDIR=$(abs_srcdir)/testdata/ \

+ 272 - 1
src/bin/ddns/tests/ddns_test.py

@@ -19,6 +19,37 @@ import unittest
 import isc
 import ddns
 import isc.config
+import select
+import errno
+import isc.util.cio.socketsession
+import socket
+import os.path
+
+class FakeSocket:
+    """
+    A fake socket. It only provides a file number, peer name and accept method.
+    """
+    def __init__(self, fileno):
+        self.__fileno = fileno
+    def fileno(self):
+        return self.__fileno
+    def getpeername(self):
+        return "fake_unix_socket"
+    def accept(self):
+        return FakeSocket(self.__fileno + 1)
+
+class FakeSessionReceiver:
+    """
+    A fake socket session receiver, for our tests.
+    """
+    def __init__(self, socket):
+        self._socket = socket
+    def socket(self):
+        """
+        This method is not present in the real receiver, but we use it to
+        inspect the socket passed to the constructor.
+        """
+        return self._socket
 
 class MyCCSession(isc.config.ConfigData):
     '''Fake session with minimal interface compliance'''
@@ -32,6 +63,12 @@ class MyCCSession(isc.config.ConfigData):
         '''Called by DDNSServer initialization, but not used in tests'''
         self._started = True
 
+    def get_socket(self):
+        """
+        Used to get the file number for select.
+        """
+        return FakeSocket(1)
+
 class MyDDNSServer():
     '''Fake DDNS server used to test the main() function'''
     def __init__(self):
@@ -63,7 +100,41 @@ class TestDDNSServer(unittest.TestCase):
         cc_session = MyCCSession()
         self.assertFalse(cc_session._started)
         self.ddns_server = ddns.DDNSServer(cc_session)
+        self.__cc_session = cc_session
         self.assertTrue(cc_session._started)
+        self.__select_expected = None
+        self.__select_answer = None
+        self.__select_exception = None
+        self.__hook_called = False
+        self.ddns_server._listen_socket = FakeSocket(2)
+        ddns.select.select = self.__select
+
+    def tearDown(self):
+        ddns.select.select = select.select
+        ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
+            isc.util.cio.socketsession.SocketSessionReceiver
+
+    def test_listen(self):
+        '''
+        Test the old socket file is removed (if any) and a new socket
+        is created when the ddns server is created.
+        '''
+        # Make sure the socket does not exist now
+        ddns.clear_socket()
+        # Hook the call for clearing the socket
+        orig_clear = ddns.clear_socket
+        ddns.clear_socket = self.__hook
+        # Create the server
+        ddnss = ddns.DDNSServer(MyCCSession())
+        ddns.clear_socket = orig_clear
+        # The socket is created
+        self.assertTrue(os.path.exists(ddns.SOCKET_FILE))
+        self.assertTrue(isinstance(ddnss._listen_socket, socket.socket))
+        # And deletion of the socket was requested
+        self.assertIsNone(self.__hook_called)
+        # Now make sure the clear_socket really works
+        ddns.clear_socket()
+        self.assertFalse(os.path.exists(ddns.SOCKET_FILE))
 
     def test_config_handler(self):
         # Config handler does not do anything yet, but should at least
@@ -93,14 +164,215 @@ class TestDDNSServer(unittest.TestCase):
         signal_handler(None, None)
         self.assertTrue(self.ddns_server._shutdown)
 
+    def __select(self, reads, writes, exceptions, timeout=None):
+        """
+        A fake select. It checks it was called with the correct parameters and
+        returns a preset answer.
+
+        If there's an exception stored in __select_exception, it is raised
+        instead and the exception is cleared.
+        """
+        self.assertEqual(self.__select_expected, (reads, writes, exceptions,
+                                                  timeout))
+        if self.__select_exception is not None:
+            (self.__select_exception, exception) = (None,
+                                                    self.__select_exception)
+            raise exception
+        answer = self.__select_answer
+        self.__select_answer = None
+        self.ddns_server._shutdown = True
+        return answer
+
+    def __hook(self, param=None):
+        """
+        A hook that can be installed to any nullary or unary function and see
+        if it was really called.
+        """
+        self.__hook_called = param
+
+    def test_accept_called(self):
+        """
+        Test we call the accept function when a new connection comes.
+        """
+        self.ddns_server.accept = self.__hook
+        self.__select_expected = ([1, 2], [], [], None)
+        self.__select_answer = ([2], [], [])
+        self.__hook_called = "Not called"
+        self.ddns_server.run()
+        self.assertTrue(self.ddns_server._shutdown)
+        # The answer got used
+        self.assertIsNone(self.__select_answer)
+        # Reset, when called without parameter
+        self.assertIsNone(self.__hook_called)
+
+    def test_check_command_called(self):
+        """
+        Test the check_command is called when there's something on the
+        socket.
+        """
+        self.__cc_session.check_command = self.__hook
+        self.__select_expected = ([1, 2], [], [], None)
+        self.__select_answer = ([1], [], [])
+        self.ddns_server.run()
+        self.assertTrue(self.ddns_server._shutdown)
+        # The answer got used
+        self.assertIsNone(self.__select_answer)
+        # And the check_command was called with true parameter (eg.
+        # non-blocking)
+        self.assertTrue(self.__hook_called)
+
+    def test_accept(self):
+        """
+        Test that we can accept a new connection.
+        """
+        # There's nothing before the accept
+        ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
+            FakeSessionReceiver
+        self.assertEqual({}, self.ddns_server._socksession_receivers)
+        self.ddns_server.accept()
+        # Now the new socket session receiver is stored in the dict
+        # The 3 comes from _listen_socket.accept() - _listen_socket has
+        # fileno 2 and accept returns socket with fileno increased by one.
+        self.assertEqual([3],
+                         list(self.ddns_server._socksession_receivers.keys()))
+        (socket, receiver) = self.ddns_server._socksession_receivers[3]
+        self.assertTrue(isinstance(socket, FakeSocket))
+        self.assertEqual(3, socket.fileno())
+        self.assertTrue(isinstance(receiver, FakeSessionReceiver))
+        self.assertEqual(socket, receiver.socket())
+
+    def test_accept_fail(self):
+        """
+        Test we don't crash if an accept fails and that we don't modify the
+        internals.
+        """
+        # Make the accept fail
+        def accept_failure():
+            raise socket.error(errno.ECONNABORTED)
+        orig = self.ddns_server._listen_socket.accept
+        self.ddns_server._listen_socket.accept = accept_failure
+        self.assertEqual({}, self.ddns_server._socksession_receivers)
+        # Doesn't raise the exception
+        self.ddns_server.accept()
+        # And nothing is stored
+        self.assertEqual({}, self.ddns_server._socksession_receivers)
+        # Now make the socket receiver fail
+        self.ddns_server._listen_socket.accept = orig
+        def receiver_failure(sock):
+            raise isc.util.cio.socketsession.SocketSessionError('Test error')
+        ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
+            receiver_failure
+        # Doesn't raise the exception
+        self.ddns_server.accept()
+        # And nothing is stored
+        self.assertEqual({}, self.ddns_server._socksession_receivers)
+        # Check we don't catch everything, so raise just an exception
+        def unexpected_failure(sock):
+            raise Exception('Test error')
+        ddns.isc.util.cio.socketsession.SocketSessionReceiver = \
+            unexpected_failure
+        # This one gets through
+        self.assertRaises(Exception, self.ddns_server.accept)
+        # Nothing is stored as well
+        self.assertEqual({}, self.ddns_server._socksession_receivers)
+
+    def test_session_called(self):
+        """
+        Test the run calls handle_session when there's something on the
+        socket.
+        """
+        socket = FakeSocket(3)
+        self.ddns_server._socksession_receivers = \
+            {3: (socket, FakeSessionReceiver(socket))}
+        self.ddns_server.handle_session = self.__hook
+        self.__select_expected = ([1, 2, 3], [], [], None)
+        self.__select_answer = ([3], [], [])
+        self.ddns_server.run()
+        self.assertTrue(self.ddns_server._shutdown)
+        self.assertIsNone(self.__select_answer)
+        self.assertEqual(3, self.__hook_called)
+
+    def test_handle_session_ok(self):
+        """
+        Test the handle_session pops the receiver and calls handle_request
+        when everything is OK.
+        """
+        socket = FakeSocket(3)
+        receiver = FakeSessionReceiver(socket)
+        # It doesn't really matter what data we use here, it is only passed
+        # through the code
+        param = (FakeSocket(4), ('127.0.0.1', 1234), ('127.0.0.1', 1235),
+                 'Some data')
+        def pop():
+            return param
+        # Prepare data into the receiver
+        receiver.pop = pop
+        self.ddns_server._socksession_receivers = {3: (socket, receiver)}
+        self.ddns_server.handle_request = self.__hook
+        # Call it
+        self.ddns_server.handle_session(3)
+        # The popped data are passed into the handle_request
+        self.assertEqual(param, self.__hook_called)
+        # The receivers are kept the same
+        self.assertEqual({3: (socket, receiver)},
+                         self.ddns_server._socksession_receivers)
+
+    def test_handle_session_fail(self):
+        """
+        Test the handle_session removes (and closes) the socket and receiver
+        when the receiver complains.
+        """
+        socket = FakeSocket(3)
+        receiver = FakeSessionReceiver(socket)
+        def pop():
+            raise isc.util.cio.socketsession.SocketSessionError('Test error')
+        receiver.pop = pop
+        socket.close = self.__hook
+        self.__hook_called = False
+        self.ddns_server._socksession_receivers = {3: (socket, receiver)}
+        self.ddns_server.handle_session(3)
+        # The "dead" receiver is removed
+        self.assertEqual({}, self.ddns_server._socksession_receivers)
+        # Close is called with no parameter, so the default None
+        self.assertIsNone(self.__hook_called)
+
+    def test_select_exception_ignored(self):
+        """
+        Test that the EINTR is ignored in select.
+        """
+        # Prepare the EINTR exception
+        self.__select_exception = select.error(errno.EINTR)
+        # We reuse the test here, as it should act the same. The exception
+        # should just get ignored.
+        self.test_check_command_called()
+
+    def test_select_exception_fatal(self):
+        """
+        Test that other exceptions are fatal to the run.
+        """
+        # Prepare a different exception
+        self.__select_exception = select.error(errno.EBADF)
+        self.__select_expected = ([1, 2], [], [], None)
+        self.assertRaises(select.error, self.ddns_server.run)
+
 class TestMain(unittest.TestCase):
     def setUp(self):
         self._server = MyDDNSServer()
+        self.__orig_clear = ddns.clear_socket
+        ddns.clear_socket = self.__clear_socket
+        self.__clear_called = False
+
+    def tearDown(self):
+        ddns.clear_socket = self.__orig_clear
 
     def test_main(self):
         self.assertFalse(self._server.run_called)
         ddns.main(self._server)
         self.assertTrue(self._server.run_called)
+        self.assertTrue(self.__clear_called)
+
+    def __clear_socket(self):
+        self.__clear_called = True
 
     def check_exception(self, ex):
         '''Common test sequence to see if the given exception is caused.
@@ -135,7 +407,6 @@ class TestMain(unittest.TestCase):
         self._server.set_exception(BaseException("error"))
         self.assertRaises(BaseException, ddns.main, self._server)
         self.assertTrue(self._server.exception_raised)
-        
 
 if __name__== "__main__":
     isc.log.resetUnitTestRootLogger()

+ 5 - 5
src/bin/xfrin/tests/xfrin_test.py

@@ -167,14 +167,14 @@ class MockDataSourceClient():
 
         '''
         if name == TEST_ZONE_NAME and rrtype == RRType.SOA():
-            return (ZoneFinder.SUCCESS, begin_soa_rrset)
+            return (ZoneFinder.SUCCESS, begin_soa_rrset, 0)
         if name == Name('no-soa.example'):
-            return (ZoneFinder.NXDOMAIN, None)
+            return (ZoneFinder.NXDOMAIN, None, 0)
         if name == Name('dup-soa.example'):
             dup_soa_rrset = RRset(name, TEST_RRCLASS, RRType.SOA(), RRTTL(0))
             dup_soa_rrset.add_rdata(begin_soa_rdata)
             dup_soa_rrset.add_rdata(soa_rdata)
-            return (ZoneFinder.SUCCESS, dup_soa_rrset)
+            return (ZoneFinder.SUCCESS, dup_soa_rrset, 0)
         raise ValueError('Unexpected input to mock finder: bug in test case?')
 
     def get_updater(self, zone_name, replace, journaling=False):
@@ -1751,7 +1751,7 @@ class TestXFRSessionWithSQLite3(TestXfrinConnection):
     def get_zone_serial(self):
         result, finder = self.conn._datasrc_client.find_zone(TEST_ZONE_NAME)
         self.assertEqual(DataSourceClient.SUCCESS, result)
-        result, soa = finder.find(TEST_ZONE_NAME, RRType.SOA())
+        result, soa, _ = finder.find(TEST_ZONE_NAME, RRType.SOA())
         self.assertEqual(ZoneFinder.SUCCESS, result)
         self.assertEqual(1, soa.get_rdata_count())
         return get_soa_serial(soa.get_rdata()[0])
@@ -1759,7 +1759,7 @@ class TestXFRSessionWithSQLite3(TestXfrinConnection):
     def record_exist(self, name, type):
         result, finder = self.conn._datasrc_client.find_zone(TEST_ZONE_NAME)
         self.assertEqual(DataSourceClient.SUCCESS, result)
-        result, soa = finder.find(name, type)
+        result, soa, _ = finder.find(name, type)
         return result == ZoneFinder.SUCCESS
 
     def test_do_ixfrin_sqlite3(self):

+ 1 - 1
src/bin/xfrin/xfrin.py.in

@@ -583,7 +583,7 @@ class XfrinConnection(asyncore.dispatcher):
             result, finder = self._datasrc_client.find_zone(self._zone_name)
         if result != DataSourceClient.SUCCESS:
             return None
-        result, soa_rrset = finder.find(self._zone_name, RRType.SOA())
+        result, soa_rrset, _ = finder.find(self._zone_name, RRType.SOA())
         if result != ZoneFinder.SUCCESS:
             logger.info(XFRIN_ZONE_NO_SOA, self.zone_str())
             return None

+ 4 - 4
src/bin/xfrout/tests/xfrout_test.py.in

@@ -105,16 +105,16 @@ class MockDataSrcClient:
 
         '''
         if name == Name('nosoa.example.com') and rrtype == RRType.SOA():
-            return (ZoneFinder.NXDOMAIN, None)
+            return (ZoneFinder.NXDOMAIN, None, 0)
         elif name == Name('multisoa.example.com') and rrtype == RRType.SOA():
             soa_rrset = create_soa(SOA_CURRENT_VERSION)
             soa_rrset.add_rdata(soa_rrset.get_rdata()[0])
-            return (ZoneFinder.SUCCESS, soa_rrset)
+            return (ZoneFinder.SUCCESS, soa_rrset, 0)
         elif name == Name('maxserial.example.com'):
             soa_rrset = create_soa(0xffffffff)
-            return (ZoneFinder.SUCCESS, soa_rrset)
+            return (ZoneFinder.SUCCESS, soa_rrset, 0)
         elif rrtype == RRType.SOA():
-            return (ZoneFinder.SUCCESS, create_soa(SOA_CURRENT_VERSION))
+            return (ZoneFinder.SUCCESS, create_soa(SOA_CURRENT_VERSION), 0)
         raise ValueError('Unexpected input to mock finder: bug in test case?')
 
     def get_iterator(self, zone_name, adjust_ttl=False):

+ 1 - 1
src/bin/xfrout/xfrout.py.in

@@ -335,7 +335,7 @@ class XfroutSession():
         result, finder = self._datasrc_client.find_zone(zone_name)
         if result != DataSourceClient.SUCCESS:
             return (Rcode.NOTAUTH(), None)
-        result, soa_rrset = finder.find(zone_name, RRType.SOA())
+        result, soa_rrset, _ = finder.find(zone_name, RRType.SOA())
         if result != ZoneFinder.SUCCESS:
             return (Rcode.SERVFAIL(), None)
         # Especially for database-based zones, a working zone may be in

+ 50 - 31
src/lib/datasrc/database.cc

@@ -616,7 +616,7 @@ DatabaseClient::Finder::findWildcardMatch(
                           DATASRC_DATABASE_WILDCARD_CANCEL_NS).
                     arg(accessor_->getDBName()).arg(wildcard).
                     arg(dresult.first_ns->getName());
-                return (ZoneFinder::FindResult(DELEGATION, dresult.first_ns));
+                return (FindResult(DELEGATION, dresult.first_ns));
 
             } else if (!hasSubdomains(name.split(i - 1).toText())) {
                 // The wildcard match is the best one, find the final result
@@ -630,7 +630,7 @@ DatabaseClient::Finder::findWildcardMatch(
                           DATASRC_DATABASE_WILDCARD_CANCEL_SUB).
                     arg(accessor_->getDBName()).arg(wildcard).
                     arg(name).arg(superdomain);
-                return (ZoneFinder::FindResult(NXDOMAIN, ConstRRsetPtr()));
+                return (FindResult(NXDOMAIN, ConstRRsetPtr()));
             }
 
         } else if (hasSubdomains(wildcard)) {
@@ -641,22 +641,23 @@ DatabaseClient::Finder::findWildcardMatch(
             if ((options & FIND_DNSSEC) != 0) {
                 ConstRRsetPtr nsec = findNSECCover(Name(wildcard));
                 if (nsec) {
-                    return (ZoneFinder::FindResult(WILDCARD_NXRRSET, nsec));
+                    return (FindResult(NXRRSET, nsec,
+                                       RESULT_WILDCARD | RESULT_NSEC_SIGNED));
                 }
             }
-            return (ZoneFinder::FindResult(NXRRSET, ConstRRsetPtr()));
+            return (FindResult(NXRRSET, ConstRRsetPtr(), RESULT_WILDCARD));
         }
     }
 
     // Nothing found at any level.
-    return (ZoneFinder::FindResult(NXDOMAIN, ConstRRsetPtr()));
+    return (FindResult(NXDOMAIN, ConstRRsetPtr()));
 }
 
 ZoneFinder::FindResult
 DatabaseClient::Finder::logAndCreateResult(
     const Name& name, const string* wildname, const RRType& type,
     ZoneFinder::Result code, ConstRRsetPtr rrset,
-    const isc::log::MessageID& log_id) const
+    const isc::log::MessageID& log_id, FindResultFlags flags) const
 {
     if (rrset) {
         if (wildname == NULL) {
@@ -679,7 +680,7 @@ DatabaseClient::Finder::logAndCreateResult(
                 arg(getClass()).arg(*wildname);
         }
     }
-    return (ZoneFinder::FindResult(code, rrset));
+    return (ZoneFinder::FindResult(code, rrset, flags));
 }
 
 ZoneFinder::FindResult
@@ -693,12 +694,23 @@ DatabaseClient::Finder::findOnNameResult(const Name& name,
                                          target)
 {
     const bool wild = (wildname != NULL);
+    FindResultFlags flags = wild ? RESULT_WILDCARD : RESULT_DEFAULT;
 
     // Get iterators for the different types of records we are interested in -
     // CNAME, NS and Wanted types.
     const FoundIterator nsi(found.second.find(RRType::NS()));
     const FoundIterator cni(found.second.find(RRType::CNAME()));
     const FoundIterator wti(found.second.find(type));
+    // For wildcard case with DNSSEC required, the caller would need to know
+    // whether it's NSEC or NSEC3 signed.  So we need to do an additional
+    // search here, even though the NSEC RR may not be returned.
+    // TODO: this part should be revised when we support NSEC3; ideally we
+    // should use more effective and efficient way to identify (whether and)
+    // in which way the zone is signed.
+    if (wild && (options & FIND_DNSSEC) != 0 &&
+        found.second.find(RRType::NSEC()) != found.second.end()) {
+        flags = flags | RESULT_NSEC_SIGNED;
+    }
 
     if (!is_origin && ((options & FIND_GLUE_OK) == 0) &&
         nsi != found.second.end()) {
@@ -709,7 +721,8 @@ DatabaseClient::Finder::findOnNameResult(const Name& name,
         return (logAndCreateResult(name, wildname, type, DELEGATION,
                                    nsi->second,
                                    wild ? DATASRC_DATABASE_WILDCARD_NS :
-                                   DATASRC_DATABASE_FOUND_DELEGATION_EXACT));
+                                   DATASRC_DATABASE_FOUND_DELEGATION_EXACT,
+                                   flags));
 
     } else if (type != RRType::CNAME() && cni != found.second.end()) {
         // We are not searching for a CNAME but nevertheless we have found one
@@ -722,10 +735,10 @@ DatabaseClient::Finder::findOnNameResult(const Name& name,
                       cni->second->getRdataCount() << " rdata at " << name <<
                       ", expected 1");
         }
-        return (logAndCreateResult(name, wildname, type,
-                                   wild ? WILDCARD_CNAME : CNAME, cni->second,
+        return (logAndCreateResult(name, wildname, type, CNAME, cni->second,
                                    wild ? DATASRC_DATABASE_WILDCARD_CNAME :
-                                   DATASRC_DATABASE_FOUND_CNAME));
+                                   DATASRC_DATABASE_FOUND_CNAME,
+                                   flags));
 
     } else if (wti != found.second.end()) {
         bool any(type == RRType::ANY());
@@ -748,9 +761,8 @@ DatabaseClient::Finder::findOnNameResult(const Name& name,
         // includes the case where we were explicitly querying for a CNAME and
         // found it.  It also includes the case where we were querying for an
         // NS RRset and found it at the apex of the zone.)
-        return (logAndCreateResult(name, wildname, type,
-                                   wild ? WILDCARD : SUCCESS, wti->second,
-                                   lid));
+        return (logAndCreateResult(name, wildname, type, SUCCESS,
+                                   wti->second, lid, flags));
     }
 
     // If we get here, we have found something at the requested name but not
@@ -778,15 +790,13 @@ DatabaseClient::Finder::findOnNameResult(const Name& name,
     if (nsec_rrset) {
         // This log message covers both normal and wildcard cases, so we pass
         // NULL for 'wildname'.
-        return (logAndCreateResult(name, NULL, type,
-                                   wild ? WILDCARD_NXRRSET : NXRRSET,
-                                   nsec_rrset,
-                                   DATASRC_DATABASE_FOUND_NXRRSET_NSEC));
+        return (logAndCreateResult(name, NULL, type, NXRRSET, nsec_rrset,
+                                   DATASRC_DATABASE_FOUND_NXRRSET_NSEC,
+                                   flags | RESULT_NSEC_SIGNED));
     }
-    return (logAndCreateResult(name, wildname, type,
-                               wild ? WILDCARD_NXRRSET : NXRRSET, nsec_rrset,
+    return (logAndCreateResult(name, wildname, type, NXRRSET, nsec_rrset,
                                wild ? DATASRC_DATABASE_WILDCARD_NXRRSET :
-                               DATASRC_DATABASE_FOUND_NXRRSET));
+                               DATASRC_DATABASE_FOUND_NXRRSET, flags));
 }
 
 ZoneFinder::FindResult
@@ -809,9 +819,10 @@ DatabaseClient::Finder::findNoNameResult(const Name& name, const RRType& type,
         LOG_DEBUG(logger, DBG_TRACE_DETAILED,
                   DATASRC_DATABASE_FOUND_EMPTY_NONTERMINAL).
             arg(accessor_->getDBName()).arg(name);
-        return (FindResult(NXRRSET, dnssec_data ? findNSECCover(name) :
-                           ConstRRsetPtr()));
-
+        const ConstRRsetPtr nsec = dnssec_data ? findNSECCover(name) :
+            ConstRRsetPtr();
+        return (FindResult(NXRRSET, nsec,
+                           nsec ? RESULT_NSEC_SIGNED : RESULT_DEFAULT));
     } else if ((options & NO_WILDCARD) == 0) {
         // It's not an empty non-terminal and wildcard matching is not
         // disabled, so check for wildcards. If there is a wildcard match
@@ -820,7 +831,7 @@ DatabaseClient::Finder::findNoNameResult(const Name& name, const RRType& type,
         const ZoneFinder::FindResult wresult =
             findWildcardMatch(name, type, options, dresult, target);
         if (wresult.code != NXDOMAIN) {
-            return (FindResult(wresult.code, wresult.rrset));
+            return (wresult);
         }
     }
 
@@ -828,15 +839,16 @@ DatabaseClient::Finder::findNoNameResult(const Name& name, const RRType& type,
     // NSEC records if requested).
     LOG_DEBUG(logger, DBG_TRACE_DETAILED, DATASRC_DATABASE_NO_MATCH).
               arg(accessor_->getDBName()).arg(name).arg(type).arg(getClass());
-    return (FindResult(NXDOMAIN, dnssec_data ? findNSECCover(name) :
-                           ConstRRsetPtr()));
+    const ConstRRsetPtr nsec = dnssec_data ? findNSECCover(name) :
+        ConstRRsetPtr();
+    return (FindResult(NXDOMAIN, nsec,
+                       nsec ? RESULT_NSEC_SIGNED : RESULT_DEFAULT));
 }
 
 ZoneFinder::FindResult
-DatabaseClient::Finder::findInternal(const isc::dns::Name& name,
-                             const isc::dns::RRType& type,
-                             std::vector<isc::dns::ConstRRsetPtr>* target,
-                             const FindOptions options)
+DatabaseClient::Finder::findInternal(const Name& name, const RRType& type,
+                                     std::vector<ConstRRsetPtr>* target,
+                                     const FindOptions options)
 {
     LOG_DEBUG(logger, DBG_TRACE_DETAILED, DATASRC_DATABASE_FIND_RECORDS)
               .arg(accessor_->getDBName()).arg(name).arg(type).arg(getClass());
@@ -864,6 +876,7 @@ DatabaseClient::Finder::findInternal(const isc::dns::Name& name,
     // presence of the delegation.)
     const DelegationSearchResult dresult = findDelegationPoint(name, options);
     if (dresult.rrset) {
+        // In this case no special flags are needed.
         return (FindResult(dresult.code, dresult.rrset));
     }
 
@@ -892,6 +905,12 @@ DatabaseClient::Finder::findInternal(const isc::dns::Name& name,
     }
 }
 
+ZoneFinder::FindNSEC3Result
+DatabaseClient::Finder::findNSEC3(const Name&, bool) {
+    isc_throw(NotImplemented, "findNSEC3 is not yet implemented for database "
+              "data source");
+}
+
 Name
 DatabaseClient::Finder::findPreviousName(const Name& name) const {
     const string str(accessor_->findPreviousName(zone_id_,

+ 8 - 1
src/lib/datasrc/database.h

@@ -754,6 +754,12 @@ public:
         virtual isc::dns::Name findPreviousName(const isc::dns::Name& query)
             const;
 
+        /// Look for NSEC3 for proving (non)existence of given name.
+        ///
+        /// See documentation in \c Zone.
+        virtual FindNSEC3Result
+        findNSEC3(const isc::dns::Name& name, bool recursive);
+
         /// \brief The zone ID
         ///
         /// This function provides the stored zone ID as passed to the
@@ -1050,7 +1056,8 @@ public:
                                       const isc::dns::RRType& type,
                                       ZoneFinder::Result code,
                                       isc::dns::ConstRRsetPtr rrset,
-                                      const isc::log::MessageID& log_id) const;
+                                      const isc::log::MessageID& log_id,
+                                      FindResultFlags flags) const;
 
         /// \brief Checks if something lives below this domain.
         ///

+ 79 - 3
src/lib/datasrc/memory_datasrc.cc

@@ -21,6 +21,7 @@
 #include <exceptions/exceptions.h>
 
 #include <dns/name.h>
+#include <dns/rdataclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrsetlist.h>
 #include <dns/masterload.h>
@@ -36,6 +37,7 @@
 
 using namespace std;
 using namespace isc::dns;
+using namespace isc::dns::rdata;
 using namespace isc::data;
 
 namespace isc {
@@ -181,8 +183,12 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         if (!rrset) {
             isc_throw(NullRRset, "The rrset provided is NULL");
         }
+        if (rrset->getRdataCount() == 0) {
+            isc_throw(AddError, "The rrset provided is empty: " <<
+                      rrset->getName() << "/" << rrset->getType());
+        }
         // Check for singleton RRs. It should probably handled at a different
-        // in future.
+        // layer in future.
         if ((rrset->getType() == RRType::CNAME() ||
             rrset->getType() == RRType::DNAME()) &&
             rrset->getRdataCount() > 1)
@@ -230,6 +236,61 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         }
     }
 
+    result::Result addRRsig(const ConstRRsetPtr sig_rrset,
+                            DomainTree& domains)
+    {
+        DomainNode* node = NULL;
+        if (domains.find(sig_rrset->getName(), &node) !=
+            DomainTree::EXACTMATCH || node == NULL || !node->getData()) {
+            isc_throw(AddError,
+                      "RRSIG is being added, but no RR to be covered: "
+                      << sig_rrset->getName());
+        }
+
+        // Check consistency of the type covered.
+        // We know the RRset isn't empty, so the following check is safe.
+        RdataIteratorPtr rit = sig_rrset->getRdataIterator();
+        const RRType covered = dynamic_cast<const generic::RRSIG&>(
+            rit->getCurrent()).typeCovered();
+        for (rit->next(); !rit->isLast(); rit->next()) {
+            if (dynamic_cast<const generic::RRSIG&>(
+                    rit->getCurrent()).typeCovered() != covered) {
+                isc_throw(AddError, "RRSIG contains mixed covered types: "
+                          << sig_rrset->toText());
+            }
+        }
+
+        // Find the RRset to be covered; if not found, treat it as an error
+        // for now.
+        const Domain::const_iterator it = node->getData()->find(covered);
+        if (it == node->getData()->end()) {
+            isc_throw(AddError,
+                      "RRSIG is being added, but no RR of covered type found: "
+                      << sig_rrset->toText());
+        }
+
+        // The current implementation doesn't allow an existing RRSIG to be
+        // overridden (or updated with additional ones).
+        if ((it->second)->getRRsig()) {
+            isc_throw(AddError,
+                      "RRSIG is being added to override an existing one: "
+                      << sig_rrset->toText());
+        }
+
+        // All okay, setting the RRSIG.
+        // XXX: we break const-ness of the covered RRsets.  In practice the
+        // ownership of these RRsets would have been given to us so it should
+        // be safe, but it's still a very bad practice.
+        // We'll fix this problem anyway when we update the underlying
+        // representation so that it's more space efficient.
+        // Note: there's a slight chance of getting an exception.
+        // As noted in add(), we give up strong exception guarantee in such
+        // cases.
+        boost::const_pointer_cast<RRset>(it->second)->addRRsig(sig_rrset);
+
+        return (result::SUCCESS);
+    }
+
     /*
      * Implementation of longer methods. We put them here, because the
      * access is without the impl_-> and it will get inlined anyway.
@@ -244,6 +305,12 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
         LOG_DEBUG(logger, DBG_TRACE_DATA, DATASRC_MEM_ADD_RRSET).
             arg(rrset->getName()).arg(rrset->getType()).arg(origin_);
 
+        // RRSIGs are special in various points, so we handle it in a
+        // separate dedicated method.
+        if (rrset->getType() == RRType::RRSIG()) {
+            return (addRRsig(rrset, *domains));
+        }
+
         // Add wildcards possibly contained in the owner name to the domain
         // tree.
         // Note: this can throw an exception, breaking strong exception
@@ -283,7 +350,7 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
             if (rrset->getType() == RRType::NS() &&
                 rrset->getName() != origin_) {
                 node->setFlag(DomainNode::FLAG_CALLBACK);
-            // If it is DNAME, we have a callback as well here
+                // If it is DNAME, we have a callback as well here
             } else if (rrset->getType() == RRType::DNAME()) {
                 node->setFlag(DomainNode::FLAG_CALLBACK);
             }
@@ -557,7 +624,10 @@ struct InMemoryZoneFinder::InMemoryZoneFinderImpl {
 
         // If the node callback is enabled, this may be a zone cut.  If it
         // has a NS RR, we should return a delegation, but not in the apex.
-        if (node->getFlag(DomainNode::FLAG_CALLBACK) && node != origin_data_) {
+        // There is one exception: the case for DS query, which should always
+        // be considered in-zone lookup.
+        if (node->getFlag(DomainNode::FLAG_CALLBACK) && node != origin_data_ &&
+            type != RRType::DS()) {
             found = node->getData()->find(RRType::NS());
             if (found != node->getData()->end()) {
                 LOG_DEBUG(logger, DBG_TRACE_DATA,
@@ -641,6 +711,12 @@ InMemoryZoneFinder::findAll(const Name& name,
     return (impl_->find(name, RRType::ANY(), &target, options));
 }
 
+ZoneFinder::FindNSEC3Result
+InMemoryZoneFinder::findNSEC3(const Name&, bool) {
+    isc_throw(NotImplemented, "findNSEC3 is not yet implemented for in memory "
+              "data source");
+}
+
 result::Result
 InMemoryZoneFinder::add(const ConstRRsetPtr& rrset) {
     return (impl_->add(rrset, &impl_->domains_));

+ 15 - 0
src/lib/datasrc/memory_datasrc.h

@@ -83,6 +83,12 @@ public:
                                std::vector<isc::dns::ConstRRsetPtr>& target,
                                const FindOptions options = FIND_DEFAULT);
 
+    /// Look for NSEC3 for proving (non)existence of given name.
+    ///
+    /// See documentation in \c Zone.
+    virtual FindNSEC3Result
+    findNSEC3(const isc::dns::Name& name, bool recursive);
+
     /// \brief Imelementation of the ZoneFinder::findPreviousName method
     ///
     /// This one throws NotImplemented exception, as InMemory doesn't
@@ -93,6 +99,15 @@ public:
     ///
     /// It puts another RRset into the zone.
     ///
+    /// In the current implementation, this method doesn't allow an existing
+    /// RRset to be updated or overridden.  So the caller must make sure that
+    /// all RRs of the same type and name must be given in the form of a
+    /// single RRset.  The current implementation will also require that
+    /// when an RRSIG is added the RRset to be covered has already been
+    /// added.  These restrictions are probably too strict when this data
+    /// source accepts various forms of input, so they should be revisited
+    /// later.
+    ///
     /// Except for NullRRset and OutOfZone, this method does not guarantee
     /// strong exception safety (it is currently not needed, if it is needed
     /// in future, it should be implemented).

+ 81 - 52
src/lib/datasrc/tests/database_unittest.cc

@@ -1416,12 +1416,20 @@ doFindTest(ZoneFinder& finder,
            ZoneFinder::Result expected_result,
            const std::vector<std::string>& expected_rdatas,
            const std::vector<std::string>& expected_sig_rdatas,
+           ZoneFinder::FindResultFlags expected_flags =
+           ZoneFinder::RESULT_DEFAULT,
            const isc::dns::Name& expected_name = isc::dns::Name::ROOT_NAME(),
            const ZoneFinder::FindOptions options = ZoneFinder::FIND_DEFAULT)
 {
     SCOPED_TRACE("doFindTest " + name.toText() + " " + type.toText());
     const ZoneFinder::FindResult result = finder.find(name, type, options);
     ASSERT_EQ(expected_result, result.code) << name << " " << type;
+    EXPECT_EQ((expected_flags & ZoneFinder::RESULT_WILDCARD) != 0,
+              result.isWildcard());
+    EXPECT_EQ((expected_flags & ZoneFinder::RESULT_NSEC_SIGNED) != 0,
+              result.isNSECSigned());
+    EXPECT_EQ((expected_flags & ZoneFinder::RESULT_NSEC3_SIGNED) != 0,
+              result.isNSEC3Signed());
     if (!expected_rdatas.empty() && result.rrset) {
         checkRRset(result.rrset, expected_name != Name(".") ? expected_name :
                    name, finder.getClass(), expected_type, expected_ttl,
@@ -1838,17 +1846,17 @@ TYPED_TEST(DatabaseClientTest, findDelegation) {
     doFindTest(*finder, isc::dns::Name("ns.delegation.example.org."),
                this->qtype_, isc::dns::RRType::NS(),
                this->rrttl_, ZoneFinder::DELEGATION, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("delegation.example.org."));
     doFindTest(*finder, isc::dns::Name("ns.delegation.example.org."),
                isc::dns::RRType::AAAA(), isc::dns::RRType::NS(),
                this->rrttl_, ZoneFinder::DELEGATION, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("delegation.example.org."));
     doFindTest(*finder, isc::dns::Name("deep.below.delegation.example.org."),
                isc::dns::RRType::AAAA(), isc::dns::RRType::NS(),
                this->rrttl_, ZoneFinder::DELEGATION, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("delegation.example.org."));
 
     // Even when we check directly at the delegation point, we should get
@@ -1876,15 +1884,18 @@ TYPED_TEST(DatabaseClientTest, findDelegation) {
     doFindTest(*finder, isc::dns::Name("below.dname.example.org."),
                this->qtype_, isc::dns::RRType::DNAME(),
                this->rrttl_, ZoneFinder::DNAME, this->expected_rdatas_,
-               this->expected_sig_rdatas_, isc::dns::Name("dname.example.org."));
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
+               isc::dns::Name("dname.example.org."));
     doFindTest(*finder, isc::dns::Name("below.dname.example.org."),
                isc::dns::RRType::AAAA(), isc::dns::RRType::DNAME(),
                this->rrttl_, ZoneFinder::DNAME, this->expected_rdatas_,
-               this->expected_sig_rdatas_, isc::dns::Name("dname.example.org."));
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
+               isc::dns::Name("dname.example.org."));
     doFindTest(*finder, isc::dns::Name("really.deep.below.dname.example.org."),
                isc::dns::RRType::AAAA(), isc::dns::RRType::DNAME(),
                this->rrttl_, ZoneFinder::DNAME, this->expected_rdatas_,
-               this->expected_sig_rdatas_, isc::dns::Name("dname.example.org."));
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
+               isc::dns::Name("dname.example.org."));
 
     // Asking direcly for DNAME should give SUCCESS
     doFindTest(*finder, isc::dns::Name("dname.example.org."),
@@ -1944,12 +1955,14 @@ TYPED_TEST(DatabaseClientTest, glueOK) {
                isc::dns::RRType::AAAA(), isc::dns::RRType::AAAA(),
                this->rrttl_, ZoneFinder::NXRRSET,
                this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("ns.delegation.example.org."),
                ZoneFinder::FIND_GLUE_OK);
     doFindTest(*finder, isc::dns::Name("nothere.delegation.example.org."),
                isc::dns::RRType::AAAA(), isc::dns::RRType::AAAA(),
                this->rrttl_, ZoneFinder::NXDOMAIN,
                this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("nothere.delegation.example.org."),
                ZoneFinder::FIND_GLUE_OK);
     this->expected_rdatas_.push_back("192.0.2.1");
@@ -1957,6 +1970,7 @@ TYPED_TEST(DatabaseClientTest, glueOK) {
                this->qtype_, this->qtype_,
                this->rrttl_, ZoneFinder::SUCCESS,
                this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("ns.delegation.example.org."),
                ZoneFinder::FIND_GLUE_OK);
     this->expected_rdatas_.clear();
@@ -1972,6 +1986,7 @@ TYPED_TEST(DatabaseClientTest, glueOK) {
                isc::dns::RRType::NS(), isc::dns::RRType::NS(),
                this->rrttl_, ZoneFinder::SUCCESS,
                this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("delegation.example.org."),
                ZoneFinder::FIND_GLUE_OK);
     this->expected_rdatas_.clear();
@@ -1983,12 +1998,12 @@ TYPED_TEST(DatabaseClientTest, glueOK) {
     doFindTest(*finder, isc::dns::Name("below.dname.example.org."),
                this->qtype_, isc::dns::RRType::DNAME(),
                this->rrttl_, ZoneFinder::DNAME, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("dname.example.org."), ZoneFinder::FIND_GLUE_OK);
     doFindTest(*finder, isc::dns::Name("below.dname.example.org."),
                isc::dns::RRType::AAAA(), isc::dns::RRType::DNAME(),
                this->rrttl_, ZoneFinder::DNAME, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("dname.example.org."), ZoneFinder::FIND_GLUE_OK);
 }
 
@@ -2003,21 +2018,24 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
                                          "FAKEFAKEFAKE");
     doFindTest(*finder, isc::dns::Name("a.wild.example.org"),
                this->qtype_, this->qtype_, this->rrttl_,
-               ZoneFinder::WILDCARD, this->expected_rdatas_,
-               this->expected_sig_rdatas_);
+               ZoneFinder::SUCCESS, this->expected_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_WILDCARD);
     doFindTest(*finder, isc::dns::Name("b.a.wild.example.org"),
-               this->qtype_, this->qtype_, this->rrttl_, ZoneFinder::WILDCARD,
-               this->expected_rdatas_, this->expected_sig_rdatas_);
+               this->qtype_, this->qtype_, this->rrttl_, ZoneFinder::SUCCESS,
+               this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_WILDCARD);
     this->expected_rdatas_.clear();
     this->expected_sig_rdatas_.clear();
     doFindTest(*finder, isc::dns::Name("a.wild.example.org"),
                isc::dns::RRType::AAAA(), isc::dns::RRType::AAAA(),
-               this->rrttl_, ZoneFinder::WILDCARD_NXRRSET,
-               this->expected_rdatas_, this->expected_sig_rdatas_);
+               this->rrttl_, ZoneFinder::NXRRSET,
+               this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_WILDCARD);
     doFindTest(*finder, isc::dns::Name("b.a.wild.example.org"),
                isc::dns::RRType::AAAA(), isc::dns::RRType::AAAA(),
-               this->rrttl_, ZoneFinder::WILDCARD_NXRRSET,
-               this->expected_rdatas_, this->expected_sig_rdatas_);
+               this->rrttl_, ZoneFinder::NXRRSET,
+               this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_WILDCARD);
 
     // Direct request for this wildcard
     this->expected_rdatas_.push_back("192.0.2.5");
@@ -2067,14 +2085,14 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
     doFindTest(*finder, isc::dns::Name("below.delegatedwild.example.org"),
                this->qtype_, isc::dns::RRType::NS(), this->rrttl_,
                ZoneFinder::DELEGATION, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("delegatedwild.example.org"));
     // FIXME: This doesn't look logically OK, GLUE_OK should make it transparent,
     // so the match should either work or be canceled, but return NXDOMAIN
     doFindTest(*finder, isc::dns::Name("below.delegatedwild.example.org"),
                this->qtype_, isc::dns::RRType::NS(), this->rrttl_,
                ZoneFinder::DELEGATION, this->expected_rdatas_,
-               this->expected_sig_rdatas_,
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
                isc::dns::Name("delegatedwild.example.org"),
                ZoneFinder::FIND_GLUE_OK);
 
@@ -2097,16 +2115,10 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
     this->expected_rdatas_.clear();
     const char* negative_names[] = {
         "a.foo.example.org.",
-        "*.foo.example.org.",
-        "foo.example.org.",
         "wild.bar.foo.example.org.",
         "baz.foo.*.bar.example.org",
         "baz.foo.baz.bar.example.org",
         "*.foo.baz.bar.example.org",
-        "*.foo.*.bar.example.org",
-        "foo.*.bar.example.org",
-        "*.bar.example.org",
-        "bar.example.org",
         NULL
     };
     // Unless FIND_DNSSEC is specified, this is no different from other
@@ -2114,10 +2126,11 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
     for (const char** name = negative_names; *name != NULL; ++ name) {
         doFindTest(*finder, isc::dns::Name(*name), this->qtype_,
                    this->qtype_, this->rrttl_, ZoneFinder::NXRRSET,
-                   this->expected_rdatas_, this->expected_sig_rdatas_);
+                   this->expected_rdatas_, this->expected_sig_rdatas_,
+                   ZoneFinder::RESULT_WILDCARD);
     }
 
-    // With FIND_DNSSEC, it should result in WILDCARD_NXRRSET.
+    // With FIND_DNSSEC, it should have NSEC_SIGNED flag.
     const char* negative_dnssec_names[] = {
         "a.bar.example.org.",
         "foo.baz.bar.example.org.",
@@ -2129,8 +2142,9 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
     this->expected_sig_rdatas_.clear();
     for (const char** name = negative_dnssec_names; *name != NULL; ++ name) {
         doFindTest(*finder, isc::dns::Name(*name), this->qtype_,
-                   RRType::NSEC(), this->rrttl_, ZoneFinder::WILDCARD_NXRRSET,
+                   RRType::NSEC(), this->rrttl_, ZoneFinder::NXRRSET,
                    this->expected_rdatas_, this->expected_sig_rdatas_,
+                   ZoneFinder::RESULT_WILDCARD | ZoneFinder::RESULT_NSEC_SIGNED,
                    Name("bao.example.org."), ZoneFinder::FIND_DNSSEC);
     }
 
@@ -2140,19 +2154,18 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
     this->expected_sig_rdatas_.clear();
     doFindTest(*finder, isc::dns::Name("a.cnamewild.example.org."),
                isc::dns::RRType::TXT(), isc::dns::RRType::CNAME(),
-               this->rrttl_, ZoneFinder::WILDCARD_CNAME,
-               this->expected_rdatas_, this->expected_sig_rdatas_);
+               this->rrttl_, ZoneFinder::CNAME,
+               this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_WILDCARD);
 
     // DNAME on a wildcard.  In our implementation we ignore DNAMEs on a
     // wildcard, but at a higher level we say the behavior is "unspecified".
     // rfc2672bis strongly discourages the mixture of DNAME and wildcard
     // (with SHOULD NOT).
-    this->expected_rdatas_.clear();
-    this->expected_sig_rdatas_.clear();
     doFindTest(*finder, Name("a.dnamewild.example.org."),
                this->qtype_, this->qtype_, this->rrttl_,
-               ZoneFinder::WILDCARD_NXRRSET, this->expected_rdatas_,
-               this->expected_sig_rdatas_);
+               ZoneFinder::NXRRSET, this->empty_rdatas_,
+               this->empty_rdatas_, ZoneFinder::RESULT_WILDCARD);
 
     // Some strange things in the wild node
     this->expected_rdatas_.clear();
@@ -2160,7 +2173,8 @@ TYPED_TEST(DatabaseClientTest, wildcard) {
     doFindTest(*finder, isc::dns::Name("a.nswild.example.org."),
                isc::dns::RRType::TXT(), isc::dns::RRType::NS(),
                this->rrttl_, ZoneFinder::DELEGATION,
-               this->expected_rdatas_, this->expected_sig_rdatas_);
+               this->expected_rdatas_, this->empty_rdatas_,
+               ZoneFinder::RESULT_WILDCARD);
 }
 
 TYPED_TEST(DatabaseClientTest, noWildcard) {
@@ -2178,7 +2192,8 @@ TYPED_TEST(DatabaseClientTest, noWildcard) {
     doFindTest(*finder, isc::dns::Name("a.wild.example.org"),
                RRType::NSEC(), RRType::NSEC(), this->rrttl_,
                ZoneFinder::NXDOMAIN, this->expected_rdatas_,
-               this->expected_sig_rdatas_, Name("*.wild.example.org."),
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_NSEC_SIGNED,
+               Name("*.wild.example.org."),
                ZoneFinder::FIND_DNSSEC | ZoneFinder::NO_WILDCARD);
 
     // Should be the same without FIND_DNSSEC (but in this case no RRsets
@@ -2186,7 +2201,8 @@ TYPED_TEST(DatabaseClientTest, noWildcard) {
     doFindTest(*finder, isc::dns::Name("a.wild.example.org"),
                RRType::NSEC(), RRType::NSEC(), this->rrttl_,
                ZoneFinder::NXDOMAIN, this->empty_rdatas_,
-               this->empty_rdatas_, Name::ROOT_NAME(), // name is dummy
+               this->empty_rdatas_, ZoneFinder::RESULT_DEFAULT,
+               Name::ROOT_NAME(), // name is dummy
                ZoneFinder::NO_WILDCARD);
 
     // Same for wildcard empty non terminal.
@@ -2195,7 +2211,8 @@ TYPED_TEST(DatabaseClientTest, noWildcard) {
     doFindTest(*finder, isc::dns::Name("a.bar.example.org"),
                RRType::NSEC(), RRType::NSEC(), this->rrttl_,
                ZoneFinder::NXDOMAIN, this->expected_rdatas_,
-               this->empty_rdatas_, Name("wild.*.foo.*.bar.example.org"),
+               this->empty_rdatas_, ZoneFinder::RESULT_NSEC_SIGNED,
+               Name("wild.*.foo.*.bar.example.org"),
                ZoneFinder::FIND_DNSSEC | ZoneFinder::NO_WILDCARD);
 
     // Search for a wildcard name with NO_WILDCARD.  There should be no
@@ -2206,7 +2223,8 @@ TYPED_TEST(DatabaseClientTest, noWildcard) {
     doFindTest(*finder, isc::dns::Name("*.nonterminal.example.org"),
                RRType::NSEC(), RRType::NSEC(), this->rrttl_,
                ZoneFinder::NXDOMAIN, this->expected_rdatas_,
-               this->empty_rdatas_, Name("l.example.org"),
+               this->empty_rdatas_, ZoneFinder::RESULT_NSEC_SIGNED,
+               Name("l.example.org"),
                ZoneFinder::FIND_DNSSEC | ZoneFinder::NO_WILDCARD);
 
     // On the other hand, if there's exact match for the wildcard name
@@ -2220,8 +2238,8 @@ TYPED_TEST(DatabaseClientTest, noWildcard) {
     doFindTest(*finder, isc::dns::Name("*.wild.example.org"),
                this->qtype_, this->qtype_, this->rrttl_,
                ZoneFinder::SUCCESS, this->expected_rdatas_,
-               this->expected_sig_rdatas_, Name("*.wild.example.org"),
-               ZoneFinder::NO_WILDCARD);
+               this->expected_sig_rdatas_, ZoneFinder::RESULT_DEFAULT,
+               Name("*.wild.example.org"), ZoneFinder::NO_WILDCARD);
 }
 
 TYPED_TEST(DatabaseClientTest, NXRRSET_NSEC) {
@@ -2237,7 +2255,8 @@ TYPED_TEST(DatabaseClientTest, NXRRSET_NSEC) {
                isc::dns::RRType::TXT(), isc::dns::RRType::NSEC(),
                this->rrttl_, ZoneFinder::NXRRSET,
                this->expected_rdatas_, this->expected_sig_rdatas_,
-               Name::ROOT_NAME(), ZoneFinder::FIND_DNSSEC);
+               ZoneFinder::RESULT_NSEC_SIGNED, Name::ROOT_NAME(),
+               ZoneFinder::FIND_DNSSEC);
 }
 
 TYPED_TEST(DatabaseClientTest, wildcardNXRRSET_NSEC) {
@@ -2256,8 +2275,9 @@ TYPED_TEST(DatabaseClientTest, wildcardNXRRSET_NSEC) {
     // Note that the NSEC name should NOT be synthesized.
     doFindTest(*finder, isc::dns::Name("a.wild.example.org."),
                isc::dns::RRType::TXT(), isc::dns::RRType::NSEC(),
-               this->rrttl_, ZoneFinder::WILDCARD_NXRRSET,
+               this->rrttl_, ZoneFinder::NXRRSET,
                this->expected_rdatas_, this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_WILDCARD | ZoneFinder::RESULT_NSEC_SIGNED,
                Name("*.wild.example.org"), ZoneFinder::FIND_DNSSEC);
 }
 
@@ -2273,7 +2293,8 @@ TYPED_TEST(DatabaseClientTest, NXDOMAIN_NSEC) {
                isc::dns::RRType::TXT(), isc::dns::RRType::NSEC(),
                this->rrttl_, ZoneFinder::NXDOMAIN,
                this->expected_rdatas_, this->expected_sig_rdatas_,
-               Name("www.example.org."), ZoneFinder::FIND_DNSSEC);
+               ZoneFinder::RESULT_NSEC_SIGNED, Name("www.example.org."),
+               ZoneFinder::FIND_DNSSEC);
     this->expected_rdatas_.clear();
     this->expected_rdatas_.push_back("acnamesig1.example.org. NS A NSEC RRSIG");
     // This tests it works correctly in apex (there was a bug, where a check
@@ -2282,7 +2303,8 @@ TYPED_TEST(DatabaseClientTest, NXDOMAIN_NSEC) {
                isc::dns::RRType::TXT(), isc::dns::RRType::NSEC(),
                this->rrttl_, ZoneFinder::NXDOMAIN,
                this->expected_rdatas_, this->expected_sig_rdatas_,
-               Name("example.org."), ZoneFinder::FIND_DNSSEC);
+               ZoneFinder::RESULT_NSEC_SIGNED, Name("example.org."),
+               ZoneFinder::FIND_DNSSEC);
 
     // Check that if the DB doesn't support it, the exception from there
     // is not propagated and it only does not include the NSEC
@@ -2294,8 +2316,9 @@ TYPED_TEST(DatabaseClientTest, NXDOMAIN_NSEC) {
                                isc::dns::RRType::TXT(),
                                isc::dns::RRType::NSEC(), this->rrttl_,
                                ZoneFinder::NXDOMAIN, this->empty_rdatas_,
-                               this->empty_rdatas_, Name::ROOT_NAME(),
-                               ZoneFinder::FIND_DNSSEC));
+                               this->empty_rdatas_,
+                               ZoneFinder::RESULT_DEFAULT,
+                               Name::ROOT_NAME(), ZoneFinder::FIND_DNSSEC));
 }
 
 TYPED_TEST(DatabaseClientTest, emptyNonterminalNSEC) {
@@ -2305,9 +2328,10 @@ TYPED_TEST(DatabaseClientTest, emptyNonterminalNSEC) {
     this->expected_rdatas_.push_back("empty.nonterminal.example.org. NSEC");
     doFindTest(*finder, isc::dns::Name("nonterminal.example.org."),
                isc::dns::RRType::TXT(), isc::dns::RRType::NSEC(), this->rrttl_,
-               ZoneFinder::NXRRSET,
-               this->expected_rdatas_, this->expected_sig_rdatas_,
-               Name("l.example.org."), ZoneFinder::FIND_DNSSEC);
+               ZoneFinder::NXRRSET, this->expected_rdatas_,
+               this->expected_sig_rdatas_,
+               ZoneFinder::RESULT_NSEC_SIGNED, Name("l.example.org."),
+               ZoneFinder::FIND_DNSSEC);
 
     // Check that if the DB doesn't support it, the exception from there
     // is not propagated and it only does not include the NSEC
@@ -2320,6 +2344,7 @@ TYPED_TEST(DatabaseClientTest, emptyNonterminalNSEC) {
                                isc::dns::RRType::NSEC(),
                                this->rrttl_, ZoneFinder::NXRRSET,
                                this->empty_rdatas_, this->empty_rdatas_,
+                               ZoneFinder::RESULT_DEFAULT,
                                Name::ROOT_NAME(), ZoneFinder::FIND_DNSSEC));
 }
 
@@ -2387,9 +2412,13 @@ TYPED_TEST(DatabaseClientTest, getAll) {
 
     // And on wildcard. Check the signatures as well.
     target.clear();
-    EXPECT_EQ(ZoneFinder::WILDCARD,
-              finder->findAll(isc::dns::Name("a.wild.example.org"),
-                              target, ZoneFinder::FIND_DNSSEC).code);
+    const ZoneFinder::FindResult result =
+        finder->findAll(Name("a.wild.example.org"), target,
+                        ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::SUCCESS, result.code);
+    EXPECT_TRUE(result.isWildcard());
+    EXPECT_TRUE(result.isNSECSigned());
+    EXPECT_FALSE(result.isNSEC3Signed());
     ASSERT_EQ(2, target.size());
     a_idx = target[1]->getType() == RRType::A();
     EXPECT_EQ(RRType::A(), target[a_idx]->getType());

+ 154 - 11
src/lib/datasrc/tests/memory_datasrc_unittest.cc

@@ -33,12 +33,15 @@
 #include <datasrc/data_source.h>
 #include <datasrc/iterator.h>
 
+#include <testutils/dnsmessage_test.h>
+
 #include <gtest/gtest.h>
 
 using namespace std;
 using namespace isc::dns;
 using namespace isc::dns::rdata;
 using namespace isc::datasrc;
+using namespace isc::testutils;
 
 namespace {
 // Commonly used result codes (Who should write the prefix all the time)
@@ -251,6 +254,20 @@ TEST_F(InMemoryClientTest, startUpdateZone) {
                  isc::NotImplemented);
 }
 
+// Commonly used RRSIG data
+const char* const rrsig_a_txt =
+    "example.org. 300 IN RRSIG A 5 3 3600 20000101000000 20000201000000 12345 "
+    "example.org. FAKEFAKEFAKE\n";
+const char* const rrsig_ns_txt =
+    "example.org. 300 IN RRSIG NS 5 3 3600 20000101000000 20000201000000 "
+    "54321 example.org. FAKEFAKEFAKEFAKE\n";
+// This RRset has two RRSIGs
+const char* const rrsig_aaaa_txt =
+    "ns.example.org. 300 IN RRSIG AAAA 5 3 3600 20000101000000 20000201000000 "
+    "12345 example.org. FAKEFAKEFAKE\n"
+    "ns.example.org. 300 IN RRSIG AAAA 5 3 3600 20000101000000 20000201000000 "
+    "54321 example.org. FAKEFAKEFAKEFAKE\n";
+
 // A helper callback of masterLoad() used in InMemoryZoneFinderTest.
 void
 setRRset(RRsetPtr rrset, vector<RRsetPtr*>::iterator& it) {
@@ -291,6 +308,8 @@ public:
             {"example.org. 300 IN DNAME example.com.", &rr_dname_apex_},
             {"child.example.org. 300 IN NS ns.child.example.org.",
              &rr_child_ns_},
+            {"child.example.org. 300 IN DS 12345 5 1 DEADBEEF",
+             &rr_child_ds_},
             {"ns.child.example.org. 300 IN A 192.0.2.153",
              &rr_child_glue_},
             {"grand.child.example.org. 300 IN NS ns.grand.child.example.org.",
@@ -322,9 +341,8 @@ public:
             rrsets.push_back(zone_data[i].rrset);
         }
 
-        vector<RRsetPtr*>::iterator it = rrsets.begin();
         masterLoad(zone_data_stream, Name::ROOT_NAME(), class_,
-                   boost::bind(setRRset, _1, it));
+                   boost::bind(setRRset, _1, rrsets.begin()));
     }
     // Some data to test with
     const RRClass class_;
@@ -332,6 +350,9 @@ public:
     // The zone finder to torture by tests
     InMemoryZoneFinder zone_finder_;
 
+    // Placeholder for storing RRsets to be checked with rrsetsCheck()
+    vector<ConstRRsetPtr> actual_rrsets_;
+
     /*
      * Some RRsets to put inside the zone.
      */
@@ -353,6 +374,7 @@ public:
     RRsetPtr rr_dname_ns_; // for mixed DNAME + NS case
     RRsetPtr rr_dname_apex_; // for mixed DNAME + NS case in the apex
     RRsetPtr rr_child_ns_; // NS of a child domain (for delegation)
+    RRsetPtr rr_child_ds_; // DS of a child domain (for delegation, auth data)
     RRsetPtr rr_child_glue_; // glue RR of the child domain
     RRsetPtr rr_grandchild_ns_; // NS below a zone cut (unusual)
     RRsetPtr rr_grandchild_glue_; // glue RR below a deeper zone cut
@@ -466,6 +488,18 @@ public:
     }
     // Internal part of the cancelWildcard test that is multiple times
     void doCancelWildcardTest();
+
+    ConstRRsetPtr textToRRset(const string& text_rrset,
+                              const RRClass& rrclass = RRClass::IN()) const
+    {
+        stringstream ss(text_rrset);
+        RRsetPtr rrset;
+        vector<RRsetPtr*> rrsets;
+        rrsets.push_back(&rrset);
+        masterLoad(ss, Name::ROOT_NAME(), rrclass,
+                   boost::bind(setRRset, _1, rrsets.begin()));
+        return (rrset);
+    }
 };
 
 /**
@@ -543,9 +577,8 @@ TEST_F(InMemoryZoneFinderTest, findCNAMEUnderZoneCut) {
     // (with FIND_GLUE_OK).  The behavior is different from BIND 9,
     // so we test this case explicitly.
     EXPECT_EQ(SUCCESS, zone_finder_.add(rr_child_ns_));
-    RRsetPtr rr_cname_under_cut_(new RRset(Name("cname.child.example.org"),
-                                           class_, RRType::CNAME(),
-                                           RRTTL(300)));
+    ConstRRsetPtr rr_cname_under_cut_ = textToRRset(
+        "cname.child.example.org. 300 IN CNAME target.child.example.org.");
     EXPECT_EQ(SUCCESS, zone_finder_.add(rr_cname_under_cut_));
     findTest(Name("cname.child.example.org"), RRType::AAAA(),
              ZoneFinder::CNAME, true, rr_cname_under_cut_, NULL,
@@ -656,6 +689,26 @@ TEST_F(InMemoryZoneFinderTest, delegationNS) {
              ZoneFinder::DELEGATION, true, rr_child_ns_);
 }
 
+TEST_F(InMemoryZoneFinderTest, delegationWithDS) {
+    // Similar setup to the previous one, but with DS RR at the delegation
+    // point.
+    EXPECT_EQ(SUCCESS, zone_finder_.add(rr_ns_));
+    EXPECT_EQ(SUCCESS, zone_finder_.add(rr_child_ns_));
+    EXPECT_EQ(SUCCESS, zone_finder_.add(rr_child_ds_));
+
+    // Normal types of query should result in delegation, but DS query
+    // should be considered in-zone.
+    findTest(Name("child.example.org"), RRType::A(), ZoneFinder::DELEGATION,
+             true, rr_child_ns_);
+    findTest(Name("child.example.org"), RRType::DS(), ZoneFinder::SUCCESS,
+             true, rr_child_ds_);
+
+    // There's nothing special for DS query at the zone apex.  It would
+    // normally result in NXRRSET.
+    findTest(Name("example.org"), RRType::DS(), ZoneFinder::NXRRSET,
+             true, ConstRRsetPtr());
+}
+
 TEST_F(InMemoryZoneFinderTest, findAny) {
     EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_finder_.add(rr_a_)));
     EXPECT_NO_THROW(EXPECT_EQ(SUCCESS, zone_finder_.add(rr_ns_)));
@@ -776,11 +829,11 @@ TEST_F(InMemoryZoneFinderTest, emptyNode) {
 
     // Construct the test zone
     const char* const names[] = {
-        "bar.example.org", "x.foo.example.org", "aaa.baz.example.org",
+        "bar.example.org.", "x.foo.example.org.", "aaa.baz.example.org.",
         "bbb.baz.example.org.", NULL};
     for (int i = 0; names[i] != NULL; ++i) {
-        ConstRRsetPtr rrset(new RRset(Name(names[i]), class_, RRType::A(),
-                                      RRTTL(300)));
+        ConstRRsetPtr rrset = textToRRset(string(names[i]) +
+                                          " 300 IN A 192.0.2.1");
         EXPECT_EQ(SUCCESS, zone_finder_.add(rrset));
     }
 
@@ -1120,9 +1173,8 @@ TEST_F(InMemoryZoneFinderTest, swap) {
     ASSERT_NE(origin_, other_origin); // make sure these two are different
     InMemoryZoneFinder finder2(RRClass::CH(), other_origin);
     EXPECT_EQ(result::SUCCESS,
-              finder2.add(RRsetPtr(new RRset(Name("version.bind"),
-                                           RRClass::CH(), RRType::TXT(),
-                                           RRTTL(0)))));
+              finder2.add(textToRRset("version.bind. 0 CH TXT \"test\"",
+                                      RRClass::CH())));
 
     finder1.swap(finder2);
     EXPECT_EQ(other_origin, finder1.getOrigin());
@@ -1164,4 +1216,95 @@ TEST_F(InMemoryZoneFinderTest, getFileName) {
     EXPECT_EQ(TEST_DATA_DIR "/root.zone", zone_finder_.getFileName());
     EXPECT_TRUE(rootzone.getFileName().empty());
 }
+
+TEST_F(InMemoryZoneFinderTest, addRRsig) {
+    // A simple valid case: adding an RRset to be signed followed by an RRSIG
+    // that covers the first RRset
+    zone_finder_.add(rr_a_);
+    zone_finder_.add(textToRRset(rrsig_a_txt));
+    ZoneFinder::FindResult result = zone_finder_.find(origin_, RRType::A(),
+                                                      ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::SUCCESS, result.code);
+    ASSERT_TRUE(result.rrset);
+    ASSERT_TRUE(result.rrset->getRRsig());
+    actual_rrsets_.push_back(result.rrset->getRRsig());
+    rrsetsCheck(rrsig_a_txt, actual_rrsets_.begin(), actual_rrsets_.end());
+
+    // Confirm a separate RRISG for a different type can be added
+    actual_rrsets_.clear();
+    zone_finder_.add(rr_ns_);
+    zone_finder_.add(textToRRset(rrsig_ns_txt));
+    ZoneFinder::FindResult result2 =
+        zone_finder_.find(origin_, RRType::NS(), ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::SUCCESS, result2.code);
+    ASSERT_TRUE(result2.rrset);
+    ASSERT_TRUE(result2.rrset->getRRsig());
+    actual_rrsets_.push_back(result2.rrset->getRRsig());
+    rrsetsCheck(rrsig_ns_txt, actual_rrsets_.begin(), actual_rrsets_.end());
+
+    // Check a case with multiple RRSIGs
+    actual_rrsets_.clear();
+    zone_finder_.add(rr_ns_aaaa_);
+    zone_finder_.add(textToRRset(rrsig_aaaa_txt));
+    ZoneFinder::FindResult result3 =
+        zone_finder_.find(Name("ns.example.org"), RRType::AAAA(),
+                          ZoneFinder::FIND_DNSSEC);
+    EXPECT_EQ(ZoneFinder::SUCCESS, result3.code);
+    ASSERT_TRUE(result3.rrset);
+    ASSERT_TRUE(result3.rrset->getRRsig());
+    actual_rrsets_.push_back(result3.rrset->getRRsig());
+    rrsetsCheck(rrsig_aaaa_txt, actual_rrsets_.begin(), actual_rrsets_.end());
+}
+
+TEST_F(InMemoryZoneFinderTest, addRRsigWithoutCovered) {
+    // The current implementation rejects attempts of adding RRSIG without
+    // covered RRsets already in the zone.
+
+    // Name doesn't exist
+    EXPECT_THROW(zone_finder_.add(
+                     textToRRset("notexist.example.org. 300 IN RRSIG A 5 3 "
+                                 "3600 20000101000000 20000201000000 12345 "
+                                 "example.org. FAKEFAKEFAKE\n")),
+                 InMemoryZoneFinder::AddError);
+
+    // Name exists, but is empty.
+    zone_finder_.add(rr_emptywild_);
+    EXPECT_THROW(zone_finder_.add(
+                     textToRRset("foo.example.org. 300 IN RRSIG A 5 3 "
+                                 "3600 20000101000000 20000201000000 12345 "
+                                 "example.org. FAKEFAKEFAKE\n")),
+                 InMemoryZoneFinder::AddError);
+
+    // Add RRSIG RRset without covered RR
+    zone_finder_.add(rr_a_);
+    EXPECT_THROW(zone_finder_.add(textToRRset(rrsig_ns_txt)),
+                 InMemoryZoneFinder::AddError);
+}
+
+TEST_F(InMemoryZoneFinderTest, addbadRRsig) {
+    // Tests with other types of bogus input
+
+    // Empty RRSIG RRset.
+    EXPECT_THROW(zone_finder_.add(RRsetPtr(new RRset(origin_, class_,
+                                                     RRType::RRSIG(),
+                                                     RRTTL(300)))),
+                                  InMemoryZoneFinder::AddError);
+
+    // RRSIG with mixed covered types
+    zone_finder_.add(rr_a_);    // make sure the covered name exists
+    // textToRRset() doesn't work as intended for this pathological case,
+    // so we need to construct the RRset by hand.
+    RRsetPtr rrset(new RRset(origin_, class_, RRType::RRSIG(), RRTTL(300)));
+    rrset->addRdata(generic::RRSIG("A 5 3 3600 20000101000000 20000201000000 "
+                                   "12345 example.org. FAKEFAKEFAKE"));
+    rrset->addRdata(generic::RRSIG("NS 5 3 3600 20000101000000 20000201000000 "
+                                   "54321 example.org. FAKEFAKEFAKEFAKE"));
+    EXPECT_THROW(zone_finder_.add(rrset), InMemoryZoneFinder::AddError);
+
+    // An attempt of overriding an existing RRSIG.  The current implementation
+    // prohibits that.
+    zone_finder_.add(textToRRset(rrsig_a_txt));
+    EXPECT_THROW(zone_finder_.add(textToRRset(rrsig_a_txt)),
+                 InMemoryZoneFinder::AddError);
+}
 }

+ 273 - 92
src/lib/datasrc/zone.h

@@ -15,6 +15,9 @@
 #ifndef __ZONE_H
 #define __ZONE_H 1
 
+#include <utility>
+#include <vector>
+
 #include <dns/rrset.h>
 #include <dns/rrsetlist.h>
 
@@ -55,103 +58,52 @@ public:
     /// Note: the codes are tentative.  We may need more, or we may find
     /// some of them unnecessary as we implement more details.
     ///
-    /// Some are synonyms of others in terms of RCODE returned to user.
-    /// But they help the logic to decide if it should ask for a NSEC
-    /// that covers something or not (for example, in case of NXRRSET,
-    /// the directly returned NSEC is sufficient, but with wildcard one,
-    /// we need to add one proving there's no exact match and this is
-    /// actually the best wildcard we have). Data sources that don't
-    /// support DNSSEC don't need to distinguish them.
-    ///
-    /// In case of CNAME, if the CNAME is a wildcard (i.e., its owner name
-    /// starts with the label "*"), WILDCARD_CNAME will be returned instead
-    /// of CNAME.
-    ///
-    /// In case of NXDOMAIN, the returned NSEC covers the queried domain
-    /// that proves that the query name does not exist in the zone.  Note that
-    /// this does not necessarily prove it doesn't even match a wildcard
-    /// (even if the result of NXDOMAIN can only happen when there's no
-    /// matching wildcard either).  It is caller's responsibility to provide
-    /// a proof that there is no matching wildcard if that proof is necessary.
-    ///
-    /// Various variants of "no data" cases are complicated, when involves
-    /// DNSSEC and wildcard processing.  Referring to Section 3.1.3 of
-    /// RFC4035, we need to consider the following cases:
-    /// -# (Normal) no data: there is a matching non-wildcard name with a
-    ///    different RR type.  This is the "No Data" case of the RFC.
-    /// -# (Normal) empty non terminal: there is no matching (exact or
-    ///    wildcard) name, but there is a subdomain with an RR of the query
-    ///    name.  This is one case of "Name Error" of the RFC.
-    /// -# Wildcard empty non terminal: similar to 2a, but the empty name
-    ///    is a wildcard, and matches the query name by wildcard expansion.
-    ///    This is a special case of "Name Error" of the RFC.
-    /// -# Wildcard no data: there is no exact match name, but there is a
-    ///    wildcard name that matches the query name with a different type
-    ///    of RR.  This is the "Wildcard No Data" case of the RFC.
-    ///
-    /// In any case, \c find() will result in \c NXRRSET with no RRset
-    /// unless the \c FIND_DNSSEC option is specified.  The rest of the
-    /// discussion only applies to the case where this option is specified.
-    ///
-    /// In case 1, \c find() will result in NXRRSET, and return NSEC of the
-    /// matching name.
-    ///
-    /// In case 2, \c find() will result in NXRRSET, and return NSEC for the
-    /// interval where the empty nonterminal lives. The end of the interval
-    /// is the subdomain causing existence of the empty nonterminal (if
-    /// there's sub.x.example.com, and no record in x.example.com, then
-    /// x.example.com exists implicitly - is the empty nonterminal and
-    /// sub.x.example.com is the subdomain causing it).  Note that this NSEC
-    /// proves not only the existence of empty non terminal name but also
-    /// the non existence of possibly matching wildcard name, because
-    /// there can be no better wildcard match than the exact matching empty
-    /// name.
-    ///
-    /// In case 3, \c find() will result in WILDCARD_NXRRSET, and return NSEC
-    /// for the interval where the wildcard empty nonterminal lives.
-    /// Cases 2 and 3 are especially complicated and confusing.  See the
-    /// examples below.
-    ///
-    /// In case 4, \c find() will result in WILDCARD_NXRRSET, and return
-    /// NSEC of the matching wildcard name.
-    ///
-    /// Examples: if zone "example.com" has the following record:
-    /// \code
-    /// a.example.com. NSEC a.b.example.com.
-    /// \endcode
-    /// a call to \c find() for "b.example.com." with the FIND_DNSSEC option
-    /// will result in NXRRSET, and this NSEC will be returned.
-    /// Likewise, if zone "example.org" has the following record,
-    /// \code
-    /// a.example.org. NSEC x.*.b.example.org.
-    /// \endcode
-    /// a call to \c find() for "y.b.example.org" with FIND_DNSSEC will
-    /// result in NXRRSET_NXRRSET, and this NSEC will be returned.
+    /// See the description of \c find() for further details of how
+    /// these results should be interpreted.
     enum Result {
         SUCCESS,                ///< An exact match is found.
         DELEGATION,             ///< The search encounters a zone cut.
         NXDOMAIN, ///< There is no domain name that matches the search name
         NXRRSET,  ///< There is a matching name but no RRset of the search type
         CNAME,    ///< The search encounters and returns a CNAME RR
-        DNAME,    ///< The search encounters and returns a DNAME RR
-        WILDCARD, ///< Succes by wildcard match, for DNSSEC
-        WILDCARD_CNAME, ///< CNAME on wildcard, search returns CNAME, for DNSSEC
-        WILDCARD_NXRRSET ///< NXRRSET on wildcard, for DNSSEC
+        DNAME    ///< The search encounters and returns a DNAME RR
+    };
+
+    /// Special attribute flags on the result of the \c find() method
+    ///
+    /// The flag values defined here are intended to signal to the caller
+    /// that it may need special handling on the result.  This is particularly
+    /// of concern when DNSSEC is requested.  For example, for negative
+    /// responses the caller would want to know whether the zone is signed
+    /// with NSEC or NSEC3 so that it can subsequently provide necessary
+    /// proof of the result.
+    ///
+    /// The caller is generally expected to get access to the information
+    /// via read-only getter methods of \c FindResult so that it won't rely
+    /// on specific details of the representation of the flags.  So these
+    /// definitions are basically only meaningful for data source
+    /// implementations.
+    enum FindResultFlags {
+        RESULT_DEFAULT = 0,       ///< The default flags
+        RESULT_WILDCARD = 1,      ///< find() resulted in a wildcard match
+        RESULT_NSEC_SIGNED = 2,   ///< The zone is signed with NSEC RRs
+        RESULT_NSEC3_SIGNED = 4   ///< The zone is signed with NSEC3 RRs
     };
 
     /// A helper structure to represent the search result of \c find().
     ///
     /// This is a straightforward tuple of the result code and a pointer
-    /// to the found RRset to represent the result of \c find()
-    /// (there will be more members in the future - see the class
-    /// description).
+    /// (and optionally special flags) to the found RRset to represent the
+    /// result of \c find() (there will be more members in the future -
+    /// see the class description).
     /// We use this in order to avoid overloading the return value for both
     /// the result code ("success" or "not found") and the found object,
     /// i.e., avoid using \c NULL to mean "not found", etc.
     ///
     /// This is a simple value class whose internal state never changes,
-    /// so for convenience we allow the applications to refer to the members
-    /// directly.
+    /// so for convenience we allow the applications to refer to some of the
+    /// members directly.  For others we provide read-only accessor methods
+    /// to hide specific representation.
     ///
     /// Note: we should eventually include a notion of "zone node", which
     /// corresponds to a particular domain name of the zone, so that we can
@@ -162,11 +114,39 @@ public:
     /// optimize including the NSEC for no-wildcard proof (FWIW NSD does that).
     struct FindResult {
         FindResult(Result param_code,
-                   const isc::dns::ConstRRsetPtr param_rrset) :
-            code(param_code), rrset(param_rrset)
+                   const isc::dns::ConstRRsetPtr param_rrset,
+                   FindResultFlags param_flags = RESULT_DEFAULT) :
+            code(param_code), rrset(param_rrset), flags(param_flags)
         {}
         const Result code;
         const isc::dns::ConstRRsetPtr rrset;
+
+        /// Return true iff find() results in a wildcard match.
+        bool isWildcard() const { return ((flags & RESULT_WILDCARD) != 0); }
+
+        /// Return true when the underlying zone is signed with NSEC.
+        ///
+        /// The \c find() implementation allows this to return false if
+        /// \c FIND_DNSSEC isn't specified regardless of whether the zone
+        /// is signed or which of NSEC/NSEC3 is used.
+        ///
+        /// When this is returned, the implementation of find() must ensure
+        /// that \c rrset be a valid NSEC RRset as described in \c find()
+        /// documentation.
+        bool isNSECSigned() const {
+            return ((flags & RESULT_NSEC_SIGNED) != 0);
+        }
+
+        /// Return true when the underlying zone is signed with NSEC3.
+        ///
+        /// The \c find() implementation allows this to return false if
+        /// \c FIND_DNSSEC isn't specified regardless of whether the zone
+        /// is signed or which of NSEC/NSEC3 is used.
+        bool isNSEC3Signed() const {
+            return ((flags & RESULT_NSEC3_SIGNED) != 0);
+        }
+    private:
+        FindResultFlags flags;
     };
 
     /// Find options.
@@ -224,12 +204,10 @@ public:
     ///
     /// - If the search name belongs under a zone cut, it returns the code
     ///   of \c DELEGATION and the NS RRset at the zone cut.
-    /// - If there is no matching name, it returns the code of \c NXDOMAIN,
-    ///   and, if DNSSEC is requested, the NSEC RRset that proves the
-    ///   non-existence.
+    /// - If there is no matching name, it returns the code of \c NXDOMAIN.
     /// - If there is a matching name but no RRset of the search type, it
-    ///   returns the code of \c NXRRSET, and, if DNSSEC is required,
-    ///   the NSEC RRset for that name.
+    ///   returns the code of \c NXRRSET.  This case includes the search name
+    ///   matches an empty node of the zone.
     /// - If there is a CNAME RR of the searched name but there is no
     ///   RR of the searched type of the name (so this type is different from
     ///   CNAME), it returns the code of \c CNAME and that CNAME RR.
@@ -238,6 +216,16 @@ public:
     /// - If the search name matches a delegation point of DNAME, it returns
     ///   the code of \c DNAME and that DNAME RR.
     ///
+    /// No RRset will be returned in the \c NXDOMAIN and \c NXRRSET cases
+    /// (\c rrset member of \c FindResult will be NULL), unless DNSSEC data
+    /// are required.  See below for the cases with DNSSEC.
+    ///
+    /// The returned \c FindResult object can also provide supplemental
+    /// information about the search result via its methods returning a
+    /// boolean value.  Such information may be useful for the caller if
+    /// the caller wants to collect additional DNSSEC proofs based on the
+    /// search result.
+    ///
     /// The \c options parameter specifies customized behavior of the search.
     /// Their semantics is as follows (they are or bit-field):
     ///
@@ -267,6 +255,86 @@ public:
     /// future version.  In any case applications shouldn't call this method
     /// for an out-of-zone name.
     ///
+    /// <b>DNSSEC considerations:</b>
+    /// The result when DNSSEC data are required can be very complicated,
+    /// especially if it involves negative result or wildcard match.
+    /// Specifically, if an application calls this method for DNS query
+    /// processing with DNSSEC data, and if the search result code is
+    /// either \c NXDOMAIN or \c NXRRRSET, and/or \c isWildcard() returns
+    /// true, then the application will need to find additional NSEC or
+    /// NSEC3 records for supplemental proofs.  This method helps the
+    /// application for such post search processing.
+    ///
+    /// First, it tells the application whether the zone is signed with
+    /// NSEC or NSEC3 via the \c isNSEC(3)Signed() method.  Any sanely signed
+    /// zone should be signed with either (and only one) of these two types
+    /// of RRs; however, the application should expect that the zone could
+    /// be broken and these methods could both return false.  But this method
+    /// should ensure that not both of these methods return true.
+    ///
+    /// In case it's signed with NSEC3, there is no further information
+    /// returned from this method.
+    ///
+    /// In case it's signed with NSEC, this method will possibly return
+    /// a related NSEC RRset in the \c rrset member of \c FindResult.
+    /// What kind of NSEC is returned depends on the result code
+    /// (\c NXDOMAIN or \c NXRRSET) and on whether it's a wildcard match:
+    ///
+    /// - In case of NXDOMAIN, the returned NSEC covers the queried domain
+    ///   that proves that the query name does not exist in the zone.  Note
+    ///   that this does not necessarily prove it doesn't even match a
+    ///   wildcard (even if the result of NXDOMAIN can only happen when
+    ///   there's no matching wildcard either).  It is caller's
+    ///   responsibility to provide a proof that there is no matching
+    ///   wildcard if that proof is necessary.
+    /// - In case of NXRRSET, we need to consider the following cases
+    ///   referring to Section 3.1.3 of RFC4035:
+    ///
+    /// -# (Normal) no data: there is a matching non-wildcard name with a
+    ///    different RR type.  This is the "No Data" case of the RFC.
+    /// -# (Normal) empty non terminal: there is no matching (exact or
+    ///    wildcard) name, but there is a subdomain with an RR of the query
+    ///    name.  This is one case of "Name Error" of the RFC.
+    /// -# Wildcard empty non terminal: similar to 2a, but the empty name
+    ///    is a wildcard, and matches the query name by wildcard expansion.
+    ///    This is a special case of "Name Error" of the RFC.
+    /// -# Wildcard no data: there is no exact match name, but there is a
+    ///    wildcard name that matches the query name with a different type
+    ///    of RR.  This is the "Wildcard No Data" case of the RFC.
+    ///
+    /// In case 1, \c find() returns NSEC of the matching name.
+    ///
+    /// In case 2, \c find() will return NSEC for the interval where the
+    /// empty nonterminal lives. The end of the interval is the subdomain
+    /// causing existence of the empty nonterminal (if there's
+    /// sub.x.example.com, and no record in x.example.com, then
+    /// x.example.com exists implicitly - is the empty nonterminal and
+    /// sub.x.example.com is the subdomain causing it).  Note that this NSEC
+    /// proves not only the existence of empty non terminal name but also
+    /// the non existence of possibly matching wildcard name, because
+    /// there can be no better wildcard match than the exact matching empty
+    /// name.
+    ///
+    /// In case 3, \c find() will return NSEC for the interval where the
+    /// wildcard empty nonterminal lives.   Cases 2 and 3 are especially
+    /// complicated and confusing.  See the examples below.
+    ///
+    /// In case 4, \c find() will return NSEC of the matching wildcard name.
+    ///
+    /// Examples: if zone "example.com" has the following record:
+    /// \code
+    /// a.example.com. NSEC a.b.example.com.
+    /// \endcode
+    /// a call to \c find() for "b.example.com." with the FIND_DNSSEC option
+    /// will result in NXRRSET, and this NSEC will be returned.
+    /// Likewise, if zone "example.org" has the following record,
+    /// \code
+    /// a.example.org. NSEC x.*.b.example.org.
+    /// \endcode
+    /// a call to \c find() for "y.b.example.org" with FIND_DNSSEC will
+    /// result in NXRRSET and this NSEC; \c isWildcard() on the returned
+    /// \c FindResult object will return true.
+    ///
     /// \exception std::bad_alloc Memory allocation such as for constructing
     ///  the resulting RRset fails
     /// \exception DataSourceError Derived class specific exception, e.g.
@@ -291,12 +359,12 @@ public:
     ///
     /// This function works almost exactly in the same way as the find one. The
     /// only difference is, when the lookup is successful (eg. the code is
-    /// SUCCESS or WILDCARD), all the RRsets residing in the named node are
+    /// SUCCESS), all the RRsets residing in the named node are
     /// copied into the \c target parameter and the rrset member of the result
     /// is NULL. All the other (unsuccessful) cases are handled the same,
-    /// including returning delegations, NSEC/NSEC3 proofs, etc. The options
-    /// parameter works the same way and it should conform to the same exception
-    /// restrictions.
+    /// including returning delegations, NSEC/NSEC3 availability and NSEC
+    /// proofs, wildcard information etc. The options parameter works the
+    /// same way and it should conform to the same exception restrictions.
     ///
     /// \param name \see find, parameter name
     /// \param target the successfull result is returned through this
@@ -306,6 +374,107 @@ public:
                                std::vector<isc::dns::ConstRRsetPtr> &target,
                                const FindOptions options = FIND_DEFAULT) = 0;
 
+    /// A helper structure to represent the search result of \c findNSEC3().
+    ///
+    /// The idea is similar to that of \c FindResult, but \c findNSEC3() has
+    /// special interface and semantics, we use a different structure to
+    /// represent the result.
+    struct FindNSEC3Result {
+        FindNSEC3Result(bool param_matched, uint8_t param_closest_labels,
+                        isc::dns::ConstRRsetPtr param_closest_proof,
+                        isc::dns::ConstRRsetPtr param_next_proof) :
+            matched(param_matched), closest_labels(param_closest_labels),
+            closest_proof(param_closest_proof),
+            next_proof(param_next_proof)
+        {}
+
+        /// true iff closest_proof is a matching NSEC3
+        const bool matched;
+
+        /// The number of labels of the identified closest encloser.
+        const uint8_t closest_labels;
+
+        /// Either the NSEC3 for the closest provable encloser of the given
+        /// name or NSEC3 that covers the name
+        const isc::dns::ConstRRsetPtr closest_proof;
+
+        /// When non NULL, NSEC3 for the next closer name.
+        const isc::dns::ConstRRsetPtr next_proof;
+    };
+
+    /// Search the zone for the NSEC3 RR(s) that prove existence or non
+    /// existence of a give name.
+    ///
+    /// It searches the NSEC3 namespace of the zone (how that namespace is
+    /// implemented can vary in specific data source implementation) for NSEC3
+    /// RRs that match or cover the NSEC3 hash value for the given name.
+    ///
+    /// If \c recursive is false, it will first look for the NSEC3 that has
+    /// a matching hash.  If it doesn't exist, it identifies the covering NSEC3
+    /// for the hash.  In either case the search stops at that point and the
+    /// found NSEC3 RR(set) will be returned in the closest_proof member of
+    /// \c FindNSEC3Result.  \c matched is true or false depending on
+    /// the found NSEC3 is a matched one or covering one.  \c next_proof
+    /// is always NULL.  closest_labels must be equal to the number of
+    /// labels of \c name (and therefore meaningless).
+    ///
+    /// If \c recursive is true, it will continue the search toward the zone
+    /// apex (origin name) until it finds a provable encloser, that is,
+    /// an ancestor of \c name that has a matching NSEC3.  This is the closest
+    /// provable encloser of \c name as defined in RFC5155.  In this case,
+    /// if the found encloser is not equal to \c name, the search should
+    /// have seen a covering NSEC3 for the immediate child of the found
+    /// encloser.  That child name is the next closer name as defined in
+    /// RFC5155.  In this case, this method returns the NSEC3 for the
+    /// closest encloser in \c closest_proof, and the NSEC3 for the next
+    /// closer name in \c next_proof of \c FindNSEC3Result.  This set of
+    /// NSEC3 RRs provide the closest encloser proof as defined in RFC5155.
+    /// closest_labels will be set to the number of labels of the identified
+    /// closest encloser.  This will be useful when the caller needs to
+    /// construct the closest encloser name from the original \c name.
+    /// If, on the other hand, the found closest name is equal to \c name,
+    /// this method simply returns it in \c closest_proof.  \c next_proof
+    /// is set to NULL.  In all cases \c matched is set to true.
+    /// closest_labels will be set to the number of labels of \c name.
+    ///
+    /// When looking for NSEC3, this method retrieves NSEC3 parameters from
+    /// the corresponding zone to calculate hash values.  Actual implementation
+    /// of how to do this will differ in different data sources.  If the
+    /// NSEC3 parameters are not available \c DataSourceError exception
+    /// will be thrown.
+    ///
+    /// \note This implicitly means this method assumes the zone does not
+    /// have more than one set of parameters.  This assumption should be
+    /// reasonable in actual deployment and will help simplify the interface
+    /// and implementation.  But if there's a real need for supporting
+    /// multiple sets of parameters in a single zone, we will have to
+    /// extend this method so that, e.g., the caller can specify the parameter
+    /// set.
+    ///
+    /// In general, this method expects the zone is properly signed with NSEC3
+    /// RRs.  Specifically, it assumes at least the apex node has a matching
+    /// NSEC3 RR (so the search in the recursive mode must always succeed);
+    /// it also assumes that it can retrieve NSEC parameters (iterations,
+    /// algorithm, and salt) from the zone as noted above.  If these
+    /// assumptions aren't met, \c DataSourceError exception will be thrown.
+    ///
+    /// \exception InvalidParameter name is not a subdomain of the zone origin
+    /// \exception DataSourceError Low-level or internal datasource errors
+    /// happened, or the zone isn't properly signed with NSEC3
+    /// (NSEC3 parameters cannot be found, no NSEC3s are available, etc).
+    /// \exception std::bad_alloc The underlying implementation involves
+    /// memory allocation and it fails
+    ///
+    /// \param name The name for which NSEC3 RRs are to be found.  It must
+    /// be a subdomain of the zone.
+    /// \param recursive Whether or not search should continue until it finds
+    /// a provable encloser (see above).
+    ///
+    /// \return The search result and whether or not the closest_proof is
+    /// a matching NSEC3, in the form of \c FindNSEC3Result object.
+    virtual FindNSEC3Result
+    findNSEC3(const isc::dns::Name& name, bool recursive) = 0;
+
     /// \brief Get previous name in the zone
     ///
     /// Gets the previous name in the DNSSEC order. This can be used
@@ -346,6 +515,18 @@ inline ZoneFinder::FindOptions operator |(ZoneFinder::FindOptions a,
                                                  static_cast<unsigned>(b)));
 }
 
+/// \brief Operator to combine FindResultFlags
+///
+/// Similar to the same operator for \c FindOptions.  Refer to the description
+/// of that function.
+inline ZoneFinder::FindResultFlags operator |(
+    ZoneFinder::FindResultFlags a,
+    ZoneFinder::FindResultFlags b)
+{
+    return (static_cast<ZoneFinder::FindResultFlags>(
+                static_cast<unsigned>(a) | static_cast<unsigned>(b)));
+}
+
 /// \brief A pointer-like type pointing to a \c ZoneFinder object.
 typedef boost::shared_ptr<ZoneFinder> ZoneFinderPtr;
 

+ 15 - 0
src/lib/dns/masterload.cc

@@ -25,6 +25,7 @@
 #include <dns/masterload.h>
 #include <dns/name.h>
 #include <dns/rdata.h>
+#include <dns/rdataclass.h>
 #include <dns/rrclass.h>
 #include <dns/rrset.h>
 #include <dns/rrttl.h>
@@ -58,6 +59,7 @@ masterLoad(istream& input, const Name& origin, const RRClass& zone_class,
            MasterLoadCallback callback)
 {
     RRsetPtr rrset;
+    ConstRdataPtr prev_rdata;   // placeholder for special case of RRSIGs
     string line;
     unsigned int line_count = 1;
 
@@ -145,8 +147,20 @@ masterLoad(istream& input, const Name& origin, const RRClass& zone_class,
         // Everything is okay.  Now create/update RRset with the new RR.
         // If this is the first RR or the RR type/name is new, we are seeing
         // a new RRset.
+        bool new_rrset = false;
         if (!rrset || rrset->getType() != *rrtype ||
             rrset->getName() != *owner) {
+            new_rrset = true;
+        } else if (rrset->getType() == RRType::RRSIG()) {
+            // We are seeing two consecutive RRSIGs of the same name.
+            // They can be combined iff they have the same type covered.
+            if (dynamic_cast<const generic::RRSIG&>(*rdata).typeCovered() !=
+                dynamic_cast<const generic::RRSIG&>(*prev_rdata).typeCovered())
+            {
+                new_rrset = true;
+            }
+        }
+        if (new_rrset) {
             // Commit the previous RRset, if any.
             if (rrset) {
                 callback(rrset);
@@ -154,6 +168,7 @@ masterLoad(istream& input, const Name& origin, const RRClass& zone_class,
             rrset = RRsetPtr(new RRset(*owner, *rrclass, *rrtype, *ttl));
         }
         rrset->addRdata(rdata);
+        prev_rdata = rdata;
     } while (++line_count, !input.eof());
 
     // Commit the last RRset, if any.

+ 2 - 3
src/lib/dns/masterload.h

@@ -206,9 +206,8 @@ typedef boost::function<void(RRsetPtr)> MasterLoadCallback;
 /// - We may want to support incremental loading.
 /// - If we add these optional features we may want to introduce a class
 ///   that encapsulates loading status and options.
-/// - RRSIGs are currently identified as their owner name and RR type (RRSIG).
-///   In practice it should be sufficient, but technically we should also
-///   consider the Type Covered field.
+/// - RRSIGs are handled as separate RRsets, i.e. they are not included in
+///   the RRset they cover.
 ///
 /// \param filename A path to a master zone file to be loaded.
 /// \param origin The origin name of the zone.

+ 13 - 2
src/lib/dns/rrset.h

@@ -693,7 +693,7 @@ public:
     }
 
     /// \brief Adds an RRSIG RR to this RRset's signatures
-    virtual void addRRsig(const rdata::RdataPtr rdata) {
+    virtual void addRRsig(const rdata::ConstRdataPtr rdata) {
         if (!rrsig_) {
             rrsig_ = RRsetPtr(new RRset(getName(), getClass(),
                                         RRType::RRSIG(), getTTL()));
@@ -701,8 +701,16 @@ public:
         rrsig_->addRdata(rdata);
     }
 
+    // Workaround for older versions of boost: some don't support implicit
+    // conversion from shared_ptr<X> to shared_ptr<const X>.  Note: we should
+    // revisit the interface of managing RRset signatures, at which point this
+    // problem may go away.
+    void addRRsig(const rdata::RdataPtr rdata) {
+        addRRsig(static_cast<rdata::ConstRdataPtr>(rdata));
+    }
+
     /// \brief Adds an RRSIG RRset to this RRset
-    void addRRsig(AbstractRRset& sigs) {
+    void addRRsig(const AbstractRRset& sigs) {
         RdataIteratorPtr it = sigs.getRdataIterator();
 
         if (!rrsig_) {
@@ -715,6 +723,9 @@ public:
         }
     }
 
+    void addRRsig(ConstRRsetPtr sigs) { addRRsig(*sigs); }
+
+    // Another workaround for older boost (see above)
     void addRRsig(RRsetPtr sigs) { addRRsig(*sigs); }
 
     /// \brief Clear the RRSIGs for this RRset

+ 14 - 0
src/lib/dns/tests/masterload_unittest.cc

@@ -72,6 +72,13 @@ const char* const a_rr2 = "www.example.com. 60 IN A 192.0.2.2\n";
 const char* const a_rr3 = "ftp.example.com. 60 IN A 192.0.2.3\n";
 // multi-field RR case
 const char* const soa_rr = "example.com. 7200 IN SOA . . 0 0 0 0 0\n";
+// A couple of RRSIGs, different type covered
+const char* const rrsig_rr1 =
+    "www.example.com. 60 IN RRSIG A 5 3 3600 20000101000000 20000201000000 "
+    "12345 example.com. FAKEFAKEFAKE\n";
+const char* const rrsig_rr2 =
+    "www.example.com. 60 IN RRSIG AAAA 5 3 3600 20000101000000 20000201000000 "
+    "12345 example.com. FAKEFAKEFAKE\n";
 
 TEST_F(MasterLoadTest, loadRRs) {
     // a simple case: loading 3 RRs, each consists of a single RRset.
@@ -147,6 +154,13 @@ TEST_F(MasterLoadTest, loadRRsetsInterleaved) {
     EXPECT_EQ(a_rr2, results[2]->toText());
 }
 
+TEST_F(MasterLoadTest, loadRRsigs) {
+    // RRSIGs of different types covered should be separated
+    rr_stream << rrsig_rr1 << rrsig_rr2;
+    masterLoad(rr_stream, origin, zclass, callback);
+    EXPECT_EQ(2, results.size());
+}
+
 TEST_F(MasterLoadTest, loadWithNoEOF) {
     // the input stream doesn't end with a new line (and the following blank
     // line).  It should be accepted.

+ 9 - 6
src/lib/python/isc/datasrc/datasrc.cc

@@ -129,12 +129,6 @@ initModulePart_ZoneFinder(PyObject* mod) {
                              Py_BuildValue("I", ZoneFinder::CNAME));
         installClassVariable(zonefinder_type, "DNAME",
                              Py_BuildValue("I", ZoneFinder::DNAME));
-        installClassVariable(zonefinder_type, "WILDCARD",
-                             Py_BuildValue("I", ZoneFinder::WILDCARD));
-        installClassVariable(zonefinder_type, "WILDCARD_NXRRSET",
-                             Py_BuildValue("I", ZoneFinder::WILDCARD_NXRRSET));
-        installClassVariable(zonefinder_type, "WILDCARD_CNAME",
-                             Py_BuildValue("I", ZoneFinder::WILDCARD_CNAME));
 
         installClassVariable(zonefinder_type, "FIND_DEFAULT",
                              Py_BuildValue("I", ZoneFinder::FIND_DEFAULT));
@@ -144,6 +138,15 @@ initModulePart_ZoneFinder(PyObject* mod) {
                              Py_BuildValue("I", ZoneFinder::FIND_DNSSEC));
         installClassVariable(zonefinder_type, "NO_WILDCARD",
                              Py_BuildValue("I", ZoneFinder::NO_WILDCARD));
+
+        installClassVariable(zonefinder_type, "RESULT_WILDCARD",
+                             Py_BuildValue("I", ZoneFinder::RESULT_WILDCARD));
+        installClassVariable(zonefinder_type, "RESULT_NSEC_SIGNED",
+                             Py_BuildValue("I",
+                                           ZoneFinder::RESULT_NSEC_SIGNED));
+        installClassVariable(zonefinder_type, "RESULT_NSEC3_SIGNED",
+                             Py_BuildValue("I",
+                                           ZoneFinder::RESULT_NSEC3_SIGNED));
     } catch (const std::exception& ex) {
         const std::string ex_what =
             "Unexpected failure in ZoneFinder initialization: " +

+ 118 - 19
src/lib/python/isc/datasrc/finder_inc.cc

@@ -47,7 +47,7 @@ Return the RR class of the zone.\n\
 // - NULL->None
 // - exceptions
 const char* const ZoneFinder_find_doc = "\
-find(name, type, options=FIND_DEFAULT) -> (integer, RRset)\n\
+find(name, type, options=FIND_DEFAULT) -> (integer, RRset, integer)\n\
 \n\
 Search the zone for a given pair of domain name and RR type.\n\
 \n\
@@ -58,12 +58,10 @@ answer for the search key. Specifically,\n\
 \n\
 - If the search name belongs under a zone cut, it returns the code of\n\
   DELEGATION and the NS RRset at the zone cut.\n\
-- If there is no matching name, it returns the code of NXDOMAIN, and,\n\
-  if DNSSEC is requested, the NSEC RRset that proves the non-\n\
-  existence.\n\
+- If there is no matching name, it returns the code of NXDOMAIN.\n\
 - If there is a matching name but no RRset of the search type, it\n\
-  returns the code of NXRRSET, and, if DNSSEC is required, the NSEC\n\
-  RRset for that name.\n\
+  returns the code of NXRRSET. This case includes the search name\n\
+  matches an empty node of the zone.\n\
 - If there is a CNAME RR of the searched name but there is no RR of\n\
   the searched type of the name (so this type is different from\n\
   CNAME), it returns the code of CNAME and that CNAME RR. Note that if\n\
@@ -72,6 +70,16 @@ answer for the search key. Specifically,\n\
 - If the search name matches a delegation point of DNAME, it returns\n\
   the code of DNAME and that DNAME RR.\n\
 \n\
+No RRset will be returned in the NXDOMAIN and NXRRSET cases (the\n\
+second element of the tuple will be None), unless DNSSEC data are\n\
+required. See below for the cases with DNSSEC.\n\
+\n\
+The third element of the returned tuple provides supplemental\n\
+information about the search result in the form of a bitmask (called\n\
+\"flags\"). Such information may be useful for the caller if the\n\
+caller wants to collect additional DNSSEC proofs based on the search\n\
+result.\n\
+\n\
 The options parameter specifies customized behavior of the search.\n\
 Their semantics is as follows (they are or bit-field):\n\
 \n\
@@ -96,6 +104,91 @@ should be equal to or a subdomain of the zone origin. Otherwise this\n\
 method will return NXDOMAIN with an empty RRset. But such a case\n\
 should rather be considered a caller's bug.\n\
 \n\
+Note: For this reason it's probably better to throw an exception than\n\
+returning NXDOMAIN. This point should be revisited in a near future\n\
+version. In any case applications shouldn't call this method for an\n\
+out-of-zone name.\n\
+\n\
+DNSSEC considerations: The result when DNSSEC data are required can be\n\
+very complicated, especially if it involves negative result or\n\
+wildcard match. Specifically, if an application calls this method for\n\
+DNS query processing with DNSSEC data, and if the search result code\n\
+is either NXDOMAIN or NXRRRSET, and/or RESULT_WILDCARD\n\
+flag is set in the returned flags value,\n\
+then the application will need to find additional NSEC or NSEC3\n\
+records for supplemental proofs. This method helps the application for\n\
+such post search processing.\n\
+\n\
+First, it tells the application whether the zone is signed with NSEC\n\
+or NSEC3 via the RESULT_NSEC_SIGNED and RESULT_NSEC3_SIGNED flags\n\
+in the returned flags value. Any sanely signed zone\n\
+should be signed with either (and only one) of these two types of RRs;\n\
+however, the application should expect that the zone could be broken\n\
+and these methods could both return false. But this method should\n\
+ensure that not both of these methods return true.\n\
+\n\
+In case it's signed with NSEC3, there is no further information\n\
+returned from this method.\n\
+\n\
+In case it's signed with NSEC, this method will possibly return a\n\
+related NSEC RRset in the second element of the tuple. What kind of\n\
+NSEC is returned depends on the result code (NXDOMAIN or NXRRSET) and\n\
+on whether it's a wildcard match:\n\
+\n\
+- In case of NXDOMAIN, the returned NSEC covers the queried domain\n\
+  that proves that the query name does not exist in the zone. Note\n\
+  that this does not necessarily prove it doesn't even match a\n\
+  wildcard (even if the result of NXDOMAIN can only happen when\n\
+  there's no matching wildcard either). It is caller's responsibility\n\
+  to provide a proof that there is no matching wildcard if that proof\n\
+  is necessary.\n\
+- In case of NXRRSET, we need to consider the following cases\n\
+  referring to Section 3.1.3 of RFC4035:\n\
+\n\
+1. (Normal) no data: there is a matching non-wildcard name with a\n\
+   different RR type. This is the \"No Data\" case of the RFC.\n\
+2. (Normal) empty non terminal: there is no matching (exact or\n\
+   wildcard) name, but there is a subdomain with an RR of the query\n\
+   name. This is one case of \"Name Error\" of the RFC.\n\
+3. Wildcard empty non terminal: similar to 2, but the empty name is\n\
+   a wildcard, and matches the query name by wildcard expansion. This\n\
+   is a special case of \"Name Error\" of the RFC.\n\
+4. Wildcard no data: there is no exact match name, but there is a\n\
+   wildcard name that matches the query name with a different type of RR.\n\
+   This is the \"Wildcard No Data\" case of the RFC.\n\
+\n\
+In case 1, find() returns NSEC of the matching name.\n\
+\n\
+In case 2, find() will return NSEC for the interval where the empty\n\
+nonterminal lives. The end of the interval is the subdomain causing\n\
+existence of the empty nonterminal (if there's sub.x.example.com, and\n\
+no record in x.example.com, then x.example.com exists implicitly - is\n\
+the empty nonterminal and sub.x.example.com is the subdomain causing\n\
+it). Note that this NSEC proves not only the existence of empty non\n\
+terminal name but also the non existence of possibly matching wildcard\n\
+name, because there can be no better wildcard match than the exact\n\
+matching empty name.\n\
+\n\
+In case 3, find() will return NSEC for the interval where the wildcard\n\
+empty nonterminal lives. Cases 2 and 3 are especially complicated and\n\
+confusing. See the examples below.\n\
+\n\
+In case 4, find() will return NSEC of the matching wildcard name.\n\
+\n\
+Examples: if zone \"example.com\" has the following record:\n\
+\n\
+a.example.com. NSEC a.b.example.com.\n\
+\n\
+a call to  find() for \"b.example.com.\" with the FIND_DNSSEC option\n\
+will result in NXRRSET, and this NSEC will be returned.\n\
+Likewise, if zone \"example.org\" has the following record,\n\
+\n\
+a.example.org. NSEC x.*.b.example.org.\n\
+\n\
+a call to  find() for \"y.b.example.org\" with FIND_DNSSEC will\n\
+result in NXRRSET and this NSEC;  RESULT_WILDCARD bit is set in the\n\
+returned flags.\n\
+\n\
 This method raises an isc.datasrc.Error exception if there is an\n\
 internal error in the datasource.\n\
 \n\
@@ -104,28 +197,34 @@ Parameters:\n\
   type       The RR type to be searched for.\n\
   options    The search options.\n\
 \n\
-Return Value(s): A tuple of a result code (integer) and an RRset object\n\
-enclosing the search result (see above).\n\
+Return Value(s): A tuple of a result code (integer), an RRset object\n\
+and flags bitmask (integer).\n\
 ";
 
-const char* const ZoneFinder_find_all_doc = "\
-find_all(isc.dns.Name, options=FIND_DEFAULT) -> (integer, RRset) | (integer, [RRset])\
+const char* const ZoneFinder_findAll_doc = "\
+find_all(isc.dns.Name, options=FIND_DEFAULT) ->\n\
+   (integer, RRset, integer) | (integer, [RRset], integer)\
 \n\
-This acts mostly the same as the find method. The main difference is,\n\
-when the lookup is successful (eg. the first part of the result is either\n\
-SUCCESS or WILDCARD), the second part is list of all RRsets in the given name\n\
-instead of a single RRset as in case of find.\n\
+Finds all RRsets in the given name.\n\
 \n\
-This method raises an isc.datasrc.Error exception if there is an\n\
-internal error in the datasource.\n\
+This function works almost exactly in the same way as the find one.\n\
+The only difference is, when the lookup is successful (eg. the code is\n\
+SUCCESS), all the RRsets residing in the named node are returned in the\n\
+second element of the returned tuple. All\n\
+the other (unsuccessful) cases are handled the same, including\n\
+returning delegations, NSEC/NSEC3 availability and NSEC proofs,\n\
+wildcard information etc. The options parameter works the same way and\n\
+it should conform to the same exception restrictions.\n\
 \n\
 Parameters:\n\
   name       The domain name to be searched for.\n\
   options    The search options.\n\
 \n\
-Return Value(s): A tuple of a result code (integer) and an either RRset object,\n\
-for cases where the result is some kind of delegation, CNAME or similar, or list\n\
-of RRset objects, containing all the results.\n\
+Return Value(s): A tuple of a result code (integer), an either\n\
+RRset object or a list of RRsets, and flags (integer).\n\
+In the second element a single RRset is returned for cases where the\n\
+result is some kind of delegation, CNAME or similar; in other cases\n\
+a list of RRsets is returned, containing all the results.\n\
 ";
 
 const char* const ZoneFinder_find_previous_name_doc = "\

+ 29 - 6
src/lib/python/isc/datasrc/finder_python.cc

@@ -45,6 +45,23 @@ using namespace isc::dns::python;
 using namespace isc::datasrc;
 using namespace isc::datasrc::python;
 
+namespace  {
+ZoneFinder::FindResultFlags
+getFindResultFlags(const ZoneFinder::FindResult& result) {
+    ZoneFinder::FindResultFlags result_flags = ZoneFinder::RESULT_DEFAULT;
+    if (result.isWildcard()) {
+        result_flags = result_flags | ZoneFinder::RESULT_WILDCARD;
+    }
+    if (result.isNSECSigned()) {
+        result_flags = result_flags | ZoneFinder::RESULT_NSEC_SIGNED;
+    }
+    if (result.isNSEC3Signed()) {
+        result_flags = result_flags | ZoneFinder::RESULT_NSEC3_SIGNED;
+    }
+    return (result_flags);
+}
+}
+
 namespace isc_datasrc_internal {
 // This is the shared code for the find() call in the finder and the updater
 // Is is intentionally not available through any header, nor at our standard
@@ -71,11 +88,14 @@ PyObject* ZoneFinder_helper(ZoneFinder* finder, PyObject* args) {
                              options));
             const ZoneFinder::Result r = find_result.code;
             isc::dns::ConstRRsetPtr rrsp = find_result.rrset;
+            ZoneFinder::FindResultFlags result_flags =
+                getFindResultFlags(find_result);
             if (rrsp) {
                 // Use N instead of O so the refcount isn't increased twice
-                return (Py_BuildValue("IN", r, createRRsetObject(*rrsp)));
+                return (Py_BuildValue("INI", r, createRRsetObject(*rrsp),
+                                      result_flags));
             } else {
-                return (Py_BuildValue("IO", r, Py_None));
+                return (Py_BuildValue("IOI", r, Py_None, result_flags));
             }
         } catch (const DataSourceError& dse) {
             PyErr_SetString(getDataSourceException("Error"), dse.what());
@@ -111,7 +131,9 @@ PyObject* ZoneFinder_helper_all(ZoneFinder* finder, PyObject* args) {
                 finder->findAll(PyName_ToName(name), target, options));
             const ZoneFinder::Result r = find_result.code;
             isc::dns::ConstRRsetPtr rrsp = find_result.rrset;
-            if (r == ZoneFinder::SUCCESS || r == ZoneFinder::WILDCARD) {
+            ZoneFinder::FindResultFlags result_flags =
+                getFindResultFlags(find_result);
+            if (r == ZoneFinder::SUCCESS) {
                 // Copy all the RRsets to the result list
                 PyObjectContainer list_container(PyList_New(target.size()));
                 for (size_t i(0); i < target.size(); ++i) {
@@ -126,9 +148,10 @@ PyObject* ZoneFinder_helper_all(ZoneFinder* finder, PyObject* args) {
             } else {
                 if (rrsp) {
                     // Use N instead of O so the refcount isn't increased twice
-                    return (Py_BuildValue("IN", r, createRRsetObject(*rrsp)));
+                    return (Py_BuildValue("INI", r, createRRsetObject(*rrsp),
+                                          result_flags));
                 } else {
-                    return (Py_BuildValue("IO", r, Py_None));
+                    return (Py_BuildValue("IOI", r, Py_None, result_flags));
                 }
             }
         } catch (const DataSourceError& dse) {
@@ -263,7 +286,7 @@ PyMethodDef ZoneFinder_methods[] = {
        ZoneFinder_getOrigin_doc },
     { "get_class", ZoneFinder_getClass, METH_NOARGS, ZoneFinder_getClass_doc },
     { "find", ZoneFinder_find, METH_VARARGS, ZoneFinder_find_doc },
-    { "find_all", ZoneFinder_find_all, METH_VARARGS, ZoneFinder_find_all_doc },
+    { "find_all", ZoneFinder_find_all, METH_VARARGS, ZoneFinder_findAll_doc },
     { "find_previous_name", ZoneFinder_findPreviousName, METH_VARARGS,
       ZoneFinder_find_previous_name_doc },
     { NULL, NULL, 0, NULL }

+ 75 - 68
src/lib/python/isc/datasrc/tests/datasrc_test.py

@@ -63,8 +63,8 @@ def test_findall_common(self, tested):
     object.
     """
     # Some "failure" responses
-    result, rrset = tested.find_all(isc.dns.Name("www.sql1.example.com"),
-                                    ZoneFinder.FIND_DEFAULT)
+    result, rrset, _ = tested.find_all(isc.dns.Name("www.sql1.example.com"),
+                                       ZoneFinder.FIND_DEFAULT)
     self.assertEqual(ZoneFinder.DELEGATION, result)
     expected = RRset(Name('sql1.example.com.'), RRClass.IN(), RRType.NS(),
                      RRTTL(3600))
@@ -76,8 +76,8 @@ def test_findall_common(self, tested):
                              'dns03.example.com.'))
     self.assertTrue(rrsets_equal(expected, rrset))
 
-    result, rrset = tested.find_all(isc.dns.Name("nxdomain.example.com"),
-                                     ZoneFinder.FIND_DEFAULT)
+    result, rrset, _ = tested.find_all(isc.dns.Name("nxdomain.example.com"),
+                                       ZoneFinder.FIND_DEFAULT)
     self.assertEqual(ZoneFinder.NXDOMAIN, result)
     self.assertIsNone(None, rrset)
 
@@ -319,10 +319,15 @@ class DataSrcClient(unittest.TestCase):
         self.assertNotEqual(ZoneFinder.NXDOMAIN, ZoneFinder.NXRRSET)
         self.assertNotEqual(ZoneFinder.NXRRSET, ZoneFinder.CNAME)
         self.assertNotEqual(ZoneFinder.CNAME, ZoneFinder.DNAME)
-        self.assertNotEqual(ZoneFinder.DNAME, ZoneFinder.WILDCARD)
-        self.assertNotEqual(ZoneFinder.WILDCARD, ZoneFinder.WILDCARD_CNAME)
-        self.assertNotEqual(ZoneFinder.WILDCARD_CNAME,
-                            ZoneFinder.WILDCARD_NXRRSET)
+
+    def test_findresultflags(self):
+        '''A simple test just confirming the flags are all different.'''
+        self.assertNotEqual(ZoneFinder.RESULT_WILDCARD,
+                            ZoneFinder.RESULT_NSEC_SIGNED)
+        self.assertNotEqual(ZoneFinder.RESULT_NSEC_SIGNED,
+                            ZoneFinder.RESULT_NSEC3_SIGNED)
+        self.assertNotEqual(ZoneFinder.RESULT_NSEC3_SIGNED,
+                            ZoneFinder.RESULT_WILDCARD)
 
     def test_findall(self):
         """
@@ -345,66 +350,68 @@ class DataSrcClient(unittest.TestCase):
         self.assertEqual(isc.dns.RRClass.IN(), finder.get_class())
         self.assertEqual("example.com.", finder.get_origin().to_text())
 
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
 
         # Check the optional parameters are optional
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A())
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A())
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
 
-        result, rrset = finder.find(isc.dns.Name("www.sql1.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.sql1.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.DELEGATION, result)
         self.assertEqual("sql1.example.com. 3600 IN NS dns01.example.com.\n" +
                          "sql1.example.com. 3600 IN NS dns02.example.com.\n" +
                          "sql1.example.com. 3600 IN NS dns03.example.com.\n",
                          rrset.to_text())
 
-        result, rrset = finder.find(isc.dns.Name("doesnotexist.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("doesnotexist.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.NXDOMAIN, result)
         self.assertEqual(None, rrset)
 
-        result, rrset = finder.find(isc.dns.Name("www.some.other.domain"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.some.other.domain"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.NXDOMAIN, result)
         self.assertEqual(None, rrset)
 
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.TXT(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.TXT(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.NXRRSET, result)
         self.assertEqual(None, rrset)
 
-        result, rrset = finder.find(isc.dns.Name("cname-ext.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("cname-ext.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.CNAME, result)
         self.assertEqual(
             "cname-ext.example.com. 3600 IN CNAME www.sql1.example.com.\n",
             rrset.to_text())
 
-        result, rrset = finder.find(isc.dns.Name("foo.wild.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
-        self.assertEqual(finder.WILDCARD, result)
+        result, rrset, flags = \
+            finder.find(isc.dns.Name("foo.wild.example.com"),
+                        isc.dns.RRType.A(), finder.FIND_DEFAULT)
+        self.assertEqual(finder.SUCCESS, result)
+        self.assertEqual(finder.RESULT_WILDCARD, flags)
         self.assertEqual("foo.wild.example.com. 3600 IN A 192.0.2.255\n",
                          rrset.to_text())
 
-        result, rrset = finder.find(isc.dns.Name("foo.wild.example.com"),
-                                    isc.dns.RRType.TXT(),
-                                    finder.FIND_DEFAULT)
-        self.assertEqual(finder.WILDCARD_NXRRSET, result)
+        result, rrset, _ = finder.find(isc.dns.Name("foo.wild.example.com"),
+                                       isc.dns.RRType.TXT(),
+                                       finder.FIND_DEFAULT)
+        self.assertEqual(finder.NXRRSET, result)
+        self.assertTrue(finder.RESULT_WILDCARD, flags)
         self.assertEqual(None, rrset)
 
         self.assertRaises(TypeError, finder.find,
@@ -463,16 +470,16 @@ class DataSrcUpdater(unittest.TestCase):
         # Check basic behavior of updater's finder
         dsc = isc.datasrc.DataSourceClient("sqlite3", WRITE_ZONE_DB_CONFIG)
         updater = dsc.get_updater(isc.dns.Name("example.com"), False)
-        result, rrset = updater.find(isc.dns.Name("www.example.com"),
-                                     isc.dns.RRType.A(),
-                                     ZoneFinder.FIND_DEFAULT)
+        result, rrset, _ = updater.find(isc.dns.Name("www.example.com"),
+                                        isc.dns.RRType.A(),
+                                        ZoneFinder.FIND_DEFAULT)
         self.assertEqual(ZoneFinder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
 
         # Omit optional parameters
-        result, rrset = updater.find(isc.dns.Name("www.example.com"),
-                                     isc.dns.RRType.A())
+        result, rrset, _ = updater.find(isc.dns.Name("www.example.com"),
+                                        isc.dns.RRType.A())
         self.assertEqual(ZoneFinder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
@@ -487,9 +494,9 @@ class DataSrcUpdater(unittest.TestCase):
         self.assertEqual(isc.dns.RRClass.IN(), finder.get_class())
         self.assertEqual("example.com.", finder.get_origin().to_text())
 
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
@@ -508,15 +515,15 @@ class DataSrcUpdater(unittest.TestCase):
 
         # The record should be gone in the updater, but not in the original
         # finder (since we have not committed)
-        result, rrset = updater.find(isc.dns.Name("www.example.com"),
-                                     isc.dns.RRType.A(),
-                                     finder.FIND_DEFAULT)
+        result, rrset, _ = updater.find(isc.dns.Name("www.example.com"),
+                                        isc.dns.RRType.A(),
+                                        finder.FIND_DEFAULT)
         self.assertEqual(finder.NXDOMAIN, result)
         self.assertEqual(None, rrset)
 
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
@@ -526,9 +533,9 @@ class DataSrcUpdater(unittest.TestCase):
         self.assertRaises(isc.datasrc.Error, updater.commit)
 
         # the record should be gone now in the 'real' finder as well
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.NXDOMAIN, result)
         self.assertEqual(None, rrset)
 
@@ -540,9 +547,9 @@ class DataSrcUpdater(unittest.TestCase):
         # second commit should throw
         self.assertRaises(isc.datasrc.Error, updater.commit)
 
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
@@ -564,9 +571,9 @@ class DataSrcUpdater(unittest.TestCase):
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual(isc.dns.RRClass.IN(), finder.get_class())
         self.assertEqual("example.com.", finder.get_origin().to_text())
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
@@ -587,9 +594,9 @@ class DataSrcUpdater(unittest.TestCase):
         self.assertEqual(isc.dns.RRClass.IN(), finder.get_class())
         self.assertEqual("example.com.", finder.get_origin().to_text())
 
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())
@@ -608,9 +615,9 @@ class DataSrcUpdater(unittest.TestCase):
 
         # The record should be gone in the updater, but not in the original
         # finder (since we have not committed)
-        result, rrset = updater.find(isc.dns.Name("www.example.com"),
-                                     isc.dns.RRType.A(),
-                                     finder.FIND_DEFAULT)
+        result, rrset, _ = updater.find(isc.dns.Name("www.example.com"),
+                                        isc.dns.RRType.A(),
+                                        finder.FIND_DEFAULT)
         self.assertEqual(finder.NXDOMAIN, result)
         self.assertEqual(None, rrset)
 
@@ -618,9 +625,9 @@ class DataSrcUpdater(unittest.TestCase):
         updater = None
 
         # the record should still be available in the 'real' finder as well
-        result, rrset = finder.find(isc.dns.Name("www.example.com"),
-                                    isc.dns.RRType.A(),
-                                    finder.FIND_DEFAULT)
+        result, rrset, _ = finder.find(isc.dns.Name("www.example.com"),
+                                       isc.dns.RRType.A(),
+                                       finder.FIND_DEFAULT)
         self.assertEqual(finder.SUCCESS, result)
         self.assertEqual("www.example.com. 3600 IN A 192.0.2.1\n",
                          rrset.to_text())

+ 1 - 1
src/lib/python/isc/datasrc/updater_python.cc

@@ -217,7 +217,7 @@ PyMethodDef ZoneUpdater_methods[] = {
       METH_NOARGS, ZoneFinder_getClass_doc },
     { "find", ZoneUpdater_find, METH_VARARGS, ZoneFinder_find_doc },
     { "find_all", ZoneUpdater_find_all, METH_VARARGS,
-      ZoneFinder_find_all_doc },
+      ZoneFinder_findAll_doc },
     { NULL, NULL, 0, NULL }
 };
 

+ 5 - 5
src/lib/python/isc/notify/notify_out.py

@@ -284,12 +284,12 @@ class NotifyOut:
                          format_zone_str(zone_name, zone_class))
             return []
 
-        result, ns_rrset = finder.find(zone_name, RRType.NS())
+        result, ns_rrset, _ = finder.find(zone_name, RRType.NS())
         if result is not finder.SUCCESS or ns_rrset is None:
             logger.warn(NOTIFY_OUT_ZONE_NO_NS,
                         format_zone_str(zone_name, zone_class))
             return []
-        result, soa_rrset = finder.find(zone_name, RRType.SOA())
+        result, soa_rrset, _ = finder.find(zone_name, RRType.SOA())
         if result is not finder.SUCCESS or soa_rrset is None or \
                 soa_rrset.get_rdata_count() != 1:
             logger.warn(NOTIFY_OUT_ZONE_BAD_SOA,
@@ -302,11 +302,11 @@ class NotifyOut:
             ns_name = Name(ns_rdata.to_text())
             if soa_mname == ns_name:
                 continue
-            result, rrset = finder.find(ns_name, RRType.A())
+            result, rrset, _ = finder.find(ns_name, RRType.A())
             if result is finder.SUCCESS and rrset is not None:
                 addrs.extend([a.to_text() for a in rrset.get_rdata()])
 
-            result, rrset = finder.find(ns_name, RRType.AAAA())
+            result, rrset, _ = finder.find(ns_name, RRType.AAAA())
             if result is finder.SUCCESS and rrset is not None:
                 addrs.extend([aaaa.to_text() for aaaa in rrset.get_rdata()])
 
@@ -500,7 +500,7 @@ class NotifyOut:
                                            zone_name.to_text() + '/' +
                                            zone_class.to_text() + ' not found')
 
-        result, soa_rrset = finder.find(zone_name, RRType.SOA())
+        result, soa_rrset, _ = finder.find(zone_name, RRType.SOA())
         if result is not finder.SUCCESS or soa_rrset is None or \
                 soa_rrset.get_rdata_count() != 1:
             raise NotifyOutDataSourceError('_get_zone_soa: Zone ' +

+ 1 - 1
src/lib/python/isc/util/Makefile.am

@@ -1,4 +1,4 @@
-SUBDIRS = . io tests
+SUBDIRS = . cio tests
 
 python_PYTHON = __init__.py process.py socketserver_mixin.py file.py
 

+ 2 - 2
src/lib/python/isc/util/io/Makefile.am

@@ -5,10 +5,10 @@ AM_CPPFLAGS += $(BOOST_INCLUDES)
 AM_CXXFLAGS = $(B10_CXXFLAGS)
 
 python_PYTHON = __init__.py
-pythondir = $(PYTHON_SITEPKG_DIR)/isc/util/io
+pythondir = $(PYTHON_SITEPKG_DIR)/isc/util/cio
 
 pyexec_LTLIBRARIES = socketsession.la
-pyexecdir = $(PYTHON_SITEPKG_DIR)/isc/util/io
+pyexecdir = $(PYTHON_SITEPKG_DIR)/isc/util/cio
 
 socketsession_la_SOURCES = socketsession_python.cc socketsession_python.h
 socketsession_la_SOURCES += socketsessionforwarder_python.cc

src/lib/python/isc/util/io/__init__.py → src/lib/python/isc/util/cio/__init__.py


+ 1 - 1
src/lib/python/isc/util/io/socketsession.py

@@ -19,7 +19,7 @@ import os
 import sys
 
 for base in sys.path[:]:
-    libdir = os.path.join(base, 'isc/util/io/.libs')
+    libdir = os.path.join(base, 'isc/util/cio/.libs')
     if os.path.exists(libdir):
         sys.path.insert(0, libdir)
 

src/lib/python/isc/util/io/socketsession_inc.cc → src/lib/python/isc/util/cio/socketsession_inc.cc


+ 2 - 2
src/lib/python/isc/util/io/socketsession_python.cc

@@ -38,7 +38,7 @@ namespace {
 
 PyModuleDef socketsession = {
     { PyObject_HEAD_INIT(NULL) NULL, 0, NULL},
-    "isc.util.io.socketsession",
+    "isc.util.cio.socketsession",
     socketsession_doc,
     -1,
     NULL,
@@ -58,7 +58,7 @@ PyInit_socketsession(void) {
 
     try {
         po_SocketSessionError =
-            PyErr_NewException("isc.util.io.SocketSessionError", NULL, NULL);
+            PyErr_NewException("isc.util.cio.SocketSessionError", NULL, NULL);
         PyObjectContainer(po_SocketSessionError).
             installToModule(mod, "SocketSessionError");
     } catch (...) {

src/lib/python/isc/util/io/socketsession_python.h → src/lib/python/isc/util/cio/socketsession_python.h


src/lib/python/isc/util/io/socketsessionforwarder_inc.cc → src/lib/python/isc/util/cio/socketsessionforwarder_inc.cc


+ 1 - 1
src/lib/python/isc/util/io/socketsessionforwarder_python.cc

@@ -237,7 +237,7 @@ namespace python {
 // Most of the functions are not actually implemented and NULL here.
 PyTypeObject socketsessionforwarder_type = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    "isc.util.io.SocketSessionForwarder",
+    "isc.util.cio.SocketSessionForwarder",
     sizeof(s_SocketSessionForwarder),                 // tp_basicsize
     0,                                  // tp_itemsize
     SocketSessionForwarder_destroy,                 // tp_dealloc

src/lib/python/isc/util/io/socketsessionforwarder_python.h → src/lib/python/isc/util/cio/socketsessionforwarder_python.h


src/lib/python/isc/util/io/socketsessionreceiver_inc.cc → src/lib/python/isc/util/cio/socketsessionreceiver_inc.cc


+ 2 - 2
src/lib/python/isc/util/io/socketsessionreceiver_python.cc

@@ -237,7 +237,7 @@ namespace python {
 // Most of the functions are not actually implemented and NULL here.
 PyTypeObject socketsessionreceiver_type = {
     PyVarObject_HEAD_INIT(NULL, 0)
-    "isc.util.io.SocketSessionReceiver",
+    "isc.util.cio.SocketSessionReceiver",
     sizeof(s_SocketSessionReceiver),                 // tp_basicsize
     0,                                  // tp_itemsize
     SocketSessionReceiver_destroy,                 // tp_dealloc
@@ -311,7 +311,7 @@ initModulePart_SocketSessionReceiver(PyObject* mod) {
         Py_INCREF(socket_fromfd_obj);
     } else {
         PyErr_SetString(PyExc_RuntimeError,
-                        "isc.util.io.SocketSessionReceiver needs "
+                        "isc.util.cio.SocketSessionReceiver needs "
                         "socket.fromfd(), but it's missing");
         return (false);
     }

src/lib/python/isc/util/io/socketsessionreceiver_python.h → src/lib/python/isc/util/cio/socketsessionreceiver_python.h


src/lib/python/isc/util/io/tests/Makefile.am → src/lib/python/isc/util/cio/tests/Makefile.am


+ 1 - 1
src/lib/python/isc/util/io/tests/socketsession_test.py

@@ -16,7 +16,7 @@
 import os, signal, socket, unittest
 from socket import AF_INET, AF_INET6, SOCK_STREAM, SOCK_DGRAM, IPPROTO_UDP, \
     IPPROTO_TCP
-from isc.util.io.socketsession import *
+from isc.util.cio.socketsession import *
 
 TESTDATA_OBJDIR = os.getenv("TESTDATAOBJDIR")
 TEST_UNIX_FILE = TESTDATA_OBJDIR + '/ssessiontest.unix'

+ 12 - 0
src/lib/server_common/socket_request.cc

@@ -64,6 +64,10 @@ const std::string& RELEASE_SOCKET_COMMAND() {
     return (str);
 }
 
+// RCode constants for the get_token command
+const size_t SOCKET_ERROR_CODE = 2;
+const size_t SHARE_ERROR_CODE = 3;
+
 // A helper converter from numeric protocol ID to the corresponding string.
 // used both for generating a message for the boss process and for logging.
 inline const char*
@@ -133,6 +137,14 @@ readRequestSocketAnswer(isc::data::ConstElementPtr recv_msg,
     int rcode;
     isc::data::ConstElementPtr answer = isc::config::parseAnswer(rcode,
                                                                  recv_msg);
+    // Translate known rcodes to the corresponding exceptions
+    if (rcode == SOCKET_ERROR_CODE) {
+        isc_throw(SocketRequestor::SocketAllocateError, answer->str());
+    }
+    if (rcode == SHARE_ERROR_CODE) {
+        isc_throw(SocketRequestor::ShareError, answer->str());
+    }
+    // The unknown exceptions
     if (rcode != 0) {
         isc_throw(isc::config::CCSessionError,
                   "Error response when requesting socket: " << answer->str());

+ 53 - 5
src/lib/server_common/socket_request.h

@@ -104,6 +104,52 @@ public:
         { }
     };
 
+    /// \brief Exception when we can't return a requested socket, but we're
+    ///     sure we could return others
+    ///
+    /// This is thrown if the requested socket can't be granted, but it is only
+    /// that one socket, not that the system would be broken or anything. This
+    /// exception is a common base class for the concrete exceptions actually
+    /// thrown. You can safely keep using the SocketRequestor after this
+    /// exception (or anything derived from it) is thrown.
+    ///
+    /// \see ShareError
+    /// \see SocketAllocateError
+    class NonFatalSocketError : public SocketError {
+    public:
+        NonFatalSocketError(const char* file, size_t line, const char* what) :
+            SocketError(file, line, what)
+        { }
+    };
+
+    /// \brief Exception when the socket is allocated by other bind10 module
+    ///    and it doesn't want to share it.
+    ///
+    /// This is thrown if a socket is requested and the socket is already
+    /// allocated by bind10, but other bind10 module(s) is using it and
+    /// the sharing parameters are incompatible (the socket can't be shared
+    /// between the module and our module).
+    class ShareError : public NonFatalSocketError {
+    public:
+        ShareError(const char* file, size_t line, const char* what) :
+            NonFatalSocketError(file, line, what)
+        { }
+    };
+
+    /// \brief Exception when the operating system doesn't allow us to create
+    ///    the requested socket.
+    ///
+    /// This happens when the socket() or bind() call fails in the socket
+    /// creator. This can happen when the address/port pair is already taken
+    /// by a different application, the socket creator doesn't have enough
+    /// privileges, or for some kind of similar reason.
+    class SocketAllocateError : public NonFatalSocketError {
+    public:
+        SocketAllocateError(const char* file, size_t line, const char* what) :
+            NonFatalSocketError(file, line, what)
+        { }
+    };
+
     /// \brief Ask for a socket
     ///
     /// Asks the socket creator to give us a socket. The socket will be bound
@@ -124,11 +170,13 @@ public:
     /// \throw InvalidParameter protocol or share_mode is invalid
     /// \throw CCSessionError when we have a problem talking over the CC
     ///     session.
-    /// \throw SocketError in case the other side doesn't want to give us
-    ///     the socket for some reason (common cases are when the socket
-    ///     can't be allocated or bound, or when the socket is claimed by
-    ///     some other application and the sharing parameters don't allow
-    ///     sharing it).
+    /// \throw SocketError in case we have some other problems receiving the
+    ///     socket (eg. inconsistency in the protocol, the socket got stuck
+    ///     in the transport, etc). If the exception is not of the following
+    ///     derived ones, it usualy means something serious happened.
+    /// \throw SocketAllocateError if the other side can't create the socket.
+    /// \throw ShareError if the socket is used by other bind10 module and
+    ///     that one doesn't want to share it with us.
     virtual SocketID requestSocket(Protocol protocol,
                                    const std::string& address,
                                    uint16_t port, ShareMode share_mode,

+ 6 - 0
src/lib/server_common/tests/socket_requestor_test.cc

@@ -250,8 +250,14 @@ TEST_F(SocketRequestorTest, testBadRequestAnswers) {
     }
 
     // Send back an error response
+    // A generic one first
     session.getMessages()->add(createAnswer(1, "error"));
     ASSERT_THROW(doRequest(), CCSessionError);
+    // Now some with specific exceptions
+    session.getMessages()->add(createAnswer(2, "error"));
+    ASSERT_THROW(doRequest(), SocketRequestor::SocketAllocateError);
+    session.getMessages()->add(createAnswer(3, "error"));
+    ASSERT_THROW(doRequest(), SocketRequestor::ShareError);
 }
 
 // Helper function to create the release commands as we expect

+ 1 - 0
tests/lettuce/configurations/resolver/resolver_basic.config.orig

@@ -0,0 +1 @@
+{"version": 2, "Resolver": {"query_acl": [{"action": "REJECT", "from": "127.0.0.1"}], "listen_on": [{"port": 47806, "address": "127.0.0.1"}]}, "Boss": {"components": {"b10-resolver": {"kind": "needed"}, "b10-cmdctl": {"kind": "needed", "special": "cmdctl"}}}}

+ 26 - 0
tests/lettuce/features/resolver_basic.feature

@@ -0,0 +1,26 @@
+Feature: Basic Resolver
+    This feature set is just testing the execution of the b10-resolver
+    module. It sees whether it starts up, takes configuration, and
+    answers queries.
+
+    Scenario: Listen for and answer query
+        # This scenario starts a server that runs a real resolver.
+        # In order not to send out queries into the wild, we only
+        # query for something known to be hardcoded at this moment.
+        # NOTE: once real priming has been implemented, this test needs
+        # to be revised (as it would then leak, which is probably true
+        # for any resolver system test)
+        When I start bind10 with configuration resolver/resolver_basic.config
+        And wait for new bind10 stderr message RESOLVER_STARTED
+
+        # The ACL is set to reject any queries
+        A query for l.root-servers.net. should have rcode REFUSED
+
+        # Test whether acl ACCEPT works
+        When I set bind10 configuration Resolver/query_acl[0]/action to ACCEPT
+        # This address is currently hardcoded, so shouldn't cause outside traffic
+        A query for l.root-servers.net. should have rcode NOERROR
+
+        # Check whether setting the ACL to reject again works
+        When I set bind10 configuration Resolver/query_acl[0]/action to REJECT
+        A query for l.root-servers.net. should have rcode REFUSED

+ 1 - 1
tests/lettuce/features/terrain/querying.py

@@ -179,7 +179,7 @@ class QueryResult(object):
         """
         pass
 
-@step('A query for ([\w.]+) (?:type ([A-Z0-9]+) )?(?:class ([A-Z]+) )?' +
+@step('A query for ([\w.-]+) (?:type ([A-Z0-9]+) )?(?:class ([A-Z]+) )?' +
       '(?:to ([^:]+)(?::([0-9]+))? )?should have rcode ([\w.]+)')
 def query(step, query_name, qtype, qclass, addr, port, rcode):
     """

+ 4 - 1
tests/lettuce/features/terrain/terrain.py

@@ -42,7 +42,10 @@ import time
 # The first element is the original, the second is the target that will be
 # used by the tests that need them
 copylist = [
-["configurations/example.org.config.orig", "configurations/example.org.config"]
+    ["configurations/example.org.config.orig",
+     "configurations/example.org.config"],
+    ["configurations/resolver/resolver_basic.config.orig",
+     "configurations/resolver/resolver_basic.config"]
 ]
 
 # This is a list of files that, if present, will be removed before a scenario