]> git.ipfire.org Git - thirdparty/bind9.git/commitdiff
implementation of hook-based asynchronous functionality
authorJINMEI Tatuya <jtatuya@infoblox.com>
Wed, 16 Sep 2020 22:26:22 +0000 (15:26 -0700)
committerEvan Hunt <each@isc.org>
Tue, 24 Nov 2020 23:11:39 +0000 (15:11 -0800)
previously query plugins were strictly synchrounous - the query
process would be interrupted at some point, data would be looked
up or a change would be made, and then the query processing would
resume immediately.

this commit enables query plugins to initiate asynchronous processes
and resume on a completion event, as with recursion.

lib/ns/Makefile.am
lib/ns/include/ns/events.h [new file with mode: 0644]
lib/ns/include/ns/hooks.h
lib/ns/include/ns/query.h
lib/ns/include/ns/server.h
lib/ns/include/ns/types.h
lib/ns/query.c
lib/ns/tests/nstest.c
lib/ns/tests/query_test.c
lib/ns/win32/libns.def
util/copyrights

index d430493aef169fd04f8b29d274fc41b4aadbb945..243d087c5c8112b473f829be806a30adea2e60d4 100644 (file)
@@ -9,6 +9,7 @@ libns_ladir = $(includedir)/ns
 
 libns_la_HEADERS =                     \
        include/ns/client.h             \
+       include/ns/events.h             \
        include/ns/hooks.h              \
        include/ns/interfacemgr.h       \
        include/ns/lib.h                \
diff --git a/lib/ns/include/ns/events.h b/lib/ns/include/ns/events.h
new file mode 100644 (file)
index 0000000..8d3736c
--- /dev/null
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, you can obtain one at https://mozilla.org/MPL/2.0/.
+ *
+ * See the COPYRIGHT file distributed with this work for additional
+ * information regarding copyright ownership.
+ */
+
+#ifndef NS_EVENTS_H
+#define NS_EVENTS_H 1
+
+#include <isc/eventclass.h>
+
+/*! \file ns/events.h
+ * \brief
+ * Registry of NS event numbers.
+ */
+
+#define NS_EVENT_CLIENTCONTROL (ISC_EVENTCLASS_NS + 0)
+#define NS_EVENT_HOOKASYNCDONE (ISC_EVENTCLASS_NS + 1)
+
+#endif /* NS_EVENTS_H */
index e8568f380d46cc767cfe548011e4eb41100b0efd..a0e8b363e4b82e1a2201d133f8e2f699b7ecc0b1 100644 (file)
 
 #include <stdbool.h>
 
+#include <isc/event.h>
 #include <isc/list.h>
 #include <isc/magic.h>
+#include <isc/mem.h>
 #include <isc/result.h>
+#include <isc/task.h>
 
 #include <dns/rdatatype.h>
 
  * ns_hook_add().  As the hook action returns NS_HOOK_CONTINUE,
  * query_foo() would also be logging the "Lorem ipsum dolor sit amet..."
  * message before returning ISC_R_COMPLETE.
