ac771d
From 7d98347aeeb575a022ffae630ee02da215b6f37b Mon Sep 17 00:00:00 2001
ac771d
From: Simon Kelley <simon@thekelleys.org.uk>
ac771d
Date: Wed, 18 Nov 2020 18:34:55 +0000
ac771d
Subject: [PATCH 4/4] Handle multiple identical near simultaneous DNS queries
ac771d
 better.
ac771d
ac771d
Previously, such queries would all be forwarded
ac771d
independently. This is, in theory, inefficent but in practise
ac771d
not a problem, _except_ that is means that an answer for any
ac771d
of the forwarded queries will be accepted and cached.
ac771d
An attacker can send a query multiple times, and for each repeat,
ac771d
another {port, ID} becomes capable of accepting the answer he is
ac771d
sending in the blind, to random IDs and ports. The chance of a
ac771d
succesful attack is therefore multiplied by the number of repeats
ac771d
of the query. The new behaviour detects repeated queries and
ac771d
merely stores the clients sending repeats so that when the
ac771d
first query completes, the answer can be sent to all the
ac771d
clients who asked. Refer: CERT VU#434904.
ac771d
---
ac771d
 src/dnsmasq.h |  19 ++++---
ac771d
 src/forward.c | 141 ++++++++++++++++++++++++++++++++++++++++++--------
ac771d
 2 files changed, 132 insertions(+), 28 deletions(-)
