diff --git a/man/dnsmasq.8 b/man/dnsmasq.8 index 80b3a38..32bdeff 100644 --- a/man/dnsmasq.8 +++ b/man/dnsmasq.8 @@ -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=[,.......] +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 diff --git a/src/config.h b/src/config.h index d137d90..e722e98 100644 --- a/src/config.h +++ b/src/config.h @@ -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 */ diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 704bc63..e455c3f 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -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; diff --git a/src/dnssec.c b/src/dnssec.c index a3e60ba..ed2f53f 100644 --- a/src/dnssec.c +++ b/src/dnssec.c @@ -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 limit[LIMIT_SIG_FAIL], j = 0; j 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++; diff --git a/src/forward.c b/src/forward.c index 02a4630..32f37e4 100644 --- a/src/forward.c +++ b/src/forward.c @@ -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 diff --git a/src/option.c b/src/option.c index 1c20766..f4ff7c0 100644 --- a/src/option.c +++ b/src/option.c @@ -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, "", gettext_noop("Timestamp file to verify system clock for DNSSEC"), NULL }, + { LOPT_DNSSEC_LIMITS, ARG_ONE, ",..", gettext_noop("Set resource limits for DNSSEC validation"), NULL }, { LOPT_RA_PARAM, ARG_DUP, ",[mtu:||off,][,][,]", 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. */