ip happy eyeballing: keep attempts running

When `CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS` expires, start the next ip
connect attempt, but keep all ongoing attempts alive.

Separate happy-eyeballs connection filter into own source files.

Closes #18105
This commit is contained in:
Stefan Eissing 2025-07-31 10:23:35 +02:00 committed by Daniel Stenberg
parent 4654493fed
commit af69c9d636
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
8 changed files with 1071 additions and 955 deletions

View File

@ -15,7 +15,7 @@ Added-in: 7.59.0
# NAME
CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS - head start for IPv6 for happy eyeballs
CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS - timing of connect attempts
# SYNOPSIS
@ -28,17 +28,65 @@ CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS,
# DESCRIPTION
Happy eyeballs is an algorithm that attempts to connect to both IPv4 and IPv6
addresses for dual-stack hosts, preferring IPv6 first for *timeout*
milliseconds. If the IPv6 address cannot be connected to within that time then
a connection attempt is made to the IPv4 address in parallel. The first
connection to be established is the one that is used.
Happy eyeballs is an algorithm that controls connecting to a host that
resolves to more than one IP address. A common setup is to expose an
IPv4 and IPv6 address (dual-stack). Other host offer a range of addresses
for one or both stacks.
## IP Addresses
When curl is built with IPv6 support, it attempts to connect to IPv6
first, when available. When that fails, another connect attempt for
the first IPv4 address (again, if available) is started. Should that
fail, the next IPv6 address is used, then the next IPv4, etc. If there
are only addresses for one stack, those are tried one after the other.
When there is neither a positive nor negative response to an attempt,
another attempt is started after *timeout* has passed. Then another,
after *timeout* has passed again. As long as there are addresses available.
When all addresses have been tried and failed, the transfer fails.
All attempts are aborted after CURLOPT_CONNECTTIMEOUT_MS(3) has
passed, counted from the first attempt onward.
The range of suggested useful values for *timeout* is limited. Happy
Eyeballs RFC 6555 says "It is RECOMMENDED that connection attempts be paced
150-250 ms apart to balance human factors against network load." libcurl
currently defaults to 200 ms. Firefox and Chrome currently default to 300 ms.
As an example, for a host that resolves to 'a1_v4, a2_v4, a3_v6, a4_v6'
curl opens a socket to 'a3_v6' first. When that does not report back,
it opens another socket to 'a1_v4' after 200ms. The first socket is
left open and might still succeed. When 200ms have gone by again, a
socket for 'a4_v6' is opened. 200ms later, 'a2_v4' is tried.
At this point, there are 4 sockets open (unless the network has reported
anything back). That took 3 times the happy eyeballs timeout, so 600ms
in the default setting. When any of those four report a success, that
socket is used for the transfer and the other three are closed.
There are situations where connect attempts fail, but the failure is
considered being inconclusive. The QUIC protocol may encounter this.
When a QUIC server restarts, it may send replies indicating that it
is not accepting new connections right now, but maybe later.
Such "inclusive" connect attempt failures cause a restart of
the attempt, with the same address on a new socket, closing the
previous one. Repeatedly until CURLOPT_CONNECTTIMEOUT_MS(3) strikes.
## HTTPS
When connection with the HTTPS protocol to a host that may talk HTTP/3,
HTTP/2 or HTTP/1.1, curl applies a similar happy eyeballs strategy when
attempting these versions.
When HTTPS only involves a TCP connection, the versions are negotiated
via ALPN, the TLS extension, in a single connect. Since HTTP/3 runs on
QUIC (which runs on UDP), it requires a separate connect attempt.
The HTTP/3 attempt is started first and, after *timeout* expires, the
HTTP/2 (or 1.1) attempt is started in parallel.
# DEFAULT
CURL_HET_DEFAULT (currently defined as 200L)

View File

@ -146,6 +146,7 @@ LIB_CFILES = \
cf-h2-proxy.c \
cf-haproxy.c \
cf-https-connect.c \
cf-ip-happy.c \
cf-socket.c \
cfilters.c \
conncache.c \
@ -274,6 +275,7 @@ LIB_HFILES = \
cf-h2-proxy.h \
cf-haproxy.h \
cf-https-connect.h \
cf-ip-happy.h \
cf-socket.h \
cfilters.h \
conncache.h \

945
lib/cf-ip-happy.c Normal file
View File

