/* $NetBSD: acl.c,v 1.8 2022/09/23 12:15:29 christos Exp $ */
/*
* 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 <inttypes.h>
#include <stdbool.h>
#include <isc/mem.h>
#include <isc/once.h>
#include <isc/string.h>
#include <isc/util.h>
#include <dns/acl.h>
#include <dns/iptable.h>
/*
* Create a new ACL, including an IP table and an array with room
* for 'n' ACL elements. The elements are uninitialized and the
* length is 0.
*/
isc_result_t
dns_acl_create(isc_mem_t *mctx, int n, dns_acl_t **target) {
isc_result_t result;
dns_acl_t *acl;
/*
* Work around silly limitation of isc_mem_get().
*/
if (n == 0) {
n = 1;
}
acl = isc_mem_get(mctx, sizeof(*acl));
acl->mctx = NULL;
isc_mem_attach(mctx, &acl->mctx);
acl->name = NULL;
isc_refcount_init(&acl->refcount, 1);
result = dns_iptable_create(mctx, &acl->iptable);
if (result != ISC_R_SUCCESS) {
isc_mem_put(mctx, acl, sizeof(*acl));
return (result);
}
acl->elements = NULL;
acl->alloc = 0;
acl->length = 0;
acl->has_negatives = false;
ISC_LINK_INIT(acl, nextincache);
/*
* Must set magic early because we use dns_acl_detach() to clean up.
*/
acl->magic = DNS_ACL_MAGIC;
acl->elements = isc_mem_get(mctx, n * sizeof(dns_aclelement_t));
acl->alloc = n;
memset(acl->elements, 0, n * sizeof(dns_aclelement_t));
*target = acl;
return (ISC_R_SUCCESS);
}
/*
* Create a new ACL and initialize it with the value "any" or "none",
* depending on the value of the "neg" parameter.
* "any" is a positive iptable entry with bit length 0.
* "none" is the same as "!any".
*/
static isc_result_t
dns_acl_anyornone(isc_mem_t *mctx, bool neg, dns_acl_t **target) {
isc_result_t result;
dns_acl_t *acl = NULL;
result = dns_acl_create(mctx, 0, &acl);
if (result != ISC_R_SUCCESS) {
return (result);
}
result = dns_iptable_addprefix(acl->iptable, NULL, 0, !neg);
if (result != ISC_R_SUCCESS) {
dns_acl_detach(&acl);
return (result);
}
*target = acl;
return (result);
}
/*
* Create a new ACL that matches everything.
*/
isc_result_t
dns_acl_any(isc_mem_t *mctx, dns_acl_t **target) {
return (dns_acl_anyornone(mctx, false, target));
}
/*
* Create a new ACL that matches nothing.
*/
isc_result_t
dns_acl_none(isc_mem_t *mctx, dns_acl_t **target) {
return (dns_acl_anyornone(mctx, true, target));
}
/*
* If pos is true, test whether acl is set to "{ any; }"
* If pos is false, test whether acl is set to "{ none; }"
*/
static bool
dns_acl_isanyornone(dns_acl_t *acl, bool pos) {
/* Should never happen but let's be safe */
if (acl == NULL || acl->iptable == NULL ||
acl->iptable->radix == NULL || acl->iptable->radix->head == NULL ||
acl->iptable->radix->head->prefix == NULL)
{
return (false);
}
if (acl->length != 0 || dns_acl_node_count(acl) != 1) {
return (false);
}
if (acl->iptable->radix->head->prefix->bitlen == 0 &&
acl->iptable->radix->head->data[0] != NULL &&
acl->iptable->radix->head->data[0] ==
acl->iptable->radix->head->data[1] &&
*(bool *)(acl->iptable->radix->head->data[0]) == pos)
{
return (true);
}
return (false); /* All others */
}
/*
* Test whether acl is set to "{ any; }"
*/
bool
dns_acl_isany(dns_acl_t *acl) {
return (dns_acl_isanyornone(acl, true));
}
/*
* Test whether acl is set to "{ none; }"
*/
bool
dns_acl_isnone(dns_acl_t *acl) {
return (dns_acl_isanyornone(acl, false));
}
/*
* Determine whether a given address or signer matches a given ACL.
* For a match with a positive ACL element or iptable radix entry,
* return with a positive value in match; for a match with a negated ACL
* element or radix entry, return with a negative value in match.
*/
isc_result_t
dns_acl_match(const isc_netaddr_t *reqaddr, const dns_name_t *reqsigner,
const dns_acl_t *acl, const dns_aclenv_t *env, int *match,
const dns_aclelement_t **matchelt) {
uint16_t bitlen;
isc_prefix_t pfx;
isc_radix_node_t *node = NULL;
const isc_netaddr_t *addr = reqaddr;
isc_netaddr_t v4addr;
isc_result_t result;
int match_num = -1;
unsigned int i;
REQUIRE(reqaddr != NULL);
REQUIRE(matchelt == NULL || *matchelt == NULL);
if (env != NULL && env->match_mapped && addr->family == AF_INET6 &&
IN6_IS_ADDR_V4MAPPED(&addr->type.in6))
{
isc_netaddr_fromv4mapped(&v4addr, addr);
addr = &v4addr;
}
/* Always match with host addresses. */
bitlen = (addr->family == AF_INET6) ? 128 : 32;
NETADDR_TO_PREFIX_T(addr, pfx, bitlen);
/* Assume no match. */
*match = 0;
/* Search radix. */
result = isc_radix_search(acl->iptable->radix, &node, &pfx);
/* Found a match. */
if (result == ISC_R_SUCCESS && node != NULL) {
int fam = ISC_RADIX_FAMILY(&pfx);
match_num = node->node_num[fam];
if (*(bool *)node->data[fam]) {
*match = match_num;
} else {
*match = -match_num;
}
}
isc_refcount_destroy(&pfx.refcount);
/* Now search non-radix elements for a match with a lower node_num. */
for (i = 0; i < acl->length; i++) {
dns_aclelement_t *e = &acl->elements[i];
/* Already found a better match? */
if (match_num != -1 && match_num < e->node_num) {
break;
}
if (dns_aclelement_match(reqaddr, reqsigner, e, env, matchelt))
{
if (match_num == -1 || e->node_num < match_num) {
if (e->negative) {
*match = -e->node_num;
} else {
*match = e->node_num;
}
}
break;
}
}
return (ISC_R_SUCCESS);
}
/*
* Merge the contents of one ACL into another. Call dns_iptable_merge()
* for the IP tables, then concatenate the element arrays.
*
* If pos is set to false, then the nested ACL is to be negated. This
* means reverse the sense of each *positive* element or IP table node,
* but leave negatives alone, so as to prevent a double-negative causing
* an unexpected positive match in the parent ACL.
*/
isc_result_t
dns_acl_merge(dns_acl_t *dest, dns_acl_t *source, bool pos) {
isc_result_t result;
unsigned int newalloc, nelem, i;
int max_node = 0, nodes;
/* Resize the element array if needed. */
if (dest->length + source->length > dest->alloc) {
void *newmem;
newalloc = dest->alloc + source->alloc;
if (newalloc < 4) {
newalloc = 4;
}
newmem = isc_mem_get(dest->mctx,
newalloc * sizeof(dns_aclelement_t));
/* Zero. */
memset(newmem, 0, newalloc * sizeof(dns_aclelement_t));
/* Copy in the original elements */
memmove(newmem, dest->elements,
dest->length * sizeof(dns_aclelement_t));
/* Release the memory for the old elements array */
isc_mem_put(dest->mctx, dest->elements,
dest->alloc * sizeof(dns_aclelement_t));
dest->elements = newmem;
dest->alloc = newalloc;
}
/*
* Now copy in the new elements, increasing their node_num
* values so as to keep the new ACL consistent. If we're
* negating, then negate positive elements, but keep negative
* elements the same for security reasons.
*/
nelem = dest->length;
dest->length += source->length;
for (i = 0; i < source->length; i++) {
if (source->elements[i].node_num > max_node) {
max_node = source->elements[i].node_num;
}
/* Copy type. */
dest->elements[nelem + i].type = source->elements[i].type;
/* Adjust node numbering. */
dest->elements[nelem + i].node_num =
source->elements[i].node_num + dns_acl_node_count(dest);
/* Duplicate nested acl. */
if (source->elements[i].type == dns_aclelementtype_nestedacl &&
source->elements[i].nestedacl != NULL)
{
dns_acl_attach(source->elements[i].nestedacl,
&dest->elements[nelem + i].nestedacl);
}
/* Duplicate key name. */
if (source->elements[i].type == dns_aclelementtype_keyname) {
dns_name_init(&dest->elements[nelem + i].keyname, NULL);
dns_name_dup(&source->elements[i].keyname, dest->mctx,
&dest->elements[nelem + i].keyname);
}
#if defined(HAVE_GEOIP2)
/* Duplicate GeoIP data */
if (source->elements[i].type == dns_aclelementtype_geoip) {
dest->elements[nelem + i].geoip_elem =
source->elements[i].geoip_elem;
}
#endif /* if defined(HAVE_GEOIP2) */
/* reverse sense of positives if this is a negative acl */
if (!pos && !source->elements[i].negative) {
dest->elements[nelem + i].negative = true;
} else {
dest->elements[nelem + i].negative =
source->elements[i].negative;
}
}
/*
* Merge the iptables. Make sure the destination ACL's
* node_count value is set correctly afterward.
*/
nodes = max_node + dns_acl_node_count(dest);
result = dns_iptable_merge(dest->iptable, source->iptable, pos);
if (result != ISC_R_SUCCESS) {
return (result);
}
if (nodes > dns_acl_node_count(dest)) {
dns_acl_node_count(dest) = nodes;
}
return (ISC_R_SUCCESS);
}
/*
* Like dns_acl_match, but matches against the single ACL element 'e'
* rather than a complete ACL, and returns true iff it matched.
*
* To determine whether the match was positive or negative, the
* caller should examine e->negative. Since the element 'e' may be
* a reference to a named ACL or a nested ACL, a matching element
* returned through 'matchelt' is not necessarily 'e' itself.
*/
bool
dns_aclelement_match(const isc_netaddr_t *reqaddr, const dns_name_t *reqsigner,
const dns_aclelement_t *e, const dns_aclenv_t *env,
const dns_aclelement_t **matchelt) {
dns_acl_t *inner = NULL;
int indirectmatch;
isc_result_t result;
switch (e->type) {
case dns_aclelementtype_keyname:
if (reqsigner != NULL && dns_name_equal(reqsigner, &e->keyname))
{
if (matchelt != NULL) {
*matchelt = e;
}
return (true);
} else {
return (false);
}
case dns_aclelementtype_nestedacl:
inner = e->nestedacl;
break;
case dns_aclelementtype_localhost:
if (env == NULL || env->localhost == NULL) {
return (false);
}
inner = env->localhost;
break;
case dns_aclelementtype_localnets:
if (env == NULL || env->localnets == NULL) {
return (false);
}
inner = env->localnets;
break;
#if defined(HAVE_GEOIP2)
case dns_aclelementtype_geoip:
if (env == NULL || env->geoip == NULL) {
return (false);
}
return (dns_geoip_match(reqaddr, env->geoip, &e->geoip_elem));
#endif /* if defined(HAVE_GEOIP2) */
default:
UNREACHABLE();
}
result = dns_acl_match(reqaddr, reqsigner, inner, env, &indirectmatch,
matchelt);
INSIST(result == ISC_R_SUCCESS);
/*
* Treat negative matches in indirect ACLs as "no match".
* That way, a negated indirect ACL will never become a
* surprise positive match through double negation.
* XXXDCL this should be documented.
*/
if (indirectmatch > 0) {
if (matchelt != NULL) {
*matchelt = e;
}
return (true);
}
/*
* A negative indirect match may have set *matchelt, but we don't
* want it set when we return.
*/
if (matchelt != NULL) {
*matchelt = NULL;
}
return (false);
}
void
dns_acl_attach(dns_acl_t *source, dns_acl_t **target) {
REQUIRE(DNS_ACL_VALID(source));
isc_refcount_increment(&source->refcount);
*target = source;
}
static void
destroy(dns_acl_t *dacl) {
unsigned int i;
INSIST(!ISC_LINK_LINKED(dacl, nextincache));
for (i = 0; i < dacl->length; i++) {
dns_aclelement_t *de = &dacl->elements[i];
if (de->type == dns_aclelementtype_keyname) {
dns_name_free(&de->keyname, dacl->mctx);
} else if (de->type == dns_aclelementtype_nestedacl) {
dns_acl_detach(&de->nestedacl);
}
}
if (dacl->elements != NULL) {
isc_mem_put(dacl->mctx, dacl->elements,
dacl->alloc * sizeof(dns_aclelement_t));
}
if (dacl->name != NULL) {
isc_mem_free(dacl->mctx, dacl->name);
}
if (dacl->iptable != NULL) {
dns_iptable_detach(&dacl->iptable);
}
isc_refcount_destroy(&dacl->refcount);
dacl->magic = 0;
isc_mem_putanddetach(&dacl->mctx, dacl, sizeof(*dacl));
}
void
dns_acl_detach(dns_acl_t **aclp) {
REQUIRE(aclp != NULL && DNS_ACL_VALID(*aclp));
dns_acl_t *acl = *aclp;
*aclp = NULL;
if (isc_refcount_decrement(&acl->refcount) == 1) {
destroy(acl);
}
}
static isc_once_t insecure_prefix_once = ISC_ONCE_INIT;
static isc_mutex_t insecure_prefix_lock;
static bool insecure_prefix_found;
static void
initialize_action(void) {
isc_mutex_init(&insecure_prefix_lock);
}
/*
* Called via isc_radix_process() to find IP table nodes that are
* insecure.
*/
static void
is_insecure(isc_prefix_t *prefix, void **data) {
/*
* If all nonexistent or negative then this node is secure.
*/
if ((data[0] == NULL || !*(bool *)data[0]) &&
(data[1] == NULL || !*(bool *)data[1]))
{
return;
}
/*
* If a loopback address found and the other family
* entry doesn't exist or is negative, return.
*/
if (prefix->bitlen == 32 &&
htonl(prefix->add.sin.s_addr) == INADDR_LOOPBACK &&
(data[1] == NULL || !*(bool *)data[1]))
{
return;
}
if (prefix->bitlen == 128 && IN6_IS_ADDR_LOOPBACK(&prefix->add.sin6) &&
(data[0] == NULL || !*(bool *)data[0]))
{
return;
}
/* Non-negated, non-loopback */
insecure_prefix_found = true; /* LOCKED */
return;
}
/*
* Return true iff the acl 'a' is considered insecure, that is,
* if it contains IP addresses other than those of the local host.
* This is intended for applications such as printing warning
* messages for suspect ACLs; it is not intended for making access
* control decisions. We make no guarantee that an ACL for which
* this function returns false is safe.
*/
bool
dns_acl_isinsecure(const dns_acl_t *a) {
unsigned int i;
bool insecure;
RUNTIME_CHECK(isc_once_do(&insecure_prefix_once, initialize_action) ==
ISC_R_SUCCESS);
/*
* Walk radix tree to find out if there are any non-negated,
* non-loopback prefixes.
*/
LOCK(&insecure_prefix_lock);
insecure_prefix_found = false;
isc_radix_process(a->iptable->radix, is_insecure);
insecure = insecure_prefix_found;
UNLOCK(&insecure_prefix_lock);
if (insecure) {
return (true);
}
/* Now check non-radix elements */
for (i = 0; i < a->length; i++) {
dns_aclelement_t *e = &a->elements[i];
/* A negated match can never be insecure. */
if (e->negative) {
continue;
}
switch (e->type) {
case dns_aclelementtype_keyname:
case dns_aclelementtype_localhost:
continue;
case dns_aclelementtype_nestedacl:
if (dns_acl_isinsecure(e->nestedacl)) {
return (true);
}
continue;
#if defined(HAVE_GEOIP2)
case dns_aclelementtype_geoip:
#endif /* if defined(HAVE_GEOIP2) */
case dns_aclelementtype_localnets:
return (true);
default:
UNREACHABLE();
}
}
/* No insecure elements were found. */
return (false);
}
/*%
* Check whether an address/signer is allowed by a given acl/aclenv.
*/
bool
dns_acl_allowed(isc_netaddr_t *addr, const dns_name_t *signer, dns_acl_t *acl,
dns_aclenv_t *aclenv) {
int match;
isc_result_t result;
if (acl == NULL) {
return (true);
}
result = dns_acl_match(addr, signer, acl, aclenv, &match, NULL);
if (result == ISC_R_SUCCESS && match > 0) {
return (true);
}
return (false);
}
/*
* Initialize ACL environment, setting up localhost and localnets ACLs
*/
isc_result_t
dns_aclenv_init(isc_mem_t *mctx, dns_aclenv_t *env) {
isc_result_t result;
env->localhost = NULL;
env->localnets = NULL;
result = dns_acl_create(mctx, 0, &env->localhost);
if (result != ISC_R_SUCCESS) {
goto cleanup_nothing;
}
result = dns_acl_create(mctx, 0, &env->localnets);
if (result != ISC_R_SUCCESS) {
goto cleanup_localhost;
}
env->match_mapped = false;
#if defined(HAVE_GEOIP2)
env->geoip = NULL;
#endif /* if defined(HAVE_GEOIP2) */
return (ISC_R_SUCCESS);
cleanup_localhost:
dns_acl_detach(&env->localhost);
cleanup_nothing:
return (result);
}
void
dns_aclenv_copy(dns_aclenv_t *t, dns_aclenv_t *s) {
dns_acl_detach(&t->localhost);
dns_acl_attach(s->localhost, &t->localhost);
dns_acl_detach(&t->localnets);
dns_acl_attach(s->localnets, &t->localnets);
t->match_mapped = s->match_mapped;
#if defined(HAVE_GEOIP2)
t->geoip = s->geoip;
#endif /* if defined(HAVE_GEOIP2) */
}
void
dns_aclenv_destroy(dns_aclenv_t *env) {
if (env->localhost != NULL) {
dns_acl_detach(&env->localhost);
}
if (env->localnets != NULL) {
dns_acl_detach(&env->localnets);
}
}