Browse Source

[master] Merge branch 'trac1938'

JINMEI Tatuya 12 years ago
parent
commit
89d7de8e2f

+ 8 - 1
src/bin/auth/auth_messages.mes

@@ -266,9 +266,16 @@ bug ticket for this issue.
 This is a debug message issued when the authoritative server has received
 a command on the command channel.
 
-% AUTH_RECEIVED_NOTIFY received incoming NOTIFY for zone name %1, zone class %2
+% AUTH_RECEIVED_NOTIFY received incoming NOTIFY for zone %1/%2 from %3
 This is a debug message reporting that an incoming NOTIFY was received.
 
+% AUTH_RECEIVED_NOTIFY_NOTAUTH received bad NOTIFY for zone %1/%2 from %3
+The authoritative server received a NOTIFY message, but the specified zone
+doesn't match any of the zones served by the server.  The server doesn't
+process the message further, and returns a response with the Rcode being
+NOTAUTH.  Note: RFC 1996 does not specify the server behavior in this case;
+responding with Rcode of NOTAUTH follows BIND 9's behavior.
+
 % AUTH_RESPONSE_FAILURE exception while building response to query: %1
 This is a debug message, generated by the authoritative server when an
 attempt to create a response to a received DNS packet has failed. The

+ 21 - 5
src/bin/auth/auth_srv.cc

@@ -747,6 +747,8 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
                            std::auto_ptr<TSIGContext> tsig_context,
                            MessageAttributes& stats_attrs)
 {
+    const IOEndpoint& remote_ep = io_message.getRemoteEndpoint(); // for logs
+
     // The incoming notify must contain exactly one question for SOA of the
     // zone name.
     if (message.getRRCount(Message::SECTION_QUESTION) != 1) {
@@ -769,8 +771,23 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
     // on, but we don't check these conditions.  This behavior is compatible
     // with BIND 9.
 
-    // TODO check with the conf-mgr whether current server is the auth of the
-    // zone
+    // See if we have the specified zone in our data sources; if not return
+    // NOTAUTH, following BIND 9 (this is not specified in RFC 1996).
+    bool is_auth = false;
+    {
+        auth::DataSrcClientsMgr::Holder datasrc_holder(datasrc_clients_mgr_);
+        const shared_ptr<datasrc::ClientList> dsrc_clients =
+            datasrc_holder.findClientList(question->getClass());
+        is_auth = dsrc_clients &&
+            dsrc_clients->find(question->getName(), true, false).exact_match_;
+    }
+    if (!is_auth) {
+        LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_RECEIVED_NOTIFY_NOTAUTH)
+            .arg(question->getName()).arg(question->getClass()).arg(remote_ep);
+        makeErrorMessage(renderer_, message, buffer, Rcode::NOTAUTH(),
+                         stats_attrs, tsig_context);
+        return (true);
+    }
 
     // In the code that follows, we simply ignore the notify if any internal
     // error happens rather than returning (e.g.) SERVFAIL.  RFC 1996 is
@@ -782,10 +799,9 @@ AuthSrvImpl::processNotify(const IOMessage& io_message, Message& message,
     }
 
     LOG_DEBUG(auth_logger, DBG_AUTH_DETAIL, AUTH_RECEIVED_NOTIFY)
-      .arg(question->getName()).arg(question->getClass());
+        .arg(question->getName()).arg(question->getClass()).arg(remote_ep);
 
-    const string remote_ip_address =
-        io_message.getRemoteEndpoint().getAddress().toText();
+    const string remote_ip_address = remote_ep.getAddress().toText();
     static const string command_template_start =
         "{\"command\": [\"notify\", {\"zone_name\" : \"";
     static const string command_template_master = "\", \"master\" : \"";

+ 130 - 50
src/bin/auth/tests/auth_srv_unittest.cc

@@ -244,6 +244,62 @@ createBuiltinVersionResponse(const qid_t qid, vector<uint8_t>& data) {
                 renderer.getLength());
 }
 
