diff --git a/CHANGELOG b/CHANGELOG index af2b22c..d8fc57a 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -109,6 +109,12 @@ version 2.73 by quiet-dhcp6. Thanks to J. Pablo Abonia for spotting the problem. + Try and handle net connections with broken fragmentation + that lose large UDP packets. If a server times out, + reduce the maximum UDP packet size field in the EDNS0 + header to 1280 bytes. If it then answers, make that + change permanent. + version 2.72 Add ra-advrouter mode, for RFC-3775 mobile IPv6 support. diff --git a/src/config.h b/src/config.h index 8def6f2..f75fe9d 100644 --- a/src/config.h +++ b/src/config.h @@ -19,6 +19,7 @@ #define CHILD_LIFETIME 150 /* secs 'till terminated (RFC1035 suggests > 120s) */ #define TCP_MAX_QUERIES 100 /* Maximum number of queries per incoming TCP connection */ #define EDNS_PKTSZ 4096 /* default max EDNS.0 UDP packet from RFC5625 */ +#define SAFE_PKTSZ 1280 /* "go anywhere" UDP packet size */ #define KEYBLOCK_LEN 40 /* choose to mininise fragmentation when storing DNSSEC keys */ #define DNSSEC_WORK 50 /* Max number of queries to validate one question */ #define TIMEOUT 10 /* drop UDP queries after TIMEOUT seconds */ diff --git a/src/dnsmasq.h b/src/dnsmasq.h index 824a860..ab16f79 100644 --- a/src/dnsmasq.h +++ b/src/dnsmasq.h @@ -504,7 +504,7 @@ struct server { char interface[IF_NAMESIZE+1]; struct serverfd *sfd; char *domain; /* set if this server only handles a domain. */ - int flags, tcpfd; + int flags, tcpfd, edns_pktsz; unsigned int queries, failed_queries; #ifdef HAVE_LOOP u32 uid; @@ -594,6 +594,7 @@ struct hostsfile { #define FREC_DO_QUESTION 64 #define FREC_ADDED_PHEADER 128 #define FREC_CHECK_NOSIGN 256 +#define FREC_TEST_PKTSZ 512 #ifdef HAVE_DNSSEC #define HASH_SIZE 20 /* SHA-1 digest size */ @@ -1148,7 +1149,7 @@ int in_zone(struct auth_zone *zone, char *name, char **cut); #endif /* dnssec.c */ -size_t dnssec_generate_query(struct dns_header *header, char *end, char *name, int class, int type, union mysockaddr *addr); +size_t dnssec_generate_query(struct dns_header *header, char *end, char *name, int class, int type, union mysockaddr *addr, int edns_pktsz); int dnssec_validate_by_ds(time_t now, struct dns_header *header, size_t n, 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_reply(time_t now, struct dns_header *header, size_t plen, char *name, char *keyname, int *class, int *neganswer, int *nons); diff --git a/src/dnssec.c b/src/dnssec.c index a9e1215..e91d7c2 100644 --- a/src/dnssec.c +++ b/src/dnssec.c @@ -2162,10 +2162,12 @@ int dnskey_keytag(int alg, int flags, unsigned char *key, int keylen) } } -size_t dnssec_generate_query(struct dns_header *header, char *end, char *name, int class, int type, union mysockaddr *addr) +size_t dnssec_generate_query(struct dns_header *header, char *end, char *name, int class, + int type, union mysockaddr *addr, int edns_pktsz) { unsigned char *p; char *types = querystr("dnssec-query", type); + size_t ret; if (addr->sa.sa_family == AF_INET) log_query(F_NOEXTRA | F_DNSSEC | F_IPV4, name, (struct all_addr *)&addr->in.sin_addr, types); @@ -2194,7 +2196,12 @@ size_t dnssec_generate_query(struct dns_header *header, char *end, char *name, i PUTSHORT(type, p); PUTSHORT(class, p); - return add_do_bit(header, p - (unsigned char *)header, end); + ret = add_do_bit(header, p - (unsigned char *)header, end); + + if (find_pseudoheader(header, ret, NULL, &p, NULL)) + PUTSHORT(edns_pktsz, p); + + return ret; } /* Go through a domain name, find "pointers" and fix them up based on how many bytes diff --git a/src/forward.c b/src/forward.c index a8e403c..592243f 100644 --- a/src/forward.c +++ b/src/forward.c @@ -253,6 +253,7 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, void *hash = &crc; #endif unsigned int gotname = extract_request(header, plen, daemon->namebuff, NULL); + unsigned char *pheader; (void)do_bit; @@ -261,19 +262,32 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, forward = NULL; else if (forward || (hash && (forward = lookup_frec_by_sender(ntohs(header->id), udpaddr, hash)))) { + /* If we didn't get an answer advertising a maximal packet in EDNS, + fall back to 1280, which should work everywhere on IPv6. + If that generates an answer, it will become the new default + for this server */ + forward->flags |= FREC_TEST_PKTSZ; + #ifdef HAVE_DNSSEC /* If we've already got an answer to this query, but we're awaiting keys for validation, there's no point retrying the query, retry the key query instead...... */ if (forward->blocking_query) { int fd; - + + forward->flags &= ~FREC_TEST_PKTSZ; + while (forward->blocking_query) forward = forward->blocking_query; + + forward->flags |= FREC_TEST_PKTSZ; blockdata_retrieve(forward->stash, forward->stash_len, (void *)header); plen = forward->stash_len; + if (find_pseudoheader(header, plen, NULL, &pheader, NULL)) + PUTSHORT((forward->flags & FREC_TEST_PKTSZ) ? SAFE_PKTSZ : forward->sentto->edns_pktsz, pheader); + if (forward->sentto->addr.sa.sa_family == AF_INET) log_query(F_NOEXTRA | F_DNSSEC | F_IPV4, "retry", (struct all_addr *)&forward->sentto->addr.in.sin_addr, "dnssec"); #ifdef HAVE_IPV6 @@ -417,7 +431,7 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, plen = new_plen; } #endif - + while (1) { /* only send to servers dealing with our domain. @@ -464,6 +478,9 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr, } #endif } + + if (find_pseudoheader(header, plen, NULL, &pheader, NULL)) + PUTSHORT((forward->flags & FREC_TEST_PKTSZ) ? SAFE_PKTSZ : start->edns_pktsz, pheader); if (retry_send(sendto(fd, (char *)header, plen, 0, &start->addr.sa, @@ -760,7 +777,6 @@ void reply_query(int fd, int family, time_t now) } server = forward->sentto; - if ((forward->sentto->flags & SERV_TYPE) == 0) { if (RCODE(header) == REFUSED) @@ -781,7 +797,12 @@ void reply_query(int fd, int family, time_t now) if (!option_bool(OPT_ALL_SERVERS)) daemon->last_server = server; } - + + /* We tried resending to this server with a smaller maximum size and got an answer. + Make that permanent. */ + if (server && (forward->flags & FREC_TEST_PKTSZ)) + server->edns_pktsz = SAFE_PKTSZ; + /* 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 @@ -890,7 +911,7 @@ void reply_query(int fd, int family, time_t now) { new->flags |= FREC_DNSKEY_QUERY; nn = dnssec_generate_query(header, ((char *) header) + daemon->packet_buff_sz, - daemon->keyname, forward->class, T_DNSKEY, &server->addr); + daemon->keyname, forward->class, T_DNSKEY, &server->addr, server->edns_pktsz); } else { @@ -899,7 +920,7 @@ void reply_query(int fd, int family, time_t now) else new->flags |= FREC_DS_QUERY; nn = dnssec_generate_query(header,((char *) header) + daemon->packet_buff_sz, - daemon->keyname, forward->class, T_DS, &server->addr); + daemon->keyname, forward->class, T_DS, &server->addr, server->edns_pktsz); } if ((hash = hash_questions(header, nn, daemon->namebuff))) memcpy(new->hash, hash, HASH_SIZE); @@ -1526,7 +1547,7 @@ static int tcp_check_for_unsigned_zone(time_t now, struct dns_header *header, s /* Can't find it in the cache, have to send a query */ - m = dnssec_generate_query(header, ((char *) header) + 65536, name_start, class, T_DS, &server->addr); + m = dnssec_generate_query(header, ((char *) header) + 65536, name_start, class, T_DS, &server->addr, server->edns_pktsz); *length = htons(m); @@ -1638,7 +1659,7 @@ static int tcp_key_recurse(time_t now, int status, struct dns_header *header, si another_tcp_key: m = dnssec_generate_query(new_header, ((char *) new_header) + 65536, keyname, class, - new_status == STAT_NEED_KEY ? T_DNSKEY : T_DS, &server->addr); + new_status == STAT_NEED_KEY ? T_DNSKEY : T_DS, &server->addr, server->edns_pktsz); *length = htons(m); diff --git a/src/network.c b/src/network.c index 992f023..a1d90c8 100644 --- a/src/network.c +++ b/src/network.c @@ -1396,6 +1396,7 @@ void add_update_server(int flags, serv->domain = domain_str; serv->next = next; serv->queries = serv->failed_queries = 0; + serv->edns_pktsz = daemon->edns_pktsz; #ifdef HAVE_LOOP serv->uid = rand32(); #endif diff --git a/src/option.c b/src/option.c index f91cfbb..c7add88 100644 --- a/src/option.c +++ b/src/option.c @@ -4498,15 +4498,19 @@ void read_opts(int argc, char **argv, char *compile_opts) { struct server *tmp; for (tmp = daemon->servers; tmp; tmp = tmp->next) - if (!(tmp->flags & SERV_HAS_SOURCE)) - { - if (tmp->source_addr.sa.sa_family == AF_INET) - tmp->source_addr.in.sin_port = htons(daemon->query_port); + { + tmp->edns_pktsz = daemon->edns_pktsz; + + if (!(tmp->flags & SERV_HAS_SOURCE)) + { + if (tmp->source_addr.sa.sa_family == AF_INET) + tmp->source_addr.in.sin_port = htons(daemon->query_port); #ifdef HAVE_IPV6 - else if (tmp->source_addr.sa.sa_family == AF_INET6) - tmp->source_addr.in6.sin6_port = htons(daemon->query_port); + else if (tmp->source_addr.sa.sa_family == AF_INET6) + tmp->source_addr.in6.sin6_port = htons(daemon->query_port); #endif - } + } + } } if (daemon->if_addrs) diff --git a/src/rfc1035.c b/src/rfc1035.c index 5828055..8b1709d 100644 --- a/src/rfc1035.c +++ b/src/rfc1035.c @@ -552,7 +552,7 @@ static size_t add_pseudoheader(struct dns_header *header, size_t plen, unsigned return plen; *p++ = 0; /* empty name */ PUTSHORT(T_OPT, p); - PUTSHORT(daemon->edns_pktsz, p); /* max packet length */ + PUTSHORT(SAFE_PKTSZ, p); /* max packet length, this will be overwritten */ PUTSHORT(0, p); /* extended RCODE and version */ PUTSHORT(set_do ? 0x8000 : 0, p); /* DO flag */ lenp = p; @@ -1537,7 +1537,6 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, unsigned short flag; int q, ans, anscount = 0, addncount = 0; int dryrun = 0, sec_reqd = 0, have_pseudoheader = 0; - int is_sign; struct crec *crecp; int nxdomain = 0, auth = 1, trunc = 0, sec_data = 1; struct mx_srv_record *rec; @@ -1557,28 +1556,19 @@ size_t answer_request(struct dns_header *header, char *limit, size_t qlen, forward rather than answering from the cache, which doesn't include security information, unless we're in DNSSEC validation mode. */ - if (find_pseudoheader(header, qlen, NULL, &pheader, &is_sign)) + if (find_pseudoheader(header, qlen, NULL, &pheader, NULL)) { - unsigned short udpsz, flags; - unsigned char *psave = pheader; - + unsigned short flags; + have_pseudoheader = 1; - GETSHORT(udpsz, pheader); - pheader += 2; /* ext_rcode */ + pheader += 4; /* udp size, ext_rcode */ GETSHORT(flags, pheader); if ((sec_reqd = flags & 0x8000)) *do_bit = 1;/* do bit */ + *ad_reqd = 1; - - /* If our client is advertising a larger UDP packet size - than we allow, trim it so that we don't get an overlarge - response from upstream */ - - if (!is_sign && (udpsz > daemon->edns_pktsz)) - PUTSHORT(daemon->edns_pktsz, psave); - dryrun = 1; }