HTTP/3: add proxy CONNECT and MASQUE CONNECT-UDP support (ngtcp2 QUIC)

This patch adds two major proxy capabilities to curl (ngtcp2 QUIC):
- HTTP/3 Proxy CONNECT: Tunnel HTTP/1.1 or HTTP/2 traffic through an
  HTTPS proxy that speaks HTTP/3 (QUIC) using the standard CONNECT
  method over an HTTP/3 connection.
- MASQUE CONNECT-UDP: Tunnel HTTP/3 (QUIC) traffic through an HTTP
  proxy (speaking HTTP/1.1, HTTP/2, or HTTP/3) using the extended
  CONNECT method with the CONNECT-UDP protocol (RFC9297 & RFC9298).

Public API additions:
- `CURLPROXY_HTTPS3`: new proxy type constant for HTTP/3 proxy
- `--proxy-http3`: new CLI flag to negotiate HTTP/3 with HTTPS proxy

The implementation adds two new filters:
- `H3-PROXY` - enables negotiating HTTP/3 (QUIC) to the proxy and
  running CONNECT/CONNECT-UDP through that proxy transport.
- `CAPSULE` - dedicated filter inserted between QUIC transport and
  HTTP-PROXY to handle datagram capsule encapsulation/decapsulation.

Here is how the curl filter chaining looks in different scenarios:
- HTTP/3 Proxy CONNECT (tunneling TCP protocols over QUIC proxy):
  conn -> HTTP/1.1 or HTTP/2  -> SSL -> HTTP-PROXY ->
                                 H3-PROXY -> HAPPY-EYEBALLS -> UDP
- MASQUE CONNECT-UDP (tunneling QUIC over any proxy):
  conn -> HTTP/3 -> CAPSULE -> HTTP-PROXY -> H3-PROXY ->
                               HAPPY-EYEBALLS -> UDP
  conn -> HTTP/3 -> CAPSULE -> HTTP-PROXY -> H1-PROXY or H2-PROXY ->
                               SSL -> HAPPY-EYEBALLS -> TCP

- Both features currently require the ngtcp2 QUIC backend.
- Both features are experimental (disabled by default). Enable with
  `--enable-proxy-http3`(autotools) or `-DUSE_PROXY_HTTP3=ON`(CMake).

Tests:
- tests/unit/unit3400.c: Unit tests for capsule protocol encode/decode
- tests/http/test_60_h3_proxy.py: Comprehensive pytest integration suite
- tests/http/testenv/h2o.py: Managing h2o instances with HTTP/1.1, HTTP/2,
  and HTTP/3 (QUIC) listeners, proxy.connect and proxy.connect-udp enabled.

References:
  RFC 9297 - HTTP Datagrams and the Capsule Protocol
  RFC 9298 - Proxying UDP in HTTP
  RFC 9000 §16 — Variable-Length Integer Encoding

Signed-off-by: Aritra Basu <aritrbas+gh@cisco.com>

Closes #21153
This commit is contained in:
Aritra Basu 2026-04-27 19:35:38 -04:00 committed by Daniel Stenberg
parent efc3f2309e
commit e78b1b3ecc
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
66 changed files with 7401 additions and 473 deletions

View file

