/*
 * services/rpz.c - rpz service
 *
 * Copyright (c) 2019, NLnet Labs. All rights reserved.
 *
 * This software is open source.
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 * 
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 * 
 * Neither the name of the NLNET LABS nor the names of its contributors may
 * be used to endorse or promote products derived from this software without
 * specific prior written permission.
 * 
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

/**
 * \file
 *
 * This file contains functions to enable RPZ service.
 */

#include "config.h"
#include "services/rpz.h"
#include "util/config_file.h"
#include "sldns/wire2str.h"
#include "sldns/str2wire.h"
#include "util/data/dname.h"
#include "util/net_help.h"
#include "util/log.h"
#include "util/data/dname.h"
#include "util/locks.h"
#include "util/regional.h"

/** string for RPZ action enum */
const char*
rpz_action_to_string(enum rpz_action a)
{
	switch(a) {
	case RPZ_NXDOMAIN_ACTION:	return "nxdomain";
	case RPZ_NODATA_ACTION:		return "nodata";
	case RPZ_PASSTHRU_ACTION:	return "passthru";
	case RPZ_DROP_ACTION:		return "drop";
	case RPZ_TCP_ONLY_ACTION:	return "tcp_only";
	case RPZ_INVALID_ACTION:	return "invalid";
	case RPZ_LOCAL_DATA_ACTION:	return "local_data";
	case RPZ_DISABLED_ACTION:	return "disabled";
	case RPZ_CNAME_OVERRIDE_ACTION:	return "cname_override";
	case RPZ_NO_OVERRIDE_ACTION:	return "no_override";
	}
	return "unknown";
}

/** RPZ action enum for config string */
static enum rpz_action
rpz_config_to_action(char* a)
{
	if(strcmp(a, "nxdomain") == 0)
		return RPZ_NXDOMAIN_ACTION;
	else if(strcmp(a, "nodata") == 0)
		return RPZ_NODATA_ACTION;
	else if(strcmp(a, "passthru") == 0)
		return RPZ_PASSTHRU_ACTION;
	else if(strcmp(a, "drop") == 0)
		return RPZ_DROP_ACTION;
	else if(strcmp(a, "tcp_only") == 0)
		return RPZ_TCP_ONLY_ACTION;
	else if(strcmp(a, "cname") == 0)
		return RPZ_CNAME_OVERRIDE_ACTION;
	else if(strcmp(a, "disabled") == 0)
		return RPZ_DISABLED_ACTION;
	return RPZ_INVALID_ACTION;
}

/** string for RPZ trigger enum */
static const char*
rpz_trigger_to_string(enum rpz_trigger r)
{
	switch(r) {
	case RPZ_QNAME_TRIGGER:		return "qname";
	case RPZ_CLIENT_IP_TRIGGER:	return "client_ip";
	case RPZ_RESPONSE_IP_TRIGGER:	return "response_ip";
	case RPZ_NSDNAME_TRIGGER:	return "nsdname";
	case RPZ_NSIP_TRIGGER:		return "nsip";
	case RPZ_INVALID_TRIGGER:	return "invalid";
	}
	return "unknown";
}

/**
 * Get the label that is just before the root label.
 * @param dname: dname to work on
 * @param maxdnamelen: maximum length of the dname
 * @return: pointer to TLD label, NULL if not found or invalid dname
 */
static uint8_t*
get_tld_label(uint8_t* dname, size_t maxdnamelen)
{
	uint8_t* prevlab = dname;
	size_t dnamelen = 0;

	/* one byte needed for label length */
	if(dnamelen+1 > maxdnamelen)
		return NULL;

	/* only root label */
	if(*dname == 0)
		return NULL;

	while(*dname) {
		dnamelen += ((size_t)*dname)+1;
		if(dnamelen+1 > maxdnamelen)
			return NULL;
		dname = dname+((size_t)*dname)+1;
		if(*dname != 0)
			prevlab = dname;
	}
	return prevlab;
}

/**
 * Classify RPZ action for RR type/rdata
 * @param rr_type: the RR type
 * @param rdatawl: RDATA with 2 bytes length
 * @param rdatalen: the length of rdatawl (including its 2 bytes length)
 * @return: the RPZ action
 */
