Add --dnssec-limits option.

This commit is contained in:
Simon Kelley
2024-01-07 22:47:30 +00:00
parent 39de57499e
commit 3ae7f1ab0d
6 changed files with 61 additions and 25 deletions

View File

@@ -931,6 +931,15 @@ Authenticated Data bit correctly in all cases is not technically possible. If th
when using this option, then the cache should be disabled using --cache-size=0. In most cases, enabling DNSSEC validation
within dnsmasq is a better option. See --dnssec for details.
.TP
.B --dnssec-limits=<limit>[,<limit>.......]
Override the default resource limits applied to DNSSEC validation. Cryptographic operations are expensive and crafted domains
can DoS a DNSSEC validator by forcing it to do hundreds of thousands of such operations. To avoid this, the dnsmasq validation code
applies limits on how much work will be expended in validation. If any of the limits are exceeded, the validation will fail and the
domain treated as BOGUS. There are four limits, in order(default values in parens): number a signature validation fails per RRset(20), number of signature validations and
hash computations per query(200), number of sub-queries to fetch DS and DNSKEY RRsets per query(40), and the number of iterations in a NSEC3 record(150).
The maximum values reached during validation are stored, and dumped as part of the stats generated by SIGUSR1. Supplying a limit value of 0 leaves the default in place, so
\fB--dnssec-limits=0,0,20\fP sets the number of sub-queries to 20 whilst leaving the other limits at default values.
.TP
.B --dnssec-debug
Set debugging mode for the DNSSEC validation, set the Checking Disabled bit on upstream queries,
and don't convert replies which do not validate to responses with

View File

@@ -22,10 +22,10 @@
#define EDNS_PKTSZ 1232 /* default max EDNS.0 UDP packet from from /dnsflagday.net/2020 */
#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_SIG_FAIL 20 /* Number of signature that can fail to validate in one answer */
#define LIMIT_CRYPTO 200 /* max no. of crypto operations to validate one query. */
#define LIMIT_NSEC3_ITERS 150 /* Max. number if iterations allowed in NSEC3 record. */
#define DNSSEC_LIMIT_WORK 40 /* Max number of queries to validate one question */
#define DNSSEC_LIMIT_SIG_FAIL 20 /* Number of signature that can fail to validate in one answer */
#define DNSSEC_LIMIT_CRYPTO 200 /* max no. of crypto operations to validate one query. */
#define DNSSEC_LIMIT_NSEC3_ITERS 150 /* Max. number if iterations allowed 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 50 /* try all servers every 50 queries */

View File

