--- /dev/null
+From a77cec8d58231d71cbc26615f0c0f0292c09ef54 Mon Sep 17 00:00:00 2001
+From: Simon Kelley <simon@thekelleys.org.uk>
+Date: Fri, 8 May 2015 16:25:38 +0100
+Subject: [PATCH] Handle UDP packet loss when fragmentation of large packets
+ is broken.
+
+---
+ CHANGELOG | 6 ++++++
+ src/config.h | 1 +
+ src/dnsmasq.h | 5 +++--
+ src/dnssec.c | 11 +++++++++--
+ src/forward.c | 37 +++++++++++++++++++++++++++++--------
+ src/network.c | 1 +
+ src/option.c | 18 +++++++++++-------
+ src/rfc1035.c | 22 ++++++----------------
+ 8 files changed, 66 insertions(+), 35 deletions(-)
+
+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);
+
++ *ad_reqd = 1;
+ dryrun = 1;
+ }
+
+--
+1.7.10.4