static enum rpz_action
rpz_rr_to_action(uint16_t rr_type, uint8_t* rdatawl, size_t rdatalen)
{
	char* endptr;
	uint8_t* rdata;
	int rdatalabs;
	uint8_t* tldlab = NULL;

	switch(rr_type) {
		case LDNS_RR_TYPE_SOA:
		case LDNS_RR_TYPE_NS:
		case LDNS_RR_TYPE_DNAME:
		/* all DNSSEC-related RRs must be ignored */
		case LDNS_RR_TYPE_DNSKEY:
		case LDNS_RR_TYPE_DS:
		case LDNS_RR_TYPE_RRSIG:
		case LDNS_RR_TYPE_NSEC:
		case LDNS_RR_TYPE_NSEC3:
			return RPZ_INVALID_ACTION;
		case LDNS_RR_TYPE_CNAME:
			break;
		default:
			return RPZ_LOCAL_DATA_ACTION;
	}

	/* use CNAME target to determine RPZ action */
	log_assert(rr_type == LDNS_RR_TYPE_CNAME);
	if(rdatalen < 3)
		return RPZ_INVALID_ACTION;

	rdata = rdatawl + 2; /* 2 bytes of rdata length */
	if(dname_valid(rdata, rdatalen-2) != rdatalen-2)
		return RPZ_INVALID_ACTION;

	rdatalabs = dname_count_labels(rdata);
	if(rdatalabs == 1)
		return RPZ_NXDOMAIN_ACTION;
	else if(rdatalabs == 2) {
		if(dname_subdomain_c(rdata, (uint8_t*)&"\001*\000"))
			return RPZ_NODATA_ACTION;
		else if(dname_subdomain_c(rdata,
			(uint8_t*)&"\014rpz-passthru\000"))
			return RPZ_PASSTHRU_ACTION;
		else if(dname_subdomain_c(rdata, (uint8_t*)&"\010rpz-drop\000"))
			return RPZ_DROP_ACTION;
		else if(dname_subdomain_c(rdata,
			(uint8_t*)&"\014rpz-tcp-only\000"))
			return RPZ_TCP_ONLY_ACTION;
	}

	/* all other TLDs starting with "rpz-" are invalid */
	tldlab = get_tld_label(rdata, rdatalen-2);
	if(tldlab && dname_lab_startswith(tldlab, "rpz-", &endptr))
		return RPZ_INVALID_ACTION;

	/* no special label found */
	return RPZ_LOCAL_DATA_ACTION;
}

static enum localzone_type 
rpz_action_to_localzone_type(enum rpz_action a)
{
	switch(a) {
	case RPZ_NXDOMAIN_ACTION:	return local_zone_always_nxdomain;
	case RPZ_NODATA_ACTION:		return local_zone_always_nodata;
	case RPZ_DROP_ACTION:		return local_zone_always_deny;
	case RPZ_PASSTHRU_ACTION:	return local_zone_always_transparent;
	case RPZ_LOCAL_DATA_ACTION:	/* fallthrough */
	case RPZ_CNAME_OVERRIDE_ACTION: return local_zone_redirect;
	case RPZ_INVALID_ACTION: 	/* fallthrough */
	case RPZ_TCP_ONLY_ACTION:	/* fallthrough */
	default:			return local_zone_invalid;
	}
}

enum respip_action
rpz_action_to_respip_action(enum rpz_action a)
{
	switch(a) {
	case RPZ_NXDOMAIN_ACTION:	return respip_always_nxdomain;
	case RPZ_NODATA_ACTION:		return respip_always_nodata;
	case RPZ_DROP_ACTION:		return respip_always_deny;
	case RPZ_PASSTHRU_ACTION:	return respip_always_transparent;
	case RPZ_LOCAL_DATA_ACTION:	/* fallthrough */
	case RPZ_CNAME_OVERRIDE_ACTION: return respip_redirect;
	case RPZ_INVALID_ACTION:	/* fallthrough */
	case RPZ_TCP_ONLY_ACTION:	/* fallthrough */
	default:			return respip_invalid;
	}
}

static enum rpz_action
localzone_type_to_rpz_action(enum localzone_type lzt)
{
	switch(lzt) {
	case local_zone_always_nxdomain:	return RPZ_NXDOMAIN_ACTION;
	case local_zone_always_nodata:		return RPZ_NODATA_ACTION;
	case local_zone_always_deny:		return RPZ_DROP_ACTION;
	case local_zone_always_transparent:	return RPZ_PASSTHRU_ACTION;
	case local_zone_redirect:		return RPZ_LOCAL_DATA_ACTION;
	case local_zone_invalid:
	default:
		return RPZ_INVALID_ACTION;
	}
}

enum rpz_action
respip_action_to_rpz_action(enum respip_action a)
{
	switch(a) {
	case respip_always_nxdomain:	return RPZ_NXDOMAIN_ACTION;
	case respip_always_nodata:	return RPZ_NODATA_ACTION;
	case respip_always_deny:	return RPZ_DROP_ACTION;
	case respip_always_transparent:	return RPZ_PASSTHRU_ACTION;
	case respip_redirect:		return RPZ_LOCAL_DATA_ACTION;
	case respip_invalid:
	default:
		return RPZ_INVALID_ACTION;
	}
}

/**
 * Get RPZ trigger for dname
 * @param dname: dname containing RPZ trigger
 * @param dname_len: length of the dname
 * @return: RPZ trigger enum
 */
