dirmngr: hkp: Avoid potential race condition when some hosts die.
[gnupg.git] / dirmngr / ks-engine-hkp.c
index b6a0675..49a57eb 100644 (file)
@@ -37,7 +37,7 @@
 
 #include "dirmngr.h"
 #include "misc.h"
-#include "userids.h"
+#include "../common/userids.h"
 #include "dns-stuff.h"
 #include "ks-engine.h"
 
@@ -55,7 +55,7 @@
 
 
 /* Number of seconds after a host is marked as resurrected.  */
-#define RESURRECT_INTERVAL  (3600*3)  /* 3 hours */
+#define RESURRECT_INTERVAL  (3600+1800)  /* 1.5 hours */
 
 /* To match the behaviour of our old gpgkeys helper code we escape
    more characters than actually needed. */
@@ -67,6 +67,8 @@
 /* Number of retries done for a dead host etc.  */
 #define SEND_REQUEST_RETRIES 3
 
+enum ks_protocol { KS_PROTOCOL_HKP, KS_PROTOCOL_HKPS, KS_PROTOCOL_MAX };
+
 /* Objects used to maintain information about hosts.  */
 struct hostinfo_s;
 typedef struct hostinfo_s *hostinfo_t;
@@ -74,9 +76,11 @@ struct hostinfo_s
 {
   time_t lastfail;   /* Time we tried to connect and failed.  */
   time_t lastused;   /* Time of last use.  */
-  int *pool;         /* A -1 terminated array with indices into
-                        HOSTTABLE or NULL if NAME is not a pool
-                        name.  */
+  int *pool;         /* An array with indices into HOSTTABLE or NULL
+                        if NAME is not a pool name.  */
+  size_t pool_len;   /* Length of POOL.  */
+  size_t pool_size;  /* Allocated size of POOL.  */
+#define MAX_POOL_SIZE  128
   int poolidx;       /* Index into POOL with the used host.  -1 if not set.  */
   unsigned int v4:1; /* Host supports AF_INET.  */
   unsigned int v6:1; /* Host supports AF_INET6.  */
@@ -84,12 +88,18 @@ struct hostinfo_s
   unsigned int dead:1; /* Host is currently unresponsive.  */
   unsigned int iporname_valid:1;  /* The field IPORNAME below is valid */
                                   /* (but may be NULL) */
+  unsigned int did_a_lookup:1;    /* Have we done an A lookup yet?  */
+  unsigned int did_srv_lookup:2;  /* One bit per protocol indicating
+                                     whether we already did a SRV
+                                     lookup.  */
   time_t died_at;    /* The time the host was marked dead.  If this is
                         0 the host has been manually marked dead.  */
   char *cname;       /* Canonical name of the host.  Only set if this
                         is a pool or NAME has a numerical IP address.  */
   char *iporname;    /* Numeric IP address or name for printing.  */
-  unsigned short port; /* The port used by the host, 0 if unknown.  */
+  unsigned short port[KS_PROTOCOL_MAX];
+                     /* The port used by the host for all protocols, 0
+                        if unknown.  */
   char name[1];      /* The hostname.  */
 };
 
@@ -100,7 +110,7 @@ static hostinfo_t *hosttable;
 static int hosttable_size;
 
 /* The number of host slots we initially allocate for HOSTTABLE.  */
-#define INITIAL_HOSTTABLE_SIZE 10
+#define INITIAL_HOSTTABLE_SIZE 50
 
 
 /* Create a new hostinfo object, fill in NAME and put it into
@@ -118,6 +128,8 @@ create_new_hostinfo (const char *name)
     return -1;
   strcpy (hi->name, name);
   hi->pool = NULL;
+  hi->pool_len = 0;
+  hi->pool_size = 0;
   hi->poolidx = -1;
   hi->lastused = (time_t)(-1);
   hi->lastfail = (time_t)(-1);
@@ -125,11 +137,14 @@ create_new_hostinfo (const char *name)
   hi->v6 = 0;
   hi->onion = 0;
   hi->dead = 0;
+  hi->did_a_lookup = 0;
+  hi->did_srv_lookup = 0;
   hi->iporname_valid = 0;
   hi->died_at = 0;
   hi->cname = NULL;
   hi->iporname = NULL;
-  hi->port = 0;
+  hi->port[KS_PROTOCOL_HKP] = 0;
+  hi->port[KS_PROTOCOL_HKPS] = 0;
 
   /* Add it to the hosttable. */
   for (idx=0; idx < hosttable_size; idx++)
@@ -187,44 +202,45 @@ sort_hostpool (const void *xa, const void *xb)
 }
 
 
-/* Return true if the host with the hosttable index TBLIDX is in POOL.  */
+/* Return true if the host with the hosttable index TBLIDX is in HI->pool.  */
 static int
-host_in_pool_p (int *pool, int tblidx)
+host_in_pool_p (hostinfo_t hi, int tblidx)
 {
   int i, pidx;
 
-  for (i=0; (pidx = pool[i]) != -1; i++)
+  for (i = 0; i < hi->pool_len && (pidx = hi->pool[i]) != -1; i++)
     if (pidx == tblidx && hosttable[pidx])
       return 1;
   return 0;
 }
 
 
-/* Select a random host.  Consult TABLE which indices into the global
-   hosttable.  Returns index into TABLE or -1 if no host could be
+/* Select a random host.  Consult HI->pool which indices into the global
+   hosttable.  Returns index into HI->pool or -1 if no host could be
    selected.  */
 static int
-select_random_host (int *table)
+select_random_host (hostinfo_t hi)
 {
-  int *tbl;
-  size_t tblsize;
+  int *tbl = NULL;
+  size_t tblsize = 0;
   int pidx, idx;
 
   /* We create a new table so that we randomly select only from
      currently alive hosts.  */
-  for (idx=0, tblsize=0; (pidx = table[idx]) != -1; idx++)
+  for (idx = 0;
+       idx < hi->pool_len && (pidx = hi->pool[idx]) != -1;
+       idx++)
     if (hosttable[pidx] && !hosttable[pidx]->dead)
-      tblsize++;
+      {
+        tblsize++;
+        tbl = xtryrealloc(tbl, tblsize * sizeof *tbl);
+        if (!tbl)
+          return -1; /* memory allocation failed! */
+        tbl[tblsize-1] = pidx;
+      }
   if (!tblsize)
     return -1; /* No hosts.  */
 
-  tbl = xtrymalloc (tblsize * sizeof *tbl);
-  if (!tbl)
-    return -1;
-  for (idx=0, tblsize=0; (pidx = table[idx]) != -1; idx++)
-    if (hosttable[pidx] && !hosttable[pidx]->dead)
-      tbl[tblsize++] = pidx;
-
   if (tblsize == 1)  /* Save a get_uint_nonce.  */
     pidx = tbl[0];
   else
@@ -255,7 +271,7 @@ arecords_is_pool (dns_addrinfo_t aibuf)
 }
 
 
-/* Print a warninng iff Tor is not running but Tor has been requested.
+/* Print a warning iff Tor is not running but Tor has been requested.
  * Also return true if it is not running.  */
 static int
 tor_not_running_p (ctrl_t ctrl)
@@ -281,25 +297,27 @@ tor_not_running_p (ctrl_t ctrl)
 
 
 /* Add the host AI under the NAME into the HOSTTABLE.  If PORT is not
-   zero, it specifies which port to use to talk to the host.  If NAME
-   specifies a pool (as indicated by IS_POOL), update the given
-   reference table accordingly.  */
+   zero, it specifies which port to use to talk to the host for
+   PROTOCOL.  If NAME specifies a pool (as indicated by IS_POOL),
+   update the given reference table accordingly.  */
 static void
