From 9c8dae041daaf3d4a9c40f52185839abe5bead0b Mon Sep 17 00:00:00 2001 From: JINMEI Tatuya Date: Wed, 16 Sep 2020 13:19:03 -0700 Subject: [PATCH 1/4] ns_query refactoring for hook-based recursion several small changes to query processing to make it easier to use hook-based recursion (and other asynchronous functionlity) later. - recursion quota check is now a separate function, check_recursionquota(), which is called by ns_query_recurse(). - pass isc_result to query_nxdomain() instead of bool. the value of 'empty_wild' will be determined in the function based on the passed result. this is similar to query_nodata(), and makes the signatures of the two functions more consistent. - pass the current 'result' value into plugin hooks. --- lib/ns/query.c | 115 +++++++++++++++++++++++++++---------------------- 1 file changed, 64 insertions(+), 51 deletions(-) diff --git a/lib/ns/query.c b/lib/ns/query.c index e329b55e4c..11e0ddc6b6 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -185,7 +185,7 @@ client_trace(ns_client_t *client, int level, const char *message) { * These have the same semantics as: * * foo_attach(b, a); - * foo_detach(&a); + * foo_detach(&b); * * without the locking and magic testing. * @@ -249,7 +249,7 @@ get_hooktab(query_ctx_t *qctx) { */ #define CALL_HOOK(_id, _qctx) \ do { \ - isc_result_t _res; \ + isc_result_t _res = result; \ ns_hooktable_t *_tab = get_hooktab(_qctx); \ ns_hook_t *_hook; \ _hook = ISC_LIST_HEAD((*_tab)[_id]); \ @@ -436,7 +436,7 @@ static void query_addnxrrsetnsec(query_ctx_t *qctx); static isc_result_t -query_nxdomain(query_ctx_t *qctx, bool empty_wild); +query_nxdomain(query_ctx_t *qctx, isc_result_t res); static isc_result_t query_redirect(query_ctx_t *qctx); @@ -5203,7 +5203,7 @@ query_trace(query_ctx_t *qctx) { */ static isc_result_t query_setup(ns_client_t *client, dns_rdatatype_t qtype) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; query_ctx_t qctx; qctx_init(client, NULL, qtype, &qctx); @@ -5306,7 +5306,7 @@ root_key_sentinel_detect(query_ctx_t *qctx) { */ isc_result_t ns__query_start(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CCTRACE(ISC_LOG_DEBUG(3), "ns__query_start"); qctx->want_restart = false; qctx->authoritative = false; @@ -5518,7 +5518,7 @@ cleanup: static isc_result_t query_lookup(query_ctx_t *qctx) { isc_buffer_t b; - isc_result_t result; + isc_result_t result = ISC_R_UNSET; dns_clientinfomethods_t cm; dns_clientinfo_t ci; dns_name_t *rpzqname = NULL; @@ -5873,31 +5873,12 @@ last_init(void) { } #endif /* ifdef ISC_MUTEX_ATOMICS */ -isc_result_t -ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, - dns_name_t *qdomain, dns_rdataset_t *nameservers, - bool resuming) { - isc_result_t result; - dns_rdataset_t *rdataset, *sigrdataset; - isc_sockaddr_t *peeraddr = NULL; - - CTRACE(ISC_LOG_DEBUG(3), "ns_query_recurse"); - - /* - * Check recursion parameters from the previous query to see if they - * match. If not, update recursion parameters and proceed. - */ - if (recparam_match(&client->query.recparam, qtype, qname, qdomain)) { - ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, - ISC_LOG_INFO, "recursion loop detected"); - return (ISC_R_FAILURE); - } - - recparam_update(&client->query.recparam, qtype, qname, qdomain); - - if (!resuming) { - inc_stats(client, ns_statscounter_recursion); - } +/*% + * Check recursion quota before making the current client "recursing". + */ +static isc_result_t +check_recursionquota(ns_client_t *client) { + isc_result_t result = ISC_R_SUCCESS; /* * We are about to recurse, which means that this client will @@ -5984,6 +5965,40 @@ ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, ns_statscounter_recursclients); } + return (result); +} + +isc_result_t +ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, + dns_name_t *qdomain, dns_rdataset_t *nameservers, + bool resuming) { + isc_result_t result; + dns_rdataset_t *rdataset, *sigrdataset; + isc_sockaddr_t *peeraddr = NULL; + + CTRACE(ISC_LOG_DEBUG(3), "ns_query_recurse"); + + /* + * Check recursion parameters from the previous query to see if they + * match. If not, update recursion parameters and proceed. + */ + if (recparam_match(&client->query.recparam, qtype, qname, qdomain)) { + ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, + ISC_LOG_INFO, "recursion loop detected"); + return (ISC_R_FAILURE); + } + + recparam_update(&client->query.recparam, qtype, qname, qdomain); + + if (!resuming) { + inc_stats(client, ns_statscounter_recursion); + } + + result = check_recursionquota(client); + if (result != ISC_R_SUCCESS) { + return (result); + } + /* * Invoke the resolver. */ @@ -6042,7 +6057,7 @@ ns_query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, */ static isc_result_t query_resume(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; dns_name_t *tname; isc_buffer_t b; #ifdef WANT_QUERYTRACE @@ -6947,15 +6962,12 @@ query_gotanswer(query_ctx_t *qctx, isc_result_t res) { return (query_delegation(qctx)); case DNS_R_EMPTYNAME: - return (query_nodata(qctx, DNS_R_EMPTYNAME)); case DNS_R_NXRRSET: - return (query_nodata(qctx, DNS_R_NXRRSET)); + return (query_nodata(qctx, result)); case DNS_R_EMPTYWILD: - return (query_nxdomain(qctx, true)); - case DNS_R_NXDOMAIN: - return (query_nxdomain(qctx, false)); + return (query_nxdomain(qctx, result)); case DNS_R_COVERINGNSEC: return (query_coveringnsec(qctx)); @@ -7090,7 +7102,7 @@ static isc_result_t query_respond_any(query_ctx_t *qctx) { bool found = false, hidden = false; dns_rdatasetiter_t *rdsiter = NULL; - isc_result_t result; + isc_result_t result = ISC_R_UNSET; dns_rdatatype_t onetype = 0; /* type to use for minimal-any */ isc_buffer_t b; @@ -7365,7 +7377,7 @@ query_getexpire(query_ctx_t *qctx) { static isc_result_t query_addanswer(query_ctx_t *qctx) { dns_rdataset_t **sigrdatasetp = NULL; - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CCTRACE(ISC_LOG_DEBUG(3), "query_addanswer"); @@ -7427,7 +7439,7 @@ cleanup: */ static isc_result_t query_respond(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CCTRACE(ISC_LOG_DEBUG(3), "query_respond"); @@ -7884,7 +7896,7 @@ cleanup: */ static isc_result_t query_notfound(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CCTRACE(ISC_LOG_DEBUG(3), "query_notfound"); @@ -7970,7 +7982,7 @@ cleanup: */ static isc_result_t query_prepare_delegation_response(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; dns_rdataset_t **sigrdatasetp = NULL; bool detach = false; @@ -8026,7 +8038,7 @@ cleanup: */ static isc_result_t query_zone_delegation(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CALL_HOOK(NS_QUERY_ZONE_DELEGATION_BEGIN, qctx); @@ -8124,7 +8136,7 @@ cleanup: */ static isc_result_t query_delegation(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CCTRACE(ISC_LOG_DEBUG(3), "query_delegation"); @@ -8198,7 +8210,7 @@ cleanup: */ static isc_result_t query_delegation_recurse(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; dns_name_t *qname = qctx->client->query.qname; CCTRACE(ISC_LOG_DEBUG(3), "query_delegation_recurse"); @@ -8721,10 +8733,11 @@ query_addnxrrsetnsec(query_ctx_t *qctx) { * Handle NXDOMAIN and empty wildcard responses. */ static isc_result_t -query_nxdomain(query_ctx_t *qctx, bool empty_wild) { +query_nxdomain(query_ctx_t *qctx, isc_result_t res) { dns_section_t section; uint32_t ttl; - isc_result_t result; + isc_result_t result = res; + bool empty_wild = (res == DNS_R_EMPTYWILD); CCTRACE(ISC_LOG_DEBUG(3), "query_nxdomain"); @@ -9656,7 +9669,7 @@ cleanup: */ static isc_result_t query_cname(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; dns_name_t *tname; dns_rdataset_t *trdataset; dns_rdataset_t **sigrdatasetp = NULL; @@ -9769,7 +9782,7 @@ query_dname(query_ctx_t *qctx) { dns_namereln_t namereln; isc_buffer_t b; int order; - isc_result_t result; + isc_result_t result = ISC_R_UNSET; unsigned int nlabels; CCTRACE(ISC_LOG_DEBUG(3), "query_dname"); @@ -9998,7 +10011,7 @@ query_addcname(query_ctx_t *qctx, dns_trust_t trust, dns_ttl_t ttl) { */ static isc_result_t query_prepresponse(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; CCTRACE(ISC_LOG_DEBUG(3), "query_prepresponse"); @@ -10926,7 +10939,7 @@ query_glueanswer(query_ctx_t *qctx) { isc_result_t ns_query_done(query_ctx_t *qctx) { - isc_result_t result; + isc_result_t result = ISC_R_UNSET; const dns_namelist_t *secs = qctx->client->message->sections; CCTRACE(ISC_LOG_DEBUG(3), "ns_query_done"); From 75cdd758ed010c61fce7512f5513ab6dd422e97d Mon Sep 17 00:00:00 2001 From: JINMEI Tatuya Date: Wed, 16 Sep 2020 15:26:22 -0700 Subject: [PATCH 2/4] 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. --- lib/ns/Makefile.am | 1 + lib/ns/include/ns/events.h | 25 + lib/ns/include/ns/hooks.h | 204 ++++++++- lib/ns/include/ns/query.h | 39 ++ lib/ns/include/ns/server.h | 3 +- lib/ns/include/ns/types.h | 1 + lib/ns/query.c | 270 ++++++++++- lib/ns/tests/nstest.c | 1 + lib/ns/tests/query_test.c | 905 +++++++++++++++++++++++++++++++++++++ lib/ns/win32/libns.def | 1 + util/copyrights | 1 + 11 files changed, 1442 insertions(+), 9 deletions(-) create mode 100644 lib/ns/include/ns/events.h diff --git a/lib/ns/Makefile.am b/lib/ns/Makefile.am index d430493aef..243d087c5c 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 0000000000..8d3736cfeb --- /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 e8568f380d..a0e8b363e4 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 40f1f30ba9..011a8b4c13 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 beaed2bca2..6af3436abc 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 97c41d9e28..f26d379a17 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 11e0ddc6b6..393ddaf8ce 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 20b746023c..a779c9ee87 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 d068a8b081..0f6a598c5b 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 1680e4982d..66d706af4f 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 5a50c6523f..dd95e6bb6c 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 From c3a90b1d2c3e33e475894499043e81ef1a67f04a Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Thu, 19 Nov 2020 17:58:45 -0800 Subject: [PATCH 3/4] create system test with asynchronous plugin the test-async plugin uses ns_query_hookasync() at the NS_QUERY_DONE_SEND hook point to call an asynchronous function. the only effect is to change the query response code to "NOTIMP", so we can confirm that the hook ran and resumed correctly. --- bin/tests/system/Makefile.am | 3 +- bin/tests/system/hooks/clean.sh | 14 + bin/tests/system/hooks/driver/Makefile.am | 13 + bin/tests/system/hooks/driver/test-async.c | 364 +++++++++++++++++++++ bin/tests/system/hooks/ns1/example.db | 19 ++ bin/tests/system/hooks/ns1/named.conf.in | 39 +++ bin/tests/system/hooks/setup.sh | 14 + bin/tests/system/hooks/tests.sh | 34 ++ configure.ac | 3 +- util/copyrights | 4 + 10 files changed, 505 insertions(+), 2 deletions(-) create mode 100644 bin/tests/system/hooks/clean.sh create mode 100644 bin/tests/system/hooks/driver/Makefile.am create mode 100644 bin/tests/system/hooks/driver/test-async.c create mode 100644 bin/tests/system/hooks/ns1/example.db create mode 100644 bin/tests/system/hooks/ns1/named.conf.in create mode 100644 bin/tests/system/hooks/setup.sh create mode 100644 bin/tests/system/hooks/tests.sh diff --git a/bin/tests/system/Makefile.am b/bin/tests/system/Makefile.am index 6c4e1eb541..a4a3abb5a5 100644 --- a/bin/tests/system/Makefile.am +++ b/bin/tests/system/Makefile.am @@ -9,7 +9,7 @@ dist-hook: sed -n "s|^Would remove \(.*\)|$(distdir)/\1|p" | \ xargs -I{} rm -rf "{}" -SUBDIRS = dyndb/driver dlzexternal/driver +SUBDIRS = dyndb/driver dlzexternal/driver hooks/driver AM_CPPFLAGS += \ $(LIBISC_CFLAGS) @@ -109,6 +109,7 @@ TESTS += \ include-multiplecfg \ inline \ integrity \ + hooks \ keepalive \ legacy \ limits \ diff --git a/bin/tests/system/hooks/clean.sh b/bin/tests/system/hooks/clean.sh new file mode 100644 index 0000000000..271efd68f9 --- /dev/null +++ b/bin/tests/system/hooks/clean.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# +# 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. + +rm -f */named.run +rm -f */named.conf +rm -f */named.memstats diff --git a/bin/tests/system/hooks/driver/Makefile.am b/bin/tests/system/hooks/driver/Makefile.am new file mode 100644 index 0000000000..08187bf789 --- /dev/null +++ b/bin/tests/system/hooks/driver/Makefile.am @@ -0,0 +1,13 @@ +include $(top_srcdir)/Makefile.top + +AM_CPPFLAGS += \ + $(LIBISC_CFLAGS) \ + $(LIBDNS_CFLAGS) \ + $(LIBNS_CFLAGS) \ + $(LIBISCCFG_CFLAGS) + +hooks_LTLIBRARIES = test-async.la +hooksdir = $(abs_builddir) + +test_async_la_SOURCES = test-async.c +test_async_la_LDFLAGS = -avoid-version -module -shared -export-dynamic diff --git a/bin/tests/system/hooks/driver/test-async.c b/bin/tests/system/hooks/driver/test-async.c new file mode 100644 index 0000000000..8449c9b140 --- /dev/null +++ b/bin/tests/system/hooks/driver/test-async.c @@ -0,0 +1,364 @@ +/* + * 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. + */ + +/*! \file */ + +/* aliases for the exported symbols */ + +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#define CHECK(op) \ + do { \ + result = (op); \ + if (result != ISC_R_SUCCESS) { \ + goto cleanup; \ + } \ + } while (0) + +/* + * Persistent data for use by this module. This will be associated + * with client object address in the hash table, and will remain + * accessible until the client object is detached. + */ +typedef struct async_instance { + ns_plugin_t *module; + isc_mem_t *mctx; + isc_mempool_t *datapool; + isc_ht_t *ht; + isc_mutex_t hlock; + isc_log_t *lctx; +} async_instance_t; + +typedef struct state { + bool async; + ns_hook_resevent_t *rev; + ns_hookpoint_t hookpoint; + isc_result_t origresult; +} state_t; + +/* + * Forward declarations of functions referenced in install_hooks(). + */ +static ns_hookresult_t +async_qctx_initialize(void *arg, void *cbdata, isc_result_t *resp); +static ns_hookresult_t +async_query_done_begin(void *arg, void *cbdata, isc_result_t *resp); +static ns_hookresult_t +async_qctx_destroy(void *arg, void *cbdata, isc_result_t *resp); + +/*% + * Register the functions to be called at each hook point in 'hooktable', using + * memory context 'mctx' for allocating copies of stack-allocated structures + * passed to ns_hook_add(). Make sure 'inst' will be passed as the 'cbdata' + * argument to every callback. + */ +static void +install_hooks(ns_hooktable_t *hooktable, isc_mem_t *mctx, + async_instance_t *inst) { + const ns_hook_t async_init = { + .action = async_qctx_initialize, + .action_data = inst, + }; + const ns_hook_t async_donebegin = { + .action = async_query_done_begin, + .action_data = inst, + }; + const ns_hook_t async_destroy = { + .action = async_qctx_destroy, + .action_data = inst, + }; + + ns_hook_add(hooktable, mctx, NS_QUERY_QCTX_INITIALIZED, &async_init); + ns_hook_add(hooktable, mctx, NS_QUERY_DONE_BEGIN, &async_donebegin); + ns_hook_add(hooktable, mctx, NS_QUERY_QCTX_DESTROYED, &async_destroy); +} + +static void +logmsg(const char *fmt, ...) { + va_list ap; + + va_start(ap, fmt); + isc_log_write(ns_lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_INFO, fmt, ap); + va_end(ap); +} + +/** +** Mandatory plugin API functions: +** +** - plugin_destroy +** - plugin_register +** - plugin_version +** - plugin_check +**/ + +/* + * Called by ns_plugin_register() to initialize the plugin and + * register hook functions into the view hook table. + */ +isc_result_t +plugin_register(const char *parameters, const void *cfg, const char *cfg_file, + unsigned long cfg_line, isc_mem_t *mctx, isc_log_t *lctx, + void *actx, ns_hooktable_t *hooktable, void **instp) { + async_instance_t *inst = NULL; + isc_result_t result; + + UNUSED(parameters); + UNUSED(cfg); + UNUSED(actx); + + isc_log_write(lctx, NS_LOGCATEGORY_GENERAL, NS_LOGMODULE_HOOKS, + ISC_LOG_INFO, + "registering 'test-async' module from %s:%lu", cfg_file, + cfg_line); + + inst = isc_mem_get(mctx, sizeof(*inst)); + *inst = (async_instance_t){ .mctx = NULL }; + isc_mem_attach(mctx, &inst->mctx); + + isc_mempool_create(mctx, sizeof(state_t), &inst->datapool); + CHECK(isc_ht_init(&inst->ht, mctx, 16)); + isc_mutex_init(&inst->hlock); + + /* + * Set hook points in the view's hooktable. + */ + install_hooks(hooktable, mctx, inst); + + *instp = inst; + + return (ISC_R_SUCCESS); + +cleanup: + if (result != ISC_R_SUCCESS && inst != NULL) { + plugin_destroy((void **)&inst); + } + + return (result); +} + +isc_result_t +plugin_check(const char *parameters, const void *cfg, const char *cfg_file, + unsigned long cfg_line, isc_mem_t *mctx, isc_log_t *lctx, + void *actx) { + UNUSED(parameters); + UNUSED(cfg); + UNUSED(cfg_file); + UNUSED(cfg_line); + UNUSED(mctx); + UNUSED(lctx); + UNUSED(actx); + + return (ISC_R_SUCCESS); +} + +/* + * Called by ns_plugins_free(); frees memory allocated by + * the module when it was registered. + */ +void +plugin_destroy(void **instp) { + async_instance_t *inst = (async_instance_t *)*instp; + + if (inst->ht != NULL) { + isc_ht_destroy(&inst->ht); + isc_mutex_destroy(&inst->hlock); + } + if (inst->datapool != NULL) { + isc_mempool_destroy(&inst->datapool); + } + + isc_mem_putanddetach(&inst->mctx, inst, sizeof(*inst)); + *instp = NULL; + + return; +} + +/* + * Returns plugin API version for compatibility checks. + */ +int +plugin_version(void) { + return (NS_PLUGIN_VERSION); +} + +static state_t * +client_state_get(const query_ctx_t *qctx, async_instance_t *inst) { + state_t *state = NULL; + isc_result_t result; + + LOCK(&inst->hlock); + result = isc_ht_find(inst->ht, (const unsigned char *)&qctx->client, + sizeof(qctx->client), (void **)&state); + UNLOCK(&inst->hlock); + + return (result == ISC_R_SUCCESS ? state : NULL); +} + +static void +client_state_create(const query_ctx_t *qctx, async_instance_t *inst) { + state_t *state = NULL; + isc_result_t result; + + state = isc_mempool_get(inst->datapool); + if (state == NULL) { + return; + } + + LOCK(&inst->hlock); + result = isc_ht_add(inst->ht, (const unsigned char *)&qctx->client, + sizeof(qctx->client), state); + UNLOCK(&inst->hlock); + RUNTIME_CHECK(result == ISC_R_SUCCESS); +} + +static void +client_state_destroy(const query_ctx_t *qctx, async_instance_t *inst) { + state_t *state = client_state_get(qctx, inst); + isc_result_t result; + + if (state == NULL) { + return; + } + + LOCK(&inst->hlock); + result = isc_ht_delete(inst->ht, (const unsigned char *)&qctx->client, + sizeof(qctx->client)); + UNLOCK(&inst->hlock); + RUNTIME_CHECK(result == ISC_R_SUCCESS); + + isc_mempool_put(inst->datapool, state); +} + +static ns_hookresult_t +async_qctx_initialize(void *arg, void *cbdata, isc_result_t *resp) { + query_ctx_t *qctx = (query_ctx_t *)arg; + async_instance_t *inst = (async_instance_t *)cbdata; + state_t *state = NULL; + + logmsg("qctx init hook"); + *resp = ISC_R_UNSET; + + state = client_state_get(qctx, inst); + if (state == NULL) { + client_state_create(qctx, inst); + } + + return (NS_HOOK_CONTINUE); +} + +static void +cancelasync(ns_hookasync_t *hctx) { + UNUSED(hctx); + logmsg("cancelasync"); +} + +static void +destroyasync(ns_hookasync_t **ctxp) { + ns_hookasync_t *ctx = *ctxp; + + logmsg("destroyasync"); + *ctxp = NULL; + isc_mem_putanddetach(&ctx->mctx, ctx, sizeof(*ctx)); +} + +static isc_result_t +doasync(query_ctx_t *qctx, isc_mem_t *mctx, void *arg, isc_task_t *task, + isc_taskaction_t action, void *evarg, ns_hookasync_t **ctxp) { + ns_hook_resevent_t *rev = (ns_hook_resevent_t *)isc_event_allocate( + mctx, task, NS_EVENT_HOOKASYNCDONE, action, evarg, + sizeof(*rev)); + ns_hookasync_t *ctx = isc_mem_get(mctx, sizeof(*ctx)); + state_t *state = (state_t *)arg; + + logmsg("doasync"); + *ctx = (ns_hookasync_t){ .mctx = NULL }; + isc_mem_attach(mctx, &ctx->mctx); + ctx->cancel = cancelasync; + ctx->destroy = destroyasync; + + rev->hookpoint = state->hookpoint; + rev->origresult = state->origresult; + qctx->result = DNS_R_NOTIMP; + rev->saved_qctx = qctx; + rev->ctx = ctx; + + state->rev = rev; + + isc_task_send(task, (isc_event_t **)&rev); + + *ctxp = ctx; + return (ISC_R_SUCCESS); +} + +static ns_hookresult_t +async_query_done_begin(void *arg, void *cbdata, isc_result_t *resp) { + query_ctx_t *qctx = (query_ctx_t *)arg; + async_instance_t *inst = (async_instance_t *)cbdata; + state_t *state = client_state_get(qctx, inst); + + UNUSED(qctx); + UNUSED(cbdata); + UNUSED(state); + + logmsg("done begin hook"); + if (state->async) { + /* resuming */ + state->async = false; + return (NS_HOOK_CONTINUE); + } + + /* initial call */ + state->async = true; + state->hookpoint = NS_QUERY_DONE_BEGIN; + state->origresult = *resp; + ns_query_hookasync(qctx, doasync, state); + return (NS_HOOK_RETURN); +} + +static ns_hookresult_t +async_qctx_destroy(void *arg, void *cbdata, isc_result_t *resp) { + query_ctx_t *qctx = (query_ctx_t *)arg; + async_instance_t *inst = (async_instance_t *)cbdata; + + logmsg("qctx destroy hook"); + *resp = ISC_R_UNSET; + + if (!qctx->detach_client) { + return (NS_HOOK_CONTINUE); + } + + client_state_destroy(qctx, inst); + + return (NS_HOOK_CONTINUE); +} diff --git a/bin/tests/system/hooks/ns1/example.db b/bin/tests/system/hooks/ns1/example.db new file mode 100644 index 0000000000..ab0218c020 --- /dev/null +++ b/bin/tests/system/hooks/ns1/example.db @@ -0,0 +1,19 @@ +; 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 http://mozilla.org/MPL/2.0/. +; +; See the COPYRIGHT file distributed with this work for additional +; information regarding copyright ownership. + +$TTL 120 +@ SOA ns.unsigned. hostmaster.ns.unsigned. ( 1 3600 1200 604800 60 ) +@ NS ns +@ MX 10 mx + +ns A 10.53.0.1 + AAAA fd92:7065:b8e:ffff::1 + +a A 1.1.1.1 +mx A 2.2.2.2 diff --git a/bin/tests/system/hooks/ns1/named.conf.in b/bin/tests/system/hooks/ns1/named.conf.in new file mode 100644 index 0000000000..6696675ccd --- /dev/null +++ b/bin/tests/system/hooks/ns1/named.conf.in @@ -0,0 +1,39 @@ +/* + * 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 http://mozilla.org/MPL/2.0/. + * + * See the COPYRIGHT file distributed with this work for additional + * information regarding copyright ownership. + */ + +options { + query-source address 10.53.0.1; + notify-source 10.53.0.1; + transfer-source 10.53.0.1; + port @PORT@; + pid-file "named.pid"; + listen-on { 10.53.0.1; }; + recursion no; + dnssec-validation yes; + notify yes; + minimal-responses no; +}; + +plugin query "../driver/.libs/test-async.so"; + +key rndc_key { + secret "1234abcd8765"; + algorithm hmac-sha256; +}; + +controls { + inet 10.53.0.1 port @CONTROLPORT@ allow { any; } keys { rndc_key; }; +}; + +zone "example.com" { + type primary; + file "example.db"; +}; diff --git a/bin/tests/system/hooks/setup.sh b/bin/tests/system/hooks/setup.sh new file mode 100644 index 0000000000..110e0e3446 --- /dev/null +++ b/bin/tests/system/hooks/setup.sh @@ -0,0 +1,14 @@ +#!/bin/sh +# +# 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. + +. ../conf.sh + +copy_setports ns1/named.conf.in ns1/named.conf diff --git a/bin/tests/system/hooks/tests.sh b/bin/tests/system/hooks/tests.sh new file mode 100644 index 0000000000..284980c5bb --- /dev/null +++ b/bin/tests/system/hooks/tests.sh @@ -0,0 +1,34 @@ +#!/bin/sh +# +# 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. + +. ../conf.sh + +status=0 +n=0 + +rm -f dig.out.* + +DIGOPTS="+tcp +noadd +nosea +nostat +nocmd -p ${PORT}" +RNDCCMD="$RNDC -c ../common/rndc.conf -p ${CONTROLPORT} -s" + +n=$((n+1)) +echo_i "checking asynchronous hook action resumes correctly ($n)" +ret=0 +$DIG $DIGOPTS example.com @10.53.0.1 > dig.out.ns1.test$n || ret=1 +# the test-async plugin changes the status of any postiive answer to NOTIMP +grep -q "status: NOTIMP" dig.out.ns1.test$n || ret=1 +if [ $ret != 0 ]; then echo_i "failed"; fi +status=$((status + ret)) + + + +echo_i "exit status: $status" +[ $status -eq 0 ] || exit 1 diff --git a/configure.ac b/configure.ac index 625bc08b08..47459bc118 100644 --- a/configure.ac +++ b/configure.ac @@ -1633,7 +1633,8 @@ AC_CONFIG_FILES([bin/tests/Makefile bin/tests/system/Makefile bin/tests/system/conf.sh bin/tests/system/dyndb/driver/Makefile - bin/tests/system/dlzexternal/driver/Makefile]) + bin/tests/system/dlzexternal/driver/Makefile + bin/tests/system/hooks/driver/Makefile]) AC_CONFIG_FILES([bin/tests/system/ifconfig.sh], [chmod +x bin/tests/system/ifconfig.sh]) diff --git a/util/copyrights b/util/copyrights index dd95e6bb6c..a29a86d66d 100644 --- a/util/copyrights +++ b/util/copyrights @@ -478,6 +478,10 @@ ./bin/tests/system/glue/noglue.good X 2000,2001,2018,2019,2020 ./bin/tests/system/glue/setup.sh SH 2001,2004,2007,2012,2016,2018,2019,2020 ./bin/tests/system/glue/tests.sh SH 2000,2001,2003,2004,2007,2012,2013,2016,2017,2018,2019,2020 +./bin/tests/system/hooks/clean.sh SH 2020 +./bin/tests/system/hooks/driver/test-async.c C 2020 +./bin/tests/system/hooks/setup.sh SH 2020 +./bin/tests/system/hooks/tests.sh SH 2020 ./bin/tests/system/idna/clean.sh SH 2018,2019,2020 ./bin/tests/system/idna/setup.sh SH 2018,2019,2020 ./bin/tests/system/idna/tests.sh SH 2018,2019,2020 From 5285443c5f2c8d2ff668d6601a9e140b7fb0a061 Mon Sep 17 00:00:00 2001 From: Evan Hunt Date: Fri, 20 Nov 2020 14:26:51 -0800 Subject: [PATCH 4/4] CHANGES --- CHANGES | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGES b/CHANGES index b7d81e0566..8e61673307 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,9 @@ +5537. [func] The query plugin mechanism has been extended + to support asynchronous operations. For example, a + plugin can now trigger recursion and resume + processing when it's complete. Thanks to Jinmei + Tatuya at Infoblox. [GL #2141] + 5536. [func] Dig can now report the DNS64 prefixes in use (+dns64prefix). [GL #1154]