lib/fetch: add happy eyeballs connect algorithm (RFC6555/RFC8305)

Connect to the addresses from `getaddrinfo(3)`,
alternating between address family,
starting with ipv6 and wait `fetchConnDelay`
between each connection attempt.

If a connection is established within the attempts,
use this connection and close all others.

If `connect(3)` returns `ENETUNREACH`, don't attempt more
connections with the failing address family.

If there are no more addresses to attempt,
wait for `fetchConnTimeout` and return the first established
connection.

If no connection was established within the timeouts,
close all sockets and return -1 and set errno to
`ETIMEDOUT`.
This commit is contained in:
Duncaen 2019-03-18 17:47:28 +01:00
parent 291faddf8c
commit 9f52a7837f
3 changed files with 207 additions and 18 deletions

View File

@ -4,6 +4,7 @@
* Copyright (c) 1998-2014 Dag-Erling Smorgrav
* Copyright (c) 2008, 2010 Joerg Sonnenberger <joerg@NetBSD.org>
* Copyright (c) 2013 Michael Gmelin <freebsd@grem.de>
* Copyright (c) 2019 Duncan Overbruck <mail@duncano.de>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
@ -53,6 +54,8 @@
#include <string.h>
#include <unistd.h>
#include <strings.h>
#include <poll.h>
#include <fcntl.h>
#ifndef MSG_NOSIGNAL
#include <signal.h>
@ -417,6 +420,199 @@ fetch_socks5(conn_t *conn, struct url *url, struct url *socks, int verbose)
return 0;
}
/*
* Happy Eyeballs (RFC8305):
*
* Connect to the addresses in res0, alternating between
* address family, starting with ipv6 and waits `fetchConnDelay`
* between each connection attempt.
*
* If a connection is established within the attempts,
* use this connection and close all others.
*
* If `connect(3)` returns `ENETUNREACH`, don't attempt more
* connections with the failing address family.
*
* If there are no more addresses to attempt, wait for
* `fetchConnTimeout` and return the first established
* connection.
*
* If no connection was established within the timeouts,
* close all sockets and return -1 and set errno to
* `ETIMEDOUT`.
*/
static int
happy_eyeballs_connect(struct addrinfo *res0)
{
struct pollfd *pfd;
struct addrinfo *res;
const char *bindaddr;
int optval;
socklen_t optlen = sizeof(optval);
int rv = -1;
int err = 0;
int timeout = fetchConnDelay;
unsigned int attempts = 0, waiting = 0;
unsigned int i, n4, n6, i4, i6, done = 0;
bindaddr = getenv("FETCH_BIND_ADDRESS");
for (n4 = n6 = 0, res = res0; res; res = res->ai_next)
switch (res->ai_family) {
case AF_INET6: n6++; break;
case AF_INET: n4++; break;
}
if (n4+n6 == 0 || !(pfd = calloc(n4+n6, sizeof (struct pollfd))))
return -1;
#ifdef FULL_DEBUG
fetch_info("got %d A and %d AAAA records", n4, n6);
#endif
res = NULL;
i4 = i6 = 0;
for (;;) {
int sd = -1;
int ret;
unsigned short family;
#ifdef FULL_DEBUG
fetch_info("happy eyeballs state: i4=%u n4=%u i6=%u n6=%u", i4, n4, i6, n6);
#endif
if (res == NULL) {
/* prefer ipv6 */
family = i6+1 < n6 ? AF_INET6 : AF_INET;
} else if (i4+1 < n4) {
family = res->ai_family == AF_INET && i6+1 < n6 ? AF_INET6 : AF_INET;
} else if (i6+1 < n6) {
family = res->ai_family == AF_INET6 && i4+1 < n4 ? AF_INET : AF_INET6;
} else {
/* no more connections to try */
#ifdef FULL_DEBUG
fetch_info("attempted to connect to all addresses, waiting...");
#endif
timeout = fetchConnTimeout;
done = 1;
goto wait;
}
for (i = 0, res = res0; res; res = res->ai_next) {
if (res->ai_family == family) {
if (family == AF_INET && i == i4) {
i4++;
break;
}
if (family == AF_INET6 && i == i6) {
i6++;
break;
}
i++;
}
}
if ((sd = socket(res->ai_family, res->ai_socktype | SOCK_NONBLOCK,
res->ai_protocol)) == -1)
continue;
if (bindaddr != NULL && *bindaddr != '\0' &&
fetch_bind(sd, res->ai_family, bindaddr) != 0) {
fetch_info("failed to bind to '%s'", bindaddr);
close(sd);
continue;
}
#ifdef FULL_DEBUG
{
char hbuf[1025];
if (getnameinfo(res->ai_addr, res->ai_addrlen, hbuf, sizeof(hbuf), NULL,
0, NI_NUMERICHOST) == 0)
fetch_info("connecting to %s", hbuf);
}
#endif
if (connect(sd, res->ai_addr, res->ai_addrlen) == -1) {
if (errno == EINPROGRESS) {
pfd[attempts].fd = sd;
} else if (errno == ENETUNREACH) {
close(sd);
if (family == AF_INET)
i4 = n4;
else
i6 = n6;
continue;
} else {
err = errno;
rv = -1;
close(sd);
break;
}
} else {
/* XXX: does this actually happen? */
rv = sd;
break;
}
attempts++;
waiting++;
wait:
for (i = 0; i < attempts; i++) {
pfd[i].revents = pfd[i].events = 0;
if (pfd[i].fd != -1)
pfd[i].events = POLLOUT;
}
if ((ret = poll(pfd, attempts, timeout ? timeout : -1)) == -1) {
err = errno;
rv = -1;
break;
} else if (ret > 0) {
sd = -1;
for (i = 0; i < attempts; i++) {
if (pfd[i].revents & POLLHUP) {
/* connection failed, save errno */
if ((getsockopt(pfd[i].fd, SOL_SOCKET, SO_ERROR, &optval, &optlen)) == 0)
err = optval;
close(pfd[i].fd);
pfd[i].fd = -1;
waiting--;
} else if (pfd[i].revents & POLLOUT) {
/* connection established */
err = 0;
sd = pfd[i].fd;
break;
}
}
if (sd != -1) {
rv = sd;
break;
}
} else if (done) {
err = ETIMEDOUT;
rv = -1;
break;
}
if (!waiting)
break;
}
for (i = 0; i < attempts; i++)
if ((rv == -1 || rv != pfd[i].fd) && pfd[i].fd != -1)
close(pfd[i].fd);
free(pfd);
if (rv != -1) {
if (fcntl(rv, F_SETFL, fcntl(rv, F_GETFL, 0) & ~O_NONBLOCK) == -1) {
err = errno;
close(rv);
rv = -1;
}
}
errno = err;
return rv;
}
/*
* Establish a TCP connection to the specified port on the specified host.
*/
@ -426,8 +622,8 @@ fetch_connect(struct url *url, int af, int verbose)
conn_t *conn;
char pbuf[10];
struct url *socks_url, *connurl;
const char *bindaddr, *socks_proxy;
struct addrinfo hints, *res, *res0;
const char *socks_proxy;
struct addrinfo hints, *res0;
int sd, error;
socks_url = NULL;
@ -460,26 +656,11 @@ fetch_connect(struct url *url, int af, int verbose)
netdb_seterr(error);
return (NULL);
}
bindaddr = getenv("FETCH_BIND_ADDRESS");
if (verbose)
fetch_info("connecting to %s:%d", connurl->host, connurl->port);
/* try to connect */
for (sd = -1, res = res0; res; sd = -1, res = res->ai_next) {
if ((sd = socket(res->ai_family, res->ai_socktype,
res->ai_protocol)) == -1)
continue;
if (bindaddr != NULL && *bindaddr != '\0' &&
fetch_bind(sd, res->ai_family, bindaddr) != 0) {
fetch_info("failed to bind to '%s'", bindaddr);
close(sd);
continue;
}
if (connect(sd, res->ai_addr, res->ai_addrlen) == 0)
break;
close(sd);
}
sd = happy_eyeballs_connect(res0);
freeaddrinfo(res0);
if (sd == -1) {
fetch_syserr();

View File

@ -45,6 +45,8 @@ auth_t fetchAuthMethod;
int fetchLastErrCode;
char fetchLastErrString[MAXERRSTRING];
int fetchTimeout;
int fetchConnTimeout;
int fetchConnDelay = 250;
volatile int fetchRestartCalls = 1;
int fetchDebug;

View File

@ -173,6 +173,12 @@ extern char fetchLastErrString[MAXERRSTRING];
/* I/O timeout */
extern int fetchTimeout;
/* Connect timeout */
extern int fetchConnTimeout;
/* Connect attempt delay */
extern int fetchConnDelay;
/* Restart interrupted syscalls */
extern volatile int fetchRestartCalls;