mirror of
https://github.com/curl/curl.git
synced 2026-08-25 13:03:37 +03:00
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:
parent
efc3f2309e
commit
e78b1b3ecc
66 changed files with 7401 additions and 473 deletions
151
lib/connect.c
151
lib/connect.c
|
|
@ -63,6 +63,7 @@
|
|||
#include "curlx/inet_ntop.h"
|
||||
#include "curlx/strparse.h"
|
||||
#include "vtls/vtls.h" /* for vtls cfilters */
|
||||
#include "vquic/vquic.h" /* for QUIC cfilters */
|
||||
#include "progress.h"
|
||||
#include "conncache.h"
|
||||
#include "multihandle.h"
|
||||
|
|
@ -341,6 +342,66 @@ struct cf_setup_ctx {
|
|||
uint8_t transport;
|
||||
};
|
||||
|
||||
#ifndef CURL_DISABLE_PROXY
|
||||
static CURLcode cf_setup_add_http_proxy(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct cf_setup_ctx *ctx)
|
||||
{
|
||||
CURLcode result = CURLE_OK;
|
||||
#ifndef USE_SSL
|
||||
(void)cf;
|
||||
(void)data;
|
||||
(void)ctx;
|
||||
#else
|
||||
/* Skipping the Curl_conn_is_ssl check because SSL is a part of QUIC
|
||||
For CURLPROXY_HTTPS and CURLPROXY_HTTPS2:
|
||||
Curl_cft_setup --> Curl_cft_ssl --> Curl_cft_http_proxy --> ...
|
||||
For CURLPROXY_HTTPS3:
|
||||
Curl_cft_setup --> Curl_cft_http3 --> Curl_cft_http_proxy --> ... */
|
||||
if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy) {
|
||||
if(!IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) {
|
||||
result = Curl_cf_ssl_proxy_insert_after(cf, data);
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
else {
|
||||
if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype)
|
||||
&& !Curl_conn_is_ssl(cf->conn, cf->sockindex)
|
||||
&& !IS_QUIC_PROXY(cf->conn->http_proxy.proxytype)) {
|
||||
result = Curl_cf_ssl_proxy_insert_after(cf, data);
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
}
|
||||
#endif /* USE_SSL */
|
||||
|
||||
#ifndef CURL_DISABLE_HTTP
|
||||
if(cf->conn->bits.tunnel_proxy) {
|
||||
struct Curl_peer *dest; /* where HTTP should tunnel to */
|
||||
bool udp_tun = false;
|
||||
dest = Curl_conn_get_destination(cf->conn, cf->sockindex);
|
||||
/* Use CONNECT-UDP only for explicit HTTP/3-only target tunnels.
|
||||
Do not derive this from proxy transport (for example HTTPS3 proxy). */
|
||||
if(data->state.http_neg.wanted == CURL_HTTP_V3x) {
|
||||
#ifdef USE_PROXY_HTTP3
|
||||
udp_tun = TRUE;
|
||||
#else
|
||||
failf(data, "HTTP/3 proxy tunnel support not built-in");
|
||||
return CURLE_NOT_BUILT_IN;
|
||||
#endif /* USE_PROXY_HTTP3 */
|
||||
}
|
||||
result = Curl_cf_http_proxy_insert_after(cf, data, dest,
|
||||
cf->conn->http_proxy.proxytype,
|
||||
udp_tun);
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
#endif /* !CURL_DISABLE_HTTP */
|
||||
return result;
|
||||
}
|
||||
#endif /* !CURL_DISABLE_PROXY */
|
||||
|
||||
static CURLcode cf_setup_connect(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
bool *done)
|
||||
|
|
@ -364,7 +425,35 @@ connect_sub_chain:
|
|||
}
|
||||
|
||||
if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) {
|
||||
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
|
||||
#ifndef CURL_DISABLE_PROXY
|
||||
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
|
||||
defined(USE_PROXY_HTTP3)
|
||||
if(IS_QUIC_PROXY(cf->conn->http_proxy.proxytype) &&
|
||||
cf->conn->bits.tunnel_proxy) {
|
||||
/* For HTTPS3 proxy tunnels, H3-PROXY manages the QUIC connection
|
||||
on top of the UDP socket. Let happy eyeballs race IPv4/IPv6 using
|
||||
QUIC-transport UDP sockets so the socket is connected to the
|
||||
proxy peer and H3-PROXY can send directly via send().
|
||||
Filter chains:
|
||||
H1/H2 target (CONNECT over QUIC):
|
||||
SETUP --> HTTP/1.1 or HTTP/2 --> SSL --> HTTP-PROXY -->
|
||||
H3-PROXY --> HAPPY-EYEBALLS --> UDP
|
||||
H3 target (MASQUE CONNECT-UDP over QUIC):
|
||||
SETUP --> HTTP/3 --> CAPSULE --> HTTP-PROXY -->
|
||||
H3-PROXY --> HAPPY-EYEBALLS --> UDP */
|
||||
result = cf_ip_happy_quic_udp_insert_after(cf, data);
|
||||
}
|
||||
/* When tunneling QUIC through an HTTP proxy (CONNECT-UDP),
|
||||
the underlying conn to the proxy is TCP. */
|
||||
else
|
||||
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
|
||||
if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy
|
||||
&& !IS_QUIC_PROXY(cf->conn->http_proxy.proxytype))
|
||||
result = cf_ip_happy_insert_after(cf, data, TRNSPRT_TCP);
|
||||
else
|
||||
#endif /* !CURL_DISABLE_PROXY */
|
||||
result = cf_ip_happy_insert_after(cf, data, ctx->transport);
|
||||
|
||||
if(result)
|
||||
return result;
|
||||
ctx->state = CF_SETUP_CNNCT_EYEBALLS;
|
||||
|
|
@ -402,25 +491,9 @@ connect_sub_chain:
|
|||
}
|
||||
|
||||
if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) {
|
||||
#ifdef USE_SSL
|
||||
if(IS_HTTPS_PROXY(cf->conn->http_proxy.proxytype) &&
|
||||
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) {
|
||||
result = Curl_cf_ssl_proxy_insert_after(cf, data);
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
#endif /* USE_SSL */
|
||||
|
||||
#ifndef CURL_DISABLE_HTTP
|
||||
if(cf->conn->bits.tunnel_proxy) {
|
||||
struct Curl_peer *dest; /* where HTTP should tunnel to */
|
||||
dest = Curl_conn_get_destination(cf->conn, cf->sockindex);
|
||||
result = Curl_cf_http_proxy_insert_after(
|
||||
cf, data, dest, cf->conn->http_proxy.proxytype);
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
#endif /* !CURL_DISABLE_HTTP */
|
||||
result = cf_setup_add_http_proxy(cf, data, ctx);
|
||||
if(result)
|
||||
return result;
|
||||
ctx->state = CF_SETUP_CNNCT_HTTP_PROXY;
|
||||
if(!cf->next || !cf->next->connected)
|
||||
goto connect_sub_chain;
|
||||
|
|
@ -445,21 +518,41 @@ connect_sub_chain:
|
|||
goto connect_sub_chain;
|
||||
}
|
||||
|
||||
if(ctx->state < CF_SETUP_CNNCT_SSL) {
|
||||
#ifdef USE_SSL
|
||||
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE ||
|
||||
(ctx->ssl_mode != CURL_CF_SSL_DISABLE &&
|
||||
cf->conn->scheme->flags & PROTOPT_SSL)) && /* we want SSL */
|
||||
!Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */
|
||||
result = Curl_cf_ssl_insert_after(cf, data);
|
||||
/* Adding Curl_cf_quic_insert_after() because now we
|
||||
need the next filter to be QUIC/HTTP/3 (which has SSL) */
|
||||
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
|
||||
defined(USE_PROXY_HTTP3)
|
||||
if(ctx->transport == TRNSPRT_QUIC && cf->conn->bits.httpproxy &&
|
||||
cf->conn->bits.tunnel_proxy &&
|
||||
(data->state.http_neg.wanted == CURL_HTTP_V3x)) {
|
||||
if(ctx->state < CF_SETUP_CNNCT_SSL) {
|
||||
result = Curl_cf_quic_insert_after(cf);
|
||||
if(result)
|
||||
return result;
|
||||
ctx->state = CF_SETUP_CNNCT_SSL;
|
||||
}
|
||||
#endif /* USE_SSL */
|
||||
ctx->state = CF_SETUP_CNNCT_SSL;
|
||||
if(!cf->next || !cf->next->connected)
|
||||
goto connect_sub_chain;
|
||||
}
|
||||
else
|
||||
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
|
||||
{
|
||||
if(ctx->state < CF_SETUP_CNNCT_SSL) {
|
||||
#ifdef USE_SSL
|
||||
if((ctx->ssl_mode == CURL_CF_SSL_ENABLE ||
|
||||
(ctx->ssl_mode != CURL_CF_SSL_DISABLE &&
|
||||
cf->conn->scheme->flags & PROTOPT_SSL)) /* we want SSL */
|
||||
&& !Curl_conn_is_ssl(cf->conn, cf->sockindex)) { /* it is missing */
|
||||
result = Curl_cf_ssl_insert_after(cf, data);
|
||||
if(result)
|
||||
return result;
|
||||
}
|
||||
#endif /* USE_SSL */
|
||||
ctx->state = CF_SETUP_CNNCT_SSL;
|
||||
if(!cf->next || !cf->next->connected)
|
||||
goto connect_sub_chain;
|
||||
}
|
||||
}
|
||||
|
||||
ctx->state = CF_SETUP_DONE;
|
||||
cf->connected = TRUE;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue