# NAME
-CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS - head start for IPv6 for happy eyeballs
+CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS - timing of connect attempts
# SYNOPSIS
# 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)
cf-h2-proxy.c \
cf-haproxy.c \
cf-https-connect.c \
+ cf-ip-happy.c \
cf-socket.c \
cfilters.c \
conncache.c \
cf-h2-proxy.h \
cf-haproxy.h \
cf-https-connect.h \
+ cf-ip-happy.h \
cf-socket.h \
cfilters.h \
conncache.h \
--- /dev/null
+/***************************************************************************
+ * _ _ ____ _
+ * 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;
+}
--- /dev/null
+#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 */
#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() */
#include "conncache.h"
#include "multihandle.h"
#include "share.h"
-#include "vquic/vquic.h" /* for quic cfilters */
#include "http_proxy.h"
#include "socks.h"
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,
}
}
-/**
- * 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,
}
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;
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,
#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,
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 */
#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"
{ &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 },
#include "urldata.h"
#include "connect.h"
#include "cfilters.h"
+#include "cf-ip-happy.h"
#include "multiif.h"
#include "select.h"
#include "curl_trc.h"
/* 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 },