@ -0,0 +1,945 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#ifdef HAVE_NETINET_IN_H
#include <netinet/in.h> /* <netinet/tcp.h> may need it */
#endif
#ifdef HAVE_SYS_UN_H
#include <sys/un.h> /* for sockaddr_un */
#endif
#ifdef HAVE_LINUX_TCP_H
#include <linux/tcp.h>
#elif defined(HAVE_NETINET_TCP_H)
#include <netinet/tcp.h>
#endif
#ifdef HAVE_SYS_IOCTL_H
#include <sys/ioctl.h>
#endif
#ifdef HAVE_NETDB_H
#include <netdb.h>
#endif
#ifdef HAVE_FCNTL_H
#include <fcntl.h>
#endif
#ifdef HAVE_ARPA_INET_H
#include <arpa/inet.h>
#endif
#ifdef __VMS
#include <in.h>
#include <inet.h>
#endif
#include "urldata.h"
#include "connect.h"
#include "cfilters.h"
#include "cf-ip-happy.h"
#include "curl_trc.h"
#include "multiif.h"
#include "progress.h"
#include "vquic/vquic.h" /* for quic cfilters */
/* The last 3 #include files should be in this order */
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"
struct transport_provider {
int transport;
cf_ip_connect_create *cf_create;
};
static
#ifndef UNITTESTS
const
#endif
struct transport_provider transport_providers[] = {
{ TRNSPRT_TCP, Curl_cf_tcp_create },
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3)
{ TRNSPRT_QUIC, Curl_cf_quic_create },
#endif
#ifndef CURL_DISABLE_TFTP
{ TRNSPRT_UDP, Curl_cf_udp_create },
#endif
#ifdef USE_UNIX_SOCKETS
{ TRNSPRT_UNIX, Curl_cf_unix_create },
#endif
};
static cf_ip_connect_create *get_cf_create(int transport)
{
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(transport_providers); ++i) {
if(transport == transport_providers[i].transport)
return transport_providers[i].cf_create;
}
return NULL;
}
#ifdef UNITTESTS
/* used by unit2600.c */
void Curl_debug_set_transport_provider(int transport,
cf_ip_connect_create *cf_create)
{
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(transport_providers); ++i) {
if(transport == transport_providers[i].transport) {
transport_providers[i].cf_create = cf_create;
return;
}
}
}
#endif /* UNITTESTS */
struct cf_ai_iter {
const struct Curl_addrinfo *head;
const struct Curl_addrinfo *last;
int ai_family;
int n;
};
static void cf_ai_iter_init(struct cf_ai_iter *iter,
const struct Curl_addrinfo *list,
int ai_family)
{
iter->head = list;
iter->ai_family = ai_family;
iter->last = NULL;
iter->n = -1;
}
static const struct Curl_addrinfo *cf_ai_iter_next(struct cf_ai_iter *iter)
{
const struct Curl_addrinfo *addr;
if(iter->n < 0) {
iter->n++;
for(addr = iter->head; addr; addr = addr->ai_next) {
if(addr->ai_family == iter->ai_family)
break;
}
iter->last = addr;
}
else if(iter->last) {
iter->n++;
for(addr = iter->last->ai_next; addr; addr = addr->ai_next) {
if(addr->ai_family == iter->ai_family)
break;
}
iter->last = addr;
}
return iter->last;
}
#ifdef USE_IPV6
static bool cf_ai_iter_done(struct cf_ai_iter *iter)
{
return (iter->n >= 0) && !iter->last;
}
#endif
struct cf_ip_attempt {
struct cf_ip_attempt *next;
const struct Curl_addrinfo *addr; /* List of addresses to try, not owned */
struct Curl_cfilter *cf; /* current sub-cfilter connecting */
cf_ip_connect_create *cf_create;
struct curltime started; /* start of current attempt */
CURLcode result;
int ai_family;
int transport;
int error;
BIT(connected); /* cf has connected */
BIT(shutdown); /* cf has shutdown */
BIT(inconclusive); /* connect was not a hard failure, we
* might talk to a restarting server */
};
static void cf_ip_attempt_free(struct cf_ip_attempt *a,
struct Curl_easy *data)
{
if(a) {
if(a->cf)
Curl_conn_cf_discard_chain(&a->cf, data);
free(a);
}
}
static CURLcode cf_ip_attempt_new(struct cf_ip_attempt **pa,
struct Curl_cfilter *cf,
struct Curl_easy *data,
const struct Curl_addrinfo *addr,
int ai_family,
int transport,
cf_ip_connect_create *cf_create)
{
struct Curl_cfilter *wcf;
struct cf_ip_attempt *a;
CURLcode result = CURLE_OK;
*pa = NULL;
a = calloc(1, sizeof(*a));
if(!a)
return CURLE_OUT_OF_MEMORY;
a->addr = addr;
a->ai_family = ai_family;
a->transport = transport;
a->result = CURLE_OK;
a->cf_create = cf_create;
*pa = a;
result = a->cf_create(&a->cf, data, cf->conn, a->addr, transport);
if(result)
goto out;
/* the new filter might have sub-filters */
for(wcf = a->cf; wcf; wcf = wcf->next) {
wcf->conn = cf->conn;
wcf->sockindex = cf->sockindex;
}
out:
if(result) {
cf_ip_attempt_free(a, data);
*pa = NULL;
}
return result;
}
static CURLcode cf_ip_attempt_connect(struct cf_ip_attempt *a,
struct Curl_easy *data,
bool *connected)
{
*connected = a->connected;
if(!a->result && !*connected) {
/* evaluate again */
a->result = Curl_conn_cf_connect(a->cf, data, connected);
if(!a->result) {
if(*connected) {
a->connected = TRUE;
}
}
else if(a->result == CURLE_WEIRD_SERVER_REPLY)
a->inconclusive = TRUE;
}
return a->result;
}
struct cf_ip_ballers {
struct cf_ip_attempt *running;
struct cf_ip_attempt *winner;
struct cf_ai_iter addr_iter;
#ifdef USE_IPV6
struct cf_ai_iter ipv6_iter;
#endif
cf_ip_connect_create *cf_create; /* for creating cf */
struct curltime started;
struct curltime last_attempt_started;
timediff_t attempt_delay_ms;
int last_attempt_ai_family;
int transport;
};
static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a,
struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct Curl_cfilter *cf_prev = a->cf;
struct Curl_cfilter *wcf;
CURLcode result;
/* When restarting, we tear down and existing filter *after* we
* started up the new one. This gives us a new socket number and
* probably a new local port. Which may prevent confusion. */
a->result = CURLE_OK;
a->connected = FALSE;
a->inconclusive = FALSE;
a->cf = NULL;
result = a->cf_create(&a->cf, data, cf->conn, a->addr, a->transport);
if(!result) {
bool dummy;
/* the new filter might have sub-filters */
for(wcf = a->cf; wcf; wcf = wcf->next) {
wcf->conn = cf->conn;
wcf->sockindex = cf->sockindex;
}
a->result = cf_ip_attempt_connect(a, data, &dummy);
}
if(cf_prev)
Curl_conn_cf_discard_chain(&cf_prev, data);
return result;
}
static void cf_ip_ballers_clear(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct cf_ip_ballers *bs)
{
(void)cf;
while(bs->running) {
struct cf_ip_attempt *a = bs->running;
bs->running = a->next;
cf_ip_attempt_free(a, data);
}
cf_ip_attempt_free(bs->winner, data);
bs->winner = NULL;
}
static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, int ip_version,
const struct Curl_addrinfo *addr_list,
cf_ip_connect_create *cf_create,
int transport,
timediff_t attempt_delay_ms)
{
memset(bs, 0, sizeof(*bs));
bs->cf_create = cf_create;
bs->transport = transport;
bs->attempt_delay_ms = attempt_delay_ms;
bs->last_attempt_ai_family = AF_INET; /* so AF_INET6 is next */
if(transport == TRNSPRT_UNIX) {
#ifdef USE_UNIX_SOCKETS
cf_ai_iter_init(&bs->addr_iter, addr_list, AF_UNIX);
#else
return CURLE_UNSUPPORTED_PROTOCOL;
#endif
}
else { /* TCP/UDP/QUIC */
#ifdef USE_IPV6
if(ip_version == CURL_IPRESOLVE_V6)
cf_ai_iter_init(&bs->addr_iter, NULL, AF_INET);
else
cf_ai_iter_init(&bs->addr_iter, addr_list, AF_INET);
if(ip_version == CURL_IPRESOLVE_V4)
cf_ai_iter_init(&bs->ipv6_iter, NULL, AF_INET6);
else
cf_ai_iter_init(&bs->ipv6_iter, addr_list, AF_INET6);
#else
(void)ip_version;
cf_ai_iter_init(&bs->addr_iter, addr_list, AF_INET);
#endif
}
return CURLE_OK;
}
static CURLcode cf_ip_ballers_run(struct cf_ip_ballers *bs,
struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *connected)
{
CURLcode result = CURLE_OK;
struct cf_ip_attempt *a = NULL, **panchor;
bool do_more, more_possible;
struct curltime now;
timediff_t next_expire_ms;
int i, inconclusive, ongoing;
if(bs->winner)
return CURLE_OK;
evaluate:
now = curlx_now();
ongoing = inconclusive = 0;
more_possible = TRUE;
/* check if a running baller connects now */
i = -1;
for(panchor = &bs->running; *panchor; panchor = &((*panchor)->next)) {
++i;
a = *panchor;
a->result = cf_ip_attempt_connect(a, data, connected);
if(!a->result) {
if(*connected) {
/* connected, declare the winner, remove from running,
* clear remaining running list. */
CURL_TRC_CF(data, cf, "connect attempt #%d successful", i);
bs->winner = a;
*panchor = a->next;
a->next = NULL;
while(bs->running) {
a = bs->running;
bs->running = a->next;
cf_ip_attempt_free(a, data);
}
return CURLE_OK;
}
/* still running */
++ongoing;
}
else if(a->inconclusive) /* failed, but inconclusive */
++inconclusive;
}
if(bs->running)
CURL_TRC_CF(data, cf, "checked connect attempts: "
"%d ongoing, %d inconclusive", ongoing, inconclusive);
/* no attempt connected yet, start another one? */
if(!ongoing) {
if(!bs->started.tv_sec && !bs->started.tv_usec)
bs->started = now;
do_more = TRUE;
}
else {
do_more = (curlx_timediff(now, bs->last_attempt_started) >=
bs->attempt_delay_ms);
if(do_more)
CURL_TRC_CF(data, cf, "happy eyeballs timeout expired, "
"start next attempt");
}
if(do_more) {
/* start the next attempt if there is another ip address to try.
* Alternate between address families when possible. */
const struct Curl_addrinfo *addr = NULL;
int ai_family = 0;
#ifdef USE_IPV6
if((bs->last_attempt_ai_family == AF_INET) ||
cf_ai_iter_done(&bs->addr_iter)) {
addr = cf_ai_iter_next(&bs->ipv6_iter);
ai_family = bs->ipv6_iter.ai_family;
}
#endif
if(!addr) {
addr = cf_ai_iter_next(&bs->addr_iter);
ai_family = bs->addr_iter.ai_family;
}
if(addr) { /* try another address */
result = cf_ip_attempt_new(&a, cf, data, addr, ai_family,
bs->transport, bs->cf_create);
CURL_TRC_CF(data, cf, "starting %s attempt for ipv%s -> %d",
bs->running ? "next" : "first",
(ai_family == AF_INET) ? "4" : "6", result);
if(result)
goto out;
DEBUGASSERT(a);
/* append to running list */
panchor = &bs->running;
while(*panchor)
panchor = &((*panchor)->next);
*panchor = a;
bs->last_attempt_started = now;
bs->last_attempt_ai_family = ai_family;
/* and run everything again */
goto evaluate;
}
else if(inconclusive) {
/* tried all addresses, no success but some where inconclusive.
* Let's restart the inconclusive ones. */
if(curlx_timediff(now, bs->last_attempt_started) >=
bs->attempt_delay_ms) {
CURL_TRC_CF(data, cf, "tried all addresses with inconclusive results"
", restarting one");
i = -1;
for(a = bs->running; a; a = a->next) {
++i;
if(!a->inconclusive)
continue;
result = cf_ip_attempt_restart(a, cf, data);
CURL_TRC_CF(data, cf, "restarted baller %d -> %d", i, result);
if(result) /* serious failure */
goto out;
bs->last_attempt_started = now;
goto evaluate;
}
DEBUGASSERT(0); /* should not come here */
}
/* attempt timeout for restart has not expired yet */
goto out;
}
else if(ongoing) {
/* no more addresses, no inconclusive attempts */
more_possible = FALSE;
}
else {
CURL_TRC_CF(data, cf, "no more attempts to try");
result = CURLE_COULDNT_CONNECT;
i = 0;
for(a = bs->running; a; a = a->next) {
CURL_TRC_CF(data, cf, "baller %d: result=%d", i, a->result);
if(a->result)
result = a->result;
}
}
}
out:
if(!result) {
/* when do we need to be called again? */
next_expire_ms = Curl_timeleft(data, &now, TRUE);
if(more_possible) {
timediff_t expire_ms, elapsed_ms;
elapsed_ms = curlx_timediff(now, bs->last_attempt_started);
expire_ms = CURLMAX(bs->attempt_delay_ms - elapsed_ms, 0);
next_expire_ms = CURLMIN(next_expire_ms, expire_ms);
}
if(next_expire_ms <= 0) {
failf(data, "Connection timeout after %" FMT_OFF_T " ms",
curlx_timediff(now, data->progress.t_startsingle));
return CURLE_OPERATION_TIMEDOUT;
}
Curl_expire(data, next_expire_ms, EXPIRE_HAPPY_EYEBALLS);
}
return result;
}
static CURLcode cf_ip_ballers_shutdown(struct cf_ip_ballers *bs,
struct Curl_easy *data,
bool *done)
{
struct cf_ip_attempt *a;
/* shutdown all ballers that have not done so already. If one fails,
* continue shutting down others until all are shutdown. */
*done = TRUE;
for(a = bs->running; a; a = a->next) {
bool bdone = FALSE;
if(a->shutdown)
continue;
a->result = a->cf->cft->do_shutdown(a->cf, data, &bdone);
if(a->result || bdone)
a->shutdown = TRUE; /* treat a failed shutdown as done */
else
*done = FALSE;
}
return CURLE_OK;
}
static void cf_ip_ballers_pollset(struct cf_ip_ballers *bs,
struct Curl_easy *data,
struct easy_pollset *ps)
{
struct cf_ip_attempt *a;
for(a = bs->running; a; a = a->next) {
if(a->result)
continue;
Curl_conn_cf_adjust_pollset(a->cf, data, ps);
}
}
static bool cf_ip_ballers_pending(struct cf_ip_ballers *bs,
const struct Curl_easy *data)
{
struct cf_ip_attempt *a;
for(a = bs->running; a; a = a->next) {
if(a->result)
continue;
if(a->cf->cft->has_data_pending(a->cf, data))
return TRUE;
}
return FALSE;
}
static struct curltime cf_ip_ballers_max_time(struct cf_ip_ballers *bs,
struct Curl_easy *data,
int query)
{
struct curltime t, tmax;
struct cf_ip_attempt *a;
memset(&tmax, 0, sizeof(tmax));
for(a = bs->running; a; a = a->next) {
memset(&t, 0, sizeof(t));
if(!a->cf->cft->query(a->cf, data, query, NULL, &t)) {
if((t.tv_sec || t.tv_usec) && curlx_timediff_us(t, tmax) > 0)
tmax = t;
}
}
return tmax;
}
static int cf_ip_ballers_min_reply_ms(struct cf_ip_ballers *bs,
struct Curl_easy *data)
{
int reply_ms = -1, breply_ms;
struct cf_ip_attempt *a;
for(a = bs->running; a; a = a->next) {
if(!a->cf->cft->query(a->cf, data, CF_QUERY_CONNECT_REPLY_MS,
&breply_ms, NULL)) {
if(breply_ms >= 0 && (reply_ms < 0 || breply_ms < reply_ms))
reply_ms = breply_ms;
}
}
return reply_ms;
}
typedef enum {
SCFST_INIT,
SCFST_WAITING,
SCFST_DONE
} cf_connect_state;
struct cf_ip_happy_ctx {
int transport;
cf_ip_connect_create *cf_create;
cf_connect_state state;
struct cf_ip_ballers ballers;
struct curltime started;
};
static CURLcode is_connected(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *connected)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
struct connectdata *conn = cf->conn;
CURLcode result;
result = cf_ip_ballers_run(&ctx->ballers, cf, data, connected);
if(!result)
return CURLE_OK;
{
const char *hostname, *proxy_name = NULL;
int port;
#ifndef CURL_DISABLE_PROXY
if(conn->bits.socksproxy)
proxy_name = conn->socks_proxy.host.name;
else if(conn->bits.httpproxy)
proxy_name = conn->http_proxy.host.name;
#endif
hostname = conn->bits.conn_to_host ?
conn->conn_to_host.name : conn->host.name;
if(cf->sockindex == SECONDARYSOCKET)
port = conn->secondary_port;
else if(cf->conn->bits.conn_to_port)
port = conn->conn_to_port;
else
port = conn->remote_port;
failf(data, "Failed to connect to %s port %u %s%s%safter "
"%" FMT_TIMEDIFF_T " ms: %s",
hostname, port,
proxy_name ? "via " : "",
proxy_name ? proxy_name : "",
proxy_name ? " " : "",
curlx_timediff(curlx_now(), data->progress.t_startsingle),
curl_easy_strerror(result));
}
#ifdef SOCKETIMEDOUT
if(SOCKETIMEDOUT == data->state.os_errno)
result = CURLE_OPERATION_TIMEDOUT;
#endif
return result;
}
/*
* Connect to the given host with timeout, proxy or remote does not matter.
* There might be more than one IP address to try out.
*/
static CURLcode start_connect(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
struct Curl_dns_entry *dns = data->state.dns[cf->sockindex];
if(!dns)
return CURLE_FAILED_INIT;
if(Curl_timeleft(data, NULL, TRUE) < 0) {
/* a precaution, no need to continue if time already is up */
failf(data, "Connection time-out");
return CURLE_OPERATION_TIMEDOUT;
}
CURL_TRC_CF(data, cf, "init ip ballers for transport %d", ctx->transport);
ctx->started = curlx_now();
return cf_ip_ballers_init(&ctx->ballers, cf->conn->ip_version,
dns->addr, ctx->cf_create, ctx->transport,
data->set.happy_eyeballs_timeout);
}
static void cf_ip_happy_ctx_clear(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
DEBUGASSERT(ctx);
DEBUGASSERT(data);
cf_ip_ballers_clear(cf, data, &ctx->ballers);
}
static CURLcode cf_ip_happy_shutdown(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
DEBUGASSERT(data);
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
result = cf_ip_ballers_shutdown(&ctx->ballers, data, done);
CURL_TRC_CF(data, cf, "shutdown -> %d, done=%d", result, *done);
return result;
}
static void cf_ip_happy_adjust_pollset(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct easy_pollset *ps)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
if(!cf->connected) {
cf_ip_ballers_pollset(&ctx->ballers, data, ps);
CURL_TRC_CF(data, cf, "adjust_pollset -> %d socks", ps->num);
}
}
static CURLcode cf_ip_happy_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
DEBUGASSERT(ctx);
*done = FALSE;
switch(ctx->state) {
case SCFST_INIT:
DEBUGASSERT(CURL_SOCKET_BAD == Curl_conn_cf_get_socket(cf, data));
DEBUGASSERT(!cf->connected);
result = start_connect(cf, data);
if(result)
return result;
ctx->state = SCFST_WAITING;
FALLTHROUGH();
case SCFST_WAITING:
result = is_connected(cf, data, done);
if(!result && *done) {
DEBUGASSERT(ctx->ballers.winner);
DEBUGASSERT(ctx->ballers.winner->cf);
DEBUGASSERT(ctx->ballers.winner->cf->connected);
/* we have a winner. Install and activate it.
* close/free all others. */
ctx->state = SCFST_DONE;
cf->connected = TRUE;
cf->next = ctx->ballers.winner->cf;
ctx->ballers.winner->cf = NULL;
cf_ip_happy_ctx_clear(cf, data);
if(cf->conn->handler->protocol & PROTO_FAMILY_SSH)
Curl_pgrsTime(data, TIMER_APPCONNECT); /* we are connected already */
#ifndef CURL_DISABLE_VERBOSE_STRINGS
if(Curl_trc_cf_is_verbose(cf, data)) {
struct ip_quadruple ipquad;
bool is_ipv6;
if(!Curl_conn_cf_get_ip_info(cf->next, data, &is_ipv6, &ipquad)) {
const char *host;
int port;
Curl_conn_get_current_host(data, cf->sockindex, &host, &port);
CURL_TRC_CF(data, cf, "Connected to %s (%s) port %u",
host, ipquad.remote_ip, ipquad.remote_port);
}
}
#endif
data->info.numconnects++; /* to track the # of connections made */
}
break;
case SCFST_DONE:
*done = TRUE;
break;
}
return result;
}
static void cf_ip_happy_close(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
CURL_TRC_CF(data, cf, "close");
cf_ip_happy_ctx_clear(cf, data);
cf->connected = FALSE;
ctx->state = SCFST_INIT;
if(cf->next) {
cf->next->cft->do_close(cf->next, data);
Curl_conn_cf_discard_chain(&cf->next, data);
}
}
static bool cf_ip_happy_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
if(!cf->connected) {
return cf_ip_ballers_pending(&ctx->ballers, data);
}
return cf->next->cft->has_data_pending(cf->next, data);
}
static CURLcode cf_ip_happy_query(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query, int *pres1, void *pres2)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
if(!cf->connected) {
switch(query) {
case CF_QUERY_CONNECT_REPLY_MS: {
*pres1 = cf_ip_ballers_min_reply_ms(&ctx->ballers, data);
CURL_TRC_CF(data, cf, "query connect reply: %dms", *pres1);
return CURLE_OK;
}
case CF_QUERY_TIMER_CONNECT: {
struct curltime *when = pres2;
*when = cf_ip_ballers_max_time(&ctx->ballers, data,
CF_QUERY_TIMER_CONNECT);
return CURLE_OK;
}
case CF_QUERY_TIMER_APPCONNECT: {
struct curltime *when = pres2;
*when = cf_ip_ballers_max_time(&ctx->ballers, data,
CF_QUERY_TIMER_APPCONNECT);
return CURLE_OK;
}
default:
break;
}
}
return cf->next ?
cf->next->cft->query(cf->next, data, query, pres1, pres2) :
CURLE_UNKNOWN_OPTION;
}
static void cf_ip_happy_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_ip_happy_ctx *ctx = cf->ctx;
CURL_TRC_CF(data, cf, "destroy");
if(ctx) {
cf_ip_happy_ctx_clear(cf, data);
}
/* release any resources held in state */
Curl_safefree(ctx);
}
struct Curl_cftype Curl_cft_ip_happy = {
"HAPPY-EYEBALLS",
0,
CURL_LOG_LVL_NONE,
cf_ip_happy_destroy,
cf_ip_happy_connect,
cf_ip_happy_close,
cf_ip_happy_shutdown,
cf_ip_happy_adjust_pollset,
cf_ip_happy_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
cf_ip_happy_query,
};
/**
* Create an IP happy eyeball connection filter that uses the, once resolved,
* address information to connect on ip families based on connection
* configuration.
* @param pcf output, the created cfilter
* @param data easy handle used in creation
* @param conn connection the filter is created for
* @param cf_create method to create the sub-filters performing the
* actual connects.
*/
static CURLcode cf_ip_happy_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
cf_ip_connect_create *cf_create,
int transport)
{
struct cf_ip_happy_ctx *ctx = NULL;
CURLcode result;
(void)data;
(void)conn;
*pcf = NULL;
ctx = calloc(1, sizeof(*ctx));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
ctx->transport = transport;
ctx->cf_create = cf_create;
result = Curl_cf_create(pcf, &Curl_cft_ip_happy, ctx);
out:
if(result) {
Curl_safefree(*pcf);
free(ctx);
}
return result;
}
CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
int transport)
{
cf_ip_connect_create *cf_create;
struct Curl_cfilter *cf;
CURLcode result;
/* Need to be first */
DEBUGASSERT(cf_at);
cf_create = get_cf_create(transport);
if(!cf_create) {
CURL_TRC_CF(data, cf_at, "unsupported transport type %d", transport);
return CURLE_UNSUPPORTED_PROTOCOL;
}
result = cf_ip_happy_create(&cf, data, cf_at->conn, cf_create, transport);
if(result)
return result;
Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK;
}

