dnsmasq/SOURCES/dnsmasq-2.90-CVE-2023-50387...

2092 lines
77 KiB
Diff

commit b2298dec2a9d3c1e7fe495460c14b34b1cf655cf
Author: Tomas Korbar <tkorbar@redhat.com>
Date: Tue Feb 20 13:39:09 2024 +0100
Fix CVE 2023-50387 and CVE 2023-50868
Add limits on the resources used to do DNSSEC validation.
DNSSEC introduces a potential CPU DoS, because a crafted domain
can force a validator to a large number of cryptographic
operations whilst attempting to do validation. When using TCP
transport a DNSKEY RRset contain thousands of members and any
RRset can have thousands of signatures. The potential number
of signature validations to follow the RFC for validation
for one RRset is the cross product of the keys and signatures,
so millions. In practice, the actual numbers are much lower,
so attacks can be mitigated by limiting the amount of
cryptographic "work" to a much lower amount. The actual
limits are 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). These values are sensible, but there is, as yet,
no standardisation on the values for a "conforming" domain, so a
new option --dnssec-limit is provided should they need to be altered.
The algorithm to validate DS records has also been altered to reduce
the maximum work from cross product of the number of DS records and
number of DNSKEYs to the cross product of the number of DS records
and supported DS digest types. As the number of DS digest types
is in single figures, this reduces the exposure.
Credit is due to Elias Heftrig, Haya Schulmann, Niklas Vogel,
and Michael Waidner from the German National Research Center for
Applied Cybersecurity ATHENE for finding this vulnerability.
CVE 2023-50387 and CVE 2023-50868 apply.
Note that the is a security vulnerablity only when DNSSEC validation
is enabled.
diff --git a/man/dnsmasq.8 b/man/dnsmasq.8
index aaab1f5..8bb81b6 100644
--- a/man/dnsmasq.8
+++ b/man/dnsmasq.8
@@ -793,6 +793,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
diff --git a/src/cache.c b/src/cache.c
index 4dbfdb4..7dc7646 100644
--- a/src/cache.c
+++ b/src/cache.c
@@ -714,7 +714,18 @@ void cache_end_insert(void)
if (daemon->pipe_to_parent != -1)
{
ssize_t m = -1;
+
+ read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
+
+#ifdef HAVE_DNSSEC
+ /* Sneak out possibly updated crypto HWM values. */
+ m = daemon->metrics[METRIC_CRYPTO_HWM];
read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
+ m = daemon->metrics[METRIC_SIG_FAIL_HWM];
+ read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
+ m = daemon->metrics[METRIC_WORK_HWM];
+ read_write(daemon->pipe_to_parent, (unsigned char *)&m, sizeof(m), 0);
+#endif
}
new_chain = NULL;
@@ -733,7 +744,7 @@ int cache_recv_insert(time_t now, int fd)
cache_start_insert();
- while(1)
+ while (1)
{
if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
@@ -741,6 +752,21 @@ int cache_recv_insert(time_t now, int fd)
if (m == -1)
{
+#ifdef HAVE_DNSSEC
+ /* Sneak in possibly updated crypto HWM. */
+ if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
+ return 0;
+ if (m > daemon->metrics[METRIC_CRYPTO_HWM])
+ daemon->metrics[METRIC_CRYPTO_HWM] = m;
+ if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
+ return 0;
+ if (m > daemon->metrics[METRIC_SIG_FAIL_HWM])
+ daemon->metrics[METRIC_SIG_FAIL_HWM] = m;
+ if (!read_write(fd, (unsigned char *)&m, sizeof(m), 1))
+ return 0;
+ if (m > daemon->metrics[METRIC_WORK_HWM])
+ daemon->metrics[METRIC_WORK_HWM] = m;
+#endif
cache_end_insert();
return 1;
}
@@ -1681,6 +1707,11 @@ 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 subqueries HWM %u"), daemon->metrics[METRIC_WORK_HWM]);
+ my_syslog(LOG_INFO, _("DNSSEC per-query crypto work HWM %u"), daemon->metrics[METRIC_CRYPTO_HWM]);
+ my_syslog(LOG_INFO, _("DNSSEC per-RRSet signature fails HWM %u"), daemon->metrics[METRIC_SIG_FAIL_HWM]);
+#endif
blockdata_report();
diff --git a/src/config.h b/src/config.h
index 9a70036..388bdf0 100644
--- a/src/config.h
+++ b/src/config.h
@@ -22,7 +22,10 @@
#define EDNS_PKTSZ 1232 /* default max EDNS.0 UDP packet from from /dnsflagday.net/2020 */
#define SAFE_PKTSZ 1280 /* "go anywhere" UDP packet size */
#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 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 FORWARD_TEST 50 /* try all servers every 50 queries */
#define FORWARD_TIME 20 /* or 20 seconds */
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
index c62c3d0..6fa2593 100644
--- a/src/dnsmasq.h
+++ b/src/dnsmasq.h
@@ -663,15 +663,30 @@ struct hostsfile {
/* DNSSEC status values. */
-#define STAT_SECURE 1
-#define STAT_INSECURE 2
-#define STAT_BOGUS 3
-#define STAT_NEED_DS 4
-#define STAT_NEED_KEY 5
-#define STAT_TRUNCATED 6
-#define STAT_SECURE_WILDCARD 7
-#define STAT_OK 8
-#define STAT_ABANDONED 9
+#define STAT_SECURE 0x10000
+#define STAT_INSECURE 0x20000
+#define STAT_BOGUS 0x30000
+#define STAT_NEED_DS 0x40000
+#define STAT_NEED_KEY 0x50000
+#define STAT_TRUNCATED 0x60000
+#define STAT_SECURE_WILDCARD 0x70000
+#define STAT_OK 0x80000
+#define STAT_ABANDONED 0x90000
+
+#define DNSSEC_FAIL_NYV 0x0001 /* key not yet valid */
+#define DNSSEC_FAIL_EXP 0x0002 /* key expired */
+#define DNSSEC_FAIL_INDET 0x0004 /* indetermined */
+#define DNSSEC_FAIL_NOKEYSUP 0x0008 /* no supported key algo. */
+#define DNSSEC_FAIL_NOSIG 0x0010 /* No RRsigs */
+#define DNSSEC_FAIL_NOZONE 0x0020 /* No Zone bit set */
+#define DNSSEC_FAIL_NONSEC 0x0040 /* No NSEC */
+#define DNSSEC_FAIL_NODSSUP 0x0080 /* no supported DS algo. */
+#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))
#define FREC_NOREBIND 1
#define FREC_CHECKING_DISABLED 2
@@ -704,7 +719,7 @@ struct frec {
time_t time;
unsigned char *hash[HASH_SIZE];
#ifdef HAVE_DNSSEC
- int class, work_counter;
+ int class, work_counter, validate_counter;
struct blockdata *stash; /* Saved reply, whilst we validate */
size_t stash_len;
struct frec *dependent; /* Query awaiting internally-generated DNSKEY or DS query */
@@ -741,6 +756,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 */
@@ -1127,6 +1148,7 @@ extern struct daemon {
int rr_status_sz;
int dnssec_no_time_check;
int back_to_the_future;
+ int limit[LIMIT_MAX];
#endif
struct frec *frec_list;
struct frec_src *free_frec_src;
@@ -1280,10 +1302,12 @@ int in_zone(struct auth_zone *zone, char *name, char **cut);
/* dnssec.c */
size_t dnssec_generate_query(struct dns_header *header, unsigned char *end, char *name, int class, int type, int edns_pktsz);
-int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class);
-int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class);
+int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_count);
+int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_count);
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class,
- int check_unsigned, int *neganswer, int *nons, int *nsec_ttl);
+ int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_count);
int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen);
size_t filter_rrsigs(struct dns_header *header, size_t plen);
int setup_timestamp(void);
@@ -1680,7 +1704,7 @@ int do_poll(int timeout);
/* rrfilter.c */
size_t rrfilter(struct dns_header *header, size_t plen, int mode);
-u16 *rrfilter_desc(int type);
+short *rrfilter_desc(int type);
int expand_workspace(unsigned char ***wkspc, int *szp, int new);
/* edns0.c */
diff --git a/src/dnssec.c b/src/dnssec.c
index 3ee1e9e..e465bbd 100644
--- a/src/dnssec.c
+++ b/src/dnssec.c
@@ -1,5 +1,5 @@
/* dnssec.c is Copyright (c) 2012 Giovanni Bajo <rasky@develer.com>
- and Copyright (c) 2012-2020 Simon Kelley
+ and Copyright (c) 2012-2023 Simon Kelley
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
@@ -215,14 +215,6 @@ static int is_check_date(unsigned long curtime)
return !daemon->dnssec_no_time_check;
}
-/* Check whether today/now is between date_start and date_end */
-static int check_date_range(unsigned long curtime, u32 date_start, u32 date_end)
-{
- /* We must explicitly check against wanted values, because of SERIAL_UNDEF */
- return serial_compare_32(curtime, date_start) == SERIAL_GT
- && serial_compare_32(curtime, date_end) == SERIAL_LT;
-}
-
/* Return bytes of canonicalised rrdata one by one.
Init state->ip with the RR, and state->end with the end of same.
Init state->op to NULL.
@@ -233,7 +225,7 @@ static int check_date_range(unsigned long curtime, u32 date_start, u32 date_end)
On returning 0, the end has been reached.
*/
struct rdata_state {
- u16 *desc;
+ short *desc;
size_t c;
unsigned char *end, *ip, *op;
char *buff;
@@ -254,7 +246,7 @@ static int get_rdata(struct dns_header *header, size_t plen, struct rdata_state
{
d = *(state->desc);
- if (d == (u16)-1)
+ if (d == -1)
{
/* all the bytes to the end. */
if ((state->c = state->end - state->ip) != 0)
@@ -302,7 +294,7 @@ static int get_rdata(struct dns_header *header, size_t plen, struct rdata_state
/* Bubble sort the RRset into the canonical order. */
-static int sort_rrset(struct dns_header *header, size_t plen, u16 *rr_desc, int rrsetidx,
+static int sort_rrset(struct dns_header *header, size_t plen, short *rr_desc, int rrsetidx,
unsigned char **rrset, char *buff1, char *buff2)
{
int swap, i, j;
@@ -339,7 +331,7 @@ static int sort_rrset(struct dns_header *header, size_t plen, u16 *rr_desc, int
is the identity function and we can compare
the RRs directly. If not we compare the
canonicalised RRs one byte at a time. */
- if (*rr_desc == (u16)-1)
+ if (*rr_desc == -1)
{
int rdmin = rdlen1 > rdlen2 ? rdlen2 : rdlen1;
int cmp = memcmp(state1.ip, state2.ip, rdmin);
@@ -507,12 +499,24 @@ 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 : _("per-query 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.
STAT_SECURE_WILDCARD if it validates and is the result of wildcard expansion.
(In this case *wildcard_out points to the "body" of the wildcard within name.)
STAT_BOGUS signature is wrong, bad packet.
+ STAT_ABANDONED validation abandoned do to excess resource usage.
STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname)
STAT_NEED_DS need DS to complete validation (name is returned in keyname)
@@ -527,14 +531,15 @@ static int explore_rrset(struct dns_header *header, size_t plen, int class, int
*/
static int validate_rrset(time_t now, struct dns_header *header, size_t plen, int class, int type, int sigidx, int rrsetidx,
char *name, char *keyname, char **wildcard_out, struct blockdata *key, int keylen,
- int algo_in, int keytag_in, unsigned long *ttl_out)
+ int algo_in, int keytag_in, unsigned long *ttl_out, int *validate_counter)
{
unsigned char *p;
- int rdlen, j, name_labels, algo, labels, key_tag;
+ int rdlen, j, name_labels, algo, labels, key_tag, sig_fail_cnt;
struct crec *crecp = NULL;
- u16 *rr_desc = rrfilter_desc(type);
+ short *rr_desc = rrfilter_desc(type);
u32 sig_expiration, sig_inception;
-
+ int failflags = DNSSEC_FAIL_NOSIG | DNSSEC_FAIL_NYV | DNSSEC_FAIL_EXP | DNSSEC_FAIL_NOKEYSUP;
+
unsigned long curtime = time(0);
int time_check = is_check_date(curtime);
@@ -549,7 +554,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 (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;
@@ -557,6 +562,8 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
void *ctx;
char *name_start;
u32 nsigttl, ttl, orig_ttl;
+
+ failflags &= ~DNSSEC_FAIL_NOSIG;
p = sigs[j];
GETLONG(ttl, p);
@@ -574,12 +581,31 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
if (!extract_name(header, plen, &p, keyname, 1, 0))
return STAT_BOGUS;
- if ((time_check && !check_date_range(curtime, sig_inception, sig_expiration)) ||
- labels > name_labels ||
- !(hash = hash_find(algo_digest_name(algo))) ||
+ if (!time_check)
+ failflags &= ~(DNSSEC_FAIL_NYV | DNSSEC_FAIL_EXP);
+ else
+ {
+ /* We must explicitly check against wanted values, because of SERIAL_UNDEF */
+ if (serial_compare_32(curtime, sig_inception) == SERIAL_LT)
+ continue;
+ else
+ failflags &= ~DNSSEC_FAIL_NYV;
+
+ if (serial_compare_32(curtime, sig_expiration) == SERIAL_GT)
+ continue;
+ else
+ failflags &= ~DNSSEC_FAIL_EXP;
+ }
+
+ if (!(hash = hash_find(algo_digest_name(algo))))
+ continue;
+ else
+ failflags &= ~DNSSEC_FAIL_NOKEYSUP;
+
+ if (labels > name_labels ||
!hash_init(hash, &ctx, &digest))
continue;
-
+
/* OK, we have the signature record, see if the relevant DNSKEY is in the cache. */
if (!key && !(crecp = cache_find_by_name(NULL, keyname, now, F_DNSKEY)))
return STAT_NEED_KEY;
@@ -657,7 +683,7 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
If canonicalisation is not needed, a simple insertion into the hash works.
*/
- if (*rr_desc == (u16)-1)
+ if (*rr_desc == -1)
{
len = htons(rdlen);
hash->update(ctx, 2, (unsigned char *)&len);
@@ -710,13 +736,19 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
/* namebuff used for workspace above, restore to leave unchanged on exit */
p = (unsigned char*)(rrset[0]);
- extract_name(header, plen, &p, name, 1, 0);
+ if (!extract_name(header, plen, &p, name, 1, 0))
+ return STAT_BOGUS;
if (key)
{
- if (algo_in == algo && keytag_in == key_tag &&
- verify(key, keylen, sig, sig_len, digest, hash->digest_size, algo))
- return STAT_SECURE;
+ if (algo_in == algo && keytag_in == key_tag)
+ {
+ 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
{
@@ -724,13 +756,27 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
for (; crecp; crecp = cache_find_by_name(crecp, keyname, now, F_DNSKEY))
if (crecp->addr.key.algo == algo &&
crecp->addr.key.keytag == key_tag &&
- crecp->uid == (unsigned int)class &&
- 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;
+ crecp->uid == (unsigned int)class)
+ {
+ 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. */
+ 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;
+ }
}
}
- return STAT_BOGUS;
+ /* If we reach this point, no verifying key was found */
+ return STAT_BOGUS | failflags | DNSSEC_FAIL_NOKEY;
}
@@ -740,38 +786,45 @@ static int validate_rrset(time_t now, struct dns_header *header, size_t plen, in
STAT_OK Done, key(s) in cache.
STAT_BOGUS No DNSKEYs found, which can be validated with DS,
or self-sign for DNSKEY RRset is not valid, bad packet.
+ STAT_ABANDONED resource exhaustion.
STAT_NEED_DS DS records to validate a key not found, name in keyname
- STAT_NEED_KEY DNSKEY records to validate a key not found, name in keyname
*/
-int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class)
+int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_counter)
{
- unsigned char *psave, *p = (unsigned char *)(header+1);
+ unsigned char *psave, *p = (unsigned char *)(header+1), *keyaddr;
struct crec *crecp, *recp1;
- int rc, j, qtype, qclass, rdlen, flags, algo, valid, keytag;
+ int rc, j, qtype, qclass, rdlen, flags, algo, keytag, sigcnt, rrcnt;
unsigned long ttl, sig_ttl;
- struct blockdata *key;
union all_addr a;
+ int failflags = DNSSEC_FAIL_NODSSUP | DNSSEC_FAIL_NOZONE;
+ char valid_digest[255];
+ static unsigned char **cached_digest;
+ static size_t cached_digest_size = 0;
- if (ntohs(header->qdcount) != 1 ||
- RCODE(header) == SERVFAIL || RCODE(header) == REFUSED ||
- !extract_name(header, plen, &p, name, 1, 4))
- return STAT_BOGUS;
+ if (ntohs(header->qdcount) != 1 || RCODE(header) != NOERROR || !extract_name(header, plen, &p, name, 1, 4))
+ return STAT_BOGUS | DNSSEC_FAIL_NOKEY;
GETSHORT(qtype, p);
GETSHORT(qclass, p);
- if (qtype != T_DNSKEY || qclass != class || ntohs(header->ancount) == 0)
- return STAT_BOGUS;
+ if (qtype != T_DNSKEY || qclass != class ||
+ !explore_rrset(header, plen, class, T_DNSKEY, name, keyname, &sigcnt, &rrcnt) ||
+ rrcnt == 0)
+ return STAT_BOGUS | DNSSEC_FAIL_NOKEY;
+ if (sigcnt == 0)
+ return STAT_BOGUS | DNSSEC_FAIL_NOSIG;
+
/* See if we have cached a DS record which validates this key */
if (!(crecp = cache_find_by_name(NULL, name, now, F_DS)))
{
strcpy(keyname, name);
return STAT_NEED_DS;
}
-
+
/* NOTE, we need to find ONE DNSKEY which matches the DS */
- for (valid = 0, j = ntohs(header->ancount); j != 0 && !valid; j--)
+ for (j = ntohs(header->ancount); j != 0; j--)
{
/* Ensure we have type, class TTL and length */
if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
@@ -782,7 +835,7 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
GETLONG(ttl, p);
GETSHORT(rdlen, p);
- if (!CHECK_LEN(header, p, plen, rdlen) || rdlen < 4)
+ if (!CHECK_LEN(header, p, plen, rdlen))
return STAT_BOGUS; /* bad packet */
if (qclass != class || qtype != T_DNSKEY || rc == 2)
@@ -790,172 +843,230 @@ int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t plen, ch
p += rdlen;
continue;
}
-
+
+ if (rdlen < 5)
+ return STAT_BOGUS; /* min 1 byte key! */
+
psave = p;
GETSHORT(flags, p);
if (*p++ != 3)
- return STAT_BOGUS;
- algo = *p++;
- keytag = dnskey_keytag(algo, flags, p, rdlen - 4);
- key = NULL;
-
- /* key must have zone key flag set */
- if (flags & 0x100)
- key = blockdata_alloc((char*)p, rdlen - 4);
-
- p = psave;
-
- if (!ADD_RDLEN(header, p, plen, rdlen))
{
- if (key)
- blockdata_free(key);
- return STAT_BOGUS; /* bad packet */
+ p = psave + rdlen;
+ continue;
}
+ algo = *p++;
+ keyaddr = p;
+ keytag = dnskey_keytag(algo, flags, keyaddr, rdlen - 4);
+
+ p = psave + rdlen;
- /* No zone key flag or malloc failure */
- if (!key)
+ /* key must have zone key flag set */
+ if (!(flags & 0x100))
continue;
+ failflags &= ~DNSSEC_FAIL_NOZONE;
+
+ /* clear digest cache. */
+ memset(valid_digest, 0, sizeof(valid_digest));
+
for (recp1 = crecp; recp1; recp1 = cache_find_by_name(recp1, name, now, F_DS))
{
void *ctx;
unsigned char *digest, *ds_digest;
const struct nettle_hash *hash;
- int sigcnt, rrcnt;
+ int wire_len;
+
+ if ((recp1->flags & F_NEG) ||
+ recp1->addr.ds.algo != algo ||
+ recp1->addr.ds.keytag != keytag ||
+ recp1->uid != (unsigned int)class)
+ continue;
+
+ if (!(hash = hash_find(ds_digest_name(recp1->addr.ds.digest))))
+ continue;
+
+ failflags &= ~DNSSEC_FAIL_NODSSUP;
+
+ if (recp1->addr.ds.keylen != (int)hash->digest_size ||
+ !(ds_digest = blockdata_retrieve(recp1->addr.ds.keydata, recp1->addr.ds.keylen, NULL)))
+ continue;
- if (recp1->addr.ds.algo == algo &&
- recp1->addr.ds.keytag == keytag &&
- recp1->uid == (unsigned int)class &&
- (hash = hash_find(ds_digest_name(recp1->addr.ds.digest))) &&
- hash_init(hash, &ctx, &digest))
-
+ if (valid_digest[recp1->addr.ds.digest])
+ digest = cached_digest[recp1->addr.ds.digest];
+ else
{
- int wire_len = to_wire(name);
+ /* 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;
+
+ wire_len = to_wire(name);
/* Note that digest may be different between DSs, so
- we can't move this outside the loop. */
+ we can't move this outside the loop. We keep
+ copies of each digest we make for this key,
+ so maximum digest work is O(keys x digests_types)
+ rather then O(keys x DSs) */
hash->update(ctx, (unsigned int)wire_len, (unsigned char *)name);
hash->update(ctx, (unsigned int)rdlen, psave);
hash->digest(ctx, hash->digest_size, digest);
from_wire(name);
-
- if (!(recp1->flags & F_NEG) &&
- recp1->addr.ds.keylen == (int)hash->digest_size &&
- (ds_digest = blockdata_retrieve(recp1->addr.ds.keydata, recp1->addr.ds.keylen, NULL)) &&
- memcmp(ds_digest, digest, recp1->addr.ds.keylen) == 0 &&
- explore_rrset(header, plen, class, T_DNSKEY, name, keyname, &sigcnt, &rrcnt) &&
- sigcnt != 0 && rrcnt != 0 &&
- validate_rrset(now, header, plen, class, T_DNSKEY, sigcnt, rrcnt, name, keyname,
- NULL, key, rdlen - 4, algo, keytag, &sig_ttl) == STAT_SECURE)
+
+ if (recp1->addr.ds.digest >= cached_digest_size)
{
- valid = 1;
- break;
+ unsigned char **new;
+
+ /* whine_malloc zeros memory */
+ if ((new = whine_malloc((recp1->addr.ds.digest + 5) * sizeof(unsigned char *))))
+ {
+ if (cached_digest_size != 0)
+ {
+ memcpy(new, cached_digest, cached_digest_size * sizeof(unsigned char *));
+ free(cached_digest);
+ }
+
+ cached_digest_size = recp1->addr.ds.digest + 5;
+ cached_digest = new;
+ }
+ }
+
+ if (recp1->addr.ds.digest < cached_digest_size)
+ {
+ if (!cached_digest[recp1->addr.ds.digest])
+ cached_digest[recp1->addr.ds.digest] = whine_malloc(recp1->addr.ds.keylen);
+
+ if (cached_digest[recp1->addr.ds.digest])
+ {
+ memcpy(cached_digest[recp1->addr.ds.digest], digest, recp1->addr.ds.keylen);
+ valid_digest[recp1->addr.ds.digest] = 1;
+ }
}
}
- }
- blockdata_free(key);
- }
-
- if (valid)
- {
- /* DNSKEY RRset determined to be OK, now cache it. */
- cache_start_insert();
-
- p = skip_questions(header, plen);
-
- for (j = ntohs(header->ancount); j != 0; j--)
- {
- /* Ensure we have type, class TTL and length */
- if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
- return STAT_BOGUS; /* bad packet */
- GETSHORT(qtype, p);
- GETSHORT(qclass, p);
- GETLONG(ttl, p);
- GETSHORT(rdlen, p);
-
- /* TTL may be limited by sig. */
- if (sig_ttl < ttl)
- ttl = sig_ttl;
-
- if (!CHECK_LEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
-
- if (qclass == class && rc == 1)
+ if (memcmp(ds_digest, digest, recp1->addr.ds.keylen) == 0)
{
- psave = p;
+ /* Found the key validated by a DS record.
+ Now check the self-sig for the entire key RRset using that key.
+ Note that validate_rrset() will never return STAT_NEED_KEY here,
+ since we supply the key it will use as an argument. */
+ struct blockdata *key;
+
+ if (!(key = blockdata_alloc((char *)keyaddr, rdlen - 4)))
+ break;
+
+ rc = validate_rrset(now, header, plen, class, T_DNSKEY, sigcnt, rrcnt, name, keyname,
+ NULL, key, rdlen - 4, algo, keytag, &sig_ttl, validate_counter);
+
+ blockdata_free(key);
+
+ if (STAT_ISEQUAL(rc, STAT_ABANDONED))
+ return rc;
- if (qtype == T_DNSKEY)
+ /* can't validate KEY RRset with this key, see if there's another that
+ will, which is validated by another DS. */
+ if (!STAT_ISEQUAL(rc, STAT_SECURE))
+ break;
+
+ /* DNSKEY RRset determined to be OK, now cache it. */
+ cache_start_insert();
+
+ p = skip_questions(header, plen);
+
+ for (j = ntohs(header->ancount); j != 0; j--)
{
- if (rdlen < 4)
+ /* Ensure we have type, class TTL and length */
+ if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
return STAT_BOGUS; /* bad packet */
- GETSHORT(flags, p);
- if (*p++ != 3)
- return STAT_BOGUS;
- algo = *p++;
- keytag = dnskey_keytag(algo, flags, p, rdlen - 4);
+ GETSHORT(qtype, p);
+ GETSHORT(qclass, p);
+ GETLONG(ttl, p);
+ GETSHORT(rdlen, p);
- if ((key = blockdata_alloc((char*)p, rdlen - 4)))
- {
- a.key.keylen = rdlen - 4;
- a.key.keydata = key;
- a.key.algo = algo;
- a.key.keytag = keytag;
- a.key.flags = flags;
-
- if (!cache_insert(name, &a, class, now, ttl, F_FORWARD | F_DNSKEY | F_DNSSECOK))
- {
- blockdata_free(key);
- return STAT_BOGUS;
- }
- else
- {
- a.log.keytag = keytag;
- a.log.algo = algo;
- if (algo_digest_name(algo))
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu");
- else
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu (not supported)");
- }
- }
+ /* TTL may be limited by sig. */
+ if (sig_ttl < ttl)
+ ttl = sig_ttl;
+
+ if (!CHECK_LEN(header, p, plen, rdlen))
+ return STAT_BOGUS; /* bad packet */
+
+ psave = p;
+
+ if (qclass == class && rc == 1 && qtype == T_DNSKEY)
+ {
+ if (rdlen < 4)
+ return STAT_BOGUS; /* min 1 byte key! */
+
+ GETSHORT(flags, p);
+ if (*p++ == 3)
+ {
+ algo = *p++;
+ keytag = dnskey_keytag(algo, flags, p, rdlen - 4);
+
+ if (!(key = blockdata_alloc((char*)p, rdlen - 4)))
+ return STAT_BOGUS;
+
+ a.key.keylen = rdlen - 4;
+ a.key.keydata = key;
+ a.key.algo = algo;
+ a.key.keytag = keytag;
+ a.key.flags = flags;
+
+ if (!cache_insert(name, &a, class, now, ttl, F_FORWARD | F_DNSKEY | F_DNSSECOK))
+ {
+ blockdata_free(key);
+ return STAT_BOGUS;
+ }
+
+ a.log.keytag = keytag;
+ a.log.algo = algo;
+ if (algo_digest_name(algo))
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu");
+ else
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DNSKEY keytag %hu, algo %hu (not supported)");
+ }
+ }
+
+ p = psave + rdlen;
}
-
- p = psave;
+
+ /* commit cache insert. */
+ cache_end_insert();
+ return STAT_OK;
}
-
- if (!ADD_RDLEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
}
-
- /* commit cache insert. */
- cache_end_insert();
- return STAT_OK;
}
-
+
log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DNSKEY");
- return STAT_BOGUS;
+ return STAT_BOGUS | failflags;
}
/* The DNS packet is expected to contain the answer to a DS query
- Put all DSs in the answer which are valid into the cache.
+ Put all DSs in the answer which are valid and have hash and signature algos
+ we support into the cache.
Also handles replies which prove that there's no DS at this location,
either because the zone is unsigned or this isn't a zone cut. These are
cached too.
+ If none of the DS's are for supported algos, treat the answer as if
+ it's a proof of no DS at this location. RFC4035 para 5.2.
return codes:
STAT_OK At least one valid DS found and in cache.
STAT_BOGUS no DS in reply or not signed, fails validation, bad packet.
STAT_NEED_KEY DNSKEY records to validate a DS not found, name in keyname
STAT_NEED_DS DS record needed.
+ STAT_ABANDONED resource exhaustion.
*/
-int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int class)
+int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char *name,
+ char *keyname, int class, int *validate_counter)
{
unsigned char *p = (unsigned char *)(header+1);
- int qtype, qclass, rc, i, neganswer, nons, neg_ttl = 0;
- int aclass, atype, rdlen;
+ int qtype, qclass, rc, i, neganswer = 0, nons = 0, servfail = 0, neg_ttl = 0, found_supported = 0;
+ int aclass, atype, rdlen, flags;
unsigned long ttl;
union all_addr a;
@@ -967,38 +1078,51 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
GETSHORT(qclass, p);
if (qtype != T_DS || qclass != class)
- rc = STAT_BOGUS;
+ return STAT_BOGUS;
+
+ /* A SERVFAIL answer has been seen to a DS query not at start of authority,
+ so treat it as such and continue to search for a DS or proof of no existence
+ further down the tree. */
+ if (RCODE(header) == SERVFAIL)
+ servfail = neganswer = nons = 1;
else
- rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons, &neg_ttl);
-
- if (rc == STAT_INSECURE)
{
- my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name);
- rc = STAT_BOGUS;
- }
+ rc = dnssec_validate_reply(now, header, plen, name, keyname, NULL, 0, &neganswer, &nons, &neg_ttl, validate_counter);
- p = (unsigned char *)(header+1);
- extract_name(header, plen, &p, name, 1, 4);
- p += 4; /* qtype, qclass */
+ if (STAT_ISEQUAL(rc, STAT_INSECURE))
+ {
+ my_syslog(LOG_WARNING, _("Insecure DS reply received for %s, check domain configuration and upstream DNS server DNSSEC support"), name);
+ log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS - not secure");
+ return STAT_BOGUS | DNSSEC_FAIL_INDET;
+ }
+
+ p = (unsigned char *)(header+1);
+ if (!extract_name(header, plen, &p, name, 1, 4))
+ return STAT_BOGUS;
+
+ p += 4; /* qtype, qclass */
+
+ /* If the key needed to validate the DS is on the same domain as the DS, we'll
+ loop getting nowhere. Stop that now. This can happen of the DS answer comes
+ from the DS's zone, and not the parent zone. */
+ if (STAT_ISEQUAL(rc, STAT_NEED_KEY) && hostname_isequal(name, keyname))
+ {
+ log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS");
+ return STAT_BOGUS;
+ }
- /* If the key needed to validate the DS is on the same domain as the DS, we'll
- loop getting nowhere. Stop that now. This can happen of the DS answer comes
- from the DS's zone, and not the parent zone. */
- if (rc == STAT_BOGUS || (rc == STAT_NEED_KEY && hostname_isequal(name, keyname)))
- {
- log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, "BOGUS DS");
- return STAT_BOGUS;
+ if (!STAT_ISEQUAL(rc, STAT_SECURE))
+ return rc;
}
- if (rc != STAT_SECURE)
- return rc;
-
if (!neganswer)
{
cache_start_insert();
for (i = 0; i < ntohs(header->ancount); i++)
{
+ unsigned char *psave;
+
if (!(rc = extract_name(header, plen, &p, name, 0, 10)))
return STAT_BOGUS; /* bad packet */
@@ -1009,28 +1133,37 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
if (!CHECK_LEN(header, p, plen, rdlen))
return STAT_BOGUS; /* bad packet */
+
+ psave = p;
if (aclass == class && atype == T_DS && rc == 1)
{
int algo, digest, keytag;
- unsigned char *psave = p;
struct blockdata *key;
- if (rdlen < 4)
- return STAT_BOGUS; /* bad packet */
+ if (rdlen < 5)
+ return STAT_BOGUS; /* min 1 byte digest! */
GETSHORT(keytag, p);
algo = *p++;
digest = *p++;
- if ((key = blockdata_alloc((char*)p, rdlen - 4)))
+ if (!ds_digest_name(digest) || !algo_digest_name(algo))
+ {
+ a.log.keytag = keytag;
+ a.log.algo = algo;
+ a.log.digest = digest;
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS for keytag %hu, algo %hu, digest %hu (not supported)");
+ neg_ttl = ttl;
+ }
+ else if ((key = blockdata_alloc((char*)p, rdlen - 4)))
{
a.ds.digest = digest;
a.ds.keydata = key;
a.ds.algo = algo;
a.ds.keytag = keytag;
a.ds.keylen = rdlen - 4;
-
+
if (!cache_insert(name, &a, class, now, ttl, F_FORWARD | F_DS | F_DNSSECOK))
{
blockdata_free(key);
@@ -1041,26 +1174,26 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
a.log.keytag = keytag;
a.log.algo = algo;
a.log.digest = digest;
- if (ds_digest_name(digest) && algo_digest_name(algo))
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS keytag %hu, algo %hu, digest %hu");
- else
- log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS keytag %hu, algo %hu, digest %hu (not supported)");
+ log_query(F_NOEXTRA | F_KEYTAG | F_UPSTREAM, name, &a, "DS for keytag %hu, algo %hu, digest %hu");
+ found_supported = 1;
}
}
-
- p = psave;
}
- if (!ADD_RDLEN(header, p, plen, rdlen))
- return STAT_BOGUS; /* bad packet */
+
+ p = psave + rdlen;
}
cache_end_insert();
+ /* Fall through if no supported algo DS found. */
+ if (found_supported)
+ return STAT_OK;
}
- else
+
+ flags = F_FORWARD | F_DS | F_NEG | F_DNSSECOK;
+
+ if (neganswer)
{
- int flags = F_FORWARD | F_DS | F_NEG | F_DNSSECOK;
-
if (RCODE(header) == NXDOMAIN)
flags |= F_NXDOMAIN;
@@ -1068,17 +1201,19 @@ int dnssec_validate_ds(time_t now, struct dns_header *header, size_t plen, char
to store presence/absence of NS. */
if (nons)
flags &= ~F_DNSSECOK;
-
- cache_start_insert();
-
- /* Use TTL from NSEC for negative cache entries */
- if (!cache_insert(name, NULL, class, now, neg_ttl, flags))
- return STAT_BOGUS;
-
- cache_end_insert();
-
- log_query(F_NOEXTRA | F_UPSTREAM, name, NULL, nons ? "no DS/cut" : "no DS");
}
+
+ cache_start_insert();
+
+ /* Use TTL from NSEC for negative cache entries */
+ if (!cache_insert(name, NULL, class, now, neg_ttl, flags))
+ return STAT_BOGUS;
+
+ cache_end_insert();
+
+ if (neganswer)
+ log_query(F_NOEXTRA | F_UPSTREAM, name, NULL,
+ servfail ? "SERVFAIL" : (nons ? "no DS/cut" : "no DS"));
return STAT_OK;
}
@@ -1148,6 +1283,7 @@ static int hostname_cmp(const char *a, const char *b)
}
}
+/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsigned char **nsecs, unsigned char **labels, int nsec_count,
char *workspace1_in, char *workspace2, char *name, int type, int *nons)
{
@@ -1167,12 +1303,12 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
p = nsecs[i];
if (!extract_name(header, plen, &p, workspace1, 1, 10))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
p += 8; /* class, type, TTL */
GETSHORT(rdlen, p);
psave = p;
- if (!extract_name(header, plen, &p, workspace2, 1, 10))
- return 0;
+ if (!extract_name(header, plen, &p, workspace2, 1, 0))
+ return DNSSEC_FAIL_BADPACKET;
/* If NSEC comes from wildcard expansion, use original wildcard
as name for computation. */
@@ -1200,12 +1336,13 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
{
/* 4035 para 5.4. Last sentence */
if (type == T_NSEC || type == T_RRSIG)
- return 1;
+ return 0;
/* NSEC with the same name as the RR we're testing, check
that the type in question doesn't appear in the type map */
rdlen -= p - psave;
- /* rdlen is now length of type map, and p points to it */
+ /* rdlen is now length of type map, and p points to it
+ packet checked to be as long as rdlen implies in prove_non_existence() */
/* If we can prove that there's no NS record, return that information. */
if (nons && rdlen >= 2 && p[0] == 0 && (p[2] & (0x80 >> T_NS)) != 0)
@@ -1216,25 +1353,25 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
/* A CNAME answer would also be valid, so if there's a CNAME is should
have been returned. */
if ((p[2] & (0x80 >> T_CNAME)) != 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
/* If the SOA bit is set for a DS record, then we have the
DS from the wrong side of the delegation. For the root DS,
this is expected. */
if (name_labels != 0 && type == T_DS && (p[2] & (0x80 >> T_SOA)) != 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
while (rdlen >= 2)
{
if (!CHECK_LEN(header, p, plen, rdlen))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
if (p[0] == type >> 8)
{
/* Does the NSEC say our type exists? */
if (offset < p[1] && (p[offset+2] & mask) != 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
break; /* finished checking */
}
@@ -1243,24 +1380,24 @@ static int prove_non_existence_nsec(struct dns_header *header, size_t plen, unsi
p += p[1];
}
- return 1;
+ return 0;
}
else if (rc == -1)
{
/* Normal case, name falls between NSEC name and next domain name,
wrap around case, name falls between NSEC name (rc == -1) and end */
if (hostname_cmp(workspace2, name) >= 0 || hostname_cmp(workspace1, workspace2) >= 0)
- return 1;
+ return 0;
}
else
{
/* wrap around case, name falls between start and next domain name */
if (hostname_cmp(workspace1, workspace2) >= 0 && hostname_cmp(workspace2, name) >=0 )
- return 1;
+ return 0;
}
}
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
/* return digest length, or zero on error */
@@ -1334,23 +1471,23 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
for (i = 0; i < nsec_count; i++)
if ((p = nsecs[i]))
{
- if (!extract_name(header, plen, &p, workspace1, 1, 0) ||
+ if (!extract_name(header, plen, &p, workspace1, 1, 10) ||
!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
return 0;
p += 8; /* class, type, TTL */
GETSHORT(rdlen, p);
+
psave = p;
+
+ /* packet checked to be as long as implied by rdlen, salt_len and hash_len in prove_non_existence() */
p++; /* algo */
flags = *p++; /* flags */
p += 2; /* iterations */
salt_len = *p++; /* salt_len */
p += salt_len; /* salt */
hash_len = *p++; /* p now points to next hashed name */
-
- if (!CHECK_LEN(header, p, plen, hash_len))
- return 0;
-
+
if (digest_len == base32_len && hash_len == base32_len)
{
int rc = memcmp(workspace2, digest, digest_len);
@@ -1358,7 +1495,8 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
if (rc == 0)
{
/* We found an NSEC3 whose hashed name exactly matches the query, so
- we just need to check the type map. p points to the RR data for the record. */
+ we just need to check the type map. p points to the RR data for the record.
+ Note we have packet length up to rdlen bytes checked. */
int offset = (type & 0xff) >> 3;
int mask = 0x80 >> (type & 0x07);
@@ -1366,15 +1504,12 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
p += hash_len; /* skip next-domain hash */
rdlen -= p - psave;
- if (!CHECK_LEN(header, p, plen, rdlen))
- return 0;
-
if (rdlen >= 2 && p[0] == 0)
{
/* If we can prove that there's no NS record, return that information. */
if (nons && (p[2] & (0x80 >> T_NS)) != 0)
*nons = 0;
-
+
/* A CNAME answer would also be valid, so if there's a CNAME is should
have been returned. */
if ((p[2] & (0x80 >> T_CNAME)) != 0)
@@ -1433,8 +1568,9 @@ static int check_nsec3_coverage(struct dns_header *header, size_t plen, int dige
return 0;
}
-static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count,
- char *workspace1, char *workspace2, char *name, int type, char *wildname, int *nons)
+/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
+static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, unsigned char **nsecs, int nsec_count, char *workspace1,
+ char *workspace2, char *name, int type, char *wildname, int *nons, int *validate_counter)
{
unsigned char *salt, *p, *digest;
int digest_len, i, iterations, salt_len, base32_len, algo = 0;
@@ -1454,7 +1590,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
for (i = 0; i < nsec_count; i++)
{
if (!(p = skip_name(nsecs[i], header, plen, 15)))
- return 0; /* bad packet */
+ return DNSSEC_FAIL_BADPACKET; /* bad packet */
p += 10; /* type, class, TTL, rdlen */
algo = *p++;
@@ -1465,23 +1601,18 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
/* No usable NSEC3s */
if (i == nsec_count)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
p++; /* flags */
GETSHORT (iterations, p);
- /* Upper-bound iterations, to avoid DoS.
- Strictly, there are lower bounds for small keys, but
- since we don't have key size info here, at least limit
- to the largest bound, for 4096-bit keys. RFC 5155 10.3 */
- if (iterations > 2500)
- return 0;
+ /* Upper-bound iterations, to avoid DoS. RFC 9276 refers. */
+ if (iterations > daemon->limit[LIMIT_NSEC3_ITERS])
+ return DNSSEC_FAIL_NSEC3_ITERS;
salt_len = *p++;
salt = p;
- if (!CHECK_LEN(header, salt, plen, salt_len))
- return 0; /* bad packet */
-
+
/* Now prune so we only have NSEC3 records with same iterations, salt and algo */
for (i = 0; i < nsec_count; i++)
{
@@ -1491,7 +1622,7 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
nsecs[i] = NULL; /* Speculative, will be restored if OK. */
if (!(p = skip_name(nsec3p, header, plen, 15)))
- return 0; /* bad packet */
+ return DNSSEC_FAIL_BADPACKET; /* bad packet */
p += 10; /* type, class, TTL, rdlen */
@@ -1511,9 +1642,6 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
if (salt_len != *p++)
continue;
- if (!CHECK_LEN(header, p, plen, salt_len))
- return 0; /* bad packet */
-
if (memcmp(p, salt, salt_len) != 0)
continue;
@@ -1521,11 +1649,14 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
nsecs[i] = nsec3p;
}
+ if (dec_counter(validate_counter, NULL))
+ return DNSSEC_FAIL_WORK;
+
if ((digest_len = hash_name(name, &digest, hash, salt, salt_len, iterations)) == 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
if (check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, nons, count_labels(name)))
- return 1;
+ return 0;
/* Can't find an NSEC3 which covers the name directly, we need the "closest encloser NSEC3"
or an answer inferred from a wildcard record. */
@@ -1540,15 +1671,20 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
if (wildname && hostname_isequal(closest_encloser, wildname))
break;
+ 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 0;
+ return DNSSEC_FAIL_NONSEC;
for (i = 0; i < nsec_count; i++)
if ((p = nsecs[i]))
{
- if (!extract_name(header, plen, &p, workspace1, 1, 0) ||
- !(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
- return 0;
+ if (!extract_name(header, plen, &p, workspace1, 1, 0))
+ return DNSSEC_FAIL_BADPACKET;
+
+ if (!(base32_len = base32_decode(workspace1, (unsigned char *)workspace2)))
+ return DNSSEC_FAIL_NONSEC;
if (digest_len == base32_len &&
memcmp(digest, workspace2, digest_len) == 0)
@@ -1563,35 +1699,43 @@ static int prove_non_existence_nsec3(struct dns_header *header, size_t plen, uns
while ((closest_encloser = strchr(closest_encloser, '.')));
if (!closest_encloser || !next_closest)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
/* Look for NSEC3 that proves the non-existence of the next-closest encloser */
+ 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 0;
+ return DNSSEC_FAIL_NONSEC;
if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1))
- return 0;
+ return DNSSEC_FAIL_NONSEC;
/* Finally, check that there's no seat of wildcard synthesis */
if (!wildname)
{
if (!(wildcard = strchr(next_closest, '.')) || wildcard == next_closest)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
wildcard--;
*wildcard = '*';
+ if (dec_counter(validate_counter, NULL))
+ return DNSSEC_FAIL_WORK;
+
if ((digest_len = hash_name(wildcard, &digest, hash, salt, salt_len, iterations)) == 0)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
if (!check_nsec3_coverage(header, plen, digest_len, digest, type, workspace1, workspace2, nsecs, nsec_count, NULL, 1))
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
- return 1;
+ return 0;
}
-static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass, char *wildname, int *nons, int *nsec_ttl)
+/* returns 0 on success, or DNSSEC_FAIL_* value on failure. */
+static int prove_non_existence(struct dns_header *header, size_t plen, char *keyname, char *name, int qtype, int qclass,
+ char *wildname, int *nons, int *nsec_ttl, int *validate_counter)
{
static unsigned char **nsecset = NULL, **rrsig_labels = NULL;
static int nsecset_sz = 0, rrsig_labels_sz = 0;
@@ -1603,7 +1747,7 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
/* Move to NS section */
if (!p || !(p = skip_section(p, ntohs(header->ancount), header, plen)))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
auth_start = p;
@@ -1612,13 +1756,16 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
unsigned char *pstart = p;
if (!extract_name(header, plen, &p, daemon->workspacename, 1, 10))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
GETSHORT(type, p);
GETSHORT(class, p);
GETLONG(ttl, p);
GETSHORT(rdlen, p);
-
+
+ if (!CHECK_LEN(header, p, plen, rdlen))
+ return DNSSEC_FAIL_BADPACKET;
+
if (class == qclass && (type == T_NSEC || type == T_NSEC3))
{
if (nsec_ttl)
@@ -1631,12 +1778,12 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
/* No mixed NSECing 'round here, thankyouverymuch */
if (type_found != 0 && type_found != type)
- return 0;
+ return DNSSEC_FAIL_NONSEC;
type_found = type;
if (!expand_workspace(&nsecset, &nsecset_sz, nsecs_found))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
if (type == T_NSEC)
{
@@ -1651,30 +1798,33 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
int res, j, rdlen1, type1, class1;
if (!expand_workspace(&rrsig_labels, &rrsig_labels_sz, nsecs_found))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
rrsig_labels[nsecs_found] = NULL;
for (j = ntohs(header->nscount); j != 0; j--)
{
- if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10)))
- return 0;
+ unsigned char *psav;
+ if (!(res = extract_name(header, plen, &p1, daemon->workspacename, 0, 10)))
+ return DNSSEC_FAIL_BADPACKET;
+
GETSHORT(type1, p1);
GETSHORT(class1, p1);
p1 += 4; /* TTL */
GETSHORT(rdlen1, p1);
+ psav = p1;
+
if (!CHECK_LEN(header, p1, plen, rdlen1))
- return 0;
+ return DNSSEC_FAIL_BADPACKET;
if (res == 1 && class1 == qclass && type1 == T_RRSIG)
{
int type_covered;
- unsigned char *psav = p1;
-
+
if (rdlen1 < 18)
- return 0; /* bad packet */
+ return DNSSEC_FAIL_BADPACKET; /* bad packet */
GETSHORT(type_covered, p1);
@@ -1686,33 +1836,56 @@ static int prove_non_existence(struct dns_header *header, size_t plen, char *key
if (!rrsig_labels[nsecs_found])
rrsig_labels[nsecs_found] = p1;
else if (*rrsig_labels[nsecs_found] != *p1) /* algo */
- return 0;
- }
- p1 = psav;
+ return DNSSEC_FAIL_NONSEC;
+ }
}
- if (!ADD_RDLEN(header, p1, plen, rdlen1))
- return 0;
+ p1 = psav + rdlen1;
}
/* Must have found at least one sig. */
if (!rrsig_labels[nsecs_found])
- return 0;
+ return DNSSEC_FAIL_NONSEC;
+ }
+ else if (type == T_NSEC3)
+ {
+ /* Decode the packet structure enough to check that rdlen is big enough
+ to contain everything other than the type bitmap.
+ (packet checked to be long enough to contain rdlen above)
+ We don't need to do any further length checks in check_nes3_coverage()
+ or prove_non_existence_nsec3() */
+
+ int salt_len, hash_len;
+ unsigned char *psav = p;
+
+ if (rdlen < 5)
+ return DNSSEC_FAIL_BADPACKET;
+
+ p += 4; /* algo, flags, iterations */
+ salt_len = *p++; /* salt_len */
+ if (rdlen < (6 + salt_len))
+ return DNSSEC_FAIL_BADPACKET; /* check up to hash_length */
+
+ p += salt_len; /* salt */
+ hash_len = *p++;
+ if (rdlen < (6 + salt_len + hash_len))
+ return DNSSEC_FAIL_BADPACKET; /* check to end of next hashed name */
+
+ p = psav;
}
nsecset[nsecs_found++] = pstart;
}
- if (!ADD_RDLEN(header, p, plen, rdlen))
- return 0;
+ p += rdlen;
}
if (type_found == T_NSEC)
return prove_non_existence_nsec(header, plen, nsecset, rrsig_labels, nsecs_found, daemon->workspacename, keyname, name, qtype, nons);
else if (type_found == T_NSEC3)
- return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons);
+ return prove_non_existence_nsec3(header, plen, nsecset, nsecs_found, daemon->workspacename, keyname, name, qtype, wildname, nons, validate_counter);
else
- return 0;
+ return DNSSEC_FAIL_NONSEC;
}
/* Check signing status of name.
@@ -1807,16 +1980,17 @@ static int zone_status(char *name, int class, char *keyname, time_t now)
STAT_BOGUS signature is wrong, bad packet, no validation where there should be.
STAT_NEED_KEY need DNSKEY to complete validation (name is returned in keyname, class in *class)
STAT_NEED_DS need DS to complete validation (name is returned in keyname)
+ STAT_ABANDONED resource exhaustion.
daemon->rr_status points to a char array which corressponds to the RRs in the
- answer and auth sections. This is set to 1 for each RR which is validated, and 0 for any which aren't.
+ answer and auth sections. This is set to >1 for each RR which is validated, and 0 for any which aren't.
When validating replies to DS records, we're only interested in the NSEC{3} RRs in the auth section.
Other RRs in that section missing sigs will not cause am INSECURE reply. We determine this mode
- is the nons argument is non-NULL.
+ if the nons argument is non-NULL.
*/
int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname,
- int *class, int check_unsigned, int *neganswer, int *nons, int *nsec_ttl)
+ int *class, int check_unsigned, int *neganswer, int *nons, int *nsec_ttl, int *validate_counter)
{
static unsigned char **targets = NULL;
static int target_sz = 0;
@@ -1825,7 +1999,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
int type1, class1, rdlen1 = 0, type2, class2, rdlen2, qclass, qtype, targetidx;
int i, j, rc = STAT_INSECURE;
int secure = STAT_SECURE;
-
+ int rc_nsec;
/* extend rr_status if necessary */
if (daemon->rr_status_sz < ntohs(header->ancount) + ntohs(header->nscount))
{
@@ -1854,7 +2028,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
/* Find all the targets we're looking for answers to.
The zeroth array element is for the query, subsequent ones
- for CNAME targets, unless the query is for a CNAME. */
+ for CNAME targets, unless the query is for a CNAME or ANY. */
if (!expand_workspace(&targets, &target_sz, 0))
return STAT_BOGUS;
@@ -1873,7 +2047,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
if (qtype == T_RRSIG)
return STAT_INSECURE;
- if (qtype != T_CNAME)
+ if (qtype != T_CNAME && qtype != T_ANY)
for (j = ntohs(header->ancount); j != 0; j--)
{
if (!(p1 = skip_name(p1, header, plen, 10)))
@@ -1947,7 +2121,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
{
/* NSEC and NSEC3 records must be signed. We make this assumption elsewhere. */
if (type1 == T_NSEC || type1 == T_NSEC3)
- rc = STAT_INSECURE;
+ return STAT_BOGUS | DNSSEC_FAIL_NOSIG;
else if (nons && i >= ntohs(header->ancount))
/* If we're validating a DS reply, rather than looking for the value of AD bit,
we only care that NSEC and NSEC3 RRs in the auth section are signed.
@@ -1959,15 +2133,16 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
if (check_unsigned && i < ntohs(header->ancount))
{
rc = zone_status(name, class1, keyname, now);
- if (rc == STAT_SECURE)
- rc = STAT_BOGUS;
+ if (STAT_ISEQUAL(rc, STAT_SECURE))
+ rc = STAT_BOGUS | DNSSEC_FAIL_NOSIG;
+
if (class)
*class = class1; /* Class for NEED_DS or NEED_KEY */
}
else
rc = STAT_INSECURE;
- if (rc != STAT_INSECURE)
+ if (!STAT_ISEQUAL(rc, STAT_INSECURE))
return rc;
}
}
@@ -1978,7 +2153,7 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
strcpy(daemon->workspacename, keyname);
rc = zone_status(daemon->workspacename, class1, keyname, now);
- if (rc == STAT_BOGUS || rc == STAT_NEED_KEY || rc == STAT_NEED_DS)
+ if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS))
{
if (class)
*class = class1; /* Class for NEED_DS or NEED_KEY */
@@ -1986,13 +2161,13 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
}
/* Zone is insecure, don't need to validate RRset */
- if (rc == STAT_SECURE)
+ if (STAT_ISEQUAL(rc, STAT_SECURE))
{
unsigned long sig_ttl;
rc = validate_rrset(now, header, plen, class1, type1, sigcnt,
- rrcnt, name, keyname, &wildname, NULL, 0, 0, 0, &sig_ttl);
+ rrcnt, name, keyname, &wildname, NULL, 0, 0, 0, &sig_ttl, validate_counter);
- if (rc == STAT_BOGUS || rc == STAT_NEED_KEY || rc == STAT_NEED_DS)
+ if (STAT_ISEQUAL(rc, STAT_BOGUS) || STAT_ISEQUAL(rc, STAT_NEED_KEY) || STAT_ISEQUAL(rc, STAT_NEED_DS) || STAT_ISEQUAL(rc, STAT_ABANDONED))
{
if (class)
*class = class1; /* Class for DS or DNSKEY */
@@ -2025,50 +2200,53 @@ int dnssec_validate_reply(time_t now, struct dns_header *header, size_t plen, ch
Note that we may not yet have validated the NSEC/NSEC3 RRsets.
That's not a problem since if the RRsets later fail
we'll return BOGUS then. */
- if (rc == STAT_SECURE_WILDCARD &&
- !prove_non_existence(header, plen, keyname, name, type1, class1, wildname, NULL, NULL))
- return STAT_BOGUS;
+ 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 (rc_nsec & DNSSEC_FAIL_WORK) ? STAT_ABANDONED : (STAT_BOGUS | rc_nsec);
rc = STAT_SECURE;
}
}
}
- if (rc == STAT_INSECURE)
+ if (STAT_ISEQUAL(rc, STAT_INSECURE))
secure = STAT_INSECURE;
}
/* OK, all the RRsets validate, now see if we have a missing answer or CNAME target. */
- if (secure == STAT_SECURE)
- for (j = 0; j <targetidx; j++)
- if ((p2 = targets[j]))
- {
- if (neganswer)
- *neganswer = 1;
-
- if (!extract_name(header, plen, &p2, name, 1, 10))
- return STAT_BOGUS; /* bad packet */
-
- /* NXDOMAIN or NODATA reply, unanswered question is (name, qclass, qtype) */
-
- /* For anything other than a DS record, this situation is OK if either
- the answer is in an unsigned zone, or there's a NSEC records. */
- if (!prove_non_existence(header, plen, keyname, name, qtype, qclass, NULL, nons, nsec_ttl))
- {
- /* Empty DS without NSECS */
- if (qtype == T_DS)
- return STAT_BOGUS;
-
- if ((rc = zone_status(name, qclass, keyname, now)) != STAT_SECURE)
- {
- if (class)
- *class = qclass; /* Class for NEED_DS or NEED_KEY */
- return rc;
- }
-
- return STAT_BOGUS; /* signed zone, no NSECs */
- }
- }
+ for (j = 0; j <targetidx; j++)
+ if ((p2 = targets[j]))
+ {
+ if (neganswer)
+ *neganswer = 1;
+
+ if (!extract_name(header, plen, &p2, name, 1, 10))
+ return STAT_BOGUS; /* bad packet */
+
+ /* NXDOMAIN or NODATA reply, unanswered question is (name, qclass, qtype) */
+
+ /* For anything other than a DS record, this situation is OK if either
+ 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;
+
+ if ((rc_nsec & (DNSSEC_FAIL_NONSEC | DNSSEC_FAIL_NSEC3_ITERS)) &&
+ !STAT_ISEQUAL((rc = zone_status(name, qclass, keyname, now)), STAT_SECURE))
+ {
+ if (class)
+ *class = qclass; /* Class for NEED_DS or NEED_KEY */
+ return rc;
+ }
+
+ return STAT_BOGUS | rc_nsec; /* signed zone, no NSECs */
+ }
+ }
return secure;
}
diff --git a/src/forward.c b/src/forward.c
index 9ec426e..388b0c3 100644
--- a/src/forward.c
+++ b/src/forward.c
@@ -481,7 +481,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->work_counter = daemon->limit[LIMIT_WORK];
+ forward->validate_counter = daemon->limit[LIMIT_CRYPTO];
if (do_bit)
forward->flags |= FREC_DO_QUESTION;
#endif
@@ -865,6 +866,7 @@ void reply_query(int fd, time_t now)
serveraddr.in6.sin6_flowinfo = 0;
header = (struct dns_header *)daemon->packet;
+ size_t plen;
if (n < (int)sizeof(struct dns_header) || !(header->hb3 & HB3_QR))
return;
@@ -909,7 +911,6 @@ void reply_query(int fd, time_t now)
/* for broken servers, attempt to send to another one. */
{
unsigned char *pheader;
- size_t plen;
int is_sign;
#ifdef HAVE_DNSSEC
@@ -1048,34 +1049,40 @@ void reply_query(int fd, time_t now)
*/
if (header->hb3 & HB3_TC)
status = STAT_TRUNCATED;
-
+
+ struct frec *orig;
+ int log_resource = 0;
+
+
while (1)
{
/* As soon as anything returns BOGUS, we stop and unwind, to do otherwise
would invite infinite loops, since the answers to DNSKEY and DS queries
will not be cached, so they'll be repeated. */
+ /* Find the original query that started it all.... */
+ for (orig = forward; orig->dependent; orig = orig->dependent);
+
if (status != STAT_BOGUS && status != STAT_TRUNCATED && status != STAT_ABANDONED)
{
if (forward->flags & FREC_DNSKEY_QUERY)
- status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class);
+ status = dnssec_validate_by_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else if (forward->flags & FREC_DS_QUERY)
- status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class);
+ status = dnssec_validate_ds(now, header, n, daemon->namebuff, daemon->keyname, forward->class, &orig->validate_counter);
else
status = dnssec_validate_reply(now, header, n, daemon->namebuff, daemon->keyname, &forward->class,
!option_bool(OPT_DNSSEC_IGN_NS) && (forward->sentto->flags & SERV_DO_DNSSEC),
- NULL, NULL, NULL);
-#ifdef HAVE_DUMPFILE
- if (status == STAT_BOGUS)
- dump_packet((forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) ? DUMP_SEC_BOGUS : DUMP_BOGUS,
- header, (size_t)n, &serveraddr, NULL);
-#endif
+ NULL, NULL, NULL, &orig->validate_counter);
}
+ status = status & 0xffff0000;
+
+ if (status == STAT_ABANDONED)
+ log_resource = 1;
/* Can't validate, as we're missing key data. Put this
answer aside, whilst we get that. */
if (status == STAT_NEED_DS || status == STAT_NEED_KEY)
{
- struct frec *new, *orig;
+ struct frec *new;
/* Free any saved query */
if (forward->stash)
@@ -1086,13 +1093,13 @@ void reply_query(int fd, time_t now)
return;
forward->stash_len = n;
- /* Find the original query that started it all.... */
- for (orig = forward; orig->dependent; orig = orig->dependent);
-
/* Make sure we don't expire and free the orig frec during the
allocation of a new one. */
- if (--orig->work_counter == 0 || !(new = get_new_frec(now, NULL, orig)))
+ if (--orig->work_counter == 0 || !(new = get_new_frec(now, NULL, orig))) {
status = STAT_ABANDONED;
+ my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
+ log_resource = 1;
+ }
else
{
int querytype, fd, type = SERV_DO_DNSSEC;
@@ -1180,7 +1187,20 @@ void reply_query(int fd, time_t now)
}
return;
}
-
+ if (log_resource)
+ {
+ /* Log the actual validation that made us barf. */
+ unsigned char *p = (unsigned char *)(header+1);
+ if (extract_name(header, plen, &p, daemon->namebuff, 0, 4) == 1)
+ my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
+ daemon->namebuff[0] ? daemon->namebuff : ".");
+ }
+#ifdef HAVE_DUMPFILE
+ if (status == STAT_BOGUS)
+ dump_packet((forward->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) ? DUMP_SEC_BOGUS : DUMP_BOGUS,
+ header, (size_t)n, &serveraddr, NULL);
+#endif
+
/* Validated original answer, all done. */
if (!forward->dependent)
break;
@@ -1216,6 +1236,11 @@ void reply_query(int fd, time_t now)
domain = daemon->namebuff;
log_query(F_SECSTAT, domain, NULL, result);
+ if ((daemon->limit[LIMIT_CRYPTO] - orig->validate_counter) > (int)daemon->metrics[METRIC_CRYPTO_HWM])
+ daemon->metrics[METRIC_CRYPTO_HWM] = daemon->limit[LIMIT_CRYPTO] - orig->validate_counter;
+ if ((daemon->limit[LIMIT_WORK] - orig->work_counter) > (int)daemon->metrics[METRIC_WORK_HWM])
+ daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - orig->work_counter;
+
}
if (status == STAT_SECURE)
@@ -1622,7 +1647,7 @@ void receive_query(struct listener *listen, time_t now)
/* Recurse up the key hierarchy */
static int tcp_key_recurse(time_t now, int status, struct dns_header *header, size_t n,
int class, char *name, char *keyname, struct server *server,
- int have_mark, unsigned int mark, int *keycount)
+ int have_mark, unsigned int mark, int *keycount, int *validatecount)
{
int new_status;
unsigned char *packet = NULL;
@@ -1639,19 +1664,35 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
struct server *firstsendto = NULL;
/* limit the amount of work we do, to avoid cycling forever on loops in the DNS */
- if (--(*keycount) == 0)
- new_status = STAT_ABANDONED;
- else if (status == STAT_NEED_KEY)
- new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class);
+ if (status == STAT_NEED_KEY)
+ new_status = dnssec_validate_by_ds(now, header, n, name, keyname, class, validatecount);
else if (status == STAT_NEED_DS)
- new_status = dnssec_validate_ds(now, header, n, name, keyname, class);
+ new_status = dnssec_validate_ds(now, header, n, name, keyname, class, validatecount);
else
new_status = dnssec_validate_reply(now, header, n, name, keyname, &class,
!option_bool(OPT_DNSSEC_IGN_NS) && (server->flags & SERV_DO_DNSSEC),
- NULL, NULL, NULL);
-
- if (new_status != STAT_NEED_DS && new_status != STAT_NEED_KEY)
+ NULL, NULL, NULL, validatecount);
+ new_status = new_status & 0xffff0000;
+
+ if (new_status != STAT_NEED_DS && new_status != STAT_NEED_KEY && new_status != STAT_ABANDONED)
break;
+
+ if ((*keycount)-- == 0)
+ {
+ my_syslog(LOG_WARNING, _("limit exceeded: per-query subqueries"));
+ new_status = STAT_ABANDONED;
+ }
+
+ if (new_status == STAT_ABANDONED)
+ {
+ /* Log the actual validation that made us barf. */
+ unsigned char *p = (unsigned char *)(header+1);
+ if (extract_name(header, n, &p, daemon->namebuff, 0, 4) == 1)
+ my_syslog(LOG_WARNING, _("validation of %s failed: resource limit exceeded."),
+ daemon->namebuff[0] ? daemon->namebuff : ".");
+ break;
+ }
+
/* Can't validate because we need a key/DS whose name now in keyname.
Make query for same, and recurse to validate */
@@ -1668,7 +1709,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
new_status = STAT_ABANDONED;
break;
}
-
+
m = dnssec_generate_query(new_header, ((unsigned char *) new_header) + 65536, keyname, class,
new_status == STAT_NEED_KEY ? T_DNSKEY : T_DS, server->edns_pktsz);
@@ -1763,7 +1804,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si
server->flags |= SERV_GOT_TCP;
m = (c1 << 8) | c2;
- new_status = tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server, have_mark, mark, keycount);
+ new_status = tcp_key_recurse(now, new_status, new_header, m, class, name, keyname, server, have_mark, mark, keycount, validatecount);
break;
}
@@ -2085,9 +2126,10 @@ unsigned char *tcp_request(int confd, time_t now,
#ifdef HAVE_DNSSEC
if (option_bool(OPT_DNSSEC_VALID) && !checking_disabled && (last_server->flags & SERV_DO_DNSSEC))
{
- int keycount = DNSSEC_WORK; /* Limit to number of DNSSEC questions, to catch loops and avoid filling cache. */
+ 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,
- last_server, have_mark, mark, &keycount);
+ last_server, have_mark, mark, &keycount, &validatecount);
char *result, *domain = "result";
if (status == STAT_ABANDONED)
@@ -2102,6 +2144,12 @@ unsigned char *tcp_request(int confd, time_t now,
domain = daemon->namebuff;
log_query(F_SECSTAT, domain, NULL, result);
+
+ 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[LIMIT_WORK] - keycount) > (int)daemon->metrics[METRIC_WORK_HWM])
+ daemon->metrics[METRIC_WORK_HWM] = daemon->limit[LIMIT_WORK] - keycount;
if (status == STAT_BOGUS)
{
diff --git a/src/metrics.c b/src/metrics.c
index fac5b00..f32f426 100644
--- a/src/metrics.c
+++ b/src/metrics.c
@@ -37,6 +37,9 @@ const char * metric_names[] = {
"leases_pruned_4",
"leases_allocated_6",
"leases_pruned_6",
+ "dnssec_max_crypto_use",
+ "dnssec_max_sig_fail",
+ "dnssec_max_work"
};
const char* get_metric_name(int i) {
diff --git a/src/metrics.h b/src/metrics.h
index cecf8c8..ee0b93a 100644
--- a/src/metrics.h
+++ b/src/metrics.h
@@ -36,6 +36,9 @@ enum {
METRIC_LEASES_PRUNED_4,
METRIC_LEASES_ALLOCATED_6,
METRIC_LEASES_PRUNED_6,
+ METRIC_CRYPTO_HWM,
+ METRIC_SIG_FAIL_HWM,
+ METRIC_WORK_HWM,
__METRIC_MAX,
};
diff --git a/src/option.c b/src/option.c
index 25680c0..8b40332 100644
--- a/src/option.c
+++ b/src/option.c
@@ -170,6 +170,7 @@ struct myoption {
#define LOPT_PXE_VENDOR 361
#define LOPT_DYNHOST 362
#define LOPT_LOG_DEBUG 363
+#define LOPT_DNSSEC_LIMITS 364
#ifdef HAVE_GETOPT_LONG
static const struct option opts[] =
@@ -330,6 +331,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 },
@@ -513,6 +515,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 },
@@ -4524,6 +4527,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;
@@ -5084,6 +5105,12 @@ void read_opts(int argc, char **argv, char *compile_opts)
daemon->soa_refresh = SOA_REFRESH;
daemon->soa_retry = SOA_RETRY;
daemon->soa_expiry = SOA_EXPIRY;
+#ifdef HAVE_DNSSEC
+ 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
#ifndef NO_ID
add_txt("version.bind", "dnsmasq-" VERSION, 0 );
diff --git a/src/rrfilter.c b/src/rrfilter.c
index 58c6d8f..32ef34a 100644
--- a/src/rrfilter.c
+++ b/src/rrfilter.c
@@ -136,9 +136,9 @@ static int check_rrs(unsigned char *p, struct dns_header *header, size_t plen, i
if (class == C_IN)
{
- u16 *d;
+ short *d;
- for (pp = p, d = rrfilter_desc(type); *d != (u16)-1; d++)
+ for (pp = p, d = rrfilter_desc(type); *d != -1; d++)
{
if (*d != 0)
pp += *d;
@@ -266,7 +266,7 @@ size_t rrfilter(struct dns_header *header, size_t plen, int mode)
}
/* This is used in the DNSSEC code too, hence it's exported */
-u16 *rrfilter_desc(int type)
+short *rrfilter_desc(int type)
{
/* List of RRtypes which include domains in the data.
0 -> domain
@@ -277,7 +277,7 @@ u16 *rrfilter_desc(int type)
anything which needs no mangling.
*/
- static u16 rr_desc[] =
+ static short rr_desc[] =
{
T_NS, 0, -1,
T_MD, 0, -1,
@@ -302,10 +302,10 @@ u16 *rrfilter_desc(int type)
0, -1 /* wildcard/catchall */
};
- u16 *p = rr_desc;
+ short *p = rr_desc;
while (*p != type && *p != 0)
- while (*p++ != (u16)-1);
+ while (*p++ != -1);
return p+1;
}