mirror of
https://github.com/curl/curl.git
synced 2026-08-25 22:23:53 +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
|
|
@ -42,6 +42,7 @@
|
|||
#include "sendf.h"
|
||||
#include "select.h"
|
||||
#include "cf-h2-proxy.h"
|
||||
#include "capsule.h"
|
||||
|
||||
#define PROXY_H2_CHUNK_SIZE (16 * 1024)
|
||||
|
||||
|
|
@ -96,6 +97,20 @@ static CURLcode tunnel_stream_init(struct tunnel_stream *ts,
|
|||
return CURLE_OK;
|
||||
}
|
||||
|
||||
static void tunnel_stream_reset(struct tunnel_stream *ts)
|
||||
{
|
||||
Curl_http_resp_free(ts->resp);
|
||||
ts->resp = NULL;
|
||||
Curl_bufq_reset(&ts->recvbuf);
|
||||
Curl_bufq_reset(&ts->sendbuf);
|
||||
ts->stream_id = -1;
|
||||
ts->error = 0;
|
||||
ts->has_final_response = FALSE;
|
||||
ts->closed = FALSE;
|
||||
ts->reset = FALSE;
|
||||
ts->state = H2_TUNNEL_INIT;
|
||||
}
|
||||
|
||||
static void tunnel_stream_clear(struct tunnel_stream *ts)
|
||||
{
|
||||
Curl_http_resp_free(ts->resp);
|
||||
|
|
@ -109,9 +124,11 @@ static void tunnel_stream_clear(struct tunnel_stream *ts)
|
|||
static void h2_tunnel_go_state(struct Curl_cfilter *cf,
|
||||
struct tunnel_stream *ts,
|
||||
h2_tunnel_state new_state,
|
||||
struct Curl_easy *data)
|
||||
struct Curl_easy *data,
|
||||
bool udp_tunnel)
|
||||
{
|
||||
(void)cf;
|
||||
(void)udp_tunnel;
|
||||
|
||||
if(ts->state == new_state)
|
||||
return;
|
||||
|
|
@ -127,7 +144,7 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf,
|
|||
switch(new_state) {
|
||||
case H2_TUNNEL_INIT:
|
||||
CURL_TRC_CF(data, cf, "[%d] new tunnel state 'init'", ts->stream_id);
|
||||
tunnel_stream_clear(ts);
|
||||
tunnel_stream_reset(ts);
|
||||
break;
|
||||
|
||||
case H2_TUNNEL_CONNECT:
|
||||
|
|
@ -143,7 +160,8 @@ static void h2_tunnel_go_state(struct Curl_cfilter *cf,
|
|||
case H2_TUNNEL_ESTABLISHED:
|
||||
CURL_TRC_CF(data, cf, "[%d] new tunnel state 'established'",
|
||||
ts->stream_id);
|
||||
infof(data, "CONNECT phase completed");
|
||||
infof(data, "CONNECT%s phase completed for HTTP/2 proxy",
|
||||
udp_tunnel ? "-UDP" : "");
|
||||
data->state.authproxy.done = TRUE;
|
||||
data->state.authproxy.multipass = FALSE;
|
||||
FALLTHROUGH();
|
||||
|
|
@ -175,6 +193,7 @@ struct cf_h2_proxy_ctx {
|
|||
BIT(rcvd_goaway);
|
||||
BIT(sent_goaway);
|
||||
BIT(nw_out_blocked);
|
||||
BIT(udp_tunnel);
|
||||
};
|
||||
|
||||
/* How to access `call_data` from a cf_h2 filter */
|
||||
|
|
@ -211,7 +230,8 @@ static void drain_tunnel(struct Curl_cfilter *cf,
|
|||
struct cf_h2_proxy_ctx *ctx = cf->ctx;
|
||||
(void)cf;
|
||||
if(!tunnel->closed && !tunnel->reset &&
|
||||
!Curl_bufq_is_empty(&ctx->tunnel.sendbuf))
|
||||
(!Curl_bufq_is_empty(&ctx->tunnel.sendbuf) ||
|
||||
!Curl_bufq_is_empty(&ctx->tunnel.recvbuf)))
|
||||
Curl_multi_mark_dirty(data);
|
||||
}
|
||||
|
||||
|
|
@ -749,15 +769,15 @@ static CURLcode submit_CONNECT(struct Curl_cfilter *cf,
|
|||
CURLcode result;
|
||||
struct httpreq *req = NULL;
|
||||
|
||||
result = Curl_http_proxy_create_CONNECT(&req, cf, data, ctx->dest, 20);
|
||||
result = Curl_http_proxy_create_tunnel_request(&req, cf, data, ctx->dest,
|
||||
PROXY_HTTP_V2,
|
||||
(bool)ctx->udp_tunnel);
|
||||
if(result)
|
||||
goto out;
|
||||
result = Curl_creader_set_null(data);
|
||||
if(result)
|
||||
goto out;
|
||||
|
||||
infof(data, "Establish HTTP/2 proxy tunnel to %s", req->authority);
|
||||
|
||||
result = proxy_h2_submit(&ts->stream_id, cf, data, ctx->h2, req,
|
||||
NULL, ts, tunnel_send_callback, cf);
|
||||
if(result) {
|
||||
|
|
@ -777,41 +797,30 @@ static CURLcode inspect_response(struct Curl_cfilter *cf,
|
|||
struct Curl_easy *data,
|
||||
struct tunnel_stream *ts)
|
||||
{
|
||||
CURLcode result = CURLE_OK;
|
||||
struct dynhds_entry *auth_reply = NULL;
|
||||
(void)cf;
|
||||
struct cf_h2_proxy_ctx *ctx = cf->ctx;
|
||||
proxy_inspect_result res;
|
||||
CURLcode result;
|
||||
|
||||
DEBUGASSERT(ts->resp);
|
||||
if(ts->resp->status / 100 == 2) {
|
||||
infof(data, "CONNECT tunnel established, response %d", ts->resp->status);
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data);
|
||||
return CURLE_OK;
|
||||
result = Curl_http_proxy_inspect_tunnel_response(
|
||||
cf, data, ts->resp, (bool)ctx->udp_tunnel, &res);
|
||||
if(result)
|
||||
return result;
|
||||
switch(res) {
|
||||
case PROXY_INSPECT_OK:
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_ESTABLISHED, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
break;
|
||||
case PROXY_INSPECT_FAILED:
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
result = CURLE_COULDNT_CONNECT;
|
||||
break;
|
||||
case PROXY_INSPECT_AUTH_RETRY:
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_INIT, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
break;
|
||||
}
|
||||
|
||||
if(ts->resp->status == 401) {
|
||||
auth_reply = Curl_dynhds_cget(&ts->resp->headers, "WWW-Authenticate");
|
||||
}
|
||||
else if(ts->resp->status == 407) {
|
||||
auth_reply = Curl_dynhds_cget(&ts->resp->headers, "Proxy-Authenticate");
|
||||
}
|
||||
|
||||
if(auth_reply) {
|
||||
CURL_TRC_CF(data, cf, "[0] CONNECT: fwd auth header '%s'",
|
||||
auth_reply->value);
|
||||
result = Curl_http_input_auth(data, ts->resp->status == 407,
|
||||
auth_reply->value);
|
||||
if(result)
|
||||
return result;
|
||||
if(data->req.newurl) {
|
||||
/* Indicator that we should try again */
|
||||
curlx_safefree(data->req.newurl);
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_INIT, data);
|
||||
return CURLE_OK;
|
||||
}
|
||||
}
|
||||
|
||||
/* Seems to have failed */
|
||||
return CURLE_COULDNT_CONNECT;
|
||||
return result;
|
||||
}
|
||||
|
||||
static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
|
||||
|
|
@ -831,7 +840,8 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
|
|||
result = submit_CONNECT(cf, data, ts);
|
||||
if(result)
|
||||
goto out;
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_CONNECT, data);
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_CONNECT, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
FALLTHROUGH();
|
||||
|
||||
case H2_TUNNEL_CONNECT:
|
||||
|
|
@ -840,12 +850,14 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
|
|||
if(!result)
|
||||
result = proxy_h2_progress_egress(cf, data);
|
||||
if(result && result != CURLE_AGAIN) {
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data);
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
break;
|
||||
}
|
||||
|
||||
if(ts->has_final_response) {
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_RESPONSE, data);
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_RESPONSE, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
}
|
||||
else {
|
||||
result = CURLE_OK;
|
||||
|
|
@ -874,7 +886,8 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf,
|
|||
|
||||
out:
|
||||
if((result && (result != CURLE_AGAIN)) || ctx->tunnel.closed)
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data);
|
||||
h2_tunnel_go_state(cf, ts, H2_TUNNEL_FAILED, data,
|
||||
(bool)ctx->udp_tunnel);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
@ -1231,7 +1244,8 @@ static CURLcode cf_h2_proxy_recv(struct Curl_cfilter *cf,
|
|||
result = Curl_1st_fatal(result, proxy_h2_progress_egress(cf, data));
|
||||
|
||||
out:
|
||||
if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) &&
|
||||
if((!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) ||
|
||||
!Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) &&
|
||||
(!result || (result == CURLE_AGAIN))) {
|
||||
/* data pending and no fatal error to report. Need to trigger
|
||||
* draining to avoid stalling when no socket events happen. */
|
||||
|
|
@ -1297,7 +1311,8 @@ static CURLcode cf_h2_proxy_send(struct Curl_cfilter *cf,
|
|||
}
|
||||
|
||||
out:
|
||||
if(!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) &&
|
||||
if((!Curl_bufq_is_empty(&ctx->tunnel.recvbuf) ||
|
||||
!Curl_bufq_is_empty(&ctx->tunnel.sendbuf)) &&
|
||||
(!result || (result == CURLE_AGAIN))) {
|
||||
/* data pending and no fatal error to report. Need to trigger
|
||||
* draining to avoid stalling when no socket events happen. */
|
||||
|
|
@ -1477,7 +1492,8 @@ struct Curl_cftype Curl_cft_h2_proxy = {
|
|||
|
||||
CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf,
|
||||
struct Curl_easy *data,
|
||||
struct Curl_peer *dest)
|
||||
struct Curl_peer *dest,
|
||||
bool udp_tunnel)
|
||||
{
|
||||
struct Curl_cfilter *cf_h2_proxy = NULL;
|
||||
struct cf_h2_proxy_ctx *ctx;
|
||||
|
|
@ -1488,6 +1504,7 @@ CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf,
|
|||
if(!ctx)
|
||||
goto out;
|
||||
Curl_peer_link(&ctx->dest, dest);
|
||||
ctx->udp_tunnel = udp_tunnel;
|
||||
|
||||
result = Curl_cf_create(&cf_h2_proxy, &Curl_cft_h2_proxy, ctx);
|
||||
if(result)
|
||||
|
|
@ -1501,3 +1518,6 @@ out:
|
|||
}
|
||||
|
||||
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_PROXY && USE_NGHTTP2 */
|
||||
|
||||
/* Do not leak this filter's call_data accessor in unity builds. */
|
||||
#undef CF_CTX_CALL_DATA
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue