From: JINMEI Tatuya Date: Wed, 16 Sep 2020 22:26:22 +0000 (-0700) Subject: implementation of hook-based asynchronous functionality X-Git-Tag: v9.17.8~29^2~2 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=75cdd758ed010c61fce7512f5513ab6dd422e97d;p=thirdparty%2Fbind9.git implementation of hook-based asynchronous functionality 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. --- diff --git a/lib/ns/Makefile.am b/lib/ns/Makefile.am index d430493aef1..243d087c5c8 100644 --- a/lib/ns/Makefile.am +++ b/lib/ns/Makefile.am @@ -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 index 00000000000..8d3736cfeb0 --- /dev/null +++ b/lib/ns/include/ns/events.h @@ -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 + +/*! \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 */ diff --git a/lib/ns/include/ns/hooks.h b/lib/ns/include/ns/hooks.h index e8568f380d4..a0e8b363e4b 100644 --- a/lib/ns/include/ns/hooks.h +++ b/lib/ns/include/ns/hooks.h @@ -16,9 +16,12 @@ #include +#include #include #include +#include #include +#include #include @@ -178,13 +181,173 @@ * 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 * diff --git a/lib/ns/include/ns/query.h b/lib/ns/include/ns/query.h index 40f1f30ba9a..011a8b4c131 100644 --- a/lib/ns/include/ns/query.h +++ b/lib/ns/include/ns/query.h @@ -18,6 +18,7 @@ #include #include +#include #include #include @@ -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); diff --git a/lib/ns/include/ns/server.h b/lib/ns/include/ns/server.h index beaed2bca22..6af3436abcb 100644 --- a/lib/ns/include/ns/server.h +++ b/lib/ns/include/ns/server.h @@ -28,10 +28,9 @@ #include #include +#include #include -#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 */ diff --git a/lib/ns/include/ns/types.h b/lib/ns/include/ns/types.h index 97c41d9e288..f26d379a179 100644 --- a/lib/ns/include/ns/types.h +++ b/lib/ns/include/ns/types.h @@ -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; diff --git a/lib/ns/query.c b/lib/ns/query.c index 11e0ddc6b62..393ddaf8ce9 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -60,6 +60,7 @@ #include #include +#include #include #include #include @@ -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 diff --git a/lib/ns/tests/nstest.c b/lib/ns/tests/nstest.c index 20b746023cb..a779c9ee875 100644 --- a/lib/ns/tests/nstest.c +++ b/lib/ns/tests/nstest.c @@ -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; diff --git a/lib/ns/tests/query_test.c b/lib/ns/tests/query_test.c index d068a8b081d..0f6a598c5be 100644 --- a/lib/ns/tests/query_test.c +++ b/lib/ns/tests/query_test.c @@ -25,12 +25,18 @@ #define UNIT_TESTING #include +#include + #include #include +#include #include +#include #include #include +#include +#include #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, "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[] = { @@ -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)); diff --git a/lib/ns/win32/libns.def b/lib/ns/win32/libns.def index 1680e4982d6..66d706af4fe 100644 --- a/lib/ns/win32/libns.def +++ b/lib/ns/win32/libns.def @@ -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 diff --git a/util/copyrights b/util/copyrights index 5a50c6523f0..dd95e6bb6c0 100644 --- a/util/copyrights +++ b/util/copyrights @@ -2095,6 +2095,7 @@ ./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