Parameterise work limits for DNSSEC validation.

Signed-off-by: DL6ER <dl6er@dl6er.de>
This commit is contained in:
Simon Kelley 2024-01-02 21:43:04 +00:00 committed by DL6ER
parent 2e0d8fff72
commit a133029e4c
No known key found for this signature in database
GPG Key ID: 00135ACBD90B28DD
8 changed files with 94 additions and 52 deletions

View File

@ -850,6 +850,12 @@ void cache_end_insert(void)
if (daemon->pipe_to_parent != -1)
{
ssize_t m = -1;
#ifdef HAVE_DNSSEC
/* Sneak out possibly updated crypto HWM. */
m = -1 - daemon->metrics[METRIC_CRYTO_HWM];
#endif
read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
}
@ -875,8 +881,13 @@ int cache_recv_insert(time_t now, int fd)
if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
return 0;
if (m == -1)
if (m < 0)
{
#ifdef HAVE_DNSSEC
/* Sneak in possibly updated crypto HWM. */
if ((-m - 1) > daemon->metrics[METRIC_CRYTO_HWM])
daemon->metrics[METRIC_CRYTO_HWM] = -m - 1;
#endif
cache_end_insert();
return 1;
}
@ -1941,6 +1952,9 @@ void dump_cache(time_t now)
#ifdef HAVE_AUTH
my_syslog(LOG_INFO, _("queries for authoritative zones %u"), daemon->metrics[METRIC_DNS_AUTH_ANSWERED]);
#endif
#ifdef HAVE_DNSSEC
my_syslog(LOG_INFO, _("DNSSEC per-query crypto HWM %u"), daemon->metrics[METRIC_CRYTO_HWM]);
#endif
blockdata_report();
my_syslog(LOG_INFO, _("child processes for TCP requests: in use %zu, highest since last SIGUSR1 %zu, max allowed %zu."),

View File

@ -23,6 +23,11 @@
#define SAFE_PKTSZ 1232 /* "go anywhere" UDP packet size, see https://dnsflagday.net/2020/ */
#define KEYBLOCK_LEN 40 /* choose to minimise fragmentation when storing DNSSEC keys */
#define DNSSEC_WORK 50 /* Max number of queries to validate one question */
#define LIMIT_KEY_FAIL 15 /* Number of keys that can fail DS validate in one an answer. */
#define LIMIT_DS_FAIL 5 /* Number of DS records that can fail to validate a key in one answer */
#define LIMIT_SIG_FAIL 10 /* Number of signature that can fail to validate in one answer */
#define LIMIT_CRYPTO 40 /* max no. of crypto operations to validate one a query. */
#define LIMIT_NSEC3_ITERS 150 /* Max. number if iterations allow in NSEC3 record. */
#define TIMEOUT 10 /* drop UDP queries after TIMEOUT seconds */
#define SMALL_PORT_RANGE 30 /* If DNS port range is smaller than this, use different allocation. */
#define FORWARD_TEST 1000 /* try all servers every 1000 queries */

View File

@ -765,6 +765,7 @@ struct dyndir {
#define DNSSEC_FAIL_NOKEY 0x0100 /* no DNSKEY */
#define DNSSEC_FAIL_NSEC3_ITERS 0x0200 /* too many iterations in NSEC3 */
#define DNSSEC_FAIL_BADPACKET 0x0400 /* bad packet */
#define DNSSEC_FAIL_WORK 0x0800 /* too much crypto */
#define STAT_ISEQUAL(a, b) (((a) & 0xffff0000) == (b))
@ -1248,6 +1249,7 @@ extern struct daemon {
int rr_status_sz;
int dnssec_no_time_check;
int back_to_the_future;
int limit_key_fail, limit_ds_fail, limit_sig_fail, limit_crypto, limit_work, limit_nsec3_iters;
#endif
struct frec *frec_list;
struct frec_src *free_frec_src;

View File

@ -424,6 +424,17 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
return 1;
}
int dec_counter(int *counter, char *message)
{
if ((*counter)-- == 0)
{
my_syslog(LOG_WARNING, "limit exceeded: %s", message ? message : "crypto work");
return 1;
}
return 0;
}
/* Validate a single RRset (class, type, name) in the supplied DNS reply
Return code:
STAT_SECURE if it validates.
@ -468,7 +479,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
rrsetidx = sort_rrset(header, plen, rr_desc, rrsetidx, rrset, daemon->workspacename, keyname);
/* Now try all the sigs to try and find one which validates */
for (sig_fail_cnt = 0, j = 0; j <sigidx; j++)
for (sig_fail_cnt = daemon->limit_sig_fail, j = 0; j <sigidx; j++)
{
unsigned char *psav, *sig, *digest;
int i, wire_len, sig_len;
@ -656,10 +667,13 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
if (key)
{
if (algo_in == algo && keytag_in == key_tag)
(*validate_counter)++;
if (verify(key, keylen, sig, sig_len, digest, hash->digest_size, algo))
return STAT_SECURE;
{
if (dec_counter(validate_counter, NULL))
return STAT_ABANDONED;
if (verify(key, keylen, sig, sig_len, digest, hash->digest_size, algo))
return STAT_SECURE;
}
}
else
{
@ -669,21 +683,17 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
crecp->addr.key.keytag == key_tag &&
crecp->uid == (unsigned int)class)
{
(*validate_counter)++;
if (dec_counter(validate_counter, NULL))
return STAT_ABANDONED;
if (verify(crecp->addr.key.keydata, crecp->addr.key.keylen, sig, sig_len, digest, hash->digest_size, algo))
return (labels < name_labels) ? STAT_SECURE_WILDCARD : STAT_SECURE;
/* An attacker can waste a lot of our CPU by setting up a giant DNSKEY RRSET full of failing
keys, all of which we have to try. Since many failing keys is not likely for
a legitimate domain, set a limit on how many can fail. */
sig_fail_cnt++;
if (sig_fail_cnt > 10) /* TODO */
{
my_syslog(LOG_ERR, "sig_fail_cnt");
return STAT_ABANDONED;
}
if (dec_counter(&sig_fail_cnt, "SIG fail"))
return STAT_ABANDONED;
}
}
}
@ -733,7 +743,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
}
/* NOTE, we need to find ONE DNSKEY which matches the DS */
for (key_fail_cnt = 0, valid = 0, j = ntohs(header->ancount); j != 0 && !valid; j--)
for (key_fail_cnt = daemon->limit_key_fail, valid = 0, j = ntohs(header->ancount); j != 0 && !valid; j--)
{
/* Ensure we have type, class TTL and length */
if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
@ -781,8 +791,8 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
/* No zone key flag or malloc failure */
if (!key)
continue;
for (ds_fail_cnt = 0, recp1 = crecp; recp1; recp1 = cache_find_by_name(recp1, name, now, F_DS))
for (ds_fail_cnt = daemon->limit_ds_fail, recp1 = crecp; recp1; recp1 = cache_find_by_name(recp1, name, now, F_DS))
{
void *ctx;
unsigned char *digest, *ds_digest;
@ -801,6 +811,10 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
else
failflags &= ~DNSSEC_FAIL_NODSSUP;
/* computing a hash is a unit of crypto work. */
if (dec_counter(validate_counter, NULL))
return STAT_ABANDONED;
if (!hash_init(hash, &ctx, &digest))
continue;
@ -811,7 +825,6 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
hash->update(ctx, (unsigned int)wire_len, (unsigned char *)name);
hash->update(ctx, (unsigned int)rdlen, psave);
hash->digest(ctx, hash->digest_size, digest);
(*validate_counter)++; /* computing a hash is a unit of crypto work. */
from_wire(name);
@ -822,13 +835,8 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
if (memcmp(ds_digest, digest, recp1->addr.ds.keylen) != 0)
{
/* limit CPU exhaustion attack from large DS x KEY cross-product. */
ds_fail_cnt++;
if (ds_fail_cnt > 5) /* TODO */
{
my_syslog(LOG_ERR, "ds_fail_cnt");
return STAT_ABANDONED;
}
if (dec_counter(&ds_fail_cnt, "DS fail"))
return STAT_ABANDONED;
}
else if (explore_rrset(header, plen, class, T_DNSKEY, name, keyname, &sigcnt, &rrcnt) &&
rrcnt != 0)
@ -858,13 +866,8 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
blockdata_free(key);
/* limit CPU exhaustion attack from large DS x KEY cross-product. */
key_fail_cnt++;
if (key_fail_cnt > 15) /* TODO */
{
my_syslog(LOG_ERR, "key_fail_cnt");
return STAT_ABANDONED;
}
if (dec_counter(&key_fail_cnt, "KEY fail"))
return STAT_ABANDONED;
}
if (valid)
@ -1511,7 +1514,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
GETSHORT (iterations, p);
/* Upper-bound iterations, to avoid DoS. RFC 9276 refers. */
if (iterations > 150)
if (iterations > daemon->limit_nsec3_iters)
return DNSSEC_FAIL_NSEC3_ITERS;
salt_len = *p++;
@ -1558,7 +1561,9 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
nsecs[i] = nsec3p;
}
(*validate_counter)++;
if (dec_counter(validate_counter, NULL))
return DNSSEC_FAIL_WORK;
if ((digest_len = hash_name(name, &digest, hash, salt, salt_len, iterations)) == 0)
return DNSSEC_FAIL_NONSEC;
@ -1578,7 +1583,9 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
if (wildname && hostname_isequal(closest_encloser, wildname))
break;
(*validate_counter)++;
if (dec_counter(validate_counter, NULL))
return DNSSEC_FAIL_WORK;
if ((digest_len = hash_name(closest_encloser, &digest, hash, salt, salt_len, iterations)) == 0)
return DNSSEC_FAIL_NONSEC;
@ -1607,7 +1614,9 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
return DNSSEC_FAIL_NONSEC;
/* Look for NSEC3 that proves the non-existence of the next-closest encloser */
(*validate_counter)++;
if (dec_counter(validate_counter, NULL))
return DNSSEC_FAIL_WORK;
if ((digest_len = hash_name(next_closest, &digest, hash, salt, salt_len, iterations)) == 0)
return DNSSEC_FAIL_NONSEC;
@ -1623,7 +1632,9 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
wildcard--;
*wildcard = '*';
(*validate_counter)++;
if (dec_counter(validate_counter, NULL))
return DNSSEC_FAIL_WORK;
if ((digest_len = hash_name(wildcard, &digest, hash, salt, salt_len, iterations)) == 0)
return DNSSEC_FAIL_NONSEC;
@ -2074,7 +2085,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
we'll return BOGUS then. */
if (STAT_ISEQUAL(rc, STAT_SECURE_WILDCARD) &&
((rc_nsec = prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL, validate_counter))) != 0)
return STAT_BOGUS | rc_nsec;
return (rc_nsec & DNSSEC_FAIL_WORK) ? STAT_ABANDONED : (STAT_BOGUS | rc_nsec);
rc = STAT_SECURE;
}
@ -2101,6 +2112,9 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
the answer is in an unsigned zone, or there's a NSEC records. */
if ((rc_nsec = prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons, nsec_ttl, validate_counter)) != 0)
{
if (rc_nsec & DNSSEC_FAIL_WORK)
return STAT_ABANDONED;
/* Empty DS without NSECS */
if (qtype == T_DS)
return STAT_BOGUS | rc_nsec;

View File

@ -17,8 +17,6 @@
#include "dnsmasq.h"
#include "../dnsmasq_interface.h"
static int vchwm = 0; /* TODO */
static struct frec *get_new_frec(time_t now, struct server *serv, int force);
static struct frec *lookup_frec(unsigned short id, int fd, void *hash, int *firstp, int *lastp);
static struct frec *lookup_frec_by_query(void *hash, unsigned int flags, unsigned int flagmask);
@ -346,8 +344,8 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
if (ad_reqd)
forward->flags |= FREC_AD_QUESTION;
#ifdef HAVE_DNSSEC
forward->work_counter = DNSSEC_WORK;
forward->validate_counter = 0;
forward->work_counter = daemon->limit_work;
forward->validate_counter = daemon->limit_crypto;
if (do_bit)
forward->flags |= FREC_DO_QUESTION;
#endif
@ -1398,10 +1396,10 @@ static void return_reply(time_t now, struct frec *forward, struct dns_header *he
}
}
if (forward->validate_counter > vchwm)
vchwm = forward->validate_counter;
if ((daemon->limit_crypto - forward->validate_counter) > daemon->metrics[METRIC_CRYTO_HWM])
daemon->metrics[METRIC_CRYTO_HWM] = daemon->limit_crypto - forward->validate_counter;
if (extract_request(header, n, daemon->namebuff, NULL))
my_syslog(LOG_INFO, "Validate_counter %s is %d, HWM is %d", daemon->namebuff, forward->validate_counter, vchwm); /* TODO */
my_syslog(LOG_INFO, "Validate_counter %s is %d", daemon->namebuff, daemon->limit_crypto - forward->validate_counter); /* TODO */
#endif
if (option_bool(OPT_NO_REBIND))
@ -2542,8 +2540,8 @@ unsigned char *tcp_request(int confd, time_t now,
#ifdef HAVE_DNSSEC
if (option_bool(OPT_DNSSEC_VALID) && !checking_disabled && (master->flags & SERV_DO_DNSSEC))
{
int keycount = DNSSEC_WORK; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
int validatecount = 0; /* How many validations we did */
int keycount = daemon->limit_work; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
int validatecount = daemon->limit_crypto;
int status = tcp_key_recurse(now, STAT_OK, header, m, 0, daemon->namebuff, daemon->keyname,
serv, have_mark, mark, &keycount, &validatecount);
char *result, *domain = "result";
@ -2572,10 +2570,10 @@ unsigned char *tcp_request(int confd, time_t now,
log_query(F_SECSTAT, domain, &a, result, 0);
if (validatecount > vchwm)
vchwm = validatecount;
if ((daemon->limit_crypto - validatecount) > daemon->metrics[METRIC_CRYTO_HWM])
daemon->metrics[METRIC_CRYTO_HWM] = daemon->limit_crypto - validatecount;
if (extract_request(header, m, daemon->namebuff, NULL))
my_syslog(LOG_INFO, "Validate_counter %s is %d, HWM is %d", daemon->namebuff, validatecount, vchwm); /* TODO */
my_syslog(LOG_INFO, "Validate_counter %s is %d", daemon->namebuff, daemon->limit_crypto - validatecount); /* TODO */
}
#endif

View File

@ -24,6 +24,7 @@ const char * metric_names[] = {
"dns_local_answered",
"dns_stale_answered",
"dns_unanswered",
"max_crypto_use",
"bootp",
"pxe",
"dhcp_ack",

View File

@ -23,6 +23,7 @@ enum {
METRIC_DNS_LOCAL_ANSWERED,
METRIC_DNS_STALE_ANSWERED,
METRIC_DNS_UNANSWERED_QUERY,
METRIC_CRYTO_HWM,
METRIC_BOOTP,
METRIC_PXE,
METRIC_DHCPACK,

View File

@ -5873,7 +5873,14 @@ void read_opts(int argc, char **argv, char *compile_opts)
daemon->randport_limit = 1;
daemon->host_index = SRC_AH;
daemon->max_procs = MAX_PROCS;
daemon->max_procs_used = 0;
#ifdef HAVE_DNSSEC
daemon->limit_key_fail = LIMIT_KEY_FAIL;
daemon->limit_ds_fail = LIMIT_DS_FAIL;
daemon->limit_sig_fail = LIMIT_SIG_FAIL;
daemon->limit_crypto = LIMIT_CRYPTO;
daemon->limit_work = DNSSEC_WORK;
daemon->limit_nsec3_iters = LIMIT_NSEC3_ITERS;
#endif
/* See comment above make_servers(). Optimises server-read code. */
mark_servers(0);