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] 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/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 e329b55e4c..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_detach(&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. @@ -249,7 +257,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 +444,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); @@ -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. @@ -5203,7 +5261,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 +5364,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 +5576,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 +5931,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 +6023,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 +6115,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 @@ -6239,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 @@ -6947,15 +7226,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 +7366,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 +7641,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 +7703,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 +8160,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 +8246,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 +8302,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 +8400,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 +8474,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 +8997,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 +9933,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 +10046,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 +10275,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 +11203,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"); 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..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 @@ -2095,6 +2099,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