#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 */
*/
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
*
#include <dns/zt.h>
#include <ns/client.h>
+#include <ns/events.h>
#include <ns/hooks.h>
#include <ns/interfacemgr.h>
#include <ns/log.h>
#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 { \
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.
client->query.fetch = NULL;
}
+ if (client->query.hookactx != NULL) {
+ client->query.hookactx->cancel(client->query.hookactx);
+ client->query.hookactx = NULL;
+ }
UNLOCK(&client->query.fetchlock);
}
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.
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
#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"
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
*****/
}
}
+/*****
+***** 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, "a);
+ 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("a);
+ }
+}
+
+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[] = {
_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));