@@ -834,6 +834,12 @@ struct frec {
#define LEASE_HAVE_HWADDR 128 /* Have set hwaddress */
#define LEASE_EXP_CHANGED 256 /* Lease expiry time changed */
#define LIMIT_SIG_FAIL 0
#define LIMIT_CRYPTO 1
#define LIMIT_WORK 2
#define LIMIT_NSEC3_ITERS 3
#define LIMIT_MAX 4
struct dhcp_lease {
int clid_len; /* length of client identifier */
unsigned char *clid; /* clientid */
@@ -1243,7 +1249,7 @@ extern struct daemon {
int rr_status_sz;
int dnssec_no_time_check;
int back_to_the_future;
int limit_sig_fail, limit_crypto, limit_work, limit_nsec3_iters;
int limit[LIMIT_MAX];
#endif
struct frec *frec_list;
struct frec_src *free_frec_src;

View File

@@ -479,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 = daemon->limit_sig_fail, j = 0; j <sigidx; j++)
for (sig_fail_cnt = daemon->limit[LIMIT_SIG_FAIL], j = 0; j <sigidx; j++)
{
unsigned char *psav, *sig, *digest;
int i, wire_len, sig_len;
@@ -692,8 +692,8 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
/* 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. */
if ((daemon->limit_sig_fail - (sig_fail_cnt + 1)) > (int)daemon->metrics[METRIC_SIG_FAIL_HWM])
daemon->metrics[METRIC_SIG_FAIL_HWM] = daemon->limit_sig_fail - (sig_fail_cnt + 1);
if ((daemon->limit[LIMIT_SIG_FAIL] - (sig_fail_cnt + 1)) > (int)daemon->metrics[METRIC_SIG_FAIL_HWM])
daemon->metrics[METRIC_SIG_FAIL_HWM] = daemon->limit[LIMIT_SIG_FAIL] - (sig_fail_cnt + 1);
if (dec_counter(&sig_fail_cnt, _("per-RRSet signature fails")))
return STAT_ABANDONED;
}
@@ -1532,7 +1532,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 > daemon->limit_nsec3_iters)
if (iterations > daemon->limit[LIMIT_NSEC3_ITERS])
return DNSSEC_FAIL_NSEC3_ITERS;
salt_len = *p++;

View File

@@ -338,8 +338,8 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
if (ad_reqd)
forward->flags |= FREC_AD_QUESTION;
#ifdef HAVE_DNSSEC
forward->work_counter = daemon->limit_work;
forward->validate_counter = daemon->limit_crypto;
forward->work_counter = daemon->limit[LIMIT_WORK];
forward->validate_counter = daemon->limit[LIMIT_CRYPTO];
if (do_bit)
forward->flags |= FREC_DO_QUESTION;
#endif
@@ -1358,11 +1358,11 @@ static void return_reply(time_t now, struct frec *forward, struct dns_header *he
}
}
if ((daemon->limit_crypto - forward->validate_counter) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit_crypto - forward->validate_counter;
if ((daemon->limit[LIMIT_CRYPTO] - forward->validate_counter) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - forward->validate_counter;
if ((daemon->limit_work - forward->work_counter) > (int)daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = daemon->limit_work - forward->work_counter;
if ((daemon->limit[LIMIT_WORK] - forward->work_counter) > (int)daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - forward->work_counter;
#endif
if (option_bool(OPT_NO_REBIND))
@@ -2420,8 +2420,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 = daemon->limit_work; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
int validatecount = daemon->limit_crypto;
int keycount = daemon->limit[LIMIT_WORK]; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
int validatecount = daemon->limit[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";
@@ -2450,11 +2450,11 @@ unsigned char *tcp_request(int confd, time_t now,
log_query(F_SECSTAT, domain, &a, result, 0);
if ((daemon->limit_crypto - validatecount) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit_crypto - validatecount;
if ((daemon->limit[LIMIT_CRYPTO] - validatecount) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - validatecount;
if ((daemon->limit_work - keycount) > (int)daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = daemon->limit_work - keycount;
if ((daemon->limit[LIMIT_WORK] - keycount) > (int)daemon->metrics[METRIC_WORK_HWM])
daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - keycount;
}
#endif

View File

@@ -191,6 +191,7 @@ struct myoption {
#define LOPT_NO_DHCP6 382
#define LOPT_NO_DHCP4 383
#define LOPT_MAX_PROCS 384
#define LOPT_DNSSEC_LIMITS 385
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@@ -364,6 +365,7 @@ static const struct myoption opts[] =
{ "dnssec-check-unsigned", 2, 0, LOPT_DNSSEC_CHECK },
{ "dnssec-no-timecheck", 0, 0, LOPT_DNSSEC_TIME },
{ "dnssec-timestamp", 1, 0, LOPT_DNSSEC_STAMP },
{ "dnssec-limits", 1, 0, LOPT_DNSSEC_LIMITS },
{ "dhcp-relay", 1, 0, LOPT_RELAY },
{ "ra-param", 1, 0, LOPT_RA_PARAM },
{ "quiet-dhcp", 0, 0, LOPT_QUIET_DHCP },
@@ -568,6 +570,7 @@ static struct {
{ LOPT_DNSSEC_CHECK, ARG_DUP, NULL, gettext_noop("Ensure answers without DNSSEC are in unsigned zones."), NULL },
{ LOPT_DNSSEC_TIME, OPT_DNSSEC_TIME, NULL, gettext_noop("Don't check DNSSEC signature timestamps until first cache-reload"), NULL },
{ LOPT_DNSSEC_STAMP, ARG_ONE, "<path>", gettext_noop("Timestamp file to verify system clock for DNSSEC"), NULL },
{ LOPT_DNSSEC_LIMITS, ARG_ONE, "<limit>,..", gettext_noop("Set resource limits for DNSSEC validation"), NULL },
{ LOPT_RA_PARAM, ARG_DUP, "<iface>,[mtu:<value>|<interface>|off,][<prio>,]<intval>[,<lifetime>]", gettext_noop("Set MTU, priority, resend-interval and router-lifetime"), NULL },
{ LOPT_QUIET_DHCP, OPT_QUIET_DHCP, NULL, gettext_noop("Do not log routine DHCP."), NULL },
{ LOPT_QUIET_DHCP6, OPT_QUIET_DHCP6, NULL, gettext_noop("Do not log routine DHCPv6."), NULL },
@@ -5258,6 +5261,24 @@ err:
}
#ifdef HAVE_DNSSEC
case LOPT_DNSSEC_LIMITS:
{
int lim, val;
for (lim = LIMIT_SIG_FAIL; arg && lim < LIMIT_MAX ; lim++, arg = comma)
{
comma = split(arg);
if (!atoi_check(arg, &val))
ret_err(gen_err);
if (val != 0)
daemon->limit[lim] = val;
}
break;
}
case LOPT_DNSSEC_STAMP: /* --dnssec-timestamp */
daemon->timestamp_file = opt_string_alloc(arg);
break;
@@ -5870,10 +5891,10 @@ void read_opts(int argc, char **argv, char *compile_opts)
daemon->host_index = SRC_AH;
daemon->max_procs = MAX_PROCS;
#ifdef HAVE_DNSSEC
daemon->limit_sig_fail = LIMIT_SIG_FAIL;
daemon->limit_crypto = LIMIT_CRYPTO;
daemon->limit_work = DNSSEC_WORK;
daemon->limit_nsec3_iters = LIMIT_NSEC3_ITERS;
daemon->limit[LIMIT_SIG_FAIL] = DNSSEC_LIMIT_SIG_FAIL;
daemon->limit[LIMIT_CRYPTO] = DNSSEC_LIMIT_CRYPTO;
daemon->limit[LIMIT_WORK] = DNSSEC_LIMIT_WORK;
daemon->limit[LIMIT_NSEC3_ITERS] = DNSSEC_LIMIT_NSEC3_ITERS;
#endif
/* See comment above make_servers(). Optimises server-read code. */