@ -25,6 +25,8 @@
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h"
#include "curlx/dynbuf.h"
#include "sendf.h"
@ -33,6 +35,7 @@
#include "http_proxy.h"
#include "select.h"
#include "progress.h"
#include "multiif.h"
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "connect.h"
@ -40,7 +43,6 @@
#include "strcase.h"
#include "curlx/strparse.h"
typedef enum {
H1_TUNNEL_INIT, /* init/default/no tunnel state */
H1_TUNNEL_CONNECT, /* CONNECT request is being send */
@ -72,6 +74,12 @@ struct h1_tunnel_state {
BIT(leading_unfold);
};
/* Persistent context for the H1-PROXY filter */
struct cf_h1_proxy_ctx {
struct h1_tunnel_state *ts;
BIT(udp_tunnel);
};
static bool tunnel_is_established(struct h1_tunnel_state *ts)
{
return ts && (ts->tunnel_state == H1_TUNNEL_ESTABLISHED);
@ -82,6 +90,12 @@ static bool tunnel_is_failed(struct h1_tunnel_state *ts)
return ts && (ts->tunnel_state == H1_TUNNEL_FAILED);
}
static bool h1_proxy_is_udp(struct Curl_cfilter *cf)
{
struct cf_h1_proxy_ctx *pctx = cf->ctx;
return (pctx->udp_tunnel ? TRUE : FALSE);
}
static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h1_tunnel_state *ts)
@ -97,6 +111,8 @@ static CURLcode tunnel_reinit(struct Curl_cfilter *cf,
ts->close_connection = FALSE;
ts->maybe_folded = FALSE;
ts->leading_unfold = FALSE;
ts->nsent = 0;
ts->headerlines = 0;
return CURLE_OK;
}
@ -158,7 +174,9 @@ static void h1_tunnel_go_state(struct Curl_cfilter *cf,
case H1_TUNNEL_ESTABLISHED:
CURL_TRC_CF(data, cf, "new tunnel state 'established'");
infof(data, "CONNECT phase completed");
infof(data, "CONNECT%s phase completed for HTTP proxy",
h1_proxy_is_udp(cf) ? "-UDP" : "");
data->state.authproxy.done = TRUE;
data->state.authproxy.multipass = FALSE;
FALLTHROUGH();
@ -195,11 +213,12 @@ static void cf_tunnel_free(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
if(cf) {
struct h1_tunnel_state *ts = cf->ctx;
struct cf_h1_proxy_ctx *pctx = cf->ctx;
struct h1_tunnel_state *ts = pctx ? pctx->ts : NULL;
if(ts) {
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
tunnel_free(ts, data);
cf->ctx = NULL;
pctx->ts = NULL;
}
}
}
@ -217,17 +236,17 @@ static CURLcode start_CONNECT(struct Curl_cfilter *cf,
int http_minor;
CURLcode result;
DEBUGASSERT(data);
/* This only happens if we have looped here due to authentication reasons,
and we do not really use the newly cloned URL here then. Free it. */
curlx_safefree(data->req.newurl);
result = Curl_http_proxy_create_CONNECT(&req, cf, data,
ts->dest, ts->httpversion);
result = Curl_http_proxy_create_tunnel_request(&req, cf, data, ts->dest,
PROXY_HTTP_V1,
h1_proxy_is_udp(cf));
if(result)
goto out;
infof(data, "Establish HTTP proxy tunnel to %s", req->authority);
curlx_dyn_reset(&ts->request_data);
ts->nsent = 0;
ts->headerlines = 0;
@ -280,6 +299,92 @@ out:
return result;
}
static CURLcode on_resp_header_udp(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h1_tunnel_state *ts,
const char *header)
{
CURLcode result = CURLE_OK;
struct SingleRequest *k = &data->req;
if((checkprefix("WWW-Authenticate:", header) && (401 == k->httpcode)) ||
(checkprefix("Proxy-authenticate:", header) && (407 == k->httpcode))) {
bool proxy = (k->httpcode == 407);
char *auth = Curl_copy_header_value(header);
if(!auth)
return CURLE_OUT_OF_MEMORY;
CURL_TRC_CF(data, cf, "CONNECT-UDP: fwd auth header '%s'", header);
result = Curl_http_input_auth(data, proxy, auth);
curlx_free(auth);
if(result)
return result;
}
else if(checkprefix("Content-Length:", header)) {
if(k->httpcode / 100 == 2 || k->httpcode == 101) {
infof(data, "Ignoring Content-Length in CONNECT-UDP %03d response",
k->httpcode);
}
else {
const char *p = header + strlen("Content-Length:");
if(curlx_str_numblanks(&p, &ts->cl)) {
failf(data, "Unsupported Content-Length value");
return CURLE_WEIRD_SERVER_REPLY;
}
}
}
else if(checkprefix("Transfer-Encoding:", header)) {
if(k->httpcode / 100 == 2 || k->httpcode == 101) {
infof(data, "Ignoring Transfer-Encoding in "
"CONNECT-UDP %03d response", k->httpcode);
}
else if(Curl_compareheader(header,
STRCONST("Transfer-Encoding:"),
STRCONST("chunked"))) {
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> "
"Transfer-Encoding: chunked");
ts->chunked_encoding = TRUE;
/* reset our chunky engine */
Curl_httpchunk_reset(data, &ts->ch, TRUE);
}
}
else if(checkprefix("Capsule-protocol:", header)) {
if(Curl_compareheader(header,
STRCONST("Capsule-protocol:"),
STRCONST("?1"))) {
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> Capsule-protocol: ?1");
}
}
else if(Curl_compareheader(header,
STRCONST("Connection:"), STRCONST("close"))) {
ts->close_connection = TRUE;
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> Connection: close");
}
else if(Curl_compareheader(header,
STRCONST("Proxy-Connection:"),
STRCONST("close"))) {
ts->close_connection = TRUE;
CURL_TRC_CF(data, cf,
"CONNECT-UDP Response --> Proxy-Connection: close");
}
else if(!strncmp(header, "HTTP/1.", 7) &&
((header[7] == '0') || (header[7] == '1')) &&
(header[8] == ' ') &&
ISDIGIT(header[9]) && ISDIGIT(header[10]) && ISDIGIT(header[11]) &&
!ISDIGIT(header[12])) {
/* store the HTTP code from the proxy */
data->info.httpproxycode = k->httpcode =
((header[9] - '0') * 100) +
((header[10] - '0') * 10) +
(header[11] - '0');
CURL_TRC_CF(data, cf, "CONNECT-UDP Response --> %d", k->httpcode);
}
return result;
}
static CURLcode on_resp_header(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct h1_tunnel_state *ts,
@ -418,7 +523,13 @@ static CURLcode single_header(struct Curl_cfilter *cf,
return result;
}
result = on_resp_header(cf, data, ts, linep);
if(h1_proxy_is_udp(cf)) {
result = on_resp_header_udp(cf, data, ts, linep);
}
else {
result = on_resp_header(cf, data, ts, linep);
}
if(result)
return result;
@ -460,6 +571,13 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
}
if(!nread) {
if(ts->maybe_folded) {
/* EOF right after LF: finalize the pending header line. */
result = single_header(cf, data, ts);
if(result)
return result;
ts->maybe_folded = FALSE;
}
if(data->set.proxyauth && data->state.authproxy.avail &&
data->req.hd_proxy_auth) {
/* proxy auth was requested and there was proxy auth available,
@ -551,12 +669,16 @@ static CURLcode recv_CONNECT_resp(struct Curl_cfilter *cf,
ts->maybe_folded = TRUE;
}
if(result)
return result;
} /* while there is buffer left and loop is requested */
if(error)
result = CURLE_RECV_ERROR;
*done = (ts->keepon == KEEPON_DONE);
if(!result && *done && data->info.httpproxycode / 100 != 2) {
if(!result && *done &&
data->info.httpproxycode / 100 != 2 &&
!(h1_proxy_is_udp(cf) && data->info.httpproxycode == 101)) {
/* Deal with the possibly already received authenticate
headers. 'newurl' is set to a new URL if we must loop. */
result = Curl_http_auth_act(data);
@ -637,7 +759,7 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
infof(data, "Connect me again please");
Curl_conn_cf_close(cf, data);
result = Curl_conn_cf_connect(cf->next, data, &done);
goto out;
return result;
}
else {
/* staying on this connection, reset state */
@ -653,17 +775,36 @@ static CURLcode H1_CONNECT(struct Curl_cfilter *cf,
} while(data->req.newurl);
DEBUGASSERT(ts->tunnel_state == H1_TUNNEL_RESPONSE);
if(data->info.httpproxycode / 100 != 2) {
/* a non-2xx response and we have no next URL to try. */
curlx_safefree(data->req.newurl);
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode);
return CURLE_COULDNT_CONNECT;
if(h1_proxy_is_udp(cf)) {
/* RFC 9298: Accept 101 Upgrade for HTTP/1.1 and
* 2xx responses for HTTP/2 and HTTP/3 proxies. */
if(data->info.httpproxycode / 100 != 2 &&
data->info.httpproxycode != 101) {
curlx_safefree(data->req.newurl);
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
failf(data, "CONNECT-UDP tunnel failed, response %d",
data->req.httpcode);
return CURLE_COULDNT_CONNECT;
}
}
else {
if(data->info.httpproxycode / 100 != 2) {
/* a non-2xx response and we have no next URL to try. */
curlx_safefree(data->req.newurl);
h1_tunnel_go_state(cf, ts, H1_TUNNEL_FAILED, data);
failf(data, "CONNECT tunnel failed, response %d", data->req.httpcode);
return CURLE_COULDNT_CONNECT;
}
}
/* 2xx response, SUCCESS! */
/* 101 Switching Protocol for CONNECT-UDP */
h1_tunnel_go_state(cf, ts, H1_TUNNEL_ESTABLISHED, data);
infof(data, "CONNECT tunnel established, response %d",
data->info.httpproxycode);
if(h1_proxy_is_udp(cf))
infof(data, "CONNECT-UDP tunnel established, response %d",
data->info.httpproxycode);
else
infof(data, "CONNECT tunnel established, response %d",
data->info.httpproxycode);
result = CURLE_OK;
out:
@ -677,7 +818,8 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
bool *done)
{
CURLcode result;
struct h1_tunnel_state *ts = cf->ctx;
struct cf_h1_proxy_ctx *pctx = cf->ctx;
struct h1_tunnel_state *ts = pctx->ts;
if(cf->connected) {
*done = TRUE;
@ -694,7 +836,7 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
result = tunnel_init(cf, data, &ts);
if(result)
return result;
cf->ctx = ts;
pctx->ts = ts;
}
/* We want "seamless" operations through HTTP proxy tunnel */
@ -705,14 +847,13 @@ static CURLcode cf_h1_proxy_connect(struct Curl_cfilter *cf,
curlx_safefree(data->req.hd_proxy_auth);
out:
*done = (result == CURLE_OK) && tunnel_is_established(cf->ctx);
*done = (result == CURLE_OK) && tunnel_is_established(pctx->ts);
if(*done) {
cf->connected = TRUE;
/* The real request will follow the CONNECT, reset request partially */
Curl_req_soft_reset(&data->req, data);
Curl_client_reset(data);
Curl_pgrsReset(data);
cf_tunnel_free(cf, data);
}
return result;
@ -722,7 +863,8 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct easy_pollset *ps)
{
struct h1_tunnel_state *ts = cf->ctx;
struct cf_h1_proxy_ctx *pctx = cf->ctx;
struct h1_tunnel_state *ts = pctx->ts;
CURLcode result = CURLE_OK;
if(!cf->connected) {
@ -742,37 +884,49 @@ static CURLcode cf_h1_proxy_adjust_pollset(struct Curl_cfilter *cf,
else
result = Curl_pollset_set_out_only(data, ps, sock);
}
else {
if(cf->next)
result = cf->next->cft->adjust_pollset(cf->next, data, ps);
}
return result;
}
static bool cf_h1_proxy_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE;
}
static void cf_h1_proxy_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
CURL_TRC_CF(data, cf, "destroy");
cf_tunnel_free(cf, data);
curlx_safefree(cf->ctx);
}
static void cf_h1_proxy_close(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_h1_proxy_ctx *pctx = cf->ctx;
CURL_TRC_CF(data, cf, "close");
if(cf) {
cf->connected = FALSE;
if(cf->ctx) {
h1_tunnel_go_state(cf, cf->ctx, H1_TUNNEL_INIT, data);
}
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
cf->connected = FALSE;
if(pctx && pctx->ts)
h1_tunnel_go_state(cf, pctx->ts, H1_TUNNEL_INIT, data);
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
static CURLcode cf_h1_proxy_query(struct Curl_cfilter *cf,
struct Curl_easy *data,
int query, int *pres1, void *pres2)
{
struct h1_tunnel_state *ts = cf->ctx;
struct cf_h1_proxy_ctx *pctx = cf->ctx;
struct h1_tunnel_state *ts = pctx ? pctx->ts : NULL;
switch(query) {
case CF_QUERY_HOST_PORT:
if(!ts || !ts->dest)
break;
*pres1 = (int)ts->dest->port;
*((const char **)pres2) = ts->dest->hostname;
return CURLE_OK;
@ -799,7 +953,7 @@ struct Curl_cftype Curl_cft_h1_proxy = {
cf_h1_proxy_close,
Curl_cf_def_shutdown,
cf_h1_proxy_adjust_pollset,
Curl_cf_def_data_pending,
cf_h1_proxy_data_pending,
Curl_cf_def_send,
Curl_cf_def_recv,
Curl_cf_def_cntrl,
@ -811,9 +965,11 @@ struct Curl_cftype Curl_cft_h1_proxy = {
CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
int httpversion)
int httpversion,
bool udp_tunnel)
{
struct Curl_cfilter *cf;
struct cf_h1_proxy_ctx *pctx;
struct h1_tunnel_state *ts;
CURLcode result;
@ -834,9 +990,18 @@ CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
curlx_dyn_init(&ts->request_data, DYN_HTTP_REQUEST);
Curl_httpchunk_init(data, &ts->ch, TRUE);
result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, ts);
if(result)
pctx = curlx_calloc(1, sizeof(*pctx));
if(!pctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
pctx->udp_tunnel = udp_tunnel;
pctx->ts = ts;
result = Curl_cf_create(&cf, &Curl_cft_h1_proxy, pctx);
if(result) {
curlx_free(pctx);
goto out;
}
ts = NULL;
Curl_conn_cf_insert_after(cf_at, cf);