59
lib/cf-ip-happy.h Normal file
View File

@ -0,0 +1,59 @@
#ifndef HEADER_CURL_IP_HAPPY_H
#define HEADER_CURL_IP_HAPPY_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#include "curlx/nonblock.h" /* for curlx_nonblock() */
#include "sockaddr.h"
/**
* Create a cfilter for making an "ip" connection to the
* given address, using parameters from `conn`. The "ip" connection
* can be a TCP socket, a UDP socket or even a QUIC connection.
*
* It MUST use only the supplied `ai` for its connection attempt.
*
* Such a filter may be used in "happy eyeball" scenarios, and its
* `connect` implementation needs to support non-blocking. Once connected,
* it MAY be installed in the connection filter chain to serve transfers.
*/
typedef CURLcode cf_ip_connect_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
const struct Curl_addrinfo *ai,
int transport);
CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
int transport);
extern struct Curl_cftype Curl_cft_ip_happy;
#ifdef UNITTESTS
void Curl_debug_set_transport_provider(int transport,
cf_ip_connect_create *cf_create);
#endif
#endif /* HEADER_CURL_IP_HAPPY_H */

View File

@ -61,6 +61,7 @@
#include "connect.h"
#include "cf-haproxy.h"
#include "cf-https-connect.h"
#include "cf-ip-happy.h"
#include "cf-socket.h"
#include "select.h"
#include "url.h" /* for Curl_safefree() */
@ -74,7 +75,6 @@
#include "conncache.h"
#include "multihandle.h"
#include "share.h"
#include "vquic/vquic.h" /* for quic cfilters */
#include "http_proxy.h"
#include "socks.h"
@ -231,28 +231,6 @@ bool Curl_shutdown_started(struct Curl_easy *data, int sockindex)
return (pt->tv_sec > 0) || (pt->tv_usec > 0);
}
static const struct Curl_addrinfo *
addr_first_match(const struct Curl_addrinfo *addr, int family)
{
while(addr) {
if(addr->ai_family == family)
return addr;
addr = addr->ai_next;
}
return NULL;
}
static const struct Curl_addrinfo *
addr_next_match(const struct Curl_addrinfo *addr, int family)
{
while(addr && addr->ai_next) {
addr = addr->ai_next;
if(addr->ai_family == family)
return addr;
}
return NULL;
}
/* retrieves ip address and port from a sockaddr structure. note it calls
curlx_inet_ntop which sets errno on fail, not SOCKERRNO. */
bool Curl_addr2string(struct sockaddr *sa, curl_socklen_t salen,
@ -371,886 +349,6 @@ void Curl_conncontrol(struct connectdata *conn,
}
}
/**
* job walking the matching addr infos, creating a sub-cfilter with the
* provided method `cf_create` and running setup/connect on it.
*/
struct eyeballer {
const char *name;
const struct Curl_addrinfo *first; /* complete address list, not owned */
const struct Curl_addrinfo *addr; /* List of addresses to try, not owned */
int ai_family; /* matching address family only */
cf_ip_connect_create *cf_create; /* for creating cf */
struct Curl_cfilter *cf; /* current sub-cfilter connecting */
struct eyeballer *primary; /* eyeballer this one is backup for */
timediff_t delay_ms; /* delay until start */
struct curltime started; /* start of current attempt */
timediff_t timeoutms; /* timeout for current attempt */
expire_id timeout_id; /* ID for Curl_expire() */
CURLcode result;
int error;
BIT(has_started); /* attempts have started */
BIT(is_done); /* out of addresses/time */
BIT(connected); /* cf has connected */
BIT(shutdown); /* cf has shutdown */
BIT(inconclusive); /* connect was not a hard failure, we
* might talk to a restarting server */
};
typedef enum {
SCFST_INIT,
SCFST_WAITING,
SCFST_DONE
} cf_connect_state;
struct cf_he_ctx {
cf_ip_connect_create *cf_create;
cf_connect_state state;
struct eyeballer *baller[2];
struct eyeballer *winner;
struct curltime started;
int transport;
};
/* when there are more than one IP address left to use, this macro returns how
much of the given timeout to spend on *this* attempt */
#define TIMEOUT_LARGE 600
#define USETIME(ms) ((ms > TIMEOUT_LARGE) ? (ms / 2) : ms)
static CURLcode eyeballer_new(struct eyeballer **pballer,
cf_ip_connect_create *cf_create,
const struct Curl_addrinfo *addr,
int ai_family,
struct eyeballer *primary,
timediff_t delay_ms,
timediff_t timeout_ms,
expire_id timeout_id)
{
struct eyeballer *baller;
*pballer = NULL;
baller = calloc(1, sizeof(*baller));
if(!baller)
return CURLE_OUT_OF_MEMORY;
baller->name = ((ai_family == AF_INET) ? "ipv4" : (
#ifdef USE_IPV6
(ai_family == AF_INET6) ? "ipv6" :
#endif
"ip"));
baller->cf_create = cf_create;
baller->first = baller->addr = addr;
baller->ai_family = ai_family;
baller->primary = primary;
baller->delay_ms = delay_ms;
baller->timeoutms = addr_next_match(baller->addr, baller->ai_family) ?
USETIME(timeout_ms) : timeout_ms;
baller->timeout_id = timeout_id;
baller->result = CURLE_COULDNT_CONNECT;
*pballer = baller;
return CURLE_OK;
}
static void baller_close(struct eyeballer *baller,
struct Curl_easy *data)
{
if(baller && baller->cf) {
Curl_conn_cf_discard_chain(&baller->cf, data);
}
}
static void baller_free(struct eyeballer *baller,
struct Curl_easy *data)
{
if(baller) {
baller_close(baller, data);
free(baller);
}
}
static void baller_rewind(struct eyeballer *baller)
{
baller->addr = baller->first;
baller->inconclusive = FALSE;
}
static void baller_next_addr(struct eyeballer *baller)
{
baller->addr = addr_next_match(baller->addr, baller->ai_family);
}
/*
* Initiate a connect attempt walk.
*
* Note that even on connect fail it returns CURLE_OK, but with 'sock' set to
* CURL_SOCKET_BAD. Other errors will however return proper errors.
*/
static void baller_initiate(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct eyeballer *baller)
{
struct cf_he_ctx *ctx = cf->ctx;
struct Curl_cfilter *cf_prev = baller->cf;
struct Curl_cfilter *wcf;
CURLcode result;
/* Do not close a previous cfilter 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. */
result = baller->cf_create(&baller->cf, data, cf->conn, baller->addr,
ctx->transport);
if(result)
goto out;
/* the new filter might have sub-filters */
for(wcf = baller->cf; wcf; wcf = wcf->next) {
wcf->conn = cf->conn;
wcf->sockindex = cf->sockindex;
}
if(addr_next_match(baller->addr, baller->ai_family)) {
Curl_expire(data, baller->timeoutms, baller->timeout_id);
}
out:
if(result) {
CURL_TRC_CF(data, cf, "%s failed", baller->name);
baller_close(baller, data);
}
if(cf_prev)
Curl_conn_cf_discard_chain(&cf_prev, data);
baller->result = result;
}
/**
* Start a connection attempt on the current baller address.
* Will return CURLE_OK on the first address where a socket
* could be created and the non-blocking connect started.
* Returns error when all remaining addresses have been tried.
*/
static CURLcode baller_start(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct eyeballer *baller,
timediff_t timeoutms)
{
baller->error = 0;
baller->connected = FALSE;
baller->has_started = TRUE;
while(baller->addr) {
baller->started = curlx_now();
baller->timeoutms = addr_next_match(baller->addr, baller->ai_family) ?
USETIME(timeoutms) : timeoutms;
baller_initiate(cf, data, baller);
if(!baller->result)
break;
baller_next_addr(baller);
}
if(!baller->addr) {
baller->is_done = TRUE;
}
return baller->result;
}
/* Used within the multi interface. Try next IP address, returns error if no
more address exists or error */
static CURLcode baller_start_next(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct eyeballer *baller,
timediff_t timeoutms)
{
if(cf->sockindex == FIRSTSOCKET) {
baller_next_addr(baller);
/* If we get inconclusive answers from the server(s), we start
* again until this whole thing times out. This allows us to
* connect to servers that are gracefully restarting and the
* packet routing to the new instance has not happened yet (e.g. QUIC). */
if(!baller->addr && baller->inconclusive)
baller_rewind(baller);
baller_start(cf, data, baller, timeoutms);
}
else {
baller->error = 0;
baller->connected = FALSE;
baller->has_started = TRUE;
baller->is_done = TRUE;
baller->result = CURLE_COULDNT_CONNECT;
}
return baller->result;
}
static CURLcode baller_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct eyeballer *baller,
struct curltime *now,
bool *connected)
{
(void)cf;
*connected = baller->connected;
if(!baller->result && !*connected) {
/* evaluate again */
baller->result = Curl_conn_cf_connect(baller->cf, data, connected);
if(!baller->result) {
if(*connected) {
baller->connected = TRUE;
baller->is_done = TRUE;
}
else if(curlx_timediff(*now, baller->started) >= baller->timeoutms) {
infof(data, "%s connect timeout after %" FMT_TIMEDIFF_T
"ms, move on!", baller->name, baller->timeoutms);
#ifdef SOCKETIMEDOUT
baller->error = SOCKETIMEDOUT;
#endif
baller->result = CURLE_OPERATION_TIMEDOUT;
}
}
else if(baller->result == CURLE_WEIRD_SERVER_REPLY)
baller->inconclusive = TRUE;
}
return baller->result;
}
/*
* is_connected() checks if the socket has connected.
*/
static CURLcode is_connected(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *connected)
{
struct cf_he_ctx *ctx = cf->ctx;
struct connectdata *conn = cf->conn;
CURLcode result;
struct curltime now;
size_t i;
int ongoing, not_started;
/* Check if any of the conn->tempsock we use for establishing connections
* succeeded and, if so, close any ongoing other ones.
* Transfer the successful conn->tempsock to conn->sock[sockindex]
* and set conn->tempsock to CURL_SOCKET_BAD.
* If transport is QUIC, we need to shutdown the ongoing 'other'
* cot ballers in a QUIC appropriate way. */
evaluate:
*connected = FALSE; /* a negative world view is best */
now = curlx_now();
ongoing = not_started = 0;
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
if(!baller || baller->is_done)
continue;
if(!baller->has_started) {
++not_started;
continue;
}
baller->result = baller_connect(cf, data, baller, &now, connected);
CURL_TRC_CF(data, cf, "%s connect -> %d, connected=%d",
baller->name, baller->result, *connected);
if(!baller->result) {
if(*connected) {
/* connected, declare the winner */
ctx->winner = baller;
ctx->baller[i] = NULL;
break;
}
else { /* still waiting */
++ongoing;
}
}
else if(!baller->is_done) {
/* The baller failed to connect, start its next attempt */
if(baller->error) {
data->state.os_errno = baller->error;
SET_SOCKERRNO(baller->error);
}
baller_start_next(cf, data, baller, Curl_timeleft(data, &now, TRUE));
if(baller->is_done) {
CURL_TRC_CF(data, cf, "%s done", baller->name);
}
else {
/* next attempt was started */
CURL_TRC_CF(data, cf, "%s trying next", baller->name);
++ongoing;
Curl_multi_mark_dirty(data);
}
}
}
if(ctx->winner) {
*connected = TRUE;
return CURLE_OK;
}
/* Nothing connected, check the time before we might
* start new ballers or return ok. */
if((ongoing || not_started) && Curl_timeleft(data, &now, TRUE) < 0) {
failf(data, "Connection timeout after %" FMT_OFF_T " ms",
curlx_timediff(now, data->progress.t_startsingle));
return CURLE_OPERATION_TIMEDOUT;
}
/* Check if we have any waiting ballers to start now. */
if(not_started > 0) {
int added = 0;
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
if(!baller || baller->has_started)
continue;
/* We start its primary baller has failed to connect or if
* its start delay_ms have expired */
if((baller->primary && baller->primary->is_done) ||
curlx_timediff(now, ctx->started) >= baller->delay_ms) {
baller_start(cf, data, baller, Curl_timeleft(data, &now, TRUE));
if(baller->is_done) {
CURL_TRC_CF(data, cf, "%s done", baller->name);
}
else {
CURL_TRC_CF(data, cf, "%s starting (timeout=%" FMT_TIMEDIFF_T "ms)",
baller->name, baller->timeoutms);
++ongoing;
++added;
}
}
}
if(added > 0)
goto evaluate;
}
if(ongoing > 0) {
/* We are still trying, return for more waiting */
*connected = FALSE;
return CURLE_OK;
}
/* all ballers have failed to connect. */
CURL_TRC_CF(data, cf, "all eyeballers failed");
result = CURLE_COULDNT_CONNECT;
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
if(!baller)
continue;
CURL_TRC_CF(data, cf, "%s assess started=%d, result=%d",
baller->name, baller->has_started, baller->result);
if(baller->has_started && baller->result) {
result = baller->result;
break;
}
}
{
const char *hostname, *proxy_name = NULL;
int port;
#ifndef CURL_DISABLE_PROXY
if(conn->bits.socksproxy)
proxy_name = conn->socks_proxy.host.name;
else if(conn->bits.httpproxy)
proxy_name = conn->http_proxy.host.name;
#endif
hostname = conn->bits.conn_to_host ?
conn->conn_to_host.name : conn->host.name;
if(cf->sockindex == SECONDARYSOCKET)
port = conn->secondary_port;
else if(cf->conn->bits.conn_to_port)
port = conn->conn_to_port;
else
port = conn->remote_port;
failf(data, "Failed to connect to %s port %u %s%s%safter "
"%" FMT_TIMEDIFF_T " ms: %s",
hostname, port,
proxy_name ? "via " : "",
proxy_name ? proxy_name : "",
proxy_name ? " " : "",
curlx_timediff(now, data->progress.t_startsingle),
curl_easy_strerror(result));
}
#ifdef SOCKETIMEDOUT
if(SOCKETIMEDOUT == data->state.os_errno)
result = CURLE_OPERATION_TIMEDOUT;
#endif
return result;
}
/*
* Connect to the given host with timeout, proxy or remote does not matter.
* There might be more than one IP address to try out.
*/
static CURLcode start_connect(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_he_ctx *ctx = cf->ctx;
struct connectdata *conn = cf->conn;
CURLcode result = CURLE_COULDNT_CONNECT;
int ai_family0 = 0, ai_family1 = 0;
timediff_t timeout_ms = Curl_timeleft(data, NULL, TRUE);
const struct Curl_addrinfo *addr0 = NULL, *addr1 = NULL;
struct Curl_dns_entry *dns = data->state.dns[cf->sockindex];
if(!dns)
return CURLE_FAILED_INIT;
if(timeout_ms < 0) {
/* a precaution, no need to continue if time already is up */
failf(data, "Connection time-out");
return CURLE_OPERATION_TIMEDOUT;
}
ctx->started = curlx_now();
/* dns->addr is the list of addresses from the resolver, each
* with an address family. The list has at least one entry, possibly
* many more.
* We try at most 2 at a time, until we either get a connection or
* run out of addresses to try. Since likelihood of success is tied
* to the address family (e.g. IPV6 might not work at all ), we want
* the 2 connect attempt ballers to try different families, if possible.
*
*/
if(conn->ip_version == CURL_IPRESOLVE_V6) {
#ifdef USE_IPV6
ai_family0 = AF_INET6;
addr0 = addr_first_match(dns->addr, ai_family0);
#endif
}
else if(conn->ip_version == CURL_IPRESOLVE_V4) {
ai_family0 = AF_INET;
addr0 = addr_first_match(dns->addr, ai_family0);
}
else {
/* no user preference, we try ipv6 always first when available */
#ifdef USE_IPV6
ai_family0 = AF_INET6;
addr0 = addr_first_match(dns->addr, ai_family0);
#endif
/* next candidate is ipv4 */
ai_family1 = AF_INET;
addr1 = addr_first_match(dns->addr, ai_family1);
/* no ip address families, probably AF_UNIX or something, use the
* address family given to us */
if(!addr1 && !addr0 && dns->addr) {
ai_family0 = dns->addr->ai_family;
addr0 = addr_first_match(dns->addr, ai_family0);
}
}
if(!addr0 && addr1) {
/* switch around, so a single baller always uses addr0 */
addr0 = addr1;
ai_family0 = ai_family1;
addr1 = NULL;
}
/* We found no address that matches our criteria, we cannot connect */
if(!addr0) {
return CURLE_COULDNT_CONNECT;
}
memset(ctx->baller, 0, sizeof(ctx->baller));
result = eyeballer_new(&ctx->baller[0], ctx->cf_create, addr0, ai_family0,
NULL, 0, /* no primary/delay, start now */
timeout_ms, EXPIRE_DNS_PER_NAME);
if(result)
return result;
CURL_TRC_CF(data, cf, "created %s (timeout %" FMT_TIMEDIFF_T "ms)",
ctx->baller[0]->name, ctx->baller[0]->timeoutms);
if(addr1) {
/* second one gets a delayed start */
result = eyeballer_new(&ctx->baller[1], ctx->cf_create, addr1, ai_family1,
ctx->baller[0], /* wait on that to fail */
/* or start this delayed */
data->set.happy_eyeballs_timeout,
timeout_ms, EXPIRE_DNS_PER_NAME2);
if(result)
return result;
CURL_TRC_CF(data, cf, "created %s (timeout %" FMT_TIMEDIFF_T "ms)",
ctx->baller[1]->name, ctx->baller[1]->timeoutms);
Curl_expire(data, data->set.happy_eyeballs_timeout,
EXPIRE_HAPPY_EYEBALLS);
}
return CURLE_OK;
}
static void cf_he_ctx_clear(struct Curl_cfilter *cf, struct Curl_easy *data)
{
struct cf_he_ctx *ctx = cf->ctx;
size_t i;
DEBUGASSERT(ctx);
DEBUGASSERT(data);
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
baller_free(ctx->baller[i], data);
ctx->baller[i] = NULL;
}
baller_free(ctx->winner, data);
ctx->winner = NULL;
}
static CURLcode cf_he_shutdown(struct Curl_cfilter *cf,
struct Curl_easy *data, bool *done)
{
struct cf_he_ctx *ctx = cf->ctx;
size_t i;
CURLcode result = CURLE_OK;
DEBUGASSERT(data);
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
/* shutdown all ballers that have not done so already. If one fails,
* continue shutting down others until all are shutdown. */
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
bool bdone = FALSE;
if(!baller || !baller->cf || baller->shutdown)
continue;
baller->result = baller->cf->cft->do_shutdown(baller->cf, data, &bdone);
if(baller->result || bdone)
baller->shutdown = TRUE; /* treat a failed shutdown as done */
}
*done = TRUE;
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
if(ctx->baller[i] && !ctx->baller[i]->shutdown)
*done = FALSE;
}
if(*done) {
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
if(ctx->baller[i] && ctx->baller[i]->result)
result = ctx->baller[i]->result;
}
}
CURL_TRC_CF(data, cf, "shutdown -> %d, done=%d", result, *done);
return result;
}
static void cf_he_adjust_pollset(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct easy_pollset *ps)
{
struct cf_he_ctx *ctx = cf->ctx;
size_t i;
if(!cf->connected) {
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
if(!baller || !baller->cf)
continue;
Curl_conn_cf_adjust_pollset(baller->cf, data, ps);
}
CURL_TRC_CF(data, cf, "adjust_pollset -> %d socks", ps->num);
}
}
static CURLcode cf_he_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_he_ctx *ctx = cf->ctx;
CURLcode result = CURLE_OK;
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
DEBUGASSERT(ctx);
*done = FALSE;
switch(ctx->state) {
case SCFST_INIT:
DEBUGASSERT(CURL_SOCKET_BAD == Curl_conn_cf_get_socket(cf, data));
DEBUGASSERT(!cf->connected);
result = start_connect(cf, data);
if(result)
return result;
ctx->state = SCFST_WAITING;
FALLTHROUGH();
case SCFST_WAITING:
result = is_connected(cf, data, done);
if(!result && *done) {
DEBUGASSERT(ctx->winner);
DEBUGASSERT(ctx->winner->cf);
DEBUGASSERT(ctx->winner->cf->connected);
/* we have a winner. Install and activate it.
* close/free all others. */
ctx->state = SCFST_DONE;
cf->connected = TRUE;
cf->next = ctx->winner->cf;
ctx->winner->cf = NULL;
cf_he_ctx_clear(cf, data);
if(cf->conn->handler->protocol & PROTO_FAMILY_SSH)
Curl_pgrsTime(data, TIMER_APPCONNECT); /* we are connected already */
#ifndef CURL_DISABLE_VERBOSE_STRINGS
if(Curl_trc_cf_is_verbose(cf, data)) {
struct ip_quadruple ipquad;
bool is_ipv6;
if(!Curl_conn_cf_get_ip_info(cf->next, data, &is_ipv6, &ipquad)) {
const char *host;
int port;
Curl_conn_get_current_host(data, cf->sockindex, &host, &port);
CURL_TRC_CF(data, cf, "Connected to %s (%s) port %u",
host, ipquad.remote_ip, ipquad.remote_port);
}
}
#endif
data->info.numconnects++; /* to track the # of connections made */
}
break;
case SCFST_DONE:
*done = TRUE;
break;
}
return result;
}
static void cf_he_close(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_he_ctx *ctx = cf->ctx;
CURL_TRC_CF(data, cf, "close");
cf_he_ctx_clear(cf, data);
cf->connected = FALSE;
ctx->state = SCFST_INIT;
if(cf->next) {
cf->next->cft->do_close(cf->next, data);
Curl_conn_cf_discard_chain(&cf->next, data);
}
}
static bool cf_he_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
struct cf_he_ctx *ctx = cf->ctx;
size_t i;
if(cf->connected)
return cf->next->cft->has_data_pending(cf->next, data);
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
if(!baller || !baller->cf)
continue;
if(baller->cf->cft->has_data_pending(baller->cf, data))
return TRUE;
}
return FALSE;
}
static struct curltime get_max_baller_time(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query)
{
struct cf_he_ctx *ctx = cf->ctx;
struct curltime t, tmax;
size_t i;
memset(&tmax, 0, sizeof(tmax));
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
memset(&t, 0, sizeof(t));
if(baller && baller->cf &&
!baller->cf->cft->query(baller->cf, data, query, NULL, &t)) {
if((t.tv_sec || t.tv_usec) && curlx_timediff_us(t, tmax) > 0)
tmax = t;
}
}
return tmax;
}
static CURLcode cf_he_query(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query, int *pres1, void *pres2)
{
struct cf_he_ctx *ctx = cf->ctx;
if(!cf->connected) {
switch(query) {
case CF_QUERY_CONNECT_REPLY_MS: {
int reply_ms = -1;
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(ctx->baller); i++) {
struct eyeballer *baller = ctx->baller[i];
int breply_ms;
if(baller && baller->cf &&
!baller->cf->cft->query(baller->cf, data, query,
&breply_ms, NULL)) {
if(breply_ms >= 0 && (reply_ms < 0 || breply_ms < reply_ms))
reply_ms = breply_ms;
}
}
*pres1 = reply_ms;
CURL_TRC_CF(data, cf, "query connect reply: %dms", *pres1);
return CURLE_OK;
}
case CF_QUERY_TIMER_CONNECT: {
struct curltime *when = pres2;
*when = get_max_baller_time(cf, data, CF_QUERY_TIMER_CONNECT);
return CURLE_OK;
}
case CF_QUERY_TIMER_APPCONNECT: {
struct curltime *when = pres2;
*when = get_max_baller_time(cf, data, CF_QUERY_TIMER_APPCONNECT);
return CURLE_OK;
}
default:
break;
}
}
return cf->next ?
cf->next->cft->query(cf->next, data, query, pres1, pres2) :
CURLE_UNKNOWN_OPTION;
}
static void cf_he_destroy(struct Curl_cfilter *cf, struct Curl_easy *data)
{
struct cf_he_ctx *ctx = cf->ctx;
CURL_TRC_CF(data, cf, "destroy");
if(ctx) {
cf_he_ctx_clear(cf, data);
}
/* release any resources held in state */
Curl_safefree(ctx);
}
struct Curl_cftype Curl_cft_happy_eyeballs = {
"HAPPY-EYEBALLS",
0,
CURL_LOG_LVL_NONE,
cf_he_destroy,
cf_he_connect,
cf_he_close,
cf_he_shutdown,
cf_he_adjust_pollset,
cf_he_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
cf_he_query,
};
/**
* Create a happy eyeball connection filter that uses the, once resolved,
* address information to connect on ip families based on connection
* configuration.
* @param pcf output, the created cfilter
* @param data easy handle used in creation
* @param conn connection the filter is created for
* @param cf_create method to create the sub-filters performing the
* actual connects.
*/
static CURLcode
cf_happy_eyeballs_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
cf_ip_connect_create *cf_create,
int transport)
{
struct cf_he_ctx *ctx = NULL;
CURLcode result;
(void)data;
(void)conn;
*pcf = NULL;
ctx = calloc(1, sizeof(*ctx));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
ctx->transport = transport;
ctx->cf_create = cf_create;
result = Curl_cf_create(pcf, &Curl_cft_happy_eyeballs, ctx);
out:
if(result) {
Curl_safefree(*pcf);
free(ctx);
}
return result;
}
struct transport_provider {
int transport;
cf_ip_connect_create *cf_create;
};
static
#ifndef UNITTESTS
const
#endif
struct transport_provider transport_providers[] = {
{ TRNSPRT_TCP, Curl_cf_tcp_create },
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3)
{ TRNSPRT_QUIC, Curl_cf_quic_create },
#endif
#ifndef CURL_DISABLE_TFTP
{ TRNSPRT_UDP, Curl_cf_udp_create },
#endif
#ifdef USE_UNIX_SOCKETS
{ TRNSPRT_UNIX, Curl_cf_unix_create },
#endif
};
static cf_ip_connect_create *get_cf_create(int transport)
{
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(transport_providers); ++i) {
if(transport == transport_providers[i].transport)
return transport_providers[i].cf_create;
}
return NULL;
}
static CURLcode cf_he_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
int transport)
{
cf_ip_connect_create *cf_create;
struct Curl_cfilter *cf;
CURLcode result;
/* Need to be first */
DEBUGASSERT(cf_at);
cf_create = get_cf_create(transport);
if(!cf_create) {
CURL_TRC_CF(data, cf_at, "unsupported transport type %d", transport);
return CURLE_UNSUPPORTED_PROTOCOL;
}
result = cf_happy_eyeballs_create(&cf, data, cf_at->conn,
cf_create, transport);
if(result)
return result;
Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK;
}
typedef enum {
CF_SETUP_INIT,
CF_SETUP_CNNCT_EYEBALLS,
@ -1292,7 +390,7 @@ connect_sub_chain:
}
if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) {
result = cf_he_insert_after(cf, data, ctx->transport);
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
if(result)
return result;
ctx->state = CF_SETUP_CNNCT_EYEBALLS;
@ -1467,21 +565,6 @@ out:
return result;
}
#ifdef UNITTESTS
/* used by unit2600.c */
void Curl_debug_set_transport_provider(int transport,
cf_ip_connect_create *cf_create)
{
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(transport_providers); ++i) {
if(transport == transport_providers[i].transport) {
transport_providers[i].cf_create = cf_create;
return;
}
}
}
#endif /* UNITTESTS */
CURLcode Curl_cf_setup_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
int transport,