+void
+installDataSrcClientLists(AuthSrv& server, ClientListMapPtr lists) {
+    // For now, we use explicit swap than reconfigure() because the latter
+    // involves a separate thread and cannot guarantee the new config is
+    // available for the subsequent test.
+    server.getDataSrcClientsMgr().setDataSrcClientLists(lists);
+}
+
+void
+updateDatabase(AuthSrv& server, const char* params) {
+    const ConstElementPtr config(Element::fromJSON("{"
+        "\"IN\": [{"
+        "    \"type\": \"sqlite3\","
+        "    \"params\": " + string(params) +
+        "}]}"));
+    installDataSrcClientLists(server, configureDataSource(config));
+}
+
+// Note: if with_static is set to true, the corresponding test should be
+// disabled in case of USE_STATIC_LINK.
+void
+updateInMemory(AuthSrv& server, const char* origin, const char* filename,
+               bool with_static = true)
+{
+    string spec_txt = "{"
+        "\"IN\": [{"
+        "   \"type\": \"MasterFiles\","
+        "   \"params\": {"
+        "       \"" + string(origin) + "\": \"" + string(filename) + "\""
+        "   },"
+        "   \"cache-enable\": true"
+        "}]";
+    if (with_static) {
+        spec_txt += ", \"CH\": [{"
+        "   \"type\": \"static\","
+        "   \"params\": \"" + string(STATIC_DSRC_FILE) + "\""
+            "}]";
+    }
+    spec_txt += "}";
+
+    const ConstElementPtr config(Element::fromJSON(spec_txt));
+    installDataSrcClientLists(server, configureDataSource(config));
+}
+
+// Note: tests using this function should be disabled in case of
+// USE_STATIC_LINK.
+void
+updateBuiltin(AuthSrv& server) {
+    const ConstElementPtr config(Element::fromJSON("{"
+        "\"CH\": [{"
+        "   \"type\": \"static\","
+        "   \"params\": \"" + string(STATIC_DSRC_FILE) + "\""
+        "}]}"));
+    installDataSrcClientLists(server, configureDataSource(config));
+}
+
 // We did not configure any client lists. Therefore it should be REFUSED
 TEST_F(AuthSrvTest, noClientList) {
     UnitTestUtil::createRequestMessage(request_message, Opcode::QUERY(),
@@ -647,8 +703,10 @@ TEST_F(AuthSrvTest, IXFRDisconnectFail) {
 }
 
 TEST_F(AuthSrvTest, notify) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -664,7 +722,7 @@ TEST_F(AuthSrvTest, notify) {
                   stringValue());
     ConstElementPtr notify_args =
         notify_session.getSentMessage()->get("command")->get(1);
-    EXPECT_EQ("example.com.", notify_args->get("zone_name")->stringValue());
+    EXPECT_EQ("example.", notify_args->get("zone_name")->stringValue());
     EXPECT_EQ(DEFAULT_REMOTE_ADDRESS,
               notify_args->get("master")->stringValue());
     EXPECT_EQ("IN", notify_args->get("zone_class")->stringValue());
@@ -675,7 +733,7 @@ TEST_F(AuthSrvTest, notify) {
 
     // The question must be identical to that of the received notify
     ConstQuestionPtr question = *parse_message->beginQuestion();
-    EXPECT_EQ(Name("example.com"), question->getName());
+    EXPECT_EQ(Name("example"), question->getName());
     EXPECT_EQ(RRClass::IN(), question->getClass());
     EXPECT_EQ(RRType::SOA(), question->getType());
 
@@ -690,10 +748,17 @@ TEST_F(AuthSrvTest, notify) {
     checkStatisticsCounters(stats_after, expect);
 }
 
+#ifdef USE_STATIC_LINK
+TEST_F(AuthSrvTest, DISABLED_notifyForCHClass) {
+#else
 TEST_F(AuthSrvTest, notifyForCHClass) {
-    // Same as the previous test, but for the CH RRClass.
+#endif
+    // Same as the previous test, but for the CH RRClass (so we install the
+    // builtin (static) data source.
+    updateBuiltin(server);
+
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("bind"),
                                        RRClass::CH(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -773,9 +838,11 @@ TEST_F(AuthSrvTest, notifyNonSOAQuestion) {
 }
 
 TEST_F(AuthSrvTest, notifyWithoutAA) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     // implicitly leave the AA bit off.  our implementation will accept it.
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     createRequestPacket(request_message, IPPROTO_UDP);
     server.processMessage(*io_message, *parse_message, *response_obuffer,
@@ -786,8 +853,10 @@ TEST_F(AuthSrvTest, notifyWithoutAA) {
 }
 
 TEST_F(AuthSrvTest, notifyWithErrorRcode) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     request_message.setRcode(Rcode::SERVFAIL());
@@ -800,10 +869,12 @@ TEST_F(AuthSrvTest, notifyWithErrorRcode) {
 }
 
 TEST_F(AuthSrvTest, notifyWithoutSession) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     server.setXfrinSession(NULL);
 
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -816,10 +887,12 @@ TEST_F(AuthSrvTest, notifyWithoutSession) {
 }
 
 TEST_F(AuthSrvTest, notifySendFail) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     notify_session.disableSend();
 
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -830,10 +903,12 @@ TEST_F(AuthSrvTest, notifySendFail) {
 }
 
 TEST_F(AuthSrvTest, notifyReceiveFail) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     notify_session.disableReceive();
 
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -843,10 +918,12 @@ TEST_F(AuthSrvTest, notifyReceiveFail) {
 }
 
 TEST_F(AuthSrvTest, notifyWithBogusSessionMessage) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     notify_session.setMessage(Element::fromJSON("{\"foo\": 1}"));
 
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -856,11 +933,13 @@ TEST_F(AuthSrvTest, notifyWithBogusSessionMessage) {
 }
 
 TEST_F(AuthSrvTest, notifyWithSessionMessageError) {
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
     notify_session.setMessage(
         Element::fromJSON("{\"result\": [1, \"FAIL\"]}"));
 
     UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
-                                       default_qid, Name("example.com"),
+                                       default_qid, Name("example"),
                                        RRClass::IN(), RRType::SOA());
     request_message.setHeaderFlag(Message::HEADERFLAG_AA);
     createRequestPacket(request_message, IPPROTO_UDP);
@@ -869,49 +948,50 @@ TEST_F(AuthSrvTest, notifyWithSessionMessageError) {
     EXPECT_FALSE(dnsserv.hasAnswer());
 }
 
-void
-installDataSrcClientLists(AuthSrv& server, ClientListMapPtr lists) {
-    // For now, we use explicit swap than reconfigure() because the latter
-    // involves a separate thread and cannot guarantee the new config is
-    // available for the subsequent test.
-    server.getDataSrcClientsMgr().setDataSrcClientLists(lists);
-}
+TEST_F(AuthSrvTest, notifyNotAuth) {
+    // If the server doesn't have authority of the specified zone in NOTIFY,
+    // it will return NOTAUTH
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
 
-void
-updateDatabase(AuthSrv& server, const char* params) {
-    const ConstElementPtr config(Element::fromJSON("{"
-        "\"IN\": [{"
-        "    \"type\": \"sqlite3\","
-        "    \"params\": " + string(params) +
-        "}]}"));
-    installDataSrcClientLists(server, configureDataSource(config));
+    UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
+                                       default_qid, Name("example.com"),
+                                       RRClass::IN(), RRType::SOA());
+    request_message.setHeaderFlag(Message::HEADERFLAG_AA);
+    createRequestPacket(request_message, IPPROTO_UDP);
+    server.processMessage(*io_message, *parse_message, *response_obuffer,
+                          &dnsserv);
+    EXPECT_TRUE(dnsserv.hasAnswer());
+    headerCheck(*parse_message, default_qid, Rcode::NOTAUTH(),
+                Opcode::NOTIFY().getCode(), QR_FLAG /* no AA */, 1, 0, 0, 0);
 }
 
-void
-updateInMemory(AuthSrv& server, const char* origin, const char* filename) {
-    const ConstElementPtr config(Element::fromJSON("{"
-        "\"IN\": [{"
-        "   \"type\": \"MasterFiles\","
-        "   \"params\": {"
-        "       \"" + string(origin) + "\": \"" + string(filename) + "\""
-        "   },"
-        "   \"cache-enable\": true"
-        "}],"
-        "\"CH\": [{"
-        "   \"type\": \"static\","
-        "   \"params\": \"" + string(STATIC_DSRC_FILE) + "\""
-        "}]}"));
-    installDataSrcClientLists(server, configureDataSource(config));
+TEST_F(AuthSrvTest, notifyNotAuthSubDomain) {
+    // Similar to the previous case, but checking partial match doesn't confuse
+    // the processing.
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
+    UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
+                                       default_qid, Name("child.example"),
+                                       RRClass::IN(), RRType::SOA());
+    createRequestPacket(request_message, IPPROTO_UDP);
+    server.processMessage(*io_message, *parse_message, *response_obuffer,
+                          &dnsserv);
+    headerCheck(*parse_message, default_qid, Rcode::NOTAUTH(),
+                Opcode::NOTIFY().getCode(), QR_FLAG, 1, 0, 0, 0);
 }
 
-void
-updateBuiltin(AuthSrv& server) {
-    const ConstElementPtr config(Element::fromJSON("{"
-        "\"CH\": [{"
-        "   \"type\": \"static\","
-        "   \"params\": \"" + string(STATIC_DSRC_FILE) + "\""
-        "}]}"));
-    installDataSrcClientLists(server, configureDataSource(config));
+TEST_F(AuthSrvTest, notifyNotAuthNoClass) {
+    // Likewise, and there's not even a data source in the specified class.
+    updateInMemory(server, "example.", CONFIG_INMEMORY_EXAMPLE, false);
+
+    UnitTestUtil::createRequestMessage(request_message, Opcode::NOTIFY(),
+                                       default_qid, Name("example"),
+                                       RRClass::CH(), RRType::SOA());
+    createRequestPacket(request_message, IPPROTO_UDP);
+    server.processMessage(*io_message, *parse_message, *response_obuffer,
+                          &dnsserv);
+    headerCheck(*parse_message, default_qid, Rcode::NOTAUTH(),
+                Opcode::NOTIFY().getCode(), QR_FLAG, 1, 0, 0, 0);
 }
 
 // Try giving the server a TSIG signed request and see it can anwer signed as

+ 59 - 12
src/bin/zonemgr/tests/zonemgr_test.py

@@ -314,16 +314,22 @@ class TestZonemgrRefresh(unittest.TestCase):
         sqlite3_ds.get_zone_soa = old_get_zone_soa
 
     def test_zone_handle_notify(self):
-        self.zone_refresh.zone_handle_notify(ZONE_NAME_CLASS1_IN,"127.0.0.1")
-        notify_master = self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["notify_master"]
+        self.assertTrue(self.zone_refresh.zone_handle_notify(
+                ZONE_NAME_CLASS1_IN, "127.0.0.1"))
+        notify_master = self.zone_refresh.\
+            _zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["notify_master"]
         self.assertEqual("127.0.0.1", notify_master)
-        zone_timeout = self.zone_refresh._zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["next_refresh_time"]
+        zone_timeout = self.zone_refresh.\
+            _zonemgr_refresh_info[ZONE_NAME_CLASS1_IN]["next_refresh_time"]
         current_time = time.time()
         self.assertTrue(zone_timeout <= current_time)
-        self.assertRaises(ZonemgrException, self.zone_refresh.zone_handle_notify,\
-                          ZONE_NAME_CLASS3_CH, "127.0.0.1")
-        self.assertRaises(ZonemgrException, self.zone_refresh.zone_handle_notify,\
-                          ZONE_NAME_CLASS3_IN, "127.0.0.1")
+
+        # If the specified zone does not in the configured secondary list,
+        # it should return False.
+        self.assertFalse(self.zone_refresh.zone_handle_notify(
+                ZONE_NAME_CLASS3_CH, "127.0.0.1"))
+        self.assertFalse(self.zone_refresh.zone_handle_notify(
+                ZONE_NAME_CLASS3_IN, "127.0.0.1"))
 
     def test_zone_refresh_success(self):
         soa_rdata = 'a.example.net. root.example.net. 2009073106 1800 900 2419200 21600'
@@ -607,6 +613,19 @@ class TestZonemgrRefresh(unittest.TestCase):
                           config, self.cc_session)
 
 class MyZonemgr(Zonemgr):
+    class DummySocket:
+        """This dummy class simply steal send() to record any transmitted data.
+
+        """
+        def __init__(self):
+            self.sent_data = []
+
+        def send(self, data):
+            self.sent_data.append(data)
+
+    class DummyLock:
+        def __enter__(self): pass
+        def __exit__(self, type, value, traceback): pass
 
     def __init__(self):
         self._db_file = TEST_SQLITE3_DBFILE
@@ -621,6 +640,8 @@ class MyZonemgr(Zonemgr):
                     "reload_jitter" : 0.75,
                     "secondary_zones": []
                     }
+        self._lock = self.DummyLock()
+        self._master_socket = self.DummySocket()
 
     def _start_zone_refresh_timer(self):
         pass
@@ -672,15 +693,21 @@ class TestZonemgr(unittest.TestCase):
         self.assertEqual(TEST_SQLITE3_DBFILE, self.zonemgr.get_db_file())
 
     def test_parse_cmd_params(self):
-        params1 = {"zone_name" : "example.com.", "zone_class" : "CH", "master" : "127.0.0.1"}
+        params1 = {"zone_name" : "example.com.", "zone_class" : "CH",
+                   "master" : "127.0.0.1"}
         answer1 = (ZONE_NAME_CLASS3_CH, "127.0.0.1")
-        self.assertEqual(answer1, self.zonemgr._parse_cmd_params(params1, ZONE_NOTIFY_COMMAND))
+        self.assertEqual(answer1,
+                         self.zonemgr._parse_cmd_params(params1,
+                                                        ZONE_NOTIFY_COMMAND))
         params2 = {"zone_name" : "example.com.", "zone_class" : "IN"}
         answer2 = ZONE_NAME_CLASS3_IN
-        self.assertEqual(answer2, self.zonemgr._parse_cmd_params(params2, notify_out.ZONE_NEW_DATA_READY_CMD))
-        self.assertRaises(ZonemgrException, self.zonemgr._parse_cmd_params, params2, ZONE_NOTIFY_COMMAND)
+        self.assertEqual(answer2, self.zonemgr._parse_cmd_params(
+                params2, notify_out.ZONE_NEW_DATA_READY_CMD))
+        self.assertRaises(ZonemgrException, self.zonemgr._parse_cmd_params,
+                          params2, ZONE_NOTIFY_COMMAND)
         params1 = {"zone_class" : "CH"}
-        self.assertRaises(ZonemgrException, self.zonemgr._parse_cmd_params, params2, ZONE_NOTIFY_COMMAND)
+        self.assertRaises(ZonemgrException, self.zonemgr._parse_cmd_params,
+                          params2, ZONE_NOTIFY_COMMAND)
 
     def test_config_data_check(self):
         # jitter should not be bigger than half of the original value
@@ -697,6 +724,26 @@ class TestZonemgr(unittest.TestCase):
         self.zonemgr.run()
         self.assertTrue(self.zonemgr._module_cc.stopped)
 
+    def test_command_handler_notify(self):
+        """Check the result of NOTIFY command."""
+        self.zonemgr._zone_refresh = MyZonemgrRefresh()
+
+        # On successful case, the other thread will be notified via
+        # _master_socket.
+        self.zonemgr._zone_refresh.zone_handle_notify = lambda x, y: True
+        self.zonemgr.command_handler("notify", {"zone_name": "example.",
+                                                "zone_class": "IN",
+                                                "master": "192.0.2.1"})
+        self.assertEqual([b" "], self.zonemgr._master_socket.sent_data)
+
+        # If the specified is not found in the secondary list, it doesn't
+        # bother to wake the thread (sent_data shouldn't change)
+        self.zonemgr._zone_refresh.zone_handle_notify = lambda x, y: False
+        self.zonemgr.command_handler("notify", {"zone_name": "example.",
+                                                "zone_class": "IN",
+                                                "master": "192.0.2.1"})
+        self.assertEqual([b" "], self.zonemgr._master_socket.sent_data)
+
 if __name__== "__main__":
     isc.log.resetUnitTestRootLogger()
     unittest.main()

+ 38 - 15
src/bin/zonemgr/zonemgr.py.in

@@ -191,14 +191,31 @@ class ZonemgrRefresh:
         self._set_zone_retry_timer(zone_name_class)
 
     def zone_handle_notify(self, zone_name_class, master):
-        """Handle zone notify"""
-        if (self._zone_not_exist(zone_name_class)):
-            logger.error(ZONEMGR_UNKNOWN_ZONE_NOTIFIED, zone_name_class[0],
-                         zone_name_class[1], master)
-            raise ZonemgrException("[b10-zonemgr] Notified zone (%s, %s) "
-                                   "doesn't belong to zonemgr" % zone_name_class)
+        """Handle an incomding NOTIFY message via the Auth module.
+
+        It returns True if the specified zone matches one of the locally
+        configured list of secondary zones; otherwise returns False.
+        In the latter case it assumes the server is a primary (master) of the
+        zone; the Auth module should have rejected the case where it's not
+        even authoritative for the zone.
+
+        Note: to be more robust and less independent from other module's
+        behavior, it's probably safer to check the authority condition here,
+        too.  But right now it uses SQLite3 specific API (to be deprecated),
+        so we rather rely on Auth.
+
+        Parameters:
+        zone_name_class (Name, RRClass): the notified zone name and class.
+        master (str): textual address of the NOTIFY sender.
+
+        """
+        if self._zone_not_exist(zone_name_class):
+            logger.debug(DBG_ZONEMGR_BASIC, ZONEMGR_ZONE_NOTIFY_NOT_SECONDARY,
+                         zone_name_class[0], zone_name_class[1], master)
+            return False
         self._set_zone_notifier_master(zone_name_class, master)
         self._set_zone_notify_timer(zone_name_class)
+        return True
 
     def zonemgr_reload_zone(self, zone_name_class):
         """ Reload a zone."""
@@ -423,7 +440,7 @@ class ZonemgrRefresh:
 
         # Ask the thread to stop
         self._running = False
-        self._write_sock.send(b'shutdown') # make self._read_sock readble
+        self._write_sock.send(b'shutdown') # make self._read_sock readable
         # Wait for it to actually finnish
         self._thread.join()
         # Wipe out what we do not need
@@ -630,27 +647,33 @@ class Zonemgr:
             """ Handle Auth notify command"""
             # master is the source sender of the notify message.
             zone_name_class, master = self._parse_cmd_params(args, command)
-            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY, zone_name_class[0], zone_name_class[1])
+            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_NOTIFY,
+                         zone_name_class[0], zone_name_class[1])
             with self._lock:
-                self._zone_refresh.zone_handle_notify(zone_name_class, master)
-            # Send notification to zonemgr timer thread
-            self._master_socket.send(b" ")# make self._slave_socket readble
+                need_refresh = self._zone_refresh.zone_handle_notify(
+                    zone_name_class, master)
+            if need_refresh:
+                # Send notification to zonemgr timer thread by making
+                # self._slave_socket readable.
+                self._master_socket.send(b" ")
 
         elif command == notify_out.ZONE_NEW_DATA_READY_CMD:
             """ Handle xfrin success command"""
             zone_name_class = self._parse_cmd_params(args, command)
-            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS, zone_name_class[0], zone_name_class[1])
+            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_SUCCESS,
+                         zone_name_class[0], zone_name_class[1])
             with self._lock:
                 self._zone_refresh.zone_refresh_success(zone_name_class)
-            self._master_socket.send(b" ")# make self._slave_socket readble
+            self._master_socket.send(b" ")# make self._slave_socket readable
 
         elif command == notify_out.ZONE_XFRIN_FAILED:
             """ Handle xfrin fail command"""
             zone_name_class = self._parse_cmd_params(args, command)
-            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED, zone_name_class[0], zone_name_class[1])
+            logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_XFRIN_FAILED,
+                         zone_name_class[0], zone_name_class[1])
             with self._lock:
                 self._zone_refresh.zone_refresh_fail(zone_name_class)
