RFC 8305 Happy Eyeballs in OpenSSH

Kim Minh Kaplan openssh-unix-dev.mindrot.org at ml.kim-minh.com
Fri Feb 23 23:32:38 AEDT 2018


Hello,

I use hosts that are dual stack configured (IPv4 and IPv6) and it
happens that connectivity through one or the other is broken and
timeouts. In these case connection to the SSH server can take quite some
time as ssh waits for the first address to timeout before trying the
next.

So I gave a stab at implementing RFC 8305. This patch implements part of
it in sshconnect.c.

* It does not do section 3 (initiation of asynchronous DNS queries, a
  SHOULD).
* It does not do section 4 (sorting of resolved destination
  addresses). That means it does not do the RFC 6724 address sort
  which is a MUST.  The order is still the one from getaddrinfo(3).
* It implements section 5 (initiation of asynchronous connection
  attempts). It paces the connection attempts 250 milliseconds appart
  as recommended. Once a connection attempt succeeds it cancels all
  other initiated connections and ignores addresses not yet used.
* It does not implement RFC 8305 for channels.c, that is port
  forwardings do not use it.

Comments are welcome.

Kim Minh.

===================================================================
RCS file: /cvs/src/usr.bin/ssh/sshconnect.c,v
retrieving revision 1.296
diff -u -r1.296 sshconnect.c
--- sshconnect.c	23 Feb 2018 04:18:46 -0000	1.296
+++ sshconnect.c	23 Feb 2018 12:26:56 -0000
@@ -453,39 +453,198 @@
 	return -1;
 }
 
+/*
+ * RFC 8305 Happy Eyeballs Version 2: Better Connectivity Using Concurrency
+ *
+ * implementation can have a fixed delay for how long to wait before
+ * starting the next connection attempt [...] recommended value for a
+ * default delay is 250 milliseconds.
+ */
+#define CONNECTION_ATTEMPT_DELAY 250
+
 static int
-timeout_connect(int sockfd, const struct sockaddr *serv_addr,
-    socklen_t addrlen, int *timeoutp)
+ssh_connect_timeout(struct timeval *tv, int timeout_ms)
 {
-	int optval = 0;
-	socklen_t optlen = sizeof(optval);
+	if (timeout_ms <= 0)
+		return 0;
+	ms_subtract_diff(tv, &timeout_ms);
+	return timeout_ms <= 0;
+}
 
-	/* No timeout: just do a blocking connect() */
-	if (*timeoutp <= 0)
-		return connect(sockfd, serv_addr, addrlen);
-
-	set_nonblock(sockfd);
-	if (connect(sockfd, serv_addr, addrlen) == 0) {
-		/* Succeeded already? */
-		unset_nonblock(sockfd);
+/*
+ * Return 0 if the addrinfo was not tried. Return -1 if using it
+ * failed. Return 1 if it was used.
+ */
+static int
+ssh_connect_happy_eyeballs_initiate(const char *host, struct addrinfo *ai,
+				    int *timeout_ms, int needpriv,
+				    struct timeval *initiate,
+				    int *nfds, fd_set *fds,
+				    struct addrinfo *fd_ai[])
+{
+	int oerrno, sock;
+	char ntop[NI_MAXHOST], strport[NI_MAXSERV];
+
+	memset(ntop, 0, sizeof(ntop));
+	memset(strport, 0, sizeof(strport));
+	/* If *nfds != 0 then *initiate is initialised. */
+	if (*nfds &&
+	    (ai == NULL ||
+	      !ssh_connect_timeout(initiate, CONNECTION_ATTEMPT_DELAY)))
+		/* Do not initiate new connections yet */
 		return 0;
-	} else if (errno != EINPROGRESS)
+	if (ai->ai_family != AF_INET && ai->ai_family != AF_INET6) {
+		errno = EAFNOSUPPORT;
 		return -1;
-
-	if (waitrfd(sockfd, timeoutp) == -1)
+	}
+	if (getnameinfo(ai->ai_addr, ai->ai_addrlen,
+			ntop, sizeof(ntop),
+			strport, sizeof(strport),
+			NI_NUMERICHOST|NI_NUMERICSERV) != 0) {
+		oerrno = errno;
+		error("%s: getnameinfo failed", __func__);
+		errno = oerrno;
 		return -1;
-
-	/* Completed or failed */
-	if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &optval, &optlen) == -1) {
-		debug("getsockopt: %s", strerror(errno));
+	}
+	debug("Connecting to %.200s [%.100s] port %s.",
+	      host, ntop, strport);
+	/* Create a socket for connecting */
+	sock = ssh_create_socket(needpriv, ai);
+	if (sock < 0) {
+		/* Any error is already output */
+		errno = 0;
+		return -1;
+	}
+	if (sock >= FD_SETSIZE) {
+		error("socket number to big for select: %d", sock);
+		close(sock);
 		return -1;
 	}
