/*
 * Copyright (C) Internet Systems Consortium, Inc. ("ISC")
 *
 * SPDX-License-Identifier: MPL-2.0
 *
 * 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 */

#include <ctype.h>
#include <inttypes.h>
#include <stdbool.h>
#include <stdint.h>
#include <string.h>

#include <isc/async.h>
#include <isc/counter.h>
#include <isc/hex.h>
#include <isc/list.h>
#include <isc/log.h>
#include <isc/mem.h>
#include <isc/once.h>
#include <isc/random.h>
#include <isc/result.h>
#include <isc/rwlock.h>
#include <isc/serial.h>
#include <isc/stats.h>
#include <isc/string.h>
#include <isc/thread.h>
#include <isc/util.h>

#include <dns/adb.h>
#include <dns/badcache.h>
#include <dns/byaddr.h>
#include <dns/cache.h>
#include <dns/db.h>
#include <dns/dlz.h>
#include <dns/dns64.h>
#include <dns/dnssec.h>
#include <dns/ede.h>
#include <dns/keytable.h>
#include <dns/message.h>
#include <dns/ncache.h>
#include <dns/nsec.h>
#include <dns/nsec3.h>
#include <dns/order.h>
#include <dns/rcode.h>
#include <dns/rdata.h>
#include <dns/rdataclass.h>
#include <dns/rdatalist.h>
#include <dns/rdataset.h>
#include <dns/rdatasetiter.h>
#include <dns/rdatastruct.h>
#include <dns/rdatatype.h>
#include <dns/resolver.h>
#include <dns/result.h>
#include <dns/stats.h>
#include <dns/tkey.h>
#include <dns/types.h>
#include <dns/view.h>
#include <dns/zone.h>
#include <dns/zt.h>

#include <ns/client.h>
#include <ns/hooks.h>
#include <ns/interfacemgr.h>
#include <ns/server.h>
#include <ns/stats.h>
#include <ns/xfrout.h>

#include "probes.h"

#if 0
/*
 * It has been recommended that DNS64 be changed to return excluded
 * AAAA addresses if DNS64 synthesis does not occur.  This minimises
 * the impact on the lookup results.  While most DNS AAAA lookups are
 * done to send IP packets to a host, not all of them are and filtering
 * excluded addresses has a negative impact on those uses.
 */
#define dns64_bis_return_excluded_addresses 1
#endif /* if 0 */

#define QUERY_ERROR(qctx, r)                  \
	do {                                  \
		(qctx)->result = r;           \
		(qctx)->want_restart = false; \
		(qctx)->line = __LINE__;      \
	} while (0)

/*% Partial answer? */
#define PARTIALANSWER(c) \
	(((c)->query.attributes & NS_QUERYATTR_PARTIALANSWER) != 0)
/*% Use Cache? */
#define USECACHE(c) (((c)->query.attributes & NS_QUERYATTR_CACHEOK) != 0)
/*% Recursion OK? */
#define RECURSIONOK(c) (((c)->query.attributes & NS_QUERYATTR_RECURSIONOK) != 0)
/*% Recursing? */
#define RECURSING(c) (((c)->query.attributes & NS_QUERYATTR_RECURSING) != 0)
/*% Want Recursion? */
#define WANTRECURSION(c) \
	(((c)->query.attributes & NS_QUERYATTR_WANTRECURSION) != 0)
/*% Is TCP? */
#define TCP(c) (((c)->inner.attributes & NS_CLIENTATTR_TCP) != 0)
/*% This query needs to have been sent over TCP.  Return TC=1. */
#define NEEDTCP(c) (((c)->inner.attributes & NS_CLIENTATTR_NEEDTCP) != 0)

/*% Want DNSSEC? */
#define WANTDNSSEC(c) (((c)->inner.attributes & NS_CLIENTATTR_WANTDNSSEC) != 0)
/*% Want WANTAD? */
#define WANTAD(c) (((c)->inner.attributes & NS_CLIENTATTR_WANTAD) != 0)
/*% Client presented a bad COOKIE. */
#define BADCOOKIE(c) (((c)->inner.attributes & NS_CLIENTATTR_BADCOOKIE) != 0)
/*% Client presented a valid COOKIE. */
#define HAVECOOKIE(c) (((c)->inner.attributes & NS_CLIENTATTR_HAVECOOKIE) != 0)
/*% Client presented a COOKIE. */
#define WANTCOOKIE(c) (((c)->inner.attributes & NS_CLIENTATTR_WANTCOOKIE) != 0)
/*% Client presented a CLIENT-SUBNET option. */
#define HAVEECS(c) (((c)->inner.attributes & NS_CLIENTATTR_HAVEECS) != 0)
/*% No authority? */
#define NOAUTHORITY(c) (((c)->query.attributes & NS_QUERYATTR_NOAUTHORITY) != 0)
/*% No additional? */
#define NOADDITIONAL(c) \
	(((c)->query.attributes & NS_QUERYATTR_NOADDITIONAL) != 0)
/*% Secure? */
#define SECURE(c) (((c)->query.attributes & NS_QUERYATTR_SECURE) != 0)
/*% DNS64 A lookup? */
#define DNS64(c) (((c)->query.attributes & NS_QUERYATTR_DNS64) != 0)

#define DNS64EXCLUDE(c) \
	(((c)->query.attributes & NS_QUERYATTR_DNS64EXCLUDE) != 0)

#define REDIRECT(c) (((c)->query.attributes & NS_QUERYATTR_REDIRECT) != 0)

/*% Was the client already sent a response? */
#define QUERY_ANSWERED(q) (((q)->attributes & NS_QUERYATTR_ANSWERED) != 0)

/*% Does the query wants to check for stale RRset due to a timeout? */
#define QUERY_STALETIMEOUT(q) (((q)->dboptions & DNS_DBFIND_STALETIMEOUT) != 0)

/*% Does the rdataset 'r' have an attached 'No QNAME Proof'? */
#define NOQNAME(r) ((r)->attributes.noqname)

/*% Does the rdataset 'r' contain a stale answer? */
#define STALE(r) ((r)->attributes.stale)

/*% Does the rdataset 'r' is stale and within stale-refresh-time? */
#define STALE_WINDOW(r) ((r)->attributes.stale_window)

#ifdef WANT_QUERYTRACE
static void
client_trace(ns_client_t *client, int level, const char *message) {
	if (client != NULL && client->query.qname != NULL) {
		if (isc_log_wouldlog(level)) {
			char qbuf[DNS_NAME_FORMATSIZE];
			char tbuf[DNS_RDATATYPE_FORMATSIZE];
			dns_name_format(client->query.qname, qbuf,
					sizeof(qbuf));
			dns_rdatatype_format(client->query.qtype, tbuf,
					     sizeof(tbuf));
			isc_log_write(NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY,
				      level,
				      "query client=%p thread=0x%" PRIxPTR
				      "(%s/%s): %s",
				      client, isc_thread_self(), qbuf, tbuf,
				      message);
		}
	} else {
		isc_log_write(NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY, level,
			      "query client=%p thread=0x%" PRIxPTR
			      "(<unknown-query>): %s",
			      client, isc_thread_self(), message);
	}
}
#define CTRACE(l, m)  client_trace(client, l, m)
#define CCTRACE(l, m) client_trace(qctx->client, l, m)
#else /* ifdef WANT_QUERYTRACE */
#define CTRACE(l, m)  ((void)m)
#define CCTRACE(l, m) ((void)m)
#endif /* WANT_QUERYTRACE */

#define PENDINGOK(x) (((x) & DNS_DBFIND_PENDINGOK) != 0)

#define SFCACHE_CDFLAG 0x1

/*
 * SAVE and RESTORE have the same semantics as:
 *
 * 	foo_attach(b, &a);
 *	foo_detach(&b);
 *
 * without the locking and magic testing.
 *
 * 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 {                       \
		INSIST(a == NULL); \
		a = b;             \
		b = NULL;          \
	} while (0)
#define RESTORE(a, b) SAVE(a, b)

static bool
validate(ns_client_t *client, dns_db_t *db, dns_name_t *name,
	 dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset);

static void
query_findclosestnsec3(dns_name_t *qname, dns_db_t *db,
		       dns_dbversion_t *version, ns_client_t *client,
		       dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset,
		       dns_name_t *fname, bool exact, dns_name_t *found);

static void
log_queryerror(ns_client_t *client, isc_result_t result, int line, int level);

static void
rpz_st_clear(ns_client_t *client);

static bool
rpz_ck_dnssec(ns_client_t *client, isc_result_t qresult,
	      dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset);

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);

static isc_result_t
acquire_recursionquota(ns_client_t *client);

static void
release_recursionquota(ns_client_t *client);

/*
 * Return the hooktable in use with 'qctx', or if there isn't one
 * set, return the default hooktable.
 */
static ns_hooktable_t *
get_hooktab(query_ctx_t *qctx) {
	if (qctx == NULL || qctx->view == NULL || qctx->view->hooktable == NULL)
	{
		return ns__hook_table;
	}

	return qctx->view->hooktable;
}

/*
 * Call the specified hook function in every configured module that implements
 * that function. If any hook function returns NS_HOOK_RETURN, we
 * set 'result' and terminate processing by jumping to the 'cleanup' tag.
 *
 * (Note that a hook function may set the 'result' to ISC_R_SUCCESS but
 * still terminate processing within the calling function. That's why this
 * is a macro instead of a static function; it needs to be able to use
 * 'goto cleanup' regardless of the return value.)
 */
#define CALL_HOOK(_id, _qctx)                                       \
	do {                                                        \
		isc_result_t _res = result;                         \
		ns_hooktable_t *_tab = get_hooktab(_qctx);          \
		ns_hook_t *_hook;                                   \
		_hook = ISC_LIST_HEAD((*_tab)[_id]);                \
		while (_hook != NULL) {                             \
			ns_hook_action_t _func = _hook->action;     \
			void *_data = _hook->action_data;           \
			INSIST(_func != NULL);                      \
			switch (_func(_qctx, _data, &_res)) {       \
			case NS_HOOK_CONTINUE:                      \
				_hook = ISC_LIST_NEXT(_hook, link); \
				break;                              \
			case NS_HOOK_RETURN:                        \
				result = _res;                      \
				goto cleanup;                       \
			default:                                    \
				UNREACHABLE();                      \
			}                                           \
		}                                                   \
	} while (false)

/*
 * Call the specified hook function in every configured module that
 * implements that function. All modules are called; hook function return
 * codes are ignored. This is intended for use with initialization and
 * destruction calls which *must* run in every configured module.
 *
 * (This could be implemented as a static void function, but is left as a
 * macro for symmetry with CALL_HOOK above.)
 */
#define CALL_HOOK_NORETURN(_id, _qctx)                          \
	do {                                                    \
		isc_result_t _res;                              \
		ns_hooktable_t *_tab = get_hooktab(_qctx);      \
		ns_hook_t *_hook;                               \
		_hook = ISC_LIST_HEAD((*_tab)[_id]);            \
		while (_hook != NULL) {                         \
			ns_hook_action_t _func = _hook->action; \
			void *_data = _hook->action_data;       \
			INSIST(_func != NULL);                  \
			_func(_qctx, _data, &_res);             \
			_hook = ISC_LIST_NEXT(_hook, link);     \
		}                                               \
	} while (false)

/*
 * 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 <ns/query.h> 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 (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.
 *
 * 4. If recursion or cache access are allowed, search the cache
 *    (query_lookup() again, using the cache database) to find a better
 *    answer. If an answer is found, go to 7.
 *
 * 5. If recursion is allowed, begin recursion (ns_query_recurse()).
 *    Go to 15 to clean up this phase of the query. When recursion
 *    is complete, processing will resume at 6.
 *
 * 6. Resume from recursion; set up query context for resumed processing.
 *
 * 7. Determine what sort of answer we've found (query_gotanswer())
 *    and call other functions accordingly:
 *      - not found (auth or cache), go to 8
 *      - delegation, go to 9
 *      - no such domain (auth), go to 10
 *      - empty answer (auth), go to 11
 *      - negative response (cache), go to 12
 *      - answer found, go to 13
 *
 * 8. The answer was not found in the database (query_notfound().
 *    Set up a referral and go to 9.
 *
 * 9. Handle a delegation response (query_delegation()). If we need
 *    to and are allowed to recurse (query_delegation_recurse()), go to 5,
 *    otherwise go to 15 to clean up and return the delegation to the client.
 *
 * 10. No such domain (query_nxdomain()). Attempt redirection; if
 *     unsuccessful, add authority section records (query_addsoa(),
 *     query_addauth()), then go to 15 to return NXDOMAIN to client.
 *
 * 11. Empty answer (query_nodata()). Add authority section records
 *     (query_addsoa(), query_addauth()) and signatures if authoritative
 *     (query_sign_nodata()) then go to 15 and return
 *     NOERROR/ANCOUNT=0 to client.
 *
 * 12. No such domain or empty answer returned from cache (query_ncache()).
 *     Set response code appropriately, go to 11.
 *
 * 13. Prepare a response (query_prepresponse()) and then fill it
 *     appropriately (query_respond(), or for type ANY,
 *     query_respond_any()).
 *
 * 14. If a restart is needed due to CNAME/DNAME chaining, go to 2.
 *
 * 15. Clean up resources. If recursing, stop and wait for the event
 *     handler to be called back (step 6).  If an answer is ready,
 *     return it to the client.
 *
 * (XXX: This description omits several special cases including
 * DNS64, RPZ, RRL, and the SERVFAIL cache. It also doesn't discuss
 * plugins.)
 */

static void
query_trace(query_ctx_t *qctx);

static void
qctx_init(ns_client_t *client, dns_fetchresponse_t **respp,
	  dns_rdatatype_t qtype, query_ctx_t *qctx);

static isc_result_t
qctx_prepare_buffers(query_ctx_t *qctx, isc_buffer_t *buffer);

static void
qctx_freedata(query_ctx_t *qctx);

static void
qctx_destroy(query_ctx_t *qctx);

static void
query_setup(ns_client_t *client, dns_rdatatype_t qtype);

static isc_result_t
query_lookup(query_ctx_t *qctx);

static void
fetch_callback(void *arg);

static void
recparam_update(ns_query_recparam_t *param, dns_rdatatype_t qtype,
		const dns_name_t *qname, const dns_name_t *qdomain);

static isc_result_t
query_resume(query_ctx_t *qctx);

static isc_result_t
query_checkrrl(query_ctx_t *qctx, isc_result_t result);

static isc_result_t
query_checkrpz(query_ctx_t *qctx, isc_result_t result);

static isc_result_t
query_rpzcname(query_ctx_t *qctx, dns_name_t *cname);

static isc_result_t
query_gotanswer(query_ctx_t *qctx, isc_result_t result);

static void
query_addnoqnameproof(query_ctx_t *qctx);

static isc_result_t
query_respond_any(query_ctx_t *qctx);

static isc_result_t
query_respond(query_ctx_t *qctx);

static isc_result_t
query_dns64(query_ctx_t *qctx);

static void
query_filter64(query_ctx_t *qctx);

static isc_result_t
query_notfound(query_ctx_t *qctx);

static isc_result_t
query_zone_delegation(query_ctx_t *qctx);

static isc_result_t
query_delegation(query_ctx_t *qctx);

static isc_result_t
query_delegation_recurse(query_ctx_t *qctx);

static void
query_addds(query_ctx_t *qctx);

static isc_result_t
query_nodata(query_ctx_t *qctx, isc_result_t result);

static isc_result_t
query_sign_nodata(query_ctx_t *qctx);

static void
query_addnxrrsetnsec(query_ctx_t *qctx);

static isc_result_t
query_nxdomain(query_ctx_t *qctx, isc_result_t result);

static isc_result_t
query_redirect(query_ctx_t *qctx, isc_result_t result);

static isc_result_t
query_ncache(query_ctx_t *qctx, isc_result_t result);

static isc_result_t
query_coveringnsec(query_ctx_t *qctx);

static isc_result_t
query_zerottl_refetch(query_ctx_t *qctx);

static isc_result_t
query_cname(query_ctx_t *qctx);

static isc_result_t
query_dname(query_ctx_t *qctx);

static void
query_addcname(query_ctx_t *qctx, dns_trust_t trust, dns_ttl_t ttl);

static isc_result_t
query_prepresponse(query_ctx_t *qctx);

static isc_result_t
query_addsoa(query_ctx_t *qctx, unsigned int override_ttl,
	     dns_section_t section);

static isc_result_t
query_addns(query_ctx_t *qctx);

static void
query_addbestns(query_ctx_t *qctx);

static void
query_addwildcardproof(query_ctx_t *qctx, bool ispositive, bool nodata);

static void
query_addauth(query_ctx_t *qctx);

/*
 * Increment query statistics counters.
 */
static void
inc_stats(ns_client_t *client, isc_statscounter_t counter) {
	dns_zone_t *zone = client->query.authzone;
	dns_rdatatype_t qtype;
	dns_rdataset_t *rdataset;
	isc_stats_t *zonestats;
	dns_stats_t *querystats = NULL;

	ns_stats_increment(client->manager->sctx->nsstats, counter);

	if (zone == NULL) {
		return;
	}

	/* Do regular response type stats */
	zonestats = dns_zone_getrequeststats(zone);

	if (zonestats != NULL) {
		isc_stats_increment(zonestats, counter);
	}

	/* Do query type statistics
	 *
	 * We only increment per-type if we're using the authoritative
	 * answer counter, preventing double-counting.
	 */
	if (counter == ns_statscounter_authans) {
		querystats = dns_zone_getrcvquerystats(zone);
		if (querystats != NULL) {
			rdataset = ISC_LIST_HEAD(client->query.qname->list);
			if (rdataset != NULL) {
				qtype = rdataset->type;
				dns_rdatatypestats_increment(querystats, qtype);
			}
		}
	}
}

#define NS_CLIENT_FLAGS_FORMATSIZE sizeof("+E(255)STDCV")

static inline void
ns_client_log_flags(ns_client_t *client, unsigned int flags,
		    unsigned int extflags, char *buf, size_t len) {
	isc_buffer_t b;

	isc_buffer_init(&b, buf, len);
	isc_buffer_putuint8(&b, WANTRECURSION(client) ? '+' : '-');
	if (client->inner.ednsversion >= 0) {
		char ednsbuf[sizeof("E(255)")] = { 0 };

		snprintf(ednsbuf, sizeof(ednsbuf), "E(%hhu)",
			 (unsigned char)client->inner.ednsversion);
		isc_buffer_putstr(&b, ednsbuf);
	}
	if (client->inner.signer != NULL) {
		isc_buffer_putuint8(&b, 'S');
	}
	if (TCP(client)) {
		isc_buffer_putuint8(&b, 'T');
	}
	if ((extflags & DNS_MESSAGEEXTFLAG_DO) != 0) {
		isc_buffer_putuint8(&b, 'D');
	}
	if ((flags & DNS_MESSAGEFLAG_CD) != 0) {
		isc_buffer_putuint8(&b, 'C');
	}
	if (HAVECOOKIE(client)) {
		isc_buffer_putuint8(&b, 'V');
	} else if (WANTCOOKIE(client)) {
		isc_buffer_putuint8(&b, 'K');
	}
	isc_buffer_putuint8(&b, 0);
}

#define NS_CLIENT_ECS_FORMATSIZE (DNS_ECS_FORMATSIZE + sizeof(" [ECS ]") - 1)

static inline void
ns_client_log_ecs(ns_client_t *client, char *ecsbuf, size_t len) {
	strlcpy(ecsbuf, " [ECS ", len);
	dns_ecs_format(&client->inner.ecs, ecsbuf + 6, len - 6);
	strlcat(ecsbuf, "]", len);
}

static inline void
log_response(ns_client_t *client, dns_rcode_t rcode) {
	char namebuf[DNS_NAME_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];
	char classbuf[DNS_RDATACLASS_FORMATSIZE];
	char rcodebuf[20];
	char onbuf[ISC_NETADDR_FORMATSIZE];
	char ecsbuf[NS_CLIENT_ECS_FORMATSIZE] = { 0 };
	char flagsbuf[NS_CLIENT_FLAGS_FORMATSIZE] = { 0 };
	isc_buffer_t b;
	int level = ISC_LOG_INFO;

	if (!isc_log_wouldlog(level)) {
		return;
	}

	dns_name_format(client->query.origqname, namebuf, sizeof(namebuf));
	dns_rdataclass_format(client->message->rdclass, classbuf,
			      sizeof(classbuf));
	dns_rdatatype_format(client->query.qtype, typebuf, sizeof(typebuf));
	isc_buffer_init(&b, rcodebuf, sizeof(rcodebuf));
	dns_rcode_totext(rcode, &b);
	isc_buffer_putuint8(&b, 0);
	isc_netaddr_format(&client->inner.destaddr, onbuf, sizeof(onbuf));

	if (HAVEECS(client)) {
		ns_client_log_ecs(client, ecsbuf, sizeof(ecsbuf));
	}

	ns_client_log_flags(client, client->message->flags,
			    client->inner.extflags, flagsbuf, sizeof(flagsbuf));
	ns_client_log(client, NS_LOGCATEGORY_RESPONSES, NS_LOGMODULE_QUERY,
		      level, "response: %s %s %s %s %u %u %u %s (%s)%s",
		      namebuf, classbuf, typebuf, rcodebuf,
		      client->message->counts[DNS_SECTION_ANSWER],
		      client->message->counts[DNS_SECTION_AUTHORITY],
		      client->message->counts[DNS_SECTION_ADDITIONAL], flagsbuf,
		      onbuf, ecsbuf);
}

static void
query_send(ns_client_t *client) {
	isc_statscounter_t counter;

	if ((client->message->flags & DNS_MESSAGEFLAG_AA) == 0) {
		inc_stats(client, ns_statscounter_nonauthans);
	} else {
		inc_stats(client, ns_statscounter_authans);
	}

	if (client->message->rcode == dns_rcode_noerror) {
		dns_section_t answer = DNS_SECTION_ANSWER;
		if (ISC_LIST_EMPTY(client->message->sections[answer])) {
			if (client->query.isreferral) {
				counter = ns_statscounter_referral;
			} else {
				counter = ns_statscounter_nxrrset;
			}
		} else {
			counter = ns_statscounter_success;
		}
	} else if (client->message->rcode == dns_rcode_nxdomain) {
		counter = ns_statscounter_nxdomain;
	} else if (client->message->rcode == dns_rcode_badcookie) {
		counter = ns_statscounter_badcookie;
	} else { /* We end up here in case of YXDOMAIN, and maybe others */
		counter = ns_statscounter_failure;
	}

	inc_stats(client, counter);
	ns_client_send(client);

	if ((client->manager->sctx->options & NS_SERVER_LOGRESPONSES) != 0) {
		log_response(client, client->message->rcode);
	}

	isc_nmhandle_detach(&client->inner.reqhandle);
}

static void
query_error(ns_client_t *client, isc_result_t result, int line) {
	int loglevel = ISC_LOG_DEBUG(3);
	dns_rcode_t rcode;

	rcode = dns_result_torcode(result);
	switch (rcode) {
	case dns_rcode_servfail:
		loglevel = ISC_LOG_DEBUG(1);
		inc_stats(client, ns_statscounter_servfail);
		break;
	case dns_rcode_formerr:
		inc_stats(client, ns_statscounter_formerr);
		break;
	default:
		inc_stats(client, ns_statscounter_failure);
		break;
	}

	if ((client->manager->sctx->options & NS_SERVER_LOGQUERIES) != 0) {
		loglevel = ISC_LOG_INFO;
	}

	log_queryerror(client, result, line, loglevel);

	ns_client_error(client, result);

	if (client->query.origqname != NULL &&
	    (client->manager->sctx->options & NS_SERVER_LOGRESPONSES) != 0)
	{
		log_response(client, rcode);
	}

	isc_nmhandle_detach(&client->inner.reqhandle);
}

static void
query_next(ns_client_t *client, isc_result_t result) {
	if (result == DNS_R_DUPLICATE) {
		inc_stats(client, ns_statscounter_duplicate);
	} else if (result == DNS_R_DROP) {
		inc_stats(client, ns_statscounter_dropped);
	} else {
		inc_stats(client, ns_statscounter_failure);
	}
	ns_client_drop(client, result);
	isc_nmhandle_detach(&client->inner.reqhandle);
}

static void
query_freefreeversions(ns_client_t *client, bool everything) {
	unsigned int i = 0;

	ISC_LIST_FOREACH(client->query.freeversions, dbversion, link) {
		/*
		 * If we're not freeing everything, we keep the first three
		 * dbversions structures around.
		 */
		if (i++ > 3 || everything) {
			ISC_LIST_UNLINK(client->query.freeversions, dbversion,
					link);
			isc_mem_put(client->manager->mctx, dbversion,
				    sizeof(*dbversion));
		}
	}
}

void
ns_query_cancel(ns_client_t *client) {
	REQUIRE(NS_CLIENT_VALID(client));

	LOCK(&client->query.fetchlock);
	for (int i = 0; i < RECTYPE_COUNT; i++) {
		dns_fetch_t **fetchp = &client->query.recursions[i].fetch;
		if (*fetchp != NULL) {
			dns_resolver_cancelfetch(*fetchp);
			*fetchp = NULL;
		}
	}
	if (client->query.hookactx != NULL) {
		client->query.hookactx->cancel(client->query.hookactx);
		client->query.hookactx = NULL;
	}
	UNLOCK(&client->query.fetchlock);
}

static void
query_reset(ns_client_t *client, bool everything) {
	CTRACE(ISC_LOG_DEBUG(3), "query_reset");

	/*%
	 * Reset the query state of a client to its default state.
	 */

	/*
	 * Cancel the fetch if it's running.
	 */
	ns_query_cancel(client);

	/*
	 * Cleanup any active versions.
	 */
	ISC_LIST_FOREACH(client->query.activeversions, dbversion, link) {
		dns_db_closeversion(dbversion->db, &dbversion->version, false);
		dns_db_detach(&dbversion->db);
		ISC_LIST_INITANDAPPEND(client->query.freeversions, dbversion,
				       link);
	}
	ISC_LIST_INIT(client->query.activeversions);

	if (client->query.authdb != NULL) {
		dns_db_detach(&client->query.authdb);
	}
	if (client->query.authzone != NULL) {
		dns_zone_detach(&client->query.authzone);
	}

	if (client->query.dns64_aaaa != NULL) {
		ns_client_putrdataset(client, &client->query.dns64_aaaa);
	}
	if (client->query.dns64_sigaaaa != NULL) {
		ns_client_putrdataset(client, &client->query.dns64_sigaaaa);
	}
	if (client->query.dns64_aaaaok != NULL) {
		isc_mem_cput(client->manager->mctx, client->query.dns64_aaaaok,
			     client->query.dns64_aaaaoklen, sizeof(bool));
		client->query.dns64_aaaaoklen = 0;
	}

	ns_client_putrdataset(client, &client->query.redirect.rdataset);
	ns_client_putrdataset(client, &client->query.redirect.sigrdataset);
	if (client->query.redirect.db != NULL) {
		if (client->query.redirect.node != NULL) {
			dns_db_detachnode(&client->query.redirect.node);
		}
		dns_db_detach(&client->query.redirect.db);
	}
	if (client->query.redirect.zone != NULL) {
		dns_zone_detach(&client->query.redirect.zone);
	}

	query_freefreeversions(client, everything);

	ISC_LIST_FOREACH(client->query.namebufs, dbuf, link) {
		if (ISC_LIST_NEXT(dbuf, link) != NULL || everything) {
			ISC_LIST_UNLINK(client->query.namebufs, dbuf, link);
			isc_buffer_free(&dbuf);
		}
	}

	if (client->query.restarts > 0) {
		/*
		 * client->query.qname was dynamically allocated.
		 */
		dns_message_puttempname(client->message, &client->query.qname);
	}
	client->query.qname = NULL;
	client->query.attributes = (NS_QUERYATTR_RECURSIONOK |
				    NS_QUERYATTR_CACHEOK | NS_QUERYATTR_SECURE);
	client->query.restarts = 0;
	client->query.timerset = false;
	if (client->query.rpz_st != NULL) {
		rpz_st_clear(client);
		if (everything) {
			isc_mem_put(client->manager->mctx, client->query.rpz_st,
				    sizeof(*client->query.rpz_st));
		}
	}
	if (client->query.qc != NULL) {
		isc_counter_detach(&client->query.qc);
	}
	client->query.origqname = NULL;
	client->query.dboptions = 0;
	client->query.fetchoptions = 0;
	client->query.gluedb = NULL;
	client->query.authdbset = false;
	client->query.isreferral = false;
	client->query.dns64_options = 0;
	client->query.dns64_ttl = UINT32_MAX;
	recparam_update(&client->query.recparam, 0, NULL, NULL);
	client->query.root_key_sentinel_keyid = 0;
	client->query.root_key_sentinel_is_ta = false;
	client->query.root_key_sentinel_not_ta = false;
}

static void
query_cleanup(ns_client_t *client) {
	query_reset(client, false);
}

void
ns_query_free(ns_client_t *client) {
	REQUIRE(NS_CLIENT_VALID(client));

	query_reset(client, true);
}

void
ns_query_init(ns_client_t *client) {
	REQUIRE(NS_CLIENT_VALID(client));

	client->query = (ns_query_t){ 0 };

	ISC_LIST_INIT(client->query.namebufs);
	ISC_LIST_INIT(client->query.activeversions);
	ISC_LIST_INIT(client->query.freeversions);

	/*
	 * This mutex is destroyed when the client is destroyed in
	 * exit_check().
	 */
	isc_mutex_init(&client->query.fetchlock);
	client->query.redirect.fname =
		dns_fixedname_initname(&client->query.redirect.fixed);
	query_reset(client, false);
	ns_client_newdbversion(client, 3);
	ns_client_newnamebuf(client);
}

/*%
 * Check if 'client' is allowed to query the cache of its associated view.
 * Unless 'options' has the 'nolog' flag set, log the result of cache ACL
 * evaluation using the appropriate level, along with 'name' and 'qtype'.
 *
 * The cache ACL is only evaluated once for each client and then the result is
 * cached: if NS_QUERYATTR_CACHEACLOKVALID is set in client->query.attributes,
 * cache ACL evaluation has already been performed.  The evaluation result is
 * also stored in client->query.attributes: if NS_QUERYATTR_CACHEACLOK is set,
 * the client is allowed cache access.
 *
 * Returns:
 *
 *\li	#ISC_R_SUCCESS	'client' is allowed to access cache
 *\li	#DNS_R_REFUSED	'client' is not allowed to access cache
 */
static isc_result_t
query_checkcacheaccess(ns_client_t *client, const dns_name_t *name,
		       dns_rdatatype_t qtype, dns_getdb_options_t options) {
	isc_result_t result;

	if ((client->query.attributes & NS_QUERYATTR_CACHEACLOKVALID) == 0) {
		enum refusal_reasons {
			ALLOW_QUERY_CACHE,
			ALLOW_QUERY_CACHE_ON
		};
		static const char *acl_desc[] = {
			"allow-query-cache did not match",
			"allow-query-cache-on did not match",
		};

		/*
		 * The view's cache ACLs have not yet been evaluated.
		 * Do it now. Both allow-query-cache and
		 * allow-query-cache-on must be satisfied.
		 */
		char msg[NS_CLIENT_ACLMSGSIZE("query (cache)")];

		enum refusal_reasons refusal_reason = ALLOW_QUERY_CACHE;
		result = ns_client_checkaclsilent(
			client, NULL, client->inner.view->cacheacl, true);
		if (result == ISC_R_SUCCESS) {
			refusal_reason = ALLOW_QUERY_CACHE_ON;
			result = ns_client_checkaclsilent(
				client, &client->inner.destaddr,
				client->inner.view->cacheonacl, true);
		}
		if (result == ISC_R_SUCCESS) {
			/*
			 * We were allowed by the "allow-query-cache" ACL.
			 */
			client->query.attributes |= NS_QUERYATTR_CACHEACLOK;
			if (!options.nolog &&
			    isc_log_wouldlog(ISC_LOG_DEBUG(3)))
			{
				ns_client_aclmsg("query (cache)", name, qtype,
						 client->inner.view->rdclass,
						 msg, sizeof(msg));
				ns_client_log(client, DNS_LOGCATEGORY_SECURITY,
					      NS_LOGMODULE_QUERY,
					      ISC_LOG_DEBUG(3), "%s approved",
					      msg);
			}
		} else {
			/*
			 * We were denied by the "allow-query-cache" ACL.
			 * There is no need to clear NS_QUERYATTR_CACHEACLOK
			 * since it is cleared by query_reset(), before query
			 * processing starts.
			 */
			dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL);

			if (!options.nolog) {
				ns_client_aclmsg("query (cache)", name, qtype,
						 client->inner.view->rdclass,
						 msg, sizeof(msg));
				ns_client_log(client, DNS_LOGCATEGORY_SECURITY,
					      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
					      "%s denied (%s)", msg,
					      acl_desc[refusal_reason]);
			}
		}

		/*
		 * Evaluation has been finished; make sure we will just consult
		 * NS_QUERYATTR_CACHEACLOK for this client from now on.
		 */
		client->query.attributes |= NS_QUERYATTR_CACHEACLOKVALID;
	}

	return (client->query.attributes & NS_QUERYATTR_CACHEACLOK) != 0
		       ? ISC_R_SUCCESS
		       : DNS_R_REFUSED;
}

static isc_result_t
query_validatezonedb(ns_client_t *client, const dns_name_t *name,
		     dns_rdatatype_t qtype, dns_getdb_options_t options,
		     dns_zone_t *zone, dns_db_t *db,
		     dns_dbversion_t **versionp) {
	isc_result_t result;
	dns_acl_t *queryacl, *queryonacl;
	ns_dbversion_t *dbversion;

	REQUIRE(zone != NULL);
	REQUIRE(db != NULL);

	/*
	 * Mirror zone data is treated as cache data.
	 */
	if (dns_zone_gettype(zone) == dns_zone_mirror) {
		return query_checkcacheaccess(client, name, qtype, options);
	}

	/*
	 * This limits our searching to the zone where the first name
	 * (the query target) was looked for.  This prevents following
	 * CNAMES or DNAMES into other zones and prevents returning
	 * additional data from other zones. This does not apply if we're
	 * answering a query where recursion is requested and allowed.
	 */
	if (client->query.rpz_st == NULL &&
	    !(WANTRECURSION(client) && RECURSIONOK(client)) &&
	    client->query.authdbset && db != client->query.authdb)
	{
		return DNS_R_REFUSED;
	}

	/*
	 * Non recursive query to a static-stub zone is prohibited; its
	 * zone content is not public data, but a part of local configuration
	 * and should not be disclosed.
	 */
	if (dns_zone_gettype(zone) == dns_zone_staticstub &&
	    !RECURSIONOK(client))
	{
		return DNS_R_REFUSED;
	}

	/*
	 * If the zone has an ACL, we'll check it, otherwise
	 * we use the view's "allow-query" ACL.  Each ACL is only checked
	 * once per query.
	 *
	 * Also, get the database version to use.
	 */

	/*
	 * Get the current version of this database.
	 */
	dbversion = ns_client_findversion(client, db);
	if (dbversion == NULL) {
		CTRACE(ISC_LOG_ERROR, "unable to get db version");
		return DNS_R_SERVFAIL;
	}

	if (options.ignoreacl) {
		goto approved;
	}
	if (dbversion->acl_checked) {
		if (!dbversion->queryok) {
			return DNS_R_REFUSED;
		}
		goto approved;
	}

	queryacl = dns_zone_getqueryacl(zone);
	if (queryacl == NULL) {
		queryacl = client->inner.view->queryacl;
		if ((client->query.attributes & NS_QUERYATTR_QUERYOKVALID) != 0)
		{
			/*
			 * We've evaluated the view's queryacl already.  If
			 * NS_QUERYATTR_QUERYOK is set, then the client is
			 * allowed to make queries, otherwise the query should
			 * be refused.
			 */
			dbversion->acl_checked = true;
			if ((client->query.attributes & NS_QUERYATTR_QUERYOK) ==
			    0)
			{
				dbversion->queryok = false;
				return DNS_R_REFUSED;
			}
			dbversion->queryok = true;
			goto approved;
		}
	}

	result = ns_client_checkaclsilent(client, NULL, queryacl, true);
	if (!options.nolog) {
		char msg[NS_CLIENT_ACLMSGSIZE("query")];
		if (result == ISC_R_SUCCESS) {
			if (isc_log_wouldlog(ISC_LOG_DEBUG(3))) {
				ns_client_aclmsg("query", name, qtype,
						 client->inner.view->rdclass,
						 msg, sizeof(msg));
				ns_client_log(client, DNS_LOGCATEGORY_SECURITY,
					      NS_LOGMODULE_QUERY,
					      ISC_LOG_DEBUG(3), "%s approved",
					      msg);
			}
		} else {
			ns_client_aclmsg("query", name, qtype,
					 client->inner.view->rdclass, msg,
					 sizeof(msg));
			ns_client_log(client, DNS_LOGCATEGORY_SECURITY,
				      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
				      "%s denied", msg);
			dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL);
		}
	}

	if (queryacl == client->inner.view->queryacl) {
		if (result == ISC_R_SUCCESS) {
			/*
			 * We were allowed by the default
			 * "allow-query" ACL.  Remember this so we
			 * don't have to check again.
			 */
			client->query.attributes |= NS_QUERYATTR_QUERYOK;
		}
		/*
		 * We've now evaluated the view's query ACL, and
		 * the NS_QUERYATTR_QUERYOK attribute is now valid.
		 */
		client->query.attributes |= NS_QUERYATTR_QUERYOKVALID;
	}

	/* If and only if we've gotten this far, check allow-query-on too */
	if (result == ISC_R_SUCCESS) {
		queryonacl = dns_zone_getqueryonacl(zone);
		if (queryonacl == NULL) {
			queryonacl = client->inner.view->queryonacl;
		}

		result = ns_client_checkaclsilent(
			client, &client->inner.destaddr, queryonacl, true);
		if (result != ISC_R_SUCCESS) {
			dns_ede_add(&client->edectx, DNS_EDE_PROHIBITED, NULL);
		}
		if (!options.nolog && result != ISC_R_SUCCESS) {
			ns_client_log(client, DNS_LOGCATEGORY_SECURITY,
				      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
				      "query-on denied");
		}
	}

	dbversion->acl_checked = true;
	if (result != ISC_R_SUCCESS) {
		dbversion->queryok = false;
		return DNS_R_REFUSED;
	}
	dbversion->queryok = true;

approved:
	/* Transfer ownership, if necessary. */
	SET_IF_NOT_NULL(versionp, dbversion->version);
	return ISC_R_SUCCESS;
}

static isc_result_t
query_getzonedb(ns_client_t *client, const dns_name_t *name,
		dns_rdatatype_t qtype, dns_getdb_options_t options,
		dns_zone_t **zonep, dns_db_t **dbp,
		dns_dbversion_t **versionp) {
	isc_result_t result;
	unsigned int ztoptions;
	dns_zone_t *zone = NULL;
	dns_db_t *db = NULL;
	bool partial = false;

	REQUIRE(zonep != NULL && *zonep == NULL);
	REQUIRE(dbp != NULL && *dbp == NULL);

	/*%
	 * Find a zone database to answer the query.
	 */
	ztoptions = DNS_ZTFIND_MIRROR;
	if (options.noexact) {
		ztoptions |= DNS_ZTFIND_NOEXACT;
	}

	result = dns_view_findzone(client->inner.view, name, ztoptions, &zone);

	if (result == DNS_R_PARTIALMATCH) {
		partial = true;
	}
	if (result == ISC_R_SUCCESS || result == DNS_R_PARTIALMATCH) {
		result = dns_zone_getdb(zone, &db);
	}

	if (result != ISC_R_SUCCESS) {
		goto fail;
	}

	result = query_validatezonedb(client, name, qtype, options, zone, db,
				      versionp);

	if (result != ISC_R_SUCCESS) {
		goto fail;
	}

	/* Transfer ownership. */
	*zonep = zone;
	*dbp = db;

	if (partial && options.partial) {
		return DNS_R_PARTIALMATCH;
	}
	return ISC_R_SUCCESS;

fail:
	if (zone != NULL) {
		dns_zone_detach(&zone);
	}
	if (db != NULL) {
		dns_db_detach(&db);
	}

	return result;
}

static void
rpz_log_rewrite(ns_client_t *client, bool disabled, dns_rpz_policy_t policy,
		dns_rpz_type_t type, dns_zone_t *p_zone, dns_name_t *p_name,
		dns_name_t *cname, dns_rpz_num_t rpz_num) {
	char cname_buf[DNS_NAME_FORMATSIZE] = { 0 };
	char p_name_buf[DNS_NAME_FORMATSIZE];
	char qname_buf[DNS_NAME_FORMATSIZE];
	char classbuf[DNS_RDATACLASS_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];
	const char *s1 = cname_buf, *s2 = cname_buf;
	dns_rdataset_t *rdataset;
	dns_rpz_st_t *st;
	isc_stats_t *zonestats;

	/*
	 * Count enabled rewrites in the global counter.
	 * Count both enabled and disabled rewrites for each zone.
	 */
	if (!disabled && policy != DNS_RPZ_POLICY_PASSTHRU) {
		ns_stats_increment(client->manager->sctx->nsstats,
				   ns_statscounter_rpz_rewrites);
	}
	if (p_zone != NULL) {
		zonestats = dns_zone_getrequeststats(p_zone);
		if (zonestats != NULL) {
			isc_stats_increment(zonestats,
					    ns_statscounter_rpz_rewrites);
		}
	}

	if (!isc_log_wouldlog(DNS_RPZ_INFO_LEVEL)) {
		return;
	}

	st = client->query.rpz_st;
	if ((st->popt.no_log & DNS_RPZ_ZBIT(rpz_num)) != 0) {
		return;
	}

	dns_name_format(client->query.qname, qname_buf, sizeof(qname_buf));
	dns_name_format(p_name, p_name_buf, sizeof(p_name_buf));
	if (cname != NULL) {
		s1 = " (CNAME to: ";
		dns_name_format(cname, cname_buf, sizeof(cname_buf));
		s2 = ")";
	}

	/*
	 *  Log Qclass and Qtype in addition to existing
	 *  fields.
	 */
	rdataset = ISC_LIST_HEAD(client->query.origqname->list);
	INSIST(rdataset != NULL);
	dns_rdataclass_format(rdataset->rdclass, classbuf, sizeof(classbuf));
	dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf));

	/* It's possible to have a separate log channel for rpz passthru. */
	isc_logcategory_t log_cat = (policy == DNS_RPZ_POLICY_PASSTHRU)
					    ? DNS_LOGCATEGORY_RPZ_PASSTHRU
					    : DNS_LOGCATEGORY_RPZ;

	ns_client_log(client, log_cat, NS_LOGMODULE_QUERY, DNS_RPZ_INFO_LEVEL,
		      "%srpz %s %s rewrite %s/%s/%s via %s%s%s%s",
		      disabled ? "disabled " : "", dns_rpz_type2str(type),
		      dns_rpz_policy2str(policy), qname_buf, typebuf, classbuf,
		      p_name_buf, s1, cname_buf, s2);
}

static void
rpz_log_fail_helper(ns_client_t *client, int level, dns_name_t *p_name,
		    dns_rpz_type_t rpz_type1, dns_rpz_type_t rpz_type2,
		    const char *str, isc_result_t result) {
	char qnamebuf[DNS_NAME_FORMATSIZE];
	char p_namebuf[DNS_NAME_FORMATSIZE];
	const char *failed, *via, *slash, *str_blank;
	const char *rpztypestr1;
	const char *rpztypestr2;

	if (!isc_log_wouldlog(level)) {
		return;
	}

	/*
	 * bin/tests/system/rpz/tests.sh looks for "rpz.*failed" for problems.
	 */
	if (level <= DNS_RPZ_DEBUG_LEVEL1) {
		failed = " failed: ";
	} else {
		failed = ": ";
	}

	rpztypestr1 = dns_rpz_type2str(rpz_type1);
	if (rpz_type2 != DNS_RPZ_TYPE_BAD) {
		slash = "/";
		rpztypestr2 = dns_rpz_type2str(rpz_type2);
	} else {
		slash = "";
		rpztypestr2 = "";
	}

	str_blank = (*str != ' ' && *str != '\0') ? " " : "";

	dns_name_format(client->query.qname, qnamebuf, sizeof(qnamebuf));

	if (p_name != NULL) {
		via = " via ";
		dns_name_format(p_name, p_namebuf, sizeof(p_namebuf));
	} else {
		via = "";
		p_namebuf[0] = '\0';
	}

	ns_client_log(client, NS_LOGCATEGORY_QUERY_ERRORS, NS_LOGMODULE_QUERY,
		      level, "rpz %s%s%s rewrite %s%s%s%s%s%s%s", rpztypestr1,
		      slash, rpztypestr2, qnamebuf, via, p_namebuf, str_blank,
		      str, failed, isc_result_totext(result));
}

static void
rpz_log_fail(ns_client_t *client, int level, dns_name_t *p_name,
	     dns_rpz_type_t rpz_type, const char *str, isc_result_t result) {
	rpz_log_fail_helper(client, level, p_name, rpz_type, DNS_RPZ_TYPE_BAD,
			    str, result);
}

/*
 * Get a policy rewrite zone database.
 */
static isc_result_t
rpz_getdb(ns_client_t *client, dns_name_t *p_name, dns_rpz_type_t rpz_type,
	  dns_zone_t **zonep, dns_db_t **dbp, dns_dbversion_t **versionp) {
	char qnamebuf[DNS_NAME_FORMATSIZE];
	char p_namebuf[DNS_NAME_FORMATSIZE];
	dns_dbversion_t *rpz_version = NULL;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_getdb");

	dns_getdb_options_t options = { .ignoreacl = true };
	result = query_getzonedb(client, p_name, dns_rdatatype_any, options,
				 zonep, dbp, &rpz_version);
	if (result == ISC_R_SUCCESS) {
		dns_rpz_st_t *st = client->query.rpz_st;

		/*
		 * It isn't meaningful to log this message when
		 * logging is disabled for some policy zones.
		 */
		if (st->popt.no_log == 0 &&
		    isc_log_wouldlog(DNS_RPZ_DEBUG_LEVEL2))
		{
			dns_name_format(client->query.qname, qnamebuf,
					sizeof(qnamebuf));
			dns_name_format(p_name, p_namebuf, sizeof(p_namebuf));
			ns_client_log(client, DNS_LOGCATEGORY_RPZ,
				      NS_LOGMODULE_QUERY, DNS_RPZ_DEBUG_LEVEL2,
				      "try rpz %s rewrite %s via %s",
				      dns_rpz_type2str(rpz_type), qnamebuf,
				      p_namebuf);
		}
		*versionp = rpz_version;
		return ISC_R_SUCCESS;
	}
	rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, p_name, rpz_type,
		     "query_getzonedb()", result);
	return result;
}

/*%
 * Find a cache database to answer the query.  This may fail with DNS_R_REFUSED
 * if the client is not allowed to use the cache.
 */
static isc_result_t
query_getcachedb(ns_client_t *client, const dns_name_t *name,
		 dns_rdatatype_t qtype, dns_db_t **dbp,
		 dns_getdb_options_t options) {
	isc_result_t result;
	dns_db_t *db = NULL;

	REQUIRE(dbp != NULL && *dbp == NULL);

	if (!USECACHE(client)) {
		return DNS_R_REFUSED;
	}

	dns_db_attach(client->inner.view->cachedb, &db);

	result = query_checkcacheaccess(client, name, qtype, options);
	if (result != ISC_R_SUCCESS) {
		dns_db_detach(&db);
	}

	/*
	 * If query_checkcacheaccess() succeeded, transfer ownership of 'db'.
	 * Otherwise, 'db' will be NULL due to the dns_db_detach() call above.
	 */
	*dbp = db;

	return result;
}

static isc_result_t
query_getdb(ns_client_t *client, dns_name_t *name, dns_rdatatype_t qtype,
	    dns_getdb_options_t options, dns_zone_t **zonep, dns_db_t **dbp,
	    dns_dbversion_t **versionp, bool *is_zonep) {
	isc_result_t result;
	isc_result_t tresult;
	unsigned int namelabels;
	unsigned int zonelabels;
	dns_zone_t *zone = NULL;

	REQUIRE(zonep != NULL && *zonep == NULL);

	/* Calculate how many labels are in name. */
	namelabels = dns_name_countlabels(name);
	zonelabels = 0;

	/* Try to find name in bind's standard database. */
	result = query_getzonedb(client, name, qtype, options, &zone, dbp,
				 versionp);

	/* See how many labels are in the zone's name.	  */
	if (result == ISC_R_SUCCESS && zone != NULL) {
		zonelabels = dns_name_countlabels(dns_zone_getorigin(zone));
	}

	/*
	 * If # zone labels < # name labels, try to find an even better match
	 * Only try if DLZ drivers are loaded for this view
	 */
	if (zonelabels < namelabels &&
	    !ISC_LIST_EMPTY(client->inner.view->dlz_searched))
	{
		dns_clientinfomethods_t cm;
		dns_clientinfo_t ci;
		dns_db_t *tdbp;

		dns_clientinfomethods_init(&cm, ns_client_sourceip);
		dns_clientinfo_init(&ci, client, NULL);
		dns_clientinfo_setecs(&ci, &client->inner.ecs);

		tdbp = NULL;
		tresult = dns_view_searchdlz(client->inner.view, name,
					     zonelabels, &cm, &ci, &tdbp);
		/* If we successful, we found a better match. */
		if (tresult == ISC_R_SUCCESS) {
			ns_dbversion_t *dbversion;

			/*
			 * If the previous search returned a zone, detach it.
			 */
			if (zone != NULL) {
				dns_zone_detach(&zone);
			}

			/*
			 * If the previous search returned a database,
			 * detach it.
			 */
			if (*dbp != NULL) {
				dns_db_detach(dbp);
			}

			/*
			 * If the previous search returned a version, clear it.
			 */
			*versionp = NULL;

			dbversion = ns_client_findversion(client, tdbp);

			/*
			 * Be sure to return our database.
			 */
			*dbp = tdbp;
			*versionp = dbversion->version;

			/*
			 * We return a null zone, No stats for DLZ zones.
			 */
			zone = NULL;
			result = tresult;
		}
	}

	/* If successful, Transfer ownership of zone. */
	if (result == ISC_R_SUCCESS) {
		*zonep = zone;
		/*
		 * If neither attempt above succeeded, return the cache instead
		 */
		*is_zonep = true;
	} else {
		if (result == ISC_R_NOTFOUND) {
			result = query_getcachedb(client, name, qtype, dbp,
						  options);
		}
		*is_zonep = false;
	}
	return result;
}

static bool
query_isduplicate(ns_client_t *client, dns_name_t *name, dns_rdatatype_t type,
		  dns_name_t **mnamep) {
	dns_section_t section;
	dns_name_t *mname = NULL;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "query_isduplicate");

	for (section = DNS_SECTION_ANSWER; section <= DNS_SECTION_ADDITIONAL;
	     section++)
	{
		result = dns_message_findname(client->message, section, name,
					      type, 0, &mname, NULL);
		if (result == ISC_R_SUCCESS) {
			/*
			 * We've already got this RRset in the response.
			 */
			CTRACE(ISC_LOG_DEBUG(3), "query_isduplicate: true: "
						 "done");
			return true;
		} else if (result == DNS_R_NXRRSET) {
			/*
			 * The name exists, but the rdataset does not.
			 */
			if (section == DNS_SECTION_ADDITIONAL) {
				break;
			}
		} else {
			RUNTIME_CHECK(result == DNS_R_NXDOMAIN);
		}
		mname = NULL;
	}

	SET_IF_NOT_NULL(mnamep, mname);

	CTRACE(ISC_LOG_DEBUG(3), "query_isduplicate: false: done");
	return false;
}

/*
 * Look up data for given 'name' and 'type' in given 'version' of 'db' for
 * 'client'. Called from query_additionalauth().
 *
 * If the lookup is successful:
 *
 *   - store the node containing the result at 'nodep',
 *
 *   - store the owner name of the returned node in 'fname',
 *
 *   - if 'type' is not ANY, dns_db_findext() will put the exact rdataset being
 *     looked for in 'rdataset' and its signatures (if any) in 'sigrdataset',
 *
 *   - if 'type' is ANY, dns_db_findext() will leave 'rdataset' and
 *     'sigrdataset' disassociated and the returned node will be iterated in
 *     query_additional_cb().
 *
 * If the lookup is not successful:
 *
 *   - 'nodep' will not be written to,
 *   - 'fname' may still be modified as it is passed to dns_db_findext(),
 *   - 'rdataset' and 'sigrdataset' will remain disassociated.
 */
static isc_result_t
query_additionalauthfind(dns_db_t *db, dns_dbversion_t *version,
			 const dns_name_t *name, dns_rdatatype_t type,
			 ns_client_t *client, dns_dbnode_t **nodep,
			 dns_name_t *fname, dns_rdataset_t *rdataset,
			 dns_rdataset_t *sigrdataset) {
	dns_clientinfomethods_t cm;
	dns_dbnode_t *node = NULL;
	dns_clientinfo_t ci;
	isc_result_t result;

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * Since we are looking for authoritative data, we do not set
	 * the GLUEOK flag.  Glue will be looked for later, but not
	 * necessarily in the same database.
	 */
	result = dns_db_findext(db, name, version, type,
				client->query.dboptions, client->inner.now,
				&node, fname, &cm, &ci, rdataset, sigrdataset);
	if (result != ISC_R_SUCCESS) {
		if (dns_rdataset_isassociated(rdataset)) {
			dns_rdataset_disassociate(rdataset);
		}

		if (sigrdataset != NULL &&
		    dns_rdataset_isassociated(sigrdataset))
		{
			dns_rdataset_disassociate(sigrdataset);
		}

		if (node != NULL) {
			dns_db_detachnode(&node);
		}

		return result;
	}

	/*
	 * Do not return signatures if the zone is not fully signed.
	 */
	if (sigrdataset != NULL && !dns_db_issecure(db) &&
	    dns_rdataset_isassociated(sigrdataset))
	{
		dns_rdataset_disassociate(sigrdataset);
	}

	*nodep = node;

	return ISC_R_SUCCESS;
}

/*
 * For query context 'qctx', try finding authoritative additional data for
 * given 'name' and 'type'. Called from query_additional_cb().
 *
 * If successful:
 *
 *   - store pointers to the database and node which contain the result in
 *     'dbp' and 'nodep', respectively,
 *
 *   - store the owner name of the returned node in 'fname',
 *
 *   - potentially bind 'rdataset' and 'sigrdataset', as explained in the
 *     comment for query_additionalauthfind().
 *
 * If unsuccessful:
 *
 *   - 'dbp' and 'nodep' will not be written to,
 *   - 'fname' may still be modified as it is passed to dns_db_findext(),
 *   - 'rdataset' and 'sigrdataset' will remain disassociated.
 */
static isc_result_t
query_additionalauth(query_ctx_t *qctx, const dns_name_t *name,
		     dns_rdatatype_t type, dns_db_t **dbp, dns_dbnode_t **nodep,
		     dns_name_t *fname, dns_rdataset_t *rdataset,
		     dns_rdataset_t *sigrdataset) {
	ns_client_t *client = qctx->client;
	ns_dbversion_t *dbversion = NULL;
	dns_dbversion_t *version = NULL;
	dns_dbnode_t *node = NULL;
	dns_zone_t *zone = NULL;
	dns_db_t *db = NULL;
	isc_result_t result;

	/*
	 * First, look within the same zone database for authoritative
	 * additional data.
	 */
	if (!client->query.authdbset || client->query.authdb == NULL) {
		return ISC_R_NOTFOUND;
	}

	dbversion = ns_client_findversion(client, client->query.authdb);
	if (dbversion == NULL) {
		return ISC_R_NOTFOUND;
	}

	dns_db_attach(client->query.authdb, &db);
	version = dbversion->version;

	CTRACE(ISC_LOG_DEBUG(3), "query_additionalauth: same zone");

	result = query_additionalauthfind(db, version, name, type, client,
					  &node, fname, rdataset, sigrdataset);
	if (result != ISC_R_SUCCESS &&
	    qctx->view->minimalresponses == dns_minimal_no &&
	    RECURSIONOK(client))
	{
		/*
		 * If we aren't doing response minimization and recursion is
		 * allowed, we can try and see if any other zone matches.
		 */
		version = NULL;
		dns_db_detach(&db);
		dns_getdb_options_t options = { .nolog = true };
		result = query_getzonedb(client, name, type, options, &zone,
					 &db, &version);
		if (result != ISC_R_SUCCESS) {
			return result;
		}
		dns_zone_detach(&zone);

		CTRACE(ISC_LOG_DEBUG(3), "query_additionalauth: other zone");

		result = query_additionalauthfind(db, version, name, type,
						  client, &node, fname,
						  rdataset, sigrdataset);
	}

	if (result != ISC_R_SUCCESS) {
		dns_db_detach(&db);
	} else {
		*nodep = node;
		node = NULL;

		*dbp = db;
		db = NULL;
	}

	return result;
}

static isc_result_t
query_additional_cb(void *arg, const dns_name_t *name, dns_rdatatype_t qtype,
		    dns_rdataset_t *found DNS__DB_FLARG) {
	query_ctx_t *qctx = arg;
	ns_client_t *client = qctx->client;
	isc_result_t result, eresult = ISC_R_SUCCESS;
	dns_dbnode_t *node = NULL;
	dns_db_t *db = NULL;
	dns_name_t *fname = NULL, *mname = NULL;
	dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
	dns_rdataset_t *trdataset = NULL;
	isc_buffer_t *dbuf = NULL;
	isc_buffer_t b;
	ns_dbversion_t *dbversion = NULL;
	dns_dbversion_t *version = NULL;
	bool added_something = false, need_addname = false;
	dns_rdatatype_t type;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	dns_rdatasetadditional_t additionaltype =
		dns_rdatasetadditional_fromauth;

	REQUIRE(NS_CLIENT_VALID(client));
	REQUIRE(qtype != dns_rdatatype_any);

	if (!WANTDNSSEC(client) && dns_rdatatype_isdnssec(qtype)) {
		return ISC_R_SUCCESS;
	}

	CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb");

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * We treat type A additional section processing as if it
	 * were "any address type" additional section processing.
	 * To avoid multiple lookups, we do an 'any' database
	 * lookup and iterate over the node.
	 */
	if (qtype == dns_rdatatype_a) {
		type = dns_rdatatype_any;
	} else {
		type = qtype;
	}

	/*
	 * Get some resources.
	 */
	dbuf = ns_client_getnamebuf(client);
	fname = ns_client_newname(client, dbuf, &b);
	rdataset = ns_client_newrdataset(client);
	if (WANTDNSSEC(client)) {
		sigrdataset = ns_client_newrdataset(client);
	}

	/*
	 * If we want only minimal responses and are here, then it must
	 * be for glue.
	 */
	if (qctx->view->minimalresponses == dns_minimal_yes &&
	    client->query.qtype != dns_rdatatype_ns)
	{
		goto try_glue;
	}

	/*
	 * First, look for authoritative additional data.
	 */
	result = query_additionalauth(qctx, name, type, &db, &node, fname,
				      rdataset, sigrdataset);
	if (result == ISC_R_SUCCESS) {
		goto found;
	}

	/*
	 * No authoritative data was found.  The cache is our next best bet.
	 */
	if (!qctx->view->recursion) {
		goto try_glue;
	}

	additionaltype = dns_rdatasetadditional_fromcache;
	dns_getdb_options_t options = { .nolog = true };
	result = query_getcachedb(client, name, qtype, &db, options);
	if (result != ISC_R_SUCCESS) {
		/*
		 * Most likely the client isn't allowed to query the cache.
		 */
		goto try_glue;
	}
	/*
	 * Attempt to validate glue.
	 */
	if (sigrdataset == NULL) {
		sigrdataset = ns_client_newrdataset(client);
	}

	version = NULL;
	result = dns_db_findext(db, name, version, type,
				client->query.dboptions | DNS_DBFIND_GLUEOK |
					DNS_DBFIND_ADDITIONALOK,
				client->inner.now, &node, fname, &cm, &ci,
				rdataset, sigrdataset);

	dns_cache_updatestats(qctx->view->cache, result);
	if (!WANTDNSSEC(client)) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (result == ISC_R_SUCCESS) {
		goto found;
	}

	if (dns_rdataset_isassociated(rdataset)) {
		dns_rdataset_disassociate(rdataset);
	}
	if (sigrdataset != NULL && dns_rdataset_isassociated(sigrdataset)) {
		dns_rdataset_disassociate(sigrdataset);
	}
	if (node != NULL) {
		dns_db_detachnode(&node);
	}
	dns_db_detach(&db);

try_glue:
	/*
	 * No cached data was found.  Glue is our last chance.
	 * RFC1035 sayeth:
	 *
	 *	NS records cause both the usual additional section
	 *	processing to locate a type A record, and, when used
	 *	in a referral, a special search of the zone in which
	 *	they reside for glue information.
	 *
	 * This is the "special search".  Note that we must search
	 * the zone where the NS record resides, not the zone it
	 * points to, and that we only do the search in the delegation
	 * case (identified by client->query.gluedb being set).
	 */

	if (client->query.gluedb == NULL) {
		goto cleanup;
	}

	/*
	 * Don't poison caches using the bailiwick protection model.
	 */
	if (!dns_name_issubdomain(name, dns_db_origin(client->query.gluedb))) {
		goto cleanup;
	}

	dbversion = ns_client_findversion(client, client->query.gluedb);
	if (dbversion == NULL) {
		goto cleanup;
	}

	dns_db_attach(client->query.gluedb, &db);
	version = dbversion->version;
	additionaltype = dns_rdatasetadditional_fromglue;
	result = dns_db_findext(db, name, version, type,
				client->query.dboptions | DNS_DBFIND_GLUEOK,
				client->inner.now, &node, fname, &cm, &ci,
				rdataset, sigrdataset);
	if (result != ISC_R_SUCCESS && result != DNS_R_ZONECUT &&
	    result != DNS_R_GLUE)
	{
		goto cleanup;
	}

found:
	/*
	 * We have found a potential additional data rdataset, or
	 * at least a node to iterate over.
	 */
	ns_client_keepname(client, fname, dbuf);

	/*
	 * Does the caller want the found rdataset?
	 */
	if (found != NULL && dns_rdataset_isassociated(rdataset)) {
		dns_rdataset_clone(rdataset, found);
	}

	/*
	 * If we have an rdataset, add it to the additional data
	 * section.
	 */
	mname = NULL;
	if (dns_rdataset_isassociated(rdataset) &&
	    !query_isduplicate(client, fname, type, &mname))
	{
		if (mname != NULL) {
			INSIST(mname != fname);
			ns_client_releasename(client, &fname);
			fname = mname;
		} else {
			need_addname = true;
		}
		ISC_LIST_APPEND(fname->list, rdataset, link);
		trdataset = rdataset;
		rdataset = NULL;
		added_something = true;
		/*
		 * Note: we only add SIGs if we've added the type they cover,
		 * so we do not need to check if the SIG rdataset is already
		 * in the response.
		 */
		if (sigrdataset != NULL &&
		    dns_rdataset_isassociated(sigrdataset))
		{
			ISC_LIST_APPEND(fname->list, sigrdataset, link);
			sigrdataset = NULL;
		}
	}

	if (qtype == dns_rdatatype_a) {
		/*
		 * We now go looking for A and AAAA records, along with
		 * their signatures.
		 *
		 * XXXRTH  This code could be more efficient.
		 */
		if (rdataset != NULL) {
			if (dns_rdataset_isassociated(rdataset)) {
				dns_rdataset_disassociate(rdataset);
			}
		} else {
			rdataset = ns_client_newrdataset(client);
		}
		if (sigrdataset != NULL) {
			if (dns_rdataset_isassociated(sigrdataset)) {
				dns_rdataset_disassociate(sigrdataset);
			}
		} else if (WANTDNSSEC(client)) {
			sigrdataset = ns_client_newrdataset(client);
		}
		if (query_isduplicate(client, fname, dns_rdatatype_a, NULL)) {
			goto aaaa_lookup;
		}
		result = dns_db_findrdataset(db, node, version, dns_rdatatype_a,
					     0, client->inner.now, rdataset,
					     sigrdataset);
		if (result == DNS_R_NCACHENXDOMAIN) {
			goto addname;
		} else if (result == DNS_R_NCACHENXRRSET) {
			dns_rdataset_disassociate(rdataset);
			if (sigrdataset != NULL &&
			    dns_rdataset_isassociated(sigrdataset))
			{
				dns_rdataset_disassociate(sigrdataset);
			}
		} else if (result == ISC_R_SUCCESS) {
			bool invalid = false;
			mname = NULL;
			if (additionaltype ==
				    dns_rdatasetadditional_fromcache &&
			    (DNS_TRUST_PENDING(rdataset->trust) ||
			     DNS_TRUST_GLUE(rdataset->trust)))
			{
				/* validate() may change rdataset->trust */
				invalid = !validate(client, db, fname, rdataset,
						    sigrdataset);
			}
			if (invalid && DNS_TRUST_PENDING(rdataset->trust)) {
				dns_rdataset_disassociate(rdataset);
				if (sigrdataset != NULL &&
				    dns_rdataset_isassociated(sigrdataset))
				{
					dns_rdataset_disassociate(sigrdataset);
				}
			} else if (!query_isduplicate(client, fname,
						      dns_rdatatype_a, &mname))
			{
				if (mname != fname) {
					if (mname != NULL) {
						ns_client_releasename(client,
								      &fname);
						fname = mname;
					} else {
						need_addname = true;
					}
				}
				ISC_LIST_APPEND(fname->list, rdataset, link);
				added_something = true;
				if (sigrdataset != NULL &&
				    dns_rdataset_isassociated(sigrdataset))
				{
					ISC_LIST_APPEND(fname->list,
							sigrdataset, link);
					sigrdataset =
						ns_client_newrdataset(client);
				}
				rdataset = ns_client_newrdataset(client);
			} else {
				dns_rdataset_disassociate(rdataset);
				if (sigrdataset != NULL &&
				    dns_rdataset_isassociated(sigrdataset))
				{
					dns_rdataset_disassociate(sigrdataset);
				}
			}
		}
	aaaa_lookup:
		if (query_isduplicate(client, fname, dns_rdatatype_aaaa, NULL))
		{
			goto addname;
		}
		result = dns_db_findrdataset(
			db, node, version, dns_rdatatype_aaaa, 0,
			client->inner.now, rdataset, sigrdataset);
		if (result == DNS_R_NCACHENXDOMAIN) {
			goto addname;
		} else if (result == DNS_R_NCACHENXRRSET) {
			dns_rdataset_disassociate(rdataset);
			if (sigrdataset != NULL &&
			    dns_rdataset_isassociated(sigrdataset))
			{
				dns_rdataset_disassociate(sigrdataset);
			}
		} else if (result == ISC_R_SUCCESS) {
			bool invalid = false;
			mname = NULL;

			if (additionaltype ==
				    dns_rdatasetadditional_fromcache &&
			    (DNS_TRUST_PENDING(rdataset->trust) ||
			     DNS_TRUST_GLUE(rdataset->trust)))
			{
				/* validate() may change rdataset->trust */
				invalid = !validate(client, db, fname, rdataset,
						    sigrdataset);
			}

			if (invalid && DNS_TRUST_PENDING(rdataset->trust)) {
				dns_rdataset_disassociate(rdataset);
				if (sigrdataset != NULL &&
				    dns_rdataset_isassociated(sigrdataset))
				{
					dns_rdataset_disassociate(sigrdataset);
				}
			} else if (!query_isduplicate(client, fname,
						      dns_rdatatype_aaaa,
						      &mname))
			{
				if (mname != fname) {
					if (mname != NULL) {
						ns_client_releasename(client,
								      &fname);
						fname = mname;
					} else {
						need_addname = true;
					}
				}
				ISC_LIST_APPEND(fname->list, rdataset, link);
				added_something = true;
				if (sigrdataset != NULL &&
				    dns_rdataset_isassociated(sigrdataset))
				{
					ISC_LIST_APPEND(fname->list,
							sigrdataset, link);
					sigrdataset = NULL;
				}
				rdataset = NULL;
			}
		}
	}

addname:
	CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb: addname");
	/*
	 * If we haven't added anything, then we're done.
	 */
	if (!added_something) {
		goto cleanup;
	}

	/*
	 * We may have added our rdatasets to an existing name, if so, then
	 * need_addname will be false.  Whether we used an existing name
	 * or a new one, we must set fname to NULL to prevent cleanup.
	 */
	if (need_addname) {
		dns_message_addname(client->message, fname,
				    DNS_SECTION_ADDITIONAL);
	}

	/*
	 * In some cases, a record that has been added as additional
	 * data may *also* trigger the addition of additional data.
	 * This cannot go more than 'max-restarts' levels deep.
	 */
	if (trdataset != NULL && dns_rdatatype_followadditional(type)) {
		if (client->inner.additionaldepth++ <
		    client->inner.view->max_restarts)
		{
			eresult = dns_rdataset_additionaldata(
				trdataset, fname, query_additional_cb, qctx,
				DNS_RDATASET_MAXADDITIONAL);
		}
		client->inner.additionaldepth--;
	}

	/*
	 * Don't release fname.
	 */
	fname = NULL;

cleanup:
	CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb: cleanup");
	ns_client_putrdataset(client, &rdataset);
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (fname != NULL) {
		ns_client_releasename(client, &fname);
	}
	if (node != NULL) {
		dns_db_detachnode(&node);
	}
	if (db != NULL) {
		dns_db_detach(&db);
	}

	CTRACE(ISC_LOG_DEBUG(3), "query_additional_cb: done");
	return eresult;
}

/*
 * Add 'rdataset' to 'name'.
 */
static void
query_addtoname(dns_name_t *name, dns_rdataset_t *rdataset) {
	ISC_LIST_APPEND(name->list, rdataset, link);
}

/*
 * Set the ordering for 'rdataset'.
 */
static void
query_setorder(query_ctx_t *qctx, dns_name_t *name, dns_rdataset_t *rdataset) {
	ns_client_t *client = qctx->client;
	dns_order_t *order = client->inner.view->order;

	CTRACE(ISC_LOG_DEBUG(3), "query_setorder");

	UNUSED(client);

	if (order != NULL) {
		rdataset->attributes.order = dns_order_find(
			order, name, rdataset->type, rdataset->rdclass);
	}
}

/*
 * Handle glue and fetch any other needed additional data for 'rdataset'.
 */
static void
query_additional(query_ctx_t *qctx, dns_name_t *name,
		 dns_rdataset_t *rdataset) {
	ns_client_t *client = qctx->client;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "query_additional");

	if (NOADDITIONAL(client)) {
		return;
	}

	/*
	 * Try to process glue directly.
	 */
	if (rdataset->type == dns_rdatatype_ns &&
	    client->query.gluedb != NULL && dns_db_iszone(client->query.gluedb))
	{
		ns_dbversion_t *dbversion = NULL;

		dbversion = ns_client_findversion(client, client->query.gluedb);
		if (dbversion == NULL) {
			goto regular;
		}

		result = dns_db_addglue(qctx->db, dbversion->version, rdataset,
					client->message);
		if (result == ISC_R_SUCCESS) {
			return;
		}
	}

regular:
	/*
	 * Add other additional data if needed.
	 * We don't care if dns_rdataset_additionaldata() fails.
	 */
	(void)dns_rdataset_additionaldata(rdataset, name, query_additional_cb,
					  qctx, DNS_RDATASET_MAXADDITIONAL);
	CTRACE(ISC_LOG_DEBUG(3), "query_additional: done");
}

static void
query_addrrset(query_ctx_t *qctx, dns_name_t **namep,
	       dns_rdataset_t **rdatasetp, dns_rdataset_t **sigrdatasetp,
	       isc_buffer_t *dbuf, dns_section_t section) {
	isc_result_t result;
	ns_client_t *client = qctx->client;
	dns_name_t *name = *namep, *mname = NULL;
	dns_rdataset_t *rdataset = *rdatasetp, *mrdataset = NULL;
	dns_rdataset_t *sigrdataset = NULL;

	CTRACE(ISC_LOG_DEBUG(3), "query_addrrset");

	REQUIRE(name != NULL);

	if (sigrdatasetp != NULL) {
		sigrdataset = *sigrdatasetp;
	}

	/*%
	 * To the current response for 'client', add the answer RRset
	 * '*rdatasetp' and an optional signature set '*sigrdatasetp', with
	 * owner name '*namep', to section 'section', unless they are
	 * already there.  Also add any pertinent additional data.
	 *
	 * If 'dbuf' is not NULL, then '*namep' is the name whose data is
	 * stored in 'dbuf'.  In this case, query_addrrset() guarantees that
	 * when it returns the name will either have been kept or released.
	 */
	result = dns_message_findname(client->message, section, name,
				      rdataset->type, rdataset->covers, &mname,
				      &mrdataset);
	if (result == ISC_R_SUCCESS) {
		/*
		 * We've already got an RRset of the given name and type.
		 */
		CTRACE(ISC_LOG_DEBUG(3), "query_addrrset: dns_message_findname "
					 "succeeded: done");
		if (dbuf != NULL) {
			ns_client_releasename(client, namep);
		}
		if (rdataset->attributes.required) {
			mrdataset->attributes.required = true;
		}
		return;
	} else if (result == DNS_R_NXDOMAIN) {
		/*
		 * The name doesn't exist.
		 */
		if (dbuf != NULL) {
			ns_client_keepname(client, name, dbuf);
		}
		dns_message_addname(client->message, name, section);
		*namep = NULL;
		mname = name;
	} else {
		RUNTIME_CHECK(result == DNS_R_NXRRSET);
		if (dbuf != NULL) {
			ns_client_releasename(client, namep);
		}
	}

	if (rdataset->trust != dns_trust_secure &&
	    (section == DNS_SECTION_ANSWER || section == DNS_SECTION_AUTHORITY))
	{
		client->query.attributes &= ~NS_QUERYATTR_SECURE;
	}

	/*
	 * Update message name, set rdataset order, and do additional
	 * section processing if needed.
	 */
	query_addtoname(mname, rdataset);
	query_setorder(qctx, mname, rdataset);
	query_additional(qctx, mname, rdataset);

	/*
	 * Note: we only add SIGs if we've added the type they cover, so
	 * we do not need to check if the SIG rdataset is already in the
	 * response.
	 */
	*rdatasetp = NULL;
	if (sigrdataset != NULL && dns_rdataset_isassociated(sigrdataset)) {
		/*
		 * We have a signature.  Add it to the response.
		 */
		ISC_LIST_APPEND(mname->list, sigrdataset, link);
		*sigrdatasetp = NULL;
	}

	CTRACE(ISC_LOG_DEBUG(3), "query_addrrset: done");
}

/*
 * Mark the RRsets as secure.  Update the cache (db) to reflect the
 * change in trust level.
 */
static void
mark_secure(ns_client_t *client, dns_db_t *db, dns_name_t *name,
	    dns_rdata_rrsig_t *rrsig, dns_rdataset_t *rdataset,
	    dns_rdataset_t *sigrdataset) {
	isc_result_t result;
	dns_dbnode_t *node = NULL;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	isc_stdtime_t now;

	rdataset->trust = dns_trust_secure;
	sigrdataset->trust = dns_trust_secure;
	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * Save the updated secure state.  Ignore failures.
	 */
	result = dns_db_findnodeext(db, name, true, &cm, &ci, &node);
	if (result != ISC_R_SUCCESS) {
		return;
	}

	now = isc_stdtime_now();
	dns_rdataset_trimttl(rdataset, sigrdataset, rrsig, now,
			     client->inner.view->acceptexpired);

	(void)dns_db_addrdataset(db, node, NULL, client->inner.now, rdataset, 0,
				 NULL);
	(void)dns_db_addrdataset(db, node, NULL, client->inner.now, sigrdataset,
				 0, NULL);
	dns_db_detachnode(&node);
}

/*
 * Find the secure key that corresponds to rrsig.
 * Note: 'keyrdataset' maintains state between successive calls,
 * there may be multiple keys with the same keyid.
 * Return false if we have exhausted all the possible keys.
 */
static bool
get_key(ns_client_t *client, dns_db_t *db, dns_rdata_rrsig_t *rrsig,
	dns_rdataset_t *keyrdataset, dst_key_t **keyp) {
	isc_result_t result;
	dns_dbnode_t *node = NULL;
	bool secure = false;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	dst_algorithm_t sigalg, keyalg;

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	if (!dns_rdataset_isassociated(keyrdataset)) {
		result = dns_db_findnodeext(db, &rrsig->signer, false, &cm, &ci,
					    &node);
		if (result != ISC_R_SUCCESS) {
			return false;
		}

		result = dns_db_findrdataset(
			db, node, NULL, dns_rdatatype_dnskey, 0,
			client->inner.now, keyrdataset, NULL);
		dns_db_detachnode(&node);
		if (result != ISC_R_SUCCESS) {
			return false;
		}

		if (keyrdataset->trust != dns_trust_secure) {
			return false;
		}

		result = dns_rdataset_first(keyrdataset);
	} else {
		result = dns_rdataset_next(keyrdataset);
	}

	sigalg = dst_algorithm_fromdata(rrsig->algorithm, rrsig->signature,
					rrsig->siglen);

	for (; result == ISC_R_SUCCESS; result = dns_rdataset_next(keyrdataset))
	{
		dns_rdata_t rdata = DNS_RDATA_INIT;
		dns_rdata_dnskey_t key;
		isc_region_t r;

		dns_rdataset_current(keyrdataset, &rdata);
		dns_rdata_tostruct(&rdata, &key, NULL); /* can't fail */
		keyalg = dst_algorithm_fromdata(key.algorithm, key.data,
						key.datalen);

		if (sigalg != keyalg || !dns_dnssec_iszonekey(&key)) {
			continue;
		}

		dns_rdata_toregion(&rdata, &r);
		if (dst_region_computeid(&r) != rrsig->keyid) {
			continue;
		}

		result = dns_dnssec_keyfromrdata(&rrsig->signer, &rdata,
						 client->manager->mctx, keyp);
		if (result == ISC_R_SUCCESS) {
			secure = true;
			break;
		}
	}

	return secure;
}

static bool
verify(dst_key_t *key, dns_name_t *name, dns_rdataset_t *rdataset,
       dns_rdata_t *rdata, ns_client_t *client) {
	isc_result_t result;
	dns_fixedname_t fixed;
	bool ignore = false;

	dns_fixedname_init(&fixed);

again:
	result = dns_dnssec_verify(name, rdataset, key, ignore,
				   client->manager->mctx, rdata, NULL);
	if (result == DNS_R_SIGEXPIRED && client->inner.view->acceptexpired) {
		ignore = true;
		goto again;
	}
	if (result == ISC_R_SUCCESS || result == DNS_R_FROMWILDCARD) {
		return true;
	}
	return false;
}

/*
 * Validate the rdataset if possible with available records.
 */
static bool
validate(ns_client_t *client, dns_db_t *db, dns_name_t *name,
	 dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset) {
	isc_result_t result;
	dns_rdata_rrsig_t rrsig;
	dst_key_t *key = NULL;
	dns_rdataset_t keyrdataset;

	if (sigrdataset == NULL || !dns_rdataset_isassociated(sigrdataset)) {
		return false;
	}

	DNS_RDATASET_FOREACH(sigrdataset) {
		dns_rdata_t rdata = DNS_RDATA_INIT;
		dns_rdataset_current(sigrdataset, &rdata);
		result = dns_rdata_tostruct(&rdata, &rrsig, NULL);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);
		if (!dns_resolver_algorithm_supported(
			    client->inner.view->resolver, name, rrsig.algorithm,
			    rrsig.signature, rrsig.siglen))
		{
			char txt[DNS_NAME_FORMATSIZE + 32];
			isc_buffer_t buffer;
			dst_algorithm_t alg;
			alg = dst_algorithm_fromdata(
				rrsig.algorithm, rrsig.signature, rrsig.siglen);

			isc_buffer_init(&buffer, txt, sizeof(txt));
			dst_algorithm_totext(alg, &buffer);
			isc_buffer_putstr(&buffer, " ");
			dns_name_totext(name, DNS_NAME_OMITFINALDOT, &buffer);
			isc_buffer_putstr(&buffer, " (cached)");
			isc_buffer_putuint8(&buffer, 0);

			dns_ede_add(&client->edectx, DNS_EDE_DNSKEYALG,
				    isc_buffer_base(&buffer));
			continue;
		}
		if (!dns_name_issubdomain(name, &rrsig.signer)) {
			continue;
		}
		dns_rdataset_init(&keyrdataset);
		do {
			if (!get_key(client, db, &rrsig, &keyrdataset, &key)) {
				break;
			}
			if (verify(key, name, rdataset, &rdata, client)) {
				dst_key_free(&key);
				dns_rdataset_disassociate(&keyrdataset);
				mark_secure(client, db, name, &rrsig, rdataset,
					    sigrdataset);
				return true;
			}
			dst_key_free(&key);
		} while (1);
		if (dns_rdataset_isassociated(&keyrdataset)) {
			dns_rdataset_disassociate(&keyrdataset);
		}
	}
	return false;
}

static void
fixrdataset(ns_client_t *client, dns_rdataset_t **rdataset) {
	if (*rdataset == NULL) {
		*rdataset = ns_client_newrdataset(client);
	} else if (dns_rdataset_isassociated(*rdataset)) {
		dns_rdataset_disassociate(*rdataset);
	}
}

static void
fixfname(ns_client_t *client, dns_name_t **fname, isc_buffer_t **dbuf,
	 isc_buffer_t *nbuf) {
	if (*fname == NULL) {
		*dbuf = ns_client_getnamebuf(client);
		*fname = ns_client_newname(client, *dbuf, nbuf);
	}
}

static void
free_fresp(ns_client_t *client, dns_fetchresponse_t **frespp) {
	dns_fetchresponse_t *fresp = *frespp;

	CTRACE(ISC_LOG_DEBUG(3), "free_fresp");

	if (fresp->fetch != NULL) {
		dns_resolver_destroyfetch(&fresp->fetch);
	}
	if (fresp->node != NULL) {
		dns_db_detachnode(&fresp->node);
	}
	if (fresp->db != NULL) {
		dns_db_detach(&fresp->db);
	}
	if (fresp->rdataset != NULL) {
		ns_client_putrdataset(client, &fresp->rdataset);
	}
	if (fresp->sigrdataset != NULL) {
		ns_client_putrdataset(client, &fresp->sigrdataset);
	}

	dns_resolver_freefresp(frespp);
}

static isc_result_t
recursionquotatype_attach(ns_client_t *client, bool soft_limit) {
	isc_statscounter_t recurscount;
	isc_result_t result;

	result = isc_quota_acquire(&client->manager->sctx->recursionquota);
	switch (result) {
	case ISC_R_SUCCESS:
		break;
	case ISC_R_SOFTQUOTA:
		if (soft_limit) {
			/*
			 * Exceeding soft quota was allowed, so continue as if
			 * 'result' was ISC_R_SUCCESS while retaining the
			 * original result code.
			 */
			break;
		}

		isc_quota_release(&client->manager->sctx->recursionquota);
		FALLTHROUGH;
	default:
		return result;
	}

	recurscount = ns_stats_increment(client->manager->sctx->nsstats,
					 ns_statscounter_recursclients);

	ns_stats_update_if_greater(client->manager->sctx->nsstats,
				   ns_statscounter_recurshighwater,
				   recurscount + 1);

	return result;
}

static isc_result_t
recursionquotatype_attach_hard(ns_client_t *client) {
	return recursionquotatype_attach(client, false);
}

static isc_result_t
recursionquotatype_attach_soft(ns_client_t *client) {
	return recursionquotatype_attach(client, true);
}

static void
recursionquotatype_detach(ns_client_t *client) {
	isc_quota_release(&client->manager->sctx->recursionquota);
	ns_stats_decrement(client->manager->sctx->nsstats,
			   ns_statscounter_recursclients);
}

static void
stale_refresh_aftermath(ns_client_t *client, isc_result_t result) {
	dns_db_t *db = NULL;
	unsigned int dboptions;
	isc_buffer_t buffer;
	query_ctx_t qctx;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	char namebuf[DNS_NAME_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];

	/*
	 * If refreshing a stale RRset failed, we need to set the
	 * stale-refresh-time window, so that on future requests for this
	 * RRset the stale entry may be used immediately.
	 */
	switch (result) {
	case ISC_R_SUCCESS:
	case DNS_R_GLUE:
	case DNS_R_ZONECUT:
	case ISC_R_NOTFOUND:
	case DNS_R_DELEGATION:
	case DNS_R_EMPTYNAME:
	case DNS_R_NXRRSET:
	case DNS_R_EMPTYWILD:
	case DNS_R_NXDOMAIN:
	case DNS_R_COVERINGNSEC:
	case DNS_R_NCACHENXDOMAIN:
	case DNS_R_NCACHENXRRSET:
	case DNS_R_CNAME:
	case DNS_R_DNAME:
		break;
	default:
		dns_name_format(client->query.qname, namebuf, sizeof(namebuf));
		dns_rdatatype_format(client->query.qtype, typebuf,
				     sizeof(typebuf));
		ns_client_log(client, NS_LOGCATEGORY_SERVE_STALE,
			      NS_LOGMODULE_QUERY, ISC_LOG_NOTICE,
			      "%s/%s stale refresh failed: timed out", namebuf,
			      typebuf);

		/*
		 * Set up a short lived query context, solely to set the
		 * last refresh failure time on the RRset in the cache
		 * database, starting the stale-refresh-time window for it.
		 * This is a condensed form of query_lookup().
		 */
		client->inner.now = isc_stdtime_now();
		client->query.attributes &= ~NS_QUERYATTR_RECURSIONOK;
		qctx_init(client, NULL, 0, &qctx);

		dns_clientinfomethods_init(&cm, ns_client_sourceip);
		dns_clientinfo_init(&ci, qctx.client, NULL);
		if (HAVEECS(qctx.client)) {
			dns_clientinfo_setecs(&ci, &qctx.client->inner.ecs);
		}

		result = qctx_prepare_buffers(&qctx, &buffer);
		if (result != ISC_R_SUCCESS) {
			goto cleanup;
		}

		dboptions = qctx.client->query.dboptions;
		dboptions |= DNS_DBFIND_STALEOK;
		dboptions |= DNS_DBFIND_STALESTART;

		dns_db_attach(qctx.client->inner.view->cachedb, &db);
		(void)dns_db_findext(db, qctx.client->query.qname, NULL,
				     qctx.client->query.qtype, dboptions,
				     qctx.client->inner.now, &qctx.node,
				     qctx.fname, &cm, &ci, qctx.rdataset,
				     qctx.sigrdataset);
		if (qctx.node != NULL) {
			dns_db_detachnode(&qctx.node);
		}
		dns_db_detach(&db);

	cleanup:
		qctx_freedata(&qctx);
		qctx_destroy(&qctx);
	}
}

static void
cleanup_after_fetch(dns_fetchresponse_t *resp, const char *ctracestr,
		    ns_query_rectype_t recursion_type) {
	ns_client_t *client = resp->arg;
	isc_nmhandle_t **handlep = NULL;
	dns_fetch_t **fetchp = NULL;
	isc_result_t result;

	REQUIRE(NS_CLIENT_VALID(client));

	CTRACE(ISC_LOG_DEBUG(3), ctracestr);

	handlep = &client->query.recursions[recursion_type].handle;
	fetchp = &client->query.recursions[recursion_type].fetch;
	result = resp->result;

	LOCK(&client->query.fetchlock);
	if (*fetchp != NULL) {
		INSIST(resp->fetch == *fetchp);
		*fetchp = NULL;
	}
	UNLOCK(&client->query.fetchlock);

	/* Some type of recursions require a bit of aftermath. */
	if (recursion_type == RECTYPE_STALE_REFRESH) {
		stale_refresh_aftermath(client, result);
	}

	recursionquotatype_detach(client);
	free_fresp(client, &resp);
	isc_nmhandle_detach(handlep);
}

static void
prefetch_done(void *arg) {
	cleanup_after_fetch(arg, "prefetch_done", RECTYPE_PREFETCH);
}

static void
rpzfetch_done(void *arg) {
	cleanup_after_fetch(arg, "rpzfetch_done", RECTYPE_RPZ);
}

static void
stale_refresh_done(void *arg) {
	cleanup_after_fetch(arg, "stale_refresh_done", RECTYPE_STALE_REFRESH);
}

/*
 * Try initiating a fetch for the given 'qname' and 'qtype' (using the slot in
 * the 'recursions' array indicated by 'recursion_type') that will be
 * associated with 'client'.  If the recursive clients quota (or even soft
 * quota) is reached or some other error occurs, just return without starting
 * the fetch.  If a fetch is successfully created, its results will be cached
 * upon successful completion, but no further actions will be taken afterwards.
 */
static void
fetch_and_forget(ns_client_t *client, dns_name_t *qname, dns_rdatatype_t qtype,
		 ns_query_rectype_t recursion_type) {
	dns_rdataset_t *tmprdataset;
	isc_sockaddr_t *peeraddr;
	unsigned int options;
	isc_job_cb cb;
	isc_nmhandle_t **handlep;
	dns_fetch_t **fetchp;
	isc_result_t result;

	result = recursionquotatype_attach_hard(client);
	if (result != ISC_R_SUCCESS) {
		return;
	}

	tmprdataset = ns_client_newrdataset(client);

	if (!TCP(client)) {
		peeraddr = &client->inner.peeraddr;
	} else {
		peeraddr = NULL;
	}

	switch (recursion_type) {
	case RECTYPE_PREFETCH:
		options = client->query.fetchoptions | DNS_FETCHOPT_PREFETCH;
		cb = prefetch_done;
		break;
	case RECTYPE_RPZ:
		options = client->query.fetchoptions;
		cb = rpzfetch_done;
		break;
	case RECTYPE_STALE_REFRESH:
		options = client->query.fetchoptions;
		cb = stale_refresh_done;
		break;
	default:
		UNREACHABLE();
	}

	handlep = &client->query.recursions[recursion_type].handle;
	fetchp = &client->query.recursions[recursion_type].fetch;

	isc_nmhandle_attach(client->inner.handle, handlep);
	result = dns_resolver_createfetch(
		client->inner.view->resolver, qname, qtype, NULL, NULL, NULL,
		peeraddr, client->message->id, options, 0, NULL,
		client->query.qc, client->manager->loop, cb, client, NULL,
		tmprdataset, NULL, fetchp);
	if (result != ISC_R_SUCCESS) {
		ns_client_putrdataset(client, &tmprdataset);
		isc_nmhandle_detach(handlep);
		recursionquotatype_detach(client);
	}
}

static void
query_stale_refresh(ns_client_t *client, dns_name_t *qname,
		    dns_rdataset_t *rdataset) {
	CTRACE(ISC_LOG_DEBUG(3), "query_stale_refresh");

	bool stale_refresh_window = false;
	bool stale_rrset = true;

	if (rdataset != NULL) {
		stale_refresh_window = (STALE_WINDOW(rdataset) &&
					(client->query.dboptions &
					 DNS_DBFIND_STALEENABLED) != 0);
		stale_rrset = STALE(rdataset);
	}

	if (FETCH_RECTYPE_STALE_REFRESH(client) != NULL ||
	    (client->query.dboptions & DNS_DBFIND_STALETIMEOUT) == 0 ||
	    !stale_rrset || stale_refresh_window)
	{
		return;
	}

	char namebuf[DNS_NAME_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];
	dns_name_format(qname, namebuf, sizeof(namebuf));
	dns_rdatatype_format(client->query.qtype, typebuf, sizeof(typebuf));
	isc_log_write(NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY,
		      ISC_LOG_INFO,
		      "%s %s stale answer used, an attempt "
		      "to refresh the RRset will still be "
		      "made",
		      namebuf, typebuf);

	client->query.dboptions &= ~(DNS_DBFIND_STALETIMEOUT |
				     DNS_DBFIND_STALEOK |
				     DNS_DBFIND_STALEENABLED);

	fetch_and_forget(client, qname, client->query.qtype,
			 RECTYPE_STALE_REFRESH);
}

static void
query_stale_refresh_ncache(ns_client_t *client) {
	dns_name_t *qname;

	if (client->query.origqname != NULL) {
		qname = client->query.origqname;
	} else {
		qname = client->query.qname;
	}
	query_stale_refresh(client, qname, NULL);
}

static void
query_prefetch(ns_client_t *client, dns_name_t *qname,
	       dns_rdataset_t *rdataset) {
	CTRACE(ISC_LOG_DEBUG(3), "query_prefetch");

	if (FETCH_RECTYPE_PREFETCH(client) != NULL ||
	    client->inner.view->prefetch_trigger == 0U ||
	    rdataset->ttl > client->inner.view->prefetch_trigger ||
	    !rdataset->attributes.prefetch)
	{
		/* maybe refresh stale data */
		query_stale_refresh(client, qname, rdataset);
		return;
	}

	fetch_and_forget(client, qname, rdataset->type, RECTYPE_PREFETCH);

	dns_rdataset_clearprefetch(rdataset);
	ns_stats_increment(client->manager->sctx->nsstats,
			   ns_statscounter_prefetch);

	return;
}

static void
rpz_clean(dns_zone_t **zonep, dns_db_t **dbp, dns_dbnode_t **nodep,
	  dns_rdataset_t **rdatasetp) {
	if (nodep != NULL && *nodep != NULL) {
		REQUIRE(dbp != NULL && *dbp != NULL);
		dns_db_detachnode(nodep);
	}
	if (dbp != NULL && *dbp != NULL) {
		dns_db_detach(dbp);
	}
	if (zonep != NULL && *zonep != NULL) {
		dns_zone_detach(zonep);
	}
	if (rdatasetp != NULL && *rdatasetp != NULL &&
	    dns_rdataset_isassociated(*rdatasetp))
	{
		dns_rdataset_disassociate(*rdatasetp);
	}
}

static void
rpz_match_clear(dns_rpz_st_t *st) {
	rpz_clean(&st->m.zone, &st->m.db, &st->m.node, &st->m.rdataset);
	st->m.version = NULL;
}

static isc_result_t
rpz_ready(ns_client_t *client, dns_rdataset_t **rdatasetp) {
	REQUIRE(rdatasetp != NULL);

	CTRACE(ISC_LOG_DEBUG(3), "rpz_ready");

	if (*rdatasetp == NULL) {
		*rdatasetp = ns_client_newrdataset(client);
	} else if (dns_rdataset_isassociated(*rdatasetp)) {
		dns_rdataset_disassociate(*rdatasetp);
	}
	return ISC_R_SUCCESS;
}

static void
rpz_st_clear(ns_client_t *client) {
	dns_rpz_st_t *st = client->query.rpz_st;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_st_clear");

	if (st->m.rdataset != NULL) {
		ns_client_putrdataset(client, &st->m.rdataset);
	}
	rpz_match_clear(st);

	rpz_clean(NULL, &st->r.db, NULL, NULL);
	if (st->r.ns_rdataset != NULL) {
		ns_client_putrdataset(client, &st->r.ns_rdataset);
	}
	if (st->r.r_rdataset != NULL) {
		ns_client_putrdataset(client, &st->r.r_rdataset);
	}

	rpz_clean(&st->q.zone, &st->q.db, &st->q.node, NULL);
	if (st->q.rdataset != NULL) {
		ns_client_putrdataset(client, &st->q.rdataset);
	}
	if (st->q.sigrdataset != NULL) {
		ns_client_putrdataset(client, &st->q.sigrdataset);
	}
	st->state = 0;
	st->m.type = DNS_RPZ_TYPE_BAD;
	st->m.policy = DNS_RPZ_POLICY_MISS;
}

static dns_rpz_zbits_t
rpz_get_zbits(ns_client_t *client, dns_rdatatype_t ip_type,
	      dns_rpz_type_t rpz_type) {
	dns_rpz_st_t *st;
	dns_rpz_zbits_t zbits = 0;

	REQUIRE(client != NULL);
	REQUIRE(client->query.rpz_st != NULL);

	st = client->query.rpz_st;

	switch (rpz_type) {
	case DNS_RPZ_TYPE_CLIENT_IP:
		zbits = st->have.client_ip;
		break;
	case DNS_RPZ_TYPE_QNAME:
		zbits = st->have.qname;
		break;
	case DNS_RPZ_TYPE_IP:
		if (ip_type == dns_rdatatype_a) {
			zbits = st->have.ipv4;
		} else if (ip_type == dns_rdatatype_aaaa) {
			zbits = st->have.ipv6;
		} else {
			zbits = st->have.ip;
		}
		break;
	case DNS_RPZ_TYPE_NSDNAME:
		zbits = st->have.nsdname;
		break;
	case DNS_RPZ_TYPE_NSIP:
		if (ip_type == dns_rdatatype_a) {
			zbits = st->have.nsipv4;
		} else if (ip_type == dns_rdatatype_aaaa) {
			zbits = st->have.nsipv6;
		} else {
			zbits = st->have.nsip;
		}
		break;
	default:
		UNREACHABLE();
	}

	/*
	 * Choose
	 *	the earliest configured policy zone (rpz->num)
	 *	QNAME over IP over NSDNAME over NSIP (rpz_type)
	 *	the smallest name,
	 *	the longest IP address prefix,
	 *	the lexically smallest address.
	 */
	if (st->m.policy != DNS_RPZ_POLICY_MISS) {
		if (st->m.type >= rpz_type) {
			zbits &= DNS_RPZ_ZMASK(st->m.rpz->num);
		} else {
			zbits &= DNS_RPZ_ZMASK(st->m.rpz->num) >> 1;
		}
	}

	/*
	 * If the client wants recursion, allow only compatible policies.
	 */
	if (!RECURSIONOK(client)) {
		zbits &= st->popt.no_rd_ok;
	}

	return zbits;
}

static void
query_rpzfetch(ns_client_t *client, dns_name_t *qname, dns_rdatatype_t type) {
	CTRACE(ISC_LOG_DEBUG(3), "query_rpzfetch");

	if (FETCH_RECTYPE_RPZ(client) != NULL) {
		return;
	}

	fetch_and_forget(client, qname, type, RECTYPE_RPZ);
}

/*
 * Get an NS, A, or AAAA rrset related to the response for the client
 * to check the contents of that rrset for hits by eligible policy zones.
 */
static isc_result_t
rpz_rrset_find(ns_client_t *client, dns_name_t *name, dns_rdatatype_t type,
	       unsigned int options, dns_rpz_type_t rpz_type, dns_db_t **dbp,
	       dns_dbversion_t *version, dns_rdataset_t **rdatasetp,
	       bool resuming) {
	dns_rpz_st_t *st;
	bool is_zone;
	dns_dbnode_t *node;
	dns_fixedname_t fixed;
	dns_name_t *found;
	isc_result_t result;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rrset_find");

	st = client->query.rpz_st;
	if ((st->state & DNS_RPZ_RECURSING) != 0) {
		INSIST(st->r.r_type == type);
		INSIST(dns_name_equal(name, st->r_name));
		INSIST(*rdatasetp == NULL ||
		       !dns_rdataset_isassociated(*rdatasetp));
		st->state &= ~DNS_RPZ_RECURSING;
		RESTORE(*dbp, st->r.db);
		if (*rdatasetp != NULL) {
			ns_client_putrdataset(client, rdatasetp);
		}
		RESTORE(*rdatasetp, st->r.r_rdataset);
		result = st->r.r_result;
		if (result == DNS_R_DELEGATION) {
			CTRACE(ISC_LOG_ERROR, "RPZ recursing");
			rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, name,
				     rpz_type, "rpz_rrset_find(1)", result);
			st->m.policy = DNS_RPZ_POLICY_ERROR;
			result = DNS_R_SERVFAIL;
		}
		return result;
	}

	result = rpz_ready(client, rdatasetp);
	if (result != ISC_R_SUCCESS) {
		st->m.policy = DNS_RPZ_POLICY_ERROR;
		return result;
	}
	if (*dbp != NULL) {
		is_zone = false;
	} else {
		dns_zone_t *zone;

		version = NULL;
		zone = NULL;
		result = query_getdb(client, name, type,
				     (dns_getdb_options_t){ 0 }, &zone, dbp,
				     &version, &is_zone);
		if (result != ISC_R_SUCCESS) {
			rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, name,
				     rpz_type, "rpz_rrset_find(2)", result);
			st->m.policy = DNS_RPZ_POLICY_ERROR;
			if (zone != NULL) {
				dns_zone_detach(&zone);
			}
			return result;
		}
		if (zone != NULL) {
			dns_zone_detach(&zone);
		}
	}

	node = NULL;
	found = dns_fixedname_initname(&fixed);
	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);
	result = dns_db_findext(*dbp, name, version, type, options,
				client->inner.now, &node, found, &cm, &ci,
				*rdatasetp, NULL);
	if (result == DNS_R_DELEGATION && is_zone && USECACHE(client)) {
		/*
		 * Try the cache if we're authoritative for an
		 * ancestor but not the domain itself.
		 */
		rpz_clean(NULL, dbp, &node, rdatasetp);
		version = NULL;
		dns_db_attach(client->inner.view->cachedb, dbp);
		result = dns_db_findext(*dbp, name, version, type, 0,
					client->inner.now, &node, found, &cm,
					&ci, *rdatasetp, NULL);
	}
	rpz_clean(NULL, dbp, &node, NULL);
	if (result == DNS_R_DELEGATION) {
		rpz_clean(NULL, NULL, NULL, rdatasetp);
		/*
		 * Recurse for NS rrset or A or AAAA rrset for an NS.
		 * Do not recurse for addresses for the query name.
		 */
		if (rpz_type == DNS_RPZ_TYPE_IP) {
			result = DNS_R_NXRRSET;
		} else if (!client->inner.view->rpzs->p.nsip_wait_recurse ||
			   (!client->inner.view->rpzs->p.nsdname_wait_recurse &&
			    rpz_type == DNS_RPZ_TYPE_NSDNAME))
		{
			query_rpzfetch(client, name, type);
			result = DNS_R_NXRRSET;
		} else {
			dns_name_copy(name, st->r_name);
			result = ns_query_recurse(client, type, st->r_name,
						  NULL, NULL, resuming);
			if (result == ISC_R_SUCCESS) {
				st->state |= DNS_RPZ_RECURSING;
				result = DNS_R_DELEGATION;
			}
		}
	}
	return result;
}

/*
 * Compute a policy owner name, p_name, in a policy zone given the needed
 * policy type and the trigger name.
 */
static isc_result_t
rpz_get_p_name(ns_client_t *client, dns_name_t *p_name, dns_rpz_zone_t *rpz,
	       dns_rpz_type_t rpz_type, dns_name_t *trig_name) {
	dns_name_t prefix, *suffix;
	unsigned int first, labels;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_get_p_name");

	/*
	 * The policy owner name consists of a suffix depending on the type
	 * and policy zone and a prefix that is the longest possible string
	 * from the trigger name that keesp the resulting policy owner name
	 * from being too long.
	 */
	switch (rpz_type) {
	case DNS_RPZ_TYPE_CLIENT_IP:
		suffix = &rpz->client_ip;
		break;
	case DNS_RPZ_TYPE_QNAME:
		suffix = &rpz->origin;
		break;
	case DNS_RPZ_TYPE_IP:
		suffix = &rpz->ip;
		break;
	case DNS_RPZ_TYPE_NSDNAME:
		suffix = &rpz->nsdname;
		break;
	case DNS_RPZ_TYPE_NSIP:
		suffix = &rpz->nsip;
		break;
	default:
		UNREACHABLE();
	}

	/*
	 * Start with relative version of the full trigger name,
	 * and trim enough allow the addition of the suffix.
	 */
	dns_name_init(&prefix);
	labels = dns_name_countlabels(trig_name);
	first = 0;
	for (;;) {
		dns_name_getlabelsequence(trig_name, first, labels - first - 1,
					  &prefix);
		result = dns_name_concatenate(&prefix, suffix, p_name);
		if (result == ISC_R_SUCCESS) {
			break;
		}
		INSIST(result == DNS_R_NAMETOOLONG);
		/*
		 * Trim the trigger name until the combination is not too long.
		 */
		if (labels - first < 2) {
			rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, suffix,
				     rpz_type, "concatenate()", result);
			return ISC_R_FAILURE;
		}
		/*
		 * Complain once about trimming the trigger name.
		 */
		if (first == 0) {
			rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL1, suffix,
				     rpz_type, "concatenate()", result);
		}
		++first;
	}
	return ISC_R_SUCCESS;
}

/*
 * Look in policy zone rpz for a policy of rpz_type by p_name.
 * The self-name (usually the client qname or an NS name) is compared with
 * the target of a CNAME policy for the old style passthru encoding.
 * If found, the policy is recorded in *zonep, *dbp, *versionp, *nodep,
 * *rdatasetp, and *policyp.
 * The target DNS type, qtype, chooses the best rdataset for *rdatasetp.
 * The caller must decide if the found policy is most suitable, including
 * better than a previously found policy.
 * If it is best, the caller records it in client->query.rpz_st->m.
 */
static isc_result_t
rpz_find_p(ns_client_t *client, dns_name_t *self_name, dns_rdatatype_t qtype,
	   dns_name_t *p_name, dns_rpz_zone_t *rpz, dns_rpz_type_t rpz_type,
	   dns_zone_t **zonep, dns_db_t **dbp, dns_dbversion_t **versionp,
	   dns_dbnode_t **nodep, dns_rdataset_t **rdatasetp,
	   dns_rpz_policy_t *policyp) {
	dns_fixedname_t foundf;
	dns_name_t *found;
	isc_result_t result;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	bool found_a = false;

	REQUIRE(nodep != NULL);

	CTRACE(ISC_LOG_DEBUG(3), "rpz_find_p");

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * Try to find either a CNAME or the type of record demanded by the
	 * request from the policy zone.
	 */
	rpz_clean(zonep, dbp, nodep, rdatasetp);
	result = rpz_ready(client, rdatasetp);
	if (result != ISC_R_SUCCESS) {
		CTRACE(ISC_LOG_ERROR, "rpz_ready() failed");
		return DNS_R_SERVFAIL;
	}
	*versionp = NULL;
	result = rpz_getdb(client, p_name, rpz_type, zonep, dbp, versionp);
	if (result != ISC_R_SUCCESS) {
		return DNS_R_NXDOMAIN;
	}
	found = dns_fixedname_initname(&foundf);

	result = dns_db_findext(*dbp, p_name, *versionp, dns_rdatatype_any, 0,
				client->inner.now, nodep, found, &cm, &ci,
				*rdatasetp, NULL);
	/*
	 * Choose the best rdataset if we found something.
	 */
	if (result == ISC_R_SUCCESS) {
		dns_rdatasetiter_t *rdsiter = NULL;
		bool match = false;

		result = dns_db_allrdatasets(*dbp, *nodep, *versionp, 0, 0,
					     &rdsiter);
		if (result != ISC_R_SUCCESS) {
			rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, p_name,
				     rpz_type, "allrdatasets()", result);
			CTRACE(ISC_LOG_ERROR,
			       "rpz_find_p: allrdatasets failed");
			return DNS_R_SERVFAIL;
		}
		if (qtype == dns_rdatatype_aaaa &&
		    !ISC_LIST_EMPTY(client->inner.view->dns64))
		{
			DNS_RDATASETITER_FOREACH(rdsiter) {
				dns_rdatasetiter_current(rdsiter, *rdatasetp);
				if ((*rdatasetp)->type == dns_rdatatype_a) {
					found_a = true;
				}
				dns_rdataset_disassociate(*rdatasetp);
			}
		}
		DNS_RDATASETITER_FOREACH(rdsiter) {
			dns_rdatasetiter_current(rdsiter, *rdatasetp);
			if ((*rdatasetp)->type == dns_rdatatype_cname ||
			    (*rdatasetp)->type == qtype)
			{
				match = true;
				break;
			}
			dns_rdataset_disassociate(*rdatasetp);
		}
		dns_rdatasetiter_destroy(&rdsiter);
		if (!match) {
			/*
			 * Ask again to get the right DNS_R_DNAME/NXRRSET/...
			 * result if there is neither a CNAME nor target type.
			 */
			if (dns_rdataset_isassociated(*rdatasetp)) {
				dns_rdataset_disassociate(*rdatasetp);
			}
			dns_db_detachnode(nodep);

			if (dns_rdatatype_issig(qtype)) {
				result = DNS_R_NXRRSET;
			} else {
				result = dns_db_findext(
					*dbp, p_name, *versionp, qtype, 0,
					client->inner.now, nodep, found, &cm,
					&ci, *rdatasetp, NULL);
			}
		}
	}
	switch (result) {
	case ISC_R_SUCCESS:
		if ((*rdatasetp)->type != dns_rdatatype_cname) {
			*policyp = DNS_RPZ_POLICY_RECORD;
		} else {
			*policyp = dns_rpz_decode_cname(rpz, *rdatasetp,
							self_name);
			if ((*policyp == DNS_RPZ_POLICY_RECORD ||
			     *policyp == DNS_RPZ_POLICY_WILDCNAME) &&
			    qtype != dns_rdatatype_cname &&
			    qtype != dns_rdatatype_any)
			{
				return DNS_R_CNAME;
			}
		}
		return ISC_R_SUCCESS;
	case DNS_R_NXRRSET:
		if (found_a) {
			*policyp = DNS_RPZ_POLICY_DNS64;
		} else {
			*policyp = DNS_RPZ_POLICY_NODATA;
		}
		return result;
	case DNS_R_DNAME:
	/*
	 * DNAME policy RRs have very few if any uses that are not
	 * better served with simple wildcards.  Making them work would
	 * require complications to get the number of labels matched
	 * in the name or the found name to the main DNS_R_DNAME case
	 * in query_dname().  The domain also does not appear in the
	 * summary database at the right level, so this happens only
	 * with a single policy zone when we have no summary database.
	 * Treat it as a miss.
	 */
	case DNS_R_NXDOMAIN:
	case DNS_R_EMPTYNAME:
		return DNS_R_NXDOMAIN;
	default:
		rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, p_name, rpz_type, "",
			     result);
		CTRACE(ISC_LOG_ERROR, "rpz_find_p: unexpected result");
		return DNS_R_SERVFAIL;
	}
}

static void
rpz_save_p(dns_rpz_st_t *st, dns_rpz_zone_t *rpz, dns_rpz_type_t rpz_type,
	   dns_rpz_policy_t policy, dns_name_t *p_name, dns_rpz_prefix_t prefix,
	   isc_result_t result, dns_zone_t **zonep, dns_db_t **dbp,
	   dns_dbnode_t **nodep, dns_rdataset_t **rdatasetp,
	   dns_dbversion_t *version) {
	dns_rdataset_t *trdataset = NULL;

	rpz_match_clear(st);
	st->m.rpz = rpz;
	st->m.type = rpz_type;
	st->m.policy = policy;
	dns_name_copy(p_name, st->p_name);
	st->m.prefix = prefix;
	st->m.result = result;
	SAVE(st->m.zone, *zonep);
	SAVE(st->m.db, *dbp);
	SAVE(st->m.node, *nodep);
	if (*rdatasetp != NULL && dns_rdataset_isassociated(*rdatasetp)) {
		/*
		 * Save the replacement rdataset from the policy
		 * and make the previous replacement rdataset scratch.
		 */
		SAVE(trdataset, st->m.rdataset);
		SAVE(st->m.rdataset, *rdatasetp);
		SAVE(*rdatasetp, trdataset);
		st->m.ttl = ISC_MIN(st->m.rdataset->ttl, rpz->max_policy_ttl);
	} else {
		st->m.ttl = ISC_MIN(DNS_RPZ_TTL_DEFAULT, rpz->max_policy_ttl);
	}
	SAVE(st->m.version, version);
}

/*
 * Check this address in every eligible policy zone.
 */
static isc_result_t
rpz_rewrite_ip(ns_client_t *client, const isc_netaddr_t *netaddr,
	       dns_rdatatype_t qtype, dns_rpz_type_t rpz_type,
	       dns_rpz_zbits_t zbits, dns_rdataset_t **p_rdatasetp) {
	dns_rpz_zones_t *rpzs;
	dns_rpz_st_t *st;
	dns_rpz_zone_t *rpz;
	dns_rpz_prefix_t prefix;
	dns_rpz_num_t rpz_num;
	dns_fixedname_t ip_namef, p_namef;
	dns_name_t *ip_name, *p_name;
	dns_zone_t *p_zone;
	dns_db_t *p_db;
	dns_dbversion_t *p_version;
	dns_dbnode_t *p_node;
	dns_rpz_policy_t policy;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip");

	rpzs = client->inner.view->rpzs;
	st = client->query.rpz_st;

	ip_name = dns_fixedname_initname(&ip_namef);

	p_zone = NULL;
	p_db = NULL;
	p_node = NULL;

	while (zbits != 0) {
		rpz_num = dns_rpz_find_ip(rpzs, rpz_type, zbits, netaddr,
					  ip_name, &prefix);
		if (rpz_num == DNS_RPZ_INVALID_NUM) {
			break;
		}
		zbits &= (DNS_RPZ_ZMASK(rpz_num) >> 1);

		/*
		 * Do not try applying policy zones that cannot replace a
		 * previously found policy zone.
		 * Stop looking if the next best choice cannot
		 * replace what we already have.
		 */
		rpz = rpzs->zones[rpz_num];
		if (st->m.policy != DNS_RPZ_POLICY_MISS) {
			if (st->m.rpz->num < rpz->num) {
				break;
			}
			if (st->m.rpz->num == rpz->num &&
			    (st->m.type < rpz_type || st->m.prefix > prefix))
			{
				break;
			}
		}

		/*
		 * Get the policy for a prefix at least as long
		 * as the prefix of the entry we had before.
		 */
		p_name = dns_fixedname_initname(&p_namef);
		result = rpz_get_p_name(client, p_name, rpz, rpz_type, ip_name);
		if (result != ISC_R_SUCCESS) {
			continue;
		}
		result = rpz_find_p(client, ip_name, qtype, p_name, rpz,
				    rpz_type, &p_zone, &p_db, &p_version,
				    &p_node, p_rdatasetp, &policy);
		switch (result) {
		case DNS_R_NXDOMAIN:
			/*
			 * Continue after a policy record that is missing
			 * contrary to the summary data.  The summary
			 * data can out of date during races with and among
			 * policy zone updates.
			 */
			CTRACE(ISC_LOG_ERROR, "rpz_rewrite_ip: mismatched "
					      "summary data; "
					      "continuing");
			continue;
		case DNS_R_SERVFAIL:
			rpz_clean(&p_zone, &p_db, &p_node, p_rdatasetp);
			st->m.policy = DNS_RPZ_POLICY_ERROR;
			return DNS_R_SERVFAIL;
		default:
			/*
			 * Forget this policy if it is not preferable
			 * to the previously found policy.
			 * If this policy is not good, then stop looking
			 * because none of the later policy zones would work.
			 *
			 * With more than one applicable policy, prefer
			 * the earliest configured policy,
			 * client-IP over QNAME over IP over NSDNAME over NSIP,
			 * the longest prefix
			 * the lexically smallest address.
			 * dns_rpz_find_ip() ensures st->m.rpz->num >= rpz->num.
			 * We can compare new and current p_name because
			 * both are of the same type and in the same zone.
			 * The tests above eliminate other reasons to
			 * reject this policy.  If this policy can't work,
			 * then neither can later zones.
			 */
			if (st->m.policy != DNS_RPZ_POLICY_MISS &&
			    rpz->num == st->m.rpz->num &&
			    (st->m.type == rpz_type && st->m.prefix == prefix &&
			     0 > dns_name_rdatacompare(st->p_name, p_name)))
			{
				break;
			}

			/*
			 * Stop checking after saving an enabled hit in this
			 * policy zone.  The radix tree in the policy zone
			 * ensures that we found the longest match.
			 */
			if (rpz->policy != DNS_RPZ_POLICY_DISABLED) {
				CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip: "
							 "rpz_save_p");
				rpz_save_p(st, rpz, rpz_type, policy, p_name,
					   prefix, result, &p_zone, &p_db,
					   &p_node, p_rdatasetp, p_version);
				break;
			}

			/*
			 * Log DNS_RPZ_POLICY_DISABLED zones
			 * and try the next eligible policy zone.
			 */
			rpz_log_rewrite(client, true, policy, rpz_type, p_zone,
					p_name, NULL, rpz_num);
		}
	}

	rpz_clean(&p_zone, &p_db, &p_node, p_rdatasetp);
	return ISC_R_SUCCESS;
}

/*
 * Check the IP addresses in the A or AAAA rrsets for name against
 * all eligible rpz_type (IP or NSIP) response policy rewrite rules.
 */
static isc_result_t
rpz_rewrite_ip_rrset(ns_client_t *client, dns_name_t *name,
		     dns_rdatatype_t qtype, dns_rpz_type_t rpz_type,
		     dns_rdatatype_t ip_type, dns_db_t **ip_dbp,
		     dns_dbversion_t *ip_version, dns_rdataset_t **ip_rdatasetp,
		     dns_rdataset_t **p_rdatasetp, bool resuming) {
	dns_rpz_zbits_t zbits;
	isc_netaddr_t netaddr;
	struct in_addr ina;
	struct in6_addr in6a;
	isc_result_t result;
	unsigned int options = client->query.dboptions | DNS_DBFIND_GLUEOK;
	bool done = false;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip_rrset");

	do {
		zbits = rpz_get_zbits(client, ip_type, rpz_type);
		if (zbits == 0) {
			return ISC_R_SUCCESS;
		}

		/*
		 * Get the A or AAAA rdataset.
		 */
		result = rpz_rrset_find(client, name, ip_type, options,
					rpz_type, ip_dbp, ip_version,
					ip_rdatasetp, resuming);
		switch (result) {
		case ISC_R_SUCCESS:
		case DNS_R_GLUE:
		case DNS_R_ZONECUT:
			break;
		case DNS_R_EMPTYNAME:
		case DNS_R_EMPTYWILD:
		case DNS_R_NXDOMAIN:
		case DNS_R_NCACHENXDOMAIN:
		case DNS_R_NXRRSET:
		case DNS_R_NCACHENXRRSET:
		case ISC_R_NOTFOUND:
			return ISC_R_SUCCESS;
		case DNS_R_DELEGATION:
		case DNS_R_DUPLICATE:
		case DNS_R_DROP:
			return result;
		case DNS_R_CNAME:
		case DNS_R_DNAME:
			rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL1, name,
				     rpz_type, "NS address rewrite rrset",
				     result);
			return ISC_R_SUCCESS;
		default:
			if (client->query.rpz_st->m.policy !=
			    DNS_RPZ_POLICY_ERROR)
			{
				client->query.rpz_st->m.policy =
					DNS_RPZ_POLICY_ERROR;
				rpz_log_fail(client, DNS_RPZ_ERROR_LEVEL, name,
					     rpz_type,
					     "NS address rewrite rrset",
					     result);
			}
			CTRACE(ISC_LOG_ERROR,
			       "rpz_rewrite_ip_rrset: unexpected "
			       "result");
			return DNS_R_SERVFAIL;
		}

		/*
		 * If we are processing glue setup for the next loop
		 * otherwise we are done.
		 */
		if (result == DNS_R_GLUE) {
			options = client->query.dboptions;
		} else {
			options = client->query.dboptions | DNS_DBFIND_GLUEOK;
			done = true;
		}

		/*
		 * Check all of the IP addresses in the rdataset.
		 */
		DNS_RDATASET_FOREACH(*ip_rdatasetp) {
			dns_rdata_t rdata = DNS_RDATA_INIT;
			dns_rdataset_current(*ip_rdatasetp, &rdata);
			switch (rdata.type) {
			case dns_rdatatype_a:
				INSIST(rdata.length == 4);
				memmove(&ina.s_addr, rdata.data, 4);
				isc_netaddr_fromin(&netaddr, &ina);
				break;
			case dns_rdatatype_aaaa:
				INSIST(rdata.length == 16);
				memmove(in6a.s6_addr, rdata.data, 16);
				isc_netaddr_fromin6(&netaddr, &in6a);
				break;
			default:
				continue;
			}

			result = rpz_rewrite_ip(client, &netaddr, qtype,
						rpz_type, zbits, p_rdatasetp);
			if (result != ISC_R_SUCCESS) {
				return result;
			}
		}
	} while (!done &&
		 client->query.rpz_st->m.policy == DNS_RPZ_POLICY_MISS);

	return ISC_R_SUCCESS;
}

/*
 * Look for IP addresses in A and AAAA rdatasets
 * that trigger all eligible IP or NSIP policy rules.
 */
static isc_result_t
rpz_rewrite_ip_rrsets(ns_client_t *client, dns_name_t *name,
		      dns_rdatatype_t qtype, dns_rpz_type_t rpz_type,
		      dns_rdataset_t **ip_rdatasetp, bool resuming) {
	dns_rpz_st_t *st;
	dns_dbversion_t *ip_version;
	dns_db_t *ip_db;
	dns_rdataset_t *p_rdataset;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ip_rrsets");

	st = client->query.rpz_st;
	ip_version = NULL;
	ip_db = NULL;
	p_rdataset = NULL;
	if ((st->state & DNS_RPZ_DONE_IPv4) == 0 &&
	    (qtype == dns_rdatatype_a || qtype == dns_rdatatype_any ||
	     rpz_type == DNS_RPZ_TYPE_NSIP))
	{
		/*
		 * Rewrite based on an IPv4 address that will appear
		 * in the ANSWER section or if we are checking IP addresses.
		 */
		result = rpz_rewrite_ip_rrset(
			client, name, qtype, rpz_type, dns_rdatatype_a, &ip_db,
			ip_version, ip_rdatasetp, &p_rdataset, resuming);
		if (result == ISC_R_SUCCESS) {
			st->state |= DNS_RPZ_DONE_IPv4;
		}
	} else {
		result = ISC_R_SUCCESS;
	}
	if (result == ISC_R_SUCCESS &&
	    (qtype == dns_rdatatype_aaaa || qtype == dns_rdatatype_any ||
	     rpz_type == DNS_RPZ_TYPE_NSIP))
	{
		/*
		 * Rewrite based on IPv6 addresses that will appear
		 * in the ANSWER section or if we are checking IP addresses.
		 */
		result = rpz_rewrite_ip_rrset(client, name, qtype, rpz_type,
					      dns_rdatatype_aaaa, &ip_db,
					      ip_version, ip_rdatasetp,
					      &p_rdataset, resuming);
	}
	if (ip_db != NULL) {
		dns_db_detach(&ip_db);
	}
	ns_client_putrdataset(client, &p_rdataset);
	return result;
}

/*
 * Try to rewrite a request for a qtype rdataset based on the trigger name
 * trig_name and rpz_type (DNS_RPZ_TYPE_QNAME or DNS_RPZ_TYPE_NSDNAME).
 * Record the results including the replacement rdataset if any
 * in client->query.rpz_st.
 * *rdatasetp is a scratch rdataset.
 */
static isc_result_t
rpz_rewrite_name(ns_client_t *client, dns_name_t *trig_name,
		 dns_rdatatype_t qtype, dns_rpz_type_t rpz_type,
		 dns_rpz_zbits_t allowed_zbits, dns_rdataset_t **rdatasetp) {
	dns_rpz_zones_t *rpzs;
	dns_rpz_zone_t *rpz;
	dns_rpz_st_t *st;
	dns_fixedname_t p_namef;
	dns_name_t *p_name;
	dns_rpz_zbits_t zbits;
	dns_rpz_num_t rpz_num;
	dns_zone_t *p_zone;
	dns_db_t *p_db;
	dns_dbversion_t *p_version;
	dns_dbnode_t *p_node;
	dns_rpz_policy_t policy;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_name");

	rpzs = client->inner.view->rpzs;
	st = client->query.rpz_st;

	zbits = rpz_get_zbits(client, qtype, rpz_type);
	zbits &= allowed_zbits;
	if (zbits == 0) {
		return ISC_R_SUCCESS;
	}

	/*
	 * Use the summary database to find the bit mask of policy zones
	 * with policies for this trigger name. We do this even if there
	 * is only one eligible policy zone so that wildcard triggers
	 * are matched correctly, and not into their parent.
	 */
	zbits = dns_rpz_find_name(rpzs, rpz_type, zbits, trig_name);
	if (zbits == 0) {
		return ISC_R_SUCCESS;
	}

	p_name = dns_fixedname_initname(&p_namef);

	p_zone = NULL;
	p_db = NULL;
	p_node = NULL;

	/*
	 * Check the trigger name in every policy zone that the summary data
	 * says has a hit for the trigger name.
	 * Most of the time there are no eligible zones and the summary data
	 * keeps us from getting this far.
	 * We check the most eligible zone first and so usually check only
	 * one policy zone.
	 */
	for (rpz_num = 0; zbits != 0; ++rpz_num, zbits >>= 1) {
		if ((zbits & 1) == 0) {
			continue;
		}

		/*
		 * Do not check policy zones that cannot replace a previously
		 * found policy.
		 */
		rpz = rpzs->zones[rpz_num];
		if (st->m.policy != DNS_RPZ_POLICY_MISS) {
			if (st->m.rpz->num < rpz->num) {
				break;
			}
			if (st->m.rpz->num == rpz->num && st->m.type < rpz_type)
			{
				break;
			}
		}

		/*
		 * Get the next policy zone's record for this trigger name.
		 */
		result = rpz_get_p_name(client, p_name, rpz, rpz_type,
					trig_name);
		if (result != ISC_R_SUCCESS) {
			continue;
		}
		result = rpz_find_p(client, trig_name, qtype, p_name, rpz,
				    rpz_type, &p_zone, &p_db, &p_version,
				    &p_node, rdatasetp, &policy);
		switch (result) {
		case DNS_R_NXDOMAIN:
			/*
			 * Continue after a missing policy record
			 * contrary to the summary data.  The summary
			 * data can out of date during races with and among
			 * policy zone updates.
			 */
			CTRACE(ISC_LOG_ERROR, "rpz_rewrite_name: mismatched "
					      "summary data; "
					      "continuing");
			continue;
		case DNS_R_SERVFAIL:
			rpz_clean(&p_zone, &p_db, &p_node, rdatasetp);
			st->m.policy = DNS_RPZ_POLICY_ERROR;
			return DNS_R_SERVFAIL;
		default:
			/*
			 * With more than one applicable policy, prefer
			 * the earliest configured policy,
			 * client-IP over QNAME over IP over NSDNAME over NSIP,
			 * and the smallest name.
			 * We known st->m.rpz->num >= rpz->num  and either
			 * st->m.rpz->num > rpz->num or st->m.type >= rpz_type
			 */
			if (st->m.policy != DNS_RPZ_POLICY_MISS &&
			    rpz->num == st->m.rpz->num &&
			    (st->m.type < rpz_type ||
			     (st->m.type == rpz_type &&
			      0 >= dns_name_compare(p_name, st->p_name))))
			{
				continue;
			}

			if (rpz->policy != DNS_RPZ_POLICY_DISABLED) {
				CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_name: "
							 "rpz_save_p");
				rpz_save_p(st, rpz, rpz_type, policy, p_name, 0,
					   result, &p_zone, &p_db, &p_node,
					   rdatasetp, p_version);
				/*
				 * After a hit, higher numbered policy zones
				 * are irrelevant
				 */
				rpz_clean(&p_zone, &p_db, &p_node, rdatasetp);
				return ISC_R_SUCCESS;
			}
			/*
			 * Log DNS_RPZ_POLICY_DISABLED zones
			 * and try the next eligible policy zone.
			 */
			rpz_log_rewrite(client, true, policy, rpz_type, p_zone,
					p_name, NULL, rpz_num);
			break;
		}
	}

	rpz_clean(&p_zone, &p_db, &p_node, rdatasetp);
	return ISC_R_SUCCESS;
}

static void
rpz_rewrite_ns_skip(ns_client_t *client, dns_name_t *nsname,
		    isc_result_t result, int level, const char *str) {
	dns_rpz_st_t *st;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite_ns_skip");

	st = client->query.rpz_st;

	if (str != NULL) {
		rpz_log_fail_helper(client, level, nsname, DNS_RPZ_TYPE_NSIP,
				    DNS_RPZ_TYPE_NSDNAME, str, result);
	}
	if (st->r.ns_rdataset != NULL &&
	    dns_rdataset_isassociated(st->r.ns_rdataset))
	{
		dns_rdataset_disassociate(st->r.ns_rdataset);
	}

	st->r.label--;
}

/*
 * RPZ query result types
 */
typedef enum {
	qresult_type_done = 0,
	qresult_type_restart = 1,
	qresult_type_recurse = 2
} qresult_type_t;

/*
 * Look for response policy zone QNAME, NSIP, and NSDNAME rewriting.
 */
static isc_result_t
rpz_rewrite(ns_client_t *client, dns_rdatatype_t qtype, isc_result_t qresult,
	    bool resuming, dns_rdataset_t *ordataset, dns_rdataset_t *osigset) {
	dns_rpz_zones_t *rpzs;
	dns_rpz_st_t *st;
	dns_rdataset_t *rdataset;
	dns_fixedname_t nsnamef;
	dns_name_t *nsname;
	qresult_type_t qresult_type;
	dns_rpz_zbits_t zbits;
	isc_result_t result = ISC_R_SUCCESS;
	dns_rpz_have_t have;
	dns_rpz_popt_t popt;
	int rpz_ver;
	unsigned int options;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_rewrite");

	rpzs = client->inner.view->rpzs;
	st = client->query.rpz_st;

	if (rpzs == NULL) {
		return ISC_R_NOTFOUND;
	}
	if (st != NULL && (st->state & DNS_RPZ_REWRITTEN) != 0) {
		return DNS_R_DISALLOWED;
	}
	if (RECURSING(client)) {
		return DNS_R_DISALLOWED;
	}

	RWLOCK(&rpzs->search_lock, isc_rwlocktype_read);
	if ((rpzs->p.num_zones == 0) ||
	    (!RECURSIONOK(client) && rpzs->p.no_rd_ok == 0) ||
	    !rpz_ck_dnssec(client, qresult, ordataset, osigset))
	{
		RWUNLOCK(&rpzs->search_lock, isc_rwlocktype_read);
		return DNS_R_DISALLOWED;
	}
	have = rpzs->have;
	popt = rpzs->p;
	rpz_ver = rpzs->rpz_ver;
	RWUNLOCK(&rpzs->search_lock, isc_rwlocktype_read);

	if (st == NULL) {
		st = isc_mem_get(client->manager->mctx, sizeof(*st));
		st->state = 0;
	}
	if (st->state == 0) {
		st->state |= DNS_RPZ_ACTIVE;
		memset(&st->m, 0, sizeof(st->m));
		st->m.type = DNS_RPZ_TYPE_BAD;
		st->m.policy = DNS_RPZ_POLICY_MISS;
		st->m.ttl = ~0;
		memset(&st->r, 0, sizeof(st->r));
		memset(&st->q, 0, sizeof(st->q));
		st->p_name = dns_fixedname_initname(&st->_p_namef);
		st->r_name = dns_fixedname_initname(&st->_r_namef);
		st->fname = dns_fixedname_initname(&st->_fnamef);
		st->have = have;
		st->popt = popt;
		st->rpz_ver = rpz_ver;
		client->query.rpz_st = st;
	}

	/*
	 * There is nothing to rewrite if the main query failed.
	 */
	switch (qresult) {
	case ISC_R_SUCCESS:
	case DNS_R_GLUE:
	case DNS_R_ZONECUT:
		qresult_type = qresult_type_done;
		break;
	case DNS_R_EMPTYNAME:
	case DNS_R_NXRRSET:
	case DNS_R_NXDOMAIN:
	case DNS_R_EMPTYWILD:
	case DNS_R_NCACHENXDOMAIN:
	case DNS_R_NCACHENXRRSET:
	case DNS_R_COVERINGNSEC:
	case DNS_R_CNAME:
	case DNS_R_DNAME:
		qresult_type = qresult_type_restart;
		break;
	case DNS_R_DELEGATION:
	case ISC_R_NOTFOUND:
		/*
		 * If recursion is on, do only tentative rewriting.
		 * If recursion is off, this the normal and only time we
		 * can rewrite.
		 */
		if (RECURSIONOK(client)) {
			qresult_type = qresult_type_recurse;
		} else {
			qresult_type = qresult_type_restart;
		}
		break;
	case ISC_R_FAILURE:
	case ISC_R_TIMEDOUT:
	case ISC_R_CANCELED:
	case DNS_R_BROKENCHAIN:
		rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL3, NULL,
			     DNS_RPZ_TYPE_QNAME,
			     "stop on qresult in rpz_rewrite()", qresult);
		return ISC_R_SUCCESS;
	default:
		rpz_log_fail(client, DNS_RPZ_DEBUG_LEVEL1, NULL,
			     DNS_RPZ_TYPE_QNAME,
			     "stop on unrecognized qresult in rpz_rewrite()",
			     qresult);
		return ISC_R_SUCCESS;
	}

	rdataset = NULL;

	if ((st->state & (DNS_RPZ_DONE_CLIENT_IP | DNS_RPZ_DONE_QNAME)) !=
	    (DNS_RPZ_DONE_CLIENT_IP | DNS_RPZ_DONE_QNAME))
	{
		isc_netaddr_t netaddr;
		dns_rpz_zbits_t allowed;

		if (qresult_type == qresult_type_recurse) {
			/*
			 * This request needs recursion that has not been done.
			 * Get bits for the policy zones that do not need
			 * to wait for the results of recursion.
			 */
			allowed = st->have.qname_skip_recurse;
			if (allowed == 0) {
				return ISC_R_SUCCESS;
			}
		} else {
			allowed = DNS_RPZ_ALL_ZBITS;
		}

		/*
		 * Check once for triggers for the client IP address.
		 */
		if ((st->state & DNS_RPZ_DONE_CLIENT_IP) == 0) {
			zbits = rpz_get_zbits(client, dns_rdatatype_none,
					      DNS_RPZ_TYPE_CLIENT_IP);
			zbits &= allowed;
			if (zbits != 0) {
				isc_netaddr_fromsockaddr(
					&netaddr, &client->inner.peeraddr);
				result = rpz_rewrite_ip(client, &netaddr, qtype,
							DNS_RPZ_TYPE_CLIENT_IP,
							zbits, &rdataset);
				if (result != ISC_R_SUCCESS) {
					goto cleanup;
				}
			}
		}

		/*
		 * Check triggers for the query name if this is the first time
		 * for the current qname.
		 * There is a first time for each name in a CNAME chain
		 */
		if ((st->state & DNS_RPZ_DONE_QNAME) == 0) {
			result = rpz_rewrite_name(client, client->query.qname,
						  qtype, DNS_RPZ_TYPE_QNAME,
						  allowed, &rdataset);
			if (result != ISC_R_SUCCESS) {
				goto cleanup;
			}

			/*
			 * Check IPv4 addresses in A RRs next.
			 * Reset to the start of the NS names.
			 */
			st->r.label = dns_name_countlabels(client->query.qname);
			st->state &= ~(DNS_RPZ_DONE_QNAME_IP |
				       DNS_RPZ_DONE_IPv4);
		}

		/*
		 * Quit if this was an attempt to find a qname or
		 * client-IP trigger before recursion.
		 * We will be back if no pre-recursion triggers hit.
		 * For example, consider 2 policy zones, both with qname and
		 * IP address triggers.  If the qname misses the 1st zone,
		 * then we cannot know whether a hit for the qname in the
		 * 2nd zone matters until after recursing to get the A RRs and
		 * testing them in the first zone.
		 * Do not bother saving the work from this attempt,
		 * because recursion is so slow.
		 */
		if (qresult_type == qresult_type_recurse) {
			goto cleanup;
		}

		/*
		 * DNS_RPZ_DONE_QNAME but not DNS_RPZ_DONE_CLIENT_IP
		 * is reset at the end of dealing with each CNAME.
		 */
		st->state |= (DNS_RPZ_DONE_CLIENT_IP | DNS_RPZ_DONE_QNAME);
	}

	/*
	 * Check known IP addresses for the query name if the database lookup
	 * resulted in some addresses (qresult_type == qresult_type_done)
	 * and if we have not already checked them.
	 * Any recursion required for the query has already happened.
	 * Do not check addresses that will not be in the ANSWER section.
	 */
	if ((st->state & DNS_RPZ_DONE_QNAME_IP) == 0 &&
	    qresult_type == qresult_type_done &&
	    rpz_get_zbits(client, qtype, DNS_RPZ_TYPE_IP) != 0)
	{
		result = rpz_rewrite_ip_rrsets(client, client->query.qname,
					       qtype, DNS_RPZ_TYPE_IP,
					       &rdataset, resuming);
		if (result != ISC_R_SUCCESS) {
			goto cleanup;
		}
		/*
		 * We are finished checking the IP addresses for the qname.
		 * Start with IPv4 if we will check NS IP addresses.
		 */
		st->state |= DNS_RPZ_DONE_QNAME_IP;
		st->state &= ~DNS_RPZ_DONE_IPv4;
	}

	/*
	 * Stop looking for rules if there are none of the other kinds
	 * that could override what we already have.
	 */
	if (rpz_get_zbits(client, dns_rdatatype_any, DNS_RPZ_TYPE_NSDNAME) ==
		    0 &&
	    rpz_get_zbits(client, dns_rdatatype_any, DNS_RPZ_TYPE_NSIP) == 0)
	{
		result = ISC_R_SUCCESS;
		goto cleanup;
	}

	dns_fixedname_init(&nsnamef);
	dns_name_clone(client->query.qname, dns_fixedname_name(&nsnamef));
	options = client->query.dboptions | DNS_DBFIND_GLUEOK;
	while (st->r.label > st->popt.min_ns_labels) {
		bool was_glue = false;
		/*
		 * Get NS rrset for each domain in the current qname.
		 */
		if (st->r.label == dns_name_countlabels(client->query.qname)) {
			nsname = client->query.qname;
		} else {
			nsname = dns_fixedname_name(&nsnamef);
			dns_name_split(client->query.qname, st->r.label, NULL,
				       nsname);
		}
		if (st->r.ns_rdataset == NULL ||
		    !dns_rdataset_isassociated(st->r.ns_rdataset))
		{
			dns_db_t *db = NULL;
			result = rpz_rrset_find(client, nsname,
						dns_rdatatype_ns, options,
						DNS_RPZ_TYPE_NSDNAME, &db, NULL,
						&st->r.ns_rdataset, resuming);
			if (db != NULL) {
				dns_db_detach(&db);
			}
			if (st->m.policy == DNS_RPZ_POLICY_ERROR) {
				goto cleanup;
			}
			switch (result) {
			case DNS_R_GLUE:
				was_glue = true;
				FALLTHROUGH;
			case ISC_R_SUCCESS:
				result = dns_rdataset_first(st->r.ns_rdataset);
				if (result != ISC_R_SUCCESS) {
					goto cleanup;
				}
				st->state &= ~(DNS_RPZ_DONE_NSDNAME |
					       DNS_RPZ_DONE_IPv4);
				break;
			case DNS_R_DELEGATION:
			case DNS_R_DUPLICATE:
			case DNS_R_DROP:
				goto cleanup;
			case DNS_R_EMPTYNAME:
			case DNS_R_NXRRSET:
			case DNS_R_EMPTYWILD:
			case DNS_R_NXDOMAIN:
			case DNS_R_NCACHENXDOMAIN:
			case DNS_R_NCACHENXRRSET:
			case ISC_R_NOTFOUND:
			case DNS_R_CNAME:
			case DNS_R_DNAME:
				rpz_rewrite_ns_skip(client, nsname, result, 0,
						    NULL);
				continue;
			case ISC_R_TIMEDOUT:
			case DNS_R_BROKENCHAIN:
			case ISC_R_FAILURE:
				rpz_rewrite_ns_skip(client, nsname, result,
						    DNS_RPZ_DEBUG_LEVEL3,
						    " NS rpz_rrset_find()");
				continue;
			default:
				rpz_rewrite_ns_skip(client, nsname, result,
						    DNS_RPZ_INFO_LEVEL,
						    " unrecognized NS"
						    " rpz_rrset_find()");
				continue;
			}
		}

		/*
		 * Check all NS names.
		 */
		do {
			dns_rdata_ns_t ns;
			dns_rdata_t nsrdata = DNS_RDATA_INIT;

			dns_rdataset_current(st->r.ns_rdataset, &nsrdata);
			result = dns_rdata_tostruct(&nsrdata, &ns, NULL);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			dns_rdata_reset(&nsrdata);

			/*
			 * Do nothing about "NS ."
			 */
			if (dns_name_equal(&ns.name, dns_rootname)) {
				dns_rdata_freestruct(&ns);
				result = dns_rdataset_next(st->r.ns_rdataset);
				continue;
			}
			/*
			 * Check this NS name if we did not handle it
			 * during a previous recursion.
			 */
			if ((st->state & DNS_RPZ_DONE_NSDNAME) == 0) {
				result = rpz_rewrite_name(
					client, &ns.name, qtype,
					DNS_RPZ_TYPE_NSDNAME, DNS_RPZ_ALL_ZBITS,
					&rdataset);
				if (result != ISC_R_SUCCESS) {
					dns_rdata_freestruct(&ns);
					goto cleanup;
				}
				st->state |= DNS_RPZ_DONE_NSDNAME;
			}
			/*
			 * Check all IP addresses for this NS name.
			 */
			result = rpz_rewrite_ip_rrsets(client, &ns.name, qtype,
						       DNS_RPZ_TYPE_NSIP,
						       &rdataset, resuming);
			dns_rdata_freestruct(&ns);
			if (result != ISC_R_SUCCESS) {
				goto cleanup;
			}
			st->state &= ~(DNS_RPZ_DONE_NSDNAME |
				       DNS_RPZ_DONE_IPv4);
			result = dns_rdataset_next(st->r.ns_rdataset);
		} while (result == ISC_R_SUCCESS);
		dns_rdataset_disassociate(st->r.ns_rdataset);

		/*
		 * If we just checked a glue NS RRset retry without allowing
		 * glue responses, otherwise setup for the next name.
		 */
		if (was_glue) {
			options = client->query.dboptions;
		} else {
			options = client->query.dboptions | DNS_DBFIND_GLUEOK;
			st->r.label--;
		}

		if (rpz_get_zbits(client, dns_rdatatype_any,
				  DNS_RPZ_TYPE_NSDNAME) == 0 &&
		    rpz_get_zbits(client, dns_rdatatype_any,
				  DNS_RPZ_TYPE_NSIP) == 0)
		{
			break;
		}
	}

	/*
	 * Use the best hit, if any.
	 */
	result = ISC_R_SUCCESS;

cleanup:
	if (st->m.policy != DNS_RPZ_POLICY_MISS &&
	    st->m.policy != DNS_RPZ_POLICY_ERROR &&
	    st->m.rpz->policy != DNS_RPZ_POLICY_GIVEN)
	{
		st->m.policy = st->m.rpz->policy;
	}
	if (st->m.policy == DNS_RPZ_POLICY_MISS ||
	    st->m.policy == DNS_RPZ_POLICY_PASSTHRU ||
	    st->m.policy == DNS_RPZ_POLICY_ERROR)
	{
		if (st->m.policy == DNS_RPZ_POLICY_PASSTHRU &&
		    result != DNS_R_DELEGATION)
		{
			rpz_log_rewrite(client, false, st->m.policy, st->m.type,
					st->m.zone, st->p_name, NULL,
					st->m.rpz->num);
		}
		rpz_match_clear(st);
	}
	if (st->m.policy == DNS_RPZ_POLICY_ERROR) {
		CTRACE(ISC_LOG_ERROR, "SERVFAIL due to RPZ policy");
		st->m.type = DNS_RPZ_TYPE_BAD;
		result = DNS_R_SERVFAIL;
	}
	ns_client_putrdataset(client, &rdataset);
	if ((st->state & DNS_RPZ_RECURSING) == 0) {
		rpz_clean(NULL, &st->r.db, NULL, &st->r.ns_rdataset);
	}

	return result;
}

/*
 * See if response policy zone rewriting is allowed by a lack of interest
 * by the client in DNSSEC or a lack of signatures.
 */
static bool
rpz_ck_dnssec(ns_client_t *client, isc_result_t qresult,
	      dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset) {
	dns_fixedname_t fixed;
	dns_name_t *found = NULL;
	dns_rdataset_t trdataset = DNS_RDATASET_INIT;
	dns_rdatatype_t type;

	CTRACE(ISC_LOG_DEBUG(3), "rpz_ck_dnssec");

	if (client->inner.view->rpzs->p.break_dnssec || !WANTDNSSEC(client)) {
		return true;
	}

	/*
	 * We do not know if there are signatures if we have not recursed
	 * for them.
	 */
	if (qresult == DNS_R_DELEGATION || qresult == ISC_R_NOTFOUND) {
		return false;
	}

	if (sigrdataset == NULL) {
		return true;
	}
	if (dns_rdataset_isassociated(sigrdataset)) {
		return false;
	}

	/*
	 * We are happy to rewrite nothing.
	 */
	if (rdataset == NULL || !dns_rdataset_isassociated(rdataset)) {
		return true;
	}
	/*
	 * Do not rewrite if there is any sign of signatures.
	 */
	if (dns_rdatatype_isnsec(rdataset->type) ||
	    rdataset->type == dns_rdatatype_rrsig)
	{
		return false;
	}

	/*
	 * Look for a signature in a negative cache rdataset.
	 */
	if (!rdataset->attributes.negative) {
		return true;
	}
	found = dns_fixedname_initname(&fixed);
	dns_rdataset_init(&trdataset);
	DNS_RDATASET_FOREACH(rdataset) {
		dns_ncache_current(rdataset, found, &trdataset);
		type = trdataset.type;
		dns_rdataset_disassociate(&trdataset);
		if (dns_rdatatype_isnsec(type) || type == dns_rdatatype_rrsig) {
			return false;
		}
	}
	return true;
}

static unsigned char inaddr10[] = "\00210\007IN-ADDR\004ARPA";

static unsigned char inaddr16172[] = "\00216\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr17172[] = "\00217\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr18172[] = "\00218\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr19172[] = "\00219\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr20172[] = "\00220\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr21172[] = "\00221\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr22172[] = "\00222\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr23172[] = "\00223\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr24172[] = "\00224\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr25172[] = "\00225\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr26172[] = "\00226\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr27172[] = "\00227\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr28172[] = "\00228\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr29172[] = "\00229\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr30172[] = "\00230\003172\007IN-ADDR\004ARPA";
static unsigned char inaddr31172[] = "\00231\003172\007IN-ADDR\004ARPA";

static unsigned char inaddr168192[] = "\003168\003192\007IN-ADDR\004ARPA";

static dns_name_t rfc1918names[] = {
	DNS_NAME_INITABSOLUTE(inaddr10),    DNS_NAME_INITABSOLUTE(inaddr16172),
	DNS_NAME_INITABSOLUTE(inaddr17172), DNS_NAME_INITABSOLUTE(inaddr18172),
	DNS_NAME_INITABSOLUTE(inaddr19172), DNS_NAME_INITABSOLUTE(inaddr20172),
	DNS_NAME_INITABSOLUTE(inaddr21172), DNS_NAME_INITABSOLUTE(inaddr22172),
	DNS_NAME_INITABSOLUTE(inaddr23172), DNS_NAME_INITABSOLUTE(inaddr24172),
	DNS_NAME_INITABSOLUTE(inaddr25172), DNS_NAME_INITABSOLUTE(inaddr26172),
	DNS_NAME_INITABSOLUTE(inaddr27172), DNS_NAME_INITABSOLUTE(inaddr28172),
	DNS_NAME_INITABSOLUTE(inaddr29172), DNS_NAME_INITABSOLUTE(inaddr30172),
	DNS_NAME_INITABSOLUTE(inaddr31172), DNS_NAME_INITABSOLUTE(inaddr168192)
};

static unsigned char prisoner_data[] = "\010prisoner\004iana\003org";
static unsigned char hostmaster_data[] = "\012hostmaster\014root-"
					 "servers\003org";

static dns_name_t const prisoner = DNS_NAME_INITABSOLUTE(prisoner_data);
static dns_name_t const hostmaster = DNS_NAME_INITABSOLUTE(hostmaster_data);

static void
warn_rfc1918(ns_client_t *client, dns_name_t *fname, dns_rdataset_t *rdataset) {
	unsigned int i;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_rdata_soa_t soa;
	dns_rdataset_t found;
	isc_result_t result;

	for (i = 0; i < (sizeof(rfc1918names) / sizeof(*rfc1918names)); i++) {
		if (dns_name_issubdomain(fname, &rfc1918names[i])) {
			dns_rdataset_init(&found);
			result = dns_ncache_getrdataset(
				rdataset, &rfc1918names[i], dns_rdatatype_soa,
				&found);
			if (result != ISC_R_SUCCESS) {
				return;
			}

			result = dns_rdataset_first(&found);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			dns_rdataset_current(&found, &rdata);
			result = dns_rdata_tostruct(&rdata, &soa, NULL);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			if (dns_name_equal(&soa.origin, &prisoner) &&
			    dns_name_equal(&soa.contact, &hostmaster))
			{
				char buf[DNS_NAME_FORMATSIZE];
				dns_name_format(fname, buf, sizeof(buf));
				ns_client_log(client, DNS_LOGCATEGORY_SECURITY,
					      NS_LOGMODULE_QUERY,
					      ISC_LOG_WARNING,
					      "RFC 1918 response from "
					      "Internet for %s",
					      buf);
			}
			dns_rdataset_disassociate(&found);
			return;
		}
	}
}

static void
query_findclosestnsec3(dns_name_t *qname, dns_db_t *db,
		       dns_dbversion_t *version, ns_client_t *client,
		       dns_rdataset_t *rdataset, dns_rdataset_t *sigrdataset,
		       dns_name_t *fname, bool exact, dns_name_t *found) {
	unsigned char salt[256];
	size_t salt_length;
	uint16_t iterations;
	isc_result_t result;
	unsigned int dboptions;
	dns_fixedname_t fixed;
	dns_hash_t hash;
	dns_name_t name;
	unsigned int skip = 0, labels;
	dns_rdata_nsec3_t nsec3;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	bool optout;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;

	salt_length = sizeof(salt);
	result = dns_db_getnsec3parameters(db, version, &hash, NULL,
					   &iterations, salt, &salt_length);
	if (result != ISC_R_SUCCESS) {
		return;
	}

	dns_name_init(&name);
	dns_name_clone(qname, &name);
	labels = dns_name_countlabels(&name);
	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * Map unknown algorithm to known value.
	 */
	if (hash == DNS_NSEC3_UNKNOWNALG) {
		hash = 1;
	}

again:
	dns_fixedname_init(&fixed);
	result = dns_nsec3_hashname(&fixed, NULL, NULL, &name,
				    dns_db_origin(db), hash, iterations, salt,
				    salt_length);
	if (result != ISC_R_SUCCESS) {
		return;
	}

	dboptions = client->query.dboptions | DNS_DBFIND_FORCENSEC3;
	result = dns_db_findext(db, dns_fixedname_name(&fixed), version,
				dns_rdatatype_nsec3, dboptions,
				client->inner.now, NULL, fname, &cm, &ci,
				rdataset, sigrdataset);

	if (result == DNS_R_NXDOMAIN) {
		if (!dns_rdataset_isassociated(rdataset)) {
			return;
		}
		result = dns_rdataset_first(rdataset);
		INSIST(result == ISC_R_SUCCESS);
		dns_rdataset_current(rdataset, &rdata);
		result = dns_rdata_tostruct(&rdata, &nsec3, NULL);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);
		dns_rdata_reset(&rdata);
		optout = ((nsec3.flags & DNS_NSEC3FLAG_OPTOUT) != 0);
		if (found != NULL && optout &&
		    dns_name_issubdomain(&name, dns_db_origin(db)))
		{
			dns_rdataset_disassociate(rdataset);
			if (dns_rdataset_isassociated(sigrdataset)) {
				dns_rdataset_disassociate(sigrdataset);
			}
			skip++;
			dns_name_getlabelsequence(qname, skip, labels - skip,
						  &name);
			ns_client_log(client, DNS_LOGCATEGORY_DNSSEC,
				      NS_LOGMODULE_QUERY, ISC_LOG_DEBUG(3),
				      "looking for closest provable encloser");
			goto again;
		}
		if (exact) {
			ns_client_log(client, DNS_LOGCATEGORY_DNSSEC,
				      NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
				      "expected a exact match NSEC3, got "
				      "a covering record");
		}
	} else if (result != ISC_R_SUCCESS) {
		return;
	} else if (!exact) {
		ns_client_log(client, DNS_LOGCATEGORY_DNSSEC,
			      NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
			      "expected covering NSEC3, got an exact match");
	}
	if (found == qname) {
		if (skip != 0U) {
			dns_name_getlabelsequence(qname, skip, labels - skip,
						  found);
		}
	} else if (found != NULL) {
		dns_name_copy(&name, found);
	}
	return;
}

static uint32_t
dns64_ttl(dns_db_t *db, dns_dbversion_t *version) {
	dns_dbnode_t *node = NULL;
	dns_rdata_soa_t soa;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_rdataset_t rdataset;
	isc_result_t result;
	uint32_t ttl = UINT32_MAX;

	dns_rdataset_init(&rdataset);

	result = dns_db_getoriginnode(db, &node);
	if (result != ISC_R_SUCCESS) {
		goto cleanup;
	}

	result = dns_db_findrdataset(db, node, version, dns_rdatatype_soa, 0, 0,
				     &rdataset, NULL);
	if (result != ISC_R_SUCCESS) {
		goto cleanup;
	}
	result = dns_rdataset_first(&rdataset);
	if (result != ISC_R_SUCCESS) {
		goto cleanup;
	}

	dns_rdataset_current(&rdataset, &rdata);
	result = dns_rdata_tostruct(&rdata, &soa, NULL);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);
	ttl = ISC_MIN(rdataset.ttl, soa.minimum);

cleanup:
	if (dns_rdataset_isassociated(&rdataset)) {
		dns_rdataset_disassociate(&rdataset);
	}
	if (node != NULL) {
		dns_db_detachnode(&node);
	}
	return ttl;
}

static bool
dns64_aaaaok(ns_client_t *client, dns_rdataset_t *rdataset,
	     dns_rdataset_t *sigrdataset) {
	isc_netaddr_t netaddr;
	dns_aclenv_t *env = client->manager->aclenv;
	dns_dns64_t *dns64 = ISC_LIST_HEAD(client->inner.view->dns64);
	unsigned int flags = 0;
	unsigned int i, count;
	bool *aaaaok;

	INSIST(client->query.dns64_aaaaok == NULL);
	INSIST(client->query.dns64_aaaaoklen == 0);
	INSIST(client->query.dns64_aaaa == NULL);
	INSIST(client->query.dns64_sigaaaa == NULL);

	if (dns64 == NULL) {
		return true;
	}

	if (RECURSIONOK(client)) {
		flags |= DNS_DNS64_RECURSIVE;
	}

	if (WANTDNSSEC(client) && sigrdataset != NULL &&
	    dns_rdataset_isassociated(sigrdataset))
	{
		flags |= DNS_DNS64_DNSSEC;
	}

	count = dns_rdataset_count(rdataset);
	aaaaok = isc_mem_cget(client->manager->mctx, count, sizeof(bool));

	isc_netaddr_fromsockaddr(&netaddr, &client->inner.peeraddr);
	if (dns_dns64_aaaaok(dns64, &netaddr, client->inner.signer, env, flags,
			     rdataset, aaaaok, count))
	{
		for (i = 0; i < count; i++) {
			if (aaaaok != NULL && !aaaaok[i]) {
				SAVE(client->query.dns64_aaaaok, aaaaok);
				client->query.dns64_aaaaoklen = count;
				break;
			}
		}
		if (aaaaok != NULL) {
			isc_mem_cput(client->manager->mctx, aaaaok, count,
				     sizeof(bool));
		}
		return true;
	}
	if (aaaaok != NULL) {
		isc_mem_cput(client->manager->mctx, aaaaok, count,
			     sizeof(bool));
	}
	return false;
}

/*
 * Look for the name and type in the redirection zone.  If found update
 * the arguments as appropriate.  Return true if a update was
 * performed.
 *
 * Only perform the update if the client is in the allow query acl and
 * returning the update would not cause a DNSSEC validation failure.
 */
static isc_result_t
redirect(ns_client_t *client, dns_name_t *name, dns_rdataset_t *rdataset,
	 dns_dbnode_t **nodep, dns_db_t **dbp, dns_dbversion_t **versionp,
	 dns_rdatatype_t qtype) {
	dns_db_t *db = NULL;
	dns_dbnode_t *node = NULL;
	dns_fixedname_t fixed;
	dns_name_t *found = NULL;
	dns_rdataset_t trdataset = DNS_RDATASET_INIT;
	isc_result_t result;
	dns_rdatatype_t type;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	ns_dbversion_t *dbversion = NULL;

	CTRACE(ISC_LOG_DEBUG(3), "redirect");

	if (client->inner.view->redirect == NULL) {
		return ISC_R_NOTFOUND;
	}

	found = dns_fixedname_initname(&fixed);

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);
	dns_clientinfo_setecs(&ci, &client->inner.ecs);

	if (WANTDNSSEC(client) && dns_db_iszone(*dbp) && dns_db_issecure(*dbp))
	{
		return ISC_R_NOTFOUND;
	}

	if (WANTDNSSEC(client) && dns_rdataset_isassociated(rdataset)) {
		if (rdataset->trust == dns_trust_secure) {
			return ISC_R_NOTFOUND;
		}
		if (rdataset->trust == dns_trust_ultimate &&
		    dns_rdatatype_isnsec(rdataset->type))
		{
			return ISC_R_NOTFOUND;
		}
		if (rdataset->attributes.negative) {
			DNS_RDATASET_FOREACH(rdataset) {
				dns_ncache_current(rdataset, found, &trdataset);
				type = trdataset.type;
				dns_rdataset_disassociate(&trdataset);
				if (dns_rdatatype_isnsec(type) ||
				    type == dns_rdatatype_rrsig)
				{
					return ISC_R_NOTFOUND;
				}
			}
		}
	}

	result = ns_client_checkaclsilent(
		client, NULL,
		dns_zone_getqueryacl(client->inner.view->redirect), true);
	if (result != ISC_R_SUCCESS) {
		return ISC_R_NOTFOUND;
	}

	result = dns_zone_getdb(client->inner.view->redirect, &db);
	if (result != ISC_R_SUCCESS) {
		return ISC_R_NOTFOUND;
	}

	dbversion = ns_client_findversion(client, db);
	if (dbversion == NULL) {
		dns_db_detach(&db);
		return ISC_R_NOTFOUND;
	}

	/*
	 * Lookup the requested data in the redirect zone.
	 */
	result = dns_db_findext(db, client->query.qname, dbversion->version,
				qtype, DNS_DBFIND_NOZONECUT, client->inner.now,
				&node, found, &cm, &ci, &trdataset, NULL);
	if (result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) {
		if (dns_rdataset_isassociated(rdataset)) {
			dns_rdataset_disassociate(rdataset);
		}
		if (dns_rdataset_isassociated(&trdataset)) {
			dns_rdataset_disassociate(&trdataset);
		}
		goto nxrrset;
	} else if (result != ISC_R_SUCCESS) {
		if (dns_rdataset_isassociated(&trdataset)) {
			dns_rdataset_disassociate(&trdataset);
		}
		if (node != NULL) {
			dns_db_detachnode(&node);
		}
		dns_db_detach(&db);
		return ISC_R_NOTFOUND;
	}

	CTRACE(ISC_LOG_DEBUG(3), "redirect: found data: done");
	dns_name_copy(found, name);
	if (dns_rdataset_isassociated(rdataset)) {
		dns_rdataset_disassociate(rdataset);
	}
	if (dns_rdataset_isassociated(&trdataset)) {
		dns_rdataset_clone(&trdataset, rdataset);
		dns_rdataset_disassociate(&trdataset);
	}
nxrrset:
	if (*nodep != NULL) {
		dns_db_detachnode(nodep);
	}
	dns_db_detach(dbp);
	dns_db_attachnode(node, nodep);
	dns_db_attach(db, dbp);
	dns_db_detachnode(&node);
	dns_db_detach(&db);
	*versionp = dbversion->version;

	client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY |
				     NS_QUERYATTR_NOADDITIONAL);

	return result;
}

static isc_result_t
redirect2(ns_client_t *client, dns_name_t *name, dns_rdataset_t *rdataset,
	  dns_dbnode_t **nodep, dns_db_t **dbp, dns_dbversion_t **versionp,
	  dns_rdatatype_t qtype, bool *is_zonep) {
	dns_db_t *db = NULL;
	dns_dbnode_t *node = NULL;
	dns_fixedname_t fixed;
	dns_fixedname_t fixedredirect;
	dns_name_t *found = NULL, *redirectname = NULL;
	dns_rdataset_t trdataset = DNS_RDATASET_INIT;
	isc_result_t result;
	dns_rdatatype_t type;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	dns_dbversion_t *version = NULL;
	dns_zone_t *zone = NULL;
	bool is_zone;
	unsigned int labels;

	CTRACE(ISC_LOG_DEBUG(3), "redirect2");

	if (client->inner.view->redirectzone == NULL) {
		return ISC_R_NOTFOUND;
	}

	if (dns_name_issubdomain(name, client->inner.view->redirectzone)) {
		return ISC_R_NOTFOUND;
	}

	found = dns_fixedname_initname(&fixed);

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);
	dns_clientinfo_setecs(&ci, &client->inner.ecs);

	if (WANTDNSSEC(client) && dns_db_iszone(*dbp) && dns_db_issecure(*dbp))
	{
		return ISC_R_NOTFOUND;
	}

	if (WANTDNSSEC(client) && dns_rdataset_isassociated(rdataset)) {
		if (rdataset->trust == dns_trust_secure) {
			return ISC_R_NOTFOUND;
		}
		if (rdataset->trust == dns_trust_ultimate &&
		    dns_rdatatype_isnsec(rdataset->type))
		{
			return ISC_R_NOTFOUND;
		}
		if (rdataset->attributes.negative) {
			DNS_RDATASET_FOREACH(rdataset) {
				dns_ncache_current(rdataset, found, &trdataset);
				type = trdataset.type;
				dns_rdataset_disassociate(&trdataset);
				if (dns_rdatatype_isnsec(type) ||
				    type == dns_rdatatype_rrsig)
				{
					return ISC_R_NOTFOUND;
				}
			}
		}
	}

	redirectname = dns_fixedname_initname(&fixedredirect);
	labels = dns_name_countlabels(client->query.qname);
	if (labels > 1U) {
		dns_name_t prefix;

		dns_name_init(&prefix);
		dns_name_getlabelsequence(client->query.qname, 0, labels - 1,
					  &prefix);
		result = dns_name_concatenate(&prefix,
					      client->inner.view->redirectzone,
					      redirectname);
		if (result != ISC_R_SUCCESS) {
			return ISC_R_NOTFOUND;
		}
	} else {
		dns_name_copy(redirectname, client->inner.view->redirectzone);
	}

	result = query_getdb(client, redirectname, qtype,
			     (dns_getdb_options_t){ 0 }, &zone, &db, &version,
			     &is_zone);
	if (result != ISC_R_SUCCESS) {
		return ISC_R_NOTFOUND;
	}
	if (zone != NULL) {
		dns_zone_detach(&zone);
	}

	/*
	 * Lookup the requested data in the redirect zone.
	 */
	result = dns_db_findext(db, redirectname, version, qtype, 0,
				client->inner.now, &node, found, &cm, &ci,
				&trdataset, NULL);
	if (result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) {
		if (dns_rdataset_isassociated(rdataset)) {
			dns_rdataset_disassociate(rdataset);
		}
		if (dns_rdataset_isassociated(&trdataset)) {
			dns_rdataset_disassociate(&trdataset);
		}
		goto nxrrset;
	} else if (result == ISC_R_NOTFOUND || result == DNS_R_DELEGATION) {
		/*
		 * Cleanup.
		 */
		if (dns_rdataset_isassociated(&trdataset)) {
			dns_rdataset_disassociate(&trdataset);
		}
		if (node != NULL) {
			dns_db_detachnode(&node);
		}
		dns_db_detach(&db);
		/*
		 * Don't loop forever if the lookup failed last time.
		 */
		if (!REDIRECT(client)) {
			result = ns_query_recurse(client, qtype, redirectname,
						  NULL, NULL, true);
			if (result == ISC_R_SUCCESS) {
				client->query.attributes |=
					NS_QUERYATTR_RECURSING;
				client->query.attributes |=
					NS_QUERYATTR_REDIRECT;
				return DNS_R_CONTINUE;
			}
		}
		return ISC_R_NOTFOUND;
	} else if (result != ISC_R_SUCCESS) {
		if (dns_rdataset_isassociated(&trdataset)) {
			dns_rdataset_disassociate(&trdataset);
		}
		if (node != NULL) {
			dns_db_detachnode(&node);
		}
		dns_db_detach(&db);
		return ISC_R_NOTFOUND;
	}

	CTRACE(ISC_LOG_DEBUG(3), "redirect2: found data: done");
	/*
	 * Adjust the found name to not include the redirectzone suffix.
	 */
	dns_name_split(found,
		       dns_name_countlabels(client->inner.view->redirectzone),
		       found, NULL);
	/*
	 * Make the name absolute.
	 */
	result = dns_name_concatenate(found, dns_rootname, found);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);

	dns_name_copy(found, name);
	if (dns_rdataset_isassociated(rdataset)) {
		dns_rdataset_disassociate(rdataset);
	}
	if (dns_rdataset_isassociated(&trdataset)) {
		dns_rdataset_clone(&trdataset, rdataset);
		dns_rdataset_disassociate(&trdataset);
	}
nxrrset:
	if (*nodep != NULL) {
		dns_db_detachnode(nodep);
	}
	dns_db_detach(dbp);
	dns_db_attachnode(node, nodep);
	dns_db_attach(db, dbp);
	dns_db_detachnode(&node);
	dns_db_detach(&db);
	*is_zonep = is_zone;
	*versionp = version;

	client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY |
				     NS_QUERYATTR_NOADDITIONAL);

	return result;
}

/*%
 * Initialize query context 'qctx'. Run by query_setup() when
 * first handling a client query, and by query_resume() when
 * returning from recursion.
 *
 * Whenever this function is called, qctx_destroy() must be called
 * when leaving the scope or freeing the qctx.
 */
static void
qctx_init(ns_client_t *client, dns_fetchresponse_t **frespp,
	  dns_rdatatype_t qtype, query_ctx_t *qctx) {
	REQUIRE(qctx != NULL);
	REQUIRE(client != NULL);

	memset(qctx, 0, sizeof(*qctx));

	/* Set this first so CCTRACE will work */
	qctx->client = client;

	dns_view_attach(client->inner.view, &qctx->view);

	CCTRACE(ISC_LOG_DEBUG(3), "qctx_init");

	if (frespp != NULL) {
		qctx->fresp = *frespp;
		*frespp = NULL;
	} else {
		qctx->fresp = NULL;
	}
	qctx->qtype = qctx->type = qtype;
	qctx->result = ISC_R_SUCCESS;
	qctx->findcoveringnsec = qctx->view->synthfromdnssec;

	/*
	 * If it's an RRSIG or SIG query, we'll iterate the node.
	 */
	if (dns_rdatatype_issig(qctx->qtype)) {
		qctx->type = dns_rdatatype_any;
	}

	CALL_HOOK_NORETURN(NS_QUERY_QCTX_INITIALIZED, qctx);
}

/*%
 * Clean up and disassociate the rdataset and node pointers in qctx.
 */
static void
qctx_clean(query_ctx_t *qctx) {
	if (qctx->rdataset != NULL && dns_rdataset_isassociated(qctx->rdataset))
	{
		dns_rdataset_disassociate(qctx->rdataset);
	}
	if (qctx->sigrdataset != NULL &&
	    dns_rdataset_isassociated(qctx->sigrdataset))
	{
		dns_rdataset_disassociate(qctx->sigrdataset);
	}
	if (qctx->db != NULL && qctx->node != NULL) {
		dns_db_detachnode(&qctx->node);
	}
	if (qctx->client != NULL && qctx->client->query.gluedb != NULL) {
		dns_db_detach(&qctx->client->query.gluedb);
	}
}

/*%
 * Free any allocated memory associated with qctx.
 */
static void
qctx_freedata(query_ctx_t *qctx) {
	if (qctx->rdataset != NULL) {
		ns_client_putrdataset(qctx->client, &qctx->rdataset);
	}

	if (qctx->sigrdataset != NULL) {
		ns_client_putrdataset(qctx->client, &qctx->sigrdataset);
	}

	if (qctx->fname != NULL) {
		ns_client_releasename(qctx->client, &qctx->fname);
	}

	if (qctx->db != NULL) {
		INSIST(qctx->node == NULL);
		dns_db_detach(&qctx->db);
	}

	if (qctx->zone != NULL) {
		dns_zone_detach(&qctx->zone);
	}

	if (qctx->zdb != NULL) {
		ns_client_putrdataset(qctx->client, &qctx->zsigrdataset);
		ns_client_putrdataset(qctx->client, &qctx->zrdataset);
		ns_client_releasename(qctx->client, &qctx->zfname);
		dns_db_detachnode(&qctx->znode);
		dns_db_detach(&qctx->zdb);
		qctx->zversion = NULL;
	}

	if (qctx->fresp != NULL) {
		free_fresp(qctx->client, &qctx->fresp);
	}
}

static void
qctx_destroy(query_ctx_t *qctx) {
	CALL_HOOK_NORETURN(NS_QUERY_QCTX_DESTROYED, 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->fresp, src->fresp);
	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.
 */
static void
query_trace(query_ctx_t *qctx) {
#ifdef WANT_QUERYTRACE
	char mbuf[2 * DNS_NAME_FORMATSIZE];
	char qbuf[DNS_NAME_FORMATSIZE];

	if (qctx->client->query.origqname != NULL) {
		dns_name_format(qctx->client->query.origqname, qbuf,
				sizeof(qbuf));
	} else {
		snprintf(qbuf, sizeof(qbuf), "<unset>");
	}

	snprintf(mbuf, sizeof(mbuf) - 1,
		 "client attr:0x%x, query attr:0x%X, restarts:%u, "
		 "origqname:%s, timer:%d, authdb:%d, referral:%d",
		 qctx->client->inner.attributes, qctx->client->query.attributes,
		 qctx->client->query.restarts, qbuf,
		 (int)qctx->client->query.timerset,
		 (int)qctx->client->query.authdbset,
		 (int)qctx->client->query.isreferral);
	CCTRACE(ISC_LOG_DEBUG(3), mbuf);
#else  /* ifdef WANT_QUERYTRACE */
	UNUSED(qctx);
#endif /* ifdef WANT_QUERYTRACE */
}

/*
 * 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 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
 * ns__query_start() again with the same query context. Resuming from
 * recursion is handled by query_resume().
 */
static void
query_setup(ns_client_t *client, dns_rdatatype_t qtype) {
	isc_result_t result = ISC_R_UNSET;
	query_ctx_t qctx;

	qctx_init(client, NULL, qtype, &qctx);
	query_trace(&qctx);

	CALL_HOOK(NS_QUERY_SETUP, &qctx);

	/*
	 * Check SERVFAIL cache
	 */
	result = ns__query_sfcache(&qctx);
	if (result != ISC_R_COMPLETE) {
		goto cleanup;
	}

	(void)ns__query_start(&qctx);

cleanup:
	qctx_destroy(&qctx);
}

static bool
get_root_key_sentinel_id(query_ctx_t *qctx, const char *ndata) {
	unsigned int v = 0;
	int i;

	for (i = 0; i < 5; i++) {
		if (!isdigit((unsigned char)ndata[i])) {
			return false;
		}
		v *= 10;
		v += ndata[i] - '0';
	}
	if (v > 65535U) {
		return false;
	}
	qctx->client->query.root_key_sentinel_keyid = v;
	return true;
}

/*%
 * Find out if the query is for a root key sentinel and if so, record the type
 * of root key sentinel query and the key id that is being checked for.
 *
 * The code is assuming a zero padded decimal field of width 5.
 */
static void
root_key_sentinel_detect(query_ctx_t *qctx) {
	const char *ndata = (const char *)qctx->client->query.qname->ndata;

	if (qctx->client->query.qname->length > 30 && ndata[0] == 29 &&
	    strncasecmp(ndata + 1, "root-key-sentinel-is-ta-", 24) == 0)
	{
		if (!get_root_key_sentinel_id(qctx, ndata + 25)) {
			return;
		}
		qctx->client->query.root_key_sentinel_is_ta = true;
		/*
		 * Simplify processing by disabling aggressive
		 * negative caching.
		 */
		qctx->findcoveringnsec = false;
		ns_client_log(qctx->client, NS_LOGCATEGORY_TAT,
			      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
			      "root-key-sentinel-is-ta query label found");
	} else if (qctx->client->query.qname->length > 31 && ndata[0] == 30 &&
		   strncasecmp(ndata + 1, "root-key-sentinel-not-ta-", 25) == 0)
	{
		if (!get_root_key_sentinel_id(qctx, ndata + 26)) {
			return;
		}
		qctx->client->query.root_key_sentinel_not_ta = true;
		/*
		 * Simplify processing by disabling aggressive
		 * negative caching.
		 */
		qctx->findcoveringnsec = false;
		ns_client_log(qctx->client, NS_LOGCATEGORY_TAT,
			      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
			      "root-key-sentinel-not-ta query label found");
	}
}

static void
qctx_reportquery(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;

	client->inner.attributes |= NS_CLIENTATTR_WANTRC;

	/* If this isn't a report-logging zone, there's no more to do */
	dns_zoneopt_t opts = dns_zone_getoptions(qctx->zone);
	if ((opts & DNS_ZONEOPT_LOGREPORTS) == 0) {
		return;
	}

	/*
	 * Suppress EDNS Report-Channel in responses from report-
	 * logging zones; this prevents infinite loops.
	 */
	client->inner.attributes &= ~NS_CLIENTATTR_WANTRC;

	/* If this isn't an error-report query, there's nothing more to do */
	if (client->query.qtype != dns_rdatatype_txt ||
	    !dns_name_israd(client->query.qname,
			    dns_zone_getorigin(qctx->zone)))
	{
		return;
	}

	/*
	 * Check for TCP or a good server cookie.  If neither, send
	 * back BADCOOKIE or TC=1.
	 */
	if (!TCP(client) && !HAVECOOKIE(client)) {
		if (WANTCOOKIE(client)) {
			client->inner.attributes |= NS_CLIENTATTR_BADCOOKIE;
		} else {
			client->inner.attributes |= NS_CLIENTATTR_NEEDTCP;
		}
	}

	if (isc_log_wouldlog(ISC_LOG_INFO)) {
		char classbuf[DNS_RDATACLASS_FORMATSIZE];
		char namebuf[DNS_NAME_FORMATSIZE];

		dns_name_format(client->query.qname, namebuf, sizeof(namebuf));
		dns_rdataclass_format(client->inner.view->rdclass, classbuf,
				      sizeof(classbuf));

		isc_log_write(NS_LOGCATEGORY_DRA, NS_LOGMODULE_QUERY,
			      ISC_LOG_INFO, "dns-reporting-agent '%s/%s'",
			      namebuf, classbuf);
	}
}

static void
qctx_setrad(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;

	/* Set the client to send a Report-Channel option when replying */
	if ((client->inner.attributes & NS_CLIENTATTR_WANTRC) != 0) {
		dns_fixedname_t fixed;
		dns_name_t *rad = dns_fixedname_initname(&fixed);

		if (!dns_name_dynamic(&client->inner.rad) &&
		    dns_zone_getrad(qctx->zone, rad) == ISC_R_SUCCESS)
		{
			dns_name_dup(rad, client->manager->mctx,
				     &client->inner.rad);
		}
	}
}

/*%
 * Starting point for a client query or a chaining query.
 *
 * Called first by query_setup(), and then again as often as needed to
 * follow a CNAME chain.  Determines which authoritative database to
 * search, then hands off processing to query_lookup().
 */
isc_result_t
ns__query_start(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;
	CCTRACE(ISC_LOG_DEBUG(3), "ns__query_start");
	qctx->want_restart = false;
	qctx->authoritative = false;
	qctx->version = NULL;
	qctx->zversion = NULL;
	qctx->need_wildcardproof = false;
	qctx->rpz = false;

	CALL_HOOK(NS_QUERY_START_BEGIN, qctx);

	/*
	 * If we require a server cookie or the presented server
	 * cookie was bad then send back BADCOOKIE before we have
	 * done too much work.
	 */
	if (!TCP(qctx->client) &&
	    (BADCOOKIE(qctx->client) ||
	     (qctx->view->requireservercookie && WANTCOOKIE(qctx->client) &&
	      !HAVECOOKIE(qctx->client))))
	{
		qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA;
		qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD;
		qctx->client->message->rcode = dns_rcode_badcookie;
		qctx->client->inner.attributes &= ~NS_CLIENTATTR_WANTRC;
		return ns_query_done(qctx);
	}

	/*
	 * Respond with TC=1 if we need TCP for this request.
	 */
	if (!TCP(qctx->client) && NEEDTCP(qctx->client)) {
		qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA;
		qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD;
		qctx->client->message->flags |= DNS_MESSAGEFLAG_TC;
		return ns_query_done(qctx);
	}

	if (qctx->view->checknames &&
	    !dns_rdata_checkowner(qctx->client->query.qname,
				  qctx->client->message->rdclass, qctx->qtype,
				  false))
	{
		char namebuf[DNS_NAME_FORMATSIZE];
		char typebuf[DNS_RDATATYPE_FORMATSIZE];
		char classbuf[DNS_RDATACLASS_FORMATSIZE];

		dns_name_format(qctx->client->query.qname, namebuf,
				sizeof(namebuf));
		dns_rdatatype_format(qctx->qtype, typebuf, sizeof(typebuf));
		dns_rdataclass_format(qctx->client->message->rdclass, classbuf,
				      sizeof(classbuf));
		ns_client_log(qctx->client, DNS_LOGCATEGORY_SECURITY,
			      NS_LOGMODULE_QUERY, ISC_LOG_ERROR,
			      "check-names failure %s/%s/%s", namebuf, typebuf,
			      classbuf);
		QUERY_ERROR(qctx, DNS_R_REFUSED);
		return ns_query_done(qctx);
	}

	/*
	 * Setup for root key sentinel processing.
	 */
	if (qctx->view->root_key_sentinel &&
	    qctx->client->query.restarts == 0 &&
	    (dns_rdatatype_isaddr(qctx->qtype)) &&
	    (qctx->client->message->flags & DNS_MESSAGEFLAG_CD) == 0)
	{
		root_key_sentinel_detect(qctx);
	}

	/*
	 * First we must find the right database. Reset the options but preserve
	 * the 'nolog' flag.
	 */
	qctx->options = (dns_getdb_options_t){ .nolog = qctx->options.nolog };
	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.noexact = true;
	}

	result = query_getdb(qctx->client, qctx->client->query.qname,
			     qctx->qtype, qctx->options, &qctx->zone, &qctx->db,
			     &qctx->version, &qctx->is_zone);
	if ((result != ISC_R_SUCCESS || !qctx->is_zone) &&
	    qctx->qtype == dns_rdatatype_ds && !RECURSIONOK(qctx->client) &&
	    qctx->options.noexact)
	{
		/*
		 * 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;
		dns_dbversion_t *tversion = NULL;
		isc_result_t tresult;

		dns_getdb_options_t options = { .partial = true };
		tresult = query_getzonedb(
			qctx->client, qctx->client->query.qname, qctx->qtype,
			options, &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.noexact = false;
			ns_client_putrdataset(qctx->client, &qctx->rdataset);
			if (qctx->db != NULL) {
				dns_db_detach(&qctx->db);
			}
			if (qctx->zone != NULL) {
				dns_zone_detach(&qctx->zone);
			}
			qctx->version = NULL;
			RESTORE(qctx->version, tversion);
			RESTORE(qctx->db, tdb);
			RESTORE(qctx->zone, tzone);
			qctx->is_zone = 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);
			}
			if (tzone != NULL) {
				dns_zone_detach(&tzone);
			}
		}
	}
	/*
	 * 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)) {
				dns_ede_add(&qctx->client->edectx,
					    DNS_EDE_NOTAUTH,
					    "recursion disabled");
				inc_stats(qctx->client,
					  ns_statscounter_recurserej);
			} else {
				inc_stats(qctx->client,
					  ns_statscounter_authrej);
			}
			if (!PARTIALANSWER(qctx->client)) {
				QUERY_ERROR(qctx, DNS_R_REFUSED);
			}
		} else {
			CCTRACE(ISC_LOG_ERROR, "ns__query_start: query_getdb "
					       "failed");
			QUERY_ERROR(qctx, result);
		}
		return ns_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 = false;
	if (qctx->is_zone) {
		qctx->authoritative = true;
		if (qctx->zone != NULL) {
			switch (dns_zone_gettype(qctx->zone)) {
			case dns_zone_mirror:
				qctx->authoritative = false;
				break;
			case dns_zone_staticstub:
				qctx->is_staticstub_zone = true;
				break;
			case dns_zone_primary:
			case dns_zone_secondary:
				qctx_reportquery(qctx);
				qctx_setrad(qctx);
				break;
			default:
				break;
			}
		}
	}

	/*
	 * Attach to the database which will be used to prepare the answer.
	 * Update query statistics.
	 */
	if (qctx->fresp == NULL && qctx->client->query.restarts == 0) {
		if (qctx->is_zone) {
			if (qctx->zone != NULL) {
				/*
				 * if is_zone = true, zone = NULL then this is
				 * a DLZ zone.  Don't attempt to attach zone.
				 */
				dns_zone_attach(qctx->zone,
						&qctx->client->query.authzone);
			}
			dns_db_attach(qctx->db, &qctx->client->query.authdb);
		}
		qctx->client->query.authdbset = true;

		isc_nmhandle_t *handle = qctx->client->inner.handle;

		/* Track protocol stats per zone */
		switch (isc_nm_socket_type(handle)) {
		case isc_nm_httpsocket:
			switch (isc_nmhandle_proxy_type(handle)) {
			case ISC_NM_PROXY_ENCRYPTED:
				/* Encrypted PROXYv2 cannot carry plain DoH */
				INSIST(isc_nm_has_encryption(handle));
				inc_stats(qctx->client,
					  ns_statscounter_encryptedproxydoh);
				break;
			case ISC_NM_PROXY_PLAIN:
				if (isc_nm_has_encryption(handle)) {
					inc_stats(qctx->client,
						  ns_statscounter_proxydoh);
				} else {
					inc_stats(
						qctx->client,
						ns_statscounter_proxydohplain);
				}
				break;
			case ISC_NM_PROXY_NONE:
				if (isc_nm_has_encryption(handle)) {
					inc_stats(qctx->client,
						  ns_statscounter_doh);
				} else {
					inc_stats(qctx->client,
						  ns_statscounter_dohplain);
				}
				break;
			}
			break;
		case isc_nm_streamdnssocket:
			switch (isc_nmhandle_proxy_type(handle)) {
			case ISC_NM_PROXY_ENCRYPTED:
				inc_stats(qctx->client,
					  ns_statscounter_encryptedproxydot);
				break;
			case ISC_NM_PROXY_PLAIN:
				if (isc_nm_has_encryption(handle)) {
					inc_stats(qctx->client,
						  ns_statscounter_proxydot);

				} else {
					/*
					 * If the StreamDNS socket doesn't have
					 * encryption, it has to be plain TCP
					 * DNS.
					 */
					inc_stats(qctx->client,
						  ns_statscounter_proxytcp);
				}
				break;
			case ISC_NM_PROXY_NONE:
				if (isc_nm_has_encryption(handle)) {
					inc_stats(qctx->client,
						  ns_statscounter_dot);
				} else {
					/*
					 * If the StreamDNS socket doesn't have
					 * encryption, it has to be plain TCP
					 * DNS.
					 */
					inc_stats(qctx->client,
						  ns_statscounter_tcp);
				}
				break;
			}
			break;
		case isc_nm_proxyudpsocket:
			inc_stats(qctx->client, ns_statscounter_proxyudp);
			break;
		case isc_nm_udpsocket:
			inc_stats(qctx->client, ns_statscounter_udp);
			break;
		default:
			UNREACHABLE();
		}
	}

	/*
	 * If stale answers are enabled and stale-answer-client-timeout is zero,
	 * then we can promptly answer with a stale RRset if one is available in
	 * cache.
	 */
	qctx->options.stalefirst = (!qctx->is_zone &&
				    qctx->view->staleanswerclienttimeout == 0 &&
				    dns_view_staleanswerenabled(qctx->view));

	result = query_lookup(qctx);

	/*
	 * Clear "look-also-for-stale-data" flag.
	 * If a fetch is created to resolve this query, then,
	 * when it completes, this option is not expected to be set.
	 */
	qctx->options.stalefirst = false;

cleanup:
	return result;
}

static void
async_restart(void *arg) {
	query_ctx_t *qctx = arg;
	ns_client_t *client = qctx->client;
	isc_nmhandle_t *handle = client->inner.restarthandle;

	client->inner.restarthandle = NULL;

	ns__query_start(qctx);

	qctx_clean(qctx);
	qctx_freedata(qctx);
	qctx_destroy(qctx);
	isc_mem_put(client->manager->mctx, qctx, sizeof(*qctx));
	isc_nmhandle_detach(&handle);
}

/*
 * Allocate buffers in 'qctx' used to store query results.
 *
 * 'buffer' must be a pointer to an object whose lifetime
 * doesn't expire while 'qctx' is in use.
 */
static isc_result_t
qctx_prepare_buffers(query_ctx_t *qctx, isc_buffer_t *buffer) {
	REQUIRE(qctx != NULL);
	REQUIRE(qctx->client != NULL);
	REQUIRE(buffer != NULL);

	qctx->dbuf = ns_client_getnamebuf(qctx->client);
	qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, buffer);
	qctx->rdataset = ns_client_newrdataset(qctx->client);

	if ((WANTDNSSEC(qctx->client) || qctx->findcoveringnsec) &&
	    (!qctx->is_zone || dns_db_issecure(qctx->db)))
	{
		qctx->sigrdataset = ns_client_newrdataset(qctx->client);
	}

	return ISC_R_SUCCESS;
}

/*%
 * Depending on the db lookup result, we can respond to the
 * client this stale answer.
 */
static bool
stale_client_answer(isc_result_t result) {
	switch (result) {
	case ISC_R_SUCCESS:
	case DNS_R_EMPTYNAME:
	case DNS_R_NXRRSET:
	case DNS_R_NCACHENXRRSET:
	case DNS_R_CNAME:
	case DNS_R_DNAME:
		return true;
	default:
		return false;
	}

	UNREACHABLE();
}

/*%
 * Perform a local database lookup, in either an authoritative or
 * cache database. If unable to answer, call ns_query_done(); otherwise
 * hand off processing to query_gotanswer().
 */
static isc_result_t
query_lookup(query_ctx_t *qctx) {
	isc_buffer_t buffer;
	isc_result_t result = ISC_R_UNSET;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	dns_name_t *rpzqname = NULL;
	char namebuf[DNS_NAME_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];
	unsigned int dboptions;
	dns_ttl_t stale_refresh = 0;
	bool dbfind_stale = false;
	bool stale_timeout = false;
	bool answer_found = false;
	bool stale_found = false;
	bool stale_refresh_window = false;
	uint16_t ede = 0;

	CCTRACE(ISC_LOG_DEBUG(3), "query_lookup");

	CALL_HOOK(NS_QUERY_LOOKUP_BEGIN, qctx);

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, qctx->client, NULL);
	if (HAVEECS(qctx->client)) {
		dns_clientinfo_setecs(&ci, &qctx->client->inner.ecs);
	}

	/*
	 * We'll need some resources...
	 */
	result = qctx_prepare_buffers(qctx, &buffer);
	if (result != ISC_R_SUCCESS) {
		QUERY_ERROR(qctx, result);
		return ns_query_done(qctx);
	}

	/*
	 * Now look for an answer in the database.
	 */
	if (qctx->dns64 && qctx->rpz) {
		rpzqname = qctx->client->query.rpz_st->p_name;
	} else {
		rpzqname = qctx->client->query.qname;
	}

	qctx->client->query.dboptions &= ~DNS_DBFIND_STALETIMEOUT;

	if (qctx->options.stalefirst && !qctx->is_zone) {
		/*
		 * If the 'stalefirst' flag is set, it means that a stale
		 * RRset may be returned as part of this lookup. An attempt
		 * to refresh the RRset will still take place if an
		 * active RRset is not available.
		 */
		qctx->client->query.dboptions |= DNS_DBFIND_STALETIMEOUT;
	}

	(void)dns_db_getservestalerefresh(qctx->client->inner.view->cachedb,
					  &stale_refresh);
	if (stale_refresh > 0 &&
	    dns_view_staleanswerenabled(qctx->client->inner.view))
	{
		qctx->client->query.dboptions |= DNS_DBFIND_STALEENABLED;
	}

	dboptions = qctx->client->query.dboptions;
	if (!qctx->is_zone && qctx->findcoveringnsec &&
	    (qctx->type != dns_rdatatype_null || !dns_name_istat(rpzqname)))
	{
		dboptions |= DNS_DBFIND_COVERINGNSEC;
	}

	result = dns_db_findext(qctx->db, rpzqname, qctx->version, qctx->type,
				dboptions, qctx->client->inner.now, &qctx->node,
				qctx->fname, &cm, &ci, qctx->rdataset,
				qctx->sigrdataset);

	/*
	 * Fixup fname and sigrdataset.
	 */
	if (qctx->dns64 && qctx->rpz) {
		dns_name_copy(qctx->client->query.qname, qctx->fname);
		if (qctx->sigrdataset != NULL &&
		    dns_rdataset_isassociated(qctx->sigrdataset))
		{
			dns_rdataset_disassociate(qctx->sigrdataset);
		}
	}

	if (!qctx->is_zone) {
		dns_cache_updatestats(qctx->view->cache, result);
	}

	/*
	 * If DNS_DBFIND_STALEOK is set this means we are dealing with a
	 * lookup following a failed lookup and it is okay to serve a stale
	 * answer. This will (re)start the 'stale-refresh-time' window in
	 * rbtdb, tracking the last time the RRset lookup failed.
	 */
	dbfind_stale = ((dboptions & DNS_DBFIND_STALEOK) != 0);

	/*
	 * If DNS_DBFIND_STALEENABLED is set, this may be a normal lookup, but
	 * we are allowed to immediately respond with a stale answer if the
	 * request is within the 'stale-refresh-time' window.
	 */
	stale_refresh_window = (STALE_WINDOW(qctx->rdataset) &&
				(dboptions & DNS_DBFIND_STALEENABLED) != 0);

	/*
	 * If DNS_DBFIND_STALETIMEOUT is set, a stale answer is requested.
	 * This can happen if 'stale-answer-client-timeout' is enabled.
	 *
	 * If a stale answer is found, send it to the client, and try to refresh
	 * the RRset.
	 */
	stale_timeout = ((dboptions & DNS_DBFIND_STALETIMEOUT) != 0);

	if (dns_rdataset_isassociated(qctx->rdataset) &&
	    dns_rdataset_count(qctx->rdataset) > 0 && !STALE(qctx->rdataset))
	{
		/* Found non-stale usable rdataset. */
		answer_found = true;
	}

	if (dbfind_stale || stale_refresh_window || stale_timeout) {
		dns_name_format(qctx->client->query.qname, namebuf,
				sizeof(namebuf));
		dns_rdatatype_format(qctx->qtype, typebuf, sizeof(typebuf));

		inc_stats(qctx->client, ns_statscounter_trystale);

		if (dns_rdataset_isassociated(qctx->rdataset) &&
		    dns_rdataset_count(qctx->rdataset) > 0 &&
		    STALE(qctx->rdataset))
		{
			stale_found = true;
			if (result == DNS_R_NCACHENXDOMAIN ||
			    result == DNS_R_NXDOMAIN)
			{
				ede = DNS_EDE_STALENXANSWER;
			} else {
				ede = DNS_EDE_STALEANSWER;
			}
			qctx->rdataset->ttl = qctx->view->staleanswerttl;
			inc_stats(qctx->client, ns_statscounter_usedstale);
		} else {
			stale_found = false;
		}
	}

	if (dbfind_stale) {
		isc_log_write(NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY,
			      ISC_LOG_INFO,
			      "%s %s resolver failure, stale answer %s (%s)",
			      namebuf, typebuf,
			      stale_found ? "used" : "unavailable",
			      isc_result_totext(result));
		if (stale_found) {
			dns_ede_add(&qctx->client->edectx, ede,
				    "resolver failure");
		} else if (!answer_found) {
			/*
			 * Resolver failure, no stale data, nothing more we
			 * can do, return SERVFAIL.
			 */
			QUERY_ERROR(qctx, DNS_R_SERVFAIL);
			return ns_query_done(qctx);
		}
	} else if (stale_refresh_window) {
		/*
		 * A recent lookup failed, so during this time window we are
		 * allowed to return stale data immediately.
		 */
		isc_log_write(NS_LOGCATEGORY_SERVE_STALE, NS_LOGMODULE_QUERY,
			      ISC_LOG_INFO,
			      "%s %s query within stale refresh time, stale "
			      "answer %s (%s)",
			      namebuf, typebuf,
			      stale_found ? "used" : "unavailable",
			      isc_result_totext(result));

		if (stale_found) {
			dns_ede_add(&qctx->client->edectx, ede,
				    "query within stale refresh time window");
		} else if (!answer_found) {
			/*
			 * During the stale refresh window explicitly do not try
			 * to refresh the data, because a recent lookup failed.
			 */
			QUERY_ERROR(qctx, DNS_R_SERVFAIL);
			return ns_query_done(qctx);
		}
	} else if (stale_timeout) {
		if (qctx->options.stalefirst) {
			/*
			 * If 'qctx->zdb' is set, this was a cache lookup after
			 * an authoritative lookup returned a delegation (in
			 * order to find a better answer). But we still can
			 * return without getting any usable answer here, as
			 * query_notfound() should handle it from here.
			 * Otherwise, if nothing useful was found in cache then
			 * recursively call query_lookup() again without the
			 * 'stalefirst' option set.
			 */
			if (!stale_found && !answer_found && qctx->zdb == NULL)
			{
				qctx_clean(qctx);
				qctx_freedata(qctx);
				dns_db_attach(qctx->client->inner.view->cachedb,
					      &qctx->db);
				qctx->options.stalefirst = false;
				if (FETCH_RECTYPE_NORMAL(qctx->client) != NULL)
				{
					dns_resolver_destroyfetch(
						&FETCH_RECTYPE_NORMAL(
							qctx->client));
				}
				return query_lookup(qctx);
			} else if (stale_client_answer(result)) {
				/*
				 * Immediately return the stale answer, start a
				 * resolver fetch to refresh the data in cache.
				 */
				if (stale_found) {
					dns_ede_add(
						&qctx->client->edectx, ede,
						"stale data prioritized over "
						"lookup");
				}
			}
		} else {
			UNREACHABLE();
		}
	}

	result = query_gotanswer(qctx, result);

cleanup:
	return result;
}

/*
 * Event handler to resume processing a query after recursion, or when a
 * client timeout is triggered. If the query has timed out or been cancelled
 * or the system is shutting down, clean up and exit. If a client timeout is
 * triggered, see if we can respond with a stale answer from cache. Otherwise,
 * call query_resume() to continue the ongoing work.
 */
static void
fetch_callback(void *arg) {
	dns_fetchresponse_t *resp = (dns_fetchresponse_t *)arg;
	ns_client_t *client = resp->arg;
	dns_fetch_t *fetch = NULL;
	bool fetch_canceled = false;
	isc_logcategory_t logcategory = NS_LOGCATEGORY_QUERY_ERRORS;
	isc_result_t result;
	int errorloglevel;
	query_ctx_t qctx;

	REQUIRE(NS_CLIENT_VALID(client));
	REQUIRE(RECURSING(client));

	CTRACE(ISC_LOG_DEBUG(3), "fetch_callback");

	/*
	 * We are resuming from recursion. Reset any attributes, options
	 * that a lookup due to stale-answer-client-timeout may have set.
	 */
	if (client->inner.view->cachedb != NULL &&
	    client->inner.view->recursion)
	{
		client->query.attributes |= NS_QUERYATTR_RECURSIONOK;
	}
	client->query.dboptions &= ~DNS_DBFIND_STALETIMEOUT;
	client->query.dboptions &= ~DNS_DBFIND_STALEENABLED;

	LOCK(&client->query.fetchlock);
	INSIST(FETCH_RECTYPE_NORMAL(client) == resp->fetch ||
	       FETCH_RECTYPE_NORMAL(client) == NULL);
	if (FETCH_RECTYPE_NORMAL(client) != NULL) {
		/*
		 * This is the fetch we've been waiting for.
		 */
		INSIST(FETCH_RECTYPE_NORMAL(client) == resp->fetch);
		FETCH_RECTYPE_NORMAL(client) = NULL;

		/*
		 * Update client->now.
		 */
		client->inner.now = isc_stdtime_now();
	} else {
		/*
		 * This is a fetch completion event for a canceled fetch.
		 * Clean up and don't resume the find.
		 */
		fetch_canceled = true;
	}
	UNLOCK(&client->query.fetchlock);

	SAVE(fetch, resp->fetch);

	/*
	 * We're done recursing, detach from quota and unlink from
	 * the manager's recursing-clients list.
	 */
	release_recursionquota(client);

	isc_nmhandle_detach(&HANDLE_RECTYPE_NORMAL(client));

	client->query.attributes &= ~NS_QUERYATTR_RECURSING;
	client->inner.state = NS_CLIENTSTATE_WORKING;

	/*
	 * Initialize a new qctx and use it to either resume from
	 * recursion or clean up after cancelation.  Transfer
	 * ownership of resp to the new qctx in the process.
	 */
	qctx_init(client, &resp, 0, &qctx);

	if (fetch_canceled) {
		/*
		 * We've timed out or are shutting down. We can now
		 * free the event and other resources held by qctx, but
		 * don't call qctx_destroy() yet: it might destroy the
		 * client, which we still need for a moment.
		 */
		qctx_freedata(&qctx);

		/*
		 * Return an error to the client.
		 */
		CTRACE(ISC_LOG_ERROR, "fetch cancelled");
		query_error(client, DNS_R_SERVFAIL, __LINE__);

		/*
		 * Free any persistent plugin data that was allocated to
		 * service the client, then detach the client object.
		 */
		qctx.detach_client = true;
		qctx_destroy(&qctx);
	} else {
		/*
		 * Resume the find process.
		 */
		query_trace(&qctx);

		result = query_resume(&qctx);
		if (result != ISC_R_SUCCESS) {
			if (result == DNS_R_SERVFAIL) {
				errorloglevel = ISC_LOG_DEBUG(2);
			} else {
				errorloglevel = ISC_LOG_DEBUG(4);
			}
			if (isc_log_wouldlog(errorloglevel)) {
				dns_resolver_logfetch(fetch, logcategory,
						      NS_LOGMODULE_QUERY,
						      errorloglevel, false);
			}
		}

		qctx_destroy(&qctx);
	}

	dns_resolver_destroyfetch(&fetch);
}

/*%
 * Check whether the recursion parameters in 'param' match the current query's
 * recursion parameters provided in 'qtype', 'qname', and 'qdomain'.
 */
static bool
recparam_match(const ns_query_recparam_t *param, dns_rdatatype_t qtype,
	       const dns_name_t *qname, const dns_name_t *qdomain) {
	REQUIRE(param != NULL);

	return param->qtype == qtype && param->qname != NULL && qname != NULL &&
	       param->qdomain != NULL && qdomain != NULL &&
	       dns_name_equal(param->qname, qname) &&
	       dns_name_equal(param->qdomain, qdomain);
}

/*%
 * Update 'param' with current query's recursion parameters provided in
 * 'qtype', 'qname', and 'qdomain'.
 */
static void
recparam_update(ns_query_recparam_t *param, dns_rdatatype_t qtype,
		const dns_name_t *qname, const dns_name_t *qdomain) {
	REQUIRE(param != NULL);

	param->qtype = qtype;

	if (qname == NULL) {
		param->qname = NULL;
	} else {
		param->qname = dns_fixedname_initname(&param->fqname);
		dns_name_copy(qname, param->qname);
	}

	if (qdomain == NULL) {
		param->qdomain = NULL;
	} else {
		param->qdomain = dns_fixedname_initname(&param->fqdomain);
		dns_name_copy(qdomain, param->qdomain);
	}
}

static void
recursionquota_log(ns_client_t *client, atomic_uint_fast32_t *last_log_time,
		   const char *format, isc_quota_t *quota) {
	isc_stdtime_t now = isc_stdtime_now();
	if (now == atomic_load_relaxed(last_log_time)) {
		return;
	}

	atomic_store_relaxed(last_log_time, now);
	ns_client_log(client, NS_LOGCATEGORY_CLIENT, NS_LOGMODULE_QUERY,
		      ISC_LOG_WARNING, format, isc_quota_getused(quota),
		      isc_quota_getsoft(quota), isc_quota_getmax(quota));
}

static atomic_uint_fast32_t last_soft, last_hard;

/*%
 * Acquire recursion quota before making the current client "recursing".
 */
static isc_result_t
acquire_recursionquota(ns_client_t *client) {
	isc_result_t result;

	result = recursionquotatype_attach_soft(client);
	switch (result) {
	case ISC_R_SOFTQUOTA:
		recursionquota_log(client, &last_soft,
				   "recursive-clients soft limit exceeded "
				   "(%u/%u/%u), aborting oldest query",
				   &client->manager->sctx->recursionquota);
		ns_client_killoldestquery(client);
		FALLTHROUGH;
	case ISC_R_SUCCESS:
		break;
	case ISC_R_QUOTA:
		recursionquota_log(client, &last_hard,
				   "no more recursive clients (%u/%u/%u)",
				   &client->manager->sctx->recursionquota);
		ns_client_killoldestquery(client);
		return result;
	default:
		UNREACHABLE();
	}

	dns_message_clonebuffer(client->message);
	ns_client_recursing(client);

	return ISC_R_SUCCESS;
}

/*%
 * Release recursion quota and remove the client from the "recursing" list.
 */
static void
release_recursionquota(ns_client_t *client) {
	recursionquotatype_detach(client);

	LOCK(&client->manager->reclock);
	if (ISC_LINK_LINKED(client, inner.rlink)) {
		ISC_LIST_UNLINK(client->manager->recursing, client,
				inner.rlink);
	}
	UNLOCK(&client->manager->reclock);
}

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 = acquire_recursionquota(client);
	if (result != ISC_R_SUCCESS) {
		return result;
	}

	/*
	 * Invoke the resolver.
	 */
	REQUIRE(nameservers == NULL || nameservers->type == dns_rdatatype_ns);
	REQUIRE(FETCH_RECTYPE_NORMAL(client) == NULL);

	rdataset = ns_client_newrdataset(client);

	if (WANTDNSSEC(client)) {
		sigrdataset = ns_client_newrdataset(client);
	} else {
		sigrdataset = NULL;
	}

	if (!client->query.timerset) {
		ns_client_settimeout(client, 60);
	}

	if (!TCP(client)) {
		peeraddr = &client->inner.peeraddr;
	}

	isc_nmhandle_attach(client->inner.handle,
			    &HANDLE_RECTYPE_NORMAL(client));
	result = dns_resolver_createfetch(
		client->inner.view->resolver, qname, qtype, qdomain,
		nameservers, NULL, peeraddr, client->message->id,
		client->query.fetchoptions, 0, NULL, client->query.qc,
		client->manager->loop, fetch_callback, client, &client->edectx,
		rdataset, sigrdataset, &FETCH_RECTYPE_NORMAL(client));
	if (result != ISC_R_SUCCESS) {
		release_recursionquota(client);

		ns_client_putrdataset(client, &rdataset);
		if (sigrdataset != NULL) {
			ns_client_putrdataset(client, &sigrdataset);
		}

		isc_nmhandle_detach(&HANDLE_RECTYPE_NORMAL(client));
	}

	/*
	 * We're now waiting for a fetch event. A client which is
	 * shutting down will not be destroyed until all the events
	 * have been received.
	 */

	return result;
}

/*%
 * Restores the query context after resuming from recursion, and
 * continues the query processing if needed.
 */
static isc_result_t
query_resume(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;
	dns_name_t *tname;
	isc_buffer_t b;
#ifdef WANT_QUERYTRACE
	char mbuf[4 * DNS_NAME_FORMATSIZE];
	char qbuf[DNS_NAME_FORMATSIZE];
	char tbuf[DNS_RDATATYPE_FORMATSIZE];
#endif /* ifdef WANT_QUERYTRACE */

	CCTRACE(ISC_LOG_DEBUG(3), "query_resume");

	CALL_HOOK(NS_QUERY_RESUME_BEGIN, qctx);

	qctx->want_restart = false;

	qctx->rpz_st = qctx->client->query.rpz_st;
	if (qctx->rpz_st != NULL &&
	    (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0)
	{
		CCTRACE(ISC_LOG_DEBUG(3), "resume from RPZ recursion");
#ifdef WANT_QUERYTRACE
		{
			char pbuf[DNS_NAME_FORMATSIZE] = "<unset>";
			char fbuf[DNS_NAME_FORMATSIZE] = "<unset>";
			if (qctx->rpz_st->r_name != NULL) {
				dns_name_format(qctx->rpz_st->r_name, qbuf,
						sizeof(qbuf));
			} else {
				snprintf(qbuf, sizeof(qbuf), "<unset>");
			}
			if (qctx->rpz_st->p_name != NULL) {
				dns_name_format(qctx->rpz_st->p_name, pbuf,
						sizeof(pbuf));
			}
			if (qctx->rpz_st->fname != NULL) {
				dns_name_format(qctx->rpz_st->fname, fbuf,
						sizeof(fbuf));
			}

			snprintf(mbuf, sizeof(mbuf) - 1,
				 "rpz rname:%s, pname:%s, qctx->fname:%s", qbuf,
				 pbuf, fbuf);
			CCTRACE(ISC_LOG_DEBUG(3), mbuf);
		}
#endif /* ifdef WANT_QUERYTRACE */

		qctx->is_zone = qctx->rpz_st->q.is_zone;
		qctx->authoritative = qctx->rpz_st->q.authoritative;
		RESTORE(qctx->zone, qctx->rpz_st->q.zone);
		RESTORE(qctx->node, qctx->rpz_st->q.node);
		RESTORE(qctx->db, qctx->rpz_st->q.db);
		RESTORE(qctx->rdataset, qctx->rpz_st->q.rdataset);
		RESTORE(qctx->sigrdataset, qctx->rpz_st->q.sigrdataset);
		qctx->qtype = qctx->rpz_st->q.qtype;

		if (qctx->fresp->node != NULL) {
			dns_db_detachnode(&qctx->fresp->node);
		}
		SAVE(qctx->rpz_st->r.db, qctx->fresp->db);
		qctx->rpz_st->r.r_type = qctx->fresp->qtype;
		SAVE(qctx->rpz_st->r.r_rdataset, qctx->fresp->rdataset);
		ns_client_putrdataset(qctx->client, &qctx->fresp->sigrdataset);
	} else if (REDIRECT(qctx->client)) {
		/*
		 * Restore saved state.
		 */
		CCTRACE(ISC_LOG_DEBUG(3), "resume from redirect recursion");
#ifdef WANT_QUERYTRACE
		dns_name_format(qctx->client->query.redirect.fname, qbuf,
				sizeof(qbuf));
		dns_rdatatype_format(qctx->client->query.redirect.qtype, tbuf,
				     sizeof(tbuf));
		snprintf(mbuf, sizeof(mbuf) - 1,
			 "redirect qctx->fname:%s, qtype:%s, auth:%d", qbuf,
			 tbuf, qctx->client->query.redirect.authoritative);
		CCTRACE(ISC_LOG_DEBUG(3), mbuf);
#endif /* ifdef WANT_QUERYTRACE */
		qctx->qtype = qctx->client->query.redirect.qtype;
		INSIST(qctx->client->query.redirect.rdataset != NULL);
		RESTORE(qctx->rdataset, qctx->client->query.redirect.rdataset);
		RESTORE(qctx->sigrdataset,
			qctx->client->query.redirect.sigrdataset);
		RESTORE(qctx->db, qctx->client->query.redirect.db);
		RESTORE(qctx->node, qctx->client->query.redirect.node);
		RESTORE(qctx->zone, qctx->client->query.redirect.zone);
		qctx->authoritative =
			qctx->client->query.redirect.authoritative;

		/*
		 * Free resources used while recursing.
		 */
		ns_client_putrdataset(qctx->client, &qctx->fresp->rdataset);
		ns_client_putrdataset(qctx->client, &qctx->fresp->sigrdataset);
		if (qctx->fresp->node != NULL) {
			dns_db_detachnode(&qctx->fresp->node);
		}
		if (qctx->fresp->db != NULL) {
			dns_db_detach(&qctx->fresp->db);
		}
	} else {
		CCTRACE(ISC_LOG_DEBUG(3), "resume from normal recursion");
		qctx->authoritative = false;

		qctx->qtype = qctx->fresp->qtype;
		SAVE(qctx->db, qctx->fresp->db);
		SAVE(qctx->node, qctx->fresp->node);
		SAVE(qctx->rdataset, qctx->fresp->rdataset);
		SAVE(qctx->sigrdataset, qctx->fresp->sigrdataset);
	}
	INSIST(qctx->rdataset != NULL);

	if (dns_rdatatype_issig(qctx->qtype)) {
		qctx->type = dns_rdatatype_any;
	} else {
		qctx->type = qctx->qtype;
	}

	CALL_HOOK(NS_QUERY_RESUME_RESTORED, qctx);

	if (DNS64(qctx->client)) {
		qctx->client->query.attributes &= ~NS_QUERYATTR_DNS64;
		qctx->dns64 = true;
	}

	if (DNS64EXCLUDE(qctx->client)) {
		qctx->client->query.attributes &= ~NS_QUERYATTR_DNS64EXCLUDE;
		qctx->dns64_exclude = true;
	}

	if (qctx->rpz_st != NULL &&
	    (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0)
	{
		/*
		 * Has response policy changed out from under us?
		 */
		if (qctx->view->rpzs == NULL ||
		    qctx->rpz_st->rpz_ver != qctx->view->rpzs->rpz_ver)
		{
			ns_client_log(qctx->client, NS_LOGCATEGORY_CLIENT,
				      NS_LOGMODULE_QUERY, DNS_RPZ_INFO_LEVEL,
				      "query_resume: RPZ settings out of date "
				      "after of a reconfiguration");
			QUERY_ERROR(qctx, DNS_R_SERVFAIL);
			return ns_query_done(qctx);
		}
	}

	/*
	 * We'll need some resources...
	 */
	qctx->dbuf = ns_client_getnamebuf(qctx->client);
	qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b);

	if (qctx->rpz_st != NULL &&
	    (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0)
	{
		tname = qctx->rpz_st->fname;
	} else if (REDIRECT(qctx->client)) {
		tname = qctx->client->query.redirect.fname;
	} else {
		tname = qctx->fresp->foundname;
	}

	dns_name_copy(tname, qctx->fname);

	if (qctx->rpz_st != NULL &&
	    (qctx->rpz_st->state & DNS_RPZ_RECURSING) != 0)
	{
		qctx->rpz_st->r.r_result = qctx->fresp->result;
		result = qctx->rpz_st->q.result;
		free_fresp(qctx->client, &qctx->fresp);
	} else if (REDIRECT(qctx->client)) {
		result = qctx->client->query.redirect.result;
	} else {
		result = qctx->fresp->result;
	}

	qctx->resuming = true;

	return query_gotanswer(qctx, result);

cleanup:
	return result;
}

static void
query_hookresume(void *arg) {
	ns_hook_resume_t *rev = (ns_hook_resume_t *)arg;
	ns_hookasync_t *hctx = NULL;
	ns_client_t *client = rev->arg;
	query_ctx_t *qctx = rev->saved_qctx;
	bool canceled;

	CTRACE(ISC_LOG_DEBUG(3), "query_hookresume");

	REQUIRE(NS_CLIENT_VALID(client));

	LOCK(&client->query.fetchlock);
	if (client->query.hookactx != NULL) {
		INSIST(rev->ctx == client->query.hookactx);
		client->query.hookactx = NULL;
		canceled = false;
		client->inner.now = isc_stdtime_now();
	} else {
		canceled = true;
	}
	UNLOCK(&client->query.fetchlock);
	SAVE(hctx, rev->ctx);

	release_recursionquota(client);

	/*
	 * The fetch handle should be detached before resuming query processing
	 * below, since that may trigger another recursion or asynchronous hook
	 * event.
	 */
	isc_nmhandle_detach(&HANDLE_RECTYPE_HOOK(client));

	client->inner.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:
			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);
		}
	}

	isc_mem_put(hctx->mctx, rev, sizeof(*rev));
	hctx->destroy(&hctx);
	qctx_destroy(qctx);
	isc_mem_put(client->manager->mctx, qctx, sizeof(*qctx));
}

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(FETCH_RECTYPE_NORMAL(client) == NULL);

	result = acquire_recursionquota(client);
	if (result != ISC_R_SUCCESS) {
		goto cleanup;
	}

	saved_qctx = isc_mem_get(client->manager->mctx, sizeof(*saved_qctx));
	qctx_save(qctx, saved_qctx);
	result = runasync(saved_qctx, client->manager->mctx, arg,
			  client->manager->loop, query_hookresume, client,
			  &client->query.hookactx);
	if (result != ISC_R_SUCCESS) {
		goto cleanup_and_detach_from_quota;
	}

	/* Record that an asynchronous copy of the qctx has been started */
	qctx->async = true;

	/*
	 * 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.  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->inner.handle, &HANDLE_RECTYPE_HOOK(client));
	return ISC_R_SUCCESS;

cleanup_and_detach_from_quota:
	release_recursionquota(client);
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->manager->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
 * from a query with CD=1, *or* if the current query has CD=0, then we just
 * return SERVFAIL again.  This prevents a validation failure from eliciting a
 * SERVFAIL response to a CD=1 query.
 */
isc_result_t
ns__query_sfcache(query_ctx_t *qctx) {
	isc_result_t failcache;
	uint32_t flags;

	/*
	 * The SERVFAIL cache doesn't apply to authoritative queries.
	 */
	if (!RECURSIONOK(qctx->client)) {
		return ISC_R_COMPLETE;
	}

	flags = 0;
#ifdef ENABLE_AFL
	if (qctx->client->manager->sctx->fuzztype == isc_fuzz_resolver) {
		failcache = ISC_R_NOTFOUND;
	} else
#endif /* ifdef ENABLE_AFL */
	{
		failcache = dns_badcache_find(
			qctx->view->failcache, qctx->client->query.qname,
			qctx->qtype, &flags,
			isc_time_seconds(&qctx->client->inner.tnow));
	}

	if (failcache != ISC_R_SUCCESS) {
		return ISC_R_COMPLETE;
	}

	if (((flags & NS_FAILCACHE_CD) != 0) ||
	    ((qctx->client->message->flags & DNS_MESSAGEFLAG_CD) == 0))
	{
		if (isc_log_wouldlog(ISC_LOG_DEBUG(1))) {
			char namebuf[DNS_NAME_FORMATSIZE];
			char typebuf[DNS_RDATATYPE_FORMATSIZE];

			dns_name_format(qctx->client->query.qname, namebuf,
					sizeof(namebuf));
			dns_rdatatype_format(qctx->qtype, typebuf,
					     sizeof(typebuf));
			ns_client_log(qctx->client, NS_LOGCATEGORY_CLIENT,
				      NS_LOGMODULE_QUERY, ISC_LOG_DEBUG(1),
				      "servfail cache hit %s/%s (%s)", namebuf,
				      typebuf,
				      ((flags & NS_FAILCACHE_CD) != 0) ? "CD=1"
								       : "CD="
									 "0");
		}

		qctx->client->inner.attributes |= NS_CLIENTATTR_NOSETFC;
		QUERY_ERROR(qctx, DNS_R_SERVFAIL);
		return ns_query_done(qctx);
	}

	return ISC_R_COMPLETE;
}

static void
query_trace_rrldrop(query_ctx_t *qctx,
		    dns_rrl_result_t rrl_result ISC_ATTR_UNUSED) {
	if (!LIBNS_RRL_DROP_ENABLED()) {
		return;
	}

	char peerbuf[ISC_SOCKADDR_FORMATSIZE];
	isc_netaddr_t peer;
	isc_netaddr_fromsockaddr(&peer, &qctx->client->inner.peeraddr);
	isc_netaddr_format(&peer, peerbuf, sizeof(peerbuf));

	char qnamebuf[DNS_NAME_FORMATSIZE];
	char fnamebuf[DNS_NAME_FORMATSIZE];
	dns_name_format(qctx->client->query.qname, qnamebuf, sizeof(qnamebuf));
	dns_name_format(qctx->fname, fnamebuf, sizeof(fnamebuf));
	LIBNS_RRL_DROP(peerbuf, qnamebuf, fnamebuf, rrl_result);
}

/*%
 * Handle response rate limiting (RRL).
 */
static isc_result_t
query_checkrrl(query_ctx_t *qctx, isc_result_t result) {
	/*
	 * Rate limit these responses to this client.
	 * Do not delay counting and handling obvious referrals,
	 *	since those won't come here again.
	 * Delay handling delegations for which we are certain to recurse and
	 *	return here (DNS_R_DELEGATION, not a child of one of our
	 *	own zones, and recursion enabled)
	 * Don't mess with responses rewritten by RPZ
	 * Count each response at most once.
	 */

	/*
	 * XXXMPA the rrl system tests fails sometimes and RRL_CHECKED
	 * is set when we are called the second time preventing the
	 * response being dropped.
	 */
	ns_client_log(
		qctx->client, DNS_LOGCATEGORY_RRL, NS_LOGMODULE_QUERY,
		ISC_LOG_DEBUG(99),
		"rrl=%p, HAVECOOKIE=%u, result=%s, "
		"fname=%p(%u), is_zone=%u, RECURSIONOK=%u, "
		"query.rpz_st=%p(%u), RRL_CHECKED=%u",
		qctx->client->inner.view->rrl, HAVECOOKIE(qctx->client),
		isc_result_toid(result), qctx->fname,
		qctx->fname != NULL ? dns_name_isabsolute(qctx->fname) : 0,
		qctx->is_zone, RECURSIONOK(qctx->client),
		qctx->client->query.rpz_st,
		qctx->client->query.rpz_st != NULL
			? ((qctx->client->query.rpz_st->state &
			    DNS_RPZ_REWRITTEN) != 0)
			: 0,
		(qctx->client->query.attributes & NS_QUERYATTR_RRL_CHECKED) !=
			0);

	if (qctx->view->rrl != NULL && !HAVECOOKIE(qctx->client) &&
	    ((qctx->fname != NULL && dns_name_isabsolute(qctx->fname)) ||
	     (result == ISC_R_NOTFOUND && !RECURSIONOK(qctx->client))) &&
	    !(result == DNS_R_DELEGATION && !qctx->is_zone &&
	      RECURSIONOK(qctx->client)) &&
	    (qctx->client->query.rpz_st == NULL ||
	     (qctx->client->query.rpz_st->state & DNS_RPZ_REWRITTEN) == 0) &&
	    (qctx->client->query.attributes & NS_QUERYATTR_RRL_CHECKED) == 0)
	{
		bool wouldlog;
		const dns_name_t *constname;
		char log_buf[DNS_RRL_LOG_BUF_LEN];
		isc_result_t resp_result;
		dns_rrl_result_t rrl_result;

		qctx->client->query.attributes |= NS_QUERYATTR_RRL_CHECKED;

		wouldlog = isc_log_wouldlog(DNS_RRL_LOG_DROP);
		constname = qctx->fname;
		if (result == DNS_R_NXDOMAIN) {
			/*
			 * Use the database origin name to rate limit NXDOMAIN
			 */
			if (qctx->db != NULL) {
				constname = dns_db_origin(qctx->db);
			}
			resp_result = result;
		} else if (result == DNS_R_NCACHENXDOMAIN &&
			   qctx->rdataset != NULL &&
			   dns_rdataset_isassociated(qctx->rdataset) &&
			   qctx->rdataset->attributes.negative)
		{
			/*
			 * Try to use owner name in the negative cache SOA.
			 */
			dns_rdataset_t nc_rdataset = DNS_RDATASET_INIT;
			dns_fixedname_t fixed;
			dns_name_t *n = dns_fixedname_initname(&fixed);
			DNS_RDATASET_FOREACH(qctx->rdataset) {
				dns_ncache_current(qctx->rdataset, n,
						   &nc_rdataset);
				if (nc_rdataset.type == dns_rdatatype_soa) {
					dns_rdataset_disassociate(&nc_rdataset);
					constname = n;
					break;
				}
				dns_rdataset_disassociate(&nc_rdataset);
			}
			resp_result = DNS_R_NXDOMAIN;
		} else if (result == DNS_R_NXRRSET || result == DNS_R_EMPTYNAME)
		{
			resp_result = DNS_R_NXRRSET;
		} else if (result == DNS_R_DELEGATION) {
			resp_result = result;
		} else if (result == ISC_R_NOTFOUND) {
			/*
			 * Handle referral to ".", including when recursion
			 * is off or not requested and the hints have not
			 * been loaded.
			 */
			constname = dns_rootname;
			resp_result = DNS_R_DELEGATION;
		} else {
			resp_result = ISC_R_SUCCESS;
		}

		rrl_result = dns_rrl(
			qctx->view, qctx->zone, &qctx->client->inner.peeraddr,
			TCP(qctx->client), qctx->client->message->rdclass,
			qctx->qtype, constname, resp_result,
			qctx->client->inner.now, wouldlog, log_buf,
			sizeof(log_buf));
		if (rrl_result != DNS_RRL_RESULT_OK) {
			/*
			 * Log dropped or slipped responses in the query-errors
			 * category so that requests are not silently lost.
			 * Starts of rate-limited bursts are logged in
			 * DNS_LOGCATEGORY_RRL.
			 *
			 * Dropped responses are counted with dropped queries
			 * in QryDropped while slipped responses are counted
			 * with other truncated responses in RespTruncated.
			 */
			if (wouldlog) {
				ns_client_log(qctx->client,
					      NS_LOGCATEGORY_QUERY_ERRORS,
					      NS_LOGMODULE_QUERY,
					      DNS_RRL_LOG_DROP, "%s", log_buf);
			}

			/*
			 * If tracing is enabled, format some extra information
			 * to pass along.
			 */
			query_trace_rrldrop(qctx, rrl_result);

			if (!qctx->view->rrl->log_only) {
				if (rrl_result == DNS_RRL_RESULT_DROP) {
					/*
					 * These will also be counted in
					 * ns_statscounter_dropped
					 */
					inc_stats(qctx->client,
						  ns_statscounter_ratedropped);
					QUERY_ERROR(qctx, DNS_R_DROP);
				} else {
					/*
					 * These will also be counted in
					 * ns_statscounter_truncatedresp
					 */
					inc_stats(qctx->client,
						  ns_statscounter_rateslipped);
					if (WANTCOOKIE(qctx->client)) {
						qctx->client->message->flags &=
							~DNS_MESSAGEFLAG_AA;
						qctx->client->message->flags &=
							~DNS_MESSAGEFLAG_AD;
						qctx->client->message->rcode =
							dns_rcode_badcookie;
						qctx->client->inner.attributes &=
							~NS_CLIENTATTR_WANTRC;
					} else {
						qctx->client->message->flags |=
							DNS_MESSAGEFLAG_TC;
						if (resp_result ==
						    DNS_R_NXDOMAIN)
						{
							qctx->client->message
								->rcode =
								dns_rcode_nxdomain;
						}
					}
				}
				return DNS_R_DROP;
			}
		}
	}

	return ISC_R_SUCCESS;
}

static void
query_rpz_add_ede(query_ctx_t *qctx) {
	if (qctx->rpz_st->m.rpz->ede != 0 &&
	    qctx->rpz_st->m.rpz->ede != UINT16_MAX)
	{
		dns_ede_add(&qctx->client->edectx, qctx->rpz_st->m.rpz->ede,
			    NULL);
	}
}

/*%
 * Do any RPZ rewriting that may be needed for this query.
 */
static isc_result_t
query_checkrpz(query_ctx_t *qctx, isc_result_t result) {
	isc_result_t rresult;

	CCTRACE(ISC_LOG_DEBUG(3), "query_checkrpz");

	rresult = rpz_rewrite(qctx->client, qctx->qtype, result, qctx->resuming,
			      qctx->rdataset, qctx->sigrdataset);
	qctx->rpz_st = qctx->client->query.rpz_st;
	switch (rresult) {
	case ISC_R_SUCCESS:
		break;
	case ISC_R_NOTFOUND:
	case DNS_R_DISALLOWED:
		return result;
	case DNS_R_DELEGATION:
		/*
		 * recursing for NS names or addresses,
		 * so save the main query state
		 */
		INSIST(!RECURSING(qctx->client));
		qctx->rpz_st->q.qtype = qctx->qtype;
		qctx->rpz_st->q.is_zone = qctx->is_zone;
		qctx->rpz_st->q.authoritative = qctx->authoritative;
		SAVE(qctx->rpz_st->q.zone, qctx->zone);
		SAVE(qctx->rpz_st->q.db, qctx->db);
		SAVE(qctx->rpz_st->q.node, qctx->node);
		SAVE(qctx->rpz_st->q.rdataset, qctx->rdataset);
		SAVE(qctx->rpz_st->q.sigrdataset, qctx->sigrdataset);
		dns_name_copy(qctx->fname, qctx->rpz_st->fname);
		qctx->rpz_st->q.result = result;
		qctx->client->query.attributes |= NS_QUERYATTR_RECURSING;
		return ISC_R_COMPLETE;
	default:
		QUERY_ERROR(qctx, rresult);
		return ISC_R_COMPLETE;
	}

	if (qctx->rpz_st->m.policy != DNS_RPZ_POLICY_MISS) {
		qctx->rpz_st->state |= DNS_RPZ_REWRITTEN;
	}

	if (qctx->rpz_st->m.policy != DNS_RPZ_POLICY_MISS &&
	    qctx->rpz_st->m.policy != DNS_RPZ_POLICY_PASSTHRU &&
	    (qctx->rpz_st->m.policy != DNS_RPZ_POLICY_TCP_ONLY ||
	     !TCP(qctx->client)) &&
	    qctx->rpz_st->m.policy != DNS_RPZ_POLICY_ERROR)
	{
		/*
		 * We got a hit and are going to answer with our
		 * fiction. Ensure that we answer with the name
		 * we looked up even if we were stopped short
		 * in recursion or for a deferral.
		 */
		dns_name_copy(qctx->client->query.qname, qctx->fname);
		rpz_clean(&qctx->zone, &qctx->db, &qctx->node, NULL);
		if (qctx->rpz_st->m.rdataset != NULL) {
			ns_client_putrdataset(qctx->client, &qctx->rdataset);
			RESTORE(qctx->rdataset, qctx->rpz_st->m.rdataset);
		} else {
			qctx_clean(qctx);
		}
		qctx->version = NULL;

		RESTORE(qctx->node, qctx->rpz_st->m.node);
		RESTORE(qctx->db, qctx->rpz_st->m.db);
		RESTORE(qctx->version, qctx->rpz_st->m.version);
		RESTORE(qctx->zone, qctx->rpz_st->m.zone);

		/*
		 * Add SOA record to additional section
		 */
		if (qctx->rpz_st->m.rpz->addsoa) {
			rresult = query_addsoa(qctx, UINT32_MAX,
					       DNS_SECTION_ADDITIONAL);
			if (rresult != ISC_R_SUCCESS) {
				QUERY_ERROR(qctx, result);
				return ISC_R_COMPLETE;
			}
		}

		switch (qctx->rpz_st->m.policy) {
		case DNS_RPZ_POLICY_TCP_ONLY:
			qctx->client->message->flags |= DNS_MESSAGEFLAG_TC;
			if (result == DNS_R_NXDOMAIN ||
			    result == DNS_R_NCACHENXDOMAIN)
			{
				qctx->client->message->rcode =
					dns_rcode_nxdomain;
			}
			rpz_log_rewrite(qctx->client, false,
					qctx->rpz_st->m.policy,
					qctx->rpz_st->m.type, qctx->zone,
					qctx->rpz_st->p_name, NULL,
					qctx->rpz_st->m.rpz->num);
			return ISC_R_COMPLETE;
		case DNS_RPZ_POLICY_DROP:
			QUERY_ERROR(qctx, DNS_R_DROP);
			rpz_log_rewrite(qctx->client, false,
					qctx->rpz_st->m.policy,
					qctx->rpz_st->m.type, qctx->zone,
					qctx->rpz_st->p_name, NULL,
					qctx->rpz_st->m.rpz->num);
			return ISC_R_COMPLETE;
		case DNS_RPZ_POLICY_NXDOMAIN:
			result = DNS_R_NXDOMAIN;
			qctx->nxrewrite = true;
			qctx->rpz = true;
			break;
		case DNS_RPZ_POLICY_NODATA:
			qctx->nxrewrite = true;
			FALLTHROUGH;
		case DNS_RPZ_POLICY_DNS64:
			result = DNS_R_NXRRSET;
			qctx->rpz = true;
			break;
		case DNS_RPZ_POLICY_RECORD:
			result = qctx->rpz_st->m.result;
			if (qctx->qtype == dns_rdatatype_any &&
			    result != DNS_R_CNAME)
			{
				/*
				 * We will add all of the rdatasets of
				 * the node by iterating later,
				 * and set the TTL then.
				 */
				if (dns_rdataset_isassociated(qctx->rdataset)) {
					dns_rdataset_disassociate(
						qctx->rdataset);
				}
			} else {
				/*
				 * We will add this rdataset.
				 */
				qctx->rdataset->ttl =
					ISC_MIN(qctx->rdataset->ttl,
						qctx->rpz_st->m.ttl);
			}
			qctx->rpz = true;
			break;
		case DNS_RPZ_POLICY_WILDCNAME: {
			dns_rdata_t rdata = DNS_RDATA_INIT;
			dns_rdata_cname_t cname;
			result = dns_rdataset_first(qctx->rdataset);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			dns_rdataset_current(qctx->rdataset, &rdata);
			result = dns_rdata_tostruct(&rdata, &cname, NULL);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			dns_rdata_reset(&rdata);

			query_rpz_add_ede(qctx);
			result = query_rpzcname(qctx, &cname.cname);
			if (result != ISC_R_SUCCESS) {
				return ISC_R_COMPLETE;
			}
			qctx->fname = NULL;
			qctx->want_restart = true;
			return ISC_R_COMPLETE;
		}
		case DNS_RPZ_POLICY_CNAME:
			/*
			 * Add overriding CNAME from a named.conf
			 * response-policy statement
			 */
			query_rpz_add_ede(qctx);
			result = query_rpzcname(qctx,
						&qctx->rpz_st->m.rpz->cname);
			if (result != ISC_R_SUCCESS) {
				return ISC_R_COMPLETE;
			}
			qctx->fname = NULL;
			qctx->want_restart = true;
			return ISC_R_COMPLETE;
		default:
			UNREACHABLE();
		}

		query_rpz_add_ede(qctx);

		/*
		 * Turn off DNSSEC because the results of a
		 * response policy zone cannot verify.
		 */
		qctx->client->inner.attributes &= ~(NS_CLIENTATTR_WANTDNSSEC |
						    NS_CLIENTATTR_WANTAD);
		qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AD;
		ns_client_putrdataset(qctx->client, &qctx->sigrdataset);
		qctx->rpz_st->q.is_zone = qctx->is_zone;
		qctx->is_zone = true;
		rpz_log_rewrite(qctx->client, false, qctx->rpz_st->m.policy,
				qctx->rpz_st->m.type, qctx->zone,
				qctx->rpz_st->p_name, NULL,
				qctx->rpz_st->m.rpz->num);
	}

	return result;
}

/*%
 * Add a CNAME to a query response, including translating foo.evil.com and
 *	*.evil.com CNAME *.example.com
 * to
 *	foo.evil.com CNAME foo.evil.com.example.com
 */
static isc_result_t
query_rpzcname(query_ctx_t *qctx, dns_name_t *cname) {
	ns_client_t *client;
	dns_fixedname_t prefix, suffix;
	unsigned int labels;
	isc_result_t result;

	REQUIRE(qctx != NULL && qctx->client != NULL);

	client = qctx->client;

	CTRACE(ISC_LOG_DEBUG(3), "query_rpzcname");

	labels = dns_name_countlabels(cname);
	if (labels > 2 && dns_name_iswildcard(cname)) {
		dns_fixedname_init(&prefix);
		dns_name_split(client->query.qname, 1,
			       dns_fixedname_name(&prefix), NULL);
		dns_fixedname_init(&suffix);
		dns_name_split(cname, labels - 1, NULL,
			       dns_fixedname_name(&suffix));
		result = dns_name_concatenate(dns_fixedname_name(&prefix),
					      dns_fixedname_name(&suffix),
					      qctx->fname);
		if (result == DNS_R_NAMETOOLONG) {
			client->message->rcode = dns_rcode_yxdomain;
		} else if (result != ISC_R_SUCCESS) {
			return result;
		}
	} else {
		dns_name_copy(cname, qctx->fname);
	}

	ns_client_keepname(client, qctx->fname, qctx->dbuf);
	query_addcname(qctx, dns_trust_authanswer, qctx->rpz_st->m.ttl);

	rpz_log_rewrite(client, false, qctx->rpz_st->m.policy,
			qctx->rpz_st->m.type, qctx->rpz_st->m.zone,
			qctx->rpz_st->p_name, qctx->fname,
			qctx->rpz_st->m.rpz->num);

	ns_client_qnamereplace(client, qctx->fname);

	/*
	 * Turn off DNSSEC because the results of a
	 * response policy zone cannot verify.
	 */
	client->inner.attributes &= ~(NS_CLIENTATTR_WANTDNSSEC |
				      NS_CLIENTATTR_WANTAD);

	return ISC_R_SUCCESS;
}

/*%
 * Check the configured trust anchors for a root zone trust anchor
 * with a key id that matches qctx->client->query.root_key_sentinel_keyid.
 *
 * Return true when found, otherwise return false.
 */
static bool
has_ta(query_ctx_t *qctx) {
	dns_keytable_t *keytable = NULL;
	dns_keynode_t *keynode = NULL;
	dns_rdataset_t dsset;
	dns_keytag_t sentinel = qctx->client->query.root_key_sentinel_keyid;
	isc_result_t result;

	result = dns_view_getsecroots(qctx->view, &keytable);
	if (result != ISC_R_SUCCESS) {
		return false;
	}

	result = dns_keytable_find(keytable, dns_rootname, &keynode);
	if (result != ISC_R_SUCCESS) {
		if (keynode != NULL) {
			dns_keynode_detach(&keynode);
		}
		dns_keytable_detach(&keytable);
		return false;
	}

	dns_rdataset_init(&dsset);
	if (dns_keynode_dsset(keynode, &dsset)) {
		DNS_RDATASET_FOREACH(&dsset) {
			dns_rdata_t rdata = DNS_RDATA_INIT;
			dns_rdata_ds_t ds;

			dns_rdata_reset(&rdata);
			dns_rdataset_current(&dsset, &rdata);
			result = dns_rdata_tostruct(&rdata, &ds, NULL);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			if (ds.key_tag == sentinel) {
				dns_keynode_detach(&keynode);
				dns_keytable_detach(&keytable);
				dns_rdataset_disassociate(&dsset);
				return true;
			}
		}
		dns_rdataset_disassociate(&dsset);
	}

	if (keynode != NULL) {
		dns_keynode_detach(&keynode);
	}

	dns_keytable_detach(&keytable);

	return false;
}

/*%
 * Check if a root key sentinel SERVFAIL should be returned.
 */
static bool
root_key_sentinel_return_servfail(query_ctx_t *qctx, isc_result_t result) {
	/*
	 * Are we looking at a "root-key-sentinel" query?
	 */
	if (!qctx->client->query.root_key_sentinel_is_ta &&
	    !qctx->client->query.root_key_sentinel_not_ta)
	{
		return false;
	}

	/*
	 * We only care about the query if 'result' indicates we have a cached
	 * answer.
	 */
	switch (result) {
	case ISC_R_SUCCESS:
	case DNS_R_CNAME:
	case DNS_R_DNAME:
	case DNS_R_NCACHENXDOMAIN:
	case DNS_R_NCACHENXRRSET:
		break;
	default:
		return false;
	}

	/*
	 * Do we meet the specified conditions to return SERVFAIL?
	 */
	if (!qctx->is_zone && qctx->rdataset->trust == dns_trust_secure &&
	    ((qctx->client->query.root_key_sentinel_is_ta && !has_ta(qctx)) ||
	     (qctx->client->query.root_key_sentinel_not_ta && has_ta(qctx))))
	{
		return true;
	}

	/*
	 * As special processing may only be triggered by the original QNAME,
	 * disable it after following a CNAME/DNAME.
	 */
	qctx->client->query.root_key_sentinel_is_ta = false;
	qctx->client->query.root_key_sentinel_not_ta = false;

	return false;
}

/*%
 * If serving stale answers is allowed, set up 'qctx' to look for one and
 * return true; otherwise, return false.
 */
static bool
query_usestale(query_ctx_t *qctx, isc_result_t result) {
	if ((qctx->client->query.dboptions & DNS_DBFIND_STALEOK) != 0) {
		/*
		 * Query was already using stale, if that didn't work the
		 * last time, it won't work this time either.
		 */
		return false;
	}

	if (result == DNS_R_DUPLICATE || result == DNS_R_DROP) {
		/*
		 * Don't enable serve-stale if the result signals a duplicate
		 * query or query that is being dropped.
		 */
		return false;
	}

	qctx_clean(qctx);
	qctx_freedata(qctx);

	if (dns_view_staleanswerenabled(qctx->client->inner.view)) {
		isc_result_t ret;
		ret = query_getdb(qctx->client, qctx->client->query.qname,
				  qctx->client->query.qtype, qctx->options,
				  &qctx->zone, &qctx->db, &qctx->version,
				  &qctx->is_zone);
		if (ret != ISC_R_SUCCESS) {
			/*
			 * Failed to get the database, unexpected, but let us
			 * at least abandon serve-stale.
			 */
			return false;
		}

		qctx->client->query.dboptions |= DNS_DBFIND_STALEOK;
		if (FETCH_RECTYPE_NORMAL(qctx->client) != NULL) {
			dns_resolver_destroyfetch(
				&FETCH_RECTYPE_NORMAL(qctx->client));
		}

		/*
		 * Start the stale-refresh-time window in case there was a
		 * resolver query timeout.
		 */
		if (qctx->resuming && result == ISC_R_TIMEDOUT) {
			qctx->client->query.dboptions |= DNS_DBFIND_STALESTART;
		}
		return true;
	}

	return false;
}

/*%
 * Continue after doing a database lookup or returning from
 * recursion, and call out to the next function depending on the
 * result from the search.
 */
static isc_result_t
query_gotanswer(query_ctx_t *qctx, isc_result_t result) {
	char errmsg[256];

	CCTRACE(ISC_LOG_DEBUG(3), "query_gotanswer");

	CALL_HOOK(NS_QUERY_GOT_ANSWER_BEGIN, qctx);

	if (query_checkrrl(qctx, result) != ISC_R_SUCCESS) {
		return ns_query_done(qctx);
	}

	if (!dns_name_equal(qctx->client->query.qname, dns_rootname)) {
		result = query_checkrpz(qctx, result);
		if (result == ISC_R_NOTFOUND) {
			/*
			 * RPZ not configured for this view.
			 */
			goto root_key_sentinel;
		}
		if (RECURSING(qctx->client) && result == DNS_R_DISALLOWED) {
			/*
			 * We are recursing, and thus RPZ processing is not
			 * allowed at the moment. This could happen on a
			 * "stale-answer-client-timeout" lookup. In this case,
			 * bail out and wait for recursion to complete, as we
			 * we can't perform the RPZ rewrite rules.
			 */
			return result;
		}
		if (result == ISC_R_COMPLETE) {
			return ns_query_done(qctx);
		}
	}

root_key_sentinel:
	/*
	 * If required, handle special "root-key-sentinel-is-ta-<keyid>" and
	 * "root-key-sentinel-not-ta-<keyid>" labels by returning SERVFAIL.
	 */
	if (root_key_sentinel_return_servfail(qctx, result)) {
		/*
		 * Don't record this response in the SERVFAIL cache.
		 */
		qctx->client->inner.attributes |= NS_CLIENTATTR_NOSETFC;
		QUERY_ERROR(qctx, DNS_R_SERVFAIL);
		return ns_query_done(qctx);
	}

	switch (result) {
	case ISC_R_SUCCESS:
		return query_prepresponse(qctx);

	case DNS_R_GLUE:
	case DNS_R_ZONECUT:
		INSIST(qctx->is_zone);
		qctx->authoritative = false;
		return query_prepresponse(qctx);

	case ISC_R_NOTFOUND:
		return query_notfound(qctx);

	case DNS_R_DELEGATION:
		return query_delegation(qctx);

	case DNS_R_EMPTYNAME:
	case DNS_R_NXRRSET:
		return query_nodata(qctx, result);

	case DNS_R_EMPTYWILD:
	case DNS_R_NXDOMAIN:
		return query_nxdomain(qctx, result);

	case DNS_R_COVERINGNSEC:
		return query_coveringnsec(qctx);

	case DNS_R_NCACHENXDOMAIN:
		result = query_redirect(qctx, result);
		if (result != ISC_R_COMPLETE) {
			return result;
		}
		return query_ncache(qctx, DNS_R_NCACHENXDOMAIN);

	case DNS_R_NCACHENXRRSET:
		return query_ncache(qctx, DNS_R_NCACHENXRRSET);

	case DNS_R_CNAME:
		return query_cname(qctx);

	case DNS_R_DNAME:
		return query_dname(qctx);

	default:
		/*
		 * Something has gone wrong.
		 */
		snprintf(errmsg, sizeof(errmsg) - 1,
			 "query_gotanswer: unexpected error: %s",
			 isc_result_totext(result));
		CCTRACE(ISC_LOG_ERROR, errmsg);
		if (query_usestale(qctx, result)) {
			/*
			 * If serve-stale is enabled, query_usestale() already
			 * set up 'qctx' for looking up a stale response.
			 */
			return query_lookup(qctx);
		}

		/*
		 * Regardless of the triggering result, we definitely
		 * want to return SERVFAIL from here.
		 */
		qctx->client->inner.rcode_override = dns_rcode_servfail;

		QUERY_ERROR(qctx, result);
		return ns_query_done(qctx);
	}

cleanup:
	return result;
}

static void
query_addnoqnameproof(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;
	isc_buffer_t *dbuf, b;
	dns_name_t *fname = NULL;
	dns_rdataset_t *neg = NULL, *negsig = NULL;
	isc_result_t result;

	CTRACE(ISC_LOG_DEBUG(3), "query_addnoqnameproof");

	if (qctx->noqname == NULL) {
		return;
	}

	dbuf = ns_client_getnamebuf(client);
	fname = ns_client_newname(client, dbuf, &b);
	neg = ns_client_newrdataset(client);
	negsig = ns_client_newrdataset(client);

	result = dns_rdataset_getnoqname(qctx->noqname, fname, neg, negsig);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);

	query_addrrset(qctx, &fname, &neg, &negsig, dbuf,
		       DNS_SECTION_AUTHORITY);

	if (!qctx->noqname->attributes.closest) {
		goto cleanup;
	}

	if (fname == NULL) {
		dbuf = ns_client_getnamebuf(client);
		fname = ns_client_newname(client, dbuf, &b);
	}

	if (neg == NULL) {
		neg = ns_client_newrdataset(client);
	} else if (dns_rdataset_isassociated(neg)) {
		dns_rdataset_disassociate(neg);
	}

	if (negsig == NULL) {
		negsig = ns_client_newrdataset(client);
	} else if (dns_rdataset_isassociated(negsig)) {
		dns_rdataset_disassociate(negsig);
	}

	result = dns_rdataset_getclosest(qctx->noqname, fname, neg, negsig);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);

	query_addrrset(qctx, &fname, &neg, &negsig, dbuf,
		       DNS_SECTION_AUTHORITY);

cleanup:
	if (neg != NULL) {
		ns_client_putrdataset(client, &neg);
	}
	if (negsig != NULL) {
		ns_client_putrdataset(client, &negsig);
	}
	if (fname != NULL) {
		ns_client_releasename(client, &fname);
	}
}

/*%
 * Build the response for a query for type ANY.
 */
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_R_UNSET;
	dns_rdatatype_t onetype = dns_rdatatype_none; /* type to use for
							 minimal-any */
	isc_buffer_t b;

	CCTRACE(ISC_LOG_DEBUG(3), "query_respond_any");

	CALL_HOOK(NS_QUERY_RESPOND_ANY_BEGIN, qctx);

	result = dns_db_allrdatasets(qctx->db, qctx->node, qctx->version, 0, 0,
				     &rdsiter);
	if (result != ISC_R_SUCCESS) {
		CCTRACE(ISC_LOG_ERROR, "query_respond_any: allrdatasets "
				       "failed");
		QUERY_ERROR(qctx, result);
		return ns_query_done(qctx);
	}

	/*
	 * Calling query_addrrset() with a non-NULL dbuf is going
	 * to either keep or release the name.  We don't want it to
	 * release fname, since we may have to call query_addrrset()
	 * more than once.  That means we have to call ns_client_keepname()
	 * now, and pass a NULL dbuf to query_addrrset().
	 *
	 * If we do a query_addrrset() below, we must set qctx->fname to
	 * NULL before leaving this block, otherwise we might try to
	 * cleanup qctx->fname even though we're using it!
	 */
	ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
	qctx->tname = qctx->fname;

	DNS_RDATASETITER_FOREACH(rdsiter) {
		dns_rdatasetiter_current(rdsiter, qctx->rdataset);

		/*
		 * We found an NS RRset; no need to add one later.
		 */
		if (qctx->qtype == dns_rdatatype_any &&
		    qctx->rdataset->type == dns_rdatatype_ns)
		{
			qctx->answer_has_ns = true;
		}

		/*
		 * Note: if we're in this function, then qctx->type
		 * is guaranteed to be ANY, but qctx->qtype (i.e. the
		 * original type requested) might have been RRSIG or
		 * SIG; we need to check for that.
		 */
		if (qctx->is_zone && qctx->qtype == dns_rdatatype_any &&
		    !dns_db_issecure(qctx->db) &&
		    dns_rdatatype_isdnssec(qctx->rdataset->type))
		{
			/*
			 * The zone may be transitioning from insecure
			 * to secure. Hide DNSSEC records from ANY queries.
			 */
			dns_rdataset_disassociate(qctx->rdataset);
			hidden = true;
		} else if (qctx->view->minimal_any && !TCP(qctx->client) &&
			   !WANTDNSSEC(qctx->client) &&
			   qctx->qtype == dns_rdatatype_any &&
			   (dns_rdatatype_issig(qctx->rdataset->type)))
		{
			CCTRACE(ISC_LOG_DEBUG(5), "query_respond_any: "
						  "minimal-any skip signature");
			dns_rdataset_disassociate(qctx->rdataset);
		} else if (qctx->view->minimal_any && !TCP(qctx->client) &&
			   onetype != dns_rdatatype_none &&
			   qctx->rdataset->type != onetype &&
			   qctx->rdataset->covers != onetype)
		{
			CCTRACE(ISC_LOG_DEBUG(5), "query_respond_any: "
						  "minimal-any skip rdataset");
			dns_rdataset_disassociate(qctx->rdataset);
		} else if ((qctx->qtype == dns_rdatatype_any ||
			    qctx->rdataset->type == qctx->qtype) &&
			   qctx->rdataset->type != 0)
		{
			if (NOQNAME(qctx->rdataset) && WANTDNSSEC(qctx->client))
			{
				qctx->noqname = qctx->rdataset;
			} else {
				qctx->noqname = NULL;
			}

			qctx->rpz_st = qctx->client->query.rpz_st;
			if (qctx->rpz_st != NULL &&
			    qctx->rpz_st->m.policy != DNS_RPZ_POLICY_MISS &&
			    qctx->rpz_st->m.policy != DNS_RPZ_POLICY_PASSTHRU)
			{
				qctx->rdataset->ttl =
					ISC_MIN(qctx->rdataset->ttl,
						qctx->rpz_st->m.ttl);
			}

			if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
				dns_name_t *name;
				name = (qctx->fname != NULL) ? qctx->fname
							     : qctx->tname;
				query_prefetch(qctx->client, name,
					       qctx->rdataset);
			}

			/*
			 * Remember the first RRtype we find so we
			 * can skip others with minimal-any.
			 */
			if (dns_rdatatype_issig(qctx->rdataset->type)) {
				onetype = qctx->rdataset->covers;
			} else {
				onetype = qctx->rdataset->type;
			}

			query_addrrset(qctx,
				       (qctx->fname != NULL) ? &qctx->fname
							     : &qctx->tname,
				       &qctx->rdataset, NULL, NULL,
				       DNS_SECTION_ANSWER);

			query_addnoqnameproof(qctx);

			found = true;
			INSIST(qctx->tname != NULL);

			/*
			 * rdataset is non-NULL only in certain
			 * pathological cases involving DNAMEs.
			 */
			if (qctx->rdataset != NULL) {
				ns_client_putrdataset(qctx->client,
						      &qctx->rdataset);
			}

			qctx->rdataset = ns_client_newrdataset(qctx->client);
		} else {
			/*
			 * We're not interested in this rdataset.
			 */
			dns_rdataset_disassociate(qctx->rdataset);
		}
	}

	dns_rdatasetiter_destroy(&rdsiter);

	if (found) {
		/*
		 * Call hook if any answers were found.
		 * Do this before releasing qctx->fname, in case
		 * the hook function needs it.
		 */
		CALL_HOOK(NS_QUERY_RESPOND_ANY_FOUND, qctx);
	}

	if (qctx->fname != NULL) {
		dns_message_puttempname(qctx->client->message, &qctx->fname);
	}

	if (found) {
		/*
		 * At least one matching rdataset was found
		 */
		query_addauth(qctx);
	} else if (dns_rdatatype_issig(qctx->qtype)) {
		/*
		 * No matching rdatasets were found, but we got
		 * here on a search for RRSIG/SIG, so that's okay.
		 */
		if (!qctx->is_zone) {
			qctx->authoritative = false;
			qctx->client->inner.attributes &= ~NS_CLIENTATTR_RA;
			query_addauth(qctx);
			return ns_query_done(qctx);
		}

		if (qctx->qtype == dns_rdatatype_rrsig &&
		    dns_db_issecure(qctx->db))
		{
			char namebuf[DNS_NAME_FORMATSIZE];
			dns_name_format(qctx->client->query.qname, namebuf,
					sizeof(namebuf));
			ns_client_log(qctx->client, DNS_LOGCATEGORY_DNSSEC,
				      NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
				      "missing signature for %s", namebuf);
		}

		qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b);
		return query_sign_nodata(qctx);
	} else if (!hidden) {
		/*
		 * No matching rdatasets were found and nothing was
		 * deliberately hidden: something must have gone wrong.
		 */
		QUERY_ERROR(qctx, DNS_R_SERVFAIL);
	}

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*
 * Set the expire time, if requested, when answering from a secondary,
 * mirror, or primary zone.
 */
static void
query_getexpire(query_ctx_t *qctx) {
	dns_zone_t *raw = NULL, *mayberaw;

	CCTRACE(ISC_LOG_DEBUG(3), "query_getexpire");

	if (qctx->zone == NULL || !qctx->is_zone ||
	    qctx->qtype != dns_rdatatype_soa ||
	    qctx->client->query.restarts != 0 ||
	    (qctx->client->inner.attributes & NS_CLIENTATTR_WANTEXPIRE) == 0)
	{
		return;
	}

	dns_zone_getraw(qctx->zone, &raw);
	mayberaw = (raw != NULL) ? raw : qctx->zone;

	if (dns_zone_gettype(mayberaw) == dns_zone_secondary ||
	    dns_zone_gettype(mayberaw) == dns_zone_mirror)
	{
		isc_time_t expiretime;
		uint32_t secs;
		dns_zone_getexpiretime(qctx->zone, &expiretime);
		secs = isc_time_seconds(&expiretime);
		if (secs >= qctx->client->inner.now &&
		    qctx->result == ISC_R_SUCCESS)
		{
			qctx->client->inner.attributes |=
				NS_CLIENTATTR_HAVEEXPIRE;
			qctx->client->inner.expire = secs -
						     qctx->client->inner.now;
		}
	} else if (dns_zone_gettype(mayberaw) == dns_zone_primary) {
		isc_result_t result;
		dns_rdata_t rdata = DNS_RDATA_INIT;
		dns_rdata_soa_t soa;

		result = dns_rdataset_first(qctx->rdataset);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);

		dns_rdataset_current(qctx->rdataset, &rdata);
		result = dns_rdata_tostruct(&rdata, &soa, NULL);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);

		qctx->client->inner.expire = soa.expire;
		qctx->client->inner.attributes |= NS_CLIENTATTR_HAVEEXPIRE;
	}

	if (raw != NULL) {
		dns_zone_detach(&raw);
	}
}

/*
 * Set the zone version, if requested, when answering from a secondary,
 * mirror, or primary zone.
 */
static void
query_getzoneversion(query_ctx_t *qctx) {
	CCTRACE(ISC_LOG_DEBUG(3), __func__);

	if (qctx->zone == NULL || !qctx->is_zone ||
	    qctx->client->query.restarts != 0 ||
	    (qctx->client->inner.attributes & NS_CLIENTATTR_WANTZONEVERSION) ==
		    0 ||
	    (qctx->client->inner.attributes & NS_CLIENTATTR_HAVEZONEVERSION) !=
		    0)
	{
		return;
	}

	switch (dns_zone_gettype(qctx->zone)) {
	case dns_zone_mirror:
	case dns_zone_primary:
	case dns_zone_secondary: {
		isc_buffer_t b;
		unsigned char buf[128];
		isc_buffer_init(&b, buf, sizeof(buf));
		isc_result_t result = dns_zone_getzoneversion(qctx->zone, &b);
		if (result == ISC_R_SUCCESS) {
			size_t len = isc_buffer_usedlength(&b);
			/*
			 * Sanity check zone version from database
			 * implementations.  Minimum length and type 0
			 * contraints.
			 */
			if (len < 2 || (buf[1] == 0 && len != 6)) {
				return;
			}
			qctx->client->inner.attributes |=
				NS_CLIENTATTR_HAVEZONEVERSION;
			INSIST(qctx->client->inner.zoneversion == NULL);
			qctx->client->inner.zoneversion =
				isc_mem_get(qctx->client->manager->mctx, len);
			qctx->client->inner.zoneversionlength = len;
			memmove(qctx->client->inner.zoneversion, buf, len);
		}
		break;
	}
	default:
		break;
	}
}

/*%
 * Fill the ANSWER section of a positive response.
 */
static isc_result_t
query_addanswer(query_ctx_t *qctx) {
	dns_rdataset_t **sigrdatasetp = NULL;
	isc_result_t result = ISC_R_UNSET;

	CCTRACE(ISC_LOG_DEBUG(3), "query_addanswer");

	CALL_HOOK(NS_QUERY_ADDANSWER_BEGIN, qctx);

	if (qctx->dns64) {
		result = query_dns64(qctx);
		qctx->noqname = NULL;
		dns_rdataset_disassociate(qctx->rdataset);
		dns_message_puttemprdataset(qctx->client->message,
					    &qctx->rdataset);
		if (result == ISC_R_NOMORE) {
#ifndef dns64_bis_return_excluded_addresses
			if (qctx->dns64_exclude) {
				if (!qctx->is_zone) {
					return ns_query_done(qctx);
				}
				/*
				 * Add a fake SOA record.
				 */
				(void)query_addsoa(qctx, 600,
						   DNS_SECTION_AUTHORITY);
				return ns_query_done(qctx);
			}
#endif /* ifndef dns64_bis_return_excluded_addresses */
			if (qctx->is_zone) {
				return query_nodata(qctx, DNS_R_NXDOMAIN);
			} else {
				return query_ncache(qctx, DNS_R_NXDOMAIN);
			}
		} else if (result != ISC_R_SUCCESS) {
			qctx->result = result;
			return ns_query_done(qctx);
		}
	} else if (qctx->client->query.dns64_aaaaok != NULL) {
		query_filter64(qctx);
		ns_client_putrdataset(qctx->client, &qctx->rdataset);
	} else {
		if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
			query_prefetch(qctx->client, qctx->fname,
				       qctx->rdataset);
		}
		if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) {
			sigrdatasetp = &qctx->sigrdataset;
		}
		query_addrrset(qctx, &qctx->fname, &qctx->rdataset,
			       sigrdatasetp, qctx->dbuf, DNS_SECTION_ANSWER);
	}

	return ISC_R_COMPLETE;

cleanup:
	return result;
}

/*%
 * Build a response for a "normal" query, for a type other than ANY,
 * for which we have an answer (either positive or negative).
 */
static isc_result_t
query_respond(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;

	CCTRACE(ISC_LOG_DEBUG(3), "query_respond");

	/*
	 * Check to see if the AAAA RRset has non-excluded addresses
	 * in it.  If not look for a A RRset.
	 */
	INSIST(qctx->client->query.dns64_aaaaok == NULL);

	if (qctx->qtype == dns_rdatatype_aaaa && !qctx->dns64_exclude &&
	    !ISC_LIST_EMPTY(qctx->view->dns64) &&
	    qctx->client->message->rdclass == dns_rdataclass_in &&
	    !dns64_aaaaok(qctx->client, qctx->rdataset, qctx->sigrdataset))
	{
		/*
		 * Look to see if there are A records for this name.
		 */
		qctx->client->query.dns64_ttl = qctx->rdataset->ttl;
		SAVE(qctx->client->query.dns64_aaaa, qctx->rdataset);
		SAVE(qctx->client->query.dns64_sigaaaa, qctx->sigrdataset);
		ns_client_releasename(qctx->client, &qctx->fname);
		dns_db_detachnode(&qctx->node);
		qctx->type = qctx->qtype = dns_rdatatype_a;
		qctx->dns64_exclude = qctx->dns64 = true;

		return query_lookup(qctx);
	}

	/*
	 * XXX: This hook is meant to be at the top of this function,
	 * but is postponed until after DNS64 in order to avoid an
	 * assertion if the hook causes recursion. (When DNS64 also
	 * becomes a plugin, it will be necessary to find some
	 * other way to prevent that assertion, since the order in
	 * which plugins are configured can't be enforced.)
	 */
	CALL_HOOK(NS_QUERY_RESPOND_BEGIN, qctx);

	if (NOQNAME(qctx->rdataset) && WANTDNSSEC(qctx->client)) {
		qctx->noqname = qctx->rdataset;
	} else {
		qctx->noqname = NULL;
	}

	/*
	 * Special case NS handling
	 */
	if (qctx->is_zone && qctx->qtype == dns_rdatatype_ns) {
		/*
		 * We've already got an NS, no need to add one in
		 * the authority section
		 */
		if (dns_name_equal(qctx->client->query.qname,
				   dns_db_origin(qctx->db)))
		{
			qctx->answer_has_ns = true;
		}

		/*
		 * Always add glue for root priming queries, regardless
		 * of "minimal-responses" setting.
		 */
		if (dns_name_equal(qctx->client->query.qname, dns_rootname)) {
			qctx->client->query.attributes &=
				~NS_QUERYATTR_NOADDITIONAL;
			dns_db_attach(qctx->db, &qctx->client->query.gluedb);
		}
	}

	/*
	 * Set expire time
	 */
	query_getexpire(qctx);

	/*
	 * Set the zone version
	 */
	query_getzoneversion(qctx);

	result = query_addanswer(qctx);
	if (result != ISC_R_COMPLETE) {
		return result;
	}

	query_addnoqnameproof(qctx);

	/*
	 * 'qctx->rdataset' will only be non-NULL here if the ANSWER section of
	 * the message to be sent to the client already contains an RRset with
	 * the same owner name and the same type as 'qctx->rdataset'.  This
	 * should never happen, with one exception: when chasing DNAME records,
	 * one of the DNAME records placed in the ANSWER section may turn out
	 * to be the final answer to the client's query, but we have no way of
	 * knowing that until now.  In such a case, 'qctx->rdataset' will be
	 * freed later, so we do not need to free it here.
	 */
	INSIST(qctx->rdataset == NULL || qctx->qtype == dns_rdatatype_dname);

	query_addauth(qctx);

	return ns_query_done(qctx);

cleanup:
	return result;
}

static isc_result_t
query_dns64(query_ctx_t *qctx) {
	isc_result_t result;
	ns_client_t *client = qctx->client;
	dns_name_t *name = NULL, *mname = NULL;
	dns_rdataset_t *mrdataset = NULL;
	dns_rdataset_t *dns64_rdataset = NULL;
	dns_view_t *view = client->inner.view;
	const dns_section_t section = DNS_SECTION_ANSWER;
	unsigned int flags = 0;

	/*%
	 * To the current response for 'qctx->client', add the answer RRset
	 * '*rdatasetp' and an optional signature set '*sigrdatasetp', with
	 * owner name '*namep', to the answer section, unless they are
	 * already there.  Also add any pertinent additional data.
	 *
	 * If 'qctx->dbuf' is not NULL, then 'qctx->fname' is the name
	 * whose data is stored 'qctx->dbuf'.  In this case,
	 * query_addrrset() guarantees that when it returns the name
	 * will either have been kept or released.
	 */
	CTRACE(ISC_LOG_DEBUG(3), "query_dns64");

	qctx->qtype = qctx->type = dns_rdatatype_aaaa;

	name = qctx->fname;
	result = dns_message_findname(
		client->message, section, name, dns_rdatatype_aaaa,
		qctx->rdataset->covers, &mname, &mrdataset);
	if (result == ISC_R_SUCCESS) {
		/*
		 * We've already got an RRset of the given name and type.
		 * There's nothing else to do;
		 */
		CTRACE(ISC_LOG_DEBUG(3), "query_dns64: dns_message_findname "
					 "succeeded: done");
		if (qctx->dbuf != NULL) {
			ns_client_releasename(client, &qctx->fname);
		}
		return ISC_R_SUCCESS;
	} else if (result == DNS_R_NXDOMAIN) {
		/*
		 * The name doesn't exist.
		 */
		if (qctx->dbuf != NULL) {
			ns_client_keepname(client, name, qctx->dbuf);
		}
		dns_message_addname(client->message, name, section);
		qctx->fname = NULL;
		mname = name;
	} else {
		RUNTIME_CHECK(result == DNS_R_NXRRSET);
		if (qctx->dbuf != NULL) {
			ns_client_releasename(client, &qctx->fname);
		}
	}

	if (qctx->rdataset->trust != dns_trust_secure) {
		client->query.attributes &= ~NS_QUERYATTR_SECURE;
	}

	/*
	 * We use the signatures from the A lookup to set DNS_DNS64_DNSSEC
	 * as this provides a easy way to see if the answer was signed.
	 */
	if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL &&
	    dns_rdataset_isassociated(qctx->sigrdataset))
	{
		flags |= DNS_DNS64_DNSSEC;
	}

	if (RECURSIONOK(client)) {
		flags |= DNS_DNS64_RECURSIVE;
	}

	result = dns_dns64_apply(client->manager->mctx, view->dns64,
				 view->dns64cnt, client->message,
				 client->manager->aclenv,
				 &client->inner.peeraddr, client->inner.signer,
				 flags, qctx->rdataset, &dns64_rdataset);
	if (result != ISC_R_SUCCESS) {
		goto cleanup;
	}

	dns_rdataset_setownercase(dns64_rdataset, mname);
	client->query.attributes |= NS_QUERYATTR_NOADDITIONAL;

	if (client->query.dns64_ttl != UINT32_MAX) {
		dns64_rdataset->ttl = ISC_MIN(qctx->rdataset->ttl,
					      client->query.dns64_ttl);
	} else {
		dns64_rdataset->ttl = ISC_MIN(qctx->rdataset->ttl, 600);
	}

	query_addtoname(mname, dns64_rdataset);
	query_setorder(qctx, mname, dns64_rdataset);

	inc_stats(client, ns_statscounter_dns64);
	result = ISC_R_SUCCESS;

cleanup:

	CTRACE(ISC_LOG_DEBUG(3), "query_dns64: done");
	return result;
}

static void
query_filter64(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;
	dns_name_t *name = NULL, *mname = NULL;
	dns_rdatalist_t *myrdatalist = NULL;
	dns_rdataset_t *myrdataset = NULL;
	isc_buffer_t *buffer = NULL;
	isc_result_t result;
	unsigned int i;
	const dns_section_t section = DNS_SECTION_ANSWER;

	CTRACE(ISC_LOG_DEBUG(3), "query_filter64");

	INSIST(client->query.dns64_aaaaok != NULL);
	INSIST(client->query.dns64_aaaaoklen ==
	       dns_rdataset_count(qctx->rdataset));

	name = qctx->fname;
	result = dns_message_findname(
		client->message, section, name, dns_rdatatype_aaaa,
		qctx->rdataset->covers, &mname, &myrdataset);
	if (result == ISC_R_SUCCESS) {
		/*
		 * We've already got an RRset of the given name and type.
		 * There's nothing else to do;
		 */
		CTRACE(ISC_LOG_DEBUG(3), "query_filter64: dns_message_findname "
					 "succeeded: done");
		if (qctx->dbuf != NULL) {
			ns_client_releasename(client, &qctx->fname);
		}
		return;
	} else if (result == DNS_R_NXDOMAIN) {
		mname = name;
		qctx->fname = NULL;
	} else {
		RUNTIME_CHECK(result == DNS_R_NXRRSET);
		if (qctx->dbuf != NULL) {
			ns_client_releasename(client, &qctx->fname);
		}
		qctx->dbuf = NULL;
	}

	if (qctx->rdataset->trust != dns_trust_secure) {
		client->query.attributes &= ~NS_QUERYATTR_SECURE;
	}

	isc_buffer_allocate(client->manager->mctx, &buffer,
			    16 * dns_rdataset_count(qctx->rdataset));
	dns_message_gettemprdataset(client->message, &myrdataset);
	dns_message_gettemprdatalist(client->message, &myrdatalist);

	myrdatalist->rdclass = dns_rdataclass_in;
	myrdatalist->type = dns_rdatatype_aaaa;
	myrdatalist->ttl = qctx->rdataset->ttl;

	i = 0;
	DNS_RDATASET_FOREACH(qctx->rdataset) {
		dns_rdata_t rdata = DNS_RDATA_INIT;
		dns_rdata_t *myrdata = NULL;
		isc_region_t r;

		if (!client->query.dns64_aaaaok[i++]) {
			continue;
		}
		dns_rdataset_current(qctx->rdataset, &rdata);
		INSIST(rdata.length == 16);
		isc_buffer_putmem(buffer, rdata.data, rdata.length);
		isc_buffer_remainingregion(buffer, &r);
		isc_buffer_forward(buffer, rdata.length);
		dns_message_gettemprdata(client->message, &myrdata);
		dns_rdata_fromregion(myrdata, dns_rdataclass_in,
				     dns_rdatatype_aaaa, &r);
		ISC_LIST_APPEND(myrdatalist->rdata, myrdata, link);
	}

	dns_rdatalist_tordataset(myrdatalist, myrdataset);
	dns_rdataset_setownercase(myrdataset, name);
	client->query.attributes |= NS_QUERYATTR_NOADDITIONAL;
	if (mname == name) {
		if (qctx->dbuf != NULL) {
			ns_client_keepname(client, name, qctx->dbuf);
		}
		dns_message_addname(client->message, name, section);
		qctx->dbuf = NULL;
	}
	myrdataset->trust = qctx->rdataset->trust;

	query_addtoname(mname, myrdataset);
	query_setorder(qctx, mname, myrdataset);

	dns_message_takebuffer(client->message, &buffer);
	if (buffer != NULL) {
		isc_buffer_free(&buffer);
	}

	if (qctx->dbuf != NULL) {
		ns_client_releasename(client, &name);
	}

	CTRACE(ISC_LOG_DEBUG(3), "query_filter64: done");
}

/*%
 * Handle the case of a name not being found in a database lookup.
 * Called from query_gotanswer(). Passes off processing to
 * query_delegation() for a root referral if appropriate.
 */
static isc_result_t
query_notfound(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;

	CCTRACE(ISC_LOG_DEBUG(3), "query_notfound");

	CALL_HOOK(NS_QUERY_NOTFOUND_BEGIN, qctx);

	INSIST(!qctx->is_zone);

	if (qctx->db != NULL) {
		dns_db_detach(&qctx->db);
	}

	/*
	 * If the cache doesn't even have the root NS,
	 * try to get that from the hints DB.
	 */
	if (qctx->view->hints != NULL) {
		dns_clientinfomethods_t cm;
		dns_clientinfo_t ci;

		dns_clientinfomethods_init(&cm, ns_client_sourceip);
		dns_clientinfo_init(&ci, qctx->client, NULL);

		dns_db_attach(qctx->view->hints, &qctx->db);
		result = dns_db_findext(
			qctx->db, dns_rootname, NULL, dns_rdatatype_ns, 0,
			qctx->client->inner.now, &qctx->node, qctx->fname, &cm,
			&ci, qctx->rdataset, qctx->sigrdataset);
	} else {
		/* We have no hints. */
		result = ISC_R_FAILURE;
	}
	if (result != ISC_R_SUCCESS) {
		/*
		 * Nonsensical root hints may require cleanup.
		 */
		qctx_clean(qctx);

		/*
		 * We don't have any root server hints, but
		 * we may have working forwarders, so try to
		 * recurse anyway.
		 */
		if (RECURSIONOK(qctx->client)) {
			INSIST(!REDIRECT(qctx->client));
			result = ns_query_recurse(qctx->client, qctx->qtype,
						  qctx->client->query.qname,
						  NULL, NULL, qctx->resuming);
			if (result == ISC_R_SUCCESS) {
				CALL_HOOK(NS_QUERY_NOTFOUND_RECURSE, qctx);
				qctx->client->query.attributes |=
					NS_QUERYATTR_RECURSING;

				if (qctx->dns64) {
					qctx->client->query.attributes |=
						NS_QUERYATTR_DNS64;
				}
				if (qctx->dns64_exclude) {
					qctx->client->query.attributes |=
						NS_QUERYATTR_DNS64EXCLUDE;
				}
			} else if (query_usestale(qctx, result)) {
				/*
				 * If serve-stale is enabled, query_usestale()
				 * already set up 'qctx' for looking up a
				 * stale response.
				 */
				return query_lookup(qctx);
			} else {
				QUERY_ERROR(qctx, result);
			}
			return ns_query_done(qctx);
		} else {
			/* Unable to give root server referral. */
			CCTRACE(ISC_LOG_ERROR, "unable to give root server "
					       "referral");
			QUERY_ERROR(qctx, result);
			return ns_query_done(qctx);
		}
	}

	return query_delegation(qctx);

cleanup:
	return result;
}

/*%
 * We have a delegation but recursion is not allowed, so return the delegation
 * to the client.
 */
static isc_result_t
query_prepare_delegation_response(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;
	dns_rdataset_t **sigrdatasetp = NULL;
	bool detach = false;

	CALL_HOOK(NS_QUERY_PREP_DELEGATION_BEGIN, qctx);

	/*
	 * qctx->fname could be released in query_addrrset(), so save a copy of
	 * it here in case we need it.
	 */
	dns_fixedname_init(&qctx->dsname);
	dns_name_copy(qctx->fname, dns_fixedname_name(&qctx->dsname));

	/*
	 * This is the best answer.
	 */
	qctx->client->query.isreferral = true;

	if (!dns_db_iscache(qctx->db) && qctx->client->query.gluedb == NULL) {
		dns_db_attach(qctx->db, &qctx->client->query.gluedb);
		detach = true;
	}

	/*
	 * We must ensure NOADDITIONAL is off, because the generation of
	 * additional data is required in delegations.
	 */
	qctx->client->query.attributes &= ~NS_QUERYATTR_NOADDITIONAL;
	if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) {
		sigrdatasetp = &qctx->sigrdataset;
	}
	query_addrrset(qctx, &qctx->fname, &qctx->rdataset, sigrdatasetp,
		       qctx->dbuf, DNS_SECTION_AUTHORITY);
	if (detach) {
		dns_db_detach(&qctx->client->query.gluedb);
	}

	/*
	 * Add DS/NSEC(3) record(s) if needed.
	 */
	query_addds(qctx);

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*%
 * Handle a delegation response from an authoritative lookup. This
 * may trigger additional lookups, e.g. from the cache database to
 * see if we have a better answer; if that is not allowed, return the
 * delegation to the client and call ns_query_done().
 */
static isc_result_t
query_zone_delegation(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;

	CALL_HOOK(NS_QUERY_ZONE_DELEGATION_BEGIN, qctx);

	/*
	 * If the query type is DS, look to see if we are
	 * authoritative for the child zone
	 */
	if (!RECURSIONOK(qctx->client) &&
	    (qctx->options.noexact && qctx->qtype == dns_rdatatype_ds))
	{
		dns_db_t *tdb = NULL;
		dns_zone_t *tzone = NULL;
		dns_dbversion_t *tversion = NULL;
		dns_getdb_options_t options = { .partial = true };
		result = query_getzonedb(qctx->client,
					 qctx->client->query.qname, qctx->qtype,
					 options, &tzone, &tdb, &tversion);
		if (result != ISC_R_SUCCESS) {
			if (tdb != NULL) {
				dns_db_detach(&tdb);
			}
			if (tzone != NULL) {
				dns_zone_detach(&tzone);
			}
		} else {
			qctx->options.noexact = false;
			ns_client_putrdataset(qctx->client, &qctx->rdataset);
			if (qctx->sigrdataset != NULL) {
				ns_client_putrdataset(qctx->client,
						      &qctx->sigrdataset);
			}
			if (qctx->fname != NULL) {
				ns_client_releasename(qctx->client,
						      &qctx->fname);
			}
			if (qctx->node != NULL) {
				dns_db_detachnode(&qctx->node);
			}
			if (qctx->db != NULL) {
				dns_db_detach(&qctx->db);
			}
			if (qctx->zone != NULL) {
				dns_zone_detach(&qctx->zone);
			}
			qctx->version = NULL;
			RESTORE(qctx->version, tversion);
			RESTORE(qctx->db, tdb);
			RESTORE(qctx->zone, tzone);
			qctx->authoritative = true;

			return query_lookup(qctx);
		}
	}

	if (USECACHE(qctx->client) &&
	    (RECURSIONOK(qctx->client) ||
	     (qctx->zone != NULL &&
	      dns_zone_gettype(qctx->zone) == dns_zone_mirror)))
	{
		/*
		 * We might have a better answer or delegation in the
		 * cache.  We'll remember the current values of fname,
		 * rdataset, and sigrdataset.  We'll then go looking for
		 * QNAME in the cache.  If we find something better, we'll
		 * use it instead. If not, then query_lookup() calls
		 * query_notfound() which calls query_delegation(), and
		 * we'll restore these values there.
		 */
		ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
		SAVE(qctx->zdb, qctx->db);
		SAVE(qctx->znode, qctx->node);
		SAVE(qctx->zfname, qctx->fname);
		SAVE(qctx->zversion, qctx->version);
		SAVE(qctx->zrdataset, qctx->rdataset);
		SAVE(qctx->zsigrdataset, qctx->sigrdataset);
		dns_db_attach(qctx->view->cachedb, &qctx->db);
		qctx->is_zone = false;

		/*
		 * Since 'qctx->is_zone' is now false, we should reconsider
		 * setting the 'stalefirst' option, which is usually set in
		 * the beginning in ns__query_start().
		 */
		qctx->options.stalefirst =
			(qctx->view->staleanswerclienttimeout == 0 &&
			 dns_view_staleanswerenabled(qctx->view));

		result = query_lookup(qctx);

		/*
		 * After fetch completes, this option is not expected to be set.
		 */
		qctx->options.stalefirst = false;

		return result;
	}

	return query_prepare_delegation_response(qctx);

cleanup:
	return result;
}

/*%
 * Handle delegation responses, including root referrals.
 *
 * If the delegation was returned from authoritative data,
 * call query_zone_delgation().  Otherwise, we can start
 * recursion if allowed; or else return the delegation to the
 * client and call ns_query_done().
 */
static isc_result_t
query_delegation(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;

	CCTRACE(ISC_LOG_DEBUG(3), "query_delegation");

	CALL_HOOK(NS_QUERY_DELEGATION_BEGIN, qctx);

	qctx->authoritative = false;

	if (qctx->is_zone) {
		return query_zone_delegation(qctx);
	}

	if (qctx->zfname != NULL &&
	    (!dns_name_issubdomain(qctx->fname, qctx->zfname) ||
	     (qctx->is_staticstub_zone &&
	      dns_name_equal(qctx->fname, qctx->zfname))))
	{
		/*
		 * In the following cases use "authoritative"
		 * data instead of the cache delegation:
		 * 1. We've already got a delegation from
		 *    authoritative data, and it is better
		 *    than what we found in the cache.
		 *    (See the comment above.)
		 * 2. The query name matches the origin name
		 *    of a static-stub zone.  This needs to be
		 *    considered for the case where the NS of
		 *    the static-stub zone and the cached NS
		 *    are different.  We still need to contact
		 *    the nameservers configured in the
		 *    static-stub zone.
		 */
		ns_client_releasename(qctx->client, &qctx->fname);

		/*
		 * We've already done ns_client_keepname() on
		 * qctx->zfname, so we must set dbuf to NULL to
		 * prevent query_addrrset() from trying to
		 * call ns_client_keepname() again.
		 */
		qctx->dbuf = NULL;
		ns_client_putrdataset(qctx->client, &qctx->rdataset);
		if (qctx->sigrdataset != NULL) {
			ns_client_putrdataset(qctx->client, &qctx->sigrdataset);
		}
		qctx->version = NULL;

		dns_db_detachnode(&qctx->node);
		dns_db_detach(&qctx->db);
		RESTORE(qctx->db, qctx->zdb);
		RESTORE(qctx->node, qctx->znode);
		RESTORE(qctx->fname, qctx->zfname);
		RESTORE(qctx->version, qctx->zversion);
		RESTORE(qctx->rdataset, qctx->zrdataset);
		RESTORE(qctx->sigrdataset, qctx->zsigrdataset);
	}

	result = query_delegation_recurse(qctx);
	if (result != ISC_R_COMPLETE) {
		return result;
	}

	return query_prepare_delegation_response(qctx);

cleanup:
	return result;
}

/*%
 * Handle recursive queries that are triggered as part of the
 * delegation process.
 */
static isc_result_t
query_delegation_recurse(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;
	dns_name_t *qname = qctx->client->query.qname;

	CCTRACE(ISC_LOG_DEBUG(3), "query_delegation_recurse");

	if (!RECURSIONOK(qctx->client)) {
		return ISC_R_COMPLETE;
	}

	CALL_HOOK(NS_QUERY_DELEGATION_RECURSE_BEGIN, qctx);

	/*
	 * We have a delegation and recursion is allowed,
	 * so we call ns_query_recurse() to follow it.
	 * This phase of the query processing is done;
	 * we'll resume via fetch_callback() and
	 * query_resume() when the recursion is complete.
	 */

	INSIST(!REDIRECT(qctx->client));

	if (dns_rdatatype_atparent(qctx->type)) {
		/*
		 * Parent is authoritative for this RDATA type (i.e. DS).
		 */
		result = ns_query_recurse(qctx->client, qctx->qtype, qname,
					  NULL, NULL, qctx->resuming);
	} else if (qctx->dns64) {
		/*
		 * Look up an A record so we can synthesize DNS64.
		 */
		result = ns_query_recurse(qctx->client, dns_rdatatype_a, qname,
					  NULL, NULL, qctx->resuming);
	} else {
		/*
		 * Any other recursion.
		 */
		result = ns_query_recurse(qctx->client, qctx->qtype, qname,
					  qctx->fname, qctx->rdataset,
					  qctx->resuming);
	}

	if (result == ISC_R_SUCCESS) {
		qctx->client->query.attributes |= NS_QUERYATTR_RECURSING;
		if (qctx->dns64) {
			qctx->client->query.attributes |= NS_QUERYATTR_DNS64;
		}
		if (qctx->dns64_exclude) {
			qctx->client->query.attributes |=
				NS_QUERYATTR_DNS64EXCLUDE;
		}
	} else if (query_usestale(qctx, result)) {
		/*
		 * If serve-stale is enabled, query_usestale() already set up
		 * 'qctx' for looking up a stale response.
		 */
		return query_lookup(qctx);
	} else {
		QUERY_ERROR(qctx, result);
	}

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*%
 * Add DS/NSEC(3) record(s) if needed.
 */
static void
query_addds(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;
	dns_fixedname_t fixed;
	dns_name_t *fname = NULL;
	dns_name_t *name;
	dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
	isc_buffer_t *dbuf, b;
	isc_result_t result;
	unsigned int count;

	CTRACE(ISC_LOG_DEBUG(3), "query_addds");

	/*
	 * DS not needed.
	 */
	if (!WANTDNSSEC(client)) {
		return;
	}

	/*
	 * We'll need some resources...
	 */
	rdataset = ns_client_newrdataset(client);
	sigrdataset = ns_client_newrdataset(client);

	/*
	 * Look for the DS record, which may or may not be present.
	 */
	result = dns_db_findrdataset(qctx->db, qctx->node, qctx->version,
				     dns_rdatatype_ds, 0, client->inner.now,
				     rdataset, sigrdataset);
	/*
	 * If we didn't find it, look for an NSEC.
	 */
	if (result == ISC_R_NOTFOUND) {
		result = dns_db_findrdataset(
			qctx->db, qctx->node, qctx->version, dns_rdatatype_nsec,
			0, client->inner.now, rdataset, sigrdataset);
	}
	if (result != ISC_R_SUCCESS && result != ISC_R_NOTFOUND) {
		goto addnsec3;
	}
	if (!dns_rdataset_isassociated(rdataset) ||
	    !dns_rdataset_isassociated(sigrdataset))
	{
		goto addnsec3;
	}

	/*
	 * Find the delegation in the response message - it is not necessarily
	 * the first name in the AUTHORITY section when wildcard processing is
	 * involved.
	 */
	MSG_SECTION_FOREACH(client->message, DNS_SECTION_AUTHORITY, rname) {
		result = dns_message_findtype(rname, dns_rdatatype_ns, 0, NULL);
		if (result == ISC_R_SUCCESS) {
			/*
			 * Add the relevant RRset (DS or NSEC) to the
			 * delegation.
			 */
			query_addrrset(qctx, &rname, &rdataset, &sigrdataset,
				       NULL, DNS_SECTION_AUTHORITY);
			break;
		}
	}

	goto cleanup;

addnsec3:
	if (!dns_db_iszone(qctx->db)) {
		goto cleanup;
	}
	/*
	 * Add the NSEC3 which proves the DS does not exist.
	 */
	dbuf = ns_client_getnamebuf(client);
	fname = ns_client_newname(client, dbuf, &b);
	dns_fixedname_init(&fixed);
	if (dns_rdataset_isassociated(rdataset)) {
		dns_rdataset_disassociate(rdataset);
	}
	if (dns_rdataset_isassociated(sigrdataset)) {
		dns_rdataset_disassociate(sigrdataset);
	}
	name = dns_fixedname_name(&qctx->dsname);
	query_findclosestnsec3(name, qctx->db, qctx->version, client, rdataset,
			       sigrdataset, fname, true,
			       dns_fixedname_name(&fixed));
	if (!dns_rdataset_isassociated(rdataset)) {
		goto cleanup;
	}
	query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf,
		       DNS_SECTION_AUTHORITY);
	/*
	 * Did we find the closest provable encloser instead?
	 * If so add the nearest to the closest provable encloser.
	 */
	if (!dns_name_equal(name, dns_fixedname_name(&fixed))) {
		count = dns_name_countlabels(dns_fixedname_name(&fixed)) + 1;
		dns_name_getlabelsequence(name,
					  dns_name_countlabels(name) - count,
					  count, dns_fixedname_name(&fixed));
		fixfname(client, &fname, &dbuf, &b);
		fixrdataset(client, &rdataset);
		fixrdataset(client, &sigrdataset);
		if (fname == NULL || rdataset == NULL || sigrdataset == NULL) {
			goto cleanup;
		}
		query_findclosestnsec3(dns_fixedname_name(&fixed), qctx->db,
				       qctx->version, client, rdataset,
				       sigrdataset, fname, false, NULL);
		if (!dns_rdataset_isassociated(rdataset)) {
			goto cleanup;
		}
		query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf,
			       DNS_SECTION_AUTHORITY);
	}

cleanup:
	if (rdataset != NULL) {
		ns_client_putrdataset(client, &rdataset);
	}
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (fname != NULL) {
		ns_client_releasename(client, &fname);
	}
}

/*%
 * Handle authoritative NOERROR/NODATA responses.
 */
static isc_result_t
query_nodata(query_ctx_t *qctx, isc_result_t res) {
	isc_result_t result = res;

	CCTRACE(ISC_LOG_DEBUG(3), "query_nodata");

	CALL_HOOK(NS_QUERY_NODATA_BEGIN, qctx);

#ifdef dns64_bis_return_excluded_addresses
	if (qctx->dns64)
#else  /* ifdef dns64_bis_return_excluded_addresses */
	if (qctx->dns64 && !qctx->dns64_exclude)
#endif /* ifdef dns64_bis_return_excluded_addresses */
	{
		isc_buffer_t b;
		/*
		 * Restore the answers from the previous AAAA lookup.
		 */
		if (qctx->rdataset != NULL) {
			ns_client_putrdataset(qctx->client, &qctx->rdataset);
		}
		if (qctx->sigrdataset != NULL) {
			ns_client_putrdataset(qctx->client, &qctx->sigrdataset);
		}
		RESTORE(qctx->rdataset, qctx->client->query.dns64_aaaa);
		RESTORE(qctx->sigrdataset, qctx->client->query.dns64_sigaaaa);
		if (qctx->fname == NULL) {
			qctx->dbuf = ns_client_getnamebuf(qctx->client);
			qctx->fname = ns_client_newname(qctx->client,
							qctx->dbuf, &b);
		}
		dns_name_copy(qctx->client->query.qname, qctx->fname);
		qctx->dns64 = false;
#ifdef dns64_bis_return_excluded_addresses
		/*
		 * Resume the diverted processing of the AAAA response?
		 */
		if (qctx->dns64_exclude) {
			return query_prepresponse(qctx);
		}
#endif /* ifdef dns64_bis_return_excluded_addresses */
	} else if ((result == DNS_R_NXRRSET || result == DNS_R_NCACHENXRRSET) &&
		   !ISC_LIST_EMPTY(qctx->view->dns64) && !qctx->nxrewrite &&
		   qctx->client->message->rdclass == dns_rdataclass_in &&
		   qctx->qtype == dns_rdatatype_aaaa)
	{
		/*
		 * Look to see if there are A records for this name.
		 */
		switch (result) {
		case DNS_R_NCACHENXRRSET:
			/*
			 * This is from the negative cache; if the ttl is
			 * zero, we need to work out whether we have just
			 * decremented to zero or there was no negative
			 * cache ttl in the answer.
			 */
			if (qctx->rdataset->ttl != 0) {
				qctx->client->query.dns64_ttl =
					qctx->rdataset->ttl;
				break;
			}
			if (dns_rdataset_first(qctx->rdataset) == ISC_R_SUCCESS)
			{
				qctx->client->query.dns64_ttl = 0;
			}
			break;
		case DNS_R_NXRRSET:
			qctx->client->query.dns64_ttl =
				dns64_ttl(qctx->db, qctx->version);
			break;
		default:
			UNREACHABLE();
		}

		SAVE(qctx->client->query.dns64_aaaa, qctx->rdataset);
		SAVE(qctx->client->query.dns64_sigaaaa, qctx->sigrdataset);
		ns_client_releasename(qctx->client, &qctx->fname);
		dns_db_detachnode(&qctx->node);
		qctx->type = qctx->qtype = dns_rdatatype_a;
		qctx->dns64 = true;
		return query_lookup(qctx);
	}

	if (qctx->is_zone) {
		return query_sign_nodata(qctx);
	} else {
		/*
		 * We don't call query_addrrset() because we don't need any
		 * of its extra features (and things would probably break!).
		 */
		if (dns_rdataset_isassociated(qctx->rdataset)) {
			ns_client_keepname(qctx->client, qctx->fname,
					   qctx->dbuf);
			dns_message_addname(qctx->client->message, qctx->fname,
					    DNS_SECTION_AUTHORITY);
			ISC_LIST_APPEND(qctx->fname->list, qctx->rdataset,
					link);
			qctx->fname = NULL;
			qctx->rdataset = NULL;
		}
	}

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*%
 * Add RRSIGs for NOERROR/NODATA responses when answering authoritatively.
 */
isc_result_t
query_sign_nodata(query_ctx_t *qctx) {
	isc_result_t result;

	CCTRACE(ISC_LOG_DEBUG(3), "query_sign_nodata");

	/*
	 * Look for a NSEC3 record if we don't have a NSEC record.
	 */
	if (qctx->redirected) {
		return ns_query_done(qctx);
	}
	if (!dns_rdataset_isassociated(qctx->rdataset) &&
	    WANTDNSSEC(qctx->client))
	{
		if (!qctx->fname->attributes.wildcard) {
			dns_name_t *found;
			dns_name_t *qname;
			dns_fixedname_t fixed;
			isc_buffer_t b;

			found = dns_fixedname_initname(&fixed);
			qname = qctx->client->query.qname;

			query_findclosestnsec3(qname, qctx->db, qctx->version,
					       qctx->client, qctx->rdataset,
					       qctx->sigrdataset, qctx->fname,
					       true, found);
			/*
			 * Did we find the closest provable encloser
			 * instead? If so add the nearest to the
			 * closest provable encloser.
			 */
			if (dns_rdataset_isassociated(qctx->rdataset) &&
			    !dns_name_equal(qname, found) &&
			    (((qctx->client->manager->sctx->options &
			       NS_SERVER_NONEAREST) == 0) ||
			     qctx->qtype == dns_rdatatype_ds))
			{
				unsigned int count;
				unsigned int skip;

				/*
				 * Add the closest provable encloser.
				 */
				query_addrrset(qctx, &qctx->fname,
					       &qctx->rdataset,
					       &qctx->sigrdataset, qctx->dbuf,
					       DNS_SECTION_AUTHORITY);

				count = dns_name_countlabels(found) + 1;
				skip = dns_name_countlabels(qname) - count;
				dns_name_getlabelsequence(qname, skip, count,
							  found);

				/*
				 * These ensure fname, rdataset, and
				 * sigrdataset are non-NULL
				 */
				fixfname(qctx->client, &qctx->fname,
					 &qctx->dbuf, &b);
				fixrdataset(qctx->client, &qctx->rdataset);
				fixrdataset(qctx->client, &qctx->sigrdataset);

				/*
				 * 'nearest' doesn't exist so
				 * 'exist' is set to false.
				 */
				query_findclosestnsec3(
					found, qctx->db, qctx->version,
					qctx->client, qctx->rdataset,
					qctx->sigrdataset, qctx->fname, false,
					NULL);
			}
		} else {
			ns_client_releasename(qctx->client, &qctx->fname);
			query_addwildcardproof(qctx, false, true);
		}
	}
	if (dns_rdataset_isassociated(qctx->rdataset)) {
		/*
		 * If we've got a NSEC record, we need to save the
		 * name now because we're going call query_addsoa()
		 * below, and it needs to use the name buffer.
		 */
		ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
	} else if (qctx->fname != NULL) {
		/*
		 * We're not going to use fname, and need to release
		 * our hold on the name buffer so query_addsoa()
		 * may use it.
		 */
		ns_client_releasename(qctx->client, &qctx->fname);
	}

	/*
	 * The RPZ SOA has already been added to the additional section
	 * if this was an RPZ rewrite, but if it wasn't, add it now.
	 */
	if (!qctx->nxrewrite) {
		result = query_addsoa(qctx, UINT32_MAX, DNS_SECTION_AUTHORITY);
		if (result != ISC_R_SUCCESS) {
			QUERY_ERROR(qctx, result);
			return ns_query_done(qctx);
		}
	}

	/*
	 * Add NSEC record if we found one.
	 */
	if (WANTDNSSEC(qctx->client) &&
	    dns_rdataset_isassociated(qctx->rdataset))
	{
		query_addnxrrsetnsec(qctx);
	}

	return ns_query_done(qctx);
}

static void
query_addnxrrsetnsec(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;
	dns_rdata_t sigrdata;
	dns_rdata_rrsig_t sig;
	unsigned int labels;
	isc_buffer_t *dbuf, b;
	dns_name_t *fname;
	isc_result_t result;

	INSIST(qctx->fname != NULL);

	if (!qctx->fname->attributes.wildcard) {
		query_addrrset(qctx, &qctx->fname, &qctx->rdataset,
			       &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY);
		return;
	}

	if (qctx->sigrdataset == NULL ||
	    !dns_rdataset_isassociated(qctx->sigrdataset))
	{
		return;
	}

	if (dns_rdataset_first(qctx->sigrdataset) != ISC_R_SUCCESS) {
		return;
	}

	dns_rdata_init(&sigrdata);
	dns_rdataset_current(qctx->sigrdataset, &sigrdata);
	result = dns_rdata_tostruct(&sigrdata, &sig, NULL);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);

	labels = dns_name_countlabels(qctx->fname);
	if ((unsigned int)sig.labels + 1 >= labels) {
		return;
	}

	query_addwildcardproof(qctx, true, false);

	/*
	 * We'll need some resources...
	 */
	dbuf = ns_client_getnamebuf(client);
	fname = ns_client_newname(client, dbuf, &b);

	dns_name_split(qctx->fname, sig.labels + 1, NULL, fname);
	/* This will succeed, since we've stripped labels. */
	RUNTIME_CHECK(dns_name_concatenate(dns_wildcardname, fname, fname) ==
		      ISC_R_SUCCESS);
	query_addrrset(qctx, &fname, &qctx->rdataset, &qctx->sigrdataset, dbuf,
		       DNS_SECTION_AUTHORITY);
}

/*%
 * Handle NXDOMAIN and empty wildcard responses.
 */
static isc_result_t
query_nxdomain(query_ctx_t *qctx, isc_result_t result) {
	dns_section_t section;
	uint32_t ttl;
	bool empty_wild = (result == DNS_R_EMPTYWILD);

	CCTRACE(ISC_LOG_DEBUG(3), "query_nxdomain");

	CALL_HOOK(NS_QUERY_NXDOMAIN_BEGIN, qctx);

	INSIST(qctx->is_zone || REDIRECT(qctx->client));

	if (!empty_wild) {
		result = query_redirect(qctx, result);
		if (result != ISC_R_COMPLETE) {
			return result;
		}
	}

	if (dns_rdataset_isassociated(qctx->rdataset)) {
		/*
		 * If we've got a NSEC record, we need to save the
		 * name now because we're going call query_addsoa()
		 * below, and it needs to use the name buffer.
		 */
		ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
	} else if (qctx->fname != NULL) {
		/*
		 * We're not going to use fname, and need to release
		 * our hold on the name buffer so query_addsoa()
		 * may use it.
		 */
		ns_client_releasename(qctx->client, &qctx->fname);
	}

	/*
	 * Add SOA to the additional section if generated by a
	 * RPZ rewrite.
	 *
	 * If the query was for a SOA record force the
	 * ttl to zero so that it is possible for clients to find
	 * the containing zone of an arbitrary name with a stub
	 * resolver and not have it cached.
	 */
	section = qctx->nxrewrite ? DNS_SECTION_ADDITIONAL
				  : DNS_SECTION_AUTHORITY;
	ttl = UINT32_MAX;
	if (!qctx->nxrewrite && qctx->qtype == dns_rdatatype_soa &&
	    qctx->zone != NULL && dns_zone_getzeronosoattl(qctx->zone))
	{
		ttl = 0;
	}
	if (!qctx->nxrewrite ||
	    (qctx->rpz_st != NULL && qctx->rpz_st->m.rpz->addsoa))
	{
		result = query_addsoa(qctx, ttl, section);
		if (result != ISC_R_SUCCESS) {
			QUERY_ERROR(qctx, result);
			return ns_query_done(qctx);
		}
	}

	if (WANTDNSSEC(qctx->client)) {
		/*
		 * Add NSEC record if we found one.
		 */
		if (dns_rdataset_isassociated(qctx->rdataset)) {
			query_addrrset(qctx, &qctx->fname, &qctx->rdataset,
				       &qctx->sigrdataset, NULL,
				       DNS_SECTION_AUTHORITY);
		}
		query_addwildcardproof(qctx, false, false);
	}

	/*
	 * Set message rcode.
	 */
	if (empty_wild) {
		qctx->client->message->rcode = dns_rcode_noerror;
	} else {
		qctx->client->message->rcode = dns_rcode_nxdomain;
	}

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*
 * Handle both types of NXDOMAIN redirection, calling redirect()
 * (which implements type redirect zones) and redirect2() (which
 * implements recursive nxdomain-redirect lookups).
 *
 * Any result code other than ISC_R_COMPLETE means redirection was
 * successful and the result code should be returned up the call stack.
 *
 * ISC_R_COMPLETE means we reached the end of this function without
 * redirecting, so query processing should continue past it.
 */
static isc_result_t
query_redirect(query_ctx_t *qctx, isc_result_t saved_result) {
	isc_result_t result;

	CCTRACE(ISC_LOG_DEBUG(3), "query_redirect");

	result = redirect(qctx->client, qctx->fname, qctx->rdataset,
			  &qctx->node, &qctx->db, &qctx->version, qctx->type);
	switch (result) {
	case ISC_R_SUCCESS:
		inc_stats(qctx->client, ns_statscounter_nxdomainredirect);
		return query_prepresponse(qctx);
	case DNS_R_NXRRSET:
		qctx->redirected = true;
		qctx->is_zone = true;
		return query_nodata(qctx, DNS_R_NXRRSET);
	case DNS_R_NCACHENXRRSET:
		qctx->redirected = true;
		qctx->is_zone = false;
		return query_ncache(qctx, DNS_R_NCACHENXRRSET);
	default:
		break;
	}

	result = redirect2(qctx->client, qctx->fname, qctx->rdataset,
			   &qctx->node, &qctx->db, &qctx->version, qctx->type,
			   &qctx->is_zone);
	switch (result) {
	case ISC_R_SUCCESS:
		inc_stats(qctx->client, ns_statscounter_nxdomainredirect);
		return query_prepresponse(qctx);
	case DNS_R_CONTINUE:
		inc_stats(qctx->client,
			  ns_statscounter_nxdomainredirect_rlookup);
		SAVE(qctx->client->query.redirect.db, qctx->db);
		SAVE(qctx->client->query.redirect.node, qctx->node);
		SAVE(qctx->client->query.redirect.zone, qctx->zone);
		qctx->client->query.redirect.qtype = qctx->qtype;
		INSIST(qctx->rdataset != NULL);
		SAVE(qctx->client->query.redirect.rdataset, qctx->rdataset);
		SAVE(qctx->client->query.redirect.sigrdataset,
		     qctx->sigrdataset);
		qctx->client->query.redirect.result = saved_result;
		dns_name_copy(qctx->fname, qctx->client->query.redirect.fname);
		qctx->client->query.redirect.authoritative =
			qctx->authoritative;
		qctx->client->query.redirect.is_zone = qctx->is_zone;
		return ns_query_done(qctx);
	case DNS_R_NXRRSET:
		qctx->redirected = true;
		qctx->is_zone = true;
		return query_nodata(qctx, DNS_R_NXRRSET);
	case DNS_R_NCACHENXRRSET:
		qctx->redirected = true;
		qctx->is_zone = false;
		return query_ncache(qctx, DNS_R_NCACHENXRRSET);
	default:
		break;
	}

	return ISC_R_COMPLETE;
}

/*%
 * Logging function to be passed to dns_nsec_noexistnodata.
 */
static void
log_noexistnodata(void *val, int level, const char *fmt, ...) {
	query_ctx_t *qctx = val;
	va_list ap;

	va_start(ap, fmt);
	ns_client_logv(qctx->client, NS_LOGCATEGORY_QUERIES, NS_LOGMODULE_QUERY,
		       level, fmt, ap);
	va_end(ap);
}

static dns_ttl_t
query_synthttl(dns_rdataset_t *soardataset, dns_rdataset_t *sigsoardataset,
	       dns_rdataset_t *p1rdataset, dns_rdataset_t *sigp1rdataset,
	       dns_rdataset_t *p2rdataset, dns_rdataset_t *sigp2rdataset) {
	dns_rdata_soa_t soa;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_ttl_t ttl;
	isc_result_t result;

	REQUIRE(soardataset != NULL);
	REQUIRE(sigsoardataset != NULL);
	REQUIRE(p1rdataset != NULL);
	REQUIRE(sigp1rdataset != NULL);

	result = dns_rdataset_first(soardataset);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);
	dns_rdataset_current(soardataset, &rdata);
	result = dns_rdata_tostruct(&rdata, &soa, NULL);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);

	ttl = ISC_MIN(soa.minimum, soardataset->ttl);
	ttl = ISC_MIN(ttl, sigsoardataset->ttl);
	ttl = ISC_MIN(ttl, p1rdataset->ttl);
	ttl = ISC_MIN(ttl, sigp1rdataset->ttl);
	if (p2rdataset != NULL) {
		ttl = ISC_MIN(ttl, p2rdataset->ttl);
	}
	if (sigp2rdataset != NULL) {
		ttl = ISC_MIN(ttl, sigp2rdataset->ttl);
	}

	return ttl;
}

/*
 * Synthesize a NODATA response from the SOA and covering NSEC in cache.
 */
static isc_result_t
query_synthnodata(query_ctx_t *qctx, const dns_name_t *signer,
		  dns_rdataset_t **soardatasetp,
		  dns_rdataset_t **sigsoardatasetp) {
	dns_name_t *name = NULL;
	dns_ttl_t ttl;
	isc_buffer_t *dbuf, b;

	/*
	 * Determine the correct TTL to use for the SOA and RRSIG
	 */
	ttl = query_synthttl(*soardatasetp, *sigsoardatasetp, qctx->rdataset,
			     qctx->sigrdataset, NULL, NULL);
	(*soardatasetp)->ttl = (*sigsoardatasetp)->ttl = ttl;

	/*
	 * We want the SOA record to be first, so save the
	 * NODATA proof's name now or else discard it.
	 */
	if (WANTDNSSEC(qctx->client)) {
		ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
	} else {
		ns_client_releasename(qctx->client, &qctx->fname);
	}

	dbuf = ns_client_getnamebuf(qctx->client);
	name = ns_client_newname(qctx->client, dbuf, &b);
	dns_name_copy(signer, name);

	/*
	 * Add SOA record. Omit the RRSIG if DNSSEC was not requested.
	 */
	if (!WANTDNSSEC(qctx->client)) {
		sigsoardatasetp = NULL;
	}
	query_addrrset(qctx, &name, soardatasetp, sigsoardatasetp, dbuf,
		       DNS_SECTION_AUTHORITY);

	if (WANTDNSSEC(qctx->client)) {
		/*
		 * Add NODATA proof.
		 */
		query_addrrset(qctx, &qctx->fname, &qctx->rdataset,
			       &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY);
	}

	inc_stats(qctx->client, ns_statscounter_nodatasynth);

	if (name != NULL) {
		ns_client_releasename(qctx->client, &name);
	}
	return ISC_R_SUCCESS;
}

/*
 * Synthesize a wildcard answer using the contents of 'rdataset'.
 * qctx contains the NODATA proof.
 */
static isc_result_t
query_synthwildcard(query_ctx_t *qctx, dns_rdataset_t *rdataset,
		    dns_rdataset_t *sigrdataset) {
	dns_name_t *name = NULL;
	isc_buffer_t *dbuf, b;
	dns_rdataset_t *cloneset = NULL, *clonesigset = NULL;
	dns_rdataset_t **sigrdatasetp;

	CCTRACE(ISC_LOG_DEBUG(3), "query_synthwildcard");

	/*
	 * We want the answer to be first, so save the
	 * NOQNAME proof's name now or else discard it.
	 */
	if (WANTDNSSEC(qctx->client)) {
		ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
	} else {
		ns_client_releasename(qctx->client, &qctx->fname);
	}

	dbuf = ns_client_getnamebuf(qctx->client);
	name = ns_client_newname(qctx->client, dbuf, &b);
	dns_name_copy(qctx->client->query.qname, name);

	cloneset = ns_client_newrdataset(qctx->client);
	dns_rdataset_clone(rdataset, cloneset);

	/*
	 * Add answer RRset. Omit the RRSIG if DNSSEC was not requested.
	 */
	if (WANTDNSSEC(qctx->client)) {
		clonesigset = ns_client_newrdataset(qctx->client);
		dns_rdataset_clone(sigrdataset, clonesigset);
		sigrdatasetp = &clonesigset;
	} else {
		sigrdatasetp = NULL;
	}

	query_addrrset(qctx, &name, &cloneset, sigrdatasetp, dbuf,
		       DNS_SECTION_ANSWER);

	if (WANTDNSSEC(qctx->client)) {
		/*
		 * Add NOQNAME proof.
		 */
		query_addrrset(qctx, &qctx->fname, &qctx->rdataset,
			       &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY);
	}

	inc_stats(qctx->client, ns_statscounter_wildcardsynth);

	if (name != NULL) {
		ns_client_releasename(qctx->client, &name);
	}
	if (cloneset != NULL) {
		ns_client_putrdataset(qctx->client, &cloneset);
	}
	if (clonesigset != NULL) {
		ns_client_putrdataset(qctx->client, &clonesigset);
	}
	return ISC_R_SUCCESS;
}

/*
 * Add a synthesized CNAME record from the wildard RRset (rdataset)
 * and NODATA proof by calling query_synthwildcard then setup to
 * follow the CNAME.
 */
static isc_result_t
query_synthcnamewildcard(query_ctx_t *qctx, dns_rdataset_t *rdataset,
			 dns_rdataset_t *sigrdataset) {
	isc_result_t result;
	dns_name_t *tname = NULL;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_rdata_cname_t cname;

	result = query_synthwildcard(qctx, rdataset, sigrdataset);
	if (result != ISC_R_SUCCESS) {
		return result;
	}

	qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER;

	/*
	 * Reset qname to be the target name of the CNAME and restart
	 * the query.
	 */
	dns_message_gettempname(qctx->client->message, &tname);

	result = dns_rdataset_first(rdataset);
	if (result != ISC_R_SUCCESS) {
		dns_message_puttempname(qctx->client->message, &tname);
		return result;
	}

	dns_rdataset_current(rdataset, &rdata);
	result = dns_rdata_tostruct(&rdata, &cname, NULL);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);
	dns_rdata_reset(&rdata);

	if (dns_name_equal(qctx->client->query.qname, &cname.cname)) {
		dns_message_puttempname(qctx->client->message, &tname);
		dns_rdata_freestruct(&cname);
		return ISC_R_SUCCESS;
	}

	dns_name_copy(&cname.cname, tname);

	dns_rdata_freestruct(&cname);
	ns_client_qnamereplace(qctx->client, tname);
	qctx->want_restart = true;
	if (!WANTRECURSION(qctx->client)) {
		qctx->options.nolog = true;
	}

	return result;
}

/*
 * Synthesize a NXDOMAIN or NODATA response from qctx (which contains the
 * NOQNAME proof), nowild + nowildrdataset + signowildrdataset (which
 * contains the NOWILDCARD proof or NODATA at wildcard) and
 * signer + soardatasetp + sigsoardatasetp which contain the
 * SOA record + RRSIG for the negative answer.
 */
static isc_result_t
query_synthnxdomainnodata(query_ctx_t *qctx, bool nodata, dns_name_t *nowild,
			  dns_rdataset_t *nowildrdataset,
			  dns_rdataset_t *signowildrdataset, dns_name_t *signer,
			  dns_rdataset_t **soardatasetp,
			  dns_rdataset_t **sigsoardatasetp) {
	dns_name_t *name = NULL;
	dns_ttl_t ttl;
	isc_buffer_t *dbuf, b;
	dns_rdataset_t *cloneset = NULL, *clonesigset = NULL;

	CCTRACE(ISC_LOG_DEBUG(3), "query_synthnxdomain");

	/*
	 * Determine the correct TTL to use for the SOA and RRSIG
	 */
	ttl = query_synthttl(*soardatasetp, *sigsoardatasetp, qctx->rdataset,
			     qctx->sigrdataset, nowildrdataset,
			     signowildrdataset);
	(*soardatasetp)->ttl = (*sigsoardatasetp)->ttl = ttl;

	/*
	 * We want the SOA record to be first, so save the
	 * NOQNAME proof's name now or else discard it.
	 */
	if (WANTDNSSEC(qctx->client)) {
		ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);
	} else {
		ns_client_releasename(qctx->client, &qctx->fname);
	}

	dbuf = ns_client_getnamebuf(qctx->client);
	name = ns_client_newname(qctx->client, dbuf, &b);
	dns_name_copy(signer, name);

	/*
	 * Add SOA record. Omit the RRSIG if DNSSEC was not requested.
	 */
	if (!WANTDNSSEC(qctx->client)) {
		sigsoardatasetp = NULL;
	}
	query_addrrset(qctx, &name, soardatasetp, sigsoardatasetp, dbuf,
		       DNS_SECTION_AUTHORITY);

	if (WANTDNSSEC(qctx->client)) {
		/*
		 * Add NOQNAME proof.
		 */
		query_addrrset(qctx, &qctx->fname, &qctx->rdataset,
			       &qctx->sigrdataset, NULL, DNS_SECTION_AUTHORITY);

		dbuf = ns_client_getnamebuf(qctx->client);
		name = ns_client_newname(qctx->client, dbuf, &b);
		dns_name_copy(nowild, name);

		cloneset = ns_client_newrdataset(qctx->client);
		clonesigset = ns_client_newrdataset(qctx->client);

		dns_rdataset_clone(nowildrdataset, cloneset);
		dns_rdataset_clone(signowildrdataset, clonesigset);

		/*
		 * Add NOWILDCARD proof.
		 */
		query_addrrset(qctx, &name, &cloneset, &clonesigset, dbuf,
			       DNS_SECTION_AUTHORITY);
	}

	if (nodata) {
		inc_stats(qctx->client, ns_statscounter_nodatasynth);
	} else {
		qctx->client->message->rcode = dns_rcode_nxdomain;
		inc_stats(qctx->client, ns_statscounter_nxdomainsynth);
	}

	if (name != NULL) {
		ns_client_releasename(qctx->client, &name);
	}
	if (cloneset != NULL) {
		ns_client_putrdataset(qctx->client, &cloneset);
	}
	if (clonesigset != NULL) {
		ns_client_putrdataset(qctx->client, &clonesigset);
	}
	return ISC_R_SUCCESS;
}

/*
 * Check that all signer names in sigrdataset match the expected signer.
 */
static isc_result_t
checksignames(dns_name_t *signer, dns_rdataset_t *sigrdataset) {
	DNS_RDATASET_FOREACH(sigrdataset) {
		isc_result_t result;
		dns_rdata_t rdata = DNS_RDATA_INIT;
		dns_rdata_rrsig_t rrsig;

		dns_rdataset_current(sigrdataset, &rdata);
		result = dns_rdata_tostruct(&rdata, &rrsig, NULL);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);

		if (dns_name_countlabels(signer) == 0) {
			dns_name_copy(&rrsig.signer, signer);
		} else if (!dns_name_equal(signer, &rrsig.signer)) {
			return ISC_R_FAILURE;
		}
	}

	return ISC_R_SUCCESS;
}

/*%
 * Handle covering NSEC responses.
 *
 * Verify the NSEC record is appropriate for the QNAME; if not,
 * redo the initial query without DNS_DBFIND_COVERINGNSEC.
 *
 * If the covering NSEC proves that the name exists but not the type,
 * synthesize a NODATA response.
 *
 * If the name doesn't exist, compute the wildcard record and check whether
 * the wildcard name exists or not.  If we can't determine this, redo the
 * initial query without DNS_DBFIND_COVERINGNSEC.
 *
 * If the wildcard name does not exist, compute the SOA name and look that
 * up.  If the SOA record does not exist, redo the initial query without
 * DNS_DBFIND_COVERINGNSEC.  If the SOA record exists, synthesize an
 * NXDOMAIN response from the found records.
 *
 * If the wildcard name does exist, perform a lookup for the requested
 * type at the wildcard name.
 */
static isc_result_t
query_coveringnsec(query_ctx_t *qctx) {
	dns_db_t *db = NULL;
	dns_clientinfo_t ci;
	dns_clientinfomethods_t cm;
	dns_dbnode_t *node = NULL;
	dns_fixedname_t fixed;
	dns_fixedname_t fnamespace;
	dns_fixedname_t fnowild;
	dns_fixedname_t fsigner;
	dns_fixedname_t fwild;
	dns_name_t *fname = NULL;
	dns_name_t *namespace = NULL;
	dns_name_t *nowild = NULL;
	dns_name_t *signer = NULL;
	dns_name_t *wild = NULL;
	dns_name_t qname;
	dns_rdataset_t *soardataset = NULL, *sigsoardataset = NULL;
	dns_rdataset_t rdataset, sigrdataset;
	bool done = false;
	bool exists = true, data = true;
	bool redirected = false;
	isc_result_t result = ISC_R_SUCCESS;
	unsigned int dboptions = qctx->client->query.dboptions;
	unsigned int labels;

	CCTRACE(ISC_LOG_DEBUG(3), "query_coveringnsec");

	dns_name_init(&qname);
	dns_rdataset_init(&rdataset);
	dns_rdataset_init(&sigrdataset);
	namespace = dns_fixedname_initname(&fnamespace);

	/*
	 * Check that the NSEC record is from the correct namespace.
	 * For records that belong to the parent zone (i.e. DS),
	 * remove a label to find the correct namespace.
	 */
	dns_name_clone(qctx->client->query.qname, &qname);
	labels = dns_name_countlabels(&qname);
	if (dns_rdatatype_atparent(qctx->qtype) && labels > 1) {
		dns_name_getlabelsequence(&qname, 1, labels - 1, &qname);
	}
	dns_view_sfd_find(qctx->view, &qname, namespace);
	if (!dns_name_issubdomain(qctx->fname, namespace)) {
		goto cleanup;
	}

	/*
	 * If we have no signer name, stop immediately.
	 */
	if (!dns_rdataset_isassociated(qctx->sigrdataset)) {
		goto cleanup;
	}

	wild = dns_fixedname_initname(&fwild);
	fname = dns_fixedname_initname(&fixed);
	signer = dns_fixedname_initname(&fsigner);
	nowild = dns_fixedname_initname(&fnowild);

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, qctx->client, NULL);

	/*
	 * All signer names must be the same to accept.
	 */
	result = checksignames(signer, qctx->sigrdataset);
	if (result != ISC_R_SUCCESS) {
		result = ISC_R_SUCCESS;
		goto cleanup;
	}

	/*
	 * If NSEC or RRSIG are missing from the type map
	 * reject the NSEC RRset.
	 */
	if (!dns_nsec_requiredtypespresent(qctx->rdataset)) {
		goto cleanup;
	}

	/*
	 * Check that we have the correct NOQNAME NSEC record.
	 */
	result = dns_nsec_noexistnodata(qctx->qtype, qctx->client->query.qname,
					qctx->fname, qctx->rdataset, &exists,
					&data, wild, log_noexistnodata, qctx);

	if (result != ISC_R_SUCCESS || (exists && data)) {
		goto cleanup;
	}

	if (exists) {
		if (qctx->type == dns_rdatatype_any) { /* XXX not yet */
			goto cleanup;
		}
		if (!ISC_LIST_EMPTY(qctx->view->dns64) &&
		    dns_rdatatype_isaddr(qctx->type)) /* XXX not yet */
		{
			goto cleanup;
		}
		if (!qctx->resuming && !STALE(qctx->rdataset) &&
		    qctx->rdataset->ttl == 0 && RECURSIONOK(qctx->client))
		{
			goto cleanup;
		}

		soardataset = ns_client_newrdataset(qctx->client);
		sigsoardataset = ns_client_newrdataset(qctx->client);

		/*
		 * Look for SOA record to construct NODATA response.
		 */
		dns_db_attach(qctx->db, &db);
		result = dns_db_findext(db, signer, qctx->version,
					dns_rdatatype_soa, dboptions,
					qctx->client->inner.now, &node, fname,
					&cm, &ci, soardataset, sigsoardataset);

		if (result != ISC_R_SUCCESS) {
			goto cleanup;
		}
		(void)query_synthnodata(qctx, signer, &soardataset,
					&sigsoardataset);
		done = true;
		goto cleanup;
	}

	/*
	 * Look up the no-wildcard proof.
	 */
	dns_db_attach(qctx->db, &db);
	result = dns_db_findext(db, wild, qctx->version, qctx->type,
				dboptions | DNS_DBFIND_COVERINGNSEC,
				qctx->client->inner.now, &node, nowild, &cm,
				&ci, &rdataset, &sigrdataset);

	if (rdataset.trust != dns_trust_secure ||
	    sigrdataset.trust != dns_trust_secure)
	{
		goto cleanup;
	}

	/*
	 * Zero TTL handling of wildcard record.
	 *
	 * We don't yet have code to handle synthesis and type ANY or dns64
	 * processing so we abort the synthesis here if there would be a
	 * interaction.
	 */
	switch (result) {
	case ISC_R_SUCCESS:
		if (qctx->type == dns_rdatatype_any) { /* XXX not yet */
			goto cleanup;
		}
		if (!ISC_LIST_EMPTY(qctx->view->dns64) &&
		    dns_rdatatype_isaddr(qctx->type)) /* XXX not yet */
		{
			goto cleanup;
		}
		FALLTHROUGH;
	case DNS_R_CNAME:
		if (!qctx->resuming && !STALE(&rdataset) && rdataset.ttl == 0 &&
		    RECURSIONOK(qctx->client))
		{
			goto cleanup;
		}
	default:
		break;
	}

	switch (result) {
	case DNS_R_COVERINGNSEC:
		/*
		 * Check that the covering NSEC record is from the right
		 * namespace.
		 */
		if (!dns_name_issubdomain(nowild, namespace)) {
			goto cleanup;
		}
		result = dns_nsec_noexistnodata(qctx->qtype, wild, nowild,
						&rdataset, &exists, &data, NULL,
						log_noexistnodata, qctx);
		if (result != ISC_R_SUCCESS || (exists && data)) {
			goto cleanup;
		}
		break;
	case ISC_R_SUCCESS: /* wild card match */
		(void)query_synthwildcard(qctx, &rdataset, &sigrdataset);
		done = true;
		goto cleanup;
	case DNS_R_CNAME: /* wild card cname */
		(void)query_synthcnamewildcard(qctx, &rdataset, &sigrdataset);
		done = true;
		goto cleanup;
	case DNS_R_NCACHENXRRSET:  /* wild card nodata */
	case DNS_R_NCACHENXDOMAIN: /* direct nxdomain */
	default:
		goto cleanup;
	}

	/*
	 * We now have the proof that we have an NXDOMAIN.  Apply
	 * NXDOMAIN redirection if configured.
	 */
	result = query_redirect(qctx, DNS_R_COVERINGNSEC);
	if (result != ISC_R_COMPLETE) {
		redirected = true;
		goto cleanup;
	}

	/*
	 * Must be signed to accept.
	 */
	if (!dns_rdataset_isassociated(&sigrdataset)) {
		goto cleanup;
	}

	/*
	 * Check signer signer names again.
	 */
	result = checksignames(signer, &sigrdataset);
	if (result != ISC_R_SUCCESS) {
		result = ISC_R_SUCCESS;
		goto cleanup;
	}

	if (node != NULL) {
		dns_db_detachnode(&node);
	}

	soardataset = ns_client_newrdataset(qctx->client);
	sigsoardataset = ns_client_newrdataset(qctx->client);

	/*
	 * Look for SOA record to construct NXDOMAIN response.
	 */
	result = dns_db_findext(db, signer, qctx->version, dns_rdatatype_soa,
				dboptions, qctx->client->inner.now, &node,
				fname, &cm, &ci, soardataset, sigsoardataset);

	if (result != ISC_R_SUCCESS) {
		goto cleanup;
	}
	(void)query_synthnxdomainnodata(qctx, exists, nowild, &rdataset,
					&sigrdataset, signer, &soardataset,
					&sigsoardataset);
	done = true;

cleanup:
	if (dns_rdataset_isassociated(&rdataset)) {
		dns_rdataset_disassociate(&rdataset);
	}
	if (dns_rdataset_isassociated(&sigrdataset)) {
		dns_rdataset_disassociate(&sigrdataset);
	}
	if (soardataset != NULL) {
		ns_client_putrdataset(qctx->client, &soardataset);
	}
	if (sigsoardataset != NULL) {
		ns_client_putrdataset(qctx->client, &sigsoardataset);
	}
	if (db != NULL) {
		if (node != NULL) {
			dns_db_detachnode(&node);
		}
		dns_db_detach(&db);
	}

	if (redirected) {
		return result;
	}

	if (!done) {
		/*
		 * No covering NSEC was found; proceed with recursion.
		 */
		qctx->findcoveringnsec = false;
		if (qctx->fname != NULL) {
			ns_client_releasename(qctx->client, &qctx->fname);
		}
		if (qctx->node != NULL) {
			dns_db_detachnode(&qctx->node);
		}
		ns_client_putrdataset(qctx->client, &qctx->rdataset);
		if (qctx->sigrdataset != NULL) {
			ns_client_putrdataset(qctx->client, &qctx->sigrdataset);
		}
		return query_lookup(qctx);
	}

	return ns_query_done(qctx);
}

/*%
 * Handle negative cache responses, DNS_R_NCACHENXRRSET or
 * DNS_R_NCACHENXDOMAIN. (Note: may also be called with result
 * set to DNS_R_NXDOMAIN when handling DNS64 lookups.)
 */
static isc_result_t
query_ncache(query_ctx_t *qctx, isc_result_t result) {
	INSIST(!qctx->is_zone);
	INSIST(result == DNS_R_NCACHENXDOMAIN ||
	       result == DNS_R_NCACHENXRRSET || result == DNS_R_NXDOMAIN);

	CCTRACE(ISC_LOG_DEBUG(3), "query_ncache");

	CALL_HOOK(NS_QUERY_NCACHE_BEGIN, qctx);

	qctx->authoritative = false;

	if (result == DNS_R_NCACHENXDOMAIN) {
		/*
		 * Set message rcode. (This is not done when
		 * result == DNS_R_NXDOMAIN because that means we're
		 * being called after a DNS64 lookup and don't want
		 * to update the rcode now.)
		 */
		qctx->client->message->rcode = dns_rcode_nxdomain;

		/* Look for RFC 1918 leakage from Internet. */
		if (qctx->qtype == dns_rdatatype_ptr &&
		    qctx->client->message->rdclass == dns_rdataclass_in &&
		    dns_name_countlabels(qctx->fname) == 7)
		{
			warn_rfc1918(qctx->client, qctx->fname, qctx->rdataset);
		}
	}

	if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
		query_stale_refresh_ncache(qctx->client);
	}

	return query_nodata(qctx, result);

cleanup:
	return result;
}

/*
 * If we have a zero ttl from the cache, refetch.
 */
static isc_result_t
query_zerottl_refetch(query_ctx_t *qctx) {
	isc_result_t result;

	CCTRACE(ISC_LOG_DEBUG(3), "query_zerottl_refetch");

	if (qctx->is_zone || qctx->resuming || STALE(qctx->rdataset) ||
	    qctx->rdataset->ttl != 0 || !RECURSIONOK(qctx->client))
	{
		return ISC_R_COMPLETE;
	}

	qctx_clean(qctx);

	INSIST(!REDIRECT(qctx->client));

	result = ns_query_recurse(qctx->client, qctx->qtype,
				  qctx->client->query.qname, NULL, NULL,
				  qctx->resuming);
	if (result == ISC_R_SUCCESS) {
		CALL_HOOK(NS_QUERY_ZEROTTL_RECURSE, qctx);
		qctx->client->query.attributes |= NS_QUERYATTR_RECURSING;

		if (qctx->dns64) {
			qctx->client->query.attributes |= NS_QUERYATTR_DNS64;
		}
		if (qctx->dns64_exclude) {
			qctx->client->query.attributes |=
				NS_QUERYATTR_DNS64EXCLUDE;
		}
	} else {
		/*
		 * There was a zero ttl from the cache, don't fallback to
		 * serve-stale lookup.
		 */
		QUERY_ERROR(qctx, result);
	}

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*
 * Handle CNAME responses.
 */
static isc_result_t
query_cname(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;
	dns_name_t *tname = NULL;
	dns_rdataset_t *trdataset = NULL;
	dns_rdataset_t **sigrdatasetp = NULL;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_rdata_cname_t cname;

	CCTRACE(ISC_LOG_DEBUG(3), "query_cname");

	CALL_HOOK(NS_QUERY_CNAME_BEGIN, qctx);

	result = query_zerottl_refetch(qctx);
	if (result != ISC_R_COMPLETE) {
		goto cleanup;
	}

	/*
	 * Keep a copy of the rdataset.  We have to do this because
	 * query_addrrset may clear 'rdataset' (to prevent the
	 * cleanup code from cleaning it up).
	 */
	trdataset = qctx->rdataset;

	/*
	 * Add the CNAME to the answer section.
	 */
	if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) {
		sigrdatasetp = &qctx->sigrdataset;
	}

	if (WANTDNSSEC(qctx->client) && qctx->fname->attributes.wildcard) {
		dns_fixedname_init(&qctx->wildcardname);
		dns_name_copy(qctx->fname,
			      dns_fixedname_name(&qctx->wildcardname));
		qctx->need_wildcardproof = true;
	}

	if (NOQNAME(qctx->rdataset) && WANTDNSSEC(qctx->client)) {
		qctx->noqname = qctx->rdataset;
	} else {
		qctx->noqname = NULL;
	}

	if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
		query_prefetch(qctx->client, qctx->fname, qctx->rdataset);
	}

	query_addrrset(qctx, &qctx->fname, &qctx->rdataset, sigrdatasetp,
		       qctx->dbuf, DNS_SECTION_ANSWER);

	query_addnoqnameproof(qctx);

	/*
	 * We set the PARTIALANSWER attribute so that if anything goes
	 * wrong later on, we'll return what we've got so far.
	 */
	qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER;

	/*
	 * Reset qname to be the target name of the CNAME and restart
	 * the query.
	 */
	dns_message_gettempname(qctx->client->message, &tname);

	result = dns_rdataset_first(trdataset);
	if (result != ISC_R_SUCCESS) {
		dns_message_puttempname(qctx->client->message, &tname);
		(void)ns_query_done(qctx);
		goto cleanup;
	}

	dns_rdataset_current(trdataset, &rdata);
	result = dns_rdata_tostruct(&rdata, &cname, NULL);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);
	dns_rdata_reset(&rdata);

	dns_name_copy(&cname.cname, tname);

	dns_rdata_freestruct(&cname);
	ns_client_qnamereplace(qctx->client, tname);
	qctx->want_restart = true;
	if (!WANTRECURSION(qctx->client)) {
		qctx->options.nolog = true;
	}

	query_addauth(qctx);

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*
 * Handle DNAME responses.
 */
static isc_result_t
query_dname(query_ctx_t *qctx) {
	dns_name_t *tname, *prefix;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_rdata_dname_t dname;
	dns_fixedname_t fixed;
	dns_rdataset_t *trdataset;
	dns_rdataset_t **sigrdatasetp = NULL;
	dns_namereln_t namereln;
	isc_buffer_t b;
	int order;
	isc_result_t result = ISC_R_UNSET;
	unsigned int nlabels;

	CCTRACE(ISC_LOG_DEBUG(3), "query_dname");

	CALL_HOOK(NS_QUERY_DNAME_BEGIN, qctx);

	/*
	 * Compare the current qname to the found name.  We need
	 * to know how many labels and bits are in common because
	 * we're going to have to split qname later on.
	 */
	namereln = dns_name_fullcompare(qctx->client->query.qname, qctx->fname,
					&order, &nlabels);
	INSIST(namereln == dns_namereln_subdomain);

	/*
	 * Keep a copy of the rdataset.  We have to do this because
	 * query_addrrset may clear 'rdataset' (to prevent the
	 * cleanup code from cleaning it up).
	 */
	trdataset = qctx->rdataset;

	/*
	 * Add the DNAME to the answer section.
	 */
	if (WANTDNSSEC(qctx->client) && qctx->sigrdataset != NULL) {
		sigrdatasetp = &qctx->sigrdataset;
	}

	if (WANTDNSSEC(qctx->client) && qctx->fname->attributes.wildcard) {
		dns_fixedname_init(&qctx->wildcardname);
		dns_name_copy(qctx->fname,
			      dns_fixedname_name(&qctx->wildcardname));
		qctx->need_wildcardproof = true;
	}

	if (!qctx->is_zone && RECURSIONOK(qctx->client)) {
		query_prefetch(qctx->client, qctx->fname, qctx->rdataset);
	}
	query_addrrset(qctx, &qctx->fname, &qctx->rdataset, sigrdatasetp,
		       qctx->dbuf, DNS_SECTION_ANSWER);

	/*
	 * We set the PARTIALANSWER attribute so that if anything goes
	 * wrong later on, we'll return what we've got so far.
	 */
	qctx->client->query.attributes |= NS_QUERYATTR_PARTIALANSWER;

	/*
	 * Get the target name of the DNAME.
	 */
	tname = NULL;
	dns_message_gettempname(qctx->client->message, &tname);

	result = dns_rdataset_first(trdataset);
	if (result != ISC_R_SUCCESS) {
		dns_message_puttempname(qctx->client->message, &tname);
		(void)ns_query_done(qctx);
		goto cleanup;
	}

	dns_rdataset_current(trdataset, &rdata);
	result = dns_rdata_tostruct(&rdata, &dname, NULL);
	RUNTIME_CHECK(result == ISC_R_SUCCESS);
	dns_rdata_reset(&rdata);

	dns_name_copy(&dname.dname, tname);
	dns_rdata_freestruct(&dname);

	/*
	 * Construct the new qname consisting of
	 * <found name prefix>.<dname target>
	 */
	prefix = dns_fixedname_initname(&fixed);
	dns_name_split(qctx->client->query.qname, nlabels, prefix, NULL);
	INSIST(qctx->fname == NULL);
	qctx->dbuf = ns_client_getnamebuf(qctx->client);
	qctx->fname = ns_client_newname(qctx->client, qctx->dbuf, &b);
	result = dns_name_concatenate(prefix, tname, qctx->fname);
	dns_message_puttempname(qctx->client->message, &tname);

	/*
	 * RFC2672, section 4.1, subsection 3c says
	 * we should return YXDOMAIN if the constructed
	 * name would be too long.
	 */
	if (result == DNS_R_NAMETOOLONG) {
		qctx->client->message->rcode = dns_rcode_yxdomain;
	}
	if (result != ISC_R_SUCCESS) {
		(void)ns_query_done(qctx);
		goto cleanup;
	}

	ns_client_keepname(qctx->client, qctx->fname, qctx->dbuf);

	/*
	 * Synthesize a CNAME consisting of
	 *   <old qname> <dname ttl> CNAME <new qname>
	 *	    with <dname trust value>
	 *
	 * Synthesize a CNAME so old old clients that don't understand
	 * DNAME can chain.
	 *
	 * We do not try to synthesize a signature because we hope
	 * that security aware servers will understand DNAME.  Also,
	 * even if we had an online key, making a signature
	 * on-the-fly is costly, and not really legitimate anyway
	 * since the synthesized CNAME is NOT in the zone.
	 */
	query_addcname(qctx, trdataset->trust, trdataset->ttl);

	/*
	 * If the original query was not for a CNAME or ANY then follow the
	 * CNAME.
	 */
	if (qctx->qtype != dns_rdatatype_cname &&
	    qctx->qtype != dns_rdatatype_any)
	{
		/*
		 * Switch to the new qname and restart.
		 */
		ns_client_qnamereplace(qctx->client, qctx->fname);
		qctx->fname = NULL;
		qctx->want_restart = true;
		if (!WANTRECURSION(qctx->client)) {
			qctx->options.nolog = true;
		}
	}

	query_addauth(qctx);

	return ns_query_done(qctx);

cleanup:
	return result;
}

/*%
 * Add CNAME to response.
 */
static void
query_addcname(query_ctx_t *qctx, dns_trust_t trust, dns_ttl_t ttl) {
	ns_client_t *client = qctx->client;
	dns_rdataset_t *rdataset = NULL;
	dns_rdatalist_t *rdatalist = NULL;
	dns_rdata_t *rdata = NULL;
	isc_region_t r;
	dns_name_t *aname = NULL;

	dns_message_gettempname(client->message, &aname);

	dns_name_copy(client->query.qname, aname);

	dns_message_gettemprdatalist(client->message, &rdatalist);

	dns_message_gettemprdata(client->message, &rdata);

	dns_message_gettemprdataset(client->message, &rdataset);

	rdatalist->type = dns_rdatatype_cname;
	rdatalist->rdclass = client->message->rdclass;
	rdatalist->ttl = ttl;

	dns_name_toregion(qctx->fname, &r);
	rdata->data = r.base;
	rdata->length = r.length;
	rdata->rdclass = client->message->rdclass;
	rdata->type = dns_rdatatype_cname;

	ISC_LIST_APPEND(rdatalist->rdata, rdata, link);
	dns_rdatalist_tordataset(rdatalist, rdataset);
	rdataset->trust = trust;
	dns_rdataset_setownercase(rdataset, aname);

	query_addrrset(qctx, &aname, &rdataset, NULL, NULL, DNS_SECTION_ANSWER);
	if (rdataset != NULL) {
		if (dns_rdataset_isassociated(rdataset)) {
			dns_rdataset_disassociate(rdataset);
		}
		dns_message_puttemprdataset(client->message, &rdataset);
	}
	if (aname != NULL) {
		dns_message_puttempname(client->message, &aname);
	}
}

/*%
 * Prepare to respond: determine whether a wildcard proof is needed,
 * then hand off to query_respond() or (for type ANY queries)
 * query_respond_any().
 */
static isc_result_t
query_prepresponse(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;

	CCTRACE(ISC_LOG_DEBUG(3), "query_prepresponse");

	CALL_HOOK(NS_QUERY_PREP_RESPONSE_BEGIN, qctx);

	if (WANTDNSSEC(qctx->client) && qctx->fname->attributes.wildcard) {
		dns_fixedname_init(&qctx->wildcardname);
		dns_name_copy(qctx->fname,
			      dns_fixedname_name(&qctx->wildcardname));
		qctx->need_wildcardproof = true;
	}

	if (qctx->type == dns_rdatatype_any) {
		return query_respond_any(qctx);
	}

	result = query_zerottl_refetch(qctx);
	if (result != ISC_R_COMPLETE) {
		goto cleanup;
	}

	return query_respond(qctx);

cleanup:
	return result;
}

/*%
 * Add SOA to the authority section when sending negative responses
 * (or to the additional section if sending negative responses triggered
 * by RPZ rewriting.)
 */
static isc_result_t
query_addsoa(query_ctx_t *qctx, unsigned int override_ttl,
	     dns_section_t section) {
	ns_client_t *client = qctx->client;
	dns_name_t *name = NULL;
	dns_dbnode_t *node = NULL;
	isc_result_t result, eresult = ISC_R_SUCCESS;
	dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
	dns_rdataset_t **sigrdatasetp = NULL;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;

	CTRACE(ISC_LOG_DEBUG(3), "query_addsoa");

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * Don't add the SOA record for test which set "-T nosoa".
	 */
	if (((client->manager->sctx->options & NS_SERVER_NOSOA) != 0) &&
	    (!WANTDNSSEC(client) || !dns_rdataset_isassociated(qctx->rdataset)))
	{
		return ISC_R_SUCCESS;
	}

	/*
	 * Get resources and make 'name' be the database origin.
	 */
	dns_message_gettempname(client->message, &name);

	/*
	 * We'll be releasing 'name' before returning, so it's safe to
	 * use clone instead of copying here.
	 */
	dns_name_clone(dns_db_origin(qctx->db), name);

	rdataset = ns_client_newrdataset(client);
	if (WANTDNSSEC(client) && dns_db_issecure(qctx->db)) {
		sigrdataset = ns_client_newrdataset(client);
	}

	/*
	 * Find the SOA.
	 */
	result = dns_db_getoriginnode(qctx->db, &node);
	if (result == ISC_R_SUCCESS) {
		result = dns_db_findrdataset(
			qctx->db, node, qctx->version, dns_rdatatype_soa, 0,
			client->inner.now, rdataset, sigrdataset);
	} else {
		dns_fixedname_t foundname;
		dns_name_t *fname;

		fname = dns_fixedname_initname(&foundname);

		result = dns_db_findext(qctx->db, name, qctx->version,
					dns_rdatatype_soa,
					client->query.dboptions, 0, &node,
					fname, &cm, &ci, rdataset, sigrdataset);
	}
	if (result != ISC_R_SUCCESS) {
		/*
		 * This is bad.  We tried to get the SOA RR at the zone top
		 * and it didn't work!
		 */
		CTRACE(ISC_LOG_ERROR, "unable to find SOA RR at zone apex");
		eresult = DNS_R_SERVFAIL;
	} else {
		/*
		 * Extract the SOA MINIMUM.
		 */
		dns_rdata_soa_t soa;
		dns_rdata_t rdata = DNS_RDATA_INIT;
		result = dns_rdataset_first(rdataset);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);
		dns_rdataset_current(rdataset, &rdata);
		result = dns_rdata_tostruct(&rdata, &soa, NULL);
		RUNTIME_CHECK(result == ISC_R_SUCCESS);

		if (override_ttl != UINT32_MAX && override_ttl < rdataset->ttl)
		{
			rdataset->ttl = override_ttl;
			if (sigrdataset != NULL) {
				sigrdataset->ttl = override_ttl;
			}
		}

		/*
		 * Add the SOA and its SIG to the response, with the
		 * TTLs adjusted per RFC2308 section 3.
		 */
		if (rdataset->ttl > soa.minimum) {
			rdataset->ttl = soa.minimum;
		}
		if (sigrdataset != NULL && sigrdataset->ttl > soa.minimum) {
			sigrdataset->ttl = soa.minimum;
		}

		if (sigrdataset != NULL) {
			sigrdatasetp = &sigrdataset;
		} else {
			sigrdatasetp = NULL;
		}

		if (section == DNS_SECTION_ADDITIONAL) {
			rdataset->attributes.required = true;
		}
		query_addrrset(qctx, &name, &rdataset, sigrdatasetp, NULL,
			       section);
	}

	ns_client_putrdataset(client, &rdataset);
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (name != NULL) {
		ns_client_releasename(client, &name);
	}
	if (node != NULL) {
		dns_db_detachnode(&node);
	}

	return eresult;
}

/*%
 * Add NS to authority section (used when the zone apex is already known).
 */
static isc_result_t
query_addns(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;
	isc_result_t result, eresult;
	dns_name_t *name = NULL, *fname;
	dns_dbnode_t *node = NULL;
	dns_fixedname_t foundname;
	dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
	dns_rdataset_t **sigrdatasetp = NULL;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;

	CTRACE(ISC_LOG_DEBUG(3), "query_addns");

	/*
	 * Initialization.
	 */
	eresult = ISC_R_SUCCESS;
	fname = dns_fixedname_initname(&foundname);

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * Get resources and make 'name' be the database origin.
	 */
	dns_message_gettempname(client->message, &name);
	dns_name_clone(dns_db_origin(qctx->db), name);
	rdataset = ns_client_newrdataset(client);

	if (WANTDNSSEC(client) && dns_db_issecure(qctx->db)) {
		sigrdataset = ns_client_newrdataset(client);
	}

	/*
	 * Find the NS rdataset.
	 */
	result = dns_db_getoriginnode(qctx->db, &node);
	if (result == ISC_R_SUCCESS) {
		result = dns_db_findrdataset(
			qctx->db, node, qctx->version, dns_rdatatype_ns, 0,
			client->inner.now, rdataset, sigrdataset);
	} else {
		CTRACE(ISC_LOG_DEBUG(3), "query_addns: calling dns_db_find");
		result = dns_db_findext(qctx->db, name, NULL, dns_rdatatype_ns,
					client->query.dboptions, 0, &node,
					fname, &cm, &ci, rdataset, sigrdataset);
		CTRACE(ISC_LOG_DEBUG(3), "query_addns: dns_db_find complete");
	}
	if (result != ISC_R_SUCCESS) {
		CTRACE(ISC_LOG_ERROR, "query_addns: "
				      "dns_db_findrdataset or dns_db_find "
				      "failed");
		/*
		 * This is bad.  We tried to get the NS rdataset at the zone
		 * top and it didn't work!
		 */
		eresult = DNS_R_SERVFAIL;
	} else {
		if (sigrdataset != NULL) {
			sigrdatasetp = &sigrdataset;
		}
		query_addrrset(qctx, &name, &rdataset, sigrdatasetp, NULL,
			       DNS_SECTION_AUTHORITY);
	}

	CTRACE(ISC_LOG_DEBUG(3), "query_addns: cleanup");
	ns_client_putrdataset(client, &rdataset);
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (name != NULL) {
		ns_client_releasename(client, &name);
	}
	if (node != NULL) {
		dns_db_detachnode(&node);
	}

	CTRACE(ISC_LOG_DEBUG(3), "query_addns: done");
	return eresult;
}

/*%
 * Find the zone cut and add the best NS rrset to the authority section.
 */
static void
query_addbestns(query_ctx_t *qctx) {
	ns_client_t *client = qctx->client;
	dns_db_t *db = NULL, *zdb = NULL;
	dns_dbnode_t *node = NULL;
	dns_name_t *fname = NULL, *zfname = NULL;
	dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
	dns_rdataset_t *zrdataset = NULL, *zsigrdataset = NULL;
	bool is_zone = false, use_zone = false;
	isc_buffer_t *dbuf = NULL;
	isc_result_t result;
	dns_dbversion_t *version = NULL;
	dns_zone_t *zone = NULL;
	isc_buffer_t b;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;
	dns_name_t qname;

	CTRACE(ISC_LOG_DEBUG(3), "query_addbestns");

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	dns_name_init(&qname);
	dns_name_clone(client->query.qname, &qname);

	/*
	 * Find the right database.
	 */
	do {
		result = query_getdb(client, &qname, dns_rdatatype_ns,
				     (dns_getdb_options_t){ 0 }, &zone, &db,
				     &version, &is_zone);
		if (result != ISC_R_SUCCESS) {
			goto cleanup;
		}

		/*
		 * If this is a static stub zone look for a parent zone.
		 */
		if (zone != NULL &&
		    dns_zone_gettype(zone) == dns_zone_staticstub)
		{
			unsigned int labels = dns_name_countlabels(&qname);
			dns_db_detach(&db);
			dns_zone_detach(&zone);
			version = NULL;
			if (labels != 1) {
				dns_name_split(&qname, labels - 1, NULL,
					       &qname);
				continue;
			}
			if (!USECACHE(client)) {
				goto cleanup;
			}
			dns_db_attach(client->inner.view->cachedb, &db);
			is_zone = false;
		}
		break;
	} while (true);

db_find:
	/*
	 * We'll need some resources...
	 */
	dbuf = ns_client_getnamebuf(client);
	fname = ns_client_newname(client, dbuf, &b);
	rdataset = ns_client_newrdataset(client);

	/*
	 * Get the RRSIGs if the client requested them or if we may
	 * need to validate answers from the cache.
	 */
	if (WANTDNSSEC(client) || !is_zone) {
		sigrdataset = ns_client_newrdataset(client);
	}

	/*
	 * Now look for the zonecut.
	 */
	if (is_zone) {
		result = dns_db_findext(
			db, client->query.qname, version, dns_rdatatype_ns,
			client->query.dboptions, client->inner.now, &node,
			fname, &cm, &ci, rdataset, sigrdataset);
		if (result != DNS_R_DELEGATION) {
			goto cleanup;
		}
		if (USECACHE(client)) {
			ns_client_keepname(client, fname, dbuf);
			dns_db_detachnode(&node);
			SAVE(zdb, db);
			SAVE(zfname, fname);
			SAVE(zrdataset, rdataset);
			SAVE(zsigrdataset, sigrdataset);
			version = NULL;
			dns_db_attach(client->inner.view->cachedb, &db);
			is_zone = false;
			goto db_find;
		}
	} else {
		result = dns_db_findzonecut(db, client->query.qname,
					    client->query.dboptions,
					    client->inner.now, &node, fname,
					    NULL, rdataset, sigrdataset);
		if (result == ISC_R_SUCCESS) {
			if (zfname != NULL &&
			    !dns_name_issubdomain(fname, zfname))
			{
				/*
				 * We found a zonecut in the cache, but our
				 * zone delegation is better.
				 */
				use_zone = true;
			}
		} else if (result == ISC_R_NOTFOUND && zfname != NULL) {
			/*
			 * We didn't find anything in the cache, but we
			 * have a zone delegation, so use it.
			 */
			use_zone = true;
		} else {
			goto cleanup;
		}
	}

	if (use_zone) {
		ns_client_releasename(client, &fname);
		/*
		 * We've already done ns_client_keepname() on
		 * zfname, so we must set dbuf to NULL to
		 * prevent query_addrrset() from trying to
		 * call ns_client_keepname() again.
		 */
		dbuf = NULL;
		ns_client_putrdataset(client, &rdataset);
		if (sigrdataset != NULL) {
			ns_client_putrdataset(client, &sigrdataset);
		}

		if (node != NULL) {
			dns_db_detachnode(&node);
		}
		dns_db_detach(&db);

		RESTORE(db, zdb);
		RESTORE(fname, zfname);
		RESTORE(rdataset, zrdataset);
		RESTORE(sigrdataset, zsigrdataset);
	}

	/*
	 * Attempt to validate RRsets that are pending or that are glue.
	 */
	if ((DNS_TRUST_PENDING(rdataset->trust) ||
	     (sigrdataset != NULL && DNS_TRUST_PENDING(sigrdataset->trust))) &&
	    !validate(client, db, fname, rdataset, sigrdataset) &&
	    !PENDINGOK(client->query.dboptions))
	{
		goto cleanup;
	}

	if ((DNS_TRUST_GLUE(rdataset->trust) ||
	     (sigrdataset != NULL && DNS_TRUST_GLUE(sigrdataset->trust))) &&
	    !validate(client, db, fname, rdataset, sigrdataset) &&
	    SECURE(client) && WANTDNSSEC(client))
	{
		goto cleanup;
	}

	/*
	 * If the answer is secure only add NS records if they are secure
	 * when the client may be looking for AD in the response.
	 */
	if (SECURE(client) && (WANTDNSSEC(client) || WANTAD(client)) &&
	    ((rdataset->trust != dns_trust_secure) ||
	     (sigrdataset != NULL && sigrdataset->trust != dns_trust_secure)))
	{
		goto cleanup;
	}

	/*
	 * If the client doesn't want DNSSEC we can discard the sigrdataset
	 * now.
	 */
	if (!WANTDNSSEC(client)) {
		ns_client_putrdataset(client, &sigrdataset);
	}

	query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf,
		       DNS_SECTION_AUTHORITY);

cleanup:
	if (rdataset != NULL) {
		ns_client_putrdataset(client, &rdataset);
	}
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (fname != NULL) {
		ns_client_releasename(client, &fname);
	}
	if (node != NULL) {
		dns_db_detachnode(&node);
	}
	if (db != NULL) {
		dns_db_detach(&db);
	}
	if (zone != NULL) {
		dns_zone_detach(&zone);
	}
	if (zdb != NULL) {
		ns_client_putrdataset(client, &zrdataset);
		if (zsigrdataset != NULL) {
			ns_client_putrdataset(client, &zsigrdataset);
		}
		if (zfname != NULL) {
			ns_client_releasename(client, &zfname);
		}
		dns_db_detach(&zdb);
	}
}

static void
query_addwildcardproof(query_ctx_t *qctx, bool ispositive, bool nodata) {
	ns_client_t *client = qctx->client;
	isc_buffer_t *dbuf, b;
	dns_name_t *name;
	dns_name_t *fname = NULL;
	dns_rdataset_t *rdataset = NULL, *sigrdataset = NULL;
	dns_fixedname_t wfixed;
	dns_name_t *wname;
	dns_dbnode_t *node = NULL;
	unsigned int options;
	unsigned int olabels, nlabels, labels;
	isc_result_t result;
	dns_rdata_t rdata = DNS_RDATA_INIT;
	dns_rdata_nsec_t nsec;
	bool have_wname;
	int order;
	dns_fixedname_t cfixed;
	dns_name_t *cname;
	dns_clientinfomethods_t cm;
	dns_clientinfo_t ci;

	CTRACE(ISC_LOG_DEBUG(3), "query_addwildcardproof");

	dns_clientinfomethods_init(&cm, ns_client_sourceip);
	dns_clientinfo_init(&ci, client, NULL);

	/*
	 * If a name has been specifically flagged as needing
	 * a wildcard proof then it will have been copied to
	 * qctx->wildcardname. Otherwise we just use the client
	 * QNAME.
	 */
	if (qctx->need_wildcardproof) {
		name = dns_fixedname_name(&qctx->wildcardname);
	} else {
		name = client->query.qname;
	}

	/*
	 * Get the NOQNAME proof then if !ispositive
	 * get the NOWILDCARD proof.
	 *
	 * DNS_DBFIND_NOWILD finds the NSEC records that covers the
	 * name ignoring any wildcard.  From the owner and next names
	 * of this record you can compute which wildcard (if it exists)
	 * will match by finding the longest common suffix of the
	 * owner name and next names with the qname and prefixing that
	 * with the wildcard label.
	 *
	 * e.g.
	 *   Given:
	 *	example SOA
	 *	example NSEC b.example
	 *	b.example A
	 *	b.example NSEC a.d.example
	 *	a.d.example A
	 *	a.d.example NSEC g.f.example
	 *	g.f.example A
	 *	g.f.example NSEC z.i.example
	 *	z.i.example A
	 *	z.i.example NSEC example
	 *
	 *   QNAME:
	 *   a.example -> example NSEC b.example
	 *	owner common example
	 *	next common example
	 *	wild *.example
	 *   d.b.example -> b.example NSEC a.d.example
	 *	owner common b.example
	 *	next common example
	 *	wild *.b.example
	 *   a.f.example -> a.d.example NSEC g.f.example
	 *	owner common example
	 *	next common f.example
	 *	wild *.f.example
	 *  j.example -> z.i.example NSEC example
	 *	owner common example
	 *	next common example
	 *	wild *.example
	 */
	options = client->query.dboptions | DNS_DBFIND_NOWILD;
	wname = dns_fixedname_initname(&wfixed);
again:
	have_wname = false;
	/*
	 * We'll need some resources...
	 */
	dbuf = ns_client_getnamebuf(client);
	fname = ns_client_newname(client, dbuf, &b);
	rdataset = ns_client_newrdataset(client);
	sigrdataset = ns_client_newrdataset(client);

	result = dns_db_findext(qctx->db, name, qctx->version,
				dns_rdatatype_nsec, options, 0, &node, fname,
				&cm, &ci, rdataset, sigrdataset);
	if (node != NULL) {
		dns_db_detachnode(&node);
	}

	if (!dns_rdataset_isassociated(rdataset)) {
		/*
		 * No NSEC proof available, return NSEC3 proofs instead.
		 */
		cname = dns_fixedname_initname(&cfixed);

		/*
		 * Find the closest encloser using a binary search.
		 * maxlabels: suffix length of NXDOMAIN result
		 * minlabels: suffix length of non NXDOMAIN result
		 */
		unsigned int maxlabels = dns_name_countlabels(name);
		unsigned int minlabels = dns_name_countlabels(fname);
		unsigned int namelabels = maxlabels;
		bool search = result == DNS_R_NXDOMAIN;
		dns_name_copy(name, cname);
		while (search) {
			labels = (maxlabels + minlabels) / 2;
			dns_name_split(name, labels, NULL, cname);
			if (labels == minlabels) {
				break;
			}
			result = dns_db_findext(qctx->db, cname, qctx->version,
						dns_rdatatype_nsec, options, 0,
						NULL, fname, &cm, &ci, NULL,
						NULL);
			if (result == DNS_R_NXDOMAIN) {
				maxlabels = labels;
			} else {
				minlabels = labels;
			}
		}

		/*
		 * Add closest (provable) encloser NSEC3.
		 */
		query_findclosestnsec3(cname, qctx->db, qctx->version, client,
				       rdataset, sigrdataset, fname, true,
				       cname);
		if (!dns_rdataset_isassociated(rdataset)) {
			goto cleanup;
		}
		if (!ispositive) {
			query_addrrset(qctx, &fname, &rdataset, &sigrdataset,
				       dbuf, DNS_SECTION_AUTHORITY);
		}

		/*
		 * Replace resources which were consumed by query_addrrset.
		 */
		if (fname == NULL) {
			dbuf = ns_client_getnamebuf(client);
			fname = ns_client_newname(client, dbuf, &b);
		}

		if (rdataset == NULL) {
			rdataset = ns_client_newrdataset(client);
		} else if (dns_rdataset_isassociated(rdataset)) {
			dns_rdataset_disassociate(rdataset);
		}

		if (sigrdataset == NULL) {
			sigrdataset = ns_client_newrdataset(client);
		} else if (dns_rdataset_isassociated(sigrdataset)) {
			dns_rdataset_disassociate(sigrdataset);
		}

		/*
		 * Add no qname proof.
		 */
		labels = dns_name_countlabels(cname) + 1;
		if (labels > namelabels) {
			char namebuf[DNS_NAME_FORMATSIZE];
			dns_name_format(cname, namebuf, sizeof(namebuf));
			ns_client_log(qctx->client, DNS_LOGCATEGORY_DNSSEC,
				      NS_LOGMODULE_QUERY, ISC_LOG_WARNING,
				      "closest-encloser name too long: %s",
				      namebuf);
			dns_name_copy(name, wname);
		} else if (labels == namelabels) {
			dns_name_copy(name, wname);
		} else {
			dns_name_split(name, labels, NULL, wname);
		}

		query_findclosestnsec3(wname, qctx->db, qctx->version, client,
				       rdataset, sigrdataset, fname, false,
				       NULL);
		if (!dns_rdataset_isassociated(rdataset)) {
			goto cleanup;
		}
		query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf,
			       DNS_SECTION_AUTHORITY);

		if (ispositive) {
			goto cleanup;
		}

		/*
		 * Replace resources which were consumed by query_addrrset.
		 */
		if (fname == NULL) {
			dbuf = ns_client_getnamebuf(client);
			fname = ns_client_newname(client, dbuf, &b);
		}

		if (rdataset == NULL) {
			rdataset = ns_client_newrdataset(client);
		} else if (dns_rdataset_isassociated(rdataset)) {
			dns_rdataset_disassociate(rdataset);
		}

		if (sigrdataset == NULL) {
			sigrdataset = ns_client_newrdataset(client);
		} else if (dns_rdataset_isassociated(sigrdataset)) {
			dns_rdataset_disassociate(sigrdataset);
		}

		/*
		 * Add the no wildcard proof.
		 */
		result = dns_name_concatenate(dns_wildcardname, cname, wname);
		if (result != ISC_R_SUCCESS) {
			goto cleanup;
		}

		query_findclosestnsec3(wname, qctx->db, qctx->version, client,
				       rdataset, sigrdataset, fname, nodata,
				       NULL);
		if (!dns_rdataset_isassociated(rdataset)) {
			goto cleanup;
		}
		query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf,
			       DNS_SECTION_AUTHORITY);

		goto cleanup;
	} else if (result == DNS_R_NXDOMAIN) {
		if (!ispositive) {
			result = dns_rdataset_first(rdataset);
		}
		if (result == ISC_R_SUCCESS) {
			dns_rdataset_current(rdataset, &rdata);
			result = dns_rdata_tostruct(&rdata, &nsec, NULL);
			RUNTIME_CHECK(result == ISC_R_SUCCESS);
			(void)dns_name_fullcompare(name, fname, &order,
						   &olabels);
			(void)dns_name_fullcompare(name, &nsec.next, &order,
						   &nlabels);
			/*
			 * Check for a pathological condition created when
			 * serving some malformed signed zones and bail out.
			 */
			if (dns_name_countlabels(name) == nlabels) {
				goto cleanup;
			}

			if (olabels > nlabels) {
				dns_name_split(name, olabels, NULL, wname);
			} else {
				dns_name_split(name, nlabels, NULL, wname);
			}
			result = dns_name_concatenate(dns_wildcardname, wname,
						      wname);
			if (result == ISC_R_SUCCESS) {
				have_wname = true;
			}
			dns_rdata_freestruct(&nsec);
		}
		query_addrrset(qctx, &fname, &rdataset, &sigrdataset, dbuf,
			       DNS_SECTION_AUTHORITY);
	}
	if (rdataset != NULL) {
		ns_client_putrdataset(client, &rdataset);
	}
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (fname != NULL) {
		ns_client_releasename(client, &fname);
	}
	if (have_wname) {
		ispositive = true; /* prevent loop */
		if (!dns_name_equal(name, wname)) {
			name = wname;
			goto again;
		}
	}
cleanup:
	if (rdataset != NULL) {
		ns_client_putrdataset(client, &rdataset);
	}
	if (sigrdataset != NULL) {
		ns_client_putrdataset(client, &sigrdataset);
	}
	if (fname != NULL) {
		ns_client_releasename(client, &fname);
	}
}

/*%
 * Add NS records, and NSEC/NSEC3 wildcard proof records if needed,
 * to the authority section.
 */
static void
query_addauth(query_ctx_t *qctx) {
	CCTRACE(ISC_LOG_DEBUG(3), "query_addauth");
	/*
	 * Add NS records to the authority section (if we haven't already
	 * added them to the answer section).
	 */
	if (!qctx->want_restart && !NOAUTHORITY(qctx->client)) {
		if (qctx->is_zone) {
			if (!qctx->answer_has_ns) {
				(void)query_addns(qctx);
			}
		} else if (!qctx->answer_has_ns &&
			   qctx->qtype != dns_rdatatype_ns)
		{
			if (qctx->fname != NULL) {
				ns_client_releasename(qctx->client,
						      &qctx->fname);
			}
			query_addbestns(qctx);
		}
	}

	/*
	 * Add NSEC records to the authority section if they're needed for
	 * DNSSEC wildcard proofs.
	 */
	if (qctx->need_wildcardproof && dns_db_issecure(qctx->db)) {
		query_addwildcardproof(qctx, true, false);
	}
}

/*
 * When sending a referral, if the answer to the question is
 * in the glue, sort it to the start of the additional section.
 */
static void
query_glueanswer(query_ctx_t *qctx) {
	const dns_namelist_t *secs = qctx->client->message->sections;
	const dns_section_t section = DNS_SECTION_ADDITIONAL;
	dns_message_t *msg;

	if (!ISC_LIST_EMPTY(secs[DNS_SECTION_ANSWER]) ||
	    qctx->client->message->rcode != dns_rcode_noerror ||
	    !dns_rdatatype_isaddr(qctx->qtype))
	{
		return;
	}

	msg = qctx->client->message;
	MSG_SECTION_FOREACH(msg, section, name) {
		if (dns_name_equal(name, qctx->client->query.qname)) {
			ISC_LIST_FOREACH(name->list, rdataset, link) {
				if (rdataset->type == qctx->qtype) {
					ISC_LIST_UNLINK(msg->sections[section],
							name, link);
					ISC_LIST_PREPEND(msg->sections[section],
							 name, link);
					ISC_LIST_UNLINK(name->list, rdataset,
							link);
					ISC_LIST_PREPEND(name->list, rdataset,
							 link);
					rdataset->attributes.required = true;
					break;
				}
			}
			break;
		}
	}
}

isc_result_t
ns_query_done(query_ctx_t *qctx) {
	isc_result_t result = ISC_R_UNSET;
	const dns_namelist_t *secs = qctx->client->message->sections;
	bool partial_result_with_servfail = false;

	CCTRACE(ISC_LOG_DEBUG(3), "ns_query_done");

	CALL_HOOK(NS_QUERY_DONE_BEGIN, qctx);

	/*
	 * General cleanup.
	 */
	qctx->rpz_st = qctx->client->query.rpz_st;
	if (qctx->rpz_st != NULL &&
	    (qctx->rpz_st->state & DNS_RPZ_RECURSING) == 0)
	{
		rpz_match_clear(qctx->rpz_st);
		qctx->rpz_st->state &= ~DNS_RPZ_DONE_QNAME;
	}

	qctx_clean(qctx);
	qctx_freedata(qctx);

	/*
	 * Clear the AA bit if we're not authoritative.
	 */
	if (qctx->client->query.restarts == 0 && !qctx->authoritative) {
		qctx->client->message->flags &= ~DNS_MESSAGEFLAG_AA;
		qctx->client->inner.attributes &= ~NS_CLIENTATTR_WANTRC;
	}

	/*
	 * Do we need to restart the query (e.g. for CNAME chaining)?
	 */
	if (qctx->want_restart) {
		if (qctx->client->query.restarts <
		    qctx->client->inner.view->max_restarts)
		{
			query_ctx_t *saved_qctx = NULL;
			qctx->client->query.restarts++;
			saved_qctx = isc_mem_get(qctx->client->manager->mctx,
						 sizeof(*saved_qctx));
			qctx_save(qctx, saved_qctx);
			isc_nmhandle_attach(qctx->client->inner.handle,
					    &qctx->client->inner.restarthandle);
			isc_async_run(qctx->client->manager->loop,
				      async_restart, saved_qctx);
			return DNS_R_CONTINUE;
		} else {
			/*
			 * This is e.g. a long CNAME chain which we cut short.
			 */
			qctx->client->query.attributes |=
				NS_QUERYATTR_PARTIALANSWER;
			qctx->client->message->rcode = dns_rcode_servfail;
			qctx->result = DNS_R_SERVFAIL;

			/*
			 * Send the answer back with a SERVFAIL result even
			 * if recursion was requested.
			 */
			partial_result_with_servfail = true;

			dns_ede_add(&qctx->client->edectx, DNS_EDE_OTHER,
				    "max. restarts reached");
			ns_client_log(qctx->client, NS_LOGCATEGORY_CLIENT,
				      NS_LOGMODULE_QUERY, ISC_LOG_INFO,
				      "query iterations limit reached");
		}
	}

	if (qctx->result != ISC_R_SUCCESS &&
	    (!PARTIALANSWER(qctx->client) ||
	     (WANTRECURSION(qctx->client) && !partial_result_with_servfail) ||
	     qctx->result == DNS_R_DROP))
	{
		if (qctx->result == DNS_R_DUPLICATE ||
		    qctx->result == DNS_R_DROP)
		{
			/*
			 * This was a duplicate query that we are
			 * recursing on or the result of rate limiting.
			 * Don't send a response now for a duplicate query,
			 * because the original will still cause a response.
			 */
			query_next(qctx->client, qctx->result);
		} else {
			/*
			 * If we don't have any answer to give the client,
			 * or if the client requested recursion and thus wanted
			 * the complete answer, send an error response.
			 */
			INSIST(qctx->line >= 0);
			query_error(qctx->client, qctx->result, qctx->line);
		}

		qctx->detach_client = true;
		return qctx->result;
	}

	/*
	 * If we're recursing then just return; the query will
	 * resume when recursion ends.
	 */
	if (RECURSING(qctx->client) &&
	    (!QUERY_STALETIMEOUT(&qctx->client->query) ||
	     qctx->options.stalefirst))
	{
		return qctx->result;
	}

	/*
	 * We are done. Sort the answer to the front of the
	 * additional section if necessary, make a final tweak
	 * to the AA bit if the auth-nxdomain config option
	 * says so, then render and send the response.
	 */
	query_glueanswer(qctx);

	if (qctx->client->message->rcode == dns_rcode_nxdomain &&
	    qctx->view->auth_nxdomain)
	{
		qctx->client->message->flags |= DNS_MESSAGEFLAG_AA;
	}

	/*
	 * If the response is somehow unexpected for the client and this
	 * is a result of recursion, return an error to the caller
	 * to indicate it may need to be logged.
	 */
	if (qctx->resuming &&
	    (ISC_LIST_EMPTY(secs[DNS_SECTION_ANSWER]) ||
	     qctx->client->message->rcode != dns_rcode_noerror))
	{
		qctx->result = ISC_R_FAILURE;
	}

	CALL_HOOK(NS_QUERY_DONE_SEND, qctx);

	query_send(qctx->client);
	qctx->detach_client = true;

	return qctx->result;

cleanup:
	/*
	 * We'd only get here if one of the hooks above
	 * (NS_QUERY_DONE_BEGIN or NS_QUERY_DONE_SEND) returned
	 * NS_HOOK_RETURN. Some housekeeping may be needed.
	 */
	qctx_clean(qctx);
	qctx_freedata(qctx);
	if (!qctx->async) {
		qctx->detach_client = true;
		query_error(qctx->client, DNS_R_SERVFAIL, __LINE__);
	}
	return result;
}

static void
log_tat(ns_client_t *client) {
	char namebuf[DNS_NAME_FORMATSIZE];
	char clientbuf[ISC_NETADDR_FORMATSIZE];
	char classbuf[DNS_RDATACLASS_FORMATSIZE];
	isc_netaddr_t netaddr;
	char *tags = NULL;
	size_t taglen = 0;

	if (!isc_log_wouldlog(ISC_LOG_INFO)) {
		return;
	}

	if ((client->query.qtype != dns_rdatatype_null ||
	     !dns_name_istat(client->query.qname)) &&
	    (client->inner.keytag == NULL ||
	     client->query.qtype != dns_rdatatype_dnskey))
	{
		return;
	}

	isc_netaddr_fromsockaddr(&netaddr, &client->inner.peeraddr);
	dns_name_format(client->query.qname, namebuf, sizeof(namebuf));
	isc_netaddr_format(&netaddr, clientbuf, sizeof(clientbuf));
	dns_rdataclass_format(client->inner.view->rdclass, classbuf,
			      sizeof(classbuf));

	if (client->query.qtype == dns_rdatatype_dnskey) {
		uint16_t keytags = client->inner.keytag_len / 2;
		size_t len = taglen = sizeof("65000") * keytags + 1;
		char *cp = tags = isc_mem_get(client->manager->mctx, taglen);
		int i = 0;

		INSIST(client->inner.keytag != NULL);
		if (tags != NULL) {
			while (keytags-- > 0U) {
				int n;
				uint16_t keytag;
				keytag = (client->inner.keytag[i * 2] << 8) |
					 client->inner.keytag[i * 2 + 1];
				n = snprintf(cp, len, " %u", keytag);
				if (n > 0 && (size_t)n <= len) {
					cp += n;
					len -= n;
					i++;
				} else {
					break;
				}
			}
		}
	}

	isc_log_write(NS_LOGCATEGORY_TAT, NS_LOGMODULE_QUERY, ISC_LOG_INFO,
		      "trust-anchor-telemetry '%s/%s' from %s%s", namebuf,
		      classbuf, clientbuf, tags != NULL ? tags : "");
	if (tags != NULL) {
		isc_mem_put(client->manager->mctx, tags, taglen);
	}
}

static void
log_query(ns_client_t *client, unsigned int flags, unsigned int extflags) {
	char namebuf[DNS_NAME_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];
	char classbuf[DNS_RDATACLASS_FORMATSIZE];
	char sabuf[ISC_SOCKADDR_FORMATSIZE];
	char ecsbuf[NS_CLIENT_ECS_FORMATSIZE] = { 0 };
	char flagsbuf[NS_CLIENT_FLAGS_FORMATSIZE] = { 0 };
	dns_rdataset_t *rdataset;
	int level = ISC_LOG_INFO;

	if (!isc_log_wouldlog(level)) {
		return;
	}

	rdataset = ISC_LIST_HEAD(client->query.qname->list);
	INSIST(rdataset != NULL);
	dns_name_format(client->query.qname, namebuf, sizeof(namebuf));
	dns_rdataclass_format(rdataset->rdclass, classbuf, sizeof(classbuf));
	dns_rdatatype_format(rdataset->type, typebuf, sizeof(typebuf));
	isc_sockaddr_format(&client->inner.destsockaddr, sabuf, sizeof(sabuf));

	if (HAVEECS(client)) {
		ns_client_log_ecs(client, ecsbuf, sizeof(ecsbuf));
	}
	ns_client_log_flags(client, flags, extflags, flagsbuf,
			    sizeof(flagsbuf));

	ns_client_log(client, NS_LOGCATEGORY_QUERIES, NS_LOGMODULE_QUERY, level,
		      "query: %s %s %s %s (%s)%s", namebuf, classbuf, typebuf,
		      flagsbuf, sabuf, ecsbuf);
}

static void
log_queryerror(ns_client_t *client, isc_result_t result, int line, int level) {
	char namebuf[DNS_NAME_FORMATSIZE];
	char typebuf[DNS_RDATATYPE_FORMATSIZE];
	char classbuf[DNS_RDATACLASS_FORMATSIZE];
	const char *namep, *typep, *classp, *sep1, *sep2;
	dns_rdataset_t *rdataset;

	if (!isc_log_wouldlog(level)) {
		return;
	}

	namep = typep = classp = sep1 = sep2 = "";

	/*
	 * Query errors can happen for various reasons.  In some cases we cannot
	 * even assume the query contains a valid question section, so we should
	 * expect exceptional cases.
	 */
	if (client->query.origqname != NULL) {
		dns_name_format(client->query.origqname, namebuf,
				sizeof(namebuf));
		namep = namebuf;
		sep1 = " for ";

		rdataset = ISC_LIST_HEAD(client->query.origqname->list);
		if (rdataset != NULL) {
			dns_rdataclass_format(rdataset->rdclass, classbuf,
					      sizeof(classbuf));
			classp = classbuf;
			dns_rdatatype_format(rdataset->type, typebuf,
					     sizeof(typebuf));
			typep = typebuf;
			sep2 = "/";
		}
	}

	ns_client_log(client, NS_LOGCATEGORY_QUERY_ERRORS, NS_LOGMODULE_QUERY,
		      level, "query failed (%s)%s%s%s%s%s%s at %s:%d",
		      isc_result_totext(result), sep1, namep, sep2, classp,
		      sep2, typep, __FILE__, line);
}

void
ns_query_start(ns_client_t *client, isc_nmhandle_t *handle) {
	isc_result_t result;
	dns_message_t *message;
	dns_rdataset_t *rdataset;
	dns_rdatatype_t qtype;
	unsigned int saved_extflags;
	unsigned int saved_flags;

	REQUIRE(NS_CLIENT_VALID(client));

	/*
	 * Attach to the request handle
	 */
	isc_nmhandle_attach(handle, &client->inner.reqhandle);

	message = client->message;
	saved_extflags = client->inner.extflags;
	saved_flags = client->message->flags;

	CTRACE(ISC_LOG_DEBUG(3), "ns_query_start");

	/*
	 * Ensure that appropriate cleanups occur.
	 */
	client->inner.cleanup = query_cleanup;

	if ((message->flags & DNS_MESSAGEFLAG_RD) != 0) {
		client->query.attributes |= NS_QUERYATTR_WANTRECURSION;
	}

	if ((client->inner.extflags & DNS_MESSAGEEXTFLAG_DO) != 0) {
		client->inner.attributes |= NS_CLIENTATTR_WANTDNSSEC;
	}

	switch (client->inner.view->minimalresponses) {
	case dns_minimal_no:
		break;
	case dns_minimal_yes:
		client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY |
					     NS_QUERYATTR_NOADDITIONAL);
		break;
	case dns_minimal_noauth:
		client->query.attributes |= NS_QUERYATTR_NOAUTHORITY;
		break;
	case dns_minimal_noauthrec:
		if ((message->flags & DNS_MESSAGEFLAG_RD) != 0) {
			client->query.attributes |= NS_QUERYATTR_NOAUTHORITY;
		}
		break;
	}

	if (client->inner.view->cachedb == NULL ||
	    !client->inner.view->recursion)
	{
		/*
		 * We don't have a cache.  Turn off cache support and
		 * recursion.
		 */
		client->query.attributes &= ~(NS_QUERYATTR_RECURSIONOK |
					      NS_QUERYATTR_CACHEOK);
		client->inner.attributes |= NS_CLIENTATTR_NOSETFC;
	} else if ((client->inner.attributes & NS_CLIENTATTR_RA) == 0 ||
		   (message->flags & DNS_MESSAGEFLAG_RD) == 0)
	{
		/*
		 * If the client isn't allowed to recurse (due to
		 * "recursion no", the allow-recursion ACL, or the
		 * lack of a resolver in this view), or if it
		 * doesn't want recursion, turn recursion off.
		 */
		client->query.attributes &= ~NS_QUERYATTR_RECURSIONOK;
		client->inner.attributes |= NS_CLIENTATTR_NOSETFC;
	}

	/*
	 * Check for multiple question queries, since edns1 is dead.
	 */
	if (message->counts[DNS_SECTION_QUESTION] > 1) {
		query_error(client, DNS_R_FORMERR, __LINE__);
		return;
	}

	/*
	 * Get the question name.
	 */
	if (ISC_LIST_EMPTY(message->sections[DNS_SECTION_QUESTION])) {
		query_error(client, ISC_R_NOMORE, __LINE__);
		return;
	}

	client->query.qname =
		ISC_LIST_HEAD(message->sections[DNS_SECTION_QUESTION]);
	client->query.origqname = client->query.qname;

	if (ISC_LIST_NEXT(client->query.qname, link) != NULL) {
		/*
		 * There's more than one QNAME in the question
		 * section.
		 */
		query_error(client, DNS_R_FORMERR, __LINE__);
		return;
	}

	if ((client->manager->sctx->options & NS_SERVER_LOGQUERIES) != 0) {
		log_query(client, saved_flags, saved_extflags);
	}

	/*
	 * Check for meta-queries like IXFR and AXFR.
	 */
	rdataset = ISC_LIST_HEAD(client->query.qname->list);
	INSIST(rdataset != NULL);
	client->query.qtype = qtype = rdataset->type;
	dns_rdatatypestats_increment(client->manager->sctx->rcvquerystats,
				     qtype);

	log_tat(client);

	if (dns_rdatatype_ismeta(qtype)) {
		switch (qtype) {
		case dns_rdatatype_any:
			break; /* Let the query logic handle it. */
		case dns_rdatatype_ixfr:
		case dns_rdatatype_axfr:
			if (isc_nm_is_http_handle(handle)) {
				/*
				 * We cannot use DoH for zone transfers.
				 * According to RFC 8484 a DoH request contains
				 * exactly one DNS message (see Section 6:
				 * Definition of the "application/dns-message"
				 * Media Type).
				 *
				 * This makes DoH unsuitable for zone transfers
				 * as often (and usually!) these need more than
				 * one DNS message, especially for larger zones.
				 * As zone transfers over DoH are not (yet)
				 * standardised, nor discussed in RFC 8484,
				 * the best thing we can do is to return "not
				 * implemented".
				 */
				query_error(client, DNS_R_NOTIMP, __LINE__);
				return;
			}
			if (isc_nm_socket_type(handle) ==
			    isc_nm_streamdnssocket)
			{
				/*
				 * Currently this code is here for DoT, which
				 * has more complex requirements for zone
				 * transfers compared to other stream
				 * protocols. See RFC 9103 for details.
				 */
				switch (isc_nm_xfr_checkperm(handle)) {
				case ISC_R_SUCCESS:
					break;
				case ISC_R_DOTALPNERROR:
					query_error(client, DNS_R_NOALPN,
						    __LINE__);
					return;
				default:
					query_error(client, DNS_R_REFUSED,
						    __LINE__);
					return;
				}
			}
			ns_xfr_start(client, rdataset->type);
			return;
		case dns_rdatatype_maila:
		case dns_rdatatype_mailb:
			query_error(client, DNS_R_NOTIMP, __LINE__);
			return;
		case dns_rdatatype_tkey:
			result = dns_tkey_processquery(
				client->message, client->manager->sctx->tkeyctx,
				client->inner.view->dynamickeys);
			if (result == ISC_R_SUCCESS) {
				query_send(client);
			} else {
				query_error(client, result, __LINE__);
			}
			return;
		default: /* TSIG, etc. */
			query_error(client, DNS_R_FORMERR, __LINE__);
			return;
		}
	}

	/*
	 * Turn on minimal response for (C)DNSKEY and (C)DS queries.
	 */
	if (dns_rdatatype_iskeymaterial(qtype) || qtype == dns_rdatatype_ds) {
		client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY |
					     NS_QUERYATTR_NOADDITIONAL);
	} else if (qtype == dns_rdatatype_ns) {
		/*
		 * Always turn on additional records for NS queries.
		 */
		client->query.attributes &= ~(NS_QUERYATTR_NOAUTHORITY |
					      NS_QUERYATTR_NOADDITIONAL);
	}

	/*
	 * Maybe turn on minimal responses for ANY queries.
	 */
	if (qtype == dns_rdatatype_any && client->inner.view->minimal_any &&
	    !TCP(client))
	{
		client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY |
					     NS_QUERYATTR_NOADDITIONAL);
	}

	/*
	 * Turn on minimal responses for EDNS/UDP bufsize 512 queries.
	 */
	if (client->inner.ednsversion >= 0 && client->inner.udpsize <= 512U &&
	    !TCP(client))
	{
		client->query.attributes |= (NS_QUERYATTR_NOAUTHORITY |
					     NS_QUERYATTR_NOADDITIONAL);
	}

	/*
	 * If the client has requested that DNSSEC checking be disabled,
	 * allow lookups to return pending data and instruct the resolver
	 * to return data before validation has completed.
	 *
	 * We don't need to set DNS_DBFIND_PENDINGOK when validation is
	 * disabled as there will be no pending data.
	 */
	if ((message->flags & DNS_MESSAGEFLAG_CD) != 0 ||
	    qtype == dns_rdatatype_rrsig)
	{
		client->query.dboptions |= DNS_DBFIND_PENDINGOK;
		client->query.fetchoptions |= DNS_FETCHOPT_NOVALIDATE;
	} else if (!client->inner.view->enablevalidation) {
		client->query.fetchoptions |= DNS_FETCHOPT_NOVALIDATE;
	}

	if (client->inner.view->qminimization) {
		client->query.fetchoptions |= DNS_FETCHOPT_QMINIMIZE |
					      DNS_FETCHOPT_QMIN_SKIP_IP6A;
		if (client->inner.view->qmin_strict) {
			client->query.fetchoptions |= DNS_FETCHOPT_QMIN_STRICT;
		}
	}

	/*
	 * Allow glue NS records to be added to the authority section
	 * if the answer is secure.
	 */
	if ((message->flags & DNS_MESSAGEFLAG_CD) != 0) {
		client->query.attributes &= ~NS_QUERYATTR_SECURE;
	}

	/*
	 * Set NS_CLIENTATTR_WANTAD if the client has set AD in the query.
	 * This allows AD to be returned on queries without DO set.
	 */
	if ((message->flags & DNS_MESSAGEFLAG_AD) != 0) {
		client->inner.attributes |= NS_CLIENTATTR_WANTAD;
	}

	/*
	 * This is an ordinary query.
	 */
	result = dns_message_reply(message, true);
	if (result != ISC_R_SUCCESS) {
		query_next(client, result);
		return;
	}

	/*
	 * Assume authoritative response until it is known to be
	 * otherwise.
	 *
	 * If "-T noaa" has been set on the command line don't set
	 * AA on authoritative answers.
	 */
	if ((client->manager->sctx->options & NS_SERVER_NOAA) == 0) {
		message->flags |= DNS_MESSAGEFLAG_AA;
	}

	/*
	 * Set AD.  We must clear it if we add non-validated data to a
	 * response.
	 */
	if (WANTDNSSEC(client) || WANTAD(client)) {
		message->flags |= DNS_MESSAGEFLAG_AD;
	}

	/*
	 * Start global outgoing query count.
	 */
	isc_counter_create(client->manager->mctx,
			   client->inner.view->max_queries, &client->query.qc);

	query_setup(client, qtype);
}