-            self._master_socket.send(b" ")# make self._slave_socket readble
+            self._master_socket.send(b" ")# make self._slave_socket readable
 
         elif command == "shutdown":
             logger.debug(DBG_ZONEMGR_COMMAND, ZONEMGR_RECEIVE_SHUTDOWN)

+ 11 - 6
src/bin/zonemgr/zonemgr_messages.mes

@@ -138,14 +138,19 @@ zone, or, if this error appears without the administrator giving transfer
 commands, it can indicate an error in the program, as it should not have
 initiated transfers of unknown zones on its own.
 
-% ZONEMGR_UNKNOWN_ZONE_NOTIFIED notified zone %1/%2 from %3 is not known to the zone manager
-A NOTIFY was received but the zone that was the subject of the operation
-is not being managed by the zone manager.  This may indicate an error
-in the program (as the operation should not have been initiated if this
-were the case).  Please submit a bug report.
-
 % ZONEMGR_UNKNOWN_ZONE_SUCCESS zone %1 (class %2) is not known to the zone manager
 An XFRIN operation has succeeded but the zone received is not being
 managed by the zone manager.  This may indicate an error in the program
 (as the operation should not have been initiated if this were the case).
 Please submit a bug report.
+
+% ZONEMGR_ZONE_NOTIFY_NOT_SECONDARY notify for zone %1/%2 from %3 received but not in secondaries
+A NOTIFY was received but the zone is not listed in the configured
+secondary zones of the zone manager.  The most common reason for this
+is that it's simply received by a primary server of the zone.  Another
+possibility is a configuration error that it's not configured as a
+secondary while it should be.  In either case, the zone manager does
+not take action in terms of zone management, and the authoritative
+server will respond to it like in the secondary case.  If this is a
+configuration error, it will be noticed by the fact that the zone
+isn't updated even after a change is made in the primary server.