-	if (optval != 0) {
-		errno = optval;
+	fd_ai[sock] = ai;
+	set_nonblock(sock);
+	if (connect(sock, ai->ai_addr, ai->ai_addrlen) < 0 &&
+	    errno != EINPROGRESS) {
+		error("connect to address %s port %s: %s",
+		      ntop, strport, strerror(errno));
+		errno = 0;
+		close(sock);
 		return -1;
 	}
-	unset_nonblock(sockfd);
-	return 0;
+	monotime_tv(initiate);
+	FD_SET(sock, fds);
+	*nfds = MAXIMUM(*nfds, sock + 1);
+	return 1;
+}
+
+static int
+ssh_connect_happy_eyeballs_process(int *nfds, fd_set *fds,
+				   struct addrinfo *fd_ai[],
+				   int ready, fd_set *wfds)
+{
+	socklen_t optlen;
+	int sock, optval = 0;
+	char ntop[NI_MAXHOST], strport[NI_MAXSERV];
+	for (sock = *nfds - 1; ready > 0 && sock >= 0; sock--) {
+		if (FD_ISSET(sock, wfds)) {
+			ready--;
+			optlen = sizeof(optval);
+			if (getsockopt(sock, SOL_SOCKET, SO_ERROR,
+				       &optval, &optlen) < 0) {
+				optval = errno;
+				error("getsockopt failed: %s",
+				      strerror(errno));
+			} else if (optval != 0) {
+				memset(ntop, 0, sizeof(ntop));
+				memset(strport, 0, sizeof(strport));
+				if (getnameinfo(fd_ai[sock]->ai_addr,
+						fd_ai[sock]->ai_addrlen,
+						ntop, sizeof(ntop),
+						strport, sizeof(strport),
+						NI_NUMERICHOST|NI_NUMERICSERV) != 0)
+					error("connect finally failed: %s",
+					      strerror(optval));
+				else
+					error("connect to address %s port %s finally: %s",
+					      ntop, strport, strerror(optval));
+			}
+			FD_CLR(sock, fds);
+			while (*nfds > 0 && ! FD_ISSET(*nfds - 1, fds))
+				--*nfds;
+			if (optval == 0) {
+				unset_nonblock(sock);
+				return sock;
+			}
+			close(sock);
+			errno = optval;
+		}
+	}
+	return -1;
+}
+
+static int
+ssh_connect_happy_eyeballs(const char * host, struct addrinfo *ai,
+			   struct sockaddr_storage *hostaddr, int *timeout_ms,
+			   int needpriv)
+{
+	struct addrinfo *fd_ai[FD_SETSIZE];
+	struct timeval initiate_tv, start_tv, select_tv, *tv;
+	fd_set fds, wfds;
+	int res, oerrno, diff, diff0, nfds = 0, sock = -1;
+	FD_ZERO(&fds);
+	if (*timeout_ms > 0)
+		monotime_tv(&start_tv);
+	while ((ai != NULL || nfds > 0) &&
+	       ! ssh_connect_timeout(&start_tv, *timeout_ms)) {
+		res = ssh_connect_happy_eyeballs_initiate(host, ai,
+							  timeout_ms, needpriv,
+							  &initiate_tv,
+							  &nfds, &fds, fd_ai);
+		if (res != 0)
+			ai = ai->ai_next;
+		if (res == -1)
+			continue;
+		tv = NULL;
+		if (ai != NULL || *timeout_ms > 0) {
+			tv = &select_tv;
+			if (ai != NULL) {
+				diff = CONNECTION_ATTEMPT_DELAY;
+				ms_subtract_diff(&initiate_tv, &diff);
+				if (*timeout_ms > 0) {
+					diff0 = *timeout_ms;
+					ms_subtract_diff(&start_tv, &diff0);
+					diff = MINIMUM(diff, diff0);
+				}
+			} else {
+				diff = *timeout_ms;
+				ms_subtract_diff(&start_tv, &diff);
+			}
+			ms_to_timeval(tv, diff);
+		}
+		wfds = fds;
+		res = select(nfds, NULL, &wfds, NULL, tv);
+		oerrno = errno;
+		if (res < 0) {
+			error("select failed: %s", strerror(errno));
+			errno = oerrno;
+			continue;
+		}
+		sock = ssh_connect_happy_eyeballs_process(&nfds, &fds, fd_ai,
+							  res, &wfds);
+		if (sock >= 0) {
+			memcpy(hostaddr, fd_ai[sock]->ai_addr,
+			       fd_ai[sock]->ai_addrlen);
+			break;
+		}
+	}
+	oerrno = errno;
+	while (nfds-- > 0)
+		if (FD_ISSET(nfds, &fds))
+			close(nfds);
+	if (ssh_connect_timeout(&start_tv, *timeout_ms))
+		errno = ETIMEDOUT;
+	else
+		errno = oerrno;
+	return sock;
 }
 
 /*
@@ -505,13 +664,9 @@
     int connection_attempts, int *timeout_ms, int want_keepalive, int needpriv)
 {
 	int on = 1;
-	int oerrno, sock = -1, attempt;
-	char ntop[NI_MAXHOST], strport[NI_MAXSERV];
-	struct addrinfo *ai;
+	int sock = -1, attempt;
 
 	debug2("%s: needpriv %d", __func__, needpriv);
-	memset(ntop, 0, sizeof(ntop));
-	memset(strport, 0, sizeof(strport));
 
 	for (attempt = 0; attempt < connection_attempts; attempt++) {
 		if (attempt > 0) {
@@ -519,57 +674,16 @@
 			sleep(1);
 			debug("Trying again...");
 		}
-		/*
-		 * Loop through addresses for this host, and try each one in
-		 * sequence until the connection succeeds.
-		 */
-		for (ai = aitop; ai; ai = ai->ai_next) {
-			if (ai->ai_family != AF_INET &&
-			    ai->ai_family != AF_INET6) {
-				errno = EAFNOSUPPORT;
-				continue;
-			}
-			if (getnameinfo(ai->ai_addr, ai->ai_addrlen,
-			    ntop, sizeof(ntop), strport, sizeof(strport),
-			    NI_NUMERICHOST|NI_NUMERICSERV) != 0) {
-				oerrno = errno;
-				error("%s: getnameinfo failed", __func__);
-				errno = oerrno;
-				continue;
-			}
-			debug("Connecting to %.200s [%.100s] port %s.",
-				host, ntop, strport);
-
-			/* Create a socket for connecting. */
-			sock = ssh_create_socket(needpriv, ai);
-			if (sock < 0) {
-				/* Any error is already output */
-				errno = 0;
-				continue;
-			}
-
-			if (timeout_connect(sock, ai->ai_addr, ai->ai_addrlen,
-			    timeout_ms) >= 0) {
-				/* Successful connection. */
-				memcpy(hostaddr, ai->ai_addr, ai->ai_addrlen);
-				break;
-			} else {
-				oerrno = errno;
-				debug("connect to address %s port %s: %s",
-				    ntop, strport, strerror(errno));
-				close(sock);
-				sock = -1;
-				errno = oerrno;
-			}
-		}
+		sock = ssh_connect_happy_eyeballs(host, aitop, hostaddr,
+						  timeout_ms, needpriv);
 		if (sock != -1)
 			break;	/* Successful connection. */
 	}
 
 	/* Return failure if we didn't get a successful connection. */
 	if (sock == -1) {
-		error("ssh: connect to host %s port %s: %s",
-		    host, strport, errno == 0 ? "failure" : strerror(errno));
+		error("ssh: connect to host %s port %hu: %s",
+		    host, port, errno == 0 ? "failure" : strerror(errno));
 		return -1;
 	}
 


More information about the openssh-unix-dev mailing list