unbound/services/rpz.c

443 lines
12 KiB
C
Raw Normal View History

2019-04-05 11:38:43 -04:00
/*
* 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;
}