+ 4 - 0
src/lib/python/isc/notify/notify_out.py

@@ -570,6 +570,10 @@ class NotifyOut:
             logger.error(NOTIFY_OUT_REPLY_UNCAUGHT_EXCEPTION, err)
             return _BAD_REPLY_PACKET
 
+        logger.debug(logger.DBGLVL_TRACE_BASIC, NOTIFY_OUT_REPLY_RECEIVED,
+                     zone_notify_info.zone_name, zone_notify_info.zone_class,
+                     from_addr[0], from_addr[1], msg.get_rcode())
+
         return _REPLY_OK
 
     def _get_notify_reply(self, sock, tgt_addr):

+ 5 - 0
src/lib/python/isc/notify/notify_out_messages.mes

@@ -60,6 +60,11 @@ given address, but the reply did not have the QR bit set to one.
 Since there was a response, no more notifies will be sent to this
 server for this notification event.
 
+% NOTIFY_OUT_REPLY_RECEIVED Zone %1/%2: notify response from %3:%4: %5
+The notify_out library sent a notify message to the nameserver at
+the given address, and received a response.  Its Rcode will be shown,
+too.
+
 % NOTIFY_OUT_REPLY_UNCAUGHT_EXCEPTION uncaught exception: %1
 There was an uncaught exception in the handling of a notify reply
 message, either in the message parser, or while trying to extract data

tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf → tests/lettuce/configurations/xfrin/retransfer_slave_notify.conf.orig


+ 27 - 0
tests/lettuce/features/queries.feature

@@ -95,6 +95,10 @@ Feature: Querying feature
         ns2.example.org.        3600    IN      A       192.0.2.4
         """
 
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
+
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command
         And wait for new bind10 stderr message AUTH_RECEIVED_COMMAND
@@ -137,6 +141,10 @@ Feature: Querying feature
         ns2.example.org.        3600    IN      A       192.0.2.4
         """
 
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for new bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
+
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command
         And wait for new bind10 stderr message AUTH_RECEIVED_COMMAND
@@ -167,6 +175,10 @@ Feature: Querying feature
         example.org.            3600    IN      SOA     ns1.example.org. admin.example.org. 1234 3600 1800 2419200 7200
         """
 
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for new bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
+
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command
         And wait for new bind10 stderr message AUTH_RECEIVED_COMMAND
@@ -231,6 +243,10 @@ Feature: Querying feature
         mail.example.org.       3600    IN      A       192.0.2.10
         """
 
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
+
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command
         And wait for new bind10 stderr message AUTH_RECEIVED_COMMAND
@@ -282,6 +298,10 @@ Feature: Querying feature
         ns.sub.example.org.	3600	IN	A	192.0.2.101
         """
 
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
+
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command
         And wait for new bind10 stderr message AUTH_RECEIVED_COMMAND
@@ -333,6 +353,10 @@ Feature: Querying feature
         A query for example.org type SSHFP should have rcode NOERROR
         The last query response should have ancount 0
 
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
+
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command
         And wait for new bind10 stderr message AUTH_RECEIVED_COMMAND
@@ -358,6 +382,9 @@ Feature: Querying feature
         """
         shell.example.org.      3600    IN      SSHFP   2 1 123456789abcdef67890123456789abcdef67890
         """
+        # Make sure handling statistics command handling checked below is
+        # after this query
+        And wait for bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
 
         When I wait for new bind10 stderr message STATS_SEND_STATISTICS_REQUEST
         # make sure Auth module receives a command

+ 8 - 4
tests/lettuce/features/terrain/steps.py

@@ -37,8 +37,10 @@ def wait_for_stderr_message(step, times, new, process_name, message, not_message
     output.
     Parameter:
     times: Check for the string this many times.
-    new: (' new', optional): Only check the output printed since last time
-                             this step was used for this process.
+    new: (' new', optional): Only check the output from the process that has
+                             not been covered in previous calls to this
+                             function.  See RunningProcess._wait_for_output_str
+                             for details.
     process_name ('<name> stderr'): Name of the process to check the output of.
     message ('message <message>'): Output (part) to wait for.
     not_message ('not <message>'): Output (part) to wait for, and fail
@@ -60,8 +62,10 @@ def wait_for_stdout_message(step, times, new, process_name, message, not_message
     output.
     Parameter:
     times: Check for the string this many times.
-    new: (' new', optional): Only check the output printed since last time
-                             this step was used for this process.
+    new: (' new', optional): Only check the output from the process that has
+                             not been covered in previous calls to this
+                             function.  See RunningProcess._wait_for_output_str
+                             for details.
     process_name ('<name> stderr'): Name of the process to check the output of.
     message ('message <message>'): Output (part) to wait for, and succeed.
     not_message ('not <message>'): Output (part) to wait for, and fail

+ 46 - 25
tests/lettuce/features/terrain/terrain.py

@@ -68,6 +68,8 @@ copylist = [
      "configurations/xfrin/retransfer_master_nons.conf"],
     ["configurations/xfrin/retransfer_slave.conf.orig",
      "configurations/xfrin/retransfer_slave.conf"],
+    ["configurations/xfrin/retransfer_slave_notify.conf.orig",
+     "configurations/xfrin/retransfer_slave_notify.conf"],
     ["data/inmem-xfrin.sqlite3.orig",
      "data/inmem-xfrin.sqlite3"],
     ["data/xfrin-before-diffs.sqlite3.orig",
@@ -112,6 +114,10 @@ class RunningProcess:
         self._create_filenames()
         self._start_process(args)
 
+        # used in _wait_for_output_str, map from (filename, (strings))
+        # to a file offset.
+        self.__file_offsets = {}
+
     def _start_process(self, args):
         """
         Start the process.