static enum rpz_trigger
rpz_dname_to_trigger(uint8_t* dname, size_t dname_len)
{
	uint8_t* tldlab;
	char* endptr;

	if(dname_valid(dname, dname_len) != dname_len)
		return RPZ_INVALID_TRIGGER;

	tldlab = get_tld_label(dname, dname_len);
	if(!tldlab || !dname_lab_startswith(tldlab, "rpz-", &endptr))
		return RPZ_QNAME_TRIGGER;

	if(dname_subdomain_c(tldlab,
		(uint8_t*)&"\015rpz-client-ip\000"))
		return RPZ_CLIENT_IP_TRIGGER;
	else if(dname_subdomain_c(tldlab, (uint8_t*)&"\006rpz-ip\000"))
		return RPZ_RESPONSE_IP_TRIGGER;
	else if(dname_subdomain_c(tldlab, (uint8_t*)&"\013rpz-nsdname\000"))
		return RPZ_NSDNAME_TRIGGER;
	else if(dname_subdomain_c(tldlab, (uint8_t*)&"\010rpz-nsip\000"))
		return RPZ_NSIP_TRIGGER;

	return RPZ_QNAME_TRIGGER;
}

void rpz_delete(struct rpz* r)
{
	if(!r)
		return;
	local_zones_delete(r->local_zones);
	respip_set_delete(r->respip_set);
	regional_destroy(r->region);
	free(r->taglist);
	free(r->log_name);
	free(r);
}

int
rpz_clear(struct rpz* r)
{
	/* must hold write lock on auth_zone */
	local_zones_delete(r->local_zones);
	respip_set_delete(r->respip_set);
	if(!(r->local_zones = local_zones_create())){
		return 0;
	}
	if(!(r->respip_set = respip_set_create())) {
		return 0;
	}
	return 1;
}

void
rpz_finish_config(struct rpz* r)
{
	lock_rw_wrlock(&r->respip_set->lock);
	addr_tree_init_parents(&r->respip_set->ip_tree);
	lock_rw_unlock(&r->respip_set->lock);
}

/** new rrset containing CNAME override, does not yet contain a dname */
static struct ub_packed_rrset_key*
new_cname_override(struct regional* region, uint8_t* ct, size_t ctlen)
{
	struct ub_packed_rrset_key* rrset;
	struct packed_rrset_data* pd;
	uint16_t rdlength = htons(ctlen);
	rrset = (struct ub_packed_rrset_key*)regional_alloc_zero(region,
		sizeof(*rrset));
	if(!rrset) {
		log_err("out of memory");
		return NULL;
	}
	rrset->entry.key = rrset;
	pd = (struct packed_rrset_data*)regional_alloc_zero(region, sizeof(*pd));
	if(!pd) {
		log_err("out of memory");
		return NULL;
	}
	pd->trust = rrset_trust_prim_noglue;
	pd->security = sec_status_insecure;

	pd->count = 1;
	pd->rr_len = regional_alloc_zero(region, sizeof(*pd->rr_len));
	pd->rr_ttl = regional_alloc_zero(region, sizeof(*pd->rr_ttl));
	pd->rr_data = regional_alloc_zero(region, sizeof(*pd->rr_data));
	if(!pd->rr_len || !pd->rr_ttl || !pd->rr_data) {
		log_err("out of memory");
		return NULL;
	}
	pd->rr_len[0] = ctlen+2;
	pd->rr_ttl[0] = 3600;
	pd->rr_data[0] = regional_alloc_zero(region, 2 /* rdlength */ + ctlen);
	if(!pd->rr_data[0]) {
		log_err("out of memory");
		return NULL;
	}
	memmove(pd->rr_data[0], &rdlength, 2);
	memmove(pd->rr_data[0]+2, ct, ctlen);

	rrset->entry.data = pd;
	rrset->rk.type = htons(LDNS_RR_TYPE_CNAME);
	rrset->rk.rrset_class = htons(LDNS_RR_CLASS_IN);
	return rrset;
}

