cURL / Mailing Lists / curl-library / Single Mail

curl-library

[PATCH] Add "Happy Eyeballs" for IPv4/IPv6.

From: Björn Stenberg <bjorn_at_haxx.se>
Date: Sat, 26 Oct 2013 14:17:33 +0200

This patch invokes two socket connect()s nearly simultaneously, and
the socket that is first connected "wins" and is subsequently used for
the connection. The other is terminated.

There is a very slight IPv4 preference, in that if both sockets connect
simultaneously IPv4 is checked first and thus will win.

---
 lib/connect.c |  298 ++++++++++++++++++++++++++++-----------------------------
 lib/connect.h |    2 -
 lib/ftp.c     |    5 +-
 lib/multi.c   |   13 ++-
 lib/url.c     |    8 +-
 lib/urldata.h |    2 +
 6 files changed, 164 insertions(+), 164 deletions(-)
diff --git a/lib/connect.c b/lib/connect.c
index c442c48..4bf0232 100644
--- a/lib/connect.c
+++ b/lib/connect.c
@@ -233,45 +233,6 @@ long Curl_timeleft(struct SessionHandle *data,
   return timeout_ms;
 }
 
-/*
- * checkconnect() checks for a TCP connect on the given socket.
- * It returns:
- */
-
-enum chkconn_t {
-  CHKCONN_SELECT_ERROR = -1,
-  CHKCONN_CONNECTED    = 0,
-  CHKCONN_IDLE         = 1,
-  CHKCONN_FDSET_ERROR  = 2
-};
-
-static enum chkconn_t
-checkconnect(curl_socket_t sockfd)
-{
-  int rc;
-#ifdef mpeix
-  /* Call this function once now, and ignore the results. We do this to
-     "clear" the error state on the socket so that we can later read it
-     reliably. This is reported necessary on the MPE/iX operating system. */
-  (void)verifyconnect(sockfd, NULL);
-#endif
-
-  rc = Curl_socket_ready(CURL_SOCKET_BAD, sockfd, 0);
-
-  if(-1 == rc)
-    /* error, no connect here, try next */
-    return CHKCONN_SELECT_ERROR;
-
-  else if(rc & CURL_CSELECT_ERR)
-    /* error condition caught */
-    return CHKCONN_FDSET_ERROR;
-
-  else if(rc)
-    return CHKCONN_CONNECTED;
-
-  return CHKCONN_IDLE;
-}
-
 static CURLcode bindlocal(struct connectdata *conn,
                           curl_socket_t sockfd, int af)
 {
@@ -573,17 +534,19 @@ static bool verifyconnect(curl_socket_t sockfd, int *error)
    more address exists or error */
 static CURLcode trynextip(struct connectdata *conn,
                           int sockindex,
+                          int tempindex,
                           bool *connected)
 {
   curl_socket_t sockfd;
   Curl_addrinfo *ai;
+  int family = tempindex ? AF_INET6 : AF_INET;
 
   /* First clean up after the failed socket.
      Don't close it yet to ensure that the next IP's socket gets a different
      file descriptor, which can prevent bugs when the curl_multi_socket_action
      interface is used with certain select() replacements such as kqueue. */
-  curl_socket_t fd_to_close = conn->sock[sockindex];
-  conn->sock[sockindex] = CURL_SOCKET_BAD;
+  curl_socket_t fd_to_close = conn->tempsock[tempindex];
+  conn->tempsock[tempindex] = CURL_SOCKET_BAD;
   *connected = FALSE;
 
   if(sockindex != FIRSTSOCKET) {
@@ -591,21 +554,26 @@ static CURLcode trynextip(struct connectdata *conn,
     return CURLE_COULDNT_CONNECT; /* no next */
   }
 
-  /* try the next address */
-  ai = conn->ip_addr->ai_next;
+  /* try the next address with same family */
+  ai = conn->tempaddr[tempindex]->ai_next;
+  while(ai && ai->ai_family != family)
+    ai = ai->ai_next;
 
-  while(ai) {
+  while(ai && ai->ai_family == family) {
     CURLcode res = singleipconnect(conn, ai, &sockfd, connected);
     if(res)
       return res;
     if(sockfd != CURL_SOCKET_BAD) {
       /* store the new socket descriptor */
-      conn->sock[sockindex] = sockfd;
-      conn->ip_addr = ai;
+      conn->tempsock[tempindex] = sockfd;
+      conn->tempaddr[tempindex] = ai;
       Curl_closesocket(conn, fd_to_close);
       return CURLE_OK;
     }
-    ai = ai->ai_next;
+
+    do {
+      ai = ai->ai_next;
+    } while(ai && ai->ai_family == family);
   }
   Curl_closesocket(conn, fd_to_close);
   return CURLE_COULDNT_CONNECT;
@@ -707,6 +675,7 @@ void Curl_updateconninfo(struct connectdata *conn, curl_socket_t sockfd)
             error, Curl_strerror(conn, error));
       return;
     }
+    memcpy(conn->ip_addr_str, conn->primary_ip, MAX_IPADR_LEN);
 
     if(!getaddressinfo((struct sockaddr*)&ssloc,
                        conn->local_ip, &conn->local_port)) {
@@ -732,11 +701,11 @@ CURLcode Curl_is_connected(struct connectdata *conn,
 {
   struct SessionHandle *data = conn->data;
   CURLcode code = CURLE_OK;
-  curl_socket_t sockfd = conn->sock[sockindex];
   long allow = DEFAULT_CONNECT_TIMEOUT;
   int error = 0;
   struct timeval now;
-  enum chkconn_t chk;
+  int result;
+  int i;
 
   DEBUGASSERT(sockindex >= FIRSTSOCKET && sockindex <= SECONDARYSOCKET);
 
@@ -759,69 +728,94 @@ CURLcode Curl_is_connected(struct connectdata *conn,
     return CURLE_OPERATION_TIMEDOUT;
   }
 
-  /* check socket for connect */
-  chk = checkconnect(sockfd);
-  if(CHKCONN_IDLE == chk) {
-    if(curlx_tvdiff(now, conn->connecttime) >= conn->timeoutms_per_addr) {
-      infof(data, "After %ldms connect time, move on!\n",
-            conn->timeoutms_per_addr);
-      goto next;
-    }
+  for(i=0; i<2; i++) {
+    if(conn->tempsock[i] == CURL_SOCKET_BAD)
+      continue;
 
-    /* not an error, but also no connection yet */
-    return code;
-  }
+#ifdef mpeix
+    /* Call this function once now, and ignore the results. We do this to
+       "clear" the error state on the socket so that we can later read it
+       reliably. This is reported necessary on the MPE/iX operating system. */
+    (void)verifyconnect(conn->tempsock[i], NULL);
+#endif
+
+    /* check socket for connect */
+    result = Curl_socket_ready(CURL_SOCKET_BAD, conn->tempsock[i], 0);
+
+    switch(result) {
+    case 0: /* no connection yet */
+      if(curlx_tvdiff(now, conn->connecttime) >= conn->timeoutms_per_addr) {
+        infof(data, "After %ldms connect time, move on!\n",
+              conn->timeoutms_per_addr);
+        break;
+      }
+      return CURLE_OK;
+
+    case CURL_CSELECT_OUT:
+      if(verifyconnect(conn->tempsock[i], &error)) {
+        /* we are connected with TCP, awesome! */
+        int other = i ^ 1;
+
+        /* use this socket from now on */
+        conn->sock[sockindex] = conn->tempsock[i];
+        conn->ip_addr = conn->tempaddr[i];
+
+        /* close the other socket, if open */
+        if(conn->tempsock[other] != CURL_SOCKET_BAD) {
+          if(conn->fclosesocket)
+            conn->fclosesocket(conn->closesocket_client,
+                               conn->tempsock[other]);
+          else
+            sclose(conn->tempsock[other]);
+        }
 
-  if(CHKCONN_CONNECTED == chk) {
-    if(verifyconnect(sockfd, &error)) {
-      /* we are connected with TCP, awesome! */
+        /* see if we need to do any proxy magic first once we connected */
+        code = Curl_connected_proxy(conn, sockindex);
+        if(code)
+          return code;
 
-      /* see if we need to do any proxy magic first once we connected */
-      code = Curl_connected_proxy(conn, sockindex);
-      if(code)
-        return code;
+        conn->bits.tcpconnect[sockindex] = TRUE;
 
-      conn->bits.tcpconnect[sockindex] = TRUE;
+        *connected = TRUE;
+        if(sockindex == FIRSTSOCKET)
+          Curl_pgrsTime(data, TIMER_CONNECT); /* connect done */
+        Curl_updateconninfo(conn, conn->sock[sockindex]);
+        Curl_verboseconnect(conn);
 
-      *connected = TRUE;
-      if(sockindex == FIRSTSOCKET)
-        Curl_pgrsTime(data, TIMER_CONNECT); /* connect done */
-      Curl_verboseconnect(conn);
-      Curl_updateconninfo(conn, sockfd);
+        return CURLE_OK;
+      }
+      else
+        infof(data, "Connection failed\n");
+      break;
 
+    case CURL_CSELECT_ERR|CURL_CSELECT_OUT:
+      (void)verifyconnect(conn->tempsock[i], &error);
+      break;
+
+    default:
+      infof(data, "Whut?\n");
       return CURLE_OK;
     }
-    /* nope, not connected for real */
-  }
-  else {
-    /* nope, not connected  */
-    if(CHKCONN_FDSET_ERROR == chk) {
-      (void)verifyconnect(sockfd, &error);
-      infof(data, "%s\n",Curl_strerror(conn, error));
-    }
-    else
-      infof(data, "Connection failed\n");
-  }
 
-  /*
-   * The connection failed here, we should attempt to connect to the "next
-   * address" for the given host. But first remember the latest error.
-   */
-  if(error) {
-    data->state.os_errno = error;
-    SET_SOCKERRNO(error);
-  }
-  next:
+    /*
+     * The connection failed here, we should attempt to connect to the "next
+     * address" for the given host. But first remember the latest error.
+     */
+    if(error) {
+      data->state.os_errno = error;
+      SET_SOCKERRNO(error);
+    }
 
-  conn->timeoutms_per_addr = conn->ip_addr->ai_next == NULL ?
-                             allow : allow / 2;
-  code = trynextip(conn, sockindex, connected);
+    conn->timeoutms_per_addr = conn->tempaddr[i]->ai_next == NULL ?
+                               allow : allow / 2;
+    code = trynextip(conn, sockindex, i, connected);
 
-  if(code) {
-    error = SOCKERRNO;
-    data->state.os_errno = error;
-    failf(data, "Failed connect to %s:%ld; %s",
-          conn->host.name, conn->port, Curl_strerror(conn, error));
+    if(code) {
+      error = SOCKERRNO;
+      data->state.os_errno = error;
+      failf(data, "Failed connect to %s:%ld; %s",
+            conn->host.name, conn->port, Curl_strerror(conn, error));
+    }
   }
 
   return code;
@@ -948,6 +942,8 @@ singleipconnect(struct connectdata *conn,
   struct SessionHandle *data = conn->data;
   curl_socket_t sockfd;
   CURLcode res = CURLE_OK;
+  char ipaddress[MAX_IPADR_LEN];
+  long port;
 
   *sockp = CURL_SOCKET_BAD;
   *connected = FALSE; /* default is not connected */
@@ -961,7 +957,7 @@ singleipconnect(struct connectdata *conn,
 
   /* store remote address and port used in this connection attempt */
   if(!getaddressinfo((struct sockaddr*)&addr.sa_addr,
-                     conn->primary_ip, &conn->primary_port)) {
+                     ipaddress, &port)) {
     /* malformed address or bug in inet_ntop, try next address */
     error = ERRNO;
     failf(data, "sa_addr inet_ntop() failed with errno %d: %s",
@@ -969,10 +965,7 @@ singleipconnect(struct connectdata *conn,
     Curl_closesocket(conn, sockfd);
     return CURLE_OK;
   }
-  memcpy(conn->ip_addr_str, conn->primary_ip, MAX_IPADR_LEN);
-  infof(data, "  Trying %s...\n", conn->ip_addr_str);
-
-  Curl_persistconninfo(conn);
+  infof(data, "  Trying %s...\n", ipaddress);
 
   if(data->set.tcp_nodelay)
     tcpnodelay(conn, sockfd);
@@ -1074,24 +1067,18 @@ singleipconnect(struct connectdata *conn,
 
 CURLcode Curl_connecthost(struct connectdata *conn,  /* context */
                           const struct Curl_dns_entry *remotehost,
-                          curl_socket_t *sockconn,   /* the connected socket */
-                          Curl_addrinfo **addr,      /* the one we used */
                           bool *connected)           /* really connected? */
 {
   struct SessionHandle *data = conn->data;
-  curl_socket_t sockfd = CURL_SOCKET_BAD;
-  Curl_addrinfo *ai;
-  Curl_addrinfo *curr_addr;
-
   struct timeval after;
   struct timeval before = Curl_tvnow();
+  int i;
 
   /*************************************************************
    * Figure out what maximum time we have left
    *************************************************************/
   long timeout_ms;
 
-  DEBUGASSERT(sockconn);
   *connected = FALSE; /* default to not connected */
 
   /* get the timeout left */
@@ -1104,45 +1091,62 @@ CURLcode Curl_connecthost(struct connectdata *conn,  /* context */
   }
 
   conn->num_addr = Curl_num_addresses(remotehost->addr);
-
-  ai = remotehost->addr;
+  conn->tempaddr[0] = remotehost->addr;
+  conn->tempaddr[1] = remotehost->addr;
 
   /* Below is the loop that attempts to connect to all IP-addresses we
-   * know for the given host. One by one until one IP succeeds.
+   * know for the given host.
+   * One by one, for each protocol, until one IP succeeds.
    */
 
-  /*
-   * Connecting with a Curl_addrinfo chain
-   */
-  for(curr_addr = ai; curr_addr; curr_addr = curr_addr->ai_next) {
-    CURLcode res;
-
-    /* Max time for the next address */
-    conn->timeoutms_per_addr = curr_addr->ai_next == NULL ?
-                               timeout_ms : timeout_ms / 2;
-
-    /* start connecting to the IP curr_addr points to */
-    res = singleipconnect(conn, curr_addr,
-                          &sockfd, connected);
-    if(res)
-      return res;
-
-    if(sockfd != CURL_SOCKET_BAD)
-      break;
+  for(i=0; i<2; i++) {
+    curl_socket_t sockfd = CURL_SOCKET_BAD;
+    Curl_addrinfo *ai = conn->tempaddr[i];
+    int family = i ? AF_INET6 : AF_INET;
+
+    /* find first address for this address family, if any */
+    while(ai && ai->ai_family != family)
+      ai = ai->ai_next;
+
+    /*
+     * Connecting with a Curl_addrinfo chain
+     */
+    while(ai) {
+      CURLcode res;
+
+      /* Max time for the next connection attempt */
+      conn->timeoutms_per_addr = ai->ai_next == NULL ?
+                                 timeout_ms : timeout_ms / 2;
+
+      /* start connecting to the IP curr_addr points to */
+      res = singleipconnect(conn, ai, &sockfd, connected);
+      if(res)
+        return res;
+
+      if(sockfd != CURL_SOCKET_BAD)
+        break;
+
+      /* get a new timeout for next attempt */
+      after = Curl_tvnow();
+      timeout_ms -= Curl_tvdiff(after, before);
+      if(timeout_ms < 0) {
+        failf(data, "connect() timed out!");
+        return CURLE_OPERATION_TIMEDOUT;
+      }
+      before = after;
 
-    /* get a new timeout for next attempt */
-    after = Curl_tvnow();
-    timeout_ms -= Curl_tvdiff(after, before);
-    if(timeout_ms < 0) {
-      failf(data, "connect() timed out!");
-      return CURLE_OPERATION_TIMEDOUT;
-    }
-    before = after;
-  }  /* end of connect-to-each-address loop */
+      /* next addresses */
+      do {
+        ai = ai->ai_next;
+      } while(ai && ai->ai_family != family);
+    }  /* end of connect-to-each-address loop */
 
-  *sockconn = sockfd;    /* the socket descriptor we've connected */
+    conn->tempsock[i] = sockfd;
+    conn->tempaddr[i] = ai;
+  }
 
-  if(sockfd == CURL_SOCKET_BAD) {
+  if((conn->tempsock[0] == CURL_SOCKET_BAD) &&
+     (conn->tempsock[1] == CURL_SOCKET_BAD)) {
     /* no good connect was made */
     failf(data, "couldn't connect to %s at %s:%ld",
           conn->bits.proxy?"proxy":"host",
@@ -1152,10 +1156,6 @@ CURLcode Curl_connecthost(struct connectdata *conn,  /* context */
 
   /* leave the socket in non-blocking mode */
 
-  /* store the address we use */
-  if(addr)
-    *addr = curr_addr;
-
   data->info.numconnects++; /* to track the number of connections made */
 
   return CURLE_OK;
diff --git a/lib/connect.h b/lib/connect.h
index ab98002..526ce37 100644
--- a/lib/connect.h
+++ b/lib/connect.h
@@ -33,8 +33,6 @@ CURLcode Curl_is_connected(struct connectdata *conn,
 CURLcode Curl_connecthost(struct connectdata *conn,
                           const struct Curl_dns_entry *host, /* connect to
                                                                 this */
-                          curl_socket_t *sockconn, /* not set if error */
-                          Curl_addrinfo **addr, /* the one we used */
                           bool *connected); /* truly connected? */
 
 /* generic function that returns how much time there's left to run, according
diff --git a/lib/ftp.c b/lib/ftp.c
index e66a8a8..46ec8a6 100644
--- a/lib/ftp.c
+++ b/lib/ftp.c
@@ -1881,7 +1881,6 @@ static CURLcode ftp_state_pasv_resp(struct connectdata *conn,
   struct ftp_conn *ftpc = &conn->proto.ftpc;
   CURLcode result;
   struct SessionHandle *data=conn->data;
-  Curl_addrinfo *conninfo;
   struct Curl_dns_entry *addr=NULL;
   int rc;
   unsigned short connectport; /* the local port connect() should use! */
@@ -2041,8 +2040,6 @@ static CURLcode ftp_state_pasv_resp(struct connectdata *conn,
 
   result = Curl_connecthost(conn,
                             addr,
-                            &conn->sock[SECONDARYSOCKET],
-                            &conninfo,
                             &connected);
 
   Curl_resolv_unlock(data, addr); /* we're done using this address */
@@ -2064,7 +2061,7 @@ static CURLcode ftp_state_pasv_resp(struct connectdata *conn,
 
   if(data->set.verbose)
     /* this just dumps information about this second connection */
-    ftp_pasv_verbose(conn, conninfo, ftpc->newhost, connectport);
+    ftp_pasv_verbose(conn, conn->ip_addr, ftpc->newhost, connectport);
 
   if(connected) {
     /* Only do the proxy connection magic if we're actually connected.  We do
diff --git a/lib/multi.c b/lib/multi.c
index e723a3e..f11ba06 100644
--- a/lib/multi.c
+++ b/lib/multi.c
@@ -621,17 +621,26 @@ static int waitconnect_getsock(struct connectdata *conn,
                                curl_socket_t *sock,
                                int numsocks)
 {
+  int i;
+  int s=0;
+  int rc=0;
+
   if(!numsocks)
     return GETSOCK_BLANK;
 
-  sock[0] = conn->sock[FIRSTSOCKET];
+  for(i=0; i<2; i++) {
+    if(conn->tempsock[i] != CURL_SOCKET_BAD) {
+      sock[s] = conn->tempsock[i];
+      rc |= GETSOCK_WRITESOCK(s++);
+    }
+  }
 
   /* when we've sent a CONNECT to a proxy, we should rather wait for the
      socket to become readable to be able to get the response headers */
   if(conn->tunnel_state[FIRSTSOCKET] == TUNNEL_CONNECT)
     return GETSOCK_READSOCK(0);
 
-  return GETSOCK_WRITESOCK(0);
+  return rc;
 }
 
 static int domore_getsock(struct connectdata *conn,
diff --git a/lib/url.c b/lib/url.c
index 9973bd2..e86fbc2 100644
--- a/lib/url.c
+++ b/lib/url.c
@@ -3260,7 +3260,6 @@ static CURLcode ConnectPlease(struct SessionHandle *data,
                               bool *connected)
 {
   CURLcode result;
-  Curl_addrinfo *addr;
 #ifndef CURL_DISABLE_VERBOSE_STRINGS
   char *hostname = conn->bits.proxy?conn->proxy.name:conn->host.name;
 
@@ -3276,13 +3275,8 @@ static CURLcode ConnectPlease(struct SessionHandle *data,
    *************************************************************/
   result= Curl_connecthost(conn,
                            conn->dns_entry,
-                           &conn->sock[FIRSTSOCKET],
-                           &addr,
                            connected);
   if(CURLE_OK == result) {
-    /* All is cool, we store the current information */
-    conn->ip_addr = addr;
-
     if(*connected) {
       result = Curl_connected_proxy(conn, FIRSTSOCKET);
       if(!result) {
@@ -5643,8 +5637,8 @@ CURLcode Curl_setup_conn(struct connectdata *conn,
       Curl_pgrsTime(data, TIMER_APPCONNECT); /* we're connected already */
       conn->bits.tcpconnect[FIRSTSOCKET] = TRUE;
       *protocol_done = TRUE;
-      Curl_verboseconnect(conn);
       Curl_updateconninfo(conn, conn->sock[FIRSTSOCKET]);
+      Curl_verboseconnect(conn);
     }
     /* Stop the loop now */
     break;
diff --git a/lib/urldata.h b/lib/urldata.h
index ebaaf6f..98686bb 100644
--- a/lib/urldata.h
+++ b/lib/urldata.h
@@ -838,6 +838,7 @@ struct connectdata {
      within the DNS cache, so this pointer is only valid as long as the DNS
      cache entry remains locked. It gets unlocked in Curl_done() */
   Curl_addrinfo *ip_addr;
+  Curl_addrinfo *tempaddr[2]; /* for happy eyeballs */
 
   /* 'ip_addr_str' is the ip_addr data as a human readable string.
      It remains available as long as the connection does, which is longer than
@@ -889,6 +890,7 @@ struct connectdata {
   struct timeval created; /* creation time */
   curl_socket_t sock[2]; /* two sockets, the second is used for the data
                             transfer when doing FTP */
+  curl_socket_t tempsock[2]; /* temporary sockets for happy eyeballs */
   bool sock_accepted[2]; /* TRUE if the socket on this index was created with
                             accept() */
   Curl_recv *recv[2];
-- 
1.7.10.4
--SLDf9lqlvOQaIe6s
Content-Type: text/plain; charset="us-ascii"
MIME-Version: 1.0
Content-Transfer-Encoding: 7bit
Content-Disposition: inline
-------------------------------------------------------------------
List admin: http://cool.haxx.se/list/listinfo/curl-library
Etiquette:  http://curl.haxx.se/mail/etiquette.html
--SLDf9lqlvOQaIe6s--
Received on 2001-09-17