From 9f52a7837f1d6f17e5f4d15b7bdf27b8556a12c5 Mon Sep 17 00:00:00 2001 From: Duncaen Date: Mon, 18 Mar 2019 17:47:28 +0100 Subject: [PATCH] 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`. --- lib/fetch/common.c | 217 +++++++++++++++++++++++++++++++++++++++++---- lib/fetch/fetch.c | 2 + lib/fetch/fetch.h | 6 ++ 3 files changed, 207 insertions(+), 18 deletions(-) diff --git a/lib/fetch/common.c b/lib/fetch/common.c index b3d8f2f0..399c4628 100644 --- a/lib/fetch/common.c +++ b/lib/fetch/common.c @@ -4,6 +4,7 @@ * Copyright (c) 1998-2014 Dag-Erling Smorgrav * Copyright (c) 2008, 2010 Joerg Sonnenberger * Copyright (c) 2013 Michael Gmelin + * Copyright (c) 2019 Duncan Overbruck * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -53,6 +54,8 @@ #include #include #include +#include +#include #ifndef MSG_NOSIGNAL #include @@ -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(); diff --git a/lib/fetch/fetch.c b/lib/fetch/fetch.c index d1041a92..b7fee67d 100644 --- a/lib/fetch/fetch.c +++ b/lib/fetch/fetch.c @@ -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; diff --git a/lib/fetch/fetch.h b/lib/fetch/fetch.h index df1f3220..9166aa50 100644 --- a/lib/fetch/fetch.h +++ b/lib/fetch/fetch.h @@ -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;