struct rpz*
rpz_create(struct config_auth* p)
{
	struct rpz* r = calloc(1, sizeof(*r));
	if(!r)
		goto err;

	r->region = regional_create_custom(sizeof(struct regional));
	if(!r->region) {
		goto err;
	}

	if(!(r->local_zones = local_zones_create())){
		goto err;
	}
	if(!(r->respip_set = respip_set_create())) {
		goto err;
	}
	r->taglistlen = p->rpz_taglistlen;
	r->taglist = memdup(p->rpz_taglist, r->taglistlen);
	if(p->rpz_action_override) {
		r->action_override = rpz_config_to_action(p->rpz_action_override);
	}
	else
		r->action_override = RPZ_NO_OVERRIDE_ACTION;

	if(r->action_override == RPZ_CNAME_OVERRIDE_ACTION) {
		uint8_t nm[LDNS_MAX_DOMAINLEN+1];
		size_t nmlen = sizeof(nm);

		if(!p->rpz_cname) {
			log_err("RPZ override with cname action found, but no "
				"rpz-cname-override configured");
			goto err;
		}

		if(sldns_str2wire_dname_buf(p->rpz_cname, nm, &nmlen) != 0) {
			log_err("cannot parse RPZ cname override: %s",
				p->rpz_cname);
			goto err;
		}
		r->cname_override = new_cname_override(r->region, nm, nmlen);
		if(!r->cname_override) {
			goto err;
		}
	}
	r->log = p->rpz_log;
	if(p->rpz_log_name) {
		if(!(r->log_name = strdup(p->rpz_log_name))) {
			log_err("malloc failure on RPZ log_name strdup");
			goto err;
		}
	}
	return r;
err:
	if(r) {
		if(r->local_zones)
			local_zones_delete(r->local_zones);
		if(r->respip_set)
			respip_set_delete(r->respip_set);
		if(r->taglist)
			free(r->taglist);
		if(r->region)
			regional_destroy(r->region);
		free(r);
	}
	return NULL;
}

/**
 * Remove RPZ zone name from dname
 * Copy dname to newdname, without the originlen number of trailing bytes
 */
static size_t
strip_dname_origin(uint8_t* dname, size_t dnamelen, size_t originlen,
	uint8_t* newdname, size_t maxnewdnamelen)
{
	size_t newdnamelen;
	if(dnamelen < originlen)
		return 0;
	newdnamelen = dnamelen - originlen;
	if(newdnamelen+1 > maxnewdnamelen)
		return 0;
	memmove(newdname, dname, newdnamelen);
	newdname[newdnamelen] = 0;
	return newdnamelen + 1;	/* + 1 for root label */
}

/** Insert RR into RPZ's local-zone */
static void
rpz_insert_qname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
	enum rpz_action a, uint16_t rrtype, uint16_t rrclass, uint32_t ttl,
	uint8_t* rdata, size_t rdata_len, uint8_t* rr, size_t rr_len)
{
	struct local_zone* z;
	enum localzone_type tp = local_zone_always_transparent;
	int dnamelabs = dname_count_labels(dname);
	char* rrstr;
	int newzone = 0;

	if(a == RPZ_TCP_ONLY_ACTION || a == RPZ_INVALID_ACTION) {
		verbose(VERB_ALGO, "RPZ: skipping unsupported action: %s",
			rpz_action_to_string(a));
		free(dname);
		return;
	}

	lock_rw_wrlock(&r->local_zones->lock);
	/* exact match */
	z = local_zones_find(r->local_zones, dname, dnamelen, dnamelabs,
		LDNS_RR_CLASS_IN);
	if(z && a != RPZ_LOCAL_DATA_ACTION) {
		rrstr = sldns_wire2str_rr(rr, rr_len);
		if(!rrstr) {
			log_err("malloc error while inserting RPZ qname "
				"trigger");
			free(dname);
			lock_rw_unlock(&r->local_zones->lock);
			return;
		}
		verbose(VERB_ALGO, "RPZ: skipping duplicate record: '%s'",
			rrstr);
		free(rrstr);
		free(dname);
		lock_rw_unlock(&r->local_zones->lock);
		return;
	}
	if(!z) {
		tp = rpz_action_to_localzone_type(a);
		if(!(z = local_zones_add_zone(r->local_zones, dname, dnamelen,
			dnamelabs, rrclass, tp))) {
			log_warn("RPZ create failed");
			lock_rw_unlock(&r->local_zones->lock);
			/* dname will be free'd in failed local_zone_create() */
			return;
		}
		newzone = 1;
	}
	if(a == RPZ_LOCAL_DATA_ACTION) {
		rrstr = sldns_wire2str_rr(rr, rr_len);
		if(!rrstr) {
			log_err("malloc error while inserting RPZ qname "
				"trigger");
			free(dname);
			lock_rw_unlock(&r->local_zones->lock);
			return;
		}
		lock_rw_wrlock(&z->lock);
		local_zone_enter_rr(z, dname, dnamelen, dnamelabs,
			rrtype, rrclass, ttl, rdata, rdata_len, rrstr);
		lock_rw_unlock(&z->lock);
		free(rrstr);
	}
	if(!newzone)
		free(dname);
	lock_rw_unlock(&r->local_zones->lock);
	return;
}

