diff --git a/docs/libcurl/opts/CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.md b/docs/libcurl/opts/CURLOPT_HAPPY_EYEBALLS_TIMEOUT_MS.md index 91c2c50708..cc37fc1e85 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. And 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 will open a socket to 'a3_v6' first. When that does not report back, +it will open 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 will 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 atempt. + +The HTTP/3 attempt is started first and, after *timeout* expired, the +HTTP/2 (or 1.1) attempt is started in parallel. + # DEFAULT CURL_HET_DEFAULT (currently defined as 200L) diff --git a/lib/cf-ip-happy.c b/lib/cf-ip-happy.c index 2aaf54880e..a98c35ab88 100644 --- a/lib/cf-ip-happy.c +++ b/lib/cf-ip-happy.c @@ -271,15 +271,18 @@ static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a, { 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; - a->result = a->cf_create(&a->cf, data, cf->conn, a->addr, a->transport); - if(!a->result) { + 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) { @@ -290,7 +293,7 @@ static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a, } if(cf_prev) Curl_conn_cf_discard_chain(&cf_prev, data); - return a->result; + return result; } static void cf_ip_ballers_clear(struct Curl_cfilter *cf, @@ -348,24 +351,28 @@ static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, int ip_version, static CURLcode cf_ip_ballers_run(struct cf_ip_ballers *bs, struct Curl_cfilter *cf, struct Curl_easy *data, - struct curltime now, bool *connected) { CURLcode result = CURLE_OK; struct cf_ip_attempt *a = NULL, **panchor; - bool do_next = FALSE, out_of_addresses = FALSE; + bool do_more, more_possible; + struct curltime now; timediff_t next_expire_ms; - int i, ongoing, inconclusive; + int i, inconclusive, ongoing; if(bs->winner) return CURLE_OK; evaluate: - inconclusive = ongoing = i = 0; + now = curlx_now(); + ongoing = inconclusive = 0; + do_more = FALSE; + more_possible = TRUE; + /* check if a running baller connects now */ - if(bs->running) - CURL_TRC_CF(data, cf, "checking ip connect attempts"); + 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) { @@ -383,39 +390,31 @@ evaluate: } return CURLE_OK; } - /* still waiting */ + /* still running */ ++ongoing; } - else if(a->inconclusive) + else if(a->inconclusive) /* failed, but inconclusive */ ++inconclusive; - ++i; } + if(bs->running) + CURL_TRC_CF(data, cf, "checked connect attempts: " + "%d ongoing, %d inconclusive", ongoing, inconclusive); - /* no baller connected yet, start another one? */ - if(!bs->started.tv_sec && !bs->started.tv_usec) { - bs->started = now; - do_next = TRUE; + /* 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_next = (curlx_timediff(now, bs->last_attempt_started) >= + do_more = (curlx_timediff(now, bs->last_attempt_started) >= bs->attempt_delay_ms); - if(do_next) { + if(do_more) CURL_TRC_CF(data, cf, "happy eyeballs timeout expired, " "start next attempt"); - } - else { /* delay does not count when all running failed */ - for(a = bs->running; a; a = a->next) { - if(!a->result) /* still running */ - break; - } - if(!a) { /* exhausted list, no more running */ - CURL_TRC_CF(data, cf, "all previous attempts failed"); - do_next = TRUE; - } - } } - if(do_next) { + 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; @@ -431,82 +430,85 @@ evaluate: addr = cf_ai_iter_next(&bs->addr_iter); ai_family = bs->addr_iter.ai_family; } - if(!addr) { - CURL_TRC_CF(data, cf, "no more addresses to try"); - out_of_addresses = TRUE; + + 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; } - - 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(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(!ongoing && out_of_addresses && inconclusive) { - /* tried all addresses, no success but some where inconclusive. - * Let's restart the inconclusive ones. */ - int restarted = 0; - CURL_TRC_CF(data, cf, "tried all addresses, some with " - "inconclusive results, restarting those"); - 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) - goto out; - ++restarted; + 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(restarted) - goto evaluate; - } - if(!ongoing && out_of_addresses) { - /* all attempts failed and we have no more addresses */ - CURL_TRC_CF(data, cf, "all ballers failed"); - 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; + 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; } - return result; + Curl_expire(data, next_expire_ms, EXPIRE_HAPPY_EYEBALLS); } - - /* when do we need to be called again? */ - next_expire_ms = Curl_timeleft(data, &now, TRUE); - if(!out_of_addresses) { - 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; } @@ -615,10 +617,8 @@ static CURLcode is_connected(struct Curl_cfilter *cf, struct cf_ip_happy_ctx *ctx = cf->ctx; struct connectdata *conn = cf->conn; CURLcode result; - struct curltime now; - now = curlx_now(); - result = cf_ip_ballers_run(&ctx->ballers, cf, data, now, connected); + result = cf_ip_ballers_run(&ctx->ballers, cf, data, connected); if(!result) return CURLE_OK; @@ -648,7 +648,7 @@ static CURLcode is_connected(struct Curl_cfilter *cf, proxy_name ? "via " : "", proxy_name ? proxy_name : "", proxy_name ? " " : "", - curlx_timediff(now, data->progress.t_startsingle), + curlx_timediff(curlx_now(), data->progress.t_startsingle), curl_easy_strerror(result)); }