diff --git a/CHANGES b/CHANGES index 0a334814cd..92ef0bc990 100644 --- a/CHANGES +++ b/CHANGES @@ -1,3 +1,7 @@ +4772. [test] Expanded unit testing framework for libns, using + hooks to interrupt query flow and inspect state + at specified locations. [RT #46173] + 4771. [bug] When sending RFC 5011 refresh queries, disregard cached DNSKEY rrsets. [RT #46251] diff --git a/configure b/configure index 4e1ee1d5b8..7b66f9b9a4 100755 --- a/configure +++ b/configure @@ -940,6 +940,7 @@ infodir docdir oldincludedir includedir +runstatedir localstatedir sharedstatedir sysconfdir @@ -1103,6 +1104,7 @@ datadir='${datarootdir}' sysconfdir='${prefix}/etc' sharedstatedir='${prefix}/com' localstatedir='${prefix}/var' +runstatedir='${localstatedir}/run' includedir='${prefix}/include' oldincludedir='/usr/include' docdir='${datarootdir}/doc/${PACKAGE_TARNAME}' @@ -1355,6 +1357,15 @@ do | -silent | --silent | --silen | --sile | --sil) silent=yes ;; + -runstatedir | --runstatedir | --runstatedi | --runstated \ + | --runstate | --runstat | --runsta | --runst | --runs \ + | --run | --ru | --r) + ac_prev=runstatedir ;; + -runstatedir=* | --runstatedir=* | --runstatedi=* | --runstated=* \ + | --runstate=* | --runstat=* | --runsta=* | --runst=* | --runs=* \ + | --run=* | --ru=* | --r=*) + runstatedir=$ac_optarg ;; + -sbindir | --sbindir | --sbindi | --sbind | --sbin | --sbi | --sb) ac_prev=sbindir ;; -sbindir=* | --sbindir=* | --sbindi=* | --sbind=* | --sbin=* \ @@ -1492,7 +1503,7 @@ fi for ac_var in exec_prefix prefix bindir sbindir libexecdir datarootdir \ datadir sysconfdir sharedstatedir localstatedir includedir \ oldincludedir docdir infodir htmldir dvidir pdfdir psdir \ - libdir localedir mandir + libdir localedir mandir runstatedir do eval ac_val=\$$ac_var # Remove trailing slashes. @@ -1645,6 +1656,7 @@ Fine tuning of the installation directories: --sysconfdir=DIR read-only single-machine data [PREFIX/etc] --sharedstatedir=DIR modifiable architecture-independent data [PREFIX/com] --localstatedir=DIR modifiable single-machine data [PREFIX/var] + --runstatedir=DIR modifiable per-process data [LOCALSTATEDIR/run] --libdir=DIR object code libraries [EPREFIX/lib] --includedir=DIR C header files [PREFIX/include] --oldincludedir=DIR C header files for non-gcc [/usr/include] @@ -11466,7 +11478,7 @@ fi XTARGETS= case "$enable_developer" in yes) - STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1" + STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1 -DNS_HOOKS_ENABLE=1" test "${enable_fixed_rrset+set}" = set || enable_fixed_rrset=yes test "${enable_querytrace+set}" = set || enable_querytrace=yes test "${with_atf+set}" = set || with_atf=yes @@ -22225,6 +22237,7 @@ if test "no" != "$atf"; then $as_echo "#define ATF_TEST 1" >>confdefs.h STD_CINCLUDES="$STD_CINCLUDES -I$atf/include" + STD_CDEFINES="$STD_CDEFINES -DNS_HOOKS_ENABLE=1" ATFBIN="$atf/bin" ATFLIBS="-L$atf/lib -latf-c" { $as_echo "$as_me:${as_lineno-$LINENO}: checking for exp in -lm" >&5 diff --git a/configure.in b/configure.in index 2bbaf00988..11545190db 100644 --- a/configure.in +++ b/configure.in @@ -62,7 +62,7 @@ AC_ARG_ENABLE(developer, [ --enable-developer enable developer build setti XTARGETS= case "$enable_developer" in yes) - STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1" + STD_CDEFINES="$STD_CDEFINES -DISC_MEM_DEFAULTFILL=1 -DISC_LIST_CHECKINIT=1 -DNS_HOOKS_ENABLE=1" test "${enable_fixed_rrset+set}" = set || enable_fixed_rrset=yes test "${enable_querytrace+set}" = set || enable_querytrace=yes test "${with_atf+set}" = set || with_atf=yes @@ -4771,6 +4771,7 @@ ATFLIBS= if test "no" != "$atf"; then AC_DEFINE(ATF_TEST, 1, [define if ATF unit tests are to be built.]) STD_CINCLUDES="$STD_CINCLUDES -I$atf/include" + STD_CDEFINES="$STD_CDEFINES -DNS_HOOKS_ENABLE=1" ATFBIN="$atf/bin" ATFLIBS="-L$atf/lib -latf-c" AC_CHECK_LIB(m, exp, libm=yes, libm=no) diff --git a/lib/ns/hooks.h b/lib/ns/hooks.h new file mode 100644 index 0000000000..c16cebbf7f --- /dev/null +++ b/lib/ns/hooks.h @@ -0,0 +1,142 @@ +/* + * Copyright (C) 2017 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/. + */ + +#ifndef NS_HOOKS_H +#define NS_HOOKS_H 1 + +#ifdef NS_HOOKS_ENABLE + +/*! \file */ + +#include + +/* + * Hooks provide a way of running a callback function once a certain place in + * code is reached. Current use is limited to libns unit tests and thus: + * + * - hook-related types and macros are not placed in libns header files, + * - hook-related code is compiled away unless --enable-developer is used, + * - hook-related macro names are prefixed with "NS_". + * + * However, the implementation is pretty generic and could be repurposed for + * general use, e.g. as part of libisc, after some further customization. + * + * Hooks are created by inserting a macro into any function returning + * isc_result_t (NS_PROCESS_HOOK()) or void (NS_PROCESS_HOOK_VOID()). Each + * hook has an identifier, which is an integer that is an index into the hook + * table. In an attempt to keep things as simple as possible, current + * implementation: + * + * - uses hook tables which are statically-sized arrays only allowing a + * single callback to be invoked for each hook identifier, + * - only supports replacing whole hook tables. + * + * Hook callbacks are functions which: + * + * - return a boolean value; if ISC_TRUE is returned by the callback, the + * function into which the hook is inserted will return at hook insertion + * point; if ISC_FALSE is returned by the callback, execution of the + * function into which the hook is inserted continues normally, + * + * - accept three pointers as arguments: + * + * - a pointer specified by the hook itself, + * - a pointer specified upon inserting the callback into the hook table, + * - a pointer to isc_result_t which will be returned by the function + * into which the hook is inserted if the callback returns ISC_TRUE. + * + * In order for a hook callback to be called for a given hook, a pointer to + * that callback (along with an optional pointer to callback-specific data) has + * to be inserted into the hook table entry for that hook. + * + * Consider the following sample code: + * + * ---------------------------------------------------------------------------- + * const ns_hook_t *foo_hook_table = NULL; + * + * isc_result_t + * foo_bar(void) { + * int val = 42; + * ... + * NS_PROCESS_HOOK(foo_hook_table, FOO_EXTRACT_VAL, &val); + * ... + * printf("This message may not be printed due to use of hooks."); + * + * return (ISC_R_SUCCESS); + * } + * + * isc_boolean_t + * cause_failure(void *hook_data, void *callback_data, isc_result_t *resultp) { + * ... + * *resultp = ISC_R_FAILURE; + * + * return (ISC_TRUE); + * } + * + * void + * test_foo_bar(void) { + * isc_boolean_t foo_bar_called = ISC_FALSE; + * const ns_hook_t my_hooks[FOO_HOOKS_COUNT] = { + * [FOO_EXTRACT_VAL] = { + * .callback = cause_failure, + * .callback_data = &foo_bar_called, + * }, + * }; + * + * foo_hook_table = my_hooks; + * + * foo_bar(); + * } + * ---------------------------------------------------------------------------- + * + * When test_foo_bar() is called, the hook table is first replaced. Then + * foo_bar() gets invoked. Once execution reaches the insertion point for hook + * FOO_EXTRACT_VAL, cause_failure() will be called with &val as hook_data and + * &foo_bar_called as callback_data. It can do whatever it pleases with these + * two values. Eventually, cause_failure() sets *resultp to ISC_R_FAILURE and + * returns ISC_TRUE, which causes foo_bar() to return ISC_R_FAILURE and never + * execute the printf() call below hook insertion point. + */ + +enum { + NS_QUERY_SETUP_QCTX_INITIALIZED, + NS_QUERY_LOOKUP_BEGIN, + NS_QUERY_DONE_BEGIN, + NS_QUERY_HOOKS_COUNT +}; + +typedef isc_boolean_t +(*ns_hook_cb_t)(void *hook_data, void *callback_data, isc_result_t *resultp); + +typedef struct ns_hook { + ns_hook_cb_t callback; + void *callback_data; +} ns_hook_t; + +#define _NS_PROCESS_HOOK(table, id, data, ...) \ + if (table != NULL) { \ + ns_hook_cb_t _callback = table[id].callback; \ + void *_callback_data = table[id].callback_data; \ + isc_result_t _result; \ + \ + if (_callback != NULL && \ + _callback(data, _callback_data, &_result)) { \ + return __VA_ARGS__; \ + } \ + } + +#define NS_PROCESS_HOOK(table, id, data) \ + _NS_PROCESS_HOOK(table, id, data, _result) + +#define NS_PROCESS_HOOK_VOID(table, id, data) \ + _NS_PROCESS_HOOK(table, id, data) + +LIBNS_EXTERNAL_DATA extern const ns_hook_t *ns__hook_table; + +#endif /* NS_HOOKS_ENABLE */ +#endif /* NS_HOOKS_H */ diff --git a/lib/ns/include/ns/query.h b/lib/ns/include/ns/query.h index ce6d06fc77..63b15a7569 100644 --- a/lib/ns/include/ns/query.h +++ b/lib/ns/include/ns/query.h @@ -16,6 +16,7 @@ #include #include +#include #include #include @@ -92,6 +93,55 @@ struct ns_query { #define NS_QUERYATTR_RRL_CHECKED 0x10000 #define NS_QUERYATTR_REDIRECT 0x20000 +/* query context structure */ + +typedef struct query_ctx { + isc_buffer_t *dbuf; /* name buffer */ + dns_name_t *fname; /* found name from DB lookup */ + dns_rdataset_t *rdataset; /* found rdataset */ + dns_rdataset_t *sigrdataset; /* found sigrdataset */ + dns_rdataset_t *noqname; /* rdataset needing + * NOQNAME proof */ + dns_rdatatype_t qtype; + dns_rdatatype_t type; + + unsigned int options; /* DB lookup options */ + + isc_boolean_t redirected; /* nxdomain redirected? */ + isc_boolean_t is_zone; /* is DB a zone DB? */ + isc_boolean_t is_staticstub_zone; + isc_boolean_t resuming; /* resumed from recursion? */ + isc_boolean_t dns64, dns64_exclude, rpz; + isc_boolean_t authoritative; /* authoritative query? */ + isc_boolean_t want_restart; /* CNAME chain or other + * restart needed */ + isc_boolean_t need_wildcardproof; /* wilcard proof needed */ + isc_boolean_t nxrewrite; /* negative answer from RPZ */ + isc_boolean_t findcoveringnsec; /* lookup covering NSEC */ + isc_boolean_t want_stale; /* want stale records? */ + dns_fixedname_t wildcardname; /* name needing wcard proof */ + dns_fixedname_t dsname; /* name needing DS */ + + ns_client_t *client; /* client object */ + dns_fetchevent_t *event; /* recursion event */ + + dns_db_t *db; /* zone or cache database */ + dns_dbversion_t *version; /* DB version */ + dns_dbnode_t *node; /* DB node */ + + dns_db_t *zdb; /* zone DB values, saved */ + dns_name_t *zfname; /* while searching cache */ + dns_dbversion_t *zversion; /* for a better answer */ + dns_rdataset_t *zrdataset; + dns_rdataset_t *zsigrdataset; + + dns_rpz_st_t *rpz_st; /* RPZ state */ + dns_zone_t *zone; /* zone to search */ + + isc_result_t result; /* query result */ + int line; /* line to report error */ +} query_ctx_t; + isc_result_t ns_query_init(ns_client_t *client); @@ -104,4 +154,16 @@ ns_query_start(ns_client_t *client); void ns_query_cancel(ns_client_t *client); +/*% + * (Must not be used outside this module and its associated unit tests.) + */ +isc_result_t +ns__query_sfcache(query_ctx_t *qctx); + +/*% + * (Must not be used outside this module and its associated unit tests.) + */ +isc_result_t +ns__query_start(query_ctx_t *qctx); + #endif /* NS_QUERY_H */ diff --git a/lib/ns/query.c b/lib/ns/query.c index 7e19ff06fc..8c9fa5c0c8 100644 --- a/lib/ns/query.c +++ b/lib/ns/query.c @@ -62,6 +62,8 @@ #include #include +#include "hooks.h" + #if 0 /* * It has been recommended that DNS64 be changed to return excluded @@ -243,17 +245,32 @@ static void log_noexistnodata(void *val, int level, const char *fmt, ...) ISC_FORMAT_PRINTF(3, 4); -/*% - * The structure and functions defined below implement the query logic - * that previously lived in the single very complex function query_find(). - * The query_ctx_t structure maintains state from function to function. - * The call flow for the general query processing algorithm is described - * below: +#ifdef NS_HOOKS_ENABLE + +LIBNS_EXTERNAL_DATA const ns_hook_t *ns__hook_table = NULL; + +#define PROCESS_HOOK(...) \ + NS_PROCESS_HOOK(ns__hook_table, __VA_ARGS__) +#define PROCESS_HOOK_VOID(...) \ + NS_PROCESS_HOOK_VOID(ns__hook_table, __VA_ARGS__) + +#else + +#define PROCESS_HOOK(...) do {} while (0) +#define PROCESS_HOOK_VOID(...) do {} while (0) + +#endif + +/* + * The functions defined below implement the query logic that previously lived + * in the single very complex function query_find(). The query_ctx_t structure + * defined in maintains state from function to function. The call + * flow for the general query processing algorithm is described below: * * 1. Set up query context and other resources for a client * query (query_setup()) * - * 2. Start the search (query_start()) + * 2. Start the search (ns__query_start()) * * 3. Identify authoritative data sources which may have an answer; * search them (query_lookup()). If an answer is found, go to 7. @@ -310,53 +327,6 @@ log_noexistnodata(void *val, int level, const char *fmt, ...) * DNS64, filter-aaaa, RPZ, RRL, and the SERVFAIL cache.) */ -typedef struct query_ctx { - isc_buffer_t *dbuf; /* name buffer */ - dns_name_t *fname; /* found name from DB lookup */ - dns_rdataset_t *rdataset; /* found rdataset */ - dns_rdataset_t *sigrdataset; /* found sigrdataset */ - dns_rdataset_t *noqname; /* rdataset needing - * NOQNAME proof */ - dns_rdatatype_t qtype; - dns_rdatatype_t type; - - unsigned int options; /* DB lookup options */ - - isc_boolean_t redirected; /* nxdomain redirected? */ - isc_boolean_t is_zone; /* is DB a zone DB? */ - isc_boolean_t is_staticstub_zone; - isc_boolean_t resuming; /* resumed from recursion? */ - isc_boolean_t dns64, dns64_exclude, rpz; - isc_boolean_t authoritative; /* authoritative query? */ - isc_boolean_t want_restart; /* CNAME chain or other - * restart needed */ - isc_boolean_t need_wildcardproof; /* wilcard proof needed */ - isc_boolean_t nxrewrite; /* negative answer from RPZ */ - isc_boolean_t findcoveringnsec; /* lookup covering NSEC */ - isc_boolean_t want_stale; /* want stale records? */ - dns_fixedname_t wildcardname; /* name needing wcard proof */ - dns_fixedname_t dsname; /* name needing DS */ - - ns_client_t *client; /* client object */ - dns_fetchevent_t *event; /* recursion event */ - - dns_db_t *db; /* zone or cache database */ - dns_dbversion_t *version; /* DB version */ - dns_dbnode_t *node; /* DB node */ - - dns_db_t *zdb; /* zone DB values, saved */ - dns_name_t *zfname; /* while searching cache */ - dns_dbversion_t *zversion; /* for a better answer */ - dns_rdataset_t *zrdataset; - dns_rdataset_t *zsigrdataset; - - dns_rpz_st_t *rpz_st; /* RPZ state */ - dns_zone_t *zone; /* zone to search */ - - isc_result_t result; /* query result */ - int line; /* line to report error */ -} query_ctx_t; - static void query_trace(query_ctx_t *qctx); @@ -367,9 +337,6 @@ qctx_init(ns_client_t *client, dns_fetchevent_t *event, static isc_result_t query_setup(ns_client_t *client, dns_rdatatype_t qtype); -static isc_result_t -query_start(query_ctx_t *qctx); - static isc_result_t query_lookup(query_ctx_t *qctx); @@ -384,9 +351,6 @@ query_recurse(ns_client_t *client, dns_rdatatype_t qtype, dns_name_t *qname, static isc_result_t query_resume(query_ctx_t *qctx); -static isc_result_t -query_sfcache(query_ctx_t *qctx); - static isc_result_t query_checkrrl(query_ctx_t *qctx, isc_result_t result); @@ -5121,12 +5085,12 @@ query_trace(query_ctx_t *qctx) { /* * Set up query processing for the current query of 'client'. * Calls qctx_init() to initialize a query context, checks - * the SERVFAIL cache, then hands off processing to query_start(). + * the SERVFAIL cache, then hands off processing to ns__query_start(). * * This is called only from ns_query_start(), to begin a query * for the first time. Restarting an existing query (for * instance, to handle CNAME lookups), is done by calling - * query_start() again with the same query context. Resuming from + * ns__query_start() again with the same query context. Resuming from * recursion is handled by query_resume(). */ static isc_result_t @@ -5137,14 +5101,6 @@ query_setup(ns_client_t *client, dns_rdatatype_t qtype) { qctx_init(client, NULL, qtype, &qctx); query_trace(&qctx); - /* - * Check SERVFAIL cache - */ - result = query_sfcache(&qctx); - if (result != ISC_R_COMPLETE) { - return (result); - } - /* * If it's a SIG query, we'll iterate the node. */ @@ -5152,11 +5108,19 @@ query_setup(ns_client_t *client, dns_rdatatype_t qtype) { qctx.qtype == dns_rdatatype_sig) { qctx.type = dns_rdatatype_any; - } else { - qctx.type = qctx.qtype; } - return (query_start(&qctx)); + PROCESS_HOOK(NS_QUERY_SETUP_QCTX_INITIALIZED, &qctx); + + /* + * Check SERVFAIL cache + */ + result = ns__query_sfcache(&qctx); + if (result != ISC_R_COMPLETE) { + return (result); + } + + return (ns__query_start(&qctx)); } /*% @@ -5166,10 +5130,10 @@ query_setup(ns_client_t *client, dns_rdatatype_t qtype) { * follow a CNAME chain. Determines which authoritative database to * search, then hands off processing to query_lookup(). */ -static isc_result_t -query_start(query_ctx_t *qctx) { +isc_result_t +ns__query_start(query_ctx_t *qctx) { isc_result_t result; - CCTRACE(ISC_LOG_DEBUG(3), "query_start"); + CCTRACE(ISC_LOG_DEBUG(3), "ns__query_start"); qctx->want_restart = ISC_FALSE; qctx->authoritative = ISC_FALSE; qctx->version = NULL; @@ -5206,6 +5170,12 @@ query_start(query_ctx_t *qctx) { if (dns_rdatatype_atparent(qctx->qtype) && !dns_name_equal(qctx->client->query.qname, dns_rootname)) { + /* + * If authoritative data for this QTYPE is supposed to live in + * the parent zone, do not look for an exact match for QNAME, + * but rather for its containing zone (unless the QNAME is + * root). + */ qctx->options |= DNS_GETDB_NOEXACT; } @@ -5218,8 +5188,10 @@ query_start(query_ctx_t *qctx) { (qctx->options & DNS_GETDB_NOEXACT) != 0)) { /* - * If the query type is DS, look to see if we are - * authoritative for the child zone. + * This is a non-recursive QTYPE=DS query with QNAME whose + * parent we are not authoritative for. Check whether we are + * authoritative for QNAME, because if so, we need to send a + * "no data" response as required by RFC 4035, section 3.1.4.1. */ dns_db_t *tdb = NULL; dns_zone_t *tzone = NULL; @@ -5232,6 +5204,10 @@ query_start(query_ctx_t *qctx) { DNS_GETDB_PARTIAL, &tzone, &tdb, &tversion); if (tresult == ISC_R_SUCCESS) { + /* + * We are authoritative for QNAME. Attach the relevant + * zone to query context, set result to ISC_R_SUCCESS. + */ qctx->options &= ~DNS_GETDB_NOEXACT; query_putrdataset(qctx->client, &qctx->rdataset); if (qctx->db != NULL) { @@ -5247,6 +5223,10 @@ query_start(query_ctx_t *qctx) { qctx->is_zone = ISC_TRUE; result = ISC_R_SUCCESS; } else { + /* + * We are not authoritative for QNAME. Clean up and + * leave result as it was. + */ if (tdb != NULL) { dns_db_detach(&tdb); } @@ -5255,6 +5235,11 @@ query_start(query_ctx_t *qctx) { } } } + /* + * If we did not find a database from which we can answer the query, + * respond with either REFUSED or SERVFAIL, depending on what the + * result of query_getdb() was. + */ if (result != ISC_R_SUCCESS) { if (result == DNS_R_REFUSED) { if (WANTRECURSION(qctx->client)) { @@ -5269,12 +5254,17 @@ query_start(query_ctx_t *qctx) { } } else { CCTRACE(ISC_LOG_ERROR, - "query_start: query_getdb failed"); + "ns__query_start: query_getdb failed"); QUERY_ERROR(qctx, DNS_R_SERVFAIL); } return (query_done(qctx)); } + /* + * We found a database from which we can answer the query. Update + * relevant query context flags if the answer is to be prepared using + * authoritative data. + */ qctx->is_staticstub_zone = ISC_FALSE; if (qctx->is_zone) { qctx->authoritative = ISC_TRUE; @@ -5285,6 +5275,10 @@ query_start(query_ctx_t *qctx) { } } + /* + * Attach to the database which will be used to prepare the answer. + * Update query statistics. + */ if (qctx->event == NULL && qctx->client->query.restarts == 0) { if (qctx->is_zone) { if (qctx->zone != NULL) { @@ -5326,6 +5320,8 @@ query_lookup(query_ctx_t *qctx) { CCTRACE(ISC_LOG_DEBUG(3), "query_lookup"); + PROCESS_HOOK(NS_QUERY_LOOKUP_BEGIN, qctx); + dns_clientinfomethods_init(&cm, ns_client_sourceip); dns_clientinfo_init(&ci, qctx->client, NULL); @@ -5884,10 +5880,11 @@ query_resume(query_ctx_t *qctx) { * 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 * from a query with CD=1, *or* if the current query has CD=0, then we just - * return SERVFAIL again. + * return SERVFAIL again. This prevents a validation failure from eliciting a + * SERVFAIL response to a CD=1 query. */ -static isc_result_t -query_sfcache(query_ctx_t *qctx) { +isc_result_t +ns__query_sfcache(query_ctx_t *qctx) { isc_boolean_t failcache; isc_uint32_t flags; @@ -10317,7 +10314,7 @@ query_glueanswer(query_ctx_t *qctx) { * * - Clean up * - If we have an answer ready (positive or negative), send it. - * - If we need to restart for a chaining query, call query_start() again. + * - If we need to restart for a chaining query, call ns__query_start() again. * - If we've started recursion, then just clean up; things will be * restarted via fetch_callback()/query_resume(). */ @@ -10326,6 +10323,8 @@ query_done(query_ctx_t *qctx) { const dns_namelist_t *secs = qctx->client->message->sections; CCTRACE(ISC_LOG_DEBUG(3), "query_done"); + PROCESS_HOOK(NS_QUERY_DONE_BEGIN, qctx); + /* * General cleanup. */ @@ -10352,7 +10351,7 @@ query_done(query_ctx_t *qctx) { */ if (qctx->want_restart && qctx->client->query.restarts < MAX_RESTARTS) { qctx->client->query.restarts++; - return (query_start(qctx)); + return (ns__query_start(qctx)); } if (qctx->want_stale) { diff --git a/lib/ns/tests/Makefile.in b/lib/ns/tests/Makefile.in index 44cff74167..8d3de55d2f 100644 --- a/lib/ns/tests/Makefile.in +++ b/lib/ns/tests/Makefile.in @@ -32,11 +32,13 @@ LIBS = @LIBS@ @ATFLIBS@ OBJS = nstest.@O@ SRCS = nstest.c \ listenlist_test.c \ - notify_test.c + notify_test.c \ + query_test.c SUBDIRS = TARGETS = listenlist_test@EXEEXT@ \ - notify_test@EXEEXT@ + notify_test@EXEEXT@ \ + query_test @BIND9_MAKE_RULES@ @@ -50,6 +52,11 @@ notify_test@EXEEXT@: notify_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNS notify_test.@O@ nstest.@O@ ${NSLIBS} ${DNSLIBS} \ ${ISCLIBS} ${LIBS} +query_test@EXEEXT@: query_test.@O@ nstest.@O@ ${NSDEPLIBS} ${ISCDEPLIBS} ${DNSDEPLIBS} + ${LIBTOOL_MODE_LINK} ${PURIFY} ${CC} ${CFLAGS} ${LDFLAGS} -o $@ \ + query_test.@O@ nstest.@O@ ${NSLIBS} ${DNSLIBS} \ + ${ISCLIBS} ${LIBS} + unit:: sh ${top_srcdir}/unit/unittest.sh diff --git a/lib/ns/tests/notify_test.c b/lib/ns/tests/notify_test.c index 92b9d6f311..2e68781b00 100644 --- a/lib/ns/tests/notify_test.c +++ b/lib/ns/tests/notify_test.c @@ -28,49 +28,6 @@ #include "nstest.h" -static dns_zone_t *zone = NULL; -static dns_view_t *view = NULL; - -/* - * Helper functions - */ -static void -setup_zone(const char *zonename, const char *filename) { - isc_result_t result; - dns_db_t *db = NULL; - - result = ns_test_makezone(zonename, &zone, NULL, ISC_TRUE); - ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); - result = ns_test_setupzonemgr(); - ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); - result = ns_test_managezone(zone); - ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); - view = dns_zone_getview(zone); - ATF_REQUIRE(view->zonetable != NULL); - view->nocookieudp = 512; - - dns_zone_setfile(zone, filename); - result = dns_zone_load(zone); - ATF_REQUIRE(result == ISC_R_SUCCESS); - - /* The zone should now be loaded; test it */ - result = dns_zone_getdb(zone, &db); - ATF_CHECK_EQ(result, ISC_R_SUCCESS); - ATF_CHECK(db != NULL); - if (db != NULL) { - dns_db_detach(&db); - } -} - -static void -cleanup_zone() { - ns_test_releasezone(zone); - ns_test_closezonemgr(); - - dns_zone_detach(&zone); - dns_view_detach(&view); -} - static void check_response(isc_buffer_t *buf) { isc_result_t result; @@ -113,7 +70,12 @@ ATF_TC_BODY(notify_start, tc) { result = ns_test_getclient(NULL, ISC_FALSE, &client); ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); - setup_zone("example.com", "testdata/notify/zone1.db"); + result = ns_test_makeview("view", ISC_FALSE, &client->view); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + + result = ns_test_serve_zone("example.com", "testdata/notify/zone1.db", + client->view); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); /* * Create a NOTIFY message by parsing a file in testdata. @@ -136,7 +98,6 @@ ATF_TC_BODY(notify_start, tc) { * Set up client object with this message and test the NOTIFY * handler. */ - dns_view_attach(view, &client->view); if (client->message != NULL) { dns_message_destroy(&client->message); } @@ -148,7 +109,7 @@ ATF_TC_BODY(notify_start, tc) { /* * Clean up */ - cleanup_zone(); + ns_test_cleanup_zone(); ns_client_detach(&client); diff --git a/lib/ns/tests/nstest.c b/lib/ns/tests/nstest.c index 4a9f20e464..a47ee8aea0 100644 --- a/lib/ns/tests/nstest.c +++ b/lib/ns/tests/nstest.c @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ #include #include +#include #include #include #include @@ -42,6 +44,8 @@ #include #include +#include "../hooks.h" + #include "nstest.h" isc_mem_t *mctx = NULL; @@ -63,6 +67,8 @@ isc_boolean_t run_managers = ISC_FALSE; static isc_boolean_t hash_active = ISC_FALSE, dst_active = ISC_FALSE; +static dns_zone_t *served_zone = NULL; + /* * Logging categories: this needs to match the list in lib/ns/log.c. */ @@ -275,6 +281,8 @@ ns_test_begin(FILE *logfile, isc_boolean_t start_managers) { if (chdir(TESTS) == -1) CHECK(ISC_R_FAILURE); + ns__hook_table = NULL; + return (ISC_R_SUCCESS); cleanup: @@ -306,15 +314,29 @@ ns_test_end(void) { isc_mem_destroy(&mctx); } -/* - * Create a view. - */ isc_result_t -ns_test_makeview(const char *name, dns_view_t **viewp) { - isc_result_t result; +ns_test_makeview(const char *name, isc_boolean_t with_cache, + dns_view_t **viewp) +{ + dns_cache_t *cache = NULL; dns_view_t *view = NULL; + isc_result_t result; CHECK(dns_view_create(mctx, dns_rdataclass_in, name, &view)); + + if (with_cache) { + CHECK(dns_cache_create(mctx, taskmgr, timermgr, + dns_rdataclass_in, "rbt", 0, NULL, + &cache)); + dns_view_setcache(view, cache); + /* + * Reference count for "cache" is now at 2, so decrement it in + * order for the cache to be automatically freed when "view" + * gets freed. + */ + dns_cache_detach(&cache); + } + *viewp = view; return (ISC_R_SUCCESS); @@ -417,6 +439,79 @@ ns_test_closezonemgr(void) { dns_zonemgr_detach(&zonemgr); } +isc_result_t +ns_test_serve_zone(const char *zonename, const char *filename, + dns_view_t *view) +{ + isc_result_t result; + dns_db_t *db = NULL; + + /* + * Prepare zone structure for further processing. + */ + result = ns_test_makezone(zonename, &served_zone, view, ISC_TRUE); + if (result != ISC_R_SUCCESS) { + return (result); + } + + /* + * Start zone manager. + */ + result = ns_test_setupzonemgr(); + if (result != ISC_R_SUCCESS) { + goto free_zone; + } + + /* + * Add the zone to the zone manager. + */ + result = ns_test_managezone(served_zone); + if (result != ISC_R_SUCCESS) { + goto close_zonemgr; + } + + view->nocookieudp = 512; + + /* + * Set path to the master file for the zone and then load it. + */ + dns_zone_setfile(served_zone, filename); + result = dns_zone_load(served_zone); + if (result != ISC_R_SUCCESS) { + goto release_zone; + } + + /* + * The zone should now be loaded; test it. + */ + result = dns_zone_getdb(served_zone, &db); + if (result != ISC_R_SUCCESS) { + goto release_zone; + } + if (db != NULL) { + dns_db_detach(&db); + } + + return (ISC_R_SUCCESS); + +release_zone: + ns_test_releasezone(served_zone); +close_zonemgr: + ns_test_closezonemgr(); +free_zone: + dns_zone_detach(&served_zone); + + return (result); +} + +void +ns_test_cleanup_zone(void) { + ns_test_releasezone(served_zone); + ns_test_closezonemgr(); + + dns_zone_detach(&served_zone); +} + isc_result_t ns_test_getclient(ns_interface_t *ifp0, isc_boolean_t tcp, ns_client_t **clientp) @@ -435,6 +530,291 @@ ns_test_getclient(ns_interface_t *ifp0, isc_boolean_t tcp, return (result); } +/*% + * Synthesize a DNS message based on supplied QNAME, QTYPE and flags, then + * parse it and store the results in client->message. + */ +static isc_result_t +attach_query_msg_to_client(ns_client_t *client, const char *qnamestr, + dns_rdatatype_t qtype, unsigned int qflags) +{ + dns_rdataset_t *qrdataset = NULL; + dns_message_t *message = NULL; + unsigned char query[65536]; + dns_name_t *qname = NULL; + isc_buffer_t querybuf; + dns_compress_t cctx; + isc_result_t result; + isc_uint32_t qid; + + REQUIRE(client != NULL); + REQUIRE(qnamestr != NULL); + + /* + * Create a new DNS message holding a query. + */ + result = dns_message_create(mctx, DNS_MESSAGE_INTENTRENDER, &message); + if (result != ISC_R_SUCCESS) { + return (result); + } + + /* + * Set query ID to a random value. + */ + isc_random_get(&qid); + message->id = (dns_messageid_t)(qid & 0xffff); + + /* + * Set query flags as requested by the caller. + */ + message->flags = qflags; + + /* + * Allocate structures required to construct the query. + */ + result = dns_message_gettemprdataset(message, &qrdataset); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + result = dns_message_gettempname(message, &qname); + if (result != ISC_R_SUCCESS) { + goto put_rdataset; + } + + /* + * Convert "qnamestr" to a DNS name, create a question rdataset of + * class IN and type "qtype", link the two and add the result to the + * QUESTION section of the query. + */ + result = dns_name_fromstring(qname, qnamestr, 0, mctx); + if (result != ISC_R_SUCCESS) { + goto put_name; + } + dns_rdataset_makequestion(qrdataset, dns_rdataclass_in, qtype); + ISC_LIST_APPEND(qname->list, qrdataset, link); + dns_message_addname(message, qname, DNS_SECTION_QUESTION); + + /* + * Render the query. + */ + dns_compress_init(&cctx, -1, mctx); + isc_buffer_init(&querybuf, query, sizeof(query)); + result = dns_message_renderbegin(message, &cctx, &querybuf); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + result = dns_message_rendersection(message, DNS_SECTION_QUESTION, 0); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + result = dns_message_renderend(message); + if (result != ISC_R_SUCCESS) { + goto destroy_message; + } + dns_compress_invalidate(&cctx); + + /* + * Destroy the created message as it was rendered into "querybuf" and + * the latter is all we are going to need from now on. + */ + dns_message_destroy(&message); + + /* + * Parse the rendered query, storing results in client->message. + */ + isc_buffer_first(&querybuf); + return (dns_message_parse(client->message, &querybuf, 0)); + +put_name: + dns_message_puttempname(message, &qname); +put_rdataset: + dns_message_puttemprdataset(message, &qrdataset); +destroy_message: + dns_message_destroy(&message); + + return (result); +} + +/*% + * A hook callback which stores the query context pointed to by "hook_data" at + * "callback_data". Causes execution to be interrupted at hook insertion + * point. + */ +static isc_boolean_t +extract_qctx(void *hook_data, void *callback_data, isc_result_t *resultp) { + query_ctx_t **qctxp; + query_ctx_t *qctx; + + REQUIRE(hook_data != NULL); + REQUIRE(callback_data != NULL); + REQUIRE(resultp != NULL); + + /* + * qctx is a stack variable in lib/ns/query.c. Its contents need to be + * duplicated or otherwise they will become invalidated once the stack + * gets unwound. + */ + qctx = isc_mem_get(mctx, sizeof(*qctx)); + if (qctx != NULL) { + memmove(qctx, (query_ctx_t *)hook_data, sizeof(*qctx)); + } + + qctxp = (query_ctx_t **)callback_data; + /* + * If memory allocation failed, the supplied pointer will simply be set + * to NULL. We rely on the user of this hook to react properly. + */ + *qctxp = qctx; + *resultp = ISC_R_UNSET; + + return (ISC_TRUE); +} + +/*% + * Initialize a query context for "client" and store it in "qctxp". + * + * Requires: + * + * \li "client->message" to hold a parsed DNS query. + */ +static isc_result_t +create_qctx_for_client(ns_client_t *client, query_ctx_t **qctxp) { + const ns_hook_t *saved_hook_table; + const ns_hook_t query_hooks[NS_QUERY_HOOKS_COUNT] = { + [NS_QUERY_SETUP_QCTX_INITIALIZED] = { + .callback = extract_qctx, + .callback_data = qctxp, + }, + }; + + REQUIRE(client != NULL); + REQUIRE(qctxp != NULL); + REQUIRE(*qctxp == NULL); + + /* + * Call ns_query_start() to initialize a query context for given + * client, but first hook into query_setup() so that we can just + * extract an initialized query context, without kicking off any + * further processing. Make sure we do not overwrite any previously + * set hooks. + */ + saved_hook_table = ns__hook_table; + ns__hook_table = query_hooks; + ns_query_start(client); + ns__hook_table = saved_hook_table; + + if (*qctxp == NULL) { + return (ISC_R_NOMEMORY); + } else { + return (ISC_R_SUCCESS); + } +} + +isc_result_t +ns_test_qctx_create(const ns_test_qctx_create_params_t *params, + query_ctx_t **qctxp) +{ + ns_client_t *client = NULL; + isc_result_t result; + + REQUIRE(params != NULL); + REQUIRE(params->qname != NULL); + REQUIRE(qctxp != NULL); + REQUIRE(*qctxp == NULL); + + /* + * Allocate and initialize a client structure. + */ + result = ns_test_getclient(NULL, ISC_FALSE, &client); + if (result != ISC_R_SUCCESS) { + return (result); + } + TIME_NOW(&client->tnow); + + /* + * Every client needs to belong to a view. + */ + result = ns_test_makeview("view", params->with_cache, &client->view); + if (result != ISC_R_SUCCESS) { + goto detach_client; + } + + /* + * Synthesize a DNS query using given QNAME, QTYPE and flags, storing + * it in client->message. + */ + result = attach_query_msg_to_client(client, params->qname, + params->qtype, params->qflags); + if (result != ISC_R_SUCCESS) { + goto detach_client; + } + + /* + * Allow recursion for the client. As NS_CLIENTATTR_RA normally gets + * set in ns__client_request(), i.e. earlier than the unit tests hook + * into the call chain, just set it manually. + */ + client->attributes |= NS_CLIENTATTR_RA; + + /* + * Create a query context for a client sending the previously + * synthesized query. + */ + result = create_qctx_for_client(client, qctxp); + if (result != ISC_R_SUCCESS) { + goto destroy_query; + } + + /* + * Reference count for "client" is now at 2, so decrement it in order + * for it to drop to zero when "qctx" gets destroyed. + */ + ns_client_detach(&client); + + return (ISC_R_SUCCESS); + +destroy_query: + dns_message_destroy(&client->message); +detach_client: + ns_client_detach(&client); + + return (result); +} + +void +ns_test_qctx_destroy(query_ctx_t **qctxp) { + query_ctx_t *qctx; + + REQUIRE(qctxp != NULL); + REQUIRE(*qctxp != NULL); + + qctx = *qctxp; + + ns_client_detach(&qctx->client); + + if (qctx->zone != NULL) { + dns_zone_detach(&qctx->zone); + } + if (qctx->db != NULL) { + dns_db_detach(&qctx->db); + } + + isc_mem_put(mctx, qctx, sizeof(*qctx)); + *qctxp = NULL; +} + +isc_boolean_t +ns_test_hook_catch_call(void *hook_data, void *callback_data, + isc_result_t *resultp) +{ + UNUSED(hook_data); + UNUSED(callback_data); + + *resultp = ISC_R_UNSET; + + return (ISC_TRUE); +} + /* * Sleep for 'usec' microseconds. */ diff --git a/lib/ns/tests/nstest.h b/lib/ns/tests/nstest.h index 9b7f37cb2a..5d74deaabb 100644 --- a/lib/ns/tests/nstest.h +++ b/lib/ns/tests/nstest.h @@ -26,6 +26,13 @@ #include #include +typedef struct ns_test_id { + const char *description; + int lineno; +} ns_test_id_t; + +#define NS_TEST_ID(desc) { .description = desc, .lineno = __LINE__ } + #define CHECK(r) \ do { \ result = (r); \ @@ -55,8 +62,13 @@ ns_test_begin(FILE *logfile, isc_boolean_t create_managers); void ns_test_end(void); +/*% + * Create a view. If "with_cache" is set to ISC_TRUE, a cache database will + * also be created and attached to the created view. + */ isc_result_t -ns_test_makeview(const char *name, dns_view_t **viewp); +ns_test_makeview(const char *name, isc_boolean_t with_cache, + dns_view_t **viewp); isc_result_t ns_test_makezone(const char *name, dns_zone_t **zonep, dns_view_t *view, @@ -74,6 +86,21 @@ ns_test_releasezone(dns_zone_t *zone); void ns_test_closezonemgr(void); +/*% + * Load data for zone "zonename" from file "filename" and start serving it to + * clients matching "view". Only one zone loaded using this function can be + * served at any given time. + */ +isc_result_t +ns_test_serve_zone(const char *zonename, const char *filename, + dns_view_t *view); + +/*% + * Release the zone loaded by ns_test_serve_zone(). + */ +void +ns_test_cleanup_zone(void); + void ns_test_nap(isc_uint32_t usec); @@ -88,3 +115,37 @@ ns_test_getdata(const char *file, unsigned char *buf, isc_result_t ns_test_getclient(ns_interface_t *ifp0, isc_boolean_t tcp, ns_client_t **clientp); + +/*% + * Structure containing parameters for ns_test_qctx_create(). + */ +typedef struct ns_test_qctx_create_params { + const char *qname; + dns_rdatatype_t qtype; + unsigned int qflags; + isc_boolean_t with_cache; +} ns_test_qctx_create_params_t; + +/*% + * Prepare a query context identical with one that would be prepared if a query + * with given QNAME, QTYPE and flags was received from a client. Recursion is + * assumed to be allowed for this client. If "with_cache" is set to ISC_TRUE, + * a cache database will be created and associated with the view matching the + * incoming query. + */ +isc_result_t +ns_test_qctx_create(const ns_test_qctx_create_params_t *params, + query_ctx_t **qctxp); + +/*% + * Destroy a query context created by ns_test_qctx_create(). + */ +void +ns_test_qctx_destroy(query_ctx_t **qctxp); + +/*% + * A hook callback interrupting execution at given hook's insertion point. + */ +isc_boolean_t +ns_test_hook_catch_call(void *hook_data, void *callback_data, + isc_result_t *resultp); diff --git a/lib/ns/tests/query_test.c b/lib/ns/tests/query_test.c new file mode 100644 index 0000000000..7906989e54 --- /dev/null +++ b/lib/ns/tests/query_test.c @@ -0,0 +1,574 @@ +/* + * Copyright (C) 2017 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/. + */ + +/*! \file */ + +#include + +#include + +#include +#include +#include +#include +#include + +#include "../hooks.h" + +#include "nstest.h" + +/***** + ***** ns__query_sfcache() tests + *****/ + +/*% + * Structure containing parameters for ns__query_sfcache_test(). + */ +typedef struct { + const ns_test_id_t id; /* libns test identifier */ + unsigned int qflags; /* query flags */ + isc_boolean_t cache_entry_present; /* whether a SERVFAIL cache entry + matching the query should be + present */ + isc_uint32_t cache_entry_flags; /* NS_FAILCACHE_* flags to set for + the SERVFAIL cache entry */ + isc_boolean_t servfail_expected; /* whether a cached SERVFAIL is + expected to be returned */ +} ns__query_sfcache_test_params_t; + +/*% + * Perform a single ns__query_sfcache() check using given parameters. + */ +static void +ns__query_sfcache_test(const ns__query_sfcache_test_params_t *test) { + query_ctx_t *qctx = NULL; + isc_result_t result; + + REQUIRE(test != NULL); + REQUIRE(test->id.description != NULL); + REQUIRE(test->cache_entry_present == ISC_TRUE || + test->cache_entry_flags == 0); + + /* + * Interrupt execution if query_done() is called. + */ + const ns_hook_t query_hooks[NS_QUERY_HOOKS_COUNT] = { + [NS_QUERY_DONE_BEGIN] = { + .callback = ns_test_hook_catch_call, + .callback_data = NULL, + }, + }; + ns__hook_table = query_hooks; + + /* + * Construct a query context for a ./NS query with given flags. + */ + { + const ns_test_qctx_create_params_t qctx_params = { + .qname = ".", + .qtype = dns_rdatatype_ns, + .qflags = test->qflags, + .with_cache = ISC_TRUE, + }; + + result = ns_test_qctx_create(&qctx_params, &qctx); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + } + + /* + * If this test wants a SERVFAIL cache entry matching the query to + * exist, create it. + */ + if (test->cache_entry_present) { + isc_interval_t hour; + isc_time_t expire; + + isc_interval_set(&hour, 3600, 0); + result = isc_time_nowplusinterval(&expire, &hour); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + + dns_badcache_add(qctx->client->view->failcache, dns_rootname, + dns_rdatatype_ns, ISC_TRUE, + test->cache_entry_flags, &expire); + } + + /* + * Check whether ns__query_sfcache() behaves as expected. + */ + ns__query_sfcache(qctx); + + if (test->servfail_expected) { + ATF_CHECK_EQ_MSG(qctx->result, DNS_R_SERVFAIL, + "test \"%s\" on line %d: " + "expected SERVFAIL, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } else { + ATF_CHECK_EQ_MSG(qctx->result, ISC_R_SUCCESS, + "test \"%s\" on line %d: " + "expected success, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + } + + /* + * Clean up. + */ + ns_test_qctx_destroy(&qctx); + +} + +ATF_TC(ns__query_sfcache); +ATF_TC_HEAD(ns__query_sfcache, tc) { + atf_tc_set_md_var(tc, "descr", "ns__query_sfcache()"); +} +ATF_TC_BODY(ns__query_sfcache, tc) { + isc_result_t result; + size_t i; + + const ns__query_sfcache_test_params_t tests[] = { + /* + * Sanity check for an empty SERVFAIL cache. + */ + { + NS_TEST_ID("query: RD=1, CD=0; cache: empty"), + .qflags = DNS_MESSAGEFLAG_RD, + .cache_entry_present = ISC_FALSE, + .servfail_expected = ISC_FALSE, + }, + /* + * Query: RD=1, CD=0. Cache entry: CD=0. Should SERVFAIL. + */ + { + NS_TEST_ID("query: RD=1, CD=0; cache: CD=0"), + .qflags = DNS_MESSAGEFLAG_RD, + .cache_entry_present = ISC_TRUE, + .cache_entry_flags = 0, + .servfail_expected = ISC_TRUE, + }, + /* + * Query: RD=1, CD=1. Cache entry: CD=0. Should not SERVFAIL: + * failed validation should not influence CD=1 queries. + */ + { + NS_TEST_ID("query: RD=1, CD=1; cache: CD=0"), + .qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD, + .cache_entry_present = ISC_TRUE, + .cache_entry_flags = 0, + .servfail_expected = ISC_FALSE, + }, + /* + * Query: RD=1, CD=1. Cache entry: CD=1. Should SERVFAIL: + * SERVFAIL responses elicited by CD=1 queries can be + * "replayed" for other CD=1 queries during the lifetime of the + * SERVFAIL cache entry. + */ + { + NS_TEST_ID("query: RD=1, CD=1; cache: CD=1"), + .qflags = DNS_MESSAGEFLAG_RD | DNS_MESSAGEFLAG_CD, + .cache_entry_present = ISC_TRUE, + .cache_entry_flags = NS_FAILCACHE_CD, + .servfail_expected = ISC_TRUE, + }, + /* + * Query: RD=1, CD=0. Cache entry: CD=1. Should SERVFAIL: if + * a CD=1 query elicited a SERVFAIL, a CD=0 query for the same + * QNAME and QTYPE will SERVFAIL as well. + */ + { + NS_TEST_ID("query: RD=1, CD=0; cache: CD=1"), + .qflags = DNS_MESSAGEFLAG_RD, + .cache_entry_present = ISC_TRUE, + .cache_entry_flags = NS_FAILCACHE_CD, + .servfail_expected = ISC_TRUE, + }, + /* + * Query: RD=0, CD=0. Cache entry: CD=0. Should not SERVFAIL + * despite a matching entry being present as the SERVFAIL cache + * should not be consulted for non-recursive queries. + */ + { + NS_TEST_ID("query: RD=0, CD=0; cache: CD=0"), + .qflags = 0, + .cache_entry_present = ISC_TRUE, + .cache_entry_flags = 0, + .servfail_expected = ISC_FALSE, + }, + }; + + UNUSED(tc); + + result = ns_test_begin(NULL, ISC_TRUE); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + + for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { + ns__query_sfcache_test(&tests[i]); + } + + ns_test_end(); +} + +/***** + ***** ns__query_start() tests + *****/ + +/*% + * Structure containing parameters for ns__query_start_test(). + */ +typedef struct { + const ns_test_id_t id; /* libns test identifier */ + const char *qname; /* QNAME */ + dns_rdatatype_t qtype; /* QTYPE */ + unsigned int qflags; /* query flags */ + isc_boolean_t disable_name_checks; /* if set to ISC_TRUE, owner name + checks will be disabled for the + view created */ + isc_boolean_t recursive_service; /* if set to ISC_TRUE, the view + created will have a cache + database attached */ + const char *auth_zone_origin; /* origin name of the zone the + created view will be + authoritative for */ + const char *auth_zone_path; /* path to load the authoritative + zone from */ + enum { /* expected result: */ + NS__QUERY_START_R_INVALID, + NS__QUERY_START_R_REFUSE, /* query should be REFUSED */ + NS__QUERY_START_R_CACHE, /* query should be answered from + cache */ + NS__QUERY_START_R_AUTH, /* query should be answered using + authoritative data */ + } expected_result; +} ns__query_start_test_params_t; + +/*% + * Perform a single ns__query_start() check using given parameters. + */ +static void +ns__query_start_test(const ns__query_start_test_params_t *test) { + query_ctx_t *qctx = NULL; + isc_result_t result; + + REQUIRE(test != NULL); + REQUIRE(test->id.description != NULL); + REQUIRE((test->auth_zone_origin == NULL && + test->auth_zone_path == NULL) || + (test->auth_zone_origin != NULL && + test->auth_zone_path != NULL)); + + /* + * Interrupt execution if query_lookup() or query_done() is called. + */ + const ns_hook_t query_hooks[NS_QUERY_HOOKS_COUNT] = { + [NS_QUERY_LOOKUP_BEGIN] = { + .callback = ns_test_hook_catch_call, + .callback_data = NULL, + }, + [NS_QUERY_DONE_BEGIN] = { + .callback = ns_test_hook_catch_call, + .callback_data = NULL, + }, + }; + ns__hook_table = query_hooks; + + /* + * Construct a query context using the supplied parameters. + */ + { + const ns_test_qctx_create_params_t qctx_params = { + .qname = test->qname, + .qtype = test->qtype, + .qflags = test->qflags, + .with_cache = test->recursive_service, + }; + result = ns_test_qctx_create(&qctx_params, &qctx); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + } + + /* + * Enable view->checknames by default, disable if requested. + */ + qctx->client->view->checknames = !test->disable_name_checks; + + /* + * Load zone from file and attach it to the client's view, if + * requested. + */ + if (test->auth_zone_path != NULL) { + result = ns_test_serve_zone(test->auth_zone_origin, + test->auth_zone_path, + qctx->client->view); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + } + + /* + * Check whether ns__query_start() behaves as expected. + */ + ns__query_start(qctx); + + switch (test->expected_result) { + case NS__QUERY_START_R_REFUSE: + ATF_CHECK_EQ_MSG(qctx->result, DNS_R_REFUSED, + "test \"%s\" on line %d: " + "expected REFUSED, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + ATF_CHECK_EQ_MSG(qctx->zone, NULL, + "test \"%s\" on line %d: " + "no zone was expected to be attached to " + "query context, but some was", + test->id.description, test->id.lineno); + ATF_CHECK_EQ_MSG(qctx->db, NULL, + "test \"%s\" on line %d: " + "no database was expected to be attached to " + "query context, but some was", + test->id.description, test->id.lineno); + break; + case NS__QUERY_START_R_CACHE: + ATF_CHECK_EQ_MSG(qctx->result, ISC_R_SUCCESS, + "test \"%s\" on line %d: " + "expected success, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + ATF_CHECK_EQ_MSG(qctx->zone, NULL, + "test \"%s\" on line %d: " + "no zone was expected to be attached to " + "query context, but some was", + test->id.description, test->id.lineno); + ATF_CHECK_MSG((qctx->db != NULL && + qctx->db == qctx->client->view->cachedb), + "test \"%s\" on line %d: " + "cache database was expected to be attached to " + "query context, but it was not", + test->id.description, test->id.lineno); + break; + case NS__QUERY_START_R_AUTH: + ATF_CHECK_EQ_MSG(qctx->result, ISC_R_SUCCESS, + "test \"%s\" on line %d: " + "expected success, got %s", + test->id.description, test->id.lineno, + isc_result_totext(qctx->result)); + ATF_CHECK_MSG(qctx->zone != NULL, + "test \"%s\" on line %d: " + "a zone was expected to be attached to query " + "context, but it was not", + test->id.description, test->id.lineno); + ATF_CHECK_MSG((qctx->db != NULL && + qctx->db != qctx->client->view->cachedb), + "test \"%s\" on line %d: " + "cache database was not expected to be attached " + "to query context, but it is", + test->id.description, test->id.lineno); + break; + case NS__QUERY_START_R_INVALID: + ATF_REQUIRE_MSG(ISC_FALSE, + "test \"%s\" on line %d has no expected " + "result set", + test->id.description, test->id.lineno); + break; + default: + INSIST(0); + break; + } + + /* + * Clean up. + */ + if (test->auth_zone_path != NULL) { + ns_test_cleanup_zone(); + } + ns_test_qctx_destroy(&qctx); +} + +ATF_TC(ns__query_start); +ATF_TC_HEAD(ns__query_start, tc) { + atf_tc_set_md_var(tc, "descr", "ns__query_start()"); +} +ATF_TC_BODY(ns__query_start, tc) { + isc_result_t result; + size_t i; + + const ns__query_start_test_params_t tests[] = { + /* + * Recursive foo/A query to a server without recursive service + * and no zones configured. Query should be REFUSED. + */ + { + NS_TEST_ID("foo/A, no cache, no auth"), + .qname = "foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = ISC_FALSE, + .expected_result = NS__QUERY_START_R_REFUSE, + }, + /* + * Recursive foo/A query to a server with recursive service and + * no zones configured. Query should be answered from cache. + */ + { + NS_TEST_ID("foo/A, cache, no auth"), + .qname = "foo", + .qtype = dns_rdatatype_a, + .recursive_service = ISC_TRUE, + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Recursive foo/A query to a server with recursive service and + * zone "foo" configured. Query should be answered from + * authoritative data. + */ + { + NS_TEST_ID("foo/A, RD=1, cache, auth for foo"), + .qname = "foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = ISC_TRUE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Recursive bar/A query to a server without recursive service + * and zone "foo" configured. Query should be REFUSED. + */ + { + NS_TEST_ID("bar/A, RD=1, no cache, auth for foo"), + .qname = "bar", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = ISC_FALSE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_REFUSE, + }, + /* + * Recursive bar/A query to a server with recursive service and + * zone "foo" configured. Query should be answered from + * cache. + */ + { + NS_TEST_ID("bar/A, RD=1, cache, auth for foo"), + .qname = "bar", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = ISC_TRUE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Recursive bar.foo/DS query to a server with recursive + * service and zone "foo" configured. Query should be answered + * from authoritative data. + */ + { + NS_TEST_ID("bar.foo/DS, RD=1, cache, auth for foo"), + .qname = "bar.foo", + .qtype = dns_rdatatype_ds, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = ISC_TRUE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Non-recursive bar.foo/DS query to a server with recursive + * service and zone "foo" configured. Query should be answered + * from authoritative data. + */ + { + NS_TEST_ID("bar.foo/DS, RD=0, cache, auth for foo"), + .qname = "bar.foo", + .qtype = dns_rdatatype_ds, + .qflags = 0, + .recursive_service = ISC_TRUE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Recursive foo/DS query to a server with recursive service + * and zone "foo" configured. Query should be answered from + * cache. + */ + { + NS_TEST_ID("foo/DS, RD=1, cache, auth for foo"), + .qname = "foo", + .qtype = dns_rdatatype_ds, + .qflags = DNS_MESSAGEFLAG_RD, + .recursive_service = ISC_TRUE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Non-recursive foo/DS query to a server with recursive + * service and zone "foo" configured. Query should be answered + * from authoritative data. + */ + { + NS_TEST_ID("foo/DS, RD=0, cache, auth for foo"), + .qname = "foo", + .qtype = dns_rdatatype_ds, + .qflags = 0, + .recursive_service = ISC_TRUE, + .auth_zone_origin = "foo", + .auth_zone_path = "testdata/query/foo.db", + .expected_result = NS__QUERY_START_R_AUTH, + }, + /* + * Recursive _foo/A query to a server with recursive service, + * no zones configured and owner name checks disabled. Query + * should be answered from cache. + */ + { + NS_TEST_ID("_foo/A, cache, no auth, name checks off"), + .qname = "_foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .disable_name_checks = ISC_TRUE, + .recursive_service = ISC_TRUE, + .expected_result = NS__QUERY_START_R_CACHE, + }, + /* + * Recursive _foo/A query to a server with recursive service, + * no zones configured and owner name checks enabled. Query + * should be REFUSED. + */ + { + NS_TEST_ID("_foo/A, cache, no auth, name checks on"), + .qname = "_foo", + .qtype = dns_rdatatype_a, + .qflags = DNS_MESSAGEFLAG_RD, + .disable_name_checks = ISC_FALSE, + .recursive_service = ISC_TRUE, + .expected_result = NS__QUERY_START_R_REFUSE, + }, + }; + + UNUSED(tc); + + result = ns_test_begin(NULL, ISC_TRUE); + ATF_REQUIRE_EQ(result, ISC_R_SUCCESS); + + for (i = 0; i < sizeof(tests) / sizeof(tests[0]); i++) { + ns__query_start_test(&tests[i]); + } + + ns_test_end(); +} + +/* + * Main + */ +ATF_TP_ADD_TCS(tp) { + ATF_TP_ADD_TC(tp, ns__query_sfcache); + ATF_TP_ADD_TC(tp, ns__query_start); + + return (atf_no_error()); +} diff --git a/lib/ns/tests/testdata/query/foo.db b/lib/ns/tests/testdata/query/foo.db new file mode 100644 index 0000000000..20042a6e20 --- /dev/null +++ b/lib/ns/tests/testdata/query/foo.db @@ -0,0 +1,15 @@ +; Copyright (C) 2017 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/. + +$TTL 3600 +@ IN SOA localhost. postmaster.localhost. ( + 1 ;serial + 3600 ;refresh + 1800 ;retry + 604800 ;expiration + 3600 ) ;minimum + IN NS ns +ns IN A 127.0.0.1