On Sat, 7 Oct 2017 23:28:57 +0200 Julian Andres Klode <j...@debian.org>
wrote:
> JFTR: I wrote an implementation of parts of happy eyeballs for another
> bug we should probably merge in here, but it fails the test suite on
> CI: https://github.com/julian-klode/apt/commits/bugfix/happy-eyeballs
>
> I started a rewrite of that in another local branch, but it's not
> done yet.

Hi,

I started some work on this without knowing about the code that was
there. The patch attached implements Happy Eyeballs in (roughly) the
following way:

In ConnectToHostname, if the first DoConnect does not complete within
300ms, another DoConnect is performed with an address of a different IP
family. So, if the first DoConnect used IPv6, the second will use IPv4
and vice versa. Of these two connection attempts, the first one that
completes will be used. The precedence ordering of getaddrinfo is also
taken into account. Hopefully, this is helpful.

The patch is based on the 1.4.y branch, and should be applicable to
commit ac0d26d4e3. If there is something that I could change, please let
me know.

Kind regards,
Ludens
diff --git a/apt-pkg/contrib/fileutl.cc b/apt-pkg/contrib/fileutl.cc
index e4c40fb4f..8b3ac8015 100644
--- a/apt-pkg/contrib/fileutl.cc
+++ b/apt-pkg/contrib/fileutl.cc
@@ -741,22 +741,28 @@ void SetNonBlock(int Fd,bool Block)
 // WaitFd - Wait for a FD to become readable				/*{{{*/
 // ---------------------------------------------------------------------
 /* This waits for a FD to become readable using select. It is useful for
-   applications making use of non-blocking sockets. The timeout is 
-   in seconds. */
-bool WaitFd(int Fd,bool write,unsigned long timeout)
+   applications making use of non-blocking sockets. */
+bool WaitFd(int Fd,bool write,unsigned long timeout_sec,
+	    unsigned long timeout_usec)
 {
    fd_set Set;
    struct timeval tv;
    FD_ZERO(&Set);
    FD_SET(Fd,&Set);
-   tv.tv_sec = timeout;
-   tv.tv_usec = 0;
+
+   tv.tv_sec = timeout_sec;
+   tv.tv_usec = timeout_usec;
+
+   struct timeval * tv_arg = 0;
+   if (timeout_sec != 0 || timeout_usec != 0)
+      tv_arg = &tv;
+
    if (write == true) 
    {      
       int Res;
       do
       {
-	 Res = select(Fd+1,0,&Set,0,(timeout != 0?&tv:0));
+	 Res = select(Fd+1,0,&Set,0,tv_arg);
       }
       while (Res < 0 && errno == EINTR);
       
@@ -768,7 +774,7 @@ bool WaitFd(int Fd,bool write,unsigned long timeout)
       int Res;
       do
       {
-	 Res = select(Fd+1,&Set,0,0,(timeout != 0?&tv:0));
+	 Res = select(Fd+1,&Set,0,0,tv_arg);
       }
       while (Res < 0 && errno == EINTR);
       
diff --git a/apt-pkg/contrib/fileutl.h b/apt-pkg/contrib/fileutl.h
index dddeb70f5..474b283f8 100644
--- a/apt-pkg/contrib/fileutl.h
+++ b/apt-pkg/contrib/fileutl.h
@@ -191,7 +191,8 @@ std::vector<std::string> GetListOfFilesInDir(std::string const &Dir, bool SortLi
 std::string SafeGetCWD();
 void SetCloseExec(int Fd,bool Close);
 void SetNonBlock(int Fd,bool Block);
-bool WaitFd(int Fd,bool write = false,unsigned long timeout = 0);
+bool WaitFd(int Fd,bool write = false,unsigned long timeout_sec = 0,
+	    unsigned long timeout_usec = 0);
 pid_t ExecFork();
 pid_t ExecFork(std::set<int> keep_fds);
 void MergeKeepFdsFromConfiguration(std::set<int> &keep_fds);
diff --git a/methods/connect.cc b/methods/connect.cc
index dc2aee05c..be4ea3c86 100644
--- a/methods/connect.cc
+++ b/methods/connect.cc
@@ -25,8 +25,8 @@
 #include <unistd.h>
 #include <sstream>
 #include <string.h>
-#include<set>
-#include<string>
+#include <set>
+#include <string>
 
 // Internet stuff
 #include <netinet/in.h>
@@ -49,6 +49,14 @@ static std::vector<SrvRec> SrvRecords;
 // Set of IP/hostnames that we timed out before or couldn't resolve
 static std::set<std::string> bad_addr;
 
+// Time-out for attempting Happy Eyeballs rotation
+// See https://tools.ietf.org/rfc/rfc6555.txt
+static struct timeval const TimeOutHE =
+{ // 300 ms
+   0,
+   300 * 1000
+};
+
 // RotateDNS - Select a new server from a DNS rotation			/*{{{*/
 // ---------------------------------------------------------------------
 /* This is called during certain errors in order to recover by selecting a 
@@ -79,9 +87,34 @@ static bool ConnectionAllowed(char const * const Service, std::string const &Hos
 // DoConnect - Attempt a connect operation				/*{{{*/
 // ---------------------------------------------------------------------
 /* This helper function attempts a connection to a single address. */
+
+// The previous value of DoConnect's HappyEyeballsAttempt parameter.
+// We expect to see either:
+// 0 (no attempt) followed by 0 or 1
+// 1 (first connect during Happy Eyeballs) followed by 2
+// 2 (second connect during Happy Eyeballs) followed by 0 or 1
+// Anything else is wrong.
+static unsigned char prevAttemptHE = 0;
+
 static bool DoConnect(struct addrinfo *Addr,std::string const &Host,
-		      unsigned long TimeOut,int &Fd,pkgAcqMethod *Owner)
+		      unsigned long TimeOutSec,unsigned long TimeOutUsec,
+		      int &Fd,pkgAcqMethod *Owner,
+		      unsigned char HappyEyeballsAttempt = 0)
 {
+   if (HappyEyeballsAttempt > 2
+       ||
+       (prevAttemptHE == 0 && HappyEyeballsAttempt == 2)
+       ||
+       (prevAttemptHE == 1 && HappyEyeballsAttempt != 2)
+       ||
+       (prevAttemptHE == 2 && HappyEyeballsAttempt == 2))
+   {
+      prevAttemptHE = HappyEyeballsAttempt;
+      return _error->Error(_("DoConnect: invalid call sequence"));
+   }
+
+   prevAttemptHE = HappyEyeballsAttempt;
+
    // Show a status indicator
    char Name[NI_MAXHOST];
    char Service[NI_MAXSERV];
@@ -105,6 +138,10 @@ static bool DoConnect(struct addrinfo *Addr,std::string const &Host,
       ioprintf(ss, _("[IP: %s %s]"),Name,Service);
       Owner->SetIP(ss.str());
    }
+
+   // If we are making a second simultaneous connection
+   // (HappyEyeballsAttempt == 2), we need the previous Fd too
+   int const prevFd = Fd;
       
    // Get a socket
    if ((Fd = socket(Addr->ai_family,Addr->ai_socktype,
@@ -120,24 +157,81 @@ static bool DoConnect(struct addrinfo *Addr,std::string const &Host,
    
    /* This implements a timeout for connect by opening the connection
       nonblocking */
-   if (WaitFd(Fd,true,TimeOut) == false) {
-      bad_addr.insert(bad_addr.begin(), std::string(Name));
-      Owner->SetFailReason("Timeout");
-      return _error->Error(_("Could not connect to %s:%s (%s), "
-			   "connection timed out"),Host.c_str(),Service,Name);
+   if (HappyEyeballsAttempt == 0)
+   {
+      if (WaitFd(Fd,true,TimeOutSec,TimeOutUsec) == false)
+      {
+	 bad_addr.insert(bad_addr.begin(), std::string(Name));
+	 Owner->SetFailReason("Timeout");
+	 return _error->Error(_("Could not connect to %s:%s (%s), "
+			      "connection timed out"),Host.c_str(),Service,Name);
+      }
+   }
+   else if (HappyEyeballsAttempt == 1)
+   {
+      if (WaitFd(Fd,true,TimeOutSec,TimeOutUsec) == false)
+      {
+	 // Do not register an error yet, as the connection could
+	 // still complete
+	 return false;
+      }
+   }
+   else if (HappyEyeballsAttempt == 2)
+   {
+      // Wait for the first completed connection
+
+      fd_set Set;
+      struct timeval tv = { TimeOutSec, TimeOutUsec };
+      FD_ZERO(&Set);
+      FD_SET(prevFd,&Set);
+      FD_SET(Fd,&Set);
+
+      int Res;
+      do
+      {
+	 Res = select((prevFd>Fd?prevFd:Fd)+1,0,&Set,0,&tv);
+      }
+      while (Res < 0 && errno == EINTR);
+
+      // Neither connection completed successfully
+      if (Res <= 0)
+      {
+	 close(Fd);
+	 Fd = prevFd; // after this, Fd/prevFd will be closed by ConnectToHostname
+
+	 bad_addr.insert(bad_addr.begin(), std::string(Name));
+	 Owner->SetFailReason("Timeout");
+	 return _error->Error(_("Could not connect to %s:%s (%s), "
+			      "connection timed out"),Host.c_str(),Service,Name);
+      }
+
+      if (FD_ISSET(prevFd,&Set) != 0) // First connection completed
+      {
+	 close(Fd);
+	 Fd = prevFd;
+      }
+      else if (FD_ISSET(Fd,&Set) != 0) // Second connection completed
+      {
+	 close(prevFd);
+      }
+      else
+      {
+	 return _error->Error(_("Happy Eyeballs unexpected code path"));
+      }
    }
 
    // Check the socket for an error condition
    unsigned int Err;
    unsigned int Len = sizeof(Err);
+
    if (getsockopt(Fd,SOL_SOCKET,SO_ERROR,&Err,&Len) != 0)
       return _error->Errno("getsockopt",_("Failed"));
-   
+
    if (Err != 0)
    {
       errno = Err;
       if(errno == ECONNREFUSED)
-         Owner->SetFailReason("ConnectionRefused");
+	 Owner->SetFailReason("ConnectionRefused");
       else if (errno == ETIMEDOUT)
 	 Owner->SetFailReason("ConnectionTimedOut");
       bad_addr.insert(bad_addr.begin(), std::string(Name));
@@ -150,6 +244,26 @@ static bool DoConnect(struct addrinfo *Addr,std::string const &Host,
    return true;
 }
 									/*}}}*/
+// Rotate to another IP protocol family (IPv6 or IPv4).			/*{{{*/
+// Returns an address of the other family or 0 if there is no
+// such address.
+static struct addrinfo * RotateHE(struct addrinfo * Current)
+{
+   if (Current == 0)
+      return 0;
+
+   for (struct addrinfo * Next = Current->ai_next; Next != 0;
+	Next = Next->ai_next)
+   {
+      if ((Current->ai_family == AF_INET && Next->ai_family == AF_INET6)
+	  ||
+	  (Current->ai_family == AF_INET6 && Next->ai_family == AF_INET))
+	 return Next;
+   }
+
+   return 0;
+}
+									/*}}}*/
 // Connect to a given Hostname						/*{{{*/
 static bool ConnectToHostname(std::string const &Host, int const Port,
       const char * const Service, int DefPort, int &Fd,
@@ -163,7 +277,7 @@ static bool ConnectToHostname(std::string const &Host, int const Port,
       snprintf(ServStr,sizeof(ServStr),"%i", Port);
    else
       snprintf(ServStr,sizeof(ServStr),"%s", Service);
-   
+
    /* We used a cached address record.. Yes this is against the spec but
       the way we have setup our rotating dns suggests that this is more
       sensible */
@@ -194,7 +308,7 @@ static bool ConnectToHostname(std::string const &Host, int const Port,
       if (_config->FindB("Acquire::Connect::AddrConfig", true) == true)
 	 Hints.ai_flags |= AI_ADDRCONFIG;
       Hints.ai_protocol = 0;
-      
+
       if(_config->FindB("Acquire::ForceIPv4", false) == true)
          Hints.ai_family = AF_INET;
       else if(_config->FindB("Acquire::ForceIPv6", false) == true)
@@ -252,11 +366,39 @@ static bool ConnectToHostname(std::string const &Host, int const Port,
    
    while (CurHost != 0)
    {
-      if (DoConnect(CurHost,Host,TimeOut,Fd,Owner) == true)
+      struct addrinfo * NextHE = RotateHE(CurHost);
+
+      // Perform Happy Eyeballs if possible
+      // NOTE: if the user indicated ForceIPv4 or ForceIPv6 then
+      // NextHE will be 0 because there will be only one family in
+      // the rotation.
+      if (LastUsed == 0 && NextHE != 0)
       {
-	 LastUsed = CurHost;
-	 return true;
-      }      
+	 if (DoConnect(CurHost,Host,TimeOutHE.tv_sec,TimeOutHE.tv_usec,
+		       Fd,Owner,1) == true)
+	 {
+	    LastUsed = CurHost;
+	    return true;
+	 }
+
+	 int const CurHostFd = Fd;
+
+	 if (DoConnect(NextHE,Host,TimeOut,0,Fd,Owner,2) == true)
+	 {
+	    // Set the correct address
+	    LastUsed = Fd == CurHostFd ? CurHost : NextHE;
+	    return true;
+	 }
+      }
+      else
+      {
+	 if (DoConnect(CurHost,Host,TimeOut,0,Fd,Owner) == true)
+	 {
+	    LastUsed = CurHost;
+	    return true;
+	 }
+      }
+
       close(Fd);
       Fd = -1;
       

Reply via email to