@@ -192,11 +198,29 @@ class RunningProcess:
         os.remove(self.stderr_filename)
         os.remove(self.stdout_filename)
 
-    def _wait_for_output_str(self, filename, running_file, strings, only_new, matches = 1):
-        """
-        Wait for a line of output in this process. This will (if only_new is
-        False) first check all previous output from the process, and if not
-        found, check all output since the last time this method was called.
+    def _wait_for_output_str(self, filename, running_file, strings, only_new,
+                             matches=1):
+        """
+        Wait for a line of output in this process. This will (if
+        only_new is False) check all output from the process including
+        that may have been checked before.  If only_new is True, it
+        only checks output that has not been covered in previous calls
+        to this method for the file (if there was no such previous call to
+        this method, it works same as the case of only_new=False).
+
+        Care should be taken if only_new is to be set to True, as it may cause
+        counter-intuitive results.  For example, assume the file is expected
+        to contain a line that has XXX and another line has YYY, but the
+        ordering is not predictable.  If this method is called with XXX as
+        the search string, but the line containing YYY appears before the
+        target line, this method remembers the point in the file beyond
+        the line that has XXX.  If a next call to this method specifies
+        YYY as the search string with only_new being True, the search will
+        fail.  If the same string is expected to appear multiple times
+        and you want to catch the latest one, a more reliable way is to
+        specify the match number and set only_new to False, if the number
+        of matches is predictable.
+
         For each line in the output, the given strings array is checked. If
         any output lines checked contains one of the strings in the strings
         array, that string (not the line!) is returned.
@@ -204,33 +228,34 @@ class RunningProcess:
         filename: The filename to read previous output from, if applicable.
         running_file: The open file to read new output from.
         strings: Array of strings to look for.
-        only_new: If true, only check output since last time this method was
-                  called. If false, first check earlier output.
+        only_new: See above.
         matches: Check for the string this many times.
         Returns a tuple containing the matched string, and the complete line
         it was found in.
         Fails if none of the strings was read after 10 seconds
         (OUTPUT_WAIT_INTERVAL * OUTPUT_WAIT_MAX_INTERVALS).
         """
+        # Identify the start offset of search.  if only_new=True, start from
+        # the farthest point we've reached in the file; otherwise start from
+        # the beginning.
+        if not filename in self.__file_offsets:
+            self.__file_offsets[filename] = 0
+        offset = self.__file_offsets[filename] if only_new else 0
+        running_file.seek(offset)
+
         match_count = 0
-        if not only_new:
-            full_file = open(filename, "r")
-            for line in full_file:
-                for string in strings:
-                    if line.find(string) != -1:
-                        match_count += 1
-                        if match_count >= matches:
-                            full_file.close()
-                            return (string, line)
         wait_count = 0
         while wait_count < OUTPUT_WAIT_MAX_INTERVALS:
-            where = running_file.tell()
             line = running_file.readline()
+            where = running_file.tell()
             if line:
                 for string in strings:
                     if line.find(string) != -1:
                         match_count += 1
                         if match_count >= matches:
+                            # If we've gone further, update the recorded offset
+                            if where > self.__file_offsets[filename]:
+                                self.__file_offsets[filename] = where
                             return (string, line)
             else:
                 wait_count += 1
@@ -243,8 +268,7 @@ class RunningProcess:
         Wait for one of the given strings in this process's stderr output.
         Parameters:
         strings: Array of strings to look for.
-        only_new: If true, only check output since last time this method was
-                  called. If false, first check earlier output.
+        only_new: See _wait_for_output_str.
         matches: Check for the string this many times.
         Returns a tuple containing the matched string, and the complete line
         it was found in.
@@ -259,8 +283,7 @@ class RunningProcess:
         Wait for one of the given strings in this process's stdout output.
         Parameters:
         strings: Array of strings to look for.
-        only_new: If true, only check output since last time this method was
-                  called. If false, first check earlier output.
+        only_new: See _wait_for_output_str.
         matches: Check for the string this many times.
         Returns a tuple containing the matched string, and the complete line
         it was found in.
@@ -340,8 +363,7 @@ class RunningProcesses:
         Parameters:
         process_name: The name of the process to check the stderr output of.
         strings: Array of strings to look for.
-        only_new: If true, only check output since last time this method was
-                  called. If false, first check earlier output.
+        only_new: See _wait_for_output_str.
         matches: Check for the string this many times.
         Returns the matched string.
         Fails if none of the strings was read after 10 seconds
@@ -360,8 +382,7 @@ class RunningProcesses:
         Parameters:
         process_name: The name of the process to check the stdout output of.
         strings: Array of strings to look for.
-        only_new: If true, only check output since last time this method was
-                  called. If false, first check earlier output.
+        only_new: See _wait_for_output_str.
         matches: Check for the string this many times.
         Returns the matched string.
         Fails if none of the strings was read after 10 seconds

+ 12 - 5
tests/lettuce/features/xfrin_bind10.feature

@@ -33,7 +33,10 @@ Feature: Xfrin
     And wait for new bind10 stderr message XFRIN_ZONE_WARN
     # But after complaining, the zone data should be accepted.
     Then wait for new bind10 stderr message XFRIN_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    # there's no guarantee this is logged before XFRIN_TRANSFER_SUCCESS, so
+    # we can't reliably use 'wait for new'.  In this case this should be the
+    # only occurrence of this message, so this should be okay.
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
     A query for www.example.org to [::1]:47806 should have rcode NOERROR
 
     # The transferred zone should have 11 non-NSEC3 RRs and 1 NSEC3 RR.
@@ -56,7 +59,8 @@ Feature: Xfrin
     Then I send bind10 the command Xfrin retransfer example.org IN ::1 47807
     And wait for new bind10 stderr message XFRIN_ZONE_INVALID
     And wait for new bind10 stderr message XFRIN_INVALID_ZONE_DATA
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED
+    # We can't use 'wait for new' here; see above.
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED
     A query for example.org type NS to [::1]:47806 should have rcode NOERROR
     And transfer result should have 13 rrs
 
@@ -82,7 +86,8 @@ Feature: Xfrin
     # Make sure it is fully open
     When I send bind10 the command Xfrin retransfer example.org
     Then wait for new bind10 stderr message XFRIN_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
-    And wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    # this can't be 'wait for new'; see above.
+    And wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
 
     # First to master, a transfer should then fail
     When I send bind10 the following commands with cmdctl port 47804:
@@ -139,7 +144,8 @@ Feature: Xfrin
     # zone is invalid and then reject it.
     And wait for new bind10 stderr message XFRIN_ZONE_INVALID
     And wait for new bind10 stderr message XFRIN_INVALID_ZONE_DATA
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED
+    # This can't be 'wait for new'
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED
     # The zone still doesn't exist as it is rejected.
     # FIXME: This step fails. Probably an empty zone is created in the data
     # source :-|. This should be REFUSED, not SERVFAIL.
@@ -179,7 +185,8 @@ Feature: Xfrin
     When I send bind10 the command Xfrin retransfer example. IN ::1 47807
     Then wait for new bind10 stderr message XFRIN_GOT_INCREMENTAL_RESP
     Then wait for new bind10 stderr message XFRIN_IXFR_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    # This can't be 'wait for new'
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
 
     A query for example. type SOA to [::1]:47806 should have rcode NOERROR
     The answer section of the last query response should be

+ 107 - 17
tests/lettuce/features/xfrin_notify_handling.feature

@@ -51,14 +51,19 @@ Feature: Xfrin incoming notify handling
     When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
     Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
     Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_NOTIFY
-    Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
-    Then wait for new bind10 stderr message XFRIN_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
-    Then wait 5 times for new master stderr message NOTIFY_OUT_SENDING_NOTIFY
-    Then wait for new master stderr message NOTIFY_OUT_RETRY_EXCEEDED
+    # From this point we can't reliably 'wait for new' because the ordering
+    # of logs from different processes is unpredictable.  But these
+    # should be okay in this case.
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_NOTIFY
+    Then wait for bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
+    Then wait for bind10 stderr message XFRIN_TRANSFER_SUCCESS not XFRIN_XFR_PROCESS_FAILURE
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    Then wait for master stderr message NOTIFY_OUT_REPLY_RECEIVED
 
     A query for www.example.org to [::1]:47806 should have rcode NOERROR
+    # Make sure handling statistics command handling checked below is
+    # after this query
+    And wait for bind10 stderr message AUTH_SEND_NORMAL_RESPONSE
 
     #
     # Test for statistics
@@ -67,7 +72,9 @@ Feature: Xfrin incoming notify handling
     #
 
     # wait until the last stats requesting is finished
-    wait for new master stderr message STATS_SEND_STATISTICS_REQUEST
+    # note that this does not 100% guarantee the stats updated Xfrout
+    # statistics.  But there doesn't seem to be a better log message that
+    # suggests this event.
     wait for new master stderr message XFROUT_RECEIVED_GETSTATS_COMMAND
 
     When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
@@ -155,12 +162,12 @@ Feature: Xfrin incoming notify handling
     When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
     Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
     Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_NOTIFY
-    Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
-    Then wait for new bind10 stderr message XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION not XFRIN_XFR_TRANSFER_STARTED
-    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED not ZONEMGR_RECEIVE_XFRIN_SUCCESS
-    Then wait 5 times for new master stderr message NOTIFY_OUT_SENDING_NOTIFY
-    Then wait for new master stderr message NOTIFY_OUT_RETRY_EXCEEDED
+    # can't use 'wait for new' below.
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_NOTIFY
+    Then wait for bind10 stderr message XFRIN_XFR_TRANSFER_STARTED
+    Then wait for bind10 stderr message XFRIN_XFR_TRANSFER_PROTOCOL_VIOLATION not XFRIN_XFR_TRANSFER_STARTED
+    Then wait for bind10 stderr message ZONEMGR_RECEIVE_XFRIN_FAILED not ZONEMGR_RECEIVE_XFRIN_SUCCESS
+    Then wait for master stderr message NOTIFY_OUT_REPLY_RECEIVED
 
     A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
 
@@ -170,8 +177,7 @@ Feature: Xfrin incoming notify handling
     # check for statistics change
     #
 
-    # wait until the last stats requesting is finished
-    wait for new master stderr message STATS_SEND_STATISTICS_REQUEST
+    # wait until stats request at least after NOTIFY_OUT_REPLY_RECEIVED
     wait for new master stderr message XFROUT_RECEIVED_GETSTATS_COMMAND
 
     When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
@@ -220,8 +226,7 @@ Feature: Xfrin incoming notify handling
     # check statistics change
     #
 
-    # wait until the last stats requesting is finished
-    wait for new master stderr message STATS_SEND_STATISTICS_REQUEST
+    # wait until stats request at least after NOTIFY_OUT_TIMEOUT
     wait for new master stderr message XFROUT_RECEIVED_GETSTATS_COMMAND
 
     When I query statistics zones of bind10 module Xfrout with cmdctl port 47804
@@ -244,3 +249,88 @@ Feature: Xfrin incoming notify handling
     Then the statistics counter accept should be 0
     Then the statistics counter senderr should be 0
     Then the statistics counter recverr should be 0
+
+    #
+    # Test for NOTIFY that would result in NOTAUTH
+    #
+    Scenario: Handle incoming notify that does match authoritative zones
+    Given I have bind10 running with configuration xfrin/retransfer_master.conf with cmdctl port 47804 as master
+    And wait for master stderr message BIND10_STARTED_CC
+    And wait for master stderr message CMDCTL_STARTED
+    And wait for master stderr message AUTH_SERVER_STARTED
+    And wait for master stderr message XFROUT_STARTED
+    And wait for master stderr message ZONEMGR_STARTED
+    And wait for master stderr message STATS_STARTING
+
+    And I have bind10 running with configuration xfrin/retransfer_slave_notify.conf
+    And wait for bind10 stderr message BIND10_STARTED_CC
+    And wait for bind10 stderr message CMDCTL_STARTED
+    And wait for bind10 stderr message AUTH_SERVER_STARTED
+    And wait for bind10 stderr message XFRIN_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    #
+    # replace master's data source with unmatched zone for slave's zone.
+    # we restart Xfrout to make it sure.
+    #
+    When I send bind10 the following commands with cmdctl port 47804
+    """
+    config set data_sources/classes/IN[0]/params/database_file data/ixfr-out/zones.sqlite3
+    config set Auth/database_file data/ixfr-out/zones.sqlite3
+    config set Xfrout/zone_config[0]/origin example.com
+    config commit
+    Xfrout shutdown
+    """
+    last bindctl output should not contain "error"
+    And wait for new master stderr message XFROUT_STARTED
+
+    A query for www.example.com to [::1]:47806 should have rcode REFUSED
+
+    When I send bind10 with cmdctl port 47804 the command Xfrout notify example.com IN
+    Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
+    Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY_NOTAUTH
+    Then wait for new master stderr message NOTIFY_OUT_REPLY_RECEIVED
+
+    A query for www.example.com to [::1]:47806 should have rcode REFUSED
+
+    #
+    # Test for NOTIFY that's not in the secondaries list
+    #
+    Scenario: Handle incoming notify that is not in the secondaries list
+    Given I have bind10 running with configuration xfrin/retransfer_master.conf with cmdctl port 47804 as master
+    And wait for master stderr message BIND10_STARTED_CC
+    And wait for master stderr message CMDCTL_STARTED
+    And wait for master stderr message AUTH_SERVER_STARTED
+    And wait for master stderr message XFROUT_STARTED
+    And wait for master stderr message ZONEMGR_STARTED
+    And wait for master stderr message STATS_STARTING
+
+    And I have bind10 running with configuration xfrin/retransfer_slave_notify.conf
+    And wait for bind10 stderr message BIND10_STARTED_CC
+    And wait for bind10 stderr message CMDCTL_STARTED
+    And wait for bind10 stderr message AUTH_SERVER_STARTED
+    And wait for bind10 stderr message XFRIN_STARTED
+    And wait for bind10 stderr message ZONEMGR_STARTED
+
+    #
+    # Empty slave's secondaries list, and restart zonemgr to make it sure
+    #
+    When I send bind10 the following commands with cmdctl
+    """
+    config remove Zonemgr/secondary_zones[0]
+    config commit
+    Zonemgr shutdown
+    """
+    last bindctl output should not contain "error"
+    And wait for new bind10 stderr message ZONEMGR_STARTED
+
+    A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN
+
+    When I send bind10 with cmdctl port 47804 the command Xfrout notify example.org IN
+    Then wait for new master stderr message XFROUT_NOTIFY_COMMAND
+    Then wait for new bind10 stderr message AUTH_RECEIVED_NOTIFY
+    Then wait for new bind10 stderr message ZONEMGR_RECEIVE_NOTIFY
+    Then wait for new bind10 stderr message ZONEMGR_ZONE_NOTIFY_NOT_SECONDARY
+    Then wait for new master stderr message NOTIFY_OUT_REPLY_RECEIVED
+
+    A query for www.example.org to [::1]:47806 should have rcode NXDOMAIN