diff --git a/docs/libcurl/opts/CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.md b/docs/libcurl/opts/CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.md index 91c2c50708..e38a7c3be5 100644 --- a/docs/libcurl/opts/CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.md +++ b/docs/libcurl/opts/CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.md @@ -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) diff --git a/lib/Makefile.inc b/lib/Makefile.inc index a22b1e1b00..524fdcc53d 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -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 \ diff --git a/lib/cf-ip-happy.c b/lib/cf-ip-happy.c new file mode 100644 index 0000000000..6dbb5c5fdc --- /dev/null +++ b/lib/cf-ip-happy.c @@ -0,0 +1,945 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , 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 /* may need it */ +#endif +#ifdef HAVE_SYS_UN_H +#include /* for sockaddr_un */ +#endif +#ifdef HAVE_LINUX_TCP_H +#include +#elif defined(HAVE_NETINET_TCP_H) +#include +#endif +#ifdef HAVE_SYS_IOCTL_H +#include +#endif +#ifdef HAVE_NETDB_H +#include +#endif +#ifdef HAVE_FCNTL_H +#include +#endif +#ifdef HAVE_ARPA_INET_H +#include +#endif + +#ifdef __VMS +#include +#include +#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; +} diff --git a/lib/cf-ip-happy.h b/lib/cf-ip-happy.h new file mode 100644 index 0000000000..96e619ae43 --- /dev/null +++ b/lib/cf-ip-happy.h @@ -0,0 +1,59 @@ +#ifndef HEADER_CURL_IP_HAPPY_H +#define HEADER_CURL_IP_HAPPY_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , 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 */ diff --git a/lib/connect.c b/lib/connect.c index 7c36ccd40e..f0628d6206 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -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, diff --git a/lib/connect.h b/lib/connect.h index 120338eb99..6a2487ff53 100644 --- a/lib/connect.h +++ b/lib/connect.h @@ -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 */ diff --git a/lib/curl_trc.c b/lib/curl_trc.c index 7e6c48358f..8c01eff142 100644 --- a/lib/curl_trc.c +++ b/lib/curl_trc.c @@ -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 }, diff --git a/tests/unit/unit2600.c b/tests/unit/unit2600.c index b5818dd77f..f6f909bb1e 100644 --- a/tests/unit/unit2600.c +++ b/tests/unit/unit2600.c @@ -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 },