/** Insert RR into RPZ's respip_set */
static int
rpz_insert_response_ip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
	enum rpz_action a, uint16_t rrtype, uint16_t rrclass, uint32_t ttl,
	uint8_t* rdata, size_t rdata_len, uint8_t* rr, size_t rr_len)
{
	struct resp_addr* node;
	struct sockaddr_storage addr;
	socklen_t addrlen;
	int net, af;
	char* rrstr;
	enum respip_action respa = rpz_action_to_respip_action(a);

	if(a == RPZ_TCP_ONLY_ACTION || a == RPZ_INVALID_ACTION ||
		respa == respip_invalid) {
		verbose(VERB_ALGO, "RPZ: skipping unsupported action: %s",
			rpz_action_to_string(a));
		return 0;
	}

	if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af))
		return 0;

	lock_rw_wrlock(&r->respip_set->lock);
	rrstr = sldns_wire2str_rr(rr, rr_len);
	if(!rrstr) {
		log_err("malloc error while inserting RPZ respip trigger");
		lock_rw_unlock(&r->respip_set->lock);
		return 0;
	}
	if(!(node=respip_sockaddr_find_or_create(r->respip_set, &addr, addrlen,
		net, 1, rrstr))) {
		lock_rw_unlock(&r->respip_set->lock);
		free(rrstr);
		return 0;
	}

	lock_rw_wrlock(&node->lock);
	lock_rw_unlock(&r->respip_set->lock);
	node->action = respa;

	if(a == RPZ_LOCAL_DATA_ACTION) {
		respip_enter_rr(r->respip_set->region, node, rrtype,
			rrclass, ttl, rdata, rdata_len, rrstr, "");
	}
	lock_rw_unlock(&node->lock);
	free(rrstr);
	return 1;
}

int
rpz_insert_rr(struct rpz* r, uint8_t* azname, size_t aznamelen, uint8_t* dname,
	size_t dnamelen, uint16_t rr_type, uint16_t rr_class, uint32_t rr_ttl,
	uint8_t* rdatawl, size_t rdatalen, uint8_t* rr, size_t rr_len)
{
	size_t policydnamelen;
	/* name is free'd in local_zone delete */
	enum rpz_trigger t;
	enum rpz_action a;
	uint8_t* policydname;

	if(!dname_subdomain_c(dname, azname)) {
		char* dname_str = sldns_wire2str_dname(dname, dnamelen);
		char* azname_str = sldns_wire2str_dname(azname, aznamelen);
		if(dname_str && azname_str) {
			log_err("RPZ: name of record (%s) to insert into RPZ is not a "
				"subdomain of the configured name of the RPZ zone (%s)",
				dname_str, azname_str);
		} else {
			log_err("RPZ: name of record to insert into RPZ is not a "
				"subdomain of the configured name of the RPZ zone");
		}
		free(dname_str);
		free(azname_str);
		return 0;
	}

	log_assert(dnamelen >= aznamelen);
	if(!(policydname = calloc(1, (dnamelen-aznamelen)+1))) {
		log_err("malloc error while inserting RPZ RR");
		return 0;
	}

	a = rpz_rr_to_action(rr_type, rdatawl, rdatalen);
	if(!(policydnamelen = strip_dname_origin(dname, dnamelen, aznamelen,
		policydname, (dnamelen-aznamelen)+1))) {
		free(policydname);
		return 0;
	}
	t = rpz_dname_to_trigger(policydname, policydnamelen);
	if(t == RPZ_INVALID_TRIGGER) {
		free(policydname);
		verbose(VERB_ALGO, "RPZ: skipping invalid trigger");
		return 1;
	}
	if(t == RPZ_QNAME_TRIGGER) {
		rpz_insert_qname_trigger(r, policydname, policydnamelen,
			a, rr_type, rr_class, rr_ttl, rdatawl, rdatalen, rr,
			rr_len);
	}
	else if(t == RPZ_RESPONSE_IP_TRIGGER) {
		rpz_insert_response_ip_trigger(r, policydname, policydnamelen,
			a, rr_type, rr_class, rr_ttl, rdatawl, rdatalen, rr,
			rr_len);
		free(policydname);
	}
	else {
		free(policydname);
		verbose(VERB_ALGO, "RPZ: skipping unsupported trigger: %s",
			rpz_trigger_to_string(t));
	}
	return 1;
}

/**
 * Find RPZ local-zone by qname.
 * @param r: rpz containing local-zone tree
 * @param qname: qname
 * @param qname_len: length of qname
 * @param qclass: qclass
 * @param only_exact: if 1 only excact (non wildcard) matches are returned
 * @param wr: get write lock for local-zone if 1, read lock if 0
 * @param zones_keep_lock: if set do not release the r->local_zones lock, this
 * 	  makes the caller of this function responsible for releasing the lock.
 * @return: NULL or local-zone holding rd or wr lock
 */