-add_host (const char *name, int is_pool,
-          const dns_addrinfo_t ai, unsigned short port,
-          int *reftbl, size_t reftblsize, int *refidx)
+add_host (ctrl_t ctrl, const char *name, int is_pool,
+          const dns_addrinfo_t ai,
+          enum ks_protocol protocol, unsigned short port)
 {
   gpg_error_t tmperr;
   char *tmphost;
   int idx, tmpidx;
+  hostinfo_t host;
   int i;
 
   idx = find_hostinfo (name);
+  host = hosttable[idx];
 
   if (is_pool)
     {
       /* For a pool immediately convert the address to a string.  */
-      tmperr = resolve_dns_addr (ai->addr, ai->addrlen,
+      tmperr = resolve_dns_addr (ctrl, ai->addr, ai->addrlen,
                                  (DNS_NUMERICHOST | DNS_WITHBRACKET), &tmphost);
     }
   else if (!is_ip_address (name))
@@ -316,7 +334,7 @@ add_host (const char *name, int is_pool,
     {
       /* Do a PTR lookup on AI.  If a name was not found the function
        * returns the numeric address (with brackets).  */
-      tmperr = resolve_dns_addr (ai->addr, ai->addrlen,
+      tmperr = resolve_dns_addr (ctrl, ai->addr, ai->addrlen,
                                  DNS_WITHBRACKET, &tmphost);
     }
 
@@ -325,7 +343,7 @@ add_host (const char *name, int is_pool,
       log_info ("resolve_dns_addr failed while checking '%s': %s\n",
                 name, gpg_strerror (tmperr));
     }
-  else if ((*refidx) + 1 >= reftblsize)
+  else if (host->pool_len + 1 >= MAX_POOL_SIZE)
     {
       log_error ("resolve_dns_addr for '%s': '%s'"
                  " [index table full - ignored]\n", name, tmphost);
@@ -352,7 +370,7 @@ add_host (const char *name, int is_pool,
       else  /* Set or update the entry. */
         {
           if (port)
-            hosttable[tmpidx]->port = port;
+            hosttable[tmpidx]->port[protocol] = port;
 
           if (ai->family == AF_INET6)
             {
@@ -365,17 +383,54 @@ add_host (const char *name, int is_pool,
           else
             BUG ();
 
-          for (i=0; i < *refidx; i++)
-            if (reftbl[i] == tmpidx)
-              break;
-          if (!(i < *refidx) && tmpidx != idx)
-            reftbl[(*refidx)++] = tmpidx;
+          /* If we updated the main entry, we're done.  */
+          if (idx == tmpidx)
+            goto leave;
+
+          /* If we updated an existing entry, we're done.  */
+          for (i = 0; i < host->pool_len; i++)
+            if (host->pool[i] == tmpidx)
+              goto leave;
+
+          /* Otherwise, we need to add it to the pool.  Check if there
+             is space.  */
+          if (host->pool_len + 1 > host->pool_size)
+            {
+              int *new_pool;
+              size_t new_size;
+
+              if (host->pool_size == 0)
+                new_size = 4;
+              else
+                new_size = host->pool_size * 2;
+
+              new_pool = xtryrealloc (host->pool,
+                                      new_size * sizeof *new_pool);
+
+              if (new_pool == NULL)
+                goto leave;
+
+              host->pool = new_pool;
+              host->pool_size = new_size;
+            }
+
+          /* Finally, add it.  */
+          log_assert (host->pool_len < host->pool_size);
+          host->pool[host->pool_len++] = tmpidx;
         }
     }
+ leave:
   xfree (tmphost);
 }
 
 
+/* Sort the pool of the given hostinfo HI.  */
+static void
+hostinfo_sort_pool (hostinfo_t hi)
+{
+  qsort (hi->pool, hi->pool_len, sizeof *hi->pool, sort_hostpool);
+}
+
 /* Map the host name NAME to the actual to be used host name.  This
  * allows us to manage round robin DNS names.  We use our own strategy
  * to choose one of the hosts.  For example we skip those hosts which
@@ -393,12 +448,16 @@ add_host (const char *name, int is_pool,
  * pool.  */
 static gpg_error_t
 map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect,
-          char **r_host, char *r_portstr,
+          enum ks_protocol protocol, char **r_host, char *r_portstr,
           unsigned int *r_httpflags, char **r_httphost)
 {
   gpg_error_t err = 0;
   hostinfo_t hi;
   int idx;
+  dns_addrinfo_t aibuf, ai;
+  int is_pool;
+  int new_hosts = 0;
+  char *cname;
 
   *r_host = NULL;
   if (r_httpflags)
@@ -415,77 +474,65 @@ map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect,
 
   /* See whether the host is in our table.  */
   idx = find_hostinfo (name);
-  if (idx == -1 && is_onion_address (name))
+  if (idx == -1)
     {
       idx = create_new_hostinfo (name);
       if (idx == -1)
         return gpg_error_from_syserror ();
       hi = hosttable[idx];
-      hi->onion = 1;
+      hi->onion = is_onion_address (name);
     }
-  else if (idx == -1)
+  else
+    hi = hosttable[idx];
+
+  is_pool = hi->pool != NULL;
+
+  if (srvtag && !is_ip_address (name)
+      && ! hi->onion
+      && ! (hi->did_srv_lookup & 1 << protocol))
     {
-      /* We never saw this host.  Allocate a new entry.  */
-      dns_addrinfo_t aibuf, ai;
-      int *reftbl;
-      size_t reftblsize;
-      int refidx;
-      int is_pool = 0;
-      char *cname;
       struct srventry *srvs;
       unsigned int srvscount;
 
-      reftblsize = 100;
-      reftbl = xtrymalloc (reftblsize * sizeof *reftbl);
-      if (!reftbl)
-        return gpg_error_from_syserror ();
-      refidx = 0;
-
-      idx = create_new_hostinfo (name);
-      if (idx == -1)
+      /* Check for SRV records.  */
+      err = get_dns_srv (ctrl, name, srvtag, NULL, &srvs, &srvscount);
+      if (err)
         {
-          err = gpg_error_from_syserror ();
-          xfree (reftbl);
+          if (gpg_err_code (err) == GPG_ERR_ECONNREFUSED)
+            tor_not_running_p (ctrl);
           return err;
         }
-      hi = hosttable[idx];
 
-      if (srvtag && !is_ip_address (name))
+      if (srvscount > 0)
         {
-          /* Check for SRV records.  */
-          err = get_dns_srv (name, srvtag, NULL, &srvs, &srvscount);
-          if (err)
-            {
-              xfree (reftbl);
-              if (gpg_err_code (err) == GPG_ERR_ECONNREFUSED)
-                tor_not_running_p (ctrl);
-              return err;
-            }
+          int i;
+          if (! is_pool)
+            is_pool = srvscount > 1;
 
-          if (srvscount > 0)
+          for (i = 0; i < srvscount; i++)
             {
-              int i;
-              is_pool = srvscount > 1;
-
-              for (i = 0; i < srvscount; i++)
-                {
-                  err = resolve_dns_name (srvs[i].target, 0,
-                                          AF_UNSPEC, SOCK_STREAM,
-                                          &ai, &cname);
-                  if (err)
-                    continue;
-                  dirmngr_tick (ctrl);
-                  add_host (name, is_pool, ai, srvs[i].port,
-                            reftbl, reftblsize, &refidx);
-                }
-
-              xfree (srvs);
+              err = resolve_dns_name (ctrl, srvs[i].target, 0,
+                                      AF_UNSPEC, SOCK_STREAM,
+                                      &ai, &cname);
+              if (err)
+                continue;
+              dirmngr_tick (ctrl);
+              add_host (ctrl, name, is_pool, ai, protocol, srvs[i].port);
+              new_hosts = 1;
             }
+
+          xfree (srvs);
         }
 
+      hi->did_srv_lookup |= 1 << protocol;
+    }
+
+  if (! hi->did_a_lookup
+      && ! hi->onion)
+    {
       /* Find all A records for this entry and put them into the pool
          list - if any.  */
-      err = resolve_dns_name (name, 0, 0, SOCK_STREAM, &aibuf, &cname);
+      err = resolve_dns_name (ctrl, name, 0, 0, SOCK_STREAM, &aibuf, &cname);
       if (err)
         {
           log_error ("resolving '%s' failed: %s\n", name, gpg_strerror (err));
@@ -512,40 +559,28 @@ map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect,
                 continue;
               if (opt.disable_ipv4 && ai->family == AF_INET)
                 continue;
+              if (opt.disable_ipv6 && ai->family == AF_INET6)
+                continue;
               dirmngr_tick (ctrl);
 
-              add_host (name, is_pool, ai, 0, reftbl, reftblsize, &refidx);
+              add_host (ctrl, name, is_pool, ai, 0, 0);
+              new_hosts = 1;
             }
+
+          hi->did_a_lookup = 1;
         }
-      reftbl[refidx] = -1;
       xfree (cname);
       free_dns_addrinfo (aibuf);
-
-      if (refidx && is_pool)
-        {
-          assert (!hi->pool);
-          hi->pool = xtryrealloc (reftbl, (refidx+1) * sizeof *reftbl);
-          if (!hi->pool)
-            {
-              err = gpg_error_from_syserror ();
-              log_error ("shrinking index table in map_host failed: %s\n",
-                         gpg_strerror (err));
-              xfree (reftbl);
-              return err;
-            }
-          qsort (hi->pool, refidx, sizeof *reftbl, sort_hostpool);
-        }
-      else
-        xfree (reftbl);
     }
+  if (new_hosts)
+    hostinfo_sort_pool (hi);
 
-  hi = hosttable[idx];
   if (hi->pool)
     {
       /* Deal with the pool name before selecting a host. */
       if (r_httphost)
         {
-          *r_httphost = xtrystrdup (hi->cname? hi->cname : hi->name);
+          *r_httphost = xtrystrdup (hi->name);
           if (!*r_httphost)
             return gpg_error_from_syserror ();
         }
@@ -561,7 +596,7 @@ map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect,
       /* Select a host if needed.  */
       if (hi->poolidx == -1)
         {
-          hi->poolidx = select_random_host (hi->pool);
+          hi->poolidx = select_random_host (hi);
           if (hi->poolidx == -1)
             {
               log_error ("no alive host found in pool '%s'\n", name);
@@ -584,18 +619,18 @@ map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect,
        * find the canonical name so that it can be used in the HTTP
        * Host header.  Fixme: We should store that name in the
        * hosttable. */
-      dns_addrinfo_t aibuf, ai;
       char *host;
 
-      err = resolve_dns_name (hi->name, 0, 0, SOCK_STREAM, &aibuf, NULL);
+      err = resolve_dns_name (ctrl, hi->name, 0, 0, SOCK_STREAM, &aibuf, NULL);
       if (!err)
         {
           for (ai = aibuf; ai; ai = ai->next)
             {
-              if (ai->family == AF_INET6
+              if ((!opt.disable_ipv6 && ai->family == AF_INET6)
                   || (!opt.disable_ipv4 && ai->family == AF_INET))
                 {
-                  err = resolve_dns_addr (ai->addr, ai->addrlen, 0, &host);
+                  err = resolve_dns_addr (ctrl,
+                                          ai->addr, ai->addrlen, 0, &host);
                   if (!err)
                     {
                       /* Okay, we return the first found name.  */
@@ -648,9 +683,9 @@ map_host (ctrl_t ctrl, const char *name, const char *srvtag, int force_reselect,
         }
       return err;
     }
-  if (hi->port)
+  if (hi->port[protocol])
     snprintf (r_portstr, 6 /* five digits and the sentinel */,
-              "%hu", hi->port);
+              "%hu", hi->port[protocol]);
   return 0;
 }
 
@@ -738,7 +773,9 @@ ks_hkp_mark_host (ctrl_t ctrl, const char *name, int alive)
   /* If the host is a pool mark all member hosts. */
   if (!err && hi->pool)
     {
-      for (idx2=0; !err && (n=hi->pool[idx2]) != -1; idx2++)
+      for (idx2 = 0;
+           !err && idx2 < hi->pool_len && (n = hi->pool[idx2]) != -1;
+           idx2++)
         {
           assert (n >= 0 && n < hosttable_size);
 
@@ -751,7 +788,7 @@ ks_hkp_mark_host (ctrl_t ctrl, const char *name, int alive)
                   if (hosttable[idx3]
                       && hosttable[idx3]->pool
                       && idx3 != idx
-                      && host_in_pool_p (hosttable[idx3]->pool, n))
+                      && host_in_pool_p (hosttable[idx3], n))
                     break;
                 }
               if (idx3 < hosttable_size)
@@ -826,7 +863,7 @@ ks_hkp_print_hosttable (ctrl_t ctrl)
 
                 /* Turn the numerical IP address string into an AI and
                  * then do a DNS PTR lookup.  */
-                if (!resolve_dns_name (hi->name, 0, 0,
+                if (!resolve_dns_name (ctrl, hi->name, 0, 0,
                                        SOCK_STREAM,
                                        &aibuf, &canon))
                   {
@@ -837,7 +874,7 @@ ks_hkp_print_hosttable (ctrl_t ctrl)
                       }
                     for (ai = aibuf; !canon && ai; ai = ai->next)
                       {
-                        resolve_dns_addr (ai->addr, ai->addrlen,
+                        resolve_dns_addr (ctrl, ai->addr, ai->addrlen,
                                           DNS_WITHBRACKET, &canon);
                         if (canon && is_ip_address (canon))
                           {
@@ -857,14 +894,14 @@ ks_hkp_print_hosttable (ctrl_t ctrl)
                 /* Get the IP address as a string from a name.  Note
                  * that resolve_dns_addr allocates CANON on success
                  * and thus terminates the loop. */
-                if (!resolve_dns_name (hi->name, 0,
+                if (!resolve_dns_name (ctrl, hi->name, 0,
                                        hi->v6? AF_INET6 : AF_INET,
                                        SOCK_STREAM,
                                        &aibuf, NULL))
                   {
                     for (ai = aibuf; !canon && ai; ai = ai->next)
                       {
-                        resolve_dns_addr (ai->addr, ai->addrlen,
+                        resolve_dns_addr (ctrl, ai->addr, ai->addrlen,
                                           DNS_NUMERICHOST|DNS_WITHBRACKET,
                                           &canon);
                       }
@@ -901,7 +938,7 @@ ks_hkp_print_hosttable (ctrl_t ctrl)
           {
             init_membuf (&mb, 256);
             put_membuf_printf (&mb, "  .   -->");
-            for (idx2=0; hi->pool[idx2] != -1; idx2++)
+            for (idx2 = 0; idx2 < hi->pool_len && hi->pool[idx2] != -1; idx2++)
               {
                 put_membuf_printf (&mb, " %d", hi->pool[idx2]);
                 if (hi->poolidx == hi->pool[idx2])
@@ -969,6 +1006,7 @@ make_host_part (ctrl_t ctrl,
   const char *srvtag;
   char portstr[10];
   char *hostname;
+  enum ks_protocol protocol;
 
   *r_hostport = NULL;
 
@@ -976,15 +1014,17 @@ make_host_part (ctrl_t ctrl,
     {
       scheme = "https";
       srvtag = no_srv? NULL : "pgpkey-https";
+      protocol = KS_PROTOCOL_HKPS;
     }
   else /* HKP or HTTP.  */
     {
       scheme = "http";
       srvtag = no_srv? NULL : "pgpkey-http";
+      protocol = KS_PROTOCOL_HKP;
     }
 
   portstr[0] = 0;
-  err = map_host (ctrl, host, srvtag, force_reselect,
+  err = map_host (ctrl, host, srvtag, force_reselect, protocol,
                   &hostname, portstr, r_httpflags, r_httphost);
   if (err)
     return err;
@@ -1120,9 +1160,16 @@ send_request (ctrl_t ctrl, const char *request, const char *hostportstr,
   int redirects_left = MAX_REDIRECTS;
   estream_t fp = NULL;
   char *request_buffer = NULL;
+  parsed_uri_t uri = NULL;
+  int is_onion;
 
   *r_fp = NULL;
 
+  err = http_parse_uri (&uri, request, 0);
+  if (err)
+    goto leave;
+  is_onion = uri->onion;
+
   err = http_session_new (&session, httphost,
                           ((ctrl->http_no_crl? HTTP_FLAG_NO_CRL : 0)
                            | HTTP_FLAG_TRUST_DEF),
@@ -1130,9 +1177,10 @@ send_request (ctrl_t ctrl, const char *request, const char *hostportstr,
   if (err)
     goto leave;
   http_session_set_log_cb (session, cert_log_cb);
+  http_session_set_timeout (session, ctrl->timeout);
 
  once_more:
-  err = http_open (&http,
+  err = http_open (ctrl, &http,
                    post_cb? HTTP_REQ_POST : HTTP_REQ_GET,
                    request,
                    httphost,
@@ -1140,7 +1188,8 @@ send_request (ctrl_t ctrl, const char *request, const char *hostportstr,
                    (httpflags
                     |(opt.honor_http_proxy? HTTP_FLAG_TRY_PROXY:0)
                     |(dirmngr_use_tor ()? HTTP_FLAG_FORCE_TOR:0)
-                    |(opt.disable_ipv4? HTTP_FLAG_IGNORE_IPv4 : 0)),
+                    |(opt.disable_ipv4? HTTP_FLAG_IGNORE_IPv4 : 0)
+                    |(opt.disable_ipv6? HTTP_FLAG_IGNORE_IPv6 : 0)),
                    ctrl->http_proxy,
                    session,
                    NULL,
@@ -1206,6 +1255,23 @@ send_request (ctrl_t ctrl, const char *request, const char *hostportstr,
                   request, s?s:"[none]", http_get_status_code (http));
         if (s && *s && redirects_left-- )
           {
+            if (is_onion)
+              {
+                /* Make sure that an onion address only redirects to
+                 * another onion address.  */
+                http_release_parsed_uri (uri);
+                uri = NULL;
+                err = http_parse_uri (&uri, s, 0);
+                if (err)
+                  goto leave;
+
+                if (! uri->onion)
+                  {
+                    err = gpg_error (GPG_ERR_FORBIDDEN);
+                    goto leave;
+                  }
+              }
+
             xfree (request_buffer);
             request_buffer = xtrystrdup (s);
             if (request_buffer)
@@ -1254,6 +1320,7 @@ send_request (ctrl_t ctrl, const char *request, const char *hostportstr,
   http_close (http, 0);
   http_session_release (session);
   xfree (request_buffer);
+  http_release_parsed_uri (uri);
   return err;
 }
 
@@ -1283,6 +1350,9 @@ handle_send_request_error (ctrl_t ctrl, gpg_error_t err, const char *request,
     case GPG_ERR_ENETDOWN:
     case GPG_ERR_UNKNOWN_HOST:
     case GPG_ERR_NETWORK:
+    case GPG_ERR_EIO:  /* Sometimes used by estream cookie functions.  */
+    case GPG_ERR_EADDRNOTAVAIL:  /* e.g. when IPv6 is disabled */
+    case GPG_ERR_EAFNOSUPPORT:  /* e.g. when IPv6 is not compiled in */
       if (mark_host_dead (request) && *tries_left)
         retry = 1;
       break;
@@ -1511,6 +1581,7 @@ ks_hkp_get (ctrl_t ctrl, parsed_uri_t uri, const char *keyspec, estream_t *r_fp)
 
     case KEYDB_SEARCH_MODE_FPR16:
       log_error ("HKP keyservers do not support v3 fingerprints\n");
+      /* fall through */
     default:
       return gpg_error (GPG_ERR_INV_USER_ID);
     }