View File

@ -107,23 +107,6 @@ void Curl_conncontrol(struct connectdata *conn,
#define connkeep(x,y) Curl_conncontrol(x, CONNCTRL_KEEP)
#endif
/**
* Create a cfilter for making an "ip" connection to the
* given address, using parameters from `conn`. The "ip" connection
* can be a TCP socket, a UDP socket or even a QUIC connection.
*
* It MUST use only the supplied `ai` for its connection attempt.
*
* Such a filter may be used in "happy eyeball" scenarios, and its
* `connect` implementation needs to support non-blocking. Once connected,
* it MAY be installed in the connection filter chain to serve transfers.
*/
typedef CURLcode cf_ip_connect_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
const struct Curl_addrinfo *ai,
int transport);
CURLcode Curl_cf_setup_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
int transport,
@ -140,12 +123,6 @@ CURLcode Curl_conn_setup(struct Curl_easy *data,
struct Curl_dns_entry *dns,
int ssl_mode);
extern struct Curl_cftype Curl_cft_happy_eyeballs;
extern struct Curl_cftype Curl_cft_setup;
#ifdef UNITTESTS
void Curl_debug_set_transport_provider(int transport,
cf_ip_connect_create *cf_create);
#endif
#endif /* HEADER_CURL_CONNECT_H */

