From 3a2371527ff3383526e9c1f38d4a08010009ffd6 Mon Sep 17 00:00:00 2001 From: Simon Kelley Date: Thu, 12 Dec 2013 12:15:50 +0000 Subject: [PATCH] Commit to allow master merge. --- src/dnsmasq.c | 6 +- src/dnsmasq.h | 34 ++++++++--- src/dnssec.c | 14 ++--- src/forward.c | 165 +++++++++++++++++++++++++++++++++++++++++++------- src/option.c | 2 +- src/rfc1035.c | 40 ++++++++---- 6 files changed, 209 insertions(+), 52 deletions(-) diff --git a/src/dnsmasq.c b/src/dnsmasq.c index f9c2c30..8dbe275 100644 --- a/src/dnsmasq.c +++ b/src/dnsmasq.c @@ -81,10 +81,10 @@ int main (int argc, char **argv) umask(022); /* known umask, create leases and pid files as 0644 */ read_opts(argc, argv, compile_opts); - if (option_bool(OPT_DNSSEC_VALIDATE)) + if (option_bool(OPT_DNSSEC_VALID)) if (daemon->doctors) exit(1); /* TODO */ if (daemon->edns_pktsz < PACKETSZ) - daemon->edns_pktsz = option_bool(OPT_DNSSEC_VALIDATE) ? EDNS_PKTSZ : PACKETSZ; + daemon->edns_pktsz = option_bool(OPT_DNSSEC_VALID) ? EDNS_PKTSZ : PACKETSZ; daemon->packet_buff_sz = daemon->edns_pktsz > DNSMASQ_PACKETSZ ? daemon->edns_pktsz : DNSMASQ_PACKETSZ; daemon->packet = safe_malloc(daemon->packet_buff_sz); @@ -1302,7 +1302,7 @@ static int set_dns_listeners(time_t now, fd_set *set, int *maxfdp) /* will we be able to get memory? */ if (daemon->port != 0) - get_new_frec(now, &wait); + get_new_frec(now, &wait, 0); for (serverfdp = daemon->sfds; serverfdp; serverfdp = serverfdp->next) { diff --git a/src/dnsmasq.h b/src/dnsmasq.h index f96a94c..8783676 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -228,7 +228,7 @@ struct event_desc { #define OPT_QUIET_DHCP 42 #define OPT_QUIET_DHCP6 43 #define OPT_QUIET_RA 44 -#define OPT_DNSSEC_VALIDATE 45 +#define OPT_DNSSEC_VALID 45 #define OPT_LAST 46 /* extra flags for my_syslog, we use a couple of facilities since they are known @@ -352,7 +352,7 @@ struct crec { int uid; /* -1 if union is interface-name */ } cname; struct { - struct keydata *keydata; + struct blockdata *keydata; unsigned char algo; unsigned char digest; /* DS only */ unsigned short keytag; @@ -499,9 +499,20 @@ struct hostsfile { int index; /* matches to cache entries for logging */ }; + +/* 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 FREC_NOREBIND 1 #define FREC_CHECKING_DISABLED 2 #define FREC_HAS_SUBNET 4 +#define FREC_DNSSEC_QUERY 8 +#define FREC_DNSKEY_QUERY 16 +#define FREC_DS_QUERY 32 struct frec { union mysockaddr source; @@ -516,6 +527,12 @@ struct frec { int fd, forwardall, flags; unsigned int crc; time_t time; +#ifdef HAVE_DNSSEC + struct blockdata *stash; /* Saved reply, whilst we validate */ + size_t stash_len; + struct frec *dependent; /* Query awaiting internally-generated DNSKEY or DS query */ + struct frec *blocking_query; /* Query which is blocking us. */ +#endif struct frec *next; }; @@ -954,9 +971,9 @@ char *cache_get_name(struct crec *crecp); char *cache_get_cname_target(struct crec *crecp); struct crec *cache_enumerate(int init); #ifdef HAVE_DNSSEC -struct keydata *keydata_alloc(char *data, size_t len); -size_t keydata_walk(struct keydata **key, unsigned char **p, size_t cnt); -void keydata_free(struct keydata *blocks); +struct blockdata *blockdata_alloc(char *data, size_t len); +size_t blockdata_walk(struct blockdata **key, unsigned char **p, size_t cnt); +void blockdata_free(struct blockdata *blocks); #endif /* domain.c */ @@ -992,6 +1009,9 @@ size_t resize_packet(struct dns_header *header, size_t plen, unsigned char *pheader, size_t hlen); size_t add_mac(struct dns_header *header, size_t plen, char *limit, union mysockaddr *l3); size_t add_source_addr(struct dns_header *header, size_t plen, char *limit, union mysockaddr *source); +#ifdef HAVE_DNSSEC +size_t add_do_bit(struct dns_header *header, size_t plen, char *limit); +#endif int check_source(struct dns_header *header, size_t plen, unsigned char *pseudoheader, union mysockaddr *peer); int add_resource_record(struct dns_header *header, char *limit, int *truncp, int nameoffset, unsigned char **pp, unsigned long ttl, @@ -1010,7 +1030,7 @@ int in_zone(struct auth_zone *zone, char *name, char **cut); #endif /* dnssec.c */ -int dnssec_validate(struct dns_header *header, size_t plen); +int dnssec_validate(int flags, struct dns_header *header, size_t plen); /* util.c */ void rand_init(void); @@ -1072,7 +1092,7 @@ void receive_query(struct listener *listen, time_t now); unsigned char *tcp_request(int confd, time_t now, union mysockaddr *local_addr, struct in_addr netmask, int auth_dns); void server_gone(struct server *server); -struct frec *get_new_frec(time_t now, int *wait); +struct frec *get_new_frec(time_t now, int *wait, int force); int send_from(int fd, int nowild, char *packet, size_t len, union mysockaddr *to, struct all_addr *source, unsigned int iface); diff --git a/src/dnssec.c b/src/dnssec.c index 222be3f..a1f7856 100644 --- a/src/dnssec.c +++ b/src/dnssec.c @@ -713,7 +713,7 @@ int dnssec_parsekey(struct dns_header *header, size_t pktlen, char *owner, unsig int rdlen, unsigned char *rdata) { int flags, proto, alg; - struct keydata *key; struct crec *crecp; + struct blockdata *key; struct crec *crecp; unsigned char *ordata = rdata; int ordlen = rdlen; CHECKED_GETSHORT(flags, rdata, rdlen); @@ -726,7 +726,7 @@ int dnssec_parsekey(struct dns_header *header, size_t pktlen, char *owner, unsig if (!(flags & 0x100)) return 0; - key = keydata_alloc((char*)rdata, rdlen); + key = blockdata_alloc((char*)rdata, rdlen); /* TODO: time(0) is correct here? */ crecp = cache_insert(owner, NULL, time(0), ttl, F_FORWARD | F_DNSKEY); @@ -741,7 +741,7 @@ int dnssec_parsekey(struct dns_header *header, size_t pktlen, char *owner, unsig } else { - keydata_free(key); + blockdata_free(key); /* TODO: if insertion really might fail, verify we don't depend on cache insertion success for validation workflow correctness */ printf("DNSKEY: cache insertion failure\n"); @@ -754,7 +754,7 @@ int dnssec_parseds(struct dns_header *header, size_t pktlen, char *owner, unsign int rdlen, unsigned char *rdata) { int keytag, algo, dig; - struct keydata *key; struct crec *crec_ds, *crec_key; + struct blockdata *key; struct crec *crec_ds, *crec_key; CHECKED_GETSHORT(keytag, rdata, rdlen); CHECKED_GETCHAR(algo, rdata, rdlen); @@ -763,13 +763,13 @@ int dnssec_parseds(struct dns_header *header, size_t pktlen, char *owner, unsign if (!digestalg_supported(dig)) return 0; - key = keydata_alloc((char*)rdata, rdlen); + key = blockdata_alloc((char*)rdata, rdlen); /* TODO: time(0) is correct here? */ crec_ds = cache_insert(owner, NULL, time(0), ttl, F_FORWARD | F_DS); if (!crec_ds) { - keydata_free(key); + blockdata_free(key); /* TODO: if insertion really might fail, verify we don't depend on cache insertion success for validation workflow correctness */ printf("DS: cache insertion failure\n"); @@ -800,7 +800,7 @@ int dnssec_parseds(struct dns_header *header, size_t pktlen, char *owner, unsign return 0; } -int dnssec_validate(struct dns_header *header, size_t pktlen) +int dnssec1_validate(struct dns_header *header, size_t pktlen) { unsigned char *p, *reply; char *owner = daemon->namebuff; diff --git a/src/forward.c b/src/forward.c index 1c66acf..2b93af1 100644 --- a/src/forward.c +++ b/src/forward.c @@ -270,7 +270,7 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, if (gotname) flags = search_servers(now, &addrp, gotname, daemon->namebuff, &type, &domain, &norebind); - if (!flags && !(forward = get_new_frec(now, NULL))) + if (!flags && !(forward = get_new_frec(now, NULL, 0))) /* table full - server failure. */ flags = F_NEG; @@ -342,6 +342,11 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, } } +#ifdef HAVE_DNSSEC + if (option_bool(OPT_DNSSEC_VALID)) + plen = add_do_bit(header, plen, ((char *) header) + PACKETSZ); +#endif + while (1) { /* only send to servers dealing with our domain. @@ -447,12 +452,13 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, } static size_t process_reply(struct dns_header *header, time_t now, struct server *server, size_t n, int check_rebind, - int checking_disabled, int check_subnet, union mysockaddr *query_source) + int no_cache, int cache_secure, int check_subnet, union mysockaddr *query_source) { unsigned char *pheader, *sizep; char **sets = 0; int munged = 0, is_sign; size_t plen; + int squash_ad = 0; #ifdef HAVE_IPSET /* Similar algorithm to search_servers. */ @@ -495,11 +501,21 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server } } - /* RFC 4035 sect 4.6 para 3 */ if (!is_sign && !option_bool(OPT_DNSSEC_PROXY)) - header->hb4 &= ~HB4_AD; + squash_ad = 1; + +#ifdef HAVE_DNSSEC + if (option_bool(OPT_DNSSEC_VALID)) + squash_ad = no_cache; + if (cache_secure) + header->hb4 |= HB4_AD; +#endif + + if (squash_ad) + header->hb4 &= ~HB4_AD; + if (OPCODE(header) != QUERY || (RCODE(header) != NOERROR && RCODE(header) != NXDOMAIN)) return n; @@ -513,11 +529,6 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server server->flags |= SERV_WARNED_RECURSIVE; } -#ifdef HAVE_DNSSEC - printf("validate\n"); - dnssec_validate(header, n); -#endif - if (daemon->bogus_addr && RCODE(header) != NXDOMAIN && check_for_bogus_wildcard(header, n, daemon->namebuff, daemon->bogus_addr, now)) { @@ -539,7 +550,7 @@ static size_t process_reply(struct dns_header *header, time_t now, struct server SET_RCODE(header, NOERROR); } - if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, checking_disabled)) + if (extract_addresses(header, n, daemon->namebuff, now, sets, is_sign, check_rebind, no_cache)) { my_syslog(LOG_WARNING, _("possible DNS-rebind attack detected: %s"), daemon->namebuff); munged = 1; @@ -597,9 +608,7 @@ void reply_query(int fd, int family, time_t now) n < (int)sizeof(struct dns_header) || !(header->hb3 & HB3_QR) || !(forward = lookup_frec(ntohs(header->id), questions_crc(header, n, daemon->namebuff)))) return; - - server = forward->sentto; - + if ((RCODE(header) == SERVFAIL || RCODE(header) == REFUSED) && !option_bool(OPT_ORDER) && forward->forwardall == 0) @@ -624,6 +633,8 @@ void reply_query(int fd, int family, time_t now) } } } + + server = forward->sentto; if ((forward->sentto->flags & SERV_TYPE) == 0) { @@ -645,7 +656,7 @@ void reply_query(int fd, int family, time_t now) if (!option_bool(OPT_ALL_SERVERS)) daemon->last_server = server; } - + /* If the answer is an error, keep the forward record in place in case we get a good reply from another server. Kill it when we've had replies from all to avoid filling the forwarding table when @@ -653,12 +664,106 @@ void reply_query(int fd, int family, time_t now) if (forward->forwardall == 0 || --forward->forwardall == 1 || (RCODE(header) != REFUSED && RCODE(header) != SERVFAIL)) { - int check_rebind = !(forward->flags & FREC_NOREBIND); + int check_rebind = 0, no_cache_dnssec = 0, cache_secure = 0; - if (!option_bool(OPT_NO_REBIND)) - check_rebind = 0; + if (option_bool(OPT_NO_REBIND)) + check_rebind = !(forward->flags & FREC_NOREBIND); - if ((nn = process_reply(header, now, server, (size_t)n, check_rebind, forward->flags & FREC_CHECKING_DISABLED, + /* Don't cache replies where DNSSEC validation was turned off, either + the upstream server told us so, or the original query specified it. */ + if ((header->hb4 & HB4_CD) || (forward->flags & FREC_CHECKING_DISABLED)) + no_cache_dnssec = 1; + +#ifdef HAVE_DNSSEC + if (option_bool(OPT_DNSSEC_VALID) && !(forward->flags & FREC_CHECKING_DISABLED)) + { + int status = dnssec_validate(forward->flags, header, n); + + /* 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; + if ((forward->stash = blockdata_alloc((char *)header, n))) + { + forward->stash_len = n; + + /* Now formulate a query for the missing data. */ + nn = dnssec_generate_query(header, status); + new = get_new_frec(now, NULL, 1); + + if (new) + { + int fd; + + new = forward; /* copy everything, then overwrite */ + new->dependent = forward; /* to find query awaiting new one. */ + forward->blocking_query = new; /* for garbage cleaning */ + new->flags |= FREC_DNSSEC_QUERY; + if (status == STAT_NEED_KEY) + new->flags |= FREC_DNSKEY_QUERY; /* So we verify differently */ + else if (status == STAT_NEED_DS) + new->flags |= FREC_DS_QUERY; + new->crc = questions_crc(header, nn, daemon->namebuff); + new->new_id = get_id(new->crc); + + /* Don't resend this. */ + daemon->srv_save = NULL; + + if (server->sfd) + fd = server->sfd->fd; + else +#ifdef HAVE_IPV6 + /* Note that we use the same random port for the DNSSEC stuff */ + if (server->addr.sa.sa_family == AF_INET6) + { + fd = new->rfd6->fd; + new->rfd6->refcount++; + } + else +#endif + { + fd = new->rfd4->fd; + new->rfd4->refcount++; + } + + /* Send DNSSEC query to same server as original query */ + while (sendto(fd, (char *)header, nn, 0, &server->addr.sa, sa_len(&server->addr)) == -1 && retry_send()); + } + } + return; + } + + /* Ok, we reached far enough up the chain-of-trust that we can validate something. + Now wind back down, pulling back answers which wouldn't previously validate + and validate them with the new data. Failure to find needed data here is an internal error. + Once we get to the original answer (FREC_DNSSEC_QUERY not set) and it validates, + return it to the original requestor. */ + while (forward->flags & FREC_DNSSEC_QUERY) + { + if (status == STAT_SECURE) + extract_dnssec_replies(); + free_frec(forward); + forward = forward->dependent; + blockdata_retrieve_and_free(forward->stash, forward->stash_len, (void *)header); + n = forward->stash_len; + if (status == STAT_SECURE) + { + status = dnssec_validate(forward->flags, header, n); + if (status == STAT_NEED_DS || status == STAT_NEED_KEY) + my_syslog(LOG_ERR, _("Unexpected missing data for DNSSEC validation")); + } + } + + if (status == STAT_SECURE) + cache_secure = 1; + /* TODO return SERVFAIL here */ + else if (status == STAT_BOGUS) + no_cache_dnssec = 1; + } +#endif + + if ((nn = process_reply(header, now, server, (size_t)n, check_rebind, no_cache_dnssec, cache_secure, forward->flags & FREC_HAS_SUBNET, &forward->source))) { header->id = htons(forward->orig_id); @@ -1129,7 +1234,7 @@ unsigned char *tcp_request(int confd, time_t now, if (crc == questions_crc(header, (unsigned int)m, daemon->namebuff)) m = process_reply(header, now, last_server, (unsigned int)m, option_bool(OPT_NO_REBIND) && !norebind, checking_disabled, - check_subnet, &peer_addr); + 0, check_subnet, &peer_addr); /* TODO - cache secure */ break; } @@ -1163,6 +1268,9 @@ static struct frec *allocate_frec(time_t now) f->flags = 0; #ifdef HAVE_IPV6 f->rfd6 = NULL; +#endif +#ifdef HAVE_DNSSEC + f->blocking_query = NULL; #endif daemon->frec_list = f; } @@ -1221,13 +1329,26 @@ static void free_frec(struct frec *f) f->rfd6 = NULL; #endif + +#ifdef HAVE_DNSSEC + if (f->stash) + blockdata_free(f->stash); + + /* Anything we're waiting on is pointless now, too */ + if (f->blocking_query) + free_frec(f->blocking_query); + f->blocking_query = NULL; + +#endif } /* if wait==NULL return a free or older than TIMEOUT record. else return *wait zero if one available, or *wait is delay to when the oldest in-use record will expire. Impose an absolute - limit of 4*TIMEOUT before we wipe things (for random sockets) */ -struct frec *get_new_frec(time_t now, int *wait) + limit of 4*TIMEOUT before we wipe things (for random sockets). + If force is set, always return a result, even if we have + to allocate above the limit. */ +struct frec *get_new_frec(time_t now, int *wait, int force) { struct frec *f, *oldest, *target; int count; @@ -1276,7 +1397,7 @@ struct frec *get_new_frec(time_t now, int *wait) } /* none available, calculate time 'till oldest record expires */ - if (count > daemon->ftabsize) + if (!force && count > daemon->ftabsize) { static time_t last_log = 0; diff --git a/src/option.c b/src/option.c index 0ca00b9..8427497 100644 --- a/src/option.c +++ b/src/option.c @@ -427,7 +427,7 @@ static struct { { LOPT_IPSET, ARG_DUP, "//[,...]", gettext_noop("Specify ipsets to which matching domains should be added"), NULL }, { LOPT_SYNTH, ARG_DUP, ",,[]", gettext_noop("Specify a domain and address range for synthesised names"), NULL }, #ifdef HAVE_DNSSEC - { LOPT_SEC_VALID, OPT_DNSSEC_VALIDATE, NULL, gettext_noop("Activate DNSSEC validation"), NULL }, + { LOPT_SEC_VALID, OPT_DNSSEC_VALID, NULL, gettext_noop("Activate DNSSEC validation"), NULL }, #endif #ifdef OPTION6_PREFIX_CLASS { LOPT_PREF_CLSS, ARG_DUP, "set:tag,", gettext_noop("Specify DHCPv6 prefix class"), NULL }, diff --git a/src/rfc1035.c b/src/rfc1035.c index 7188e4e..7321d96 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -515,7 +515,7 @@ struct macparm { }; static size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned char *limit, - int optno, unsigned char *opt, size_t optlen) + int optno, unsigned char *opt, size_t optlen, int set_do) { unsigned char *lenp, *datap, *p; int rdlen; @@ -531,7 +531,8 @@ static size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned *p++ = 0; /* empty name */ PUTSHORT(T_OPT, p); PUTSHORT(daemon->edns_pktsz, p); /* max packet length */ - PUTLONG(0, p); /* extended RCODE */ + PUTSHORT(0, p); /* extended RCODE and version */ + PUTSHORT(set_do ? 0x8000 : 0, p); /* DO flag */ lenp = p; PUTSHORT(0, p); /* RDLEN */ rdlen = 0; @@ -543,7 +544,7 @@ static size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned else { int i, is_sign; - unsigned short code, len; + unsigned short code, len, flags; if (ntohs(header->arcount) != 1 || !(p = find_pseudoheader(header, plen, NULL, NULL, &is_sign)) || @@ -551,14 +552,24 @@ static size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned (!(p = skip_name(p, header, plen, 10)))) return plen; - p += 8; /* skip UDP length and RCODE */ - + p += 6; /* skip UDP length and RCODE */ + GETSHORT(flags, p); + if (set_do) + { + p -=2; + PUTSHORT(flags | 0x8000, p); + } + lenp = p; GETSHORT(rdlen, p); if (!CHECK_LEN(header, p, plen, rdlen)) return plen; /* bad packet */ datap = p; + /* no option to add */ + if (optno == 0) + return plen; + /* check if option already there */ for (i = 0; i + 4 < rdlen; i += len + 4) { @@ -602,7 +613,7 @@ static int filter_mac(int family, char *addrp, char *mac, size_t maclen, void *p if (!match) return 1; /* continue */ - parm->plen = add_pseudoheader(parm->header, parm->plen, parm->limit, EDNS0_OPTION_MAC, (unsigned char *)mac, maclen); + parm->plen = add_pseudoheader(parm->header, parm->plen, parm->limit, EDNS0_OPTION_MAC, (unsigned char *)mac, maclen, 0); return 0; /* done */ } @@ -681,9 +692,16 @@ size_t add_source_addr(struct dns_header *header, size_t plen, char *limit, unio struct subnet_opt opt; len = calc_subnet_opt(&opt, source); - return add_pseudoheader(header, plen, (unsigned char *)limit, EDNS0_OPTION_CLIENT_SUBNET, (unsigned char *)&opt, len); + return add_pseudoheader(header, plen, (unsigned char *)limit, EDNS0_OPTION_CLIENT_SUBNET, (unsigned char *)&opt, len, 0); } - + +#ifdef HAVE_DNSSEC +size_t add_do_bit(struct dns_header *header, size_t plen, char *limit) +{ + return add_pseudoheader(header, plen, (unsigned char *)limit, 0, NULL, 0, 1); +} +#endif + int check_source(struct dns_header *header, size_t plen, unsigned char *pseudoheader, union mysockaddr *peer) { /* Section 9.2, Check that subnet option in reply matches. */ @@ -878,7 +896,7 @@ static int find_soa(struct dns_header *header, size_t qlen, char *name) expired and cleaned out that way. Return 1 if we reject an address because it look like part of dns-rebinding attack. */ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t now, - char **ipsets, int is_sign, int check_rebind, int checking_disabled) + char **ipsets, int is_sign, int check_rebind, int no_cache_dnssec) { unsigned char *p, *p1, *endrr, *namep; int i, j, qtype, qclass, aqtype, aqclass, ardlen, res, searched_soa = 0; @@ -1118,15 +1136,13 @@ int extract_addresses(struct dns_header *header, size_t qlen, char *name, time_t } /* Don't put stuff from a truncated packet into the cache. - Don't cache replies where DNSSEC validation was turned off, either - the upstream server told us so, or the original query specified it. Don't cache replies from non-recursive nameservers, since we may get a reply containing a CNAME but not its target, even though the target does exist. */ if (!(header->hb3 & HB3_TC) && !(header->hb4 & HB4_CD) && (header->hb4 & HB4_RA) && - !checking_disabled) + !no_cache_dnssec) cache_end_insert(); return 0;