+ *
+ * ASYNCHRONOUS EVENT HANDLING IN QUERY HOOKS
+ *
+ * Usually a hook action works synchronously; it completes some particular
+ * job in the middle of query processing, thus blocking the caller (and the
+ * worker thread handling the query).  But sometimes an action can be time
+ * consuming and the blocking behavior may not be acceptable.  For example,
+ * a hook may need to send some kind of query (like a DB lookup) to an
+ * external backend server and wait for the response to complete the hook's
+ * action.  Depending on the network condition, the external server's load,
+ * etc, it may take several seconds or more.
+ *
+ * In order to handle such a situation, a hook action can start an
+ * asynchronous event by calling ns_query_hookasync().  This is similar
+ * to ns_query_recurse(), but more generic.  ns_query_hookasync() will
+ * call the 'runasync' function with a specified 'arg' (both passed to
+ * ns_query_hookasync()) and a set of task and associated event arguments
+ * to be called to resume query handling upon completion of the
+ * asynchronous event.
+ *
+ * The implementation of 'runasync' is assumed to allocate and build an
+ * instance of ns_hook_resevent_t whose action, arg, and task are set to
+ * the passed values from ns_query_hookasync().  Other fields of
+ * ns_hook_resevent_t must be correctly set in the hook implementation
+ * by the time it's sent to the specified task:
+ *
+ * - hookpoint: the point from which the query handling should be resumed
+ *   (which should usually be the hook point that triggered the asynchronous
+ *   event).
+ * - origresult: the result code passed to the hook action that triggers the
+ *   asynchronous event through the 'resultp' pointer.  Some hook points need
+ *   this value to correctly resume the query handling.
+ * - saved_qctx: the 'qctx' passed to 'runasync'.  This holds some
+ *   intermediate data for resolving the query, and will be used to resume the
+ *   query handling.  The 'runasync' implementation must not modify it.
+ *
+ * The hook implementation should somehow maintain the created event
+ * instance so that it can eventually send the event.
+ *
+ * 'runasync' then creates an instance of ns_hookasync_t with specifying its
+ * own cancel and destroy function, and returns it to ns_query_hookasync()
+ * in the passed pointer.
+ *
+ * On return from ns_query_hookasync(), the hook action MUST return
+ * NS_HOOK_RETURN to suspend the query handling.
+ *
+ * On the completion of the asynchronous event, the hook implementation is
+ * supposed to send the resumeevent to the corresponding task.  The query
+ * module resumes the query handling so that the hook action of the
+ * specified hook point will be called, skipping some intermediate query
+ * handling steps.  So, typically, the same hook action will be called
+ * twice.  The hook implementation must somehow remember the context, and
+ * handle the second call to complete its action using the result of the
+ * asynchronous event.
+ *
+ * Example: assume the following hook-specific structure to manage
+ * asynchronous events:
+ *
+ * typedef struct hookstate {
+ *     bool async;
+ *     ns_hook_resevent_t *rev
+ *     ns_hookpoint_t hookpoint;
+ *     isc_result_t origresult;
+ * } hookstate_t;
+ *
+ * 'async' is supposed to be true if and only if hook-triggered
+ * asynchronous processing is taking place.
+ *
+ * A hook action that uses an asynchronous event would look something
+ * like this:
+ *
+ * hook_recurse(void *hook_data, void *action_data, isc_result_t *resultp) {
+ *     hookstate_t *state = somehow_retrieve_from(action_data);
+ *     if (state->async) {
+ *             // just resumed from an asynchronous hook action.
+ *             // complete the hook's action using the result of the
+ *             // internal asynchronous event.
+ *             state->async = false;
+ *             return (NS_HOOK_CONTINUE);
+ *     }
+ *
+ *     // Initial call to the hook action.  Start the internal
+ *     // asynchronous event, and have the query module suspend
+ *     // its own handling by returning NS_HOOK_RETURN.
+ *     state->hookpoint = ...; // would be hook point for this hook
+ *     state->origresult = *resultp;
+ *     ns_query_hookasync(hook_data, runasync, state);
+ *     state->async = true;
+ *     return (NS_HOOK_RETURN);
+ * }
+ *
+ * And the 'runasync' function would be something like this:
+ *
+ * static isc_result_t
+ * runasync(query_ctx_t *qctx, void *arg, isc_taskaction_t action,
+ *         void *evarg, isc_task_t *task, ns_hookasync_t **ctxp) {
+ *     hookstate_t *state = arg;
+ *     ns_hook_resevent_t *rev = isc_event_allocate(
+ *             mctx, task, NS_EVENT_HOOKASYNCDONE, action, evarg,
+ *             sizeof(*rev));
+ *     ns_hookasync_t *ctx = isc_mem_get(mctx, sizeof(*ctx));
+ *
+ *     *ctx = (ns_hookasync_t){ .private = NULL };
+ *     isc_mem_attach(mctx, &ctx->mctx);
+ *     ctx->cancel = ...;  // set the cancel function, which cancels the
+ *                         // internal asynchronous event (if necessary).
+ *                         // it should eventually result in sending
+ *                         // the 'rev' event to the calling task.
+ *     ctx->destroy = ...; // set the destroy function, which frees 'ctx'
+ *
+ *     rev->hookpoint = state->hookpoint;
+ *     rev->origresult = state->origresult;
+ *     rev->saved_qctx = qctx;
+ *     rev->ctx = ctx;
+ *
+ *     state->rev = rev; // store the resume event so we can send it later
+ *
+ *     // initiate some asynchronous process here - for example, a
+ *     // recursive fetch.
+ *
+ *     *ctxp = ctx;
+ *     return (ISC_R_SUCCESS);
+ * }
+ *
+ * Finally, in the completion handler for the asynchronous process, we
+ * need to send a resumption event so that query processing can resume.
+ * For example, the completion handler might call this function:
+ *
+ * static void
+ * asyncproc_done(hookstate_t *state) {
+ *     isc_event_t *ev = (isc_event_t *)state->rev;
+ *     isc_task_send(ev->ev_sender, &ev);
+ * }
+ *
+ * Caveats:
+ * - On resuming from a hook-initiated asynchronous process, code in
+ *   the query module before the hook point needs to be exercised.
+ *   So if this part has side effects, it's possible that the resuming
+ *   doesn't work well.  Currently, NS_QUERY_RESPOND_ANY_FOUND is
+ *   explicitly prohibited to be used as the resume point.
+ * - In general, hooks other than those called at the beginning of the
+ *   caller function may not work safely with asynchronous processing for
+ *   the reason stated in the previous bullet.  For example, a hook action
+ *   for NS_QUERY_DONE_SEND may not be able to start an asychronous
+ *   function safely.
+ * - Hook-triggered asynchronous processing is not allowed to be running
+ *   while the standard DNS recursive fetch is taking place (starting
+ *   from a call to dns_resolver_createfetch()), as the two would be
+ *   using some of the same context resources.  For this reason the
+ *   NS_QUERY_NOTFOUND_RECURSE and NS_QUERY_ZEROTTL_RECURSE hook points
+ *   are explicitly prohibited from being used for asynchronous hook
+ *   actions.
+ * - Specifying multiple hook actions for the same hook point at the
+ *   same time may cause problems, as resumption from one hook action
+ *   could cause another hook to be called twice unintentionally.
+ *   It's generally not safe to assume such a use case works,
+ *   especially if the hooks are developed independently. (Note that
+ *   that's not necessarily specific to the use of asynchronous hook
+ *   actions. As long as hook actions have side effects, including
+ *   modifying the internal query state, it's not guaranteed safe
+ *   to use multiple independent hooks at the same time.)
  */
 
 /*!
- * Currently-defined hook points. So long as these are unique,
- * the order in which they are declared is unimportant, but
- * currently matches the order in which they are referenced in
- * query.c.
+ * Currently-defined hook points. So long as these are unique, the order in
+ * which they are declared is unimportant, but it currently matches the
+ * order in which they are referenced in query.c.
  */
 typedef enum {
        /* hookpoints from query.c */
@@ -249,6 +412,39 @@ typedef ns_hooklist_t ns_hooktable_t[NS_HOOKPOINTS_COUNT];
  */
 LIBNS_EXTERNAL_DATA extern ns_hooktable_t *ns__hook_table;
 
+typedef void (*ns_hook_cancelasync_t)(ns_hookasync_t *);
+typedef void (*ns_hook_destroyasync_t)(ns_hookasync_t **);
+
+/*%
+ * Context for a hook-initiated asynchronous process. This works
+ * similarly to dns_fetch_t.
+ */
+struct ns_hookasync {
+       isc_mem_t *mctx;
+
+       /*
+        * The following two are equivalent to dns_resolver_cancelfetch and
+        * dns_resolver_destroyfetch, respectively, but specified as function
+        * pointers since they can be hook-specific.
+        */
+       ns_hook_cancelasync_t  cancel;
+       ns_hook_destroyasync_t destroy;
+
+       void *private; /* hook-specific data */
+};
+
+/*
+ * isc_event to be sent on the completion of a hook-initiated asyncronous
+ * process, similar to dns_fetchevent_t.
+ */
+typedef struct ns_hook_resevent {
+       ISC_EVENT_COMMON(struct ns_hook_resevent);
+       ns_hookasync_t *ctx;       /* asynchronous processing context */
+       ns_hookpoint_t  hookpoint; /* hook point from which to resume */
+       isc_result_t origresult; /* result code at the point of call to hook */
+       query_ctx_t *saved_qctx; /* qctx at the point of call to hook */
+} ns_hook_resevent_t;
+
 /*
  * Plugin API version
  *
index 40f1f30ba9a1b236937748b2220f7df718ffc285..011a8b4c131a6d7c1d3568f2964bc62164870e8f 100644 (file)
@@ -18,6 +18,7 @@
 
 #include <isc/buffer.h>
 #include <isc/netaddr.h>
+#include <isc/task.h>
 #include <isc/types.h>
 
 #include <dns/rdataset.h>
@@ -66,6 +67,7 @@ struct ns_query {
        isc_mutex_t      fetchlock;
        dns_fetch_t *    fetch;
        dns_fetch_t *    prefetch;
+       ns_hookasync_t * hookactx;
        dns_rpz_st_t *   rpz_st;
        isc_bufferlist_t namebufs;
        ISC_LIST(ns_dbversion_t) activeversions;
@@ -174,6 +176,15 @@ struct query_ctx {
        int          line;   /* line to report error */
 };
 
+typedef isc_result_t (*ns_query_starthookasync_t)(
+       query_ctx_t *qctx, isc_mem_t *mctx, void *arg, isc_task_t *task,
+       isc_taskaction_t action, void *evarg, ns_hookasync_t **ctxp);
+
+/*
+ * The following functions are expected to be used only within query.c
+ * and query modules.
+ */
+
 isc_result_t
 ns_query_done(query_ctx_t *qctx);
 /*%<
@@ -197,6 +208,34 @@ ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname,
  * recursion completes.
  */
 
+isc_result_t
+ns_query_hookasync(query_ctx_t *qctx, ns_query_starthookasync_t runasync,
+                  void *arg);
+/*%<
+ * Prepare the client for an asynchronous hook action, then call the
+ * specified 'runasync' function to start an asynchronous process running
+ * in the background.  This function works similarly to ns_query_recurse(),
+ * but is expected to be called from a query hook action to support
+ * asynchronous event handling in a hook. A typical use case would be for
+ * a plugin to initiate recursion, but it may also be used to carry out
+ * other time-consuming tasks without blocking the caller or the worker
+ * thread.
+ *
+ * The calling plugin action must pass 'qctx' as passed from the query
+ * module.
+ *
+ * Once a plugin action calls this function, the ownership of 'qctx' is
+ * essentially transferred to the query module. Regardless of the return
+ * value of this function, the hook must not use 'qctx' anymore.
+ *
+ * This function must not be called after ns_query_recurse() is called,
+ * until the fetch is completed, as it needs resources that
+ * ns_query_recurse() would also use.
+ *
+ * See hooks.h for details about how 'runasync' is supposed to work, and
+ * other aspects of hook-triggered asynchronous event handling.
+ */
+
 isc_result_t
 ns_query_init(ns_client_t *client);
 
index beaed2bca2225ad92e4b9dd0f8ab0d9ebe8e6f43..6af3436abcba77624ced08c277cd4655e192b8bd 100644 (file)
 #include <dns/acl.h>
 #include <dns/types.h>
 
+#include <ns/events.h>
 #include <ns/types.h>
 
-#define NS_EVENT_CLIENTCONTROL (ISC_EVENTCLASS_NS + 0)
-
 #define NS_SERVER_LOGQUERIES   0x00000001U /*%< log queries */
 #define NS_SERVER_NOAA        0x00000002U /*%< -T noaa */
 #define NS_SERVER_NOSOA               0x00000004U /*%< -T nosoa */
index 97c41d9e28837a2bfd0a1226537fb6e4fdbedb3d..f26d379a17947b27482c6ae3e66c9e2ce607a4fe 100644 (file)
@@ -25,6 +25,7 @@ typedef struct ns_interfacemgr ns_interfacemgr_t;
 typedef struct ns_query               ns_query_t;
 typedef struct ns_server       ns_server_t;
 typedef struct ns_stats               ns_stats_t;
+typedef struct ns_hookasync    ns_hookasync_t;
 
 typedef enum { ns_cookiealg_aes, ns_cookiealg_siphash24 } ns_cookiealg_t;
 
index 11e0ddc6b62c0374bcb73a344e4a8147fd9a291e..393ddaf8ce92c9340d677f0c27a958539ec32397 100644 (file)
@@ -60,6 +60,7 @@
 #include <dns/zt.h>
 
 #include <ns/client.h>
+#include <ns/events.h>
 #include <ns/hooks.h>
 #include <ns/interfacemgr.h>
 #include <ns/log.h>
@@ -182,14 +183,15 @@ client_trace(ns_client_t *client, int level, const char *message) {
 #define SFCACHE_CDFLAG 0x1
 
 /*
- * These have the same semantics as:
+ * SAVE and RESTORE have the same semantics as:
  *
- *     foo_attach(b, a);
+ *     foo_attach(b, &a);
  *     foo_detach(&b);
  *
  * without the locking and magic testing.
  *
- * We use SAVE and RESTORE as that shows the operation being performed.
+ * We use the names SAVE and RESTORE to show the operation being performed,
+ * even though the two macros are identical.
  */
 #define SAVE(a, b)                 \
        do {                       \
@@ -223,6 +225,12 @@ static void
 log_noexistnodata(void *val, int level, const char *fmt, ...)
        ISC_FORMAT_PRINTF(3, 4);
 
+static isc_result_t
+query_addanswer(query_ctx_t *qctx);
+
+static isc_result_t
+query_prepare_delegation_response(query_ctx_t *qctx);
+
 /*
  * Return the hooktable in use with 'qctx', or if there isn't one
  * set, return the default hooktable.
@@ -625,6 +633,10 @@ ns_query_cancel(ns_client_t *client) {
 
                client->query.fetch = NULL;
        }
+       if (client->query.hookactx != NULL) {
+               client->query.hookactx->cancel(client->query.hookactx);
+               client->query.hookactx = NULL;
+       }
        UNLOCK(&client->query.fetchlock);
 }
 
@@ -5159,6 +5171,52 @@ qctx_destroy(query_ctx_t *qctx) {
        dns_view_detach(&qctx->view);
 }
 
+/*
+ * Call SAVE but set 'a' to NULL first so as not to assert.
+ */
+#define INITANDSAVE(a, b)   \
+       do {                \
+               a = NULL;   \
+               SAVE(a, b); \
+       } while (0)
+
+/*
+ * "save" qctx data from 'src' to 'tgt'.
+ * It essentially moves ownership of the data from src to tgt, so the former
+ * becomes unusable except for final cleanup (such as by qctx_destroy).
+ * Note: this function doesn't attach to the client's handle.  It's the caller's
+ * responsibility to do it if it's necessary.
+ */
+static void
+qctx_save(query_ctx_t *src, query_ctx_t *tgt) {
+       /* First copy all fields in a straightforward way */
+       *tgt = *src;
+
+       /* Then "move" pointers (except client and view) */
+       INITANDSAVE(tgt->dbuf, src->dbuf);
+       INITANDSAVE(tgt->fname, src->fname);
+       INITANDSAVE(tgt->tname, src->tname);
+       INITANDSAVE(tgt->rdataset, src->rdataset);
+       INITANDSAVE(tgt->sigrdataset, src->sigrdataset);
+       INITANDSAVE(tgt->noqname, src->noqname);
+       INITANDSAVE(tgt->event, src->event);
+       INITANDSAVE(tgt->db, src->db);
+       INITANDSAVE(tgt->version, src->version);
+       INITANDSAVE(tgt->node, src->node);
+       INITANDSAVE(tgt->zdb, src->zdb);
+       INITANDSAVE(tgt->znode, src->znode);
+       INITANDSAVE(tgt->zfname, src->zfname);
+       INITANDSAVE(tgt->zversion, src->zversion);
+       INITANDSAVE(tgt->zrdataset, src->zrdataset);
+       INITANDSAVE(tgt->zsigrdataset, src->zsigrdataset);
+       INITANDSAVE(tgt->rpz_st, src->rpz_st);
+       INITANDSAVE(tgt->zone, src->zone);
+
+       /* View has to stay in 'src' for qctx_destroy. */
+       tgt->view = NULL;
+       dns_view_attach(src->view, &tgt->view);
+}
+
 /*%
  * Log detailed information about the query immediately after
  * the client request or a return from recursion.
@@ -6254,6 +6312,212 @@ cleanup:
        return (result);
 }
 
+static void
+query_hookresume(isc_task_t *task, isc_event_t *event) {
+       ns_hook_resevent_t *rev = (ns_hook_resevent_t *)event;
+       ns_hookasync_t *hctx = NULL;
+       ns_client_t *client = rev->ev_arg;
+       query_ctx_t *qctx = rev->saved_qctx;
+       bool canceled;
+
+       CTRACE(ISC_LOG_DEBUG(3), "query_hookresume");
+
+       REQUIRE(NS_CLIENT_VALID(client));
+       REQUIRE(task == client->task);
+       REQUIRE(event->ev_type == NS_EVENT_HOOKASYNCDONE);
+
+       LOCK(&client->query.fetchlock);
+       if (client->query.hookactx != NULL) {
+               INSIST(rev->ctx == client->query.hookactx);
+               client->query.hookactx = NULL;
+               canceled = false;
+               isc_stdtime_get(&client->now);
+       } else {
+               canceled = true;
+       }
+       UNLOCK(&client->query.fetchlock);
+       SAVE(hctx, rev->ctx);
+
+       if (client->recursionquota != NULL) {
+               isc_quota_detach(&client->recursionquota);
+               ns_stats_decrement(client->sctx->nsstats,
+                                  ns_statscounter_recursclients);
+       }
+
+       LOCK(&client->manager->reclock);
+       if (ISC_LINK_LINKED(client, rlink)) {
+               ISC_LIST_UNLINK(client->manager->recursing, client, rlink);
+       }
+       UNLOCK(&client->manager->reclock);
+       client->state = NS_CLIENTSTATE_WORKING;
+
+       if (canceled) {
+               /*
+                * Note: unlike fetch_callback, this function doesn't bother
+                * to check the 'shutdown' condition, as that doesn't seem to
+                * happen in the latest implementation.
+                */
+               query_error(client, DNS_R_SERVFAIL, __LINE__);
+
+               /*
+                * There's no other place to free/release any data maintained
+                * in qctx.  We need to do it here to prevent leak.
+                */
+               qctx_clean(qctx);
+               qctx_freedata(qctx);
+
+               /*
+                * As we're almost done with this client, make sure any internal
+                * resource for hooks will be released (if necessary) via the
+                * QCTX_DESTROYED hook.
+                */
+               qctx->detach_client = true;
+       } else {
+               switch (rev->hookpoint) {
+               case NS_QUERY_SETUP:
+                       (void)query_setup(client, qctx->qtype);
+                       break;
+               case NS_QUERY_START_BEGIN:
+                       (void)ns__query_start(qctx);
+                       break;
+               case NS_QUERY_LOOKUP_BEGIN:
+                       (void)query_lookup(qctx);
+                       break;
+               case NS_QUERY_RESUME_BEGIN:
+               case NS_QUERY_RESUME_RESTORED:
+                       (void)query_resume(qctx);
+                       break;
+               case NS_QUERY_GOT_ANSWER_BEGIN:
+                       (void)query_gotanswer(qctx, rev->origresult);
+                       break;
+               case NS_QUERY_RESPOND_ANY_BEGIN:
+                       (void)query_respond_any(qctx);
+                       break;
+               case NS_QUERY_ADDANSWER_BEGIN:
+                       (void)query_addanswer(qctx);
+                       break;
+               case NS_QUERY_NOTFOUND_BEGIN:
+                       (void)query_notfound(qctx);
+                       break;
+               case NS_QUERY_PREP_DELEGATION_BEGIN:
+                       (void)query_prepare_delegation_response(qctx);
+                       break;
+               case NS_QUERY_ZONE_DELEGATION_BEGIN:
+                       (void)query_zone_delegation(qctx);
+                       break;
+               case NS_QUERY_DELEGATION_BEGIN:
+                       (void)query_delegation(qctx);
+                       break;
+               case NS_QUERY_DELEGATION_RECURSE_BEGIN:
+                       (void)query_delegation_recurse(qctx);
+                       break;
+               case NS_QUERY_NODATA_BEGIN:
+                       (void)query_nodata(qctx, rev->origresult);
+                       break;
+               case NS_QUERY_NXDOMAIN_BEGIN:
+                       (void)query_nxdomain(qctx, rev->origresult);
+                       break;
+               case NS_QUERY_NCACHE_BEGIN:
+                       (void)query_ncache(qctx, rev->origresult);
+                       break;
+               case NS_QUERY_CNAME_BEGIN:
+                       (void)query_cname(qctx);
+                       break;
+               case NS_QUERY_DNAME_BEGIN:
+                       (void)query_dname(qctx);
+                       break;
+               case NS_QUERY_RESPOND_BEGIN:
+                       (void)query_respond(qctx);
+                       break;
+               case NS_QUERY_PREP_RESPONSE_BEGIN:
+                       (void)query_prepresponse(qctx);
+                       break;
+               case NS_QUERY_DONE_BEGIN:
+               case NS_QUERY_DONE_SEND:
+                       (void)ns_query_done(qctx);
+                       break;
+
+               /* Not all hookpoints can use recursion.  Catch violations */
+               case NS_QUERY_RESPOND_ANY_FOUND: /* due to side effect */
+               case NS_QUERY_NOTFOUND_RECURSE:  /* in recursion */
+               case NS_QUERY_ZEROTTL_RECURSE:   /* in recursion */
+               default:                         /* catch-all just in case */
+                       INSIST(false);
+               }
+       }
+
+       hctx->destroy(&hctx);
+       qctx_destroy(qctx);
+       isc_mem_put(client->mctx, qctx, sizeof(*qctx));
+       isc_event_free(&event);
+       isc_nmhandle_detach(&client->fetchhandle);
+}
+
+isc_result_t
+ns_query_hookasync(query_ctx_t *qctx, ns_query_starthookasync_t runasync,
+                  void *arg) {
+       isc_result_t result;
+       ns_client_t *client = qctx->client;
+       query_ctx_t *saved_qctx = NULL;
+
+       CTRACE(ISC_LOG_DEBUG(3), "ns_query_hookasync");
+
+       REQUIRE(NS_CLIENT_VALID(client));
+       REQUIRE(client->query.hookactx == NULL);
+       REQUIRE(client->query.fetch == NULL);
+
+       result = check_recursionquota(client);
+       if (result != ISC_R_SUCCESS) {
+               goto cleanup;
+       }
+
+       saved_qctx = isc_mem_get(client->mctx, sizeof(*saved_qctx));
+       qctx_save(qctx, saved_qctx);
+       result = runasync(saved_qctx, client->mctx, arg, client->task,
+                         query_hookresume, client, &client->query.hookactx);
+       if (result != ISC_R_SUCCESS) {
+               goto cleanup;
+       }
+
+       /*
+        * Typically the runasync() function will trigger recursion, but
+        * there is no need to set NS_QUERYATTR_RECURSING. The calling hook
+        * is expected to return NS_HOOK_RETURN, and the RECURSING
+        * attribute won't be checked anywhere.
+        *
+        * Hook-based asynchronous processing cannot coincide with normal
+        * recursion, so we can safely use fetchhandle here.  Unlike in
+        * ns_query_recurse(), we attach to the handle only if 'runasync'
+        * succeeds. It should be safe since we're either in the client
+        * task or pausing it.
+        */
+       isc_nmhandle_attach(client->handle, &client->fetchhandle);
+       return (ISC_R_SUCCESS);
+
+cleanup:
+       /*
+        * If we fail, send SERVFAIL now.  It may be better to let the caller
+        * decide what to do on failure of this function, but hooks don't have
+        * access to query_error().
+        */
+       query_error(client, DNS_R_SERVFAIL, __LINE__);
+
+       /*
+        * Free all resource related to the query and set detach_client,
+        * similar to the cancel case of query_hookresume; the callers will
+        * simply return on failure of this function, so there's no other
+        * place for this to prevent leak.
+        */
+       if (saved_qctx != NULL) {
+               qctx_clean(saved_qctx);
+               qctx_freedata(saved_qctx);
+               qctx_destroy(saved_qctx);
+               isc_mem_put(client->mctx, saved_qctx, sizeof(*saved_qctx));
+       }
+       qctx->detach_client = true;
+       return (result);
+}
+
 /*%
  * If the query is recursive, check the SERVFAIL cache to see whether
  * identical queries have failed recently.  If we find a match, and it was
index 20b746023cbc850bc9f1010c4dd0470327fd47eb..a779c9ee8751239189206d92cb11d232d5571ab1 100644 (file)
@@ -117,6 +117,7 @@ isc_nmhandle_detach(isc_nmhandle_t **handlep) {
                ns__client_reset_cb(client);
                ns__client_put_cb(client);
                isc_mem_put(mctx, client, sizeof(ns_client_t));
+               atomic_store(&client_addrs[i], (uintptr_t)NULL);
        }
 
        return;
index d068a8b081d97411626998a825a36d2c12603b07..0f6a598c5bea82756d7a7988c3e6bb3bfda74fbf 100644 (file)
 #define UNIT_TESTING
 #include <cmocka.h>
 
+#include <isc/quota.h>
+
 #include <dns/badcache.h>
 #include <dns/view.h>
+#include <dns/zone.h>
 
 #include <ns/client.h>
+#include <ns/events.h>
 #include <ns/hooks.h>
 #include <ns/query.h>
+#include <ns/server.h>
+#include <ns/stats.h>
 
 #include "nstest.h"
 
@@ -55,6 +61,12 @@ _teardown(void **state) {
        return (0);
 }
 
+/* can be used for client->sendcb to avoid disruption on sending a response */
+static void
+send_noop(isc_buffer_t *buffer) {
+       UNUSED(buffer);
+}
+
 /*****
 ***** ns__query_sfcache() tests
 *****/