static struct local_zone*
rpz_find_zone(struct rpz* r, uint8_t* qname, size_t qname_len, uint16_t qclass,
	int only_exact, int wr, int zones_keep_lock)
{
	uint8_t* ce;
	size_t ce_len;
	int ce_labs;
	uint8_t wc[LDNS_MAX_DOMAINLEN+1];
	int exact;
	struct local_zone* z = NULL;
	if(wr) {
		lock_rw_wrlock(&r->local_zones->lock);
	} else {
		lock_rw_rdlock(&r->local_zones->lock);
	}
	z = local_zones_find_le(r->local_zones, qname, qname_len,
		dname_count_labels(qname),
		LDNS_RR_CLASS_IN, &exact);
	if(!z || (only_exact && !exact)) {
		lock_rw_unlock(&r->local_zones->lock);
		return NULL;
	}
	if(wr) {
		lock_rw_wrlock(&z->lock);
	} else {
		lock_rw_rdlock(&z->lock);
	}
	if(!zones_keep_lock) {
		lock_rw_unlock(&r->local_zones->lock);
	}

	if(exact)
		return z;

	/* No exact match found, lookup wildcard. closest encloser must
	 * be the shared parent between the qname and the best local
	 * zone match, append '*' to that and do another lookup. */

	ce = dname_get_shared_topdomain(z->name, qname);
	if(!ce /* should not happen */ || !*ce /* root */) {
		lock_rw_unlock(&z->lock);
		if(zones_keep_lock) {
			lock_rw_unlock(&r->local_zones->lock);
		}
		return NULL;
	}
	ce_labs = dname_count_size_labels(ce, &ce_len);
	if(ce_len+2 > sizeof(wc)) {
		lock_rw_unlock(&z->lock);
		if(zones_keep_lock) {
			lock_rw_unlock(&r->local_zones->lock);
		}
		return NULL;
	}
	wc[0] = 1; /* length of wildcard label */
	wc[1] = (uint8_t)'*'; /* wildcard label */
	memmove(wc+2, ce, ce_len);
	lock_rw_unlock(&z->lock);

	if(!zones_keep_lock) {
		if(wr) {
			lock_rw_wrlock(&r->local_zones->lock);
		} else {
			lock_rw_rdlock(&r->local_zones->lock);
		}
	}
	z = local_zones_find_le(r->local_zones, wc,
		ce_len+2, ce_labs+1, qclass, &exact);
	if(!z || !exact) {
		lock_rw_unlock(&r->local_zones->lock);
		return NULL;
	}
	if(wr) {
		lock_rw_wrlock(&z->lock);
	} else {
		lock_rw_rdlock(&z->lock);
	}
	if(!zones_keep_lock) {
		lock_rw_unlock(&r->local_zones->lock);
	}
	return z;
}

/**
 * Remove RR from RPZ's local-data
 * @param z: local-zone for RPZ, holding write lock
 * @param policydname: dname of RR to remove
 * @param policydnamelen: lenth of policydname
 * @param rr_type: RR type of RR to remove
 * @param rdata: rdata of RR to remove
 * @param rdatalen: length of rdata
 * @return: 1 if zone must be removed after RR deletion
 */
static int
rpz_data_delete_rr(struct local_zone* z, uint8_t* policydname,
	size_t policydnamelen, uint16_t rr_type, uint8_t* rdata,
	size_t rdatalen)
{
	struct local_data* ld;
	struct packed_rrset_data* d;
	size_t index;
	ld = local_zone_find_data(z, policydname, policydnamelen,
		dname_count_labels(policydname));
	if(ld) {
		struct local_rrset* prev=NULL, *p=ld->rrsets;
		while(p && ntohs(p->rrset->rk.type) != rr_type) {
			prev = p;
			p = p->next;
		}
		if(!p)
			return 0;
		d = (struct packed_rrset_data*)p->rrset->entry.data;
		if(packed_rrset_find_rr(d, rdata, rdatalen, &index)) {
			if(d->count == 1) {
				/* no memory recycling for zone deletions ... */
				if(prev) prev->next = p->next;
				else ld->rrsets = p->next;
			}
			if(d->count > 1) {
				if(!local_rrset_remove_rr(d, index))
					return 0;
			}
		}
	}
	if(ld && ld->rrsets)
		return 0;
	return 1;
}

/**
 * Remove RR from RPZ's respip set
 * @param raddr: respip node
 * @param rr_type: RR type of RR to remove
 * @param rdata: rdata of RR to remove
 * @param rdatalen: length of rdata
 * @return: 1 if zone must be removed after RR deletion
 */
static int
rpz_rrset_delete_rr(struct resp_addr* raddr, uint16_t rr_type, uint8_t* rdata,
	size_t rdatalen)
{
	size_t index;
	struct packed_rrset_data* d;
	if(!raddr->data)
		return 1;
	d = raddr->data->entry.data;
	if(ntohs(raddr->data->rk.type) != rr_type) {
		return 0;
	}
	if(packed_rrset_find_rr(d, rdata, rdatalen, &index)) {
		if(d->count == 1) {
			/* regional alloc'd */
			raddr->data->entry.data = NULL; 
			raddr->data = NULL;
			return 1;
		}
		if(d->count > 1) {
			if(!local_rrset_remove_rr(d, index))
				return 0;
		}
	}
	return 0;

}