ac771d
ac771d
diff --git a/src/dnsmasq.h b/src/dnsmasq.h
ac771d
index f31503d..6744e2b 100644
ac771d
--- a/src/dnsmasq.h
ac771d
+++ b/src/dnsmasq.h
ac771d
@@ -613,21 +613,26 @@ struct hostsfile {
ac771d
 #define FREC_DO_QUESTION       64
ac771d
 #define FREC_ADDED_PHEADER    128
ac771d
 #define FREC_TEST_PKTSZ       256
ac771d
-#define FREC_HAS_EXTRADATA    512        
ac771d
+#define FREC_HAS_EXTRADATA    512
ac771d
+#define FREC_HAS_PHEADER     1024
ac771d
 
ac771d
 #define HASH_SIZE 32 /* SHA-256 digest size */
ac771d
 
ac771d
 struct frec {
ac771d
-  union mysockaddr source;
ac771d
-  struct all_addr dest;
ac771d
+  struct frec_src {
ac771d
+    union mysockaddr source;
ac771d
+    struct all_addr dest;
ac771d
+    unsigned int iface, log_id;
ac771d
+    unsigned short orig_id;
ac771d
+    struct frec_src *next;
ac771d
+  } frec_src;
ac771d
   struct server *sentto; /* NULL means free */
ac771d
   struct randfd *rfd4;
ac771d
 #ifdef HAVE_IPV6
ac771d
   struct randfd *rfd6;
ac771d
 #endif
ac771d
-  unsigned int iface;
ac771d
-  unsigned short orig_id, new_id;
ac771d
-  int log_id, fd, forwardall, flags;
ac771d
+  unsigned short new_id;
ac771d
+  int fd, forwardall, flags;
ac771d
   time_t time;
ac771d
   unsigned char *hash[HASH_SIZE];
ac771d
 #ifdef HAVE_DNSSEC 
ac771d
@@ -1033,6 +1038,8 @@ extern struct daemon {
ac771d
 #endif
ac771d
   unsigned int local_answer, queries_forwarded, auth_answer;
ac771d
   struct frec *frec_list;
ac771d
+  struct frec_src *free_frec_src;
ac771d
+  int frec_src_count;
ac771d
   struct serverfd *sfds;
ac771d
   struct irec *interfaces;
ac771d
   struct listener *listeners;
ac771d
diff --git a/src/forward.c b/src/forward.c
ac771d
index 7ffcaf7..db378a5 100644
ac771d
--- a/src/forward.c
ac771d
+++ b/src/forward.c
ac771d
@@ -20,6 +20,8 @@ static struct frec *lookup_frec(unsigned short id, int fd, int family, void *has
ac771d
 static struct frec *lookup_frec_by_sender(unsigned short id,
ac771d
 					  union mysockaddr *addr,
ac771d
 					  void *hash);
ac771d
+static struct frec *lookup_frec_by_query(void *hash, unsigned int flags);
ac771d
+
ac771d
 static unsigned short get_id(void);
ac771d
 static void free_frec(struct frec *f);
ac771d
 
ac771d
@@ -238,15 +240,28 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
ac771d
   int type = SERV_DO_DNSSEC, norebind = 0;
ac771d
   struct all_addr *addrp = NULL;
ac771d
   unsigned int flags = 0;
ac771d
+  unsigned int fwd_flags = 0;
ac771d
   struct server *start = NULL;
ac771d
   void *hash = hash_questions(header, plen, daemon->namebuff);
ac771d
 #ifdef HAVE_DNSSEC
ac771d
   int do_dnssec = 0;
ac771d
 #endif
ac771d
- unsigned int gotname = extract_request(header, plen, daemon->namebuff, NULL);
ac771d
+  unsigned int gotname = extract_request(header, plen, daemon->namebuff, NULL);
ac771d
+  unsigned char *oph = find_pseudoheader(header, plen, NULL, NULL, NULL, NULL);
ac771d
 
ac771d
  (void)do_bit;
ac771d
-
ac771d
+  
ac771d
+  if (header->hb4 & HB4_CD)
ac771d
+    fwd_flags |= FREC_CHECKING_DISABLED;
ac771d
+  if (ad_reqd)
ac771d
+    fwd_flags |= FREC_AD_QUESTION;
ac771d
+  if (oph)
ac771d
+    fwd_flags |= FREC_HAS_PHEADER;
ac771d
+#ifdef HAVE_DNSSEC
ac771d
+  if (do_bit)
ac771d
+    fwd_flags |= FREC_DO_QUESTION;
ac771d
+#endif
ac771d
+  
ac771d
   /* may be no servers available. */
ac771d
   if (forward || (forward = lookup_frec_by_sender(ntohs(header->id), udpaddr, hash)))
ac771d
     {
ac771d
@@ -322,6 +337,39 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
ac771d
     }
ac771d
   else 
ac771d
     {
ac771d
+      /* Query from new source, but the same query may be in progress
ac771d
+	 from another source. If so, just add this client to the
ac771d
+	 list that will get the reply.
ac771d
+	 
ac771d
+	 Note that is the EDNS client subnet option is in use, we can't do this,
ac771d
+	 as the clients (and therefore query EDNS options) will be different
ac771d
+	 for each query. The EDNS subnet code has checks to avoid
ac771d
+	 attacks in this case. */
ac771d
+      if (!option_bool(OPT_CLIENT_SUBNET) && (forward = lookup_frec_by_query(hash, fwd_flags)))
ac771d
+	{
ac771d
+	  /* Note whine_malloc() zeros memory. */
ac771d
+	  if (!daemon->free_frec_src &&
ac771d
+	      daemon->frec_src_count < daemon->ftabsize &&
ac771d
+	      (daemon->free_frec_src = whine_malloc(sizeof(struct frec_src))))
ac771d
+	    daemon->frec_src_count++;
ac771d
+	  
ac771d
+	  /* If we've been spammed with many duplicates, just drop the query. */
ac771d
+	  if (daemon->free_frec_src)
ac771d
+	    {
ac771d
+	      struct frec_src *new = daemon->free_frec_src;
ac771d
+	      daemon->free_frec_src = new->next;
ac771d
+	      new->next = forward->frec_src.next;
ac771d
+	      forward->frec_src.next = new;
ac771d
+	      new->orig_id = ntohs(header->id);
ac771d
+	      new->source = *udpaddr;
ac771d
+	      new->dest = *dst_addr;
ac771d
+	      new->log_id = daemon->log_id;
ac771d
+	      new->iface = dst_iface;
ac771d
+	    }
ac771d
+	  
ac771d
+	  return 1;
ac771d
+	}
ac771d
+	
ac771d
       if (gotname)
ac771d
 	flags = search_servers(now, &addrp, gotname, daemon->namebuff, &type, &domain, &norebind);
ac771d
       
ac771d
@@ -329,22 +377,22 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
ac771d
       do_dnssec = type & SERV_DO_DNSSEC;
ac771d
 #endif
ac771d
       type &= ~SERV_DO_DNSSEC;      
ac771d
-
ac771d
+      
ac771d
       if (daemon->servers && !flags)
ac771d
 	forward = get_new_frec(now, NULL, 0);
ac771d
       /* table full - flags == 0, return REFUSED */
ac771d
       
ac771d
       if (forward)
ac771d
 	{
ac771d
-	  forward->source = *udpaddr;
ac771d
-	  forward->dest = *dst_addr;
ac771d
-	  forward->iface = dst_iface;
ac771d
-	  forward->orig_id = ntohs(header->id);
ac771d
+	  forward->frec_src.source = *udpaddr;
ac771d
+	  forward->frec_src.orig_id = ntohs(header->id);
ac771d
+	  forward->frec_src.dest = *dst_addr;
ac771d
+	  forward->frec_src.iface = dst_iface;
ac771d
 	  forward->new_id = get_id();
ac771d
 	  forward->fd = udpfd;
ac771d
 	  memcpy(forward->hash, hash, HASH_SIZE);
ac771d
 	  forward->forwardall = 0;
ac771d
-	  forward->flags = 0;
ac771d
+	  forward->flags = fwd_flags;
ac771d
 	  if (norebind)
ac771d
 	    forward->flags |= FREC_NOREBIND;
ac771d
 	  if (header->hb4 & HB4_CD)
ac771d
@@ -400,9 +448,9 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
ac771d
       unsigned char *pheader;
ac771d
       
ac771d
       /* If a query is retried, use the log_id for the retry when logging the answer. */
ac771d
-      forward->log_id = daemon->log_id;
ac771d
+      forward->frec_src.log_id = daemon->log_id;
ac771d
       
ac771d
-      plen = add_edns0_config(header, plen, ((unsigned char *)header) + PACKETSZ, &forward->source, now, &subnet;;
ac771d
+      plen = add_edns0_config(header, plen, ((unsigned char *)header) + PACKETSZ, &forward->frec_src.source, now, &subnet;;
ac771d
       
ac771d
       if (subnet)
ac771d
 	forward->flags |= FREC_HAS_SUBNET;
ac771d
@@ -539,7 +587,7 @@ static int forward_query(int udpfd, union mysockaddr *udpaddr,
ac771d
 	return 1;
ac771d
       
ac771d
       /* could not send on, prepare to return */ 
ac771d
-      header->id = htons(forward->orig_id);
ac771d
+      header->id = htons(forward->frec_src.orig_id);
ac771d
       free_frec(forward); /* cancel */
ac771d
     }	  
ac771d
   
ac771d
@@ -774,8 +822,8 @@ void reply_query(int fd, int family, time_t now)
ac771d
   
ac771d
   /* log_query gets called indirectly all over the place, so 
ac771d
      pass these in global variables - sorry. */
ac771d
-  daemon->log_display_id = forward->log_id;
ac771d
-  daemon->log_source_addr = &forward->source;
ac771d
+  daemon->log_display_id = forward->frec_src.log_id;
ac771d
+  daemon->log_source_addr = &forward->frec_src.source;
ac771d
   
ac771d
   if (daemon->ignore_addr && RCODE(header) == NOERROR &&
ac771d
       check_for_ignored_address(header, n, daemon->ignore_addr))
ac771d
@@ -978,6 +1026,7 @@ void reply_query(int fd, int family, time_t now)
ac771d
 #ifdef HAVE_IPV6
ac771d
 		      new->rfd6 = NULL;
ac771d
 #endif
ac771d
+		      new->frec_src.next = NULL;
ac771d
 		      new->flags &= ~(FREC_DNSKEY_QUERY | FREC_DS_QUERY);
ac771d
 		      
ac771d
 		      new->dependent = forward; /* to find query awaiting new one. */
ac771d
@@ -1099,9 +1148,11 @@ void reply_query(int fd, int family, time_t now)
ac771d
       
ac771d
       if ((nn = process_reply(header, now, forward->sentto, (size_t)n, check_rebind, no_cache_dnssec, cache_secure, bogusanswer, 
ac771d
 			      forward->flags & FREC_AD_QUESTION, forward->flags & FREC_DO_QUESTION, 
ac771d
-			      forward->flags & FREC_ADDED_PHEADER, forward->flags & FREC_HAS_SUBNET, &forward->source)))
ac771d
+			      forward->flags & FREC_ADDED_PHEADER, forward->flags & FREC_HAS_SUBNET, &forward->frec_src.source)))
ac771d
 	{
ac771d
-	  header->id = htons(forward->orig_id);
ac771d
+	  struct frec_src *src;
ac771d
+
ac771d
+	  header->id = htons(forward->frec_src.orig_id);
ac771d
 	  header->hb4 |= HB4_RA; /* recursion if available */
ac771d
 #ifdef HAVE_DNSSEC
ac771d
 	  /* We added an EDNSO header for the purpose of getting DNSSEC RRs, and set the value of the UDP payload size
ac771d
@@ -1116,9 +1167,22 @@ void reply_query(int fd, int family, time_t now)
ac771d
 	      nn = resize_packet(header, nn, NULL, 0);
ac771d
 	    }
ac771d
 #endif
ac771d
-	  send_from(forward->fd, option_bool(OPT_NOWILD) || option_bool (OPT_CLEVERBIND), daemon->packet, nn, 
ac771d
-		    &forward->source, &forward->dest, forward->iface);
ac771d
+	  for (src = &forward->frec_src; src; src = src->next)
ac771d
+	    {
ac771d
+	      header->id = htons(src->orig_id);
ac771d
+	      
ac771d
+	      send_from(forward->fd, option_bool(OPT_NOWILD) || option_bool (OPT_CLEVERBIND), daemon->packet, nn, 
ac771d
+			&src->source, &src->dest, src->iface);
ac771d
+
ac771d
+	      if (option_bool(OPT_EXTRALOG) && src != &forward->frec_src)
ac771d
+		{
ac771d
+		  daemon->log_display_id = src->log_id;
ac771d
+		  daemon->log_source_addr = &src->source;
ac771d
+		  log_query(F_UPSTREAM, "query", NULL, "duplicate");
ac771d
+		}
ac771d
+	    }
ac771d
 	}
ac771d
+
ac771d
       free_frec(forward); /* cancel */
ac771d
     }
ac771d
 }
ac771d
@@ -2053,6 +2117,17 @@ void free_rfd(struct randfd *rfd)
ac771d
 
ac771d
 static void free_frec(struct frec *f)
ac771d
 {
ac771d
+  struct frec_src *src, *tmp;
ac771d
+
ac771d
+   /* add back to freelist of not the record builtin to every frec. */
ac771d
+  for (src = f->frec_src.next; src; src = tmp)
ac771d
+    {
ac771d
+      tmp = src->next;
ac771d
+      src->next = daemon->free_frec_src;
ac771d
+      daemon->free_frec_src = src;
ac771d
+    }
ac771d
+  
ac771d
+  f->frec_src.next = NULL;    
ac771d
   free_rfd(f->rfd4);
ac771d
   f->rfd4 = NULL;
ac771d
   f->sentto = NULL;
ac771d
@@ -2196,17 +2271,39 @@ static struct frec *lookup_frec_by_sender(unsigned short id,
ac771d
 					  void *hash)
ac771d
 {
ac771d
   struct frec *f;
ac771d
+  struct frec_src *src;
ac771d
+
ac771d
+  for (f = daemon->frec_list; f; f = f->next)
ac771d
+    if (f->sentto &&
ac771d
+	!(f->flags & (FREC_DNSKEY_QUERY | FREC_DS_QUERY)) &&
ac771d
+	memcmp(hash, f->hash, HASH_SIZE) == 0)
ac771d
+      for (src = &f->frec_src; src; src = src->next)
ac771d
+	if (src->orig_id == id && 
ac771d
+	    sockaddr_isequal(&src->source, addr))
ac771d
+	  return f;
ac771d
+  
ac771d
+  return NULL;
ac771d
+}
ac771d
+
ac771d
+static struct frec *lookup_frec_by_query(void *hash, unsigned int flags)
ac771d
+{
ac771d
+  struct frec *f;
ac771d
+
ac771d
+  /* FREC_DNSKEY and FREC_DS_QUERY are never set in flags, so the test below 
ac771d
+     ensures that no frec created for internal DNSSEC query can be returned here. */
ac771d
+
ac771d
+#define FLAGMASK (FREC_CHECKING_DISABLED | FREC_AD_QUESTION | FREC_DO_QUESTION \
ac771d
+		  | FREC_HAS_PHEADER | FREC_DNSKEY_QUERY | FREC_DS_QUERY)
ac771d
   
ac771d
   for(f = daemon->frec_list; f; f = f->next)
ac771d
     if (f->sentto &&
ac771d
-	f->orig_id == id && 
ac771d
-	memcmp(hash, f->hash, HASH_SIZE) == 0 &&
ac771d
-	sockaddr_isequal(&f->source, addr))
ac771d
+	(f->flags & FLAGMASK) == flags &&
ac771d
+	memcmp(hash, f->hash, HASH_SIZE) == 0)
ac771d
       return f;
ac771d
-   
ac771d
+  
ac771d
   return NULL;
ac771d
 }
ac771d
- 
ac771d
+
ac771d
 /* Send query packet again, if we can. */
ac771d
 void resend_query()
ac771d
 {
ac771d
-- 
ac771d
2.26.2
ac771d