@@ -599,6 +611,895 @@ ns__query_start_test(void **state) {
        }
 }
 
+/*****
+***** tests for ns_query_hookasync().
+*****/
+
+/*%
+ * Structure containing parameters for ns__query_hookasync_test().
+ */
+typedef struct {
+       const ns_test_id_t id;     /* libns test identifier */
+       ns_hookpoint_t hookpoint;  /* hook point specified for resume */
+       ns_hookpoint_t hookpoint2; /* expected hook point used after resume */
+       ns_hook_action_t action;   /* action for the hook point */
+       isc_result_t start_result; /* result of 'runasync' */
+       bool quota_ok;             /* true if recursion quota should be okay */
+       bool do_cancel;            /* true if query should be canceled
+                                   * in test */
+} ns__query_hookasync_test_params_t;
+
+/* Data structure passed from tests to hooks */
+typedef struct hookasync_data {
+       bool async;                   /* true if in a hook-triggered
+                                      * asynchronous process */
+       bool canceled;                /* true if the query has been canceled  */
+       isc_result_t start_result;    /* result of 'runasync' */
+       ns_hook_resevent_t *rev;      /* resume event sent on completion */
+       query_ctx_t qctx;             /* shallow copy of qctx passed to hook */
+       ns_hookpoint_t hookpoint;     /* specifies where to resume */
+       ns_hookpoint_t lasthookpoint; /* remember the last hook point called */
+} hookasync_data_t;
+
+/*
+ * 'destroy' callback of hook recursion ctx.
+ * The dynamically allocated context will be freed here, thereby proving
+ * this is actually called; otherwise tests would fail due to memory leak.
+ */
+static void
+destroy_hookactx(ns_hookasync_t **ctxp) {
+       ns_hookasync_t *ctx = *ctxp;
+
+       *ctxp = NULL;
+       isc_mem_putanddetach(&ctx->mctx, ctx, sizeof(*ctx));
+}
+
+/* 'cancel' callback of hook recursion ctx. */
+static void
+cancel_hookactx(ns_hookasync_t *ctx) {
+       /* Mark the hook data so the test can confirm this is called. */
+       ((hookasync_data_t *)ctx->private)->canceled = true;
+}
+
+/* 'runasync' callback passed to ns_query_hookasync */
+static isc_result_t
+test_hookasync(query_ctx_t *qctx, isc_mem_t *memctx, void *arg,
+              isc_task_t *task, isc_taskaction_t action, void *evarg,
+              ns_hookasync_t **ctxp) {
+       hookasync_data_t *asdata = arg;
+       ns_hookasync_t *ctx = NULL;
+       ns_hook_resevent_t *rev = NULL;
+
+       if (asdata->start_result != ISC_R_SUCCESS) {
+               return (asdata->start_result);
+       }
+
+       ctx = isc_mem_get(memctx, sizeof(*ctx));
+       rev = (ns_hook_resevent_t *)isc_event_allocate(
+               memctx, task, NS_EVENT_HOOKASYNCDONE, action, evarg,
+               sizeof(*rev));
+
+       rev->hookpoint = asdata->hookpoint;
+       rev->origresult = DNS_R_NXDOMAIN;
+       rev->saved_qctx = qctx;
+       rev->ctx = ctx;
+       asdata->rev = rev;
+
+       *ctx = (ns_hookasync_t){ .private = asdata };
+       isc_mem_attach(memctx, &ctx->mctx);
+       ctx->destroy = destroy_hookactx;
+       ctx->cancel = cancel_hookactx;
+
+       *ctxp = ctx;
+       return (ISC_R_SUCCESS);
+}
+
+/*
+ * Main logic for hook actions.
+ * 'hookpoint' should identify the point that calls the hook.  It will be
+ * remembered in the hook data, so that the test can confirm which hook point
+ * was last used.
+ */
+static ns_hookresult_t
+hook_recurse_common(void *arg, void *data, isc_result_t *resultp,
+                   ns_hookpoint_t hookpoint) {
+       query_ctx_t *qctx = arg;
+       hookasync_data_t *asdata = data;
+       isc_result_t result;
+
+       asdata->qctx = *qctx; /* remember passed ctx for inspection */
+       asdata->lasthookpoint = hookpoint; /* ditto */
+
+       if (!asdata->async) {
+               /* Initial call to the hook; start recursion */
+               result = ns_query_hookasync(qctx, test_hookasync, asdata);
+               if (result == ISC_R_SUCCESS) {
+                       asdata->async = true;
+               }
+       } else {
+               /* Resume from the completion of recursion */
+               asdata->async = false;
+               switch (hookpoint) {
+               case NS_QUERY_GOT_ANSWER_BEGIN:
+               case NS_QUERY_NODATA_BEGIN:
+               case NS_QUERY_NXDOMAIN_BEGIN:
+               case NS_QUERY_NCACHE_BEGIN:
+                       INSIST(*resultp == DNS_R_NXDOMAIN);
+                       break;
+               default:;
+               }
+       }
+
+       *resultp = ISC_R_UNSET;
+       return (NS_HOOK_RETURN);
+}
+
+static ns_hookresult_t
+hook_recurse_query_setup(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_SETUP));
+}
+
+static ns_hookresult_t
+hook_recurse_query_start_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_START_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_lookup_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_LOOKUP_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_resume_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_RESUME_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_got_answer_begin(void *arg, void *data,
+                                   isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_GOT_ANSWER_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_respond_any_begin(void *arg, void *data,
+                                    isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_RESPOND_ANY_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_addanswer_begin(void *arg, void *data,
+                                  isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_ADDANSWER_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_notfound_begin(void *arg, void *data,
+                                 isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_NOTFOUND_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_prep_delegation_begin(void *arg, void *data,
+                                        isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_PREP_DELEGATION_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_zone_delegation_begin(void *arg, void *data,
+                                        isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_ZONE_DELEGATION_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_delegation_begin(void *arg, void *data,
+                                   isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_DELEGATION_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_delegation_recurse_begin(void *arg, void *data,
+                                           isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_DELEGATION_RECURSE_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_nodata_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_NODATA_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_nxdomain_begin(void *arg, void *data,
+                                 isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_NXDOMAIN_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_ncache_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_NCACHE_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_cname_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_CNAME_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_dname_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_DNAME_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_respond_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_RESPOND_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_response_begin(void *arg, void *data,
+                                 isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp,
+                                   NS_QUERY_PREP_RESPONSE_BEGIN));
+}
+
+static ns_hookresult_t
+hook_recurse_query_done_begin(void *arg, void *data, isc_result_t *resultp) {
+       return (hook_recurse_common(arg, data, resultp, NS_QUERY_DONE_BEGIN));
+}
+
+/*
+ * hook on destroying actx.  Can't be used for recursion, but we use this
+ * to remember the qctx at that point.
+ */
+static ns_hookresult_t
+ns_test_qctx_destroy_hook(void *arg, void *data, isc_result_t *resultp) {
+       query_ctx_t *qctx = arg;
+       hookasync_data_t *asdata = data;
+
+       asdata->qctx = *qctx; /* remember passed ctx for inspection */
+       *resultp = ISC_R_UNSET;
+       return (NS_HOOK_CONTINUE);
+}
+
+static void
+run_hookasync_test(const ns__query_hookasync_test_params_t *test) {
+       query_ctx_t *qctx = NULL;
+       isc_result_t result;
+       hookasync_data_t asdata = {
+               .async = false,
+               .canceled = false,
+               .start_result = test->start_result,
+               .hookpoint = test->hookpoint,
+       };
+       const ns_hook_t testhook = {
+               .action = test->action,
+               .action_data = &asdata,
+       };
+       const ns_hook_t destroyhook = {
+               .action = ns_test_qctx_destroy_hook,
+               .action_data = &asdata,
+       };
+       isc_quota_t *quota = NULL;
+       isc_statscounter_t srvfail_cnt;
+       bool expect_servfail = false;
+
+       /*
+        * Prepare hooks.  We always begin with ns__query_start for simplicity.
+        * Its action will specify various different resume points (unusual
+        * in practice, but that's fine for the testing purpose).
+        */
+       ns__hook_table = NULL;
+       ns_hooktable_create(mctx, &ns__hook_table);
+       ns_hook_add(ns__hook_table, mctx, NS_QUERY_START_BEGIN, &testhook);
+       if (test->hookpoint2 != NS_QUERY_START_BEGIN) {
+               /*
+                * unless testing START_BEGIN itself, specify the hook for the
+                * expected resume point, too.
+                */
+               ns_hook_add(ns__hook_table, mctx, test->hookpoint2, &testhook);
+       }
+       ns_hook_add(ns__hook_table, mctx, NS_QUERY_QCTX_DESTROYED,
+                   &destroyhook);
+
+       {
+               const ns_test_qctx_create_params_t qctx_params = {
+                       .qname = "test.example.com",
+                       .qtype = dns_rdatatype_aaaa,
+               };
+               result = ns_test_qctx_create(&qctx_params, &qctx);
+               INSIST(result == ISC_R_SUCCESS);
+               qctx->client->sendcb = send_noop;
+       }
+
+       /*
+        * Set recursion quota to the lowest possible value, then make it full
+        * if we want to exercise a quota failure case.
+        */
+       isc_quota_max(&sctx->recursionquota, 1);
+       if (!test->quota_ok) {
+               result = isc_quota_attach(&sctx->recursionquota, &quota);
+               INSIST(result == ISC_R_SUCCESS);
+       }
+
+       /* Remember SERVFAIL counter */
+       srvfail_cnt = ns_stats_get_counter(qctx->client->sctx->nsstats,
+                                          ns_statscounter_servfail);
+
+       /*
+        * If the query has been canceled, or recursion didn't succeed,
+        * SERVFAIL will have to be sent.  In this case we need to have
+        * 'reqhandle' attach to the client's handle as it's detached in
+        * query_error.
+        */
+       if (test->start_result != ISC_R_SUCCESS || !test->quota_ok ||
+           test->do_cancel) {
+               expect_servfail = true;
+               isc_nmhandle_attach(qctx->client->handle,
+                                   &qctx->client->reqhandle);
+       }
+
+       /*
+        * Emulate query handling from query_start.
+        * Specified hook should be called.
+        */
+       qctx->client->state = NS_CLIENTSTATE_WORKING;
+       result = ns__query_start(qctx);
+       INSIST(result == ISC_R_UNSET);
+
+       /*
+        * hook-triggered recursion should be happening unless it hits recursion
+        * quota limit or 'runasync' callback fails.
+        */
+       INSIST(asdata.async ==
+              (test->quota_ok && test->start_result == ISC_R_SUCCESS));
+
+       /*
+        * Emulate cancel if so specified.
+        * The cancel callback should be called.
+        */
+       if (test->do_cancel) {
+               ns_query_cancel(qctx->client);
+       }
+       INSIST(asdata.canceled == test->do_cancel);
+
+       /* If recursion has started, manually invoke the 'done' event. */
+       if (asdata.async) {
+               qctx->client->now = 0; /* set to sentinel before resume */
+               asdata.rev->ev_action(asdata.rev->ev_sender,
+                                     (isc_event_t *)asdata.rev);
+
+               /* Confirm necessary cleanup has been performed. */
+               INSIST(qctx->client->query.hookactx == NULL);
+               INSIST(qctx->client->state == NS_CLIENTSTATE_WORKING);
+               INSIST(qctx->client->recursionquota == NULL);
+               INSIST(ns_stats_get_counter(qctx->client->sctx->nsstats,
+                                           ns_statscounter_recursclients) ==
+                      0);
+               INSIST(!ISC_LINK_LINKED(qctx->client, rlink));
+               if (!test->do_cancel) {
+                       /*
+                        * In the normal case the client's timestamp is updated
+                        * and the query handling has been resumed from the
+                        * expected point.
+                        */
+                       INSIST(qctx->client->now != 0);
+                       INSIST(asdata.lasthookpoint == test->hookpoint2);
+               }
+       } else {
+               INSIST(qctx->client->query.hookactx == NULL);
+       }
+
+       /*
+        * Confirm SERVFAIL has been sent if it was expected.
+        * Also, the last-generated qctx should have detach_client being true.
+        */
+       if (expect_servfail) {
+               INSIST(ns_stats_get_counter(qctx->client->sctx->nsstats,
+                                           ns_statscounter_servfail) ==
+                      srvfail_cnt + 1);
+               if (test->do_cancel) {
+                       /* qctx was created on resume and copied in hook */
+                       INSIST(asdata.qctx.detach_client);
+               } else {
+                       INSIST(qctx->detach_client);
+               }
+       }
+
+       /*
+        * Cleanup.  Note that we've kept 'qctx' until now; otherwise
+        * qctx->client may have been invalidated while we still need it.
+        */
+       ns_test_qctx_destroy(&qctx);
+       ns_hooktable_free(mctx, (void **)&ns__hook_table);
+       if (quota != NULL) {
+               isc_quota_detach(&quota);
+       }
+}
+
+static void
+ns__query_hookasync_test(void **state) {
+       size_t i;
+
+       UNUSED(state);
+
+       const ns__query_hookasync_test_params_t tests[] = {
+               {
+                       NS_TEST_ID("normal case"),
+                       NS_QUERY_START_BEGIN,
+                       NS_QUERY_START_BEGIN,
+                       hook_recurse_query_start_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("quota fail"),
+                       NS_QUERY_START_BEGIN,
+                       NS_QUERY_START_BEGIN,
+                       hook_recurse_query_start_begin,
+                       ISC_R_SUCCESS,
+                       false,
+                       false,
+               },
+               {
+                       NS_TEST_ID("start fail"),
+                       NS_QUERY_START_BEGIN,
+                       NS_QUERY_START_BEGIN,
+                       hook_recurse_query_start_begin,
+                       ISC_R_FAILURE,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("query cancel"),
+                       NS_QUERY_START_BEGIN,
+                       NS_QUERY_START_BEGIN,
+                       hook_recurse_query_start_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       true,
+               },
+               /*
+                * The rest of the test case just confirms supported hookpoints
+                * with the same test logic.
+                */
+               {
+                       NS_TEST_ID("recurse from setup"),
+                       NS_QUERY_SETUP,
+                       NS_QUERY_SETUP,
+                       hook_recurse_query_setup,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from lookup"),
+                       NS_QUERY_LOOKUP_BEGIN,
+                       NS_QUERY_LOOKUP_BEGIN,
+                       hook_recurse_query_lookup_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from resume"),
+                       NS_QUERY_RESUME_BEGIN,
+                       NS_QUERY_RESUME_BEGIN,
+                       hook_recurse_query_resume_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from resume restored"),
+                       NS_QUERY_RESUME_RESTORED,
+                       NS_QUERY_RESUME_BEGIN,
+                       hook_recurse_query_resume_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from gotanswer"),
+                       NS_QUERY_GOT_ANSWER_BEGIN,
+                       NS_QUERY_GOT_ANSWER_BEGIN,
+                       hook_recurse_query_got_answer_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from respond any"),
+                       NS_QUERY_RESPOND_ANY_BEGIN,
+                       NS_QUERY_RESPOND_ANY_BEGIN,
+                       hook_recurse_query_respond_any_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from add answer"),
+                       NS_QUERY_ADDANSWER_BEGIN,
+                       NS_QUERY_ADDANSWER_BEGIN,
+                       hook_recurse_query_addanswer_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from notfound"),
+                       NS_QUERY_NOTFOUND_BEGIN,
+                       NS_QUERY_NOTFOUND_BEGIN,
+                       hook_recurse_query_notfound_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from prep delegation"),
+                       NS_QUERY_PREP_DELEGATION_BEGIN,
+                       NS_QUERY_PREP_DELEGATION_BEGIN,
+                       hook_recurse_query_prep_delegation_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from zone delegation"),
+                       NS_QUERY_ZONE_DELEGATION_BEGIN,
+                       NS_QUERY_ZONE_DELEGATION_BEGIN,
+                       hook_recurse_query_zone_delegation_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from delegation"),
+                       NS_QUERY_DELEGATION_BEGIN,
+                       NS_QUERY_DELEGATION_BEGIN,
+                       hook_recurse_query_delegation_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from recurse delegation"),
+                       NS_QUERY_DELEGATION_RECURSE_BEGIN,
+                       NS_QUERY_DELEGATION_RECURSE_BEGIN,
+                       hook_recurse_query_delegation_recurse_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from nodata"),
+                       NS_QUERY_NODATA_BEGIN,
+                       NS_QUERY_NODATA_BEGIN,
+                       hook_recurse_query_nodata_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from nxdomain"),
+                       NS_QUERY_NXDOMAIN_BEGIN,
+                       NS_QUERY_NXDOMAIN_BEGIN,
+                       hook_recurse_query_nxdomain_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from ncache"),
+                       NS_QUERY_NCACHE_BEGIN,
+                       NS_QUERY_NCACHE_BEGIN,
+                       hook_recurse_query_ncache_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from CNAME"),
+                       NS_QUERY_CNAME_BEGIN,
+                       NS_QUERY_CNAME_BEGIN,
+                       hook_recurse_query_cname_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from DNAME"),
+                       NS_QUERY_DNAME_BEGIN,
+                       NS_QUERY_DNAME_BEGIN,
+                       hook_recurse_query_dname_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from prep response"),
+                       NS_QUERY_PREP_RESPONSE_BEGIN,
+                       NS_QUERY_PREP_RESPONSE_BEGIN,
+                       hook_recurse_query_response_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from respond"),
+                       NS_QUERY_RESPOND_BEGIN,
+                       NS_QUERY_RESPOND_BEGIN,
+                       hook_recurse_query_respond_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from done begin"),
+                       NS_QUERY_DONE_BEGIN,
+                       NS_QUERY_DONE_BEGIN,
+                       hook_recurse_query_done_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+               {
+                       NS_TEST_ID("recurse from done send"),
+                       NS_QUERY_DONE_SEND,
+                       NS_QUERY_DONE_BEGIN,
+                       hook_recurse_query_done_begin,
+                       ISC_R_SUCCESS,
+                       true,
+                       false,
+               },
+       };
+
+       for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
+               run_hookasync_test(&tests[i]);
+       }
+}
+
+/*****
+***** tests for higher level ("e2e") behavior of ns_query_hookasync().
+***** It exercises overall behavior for some selected cases, while
+***** ns__query_hookasync_test exercises implementation details for a
+***** simple scenario and for all supported hook points.
+*****/
+
+/*%
+ * Structure containing parameters for ns__query_hookasync_e2e_test().
+ */
+typedef struct {
+       const ns_test_id_t id;     /* libns test identifier */
+       const char *qname;         /* QNAME */
+       ns_hookpoint_t hookpoint;  /* hook point specified for resume */
+       isc_result_t start_result; /* result of 'runasync' */
+       bool do_cancel;            /* true if query should be canceled
+                                   * in test */
+       dns_rcode_t expected_rcode;
+} ns__query_hookasync_e2e_test_params_t;
+
+/* data structure passed from tests to hooks */
+typedef struct hookasync_e2e_data {
+       bool async;                /* true if in a hook-triggered
+                                   * asynchronous process */
+       ns_hook_resevent_t *rev;   /* resume event sent on completion */
+       ns_hookpoint_t hookpoint;  /* specifies where to resume */
+       isc_result_t start_result; /* result of 'runasync' */
+       dns_rcode_t expected_rcode;
+       bool done; /* if SEND_DONE hook is called */
+} hookasync_e2e_data_t;
+
+/* Cancel callback.  Just need to be defined, it doesn't have to do anything. */
+static void
+cancel_e2ehookactx(ns_hookasync_t *ctx) {
+       UNUSED(ctx);
+}
+
+/* 'runasync' callback passed to ns_query_hookasync */
+static isc_result_t
+test_hookasync_e2e(query_ctx_t *qctx, isc_mem_t *memctx, void *arg,
+                  isc_task_t *task, isc_taskaction_t action, void *evarg,
+                  ns_hookasync_t **ctxp) {
+       ns_hookasync_t *ctx = NULL;
+       ns_hook_resevent_t *rev = NULL;
+       hookasync_e2e_data_t *asdata = arg;
+
+       if (asdata->start_result != ISC_R_SUCCESS) {
+               return (asdata->start_result);
+       }
+
+       ctx = isc_mem_get(memctx, sizeof(*ctx));
+       rev = (ns_hook_resevent_t *)isc_event_allocate(
+               memctx, task, NS_EVENT_HOOKASYNCDONE, action, evarg,
+               sizeof(*rev));
+
+       rev->hookpoint = asdata->hookpoint;
+       rev->saved_qctx = qctx;
+       rev->ctx = ctx;
+       asdata->rev = rev;
+
+       *ctx = (ns_hookasync_t){ .private = asdata };
+       isc_mem_attach(memctx, &ctx->mctx);
+       ctx->destroy = destroy_hookactx;
+       ctx->cancel = cancel_e2ehookactx;
+
+       *ctxp = ctx;
+       return (ISC_R_SUCCESS);
+}
+
+static ns_hookresult_t
+hook_recurse_e2e(void *arg, void *data, isc_result_t *resultp) {
+       query_ctx_t *qctx = arg;
+       hookasync_e2e_data_t *asdata = data;
+       isc_result_t result;
+
+       if (!asdata->async) {
+               /* Initial call to the hook; start recursion */
+               result = ns_query_hookasync(qctx, test_hookasync_e2e, asdata);
+               if (result != ISC_R_SUCCESS) {
+                       *resultp = result;
+                       return (NS_HOOK_RETURN);
+               }
+
+               asdata->async = true;
+               asdata->rev->origresult = *resultp; /* save it for resume */
+               *resultp = ISC_R_UNSET;
+               return (NS_HOOK_RETURN);
+       } else {
+               /* Resume from the completion of recursion */
+               asdata->async = false;
+               /* Don't touch 'resultp' */
+               return (NS_HOOK_CONTINUE);
+       }
+}
+
+/*
+ * Check whether the final response has expected the RCODE according to
+ * the test scenario.
+ */
+static ns_hookresult_t
+hook_donesend(void *arg, void *data, isc_result_t *resultp) {
+       query_ctx_t *qctx = arg;
+       hookasync_e2e_data_t *asdata = data;
+
+       INSIST(qctx->client->message->rcode == asdata->expected_rcode);
+       asdata->done = true; /* Let the test know this hook is called */
+       *resultp = ISC_R_UNSET;
+       return (NS_HOOK_CONTINUE);
+}
+
+static void
+run_hookasync_e2e_test(const ns__query_hookasync_e2e_test_params_t *test) {
+       query_ctx_t *qctx = NULL;
+       isc_result_t result;
+       hookasync_e2e_data_t asdata = {
+               .async = false,
+               .hookpoint = test->hookpoint,
+               .start_result = test->start_result,
+               .expected_rcode = test->expected_rcode,
+               .done = false,
+       };
+       const ns_hook_t donesend_hook = {
+               .action = hook_donesend,
+               .action_data = &asdata,
+       };
+       const ns_hook_t hook = {
+               .action = hook_recurse_e2e,
+               .action_data = &asdata,
+       };
+       const ns_test_qctx_create_params_t qctx_params = {
+               .qname = test->qname,
+               .qtype = dns_rdatatype_a,
+               .with_cache = true,
+       };
+
+       ns__hook_table = NULL;
+       ns_hooktable_create(mctx, &ns__hook_table);
+       ns_hook_add(ns__hook_table, mctx, test->hookpoint, &hook);
+       ns_hook_add(ns__hook_table, mctx, NS_QUERY_DONE_SEND, &donesend_hook);
+
+       result = ns_test_qctx_create(&qctx_params, &qctx);
+       INSIST(result == ISC_R_SUCCESS);
+
+       isc_sockaddr_any(&qctx->client->peeraddr); /* for sortlist */
+       qctx->client->sendcb = send_noop;
+
+       /* Load a zone.  it should have ns.foo/A */
+       result = ns_test_serve_zone("foo", "testdata/query/foo.db",
+                                   qctx->client->view);
+       INSIST(result == ISC_R_SUCCESS);
+
+       /*
+        * We expect to have a response sent all cases, so we need to
+        * setup reqhandle (which will be detached on the send).
+        */
+       isc_nmhandle_attach(qctx->client->handle, &qctx->client->reqhandle);
+
+       /* Handle the query.  hook-based recursion will be triggered. */
+       qctx->client->state = NS_CLIENTSTATE_WORKING;
+       ns__query_start(qctx);
+
+       /* If specified cancel the query at this point. */
+       if (test->do_cancel) {
+               ns_query_cancel(qctx->client);
+       }
+
+       if (test->start_result == ISC_R_SUCCESS) {
+               /* If recursion has started, manually invoke the done event. */
+               INSIST(asdata.async);
+               asdata.rev->ev_action(asdata.rev->ev_sender,
+                                     (isc_event_t *)asdata.rev);
+
+               /*
+                * Usually 'async' is reset to false on the 2nd call to
+                * the hook.  But the hook isn't called if the query is
+                * canceled.
+                */
+               INSIST(asdata.done == !test->do_cancel);
+               INSIST(asdata.async == test->do_cancel);
+       } else {
+               INSIST(!asdata.async);
+       }
+
+       /* Cleanup */
+       ns_test_qctx_destroy(&qctx);
+       ns_test_cleanup_zone();
+       ns_hooktable_free(mctx, (void **)&ns__hook_table);
+}
+
+static void
+ns__query_hookasync_e2e_test(void **state) {
+       UNUSED(state);
+
+       const ns__query_hookasync_e2e_test_params_t tests[] = {
+               {
+                       NS_TEST_ID("positive answer"),
+                       "ns.foo",
+                       NS_QUERY_GOT_ANSWER_BEGIN,
+                       ISC_R_SUCCESS,
+                       false,
+                       dns_rcode_noerror,
+               },
+               {
+                       NS_TEST_ID("NXDOMAIN"),
+                       "notexist.foo",
+                       NS_QUERY_NXDOMAIN_BEGIN,
+                       ISC_R_SUCCESS,
+                       false,
+                       dns_rcode_nxdomain,
+               },
+               {
+                       NS_TEST_ID("recurse fail"),
+                       "ns.foo",
+                       NS_QUERY_DONE_BEGIN,
+                       ISC_R_FAILURE,
+                       false,
+                       -1,
+               },
+               {
+                       NS_TEST_ID("cancel query"),
+                       "ns.foo",
+                       NS_QUERY_DONE_BEGIN,
+                       ISC_R_SUCCESS,
+                       true,
+                       -1,
+               },
+       };
+
+       for (size_t i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) {
+               run_hookasync_e2e_test(&tests[i]);
+       }
+}
+
 int
 main(void) {
        const struct CMUnitTest tests[] = {
@@ -606,6 +1507,10 @@ main(void) {
                                                _teardown),
                cmocka_unit_test_setup_teardown(ns__query_start_test, _setup,
                                                _teardown),
+               cmocka_unit_test_setup_teardown(ns__query_hookasync_test,
+                                               _setup, _teardown),
+               cmocka_unit_test_setup_teardown(ns__query_hookasync_e2e_test,
+                                               _setup, _teardown),
        };
 
        return (cmocka_run_group_tests(tests, NULL, NULL));
index 1680e4982d68a81351cb5efd4efe372bfdc71da9..66d706af4fe19eaf19fe1e97d48ce0f410177f21 100644 (file)
@@ -82,6 +82,7 @@ ns_plugins_free
 ns_query_cancel
 ns_query_done
 ns_query_free
+ns_query_hookasync
 ns_query_init
 ns_query_recurse
 ns_query_start
index 5a50c6523f0f3eabe83ecafacdc5e37182339e3a..dd95e6bb6c0fb4572b19d134281e7f141bdf7dfa 100644 (file)
 ./lib/ns/client.c                              C       2017,2018,2019,2020
 ./lib/ns/hooks.c                               C       2018,2019,2020
 ./lib/ns/include/ns/client.h                   C       2017,2018,2019,2020
+./lib/ns/include/ns/events.h                   C       2020
 ./lib/ns/include/ns/hooks.h                    C       2017,2018,2019,2020
 ./lib/ns/include/ns/interfacemgr.h             C       2017,2018,2019,2020
 ./lib/ns/include/ns/lib.h                      C       2017,2018,2019,2020