alt-svc: more flexibility on same destination

When the Alt-Svc points to the same host and port, add the destination
ALPN to the `wanted` versions and set it also as the `preferred` version
in negotiations.

This allows Alt-Svc for h3 to point to h2 and have it tried first. Also,
this allows Alt-Svc to say http/1.1 is preferred and changes the ALPN
protocol ordering for the TLS handshake.

Add tests in various combination to verify this works.

Reported-by: yushicheng7788 on github
Fixes #19740
Closes #19874
This commit is contained in:
Stefan Eissing 2025-12-08 13:36:19 +01:00 committed by Daniel Stenberg
parent f450f3801b
commit 5ed7b5b01b
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
9 changed files with 135 additions and 49 deletions

View file

@ -622,7 +622,8 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi,
enum alpnid srcalpnid, const char *srchost,
int srcport,
struct altsvc **dstentry,
const int versions) /* one or more bits */
const int versions, /* one or more bits */
bool *psame_destination)
{
struct Curl_llist_node *e;
struct Curl_llist_node *n;
@ -631,6 +632,7 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi,
DEBUGASSERT(srchost);
DEBUGASSERT(dstentry);
*psame_destination = FALSE;
for(e = Curl_llist_head(&asi->list); e; e = n) {
struct altsvc *as = Curl_node_elem(e);
n = Curl_node_next(e);
@ -646,6 +648,8 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi,
(versions & (int)as->dst.alpnid)) {
/* match */
*dstentry = as;
*psame_destination = (srcport == as->dst.port) &&
hostcompare(srchost, as->dst.host);
return TRUE;
}
}

View file

@ -65,7 +65,8 @@ bool Curl_altsvc_lookup(struct altsvcinfo *asi,
enum alpnid srcalpnid, const char *srchost,
int srcport,
struct altsvc **dstentry,
const int versions); /* CURLALTSVC_H* bits */
const int versions, /* CURLALTSVC_H* bits */
bool *psame_destination);
#else
/* disabled */
#define Curl_altsvc_save(a, b, c)

View file