/** Remove RR from RPZ's local-zone */
static void
rpz_remove_qname_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
	enum rpz_action a, uint16_t rr_type, uint16_t rr_class,
	uint8_t* rdatawl, size_t rdatalen)
{
	struct local_zone* z;
	int delete_zone = 1;
	z = rpz_find_zone(r, dname, dnamelen, rr_class,
		1 /* only exact */, 1 /* wr lock */, 1 /* keep lock*/);
	if(!z) {
		verbose(VERB_ALGO, "RPZ: cannot remove RR from IXFR, "
			"RPZ domain not found");
		return;
	}
	if(a == RPZ_LOCAL_DATA_ACTION)
		delete_zone = rpz_data_delete_rr(z, dname,
			dnamelen, rr_type, rdatawl, rdatalen);
	else if(a != localzone_type_to_rpz_action(z->type)) {
		lock_rw_unlock(&z->lock);
		lock_rw_unlock(&r->local_zones->lock);
		return;
	}
	lock_rw_unlock(&z->lock); 
	if(delete_zone) {
		local_zones_del_zone(r->local_zones, z);
	}
	lock_rw_unlock(&r->local_zones->lock); 
	return;
}

static void
rpz_remove_response_ip_trigger(struct rpz* r, uint8_t* dname, size_t dnamelen,
	enum rpz_action a, uint16_t rr_type, uint8_t* rdatawl, size_t rdatalen)
{
	struct resp_addr* node;
	struct sockaddr_storage addr;
	socklen_t addrlen;
	int net, af;
	int delete_respip = 1;

	if(!netblockdnametoaddr(dname, dnamelen, &addr, &addrlen, &net, &af))
		return;

	lock_rw_wrlock(&r->respip_set->lock);
	if(!(node = (struct resp_addr*)addr_tree_find(
		&r->respip_set->ip_tree, &addr, addrlen, net))) {
		verbose(VERB_ALGO, "RPZ: cannot remove RR from IXFR, "
			"RPZ domain not found");
		lock_rw_unlock(&r->respip_set->lock);
		return;
	}

	lock_rw_wrlock(&node->lock);
	if(a == RPZ_LOCAL_DATA_ACTION) {
		/* remove RR, signal whether RR can be removed */
		delete_respip = rpz_rrset_delete_rr(node, rr_type, rdatawl, 
			rdatalen);
	}
	lock_rw_unlock(&node->lock);
	if(delete_respip)
		respip_sockaddr_delete(r->respip_set, node);
	lock_rw_unlock(&r->respip_set->lock);
}

void
rpz_remove_rr(struct rpz* r, size_t aznamelen, uint8_t* dname, size_t dnamelen,
	uint16_t rr_type, uint16_t rr_class, uint8_t* rdatawl, size_t rdatalen)
{
	size_t policydnamelen;
	enum rpz_trigger t;
	enum rpz_action a;
	uint8_t* policydname;

	if(!(policydname = calloc(1, LDNS_MAX_DOMAINLEN + 1)))
		return;

	a = rpz_rr_to_action(rr_type, rdatawl, rdatalen);
	if(a == RPZ_INVALID_ACTION) {
		free(policydname);
		return;
	}
	if(!(policydnamelen = strip_dname_origin(dname, dnamelen, aznamelen,
		policydname, LDNS_MAX_DOMAINLEN + 1))) {
		free(policydname);
		return;
	}
	t = rpz_dname_to_trigger(policydname, policydnamelen);
	if(t == RPZ_QNAME_TRIGGER) {
		rpz_remove_qname_trigger(r, policydname, policydnamelen, a,
			rr_type, rr_class, rdatawl, rdatalen);
	} else if(t == RPZ_RESPONSE_IP_TRIGGER) {
		rpz_remove_response_ip_trigger(r, policydname, policydnamelen,
			a, rr_type, rdatawl, rdatalen);
	}
	free(policydname);
}

/** print log information for an applied RPZ policy. Based on local-zone's
 * lz_inform_print().
 */
