8461a38356
Resolves: RHEL-25639 Resolves: RHEL-25675
2092 lines
77 KiB
Diff
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;
|
|
}
|