From: Ralph Dolmans Date: Fri, 5 Apr 2019 15:38:43 +0000 (+0200) Subject: Initial RPZ commit X-Git-Tag: release-1.10.0rc1~28^2~28^2~28 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=9274d2630edb3897002142de374cf40d5eab2b59;p=thirdparty%2Funbound.git Initial RPZ commit --- diff --git a/services/rpz.c b/services/rpz.c new file mode 100644 index 000000000..4f159cb34 --- /dev/null +++ b/services/rpz.c @@ -0,0 +1,442 @@ +/* + * 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 "util/data/dname.h" +#include "util/net_help.h" +#include "util/log.h" +#include "util/data/dname.h" +#include "util/locks.h" + +/** string for RPZ action enum */ +static const char* +rpz_action_to_string(enum rpz_action a) +{ + switch(a) { + case RPZ_NXDOMAIN_ACTION: return "NXDOMAIN ACTION"; + case RPZ_NODATA_ACTION: return "NODATA ACTION"; + case RPZ_PASSTHRU_ACTION: return "PASSTHRU ACTION"; + case RPZ_DROP_ACTION: return "DROP ACTION"; + case RPZ_TCP_ONLY_ACTION: return "TCP ONLY ACTION"; + case RPZ_INVALID_ACTION: return "INVALID ACTION"; + case RPZ_LOCAL_DATA_ACTION: return "LOCAL DATA ACTION"; + } + return "UNKNOWN RPZ 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 TRIGGER"; + case RPZ_CLIENT_IP_TRIGGER: return "CLIENT IP TRIGGER"; + case RPZ_RESPONSE_IP_TRIGGER: return "RESPONSE IP TRIGGER"; + case RPZ_NSDNAME_TRIGGER: return "NSDNAME TRIGGER"; + case RPZ_NSIP_TRIGGER: return "NSIP TRIGGER"; + } + return "UNKNOWN RPZ TRIGGER"; +} + +/** + * Get the label that is just before the root label. + * @param dname: dname to work on + * @return: pointer to TLD label + */ +static uint8_t* +get_tld_label(uint8_t* dname) +{ + uint8_t* prevlab = dname; + + /* only root label */ + if(*dname == 0) + return NULL; + + while(*dname) { + dname = dname+*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); + if(tldlab && dname_lab_startswith(tldlab, "rpz-", &endptr)) + return RPZ_INVALID_ACTION; + + /* no special label found */ + return RPZ_LOCAL_DATA_ACTION; +} + +/** Get RPZ trigger for dname */ +static enum rpz_trigger +rpz_dname_to_trigger(uint8_t* dname) +{ + uint8_t* tldlab; + char* endptr; + tldlab = get_tld_label(dname); + 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); + free(r); +} + +int +rpz_clear_lz(struct rpz* r) +{ + /* must hold write lock on auth_zone */ + local_zones_delete(r->local_zones); + if(!(r->local_zones = local_zones_create())){ + return 0; + } + return 1; +} + +struct rpz* +rpz_create(struct config_auth* p) +{ + struct rpz* r = calloc(1, sizeof(*r)); + if(!r) + return 0; + + if(!(r->local_zones = local_zones_create())){ + free(r); + return 0; + } + r->taglist = memdup(p->rpz_taglist, p->rpz_taglistlen); + r->taglistlen = p->rpz_taglistlen; + return r; +} + +/** Remove RPZ zone name from dname */ +static size_t +strip_dname_origin(uint8_t* dname, size_t dnamelen, size_t originlen, + uint8_t* newdname) +{ + size_t newdnamelen; + if(dnamelen < originlen) + return 0; + newdnamelen = dnamelen - originlen; + memmove(newdname, dname, newdnamelen); + return newdnamelen + 1; /* + 1 for root label */ +} + +/** Insert RR into RPZ's local-zone */ +static int +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; + + if(a == RPZ_NXDOMAIN_ACTION) + tp = local_zone_always_nxdomain; + else if(a == RPZ_NODATA_ACTION) + tp = local_zone_always_nodata; + else if(a == RPZ_DROP_ACTION) + tp = local_zone_deny; + else if(a == RPZ_PASSTHRU_ACTION) + tp = local_zone_always_transparent; + else if(a == RPZ_LOCAL_DATA_ACTION) + tp = local_zone_redirect; + else { + verbose(VERB_ALGO, "RPZ: skipping unusupported action: %s", + rpz_action_to_string(a)); + return 0; + } + + 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); + verbose(VERB_ALGO, "RPZ: skipping duplicate record: '%s'", + rrstr); + free(rrstr); + lock_rw_unlock(&r->local_zones->lock); + return 0; + } + if(!z) { + z = local_zones_add_zone(r->local_zones, dname, dnamelen, + dnamelabs, rrclass, tp); + } + if(!z) { + log_warn("RPZ create failed"); + lock_rw_unlock(&r->local_zones->lock); + return 0; + } + if(a == RPZ_LOCAL_DATA_ACTION) { + /* insert data. TODO synth wildcard cname target on + * lookup */ + rrstr = sldns_wire2str_rr(rr, rr_len); + local_zone_enter_rr(z, dname, dnamelen, dnamelabs, + rrtype, rrclass, ttl, rdata, rdata_len, rrstr); + free(rrstr); + } + lock_rw_unlock(&r->local_zones->lock); + return 1; +} + +void +rpz_insert_rr(struct rpz* r, 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 */ + uint8_t* policydname = calloc(1, LDNS_MAX_DOMAINLEN + 1); + enum rpz_trigger t; + enum rpz_action a; + + a = rpz_rr_to_action(rr_type, rdatawl, rdatalen); + if(!(policydnamelen = strip_dname_origin(dname, dnamelen, aznamelen, + policydname))) { + free(policydname); + return; + } + t = rpz_dname_to_trigger(policydname); + 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 { + free(policydname); + verbose(VERB_ALGO, "RPZ: skipping unusupported trigger: %s", + rpz_trigger_to_string(t)); + } +} + +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, uint8_t* rr, size_t rr_len) +{ + /* TODO: remove RR, used for IXFR */ +} + +struct local_zone* +rpz_find_zone(struct rpz* r, struct query_info* qinfo) +{ + uint8_t* ce; + size_t ce_len, ce_labs; + uint8_t wc[LDNS_MAX_DOMAINLEN]; + int exact; + struct local_zone* z = NULL; + lock_rw_rdlock(&r->local_zones->lock); + z = local_zones_find_le(r->local_zones, qinfo->qname, + qinfo->qname_len, dname_count_labels(qinfo->qname), + LDNS_RR_CLASS_IN, &exact); + if(!z) { + lock_rw_unlock(&r->local_zones->lock); + return NULL; + } + 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, qinfo->qname); + if(!ce /* should not happen */ || !*ce /* root */) { + lock_rw_unlock(&z->lock); + return NULL; + } + ce_labs = dname_count_size_labels(ce, &ce_len); + if(ce_len+2 > sizeof(wc)) { + lock_rw_unlock(&z->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); + + lock_rw_rdlock(&r->local_zones->lock); + z = local_zones_find_le(r->local_zones, wc, + ce_len+2, ce_labs+1, qinfo->qclass, &exact); + if(!z || !exact) { + lock_rw_unlock(&r->local_zones->lock); + return NULL; + } + lock_rw_rdlock(&z->lock); + lock_rw_unlock(&r->local_zones->lock); + return z; +} + +/** print log information for an applied RPZ policy. Based on local-zone's + * lz_inform_print(). + */ +static void +rpz_inform_print(struct local_zone* z, struct query_info* qinfo, + struct comm_reply* repinfo) +{ + char ip[128], txt[512]; + char zname[LDNS_MAX_DOMAINLEN+1]; + uint16_t port = ntohs(((struct sockaddr_in*)&repinfo->addr)->sin_port); + dname_str(z->name, zname); + addr_to_str(&repinfo->addr, repinfo->addrlen, ip, sizeof(ip)); + snprintf(txt, sizeof(txt), "RPZ applied %s %s %s@%u", zname, + local_zone_type2str(z->type), 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 rpz* r; + int ret; + struct local_zone* z = NULL; + struct local_data* ld = NULL; + lock_rw_rdlock(&az->rpz_lock); + for(r = az->rpz_first; r && !z; r = r->next) { + if(!r->taglist || taglist_intersect(r->taglist, + r->taglistlen, taglist, taglen)) + z = rpz_find_zone(r, qinfo); + } + lock_rw_unlock(&az->rpz_lock); + if(!z) + return 0; + + if(z->type == local_zone_redirect && local_data_answer(z, env, qinfo, + edns, repinfo, buf, temp, dname_count_labels(qinfo->qname), + &ld, z->type, -1, NULL, 0, NULL, 0)) { + rpz_inform_print(z, qinfo, repinfo); + return !qinfo->local_alias; + } + + ret = local_zones_zone_answer(z, env, qinfo, edns, repinfo, buf, temp, + 0 /* no local data used */, z->type); + lock_rw_unlock(&z->lock); + if(ret) + rpz_inform_print(z, qinfo, repinfo); + + return ret; +} diff --git a/services/rpz.h b/services/rpz.h new file mode 100644 index 000000000..a63189329 --- /dev/null +++ b/services/rpz.h @@ -0,0 +1,163 @@ +/* + * services/rpz.h - 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. + */ + +#ifndef SERVICES_RPZ_H +#define SERVICES_RPZ_H + +#include "services/localzone.h" +#include "util/locks.h" +#include "util/config_file.h" +#include "services/authzone.h" +#include "sldns/sbuffer.h" + +/** + * RPZ containing policies. Pointed to from corresponding authz-one. Part of a + * linked list to keep configuration order. Iterating or changing the linked + * list requires the rpz_lock from struct auth_zones. + */ +struct rpz { + struct local_zones* local_zones; + uint8_t* taglist; + size_t taglistlen; + struct rpz* next; + struct rpz* prev; + /* tags */ +}; + +/** + * RPZ triggers, only the QNAME trigger is currently supported in Unbound. + */ +enum rpz_trigger { + RPZ_QNAME_TRIGGER = 0, + /* unsupported triggers */ + RPZ_CLIENT_IP_TRIGGER, /* rpz-client-ip */ + RPZ_RESPONSE_IP_TRIGGER, /* rpz-ip */ + RPZ_NSDNAME_TRIGGER, /* rpz-nsdname */ + RPZ_NSIP_TRIGGER, /* rpz-nsip */ +}; + +/** + * RPZ actions. + */ +enum rpz_action { + RPZ_NXDOMAIN_ACTION = 0,/* CNAME . */ + RPZ_NODATA_ACTION, /* CNAME *. */ + RPZ_PASSTHRU_ACTION, /* CNAME rpz-passthru. */ + RPZ_DROP_ACTION, /* CNAME rpz-drop. */ + RPZ_TCP_ONLY_ACTION, /* CNAME rpz-tcp-only. */ + RPZ_INVALID_ACTION, /* CNAME with (child of) TLD starting with + "rpz-" in target, SOA, NS, DNAME and + DNSSEC-related records. */ + RPZ_LOCAL_DATA_ACTION, /* anything else */ +}; + +/** + * Create policy from RR and add to this RPZ. + * @param r: the rpz to add the policy to. + * @param aznamelen: the length of the auth-zone name + * @param dname: dname of the RR + * @param dnamelen: length of the dname + * @param rr_type: RR type of the RR + * @param rr_class: RR class of the RR + * @param rr_ttl: TTL of the RR + * @param rdatawl: rdata of the RR, prepended with the rdata size + * @param rdatalen: length if the RR, including the prepended rdata size + * @param rr: the complete RR, for logging purposes + * @param rr_len: the length of the complete RR + */ +void rpz_insert_rr(struct rpz* r, 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); + +/** + * Delete policy matching RR, used for IXFR. + * @param r: the rpz to add the policy to. + * @param aznamelen: the length of the auth-zone name + * @param dname: dname of the RR + * @param dnamelen: length of the dname + * @param rr_type: RR type of the RR + * @param rr_class: RR class of the RR + * @param rdatawl: rdata of the RR, prepended with the rdata size + * @param rdatalen: length if the RR, including the prepended rdata size + * @param rr: the complete RR, for logging purposes + * @param rr_len: the length of the complete RR + */ +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, uint8_t* rr, size_t rr_len); + +/** + * Walk over the RPZ zones to find and apply a QNAME trigger policy. + * @param az: auth_zones struct, containing first RPZ item and RPZ lock + * @param env: module env + * @param qinfo: qinfo containing qname and qtype + * @param edns: edns data + * @param buf: buffer to write answer to + * @param temp: scratchpad + * @param repinfo: reply info + * @param taglist: taglist to lookup. + * @param taglen: lenth of taglist. + * @return: 1 if client answer is ready, 0 to continue resolving + */ +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); + +/** + * Delete RPZ + * @param r: RPZ struct to delete + */ +void rpz_delete(struct rpz* r); + +/** + * Clear local-zones in RPZ, used after reloading file or AXFR/HTTP transfer. + * @param r: RPZ to use + */ +int rpz_clear_lz(struct rpz* r); + +/** + * Create RPZ. RPZ must be added to linked list after creation. + * @return: the newly created RPZ + */ +struct rpz* rpz_create(struct config_auth* p); + +#endif /* SERVICES_RPZ_H */