static void
log_rpz_apply(uint8_t* dname, enum rpz_action a, struct query_info* qinfo,
	struct comm_reply* repinfo, char* log_name)
{
	char ip[128], txt[512];
	char dnamestr[LDNS_MAX_DOMAINLEN+1];
	uint16_t port = ntohs(((struct sockaddr_in*)&repinfo->addr)->sin_port);
	dname_str(dname, dnamestr);
	addr_to_str(&repinfo->addr, repinfo->addrlen, ip, sizeof(ip));
	if(log_name)
		snprintf(txt, sizeof(txt), "RPZ applied [%s] %s %s %s@%u",
			log_name, dnamestr, rpz_action_to_string(a), ip,
			(unsigned)port);
	else
		snprintf(txt, sizeof(txt), "RPZ applied %s %s %s@%u",
			dnamestr, rpz_action_to_string(a), ip, (unsigned)port);
	log_nametypeclass(0, txt, qinfo->qname, qinfo->qtype, qinfo->qclass);
}

int
rpz_apply_qname_trigger(struct auth_zones* az, struct module_env* env,
	struct query_info* qinfo, struct edns_data* edns, sldns_buffer* buf,
	struct regional* temp, struct comm_reply* repinfo,
	uint8_t* taglist, size_t taglen, struct ub_server_stats* stats)
{
	struct rpz* r = NULL;
	struct auth_zone* a;
	int ret;
	enum localzone_type lzt;
	struct local_zone* z = NULL;
	struct local_data* ld = NULL;
	lock_rw_rdlock(&az->rpz_lock);
	for(a = az->rpz_first; a; a = a->rpz_az_next) {
		lock_rw_rdlock(&a->lock);
		r = a->rpz;
		if(!r->disabled && (!r->taglist || taglist_intersect(r->taglist,
			r->taglistlen, taglist, taglen))) {
			z = rpz_find_zone(r, qinfo->qname, qinfo->qname_len,
				qinfo->qclass, 0, 0, 0);
			if(z && r->action_override == RPZ_DISABLED_ACTION) {
				if(r->log)
					log_rpz_apply(z->name,
						r->action_override,
						qinfo, repinfo, r->log_name);
				/* TODO only register stats when stats_extended?
				 * */
				stats->rpz_action[r->action_override]++;
				lock_rw_unlock(&z->lock);
				z = NULL;
			}
			if(z)
				break;
		}
		lock_rw_unlock(&a->lock); /* not found in this auth_zone */
	}
	lock_rw_unlock(&az->rpz_lock);
	if(!z)
		return 0; /* not holding auth_zone.lock anymore */

	log_assert(r);
	if(r->action_override == RPZ_NO_OVERRIDE_ACTION)
		lzt = z->type;
	else
		lzt = rpz_action_to_localzone_type(r->action_override);

	if(r->action_override == RPZ_CNAME_OVERRIDE_ACTION) {
		qinfo->local_alias =
			regional_alloc_zero(temp, sizeof(struct local_rrset));
		if(!qinfo->local_alias) {
			lock_rw_unlock(&z->lock);
			lock_rw_unlock(&a->lock);
			return 0; /* out of memory */
		}
		qinfo->local_alias->rrset =
			regional_alloc_init(temp, r->cname_override,
				sizeof(*r->cname_override));
		if(!qinfo->local_alias->rrset) {
			lock_rw_unlock(&z->lock);
			lock_rw_unlock(&a->lock);
			return 0; /* out of memory */
		}
		qinfo->local_alias->rrset->rk.dname = qinfo->qname;
		qinfo->local_alias->rrset->rk.dname_len = qinfo->qname_len;
		if(r->log)
			log_rpz_apply(z->name, RPZ_CNAME_OVERRIDE_ACTION, 
				qinfo, repinfo, r->log_name);
		stats->rpz_action[RPZ_CNAME_OVERRIDE_ACTION]++;
		lock_rw_unlock(&z->lock);
		lock_rw_unlock(&a->lock);
		return 0;
	}

	if(lzt == local_zone_redirect && local_data_answer(z, env, qinfo,
		edns, repinfo, buf, temp, dname_count_labels(qinfo->qname),
		&ld, lzt, -1, NULL, 0, NULL, 0)) {
		if(r->log)
			log_rpz_apply(z->name,
				localzone_type_to_rpz_action(lzt), qinfo,
				repinfo, r->log_name);
		stats->rpz_action[localzone_type_to_rpz_action(lzt)]++;
		lock_rw_unlock(&z->lock);
		lock_rw_unlock(&a->lock);
		return !qinfo->local_alias;
	}

	ret = local_zones_zone_answer(z, env, qinfo, edns, repinfo, buf, temp,
		0 /* no local data used */, lzt);
	if(r->log)
		log_rpz_apply(z->name, localzone_type_to_rpz_action(lzt),
			qinfo, repinfo, r->log_name);
	stats->rpz_action[localzone_type_to_rpz_action(lzt)]++;
	lock_rw_unlock(&z->lock);
	lock_rw_unlock(&a->lock);

	return ret;
}

void rpz_enable(struct rpz* r)
{
    if(!r)
        return;
    r->disabled = 0;
}

void rpz_disable(struct rpz* r)
{
    if(!r)
        return;
    r->disabled = 1;
}