@ -710,6 +710,31 @@ CURLcode Curl_cf_https_setup(struct Curl_easy *data,
}
#endif
/* Add preferred HTTP version ALPN first */
if(data->state.http_neg.preferred &&
(alpn_count < CURL_ARRAYSIZE(alpn_ids)) &&
(data->state.http_neg.preferred & data->state.http_neg.allowed)) {
enum alpnid alpn_pref = ALPN_none;
switch(data->state.http_neg.preferred) {
case CURL_HTTP_V3x:
if(!Curl_conn_may_http3(data, conn, conn->transport_wanted))
alpn_pref = ALPN_h3;
break;
case CURL_HTTP_V2x:
alpn_pref = ALPN_h2;
break;
case CURL_HTTP_V1x:
alpn_pref = ALPN_h1;
break;
default:
break;
}
if(alpn_pref &&
!cf_https_alpns_contain(alpn_pref, alpn_ids, alpn_count)) {
alpn_ids[alpn_count++] = alpn_pref;
}
}
if((alpn_count < CURL_ARRAYSIZE(alpn_ids)) &&
(data->state.http_neg.wanted & CURL_HTTP_V3x) &&
!cf_https_alpns_contain(ALPN_h3, alpn_ids, alpn_count)) {

View file

@ -72,6 +72,7 @@ struct http_negotiation {
unsigned char rcvd_min; /* minimum version seen in responses, 09, 10, 11 */
http_majors wanted; /* wanted major versions when talking to server */
http_majors allowed; /* allowed major versions when talking to server */
http_majors preferred; /* preferred major version when talking to server */
BIT(h2_upgrade); /* Do HTTP Upgrade from 1.1 to 2 */
BIT(h2_prior_knowledge); /* Directly do HTTP/2 without ALPN/SSL */
BIT(accept_09); /* Accept an HTTP/0.9 response */

View file

@ -3060,6 +3060,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
struct altsvc *as = NULL;
int allowed_alpns = ALPN_none;
struct http_negotiation *neg = &data->state.http_neg;
bool same_dest = FALSE;
DEBUGF(infof(data, "Alt-svc check wanted=%x, allowed=%x",
neg->wanted, neg->allowed));
@ -3083,7 +3084,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
hit = Curl_altsvc_lookup(data->asi,
ALPN_h3, host, conn->remote_port, /* from */
&as /* to */,
allowed_alpns);
allowed_alpns, &same_dest);
}
#endif
#ifdef USE_HTTP2
@ -3093,7 +3094,7 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
hit = Curl_altsvc_lookup(data->asi,
ALPN_h2, host, conn->remote_port, /* from */
&as /* to */,
allowed_alpns);
allowed_alpns, &same_dest);
}
#endif
if(!hit && (neg->wanted & CURL_HTTP_V1x) &&
@ -3102,10 +3103,29 @@ static CURLcode parse_connect_to_slist(struct Curl_easy *data,
hit = Curl_altsvc_lookup(data->asi,
ALPN_h1, host, conn->remote_port, /* from */
&as /* to */,
allowed_alpns);
allowed_alpns, &same_dest);
}
if(hit) {
if(hit && same_dest) {
/* same destination, but more HTTPS version options */
switch(as->dst.alpnid) {
case ALPN_h1:
neg->wanted |= CURL_HTTP_V1x;
neg->preferred = CURL_HTTP_V1x;
break;
case ALPN_h2:
neg->wanted |= CURL_HTTP_V2x;
neg->preferred = CURL_HTTP_V2x;
break;
case ALPN_h3:
neg->wanted |= CURL_HTTP_V3x;
neg->preferred = CURL_HTTP_V3x;
break;
default: /* should not be possible */
break;
}
}
else if(hit) {
char *hostd = curlx_strdup((char *)as->dst.host);
if(!hostd)
return CURLE_OUT_OF_MEMORY;

View file

@ -145,22 +145,28 @@ static const struct alpn_spec ALPN_SPEC_H2 = {
static const struct alpn_spec ALPN_SPEC_H2_H11 = {
{ ALPN_H2, ALPN_HTTP_1_1 }, 2
};
static const struct alpn_spec ALPN_SPEC_H11_H2 = {
{ ALPN_HTTP_1_1, ALPN_H2 }, 2
};
#endif
#if !defined(CURL_DISABLE_HTTP) || !defined(CURL_DISABLE_PROXY)
static const struct alpn_spec *alpn_get_spec(http_majors allowed,
static const struct alpn_spec *alpn_get_spec(http_majors wanted,
http_majors preferred,
bool use_alpn)
{
if(!use_alpn)
return NULL;
#ifdef USE_HTTP2
if(allowed & CURL_HTTP_V2x) {
if(allowed & CURL_HTTP_V1x)
return &ALPN_SPEC_H2_H11;
if(wanted & CURL_HTTP_V2x) {
if(wanted & CURL_HTTP_V1x)
return (preferred == CURL_HTTP_V1x) ?
&ALPN_SPEC_H11_H2 : &ALPN_SPEC_H2_H11;
return &ALPN_SPEC_H2;
}
#else
(void)allowed;
(void)wanted;
(void)preferred;
#endif
/* Use the ALPN protocol "http/1.1" for HTTP/1.x.
Avoid "http/1.0" because some servers do not support it. */
@ -1718,6 +1724,7 @@ static CURLcode cf_ssl_create(struct Curl_cfilter **pcf,
ctx = cf_ctx_new(data, NULL);
#else
ctx = cf_ctx_new(data, alpn_get_spec(data->state.http_neg.wanted,
data->state.http_neg.preferred,
conn->bits.tls_enable_alpn));
#endif
if(!ctx) {
@ -1770,17 +1777,17 @@ static CURLcode cf_ssl_proxy_create(struct Curl_cfilter **pcf,
CURLcode result;
/* ALPN is default, but if user explicitly disables it, obey */
bool use_alpn = data->set.ssl_enable_alpn;
http_majors allowed = CURL_HTTP_V1x;
http_majors wanted = CURL_HTTP_V1x;
(void)conn;
#ifdef USE_HTTP2
if(conn->http_proxy.proxytype == CURLPROXY_HTTPS2) {
use_alpn = TRUE;
allowed = (CURL_HTTP_V1x | CURL_HTTP_V2x);
wanted = (CURL_HTTP_V1x | CURL_HTTP_V2x);
}
#endif
ctx = cf_ctx_new(data, alpn_get_spec(allowed, use_alpn));
ctx = cf_ctx_new(data, alpn_get_spec(wanted, 0, use_alpn));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;