View File

@ -41,6 +41,7 @@
#include "cf-h2-proxy.h"
#include "cf-haproxy.h"
#include "cf-https-connect.h"
#include "cf-ip-happy.h"
#include "socks.h"
#include "curlx/strparse.h"
#include "vtls/vtls.h"
@ -462,7 +463,7 @@ static struct trc_cft_def trc_cfts[] = {
{ &Curl_cft_udp, TRC_CT_NETWORK },
{ &Curl_cft_unix, TRC_CT_NETWORK },
{ &Curl_cft_tcp_accept, TRC_CT_NETWORK },
{ &Curl_cft_happy_eyeballs, TRC_CT_NETWORK },
{ &Curl_cft_ip_happy, TRC_CT_NETWORK },
{ &Curl_cft_setup, TRC_CT_PROTOCOL },
#if !defined(CURL_DISABLE_HTTP) && defined(USE_NGHTTP2)
{ &Curl_cft_nghttp2, TRC_CT_PROTOCOL },

View File

@ -43,6 +43,7 @@
#include "urldata.h"
#include "connect.h"
#include "cfilters.h"
#include "cf-ip-happy.h"
#include "multiif.h"
#include "select.h"
#include "curl_trc.h"
@ -362,24 +363,24 @@ static CURLcode test_unit2600(const char *arg)
/* TIMEOUT_MS, FAIL_MS CREATED DURATION Result, HE_PREF */
/* CNCT HE v4 v6 v4 v6 MIN MAX */
{ 1, TURL, "test.com:123:192.0.2.1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 200, 200, 1, 0, 200, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 1, 0, 200, TC_TMOT, R_FAIL, NULL },
/* 1 ipv4, fails after ~200ms, reports COULDNT_CONNECT */
{ 2, TURL, "test.com:123:192.0.2.1,192.0.2.2", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 200, 200, 2, 0, 400, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 2, 0, 400, TC_TMOT, R_FAIL, NULL },
/* 2 ipv4, fails after ~400ms, reports COULDNT_CONNECT */
#ifdef USE_IPV6
{ 3, TURL, "test.com:123:::1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 200, 200, 0, 1, 200, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 0, 1, 200, TC_TMOT, R_FAIL, NULL },
/* 1 ipv6, fails after ~200ms, reports COULDNT_CONNECT */
{ 4, TURL, "test.com:123:::1,::2", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 200, 200, 0, 2, 400, TC_TMOT, R_FAIL, NULL },
CNCT_TMOT, 150, 250, 250, 0, 2, 400, TC_TMOT, R_FAIL, NULL },
/* 2 ipv6, fails after ~400ms, reports COULDNT_CONNECT */
{ 5, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 200, 200, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
CNCT_TMOT, 150, 250, 250, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
/* mixed ip4+6, v6 always first, v4 kicks in on HE, fails after ~350ms */
{ 6, TURL, "test.com:123:::1,192.0.2.1", CURL_IPRESOLVE_WHATEVER,
CNCT_TMOT, 150, 200, 200, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
CNCT_TMOT, 150, 250, 250, 1, 1, 350, TC_TMOT, R_FAIL, "v6" },
/* mixed ip6+4, v6 starts, v4 never starts due to high HE, TIMEOUT */
{ 7, TURL, "test.com:123:192.0.2.1,::1", CURL_IPRESOLVE_V4,
CNCT_TMOT, 150, 500, 500, 1, 0, 400, TC_TMOT, R_FAIL, NULL },