From e4139a73c82d2035142f5ae36196adb4e9831dae Mon Sep 17 00:00:00 2001 From: Stefan Eissing Date: Wed, 27 May 2026 16:50:18 +0200 Subject: [PATCH] h3-proxy: fixes around H3 proxy code: - less exception handling in existing code - true ip happy eyeballing - enable certificate verification - cf-h2-proxy: abort connection when server closed connection tests: - remove all --insecure and --proxy-insecure args - make session reuse test_60_12 a working one - resolve port conflicts between h2o and nghttpx - use proxy args better - make test_60_06 run shorter - kill h2o at the end of tests, normal stop takes too long Ref: 59213f8248cfc10e97a6a23f5e4da9b1e5057400 #21789 Follow-up to e78b1b3eccfa6a2e367a1225ea1b66dafcdac3c4 #21153 Closes #21798 --- lib/cf-capsule.c | 36 ++++-- lib/cf-capsule.h | 4 + lib/cf-h2-proxy.c | 6 + lib/cf-h3-proxy.c | 62 +++++++++ lib/cf-h3-proxy.h | 7 + lib/cf-ip-happy.c | 116 +++++++++-------- lib/cf-ip-happy.h | 9 +- lib/cf-socket.c | 24 ++-- lib/cf-socket.h | 9 +- lib/cfilters.c | 4 +- lib/connect.c | 145 ++++++++------------- lib/http_proxy.c | 16 ++- lib/http_proxy.h | 6 +- lib/url.c | 7 +- lib/vquic/curl_ngtcp2.c | 3 +- lib/vquic/curl_quiche.c | 3 +- lib/vquic/vquic.c | 8 +- lib/vquic/vquic.h | 3 +- tests/http/conftest.py | 4 +- tests/http/test_60_h3_proxy.py | 229 +++++++++++++++------------------ tests/http/testenv/curl.py | 5 +- tests/http/testenv/env.py | 11 +- tests/http/testenv/h2o.py | 18 ++- tests/http/testenv/nghttpx.py | 63 ++++----- tests/unit/unit2600.c | 9 +- 25 files changed, 442 insertions(+), 365 deletions(-) diff --git a/lib/cf-capsule.c b/lib/cf-capsule.c index 55a550954c..333a8d7efe 100644 --- a/lib/cf-capsule.c +++ b/lib/cf-capsule.c @@ -224,29 +224,47 @@ struct Curl_cftype Curl_cft_capsule = { Curl_cf_def_query, }; -CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at, - struct Curl_easy *data) +CURLcode Curl_cf_capsule_create(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn) { - struct Curl_cfilter *cf; + struct Curl_cfilter *cf = NULL; struct cf_capsule_ctx *ctx; CURLcode result; (void)data; + (void)conn; + *pcf = NULL; ctx = curlx_calloc(1, sizeof(*ctx)); - if(!ctx) - return CURLE_OUT_OF_MEMORY; + if(!ctx) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } Curl_bufq_init2(&ctx->recvbuf, CAPSULE_CHUNK_SIZE, CAPSULE_RECV_CHUNKS, BUFQ_OPT_SOFT_LIMIT); result = Curl_cf_create(&cf, &Curl_cft_capsule, ctx); - if(result) { + +out: + *pcf = (!result) ? cf : NULL; + if(result && ctx) { Curl_bufq_free(&ctx->recvbuf); curlx_free(ctx); - return result; } - Curl_conn_cf_insert_after(cf_at, cf); - return CURLE_OK; + return result; +} + +CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at, + struct Curl_easy *data) +{ + struct Curl_cfilter *cf; + CURLcode result; + + result = Curl_cf_capsule_create(&cf, data, cf_at->conn); + if(!result) + Curl_conn_cf_insert_after(cf_at, cf); + return result; } #endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ diff --git a/lib/cf-capsule.h b/lib/cf-capsule.h index 437c9681b6..e45983543a 100644 --- a/lib/cf-capsule.h +++ b/lib/cf-capsule.h @@ -27,6 +27,10 @@ #if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) +CURLcode Curl_cf_capsule_create(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn); + /* Insert a capsule protocol filter after `cf_at` in the filter chain. * The capsule filter encapsulates/decapsulates UDP datagrams using * the HTTP Datagram capsule format (RFC 9297). */ diff --git a/lib/cf-h2-proxy.c b/lib/cf-h2-proxy.c index 5eaa9571e6..316ed6c75a 100644 --- a/lib/cf-h2-proxy.c +++ b/lib/cf-h2-proxy.c @@ -381,6 +381,7 @@ static CURLcode proxy_h2_progress_ingress(struct Curl_cfilter *cf, break; } else if(nread == 0) { + CURL_TRC_CF(data, cf, "server closed connection"); ctx->conn_closed = TRUE; break; } @@ -832,6 +833,11 @@ static CURLcode H2_CONNECT(struct Curl_cfilter *cf, DEBUGASSERT(ts); DEBUGASSERT(ts->authority); + if(ctx->conn_closed) { + failf(data, "proxy closed connection"); + return CURLE_COULDNT_CONNECT; + } + do { switch(ts->state) { case H2_TUNNEL_INIT: diff --git a/lib/cf-h3-proxy.c b/lib/cf-h3-proxy.c index b370603493..5ca18ace83 100644 --- a/lib/cf-h3-proxy.c +++ b/lib/cf-h3-proxy.c @@ -53,6 +53,7 @@ #include "sendf.h" #include "multiif.h" #include "cfilters.h" +#include "cf-capsule.h" #include "cf-socket.h" #include "connect.h" #include "progress.h" @@ -3057,6 +3058,10 @@ static CURLcode cf_h3_proxy_quic_connect(struct Curl_cfilter *cf, } *done = FALSE; + if(!proxy_ctx->dest) { + Curl_peer_link(&proxy_ctx->dest, + Curl_conn_get_destination(cf->conn, cf->sockindex)); + } if(!proxy_ctx->ngtcp2_ctx) { result = cf_h3_proxy_ctx_init(cf, data); @@ -3414,6 +3419,63 @@ struct Curl_cftype Curl_cft_h3_proxy = { cf_h3_proxy_query, }; +CURLcode Curl_cf_h3_proxy_create(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn, + struct Curl_sockaddr_ex *addr, + uint8_t transport_in, + uint8_t transport_out) +{ + struct Curl_cfilter *cf = NULL; + struct cf_h3_proxy_ctx *ctx; + CURLcode result = CURLE_OUT_OF_MEMORY; + + if((transport_out != TRNSPRT_QUIC) || (!conn->http_proxy.peer)) + return CURLE_FAILED_INIT; + + ctx = curlx_calloc(1, sizeof(*ctx)); + if(!ctx) { + result = CURLE_OUT_OF_MEMORY; + goto out; + } + ctx->udp_tunnel = (transport_in == TRNSPRT_QUIC); + + result = Curl_cf_create(&cf, &Curl_cft_h3_proxy, ctx); + if(result) + goto out; + cf->conn = conn; + + result = Curl_cf_udp_create(&cf->next, data, conn, addr, + TRNSPRT_QUIC, TRNSPRT_QUIC); + if(result) + goto out; + cf->next->conn = cf->conn; + cf->next->sockindex = cf->sockindex; + + if(ctx->udp_tunnel) { + struct Curl_cfilter *cf_caps = NULL; + result = Curl_cf_capsule_create(&cf_caps, data, conn); + if(result) + goto out; + cf_caps->conn = conn; + cf_caps->sockindex = cf->sockindex; + cf_caps->next = cf; + cf = cf_caps; + } + +out: + *pcf = (!result) ? cf : NULL; + if(result) { + if(cf) + Curl_conn_cf_discard_chain(&cf, data); + else if(ctx) + cf_h3_proxy_ctx_free(ctx); + } + else + CURL_TRC_CF(data, cf, "created, udp_tunnel=%d", ctx->udp_tunnel); + return result; +} + CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, diff --git a/lib/cf-h3-proxy.h b/lib/cf-h3-proxy.h index b2f16acc0e..40f0fccf06 100644 --- a/lib/cf-h3-proxy.h +++ b/lib/cf-h3-proxy.h @@ -35,6 +35,13 @@ CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_peer *dest, bool udp_tunnel); +CURLcode Curl_cf_h3_proxy_create(struct Curl_cfilter **pcf, + struct Curl_easy *data, + struct connectdata *conn, + struct Curl_sockaddr_ex *addr, + uint8_t transport_in, + uint8_t transport_out); + extern struct Curl_cftype Curl_cft_h3_proxy; #endif diff --git a/lib/cf-ip-happy.c b/lib/cf-ip-happy.c index 965415d458..cfada937c8 100644 --- a/lib/cf-ip-happy.c +++ b/lib/cf-ip-happy.c @@ -51,6 +51,7 @@ #include "cfilters.h" #include "cf-dns.h" #include "cf-ip-happy.h" +#include "cf-h3-proxy.h" #include "curl_addrinfo.h" #include "curl_trc.h" #include "multiif.h" @@ -60,8 +61,9 @@ struct transport_provider { - uint8_t transport; cf_ip_connect_create *cf_create; + uint8_t transport; + bool tunnel_proxy; }; static @@ -69,23 +71,30 @@ static const #endif struct transport_provider transport_providers[] = { - { TRNSPRT_TCP, Curl_cf_tcp_create }, + { Curl_cf_tcp_create, TRNSPRT_TCP, FALSE }, + { Curl_cf_tcp_create, TRNSPRT_TCP, TRUE }, #if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) - { TRNSPRT_QUIC, Curl_cf_quic_create }, + { Curl_cf_quic_create, TRNSPRT_QUIC, FALSE }, +#endif +#if !defined(CURL_DISABLE_HTTP) && defined(USE_PROXY_HTTP3) + { Curl_cf_h3_proxy_create, TRNSPRT_QUIC, TRUE }, #endif #ifndef CURL_DISABLE_TFTP - { TRNSPRT_UDP, Curl_cf_udp_create }, + { Curl_cf_udp_create, TRNSPRT_UDP, FALSE }, #endif #ifdef USE_UNIX_SOCKETS - { TRNSPRT_UNIX, Curl_cf_unix_create }, + { Curl_cf_unix_create, TRNSPRT_UNIX, FALSE }, + { Curl_cf_unix_create, TRNSPRT_UNIX, TRUE }, #endif }; -static cf_ip_connect_create *get_cf_create(uint8_t transport) +static cf_ip_connect_create *get_cf_create(uint8_t transport, + bool tunnel_proxy) { size_t i; for(i = 0; i < CURL_ARRAYSIZE(transport_providers); ++i) { - if(transport == transport_providers[i].transport) + if((transport == transport_providers[i].transport) && + (tunnel_proxy == transport_providers[i].tunnel_proxy)) return transport_providers[i].cf_create; } return NULL; @@ -102,7 +111,6 @@ UNITTEST void debug_set_transport_provider( for(i = 0; i < CURL_ARRAYSIZE(transport_providers); ++i) { if(transport == transport_providers[i].transport) { transport_providers[i].cf_create = cf_create; - return; } } } @@ -154,7 +162,8 @@ struct cf_ip_attempt { struct curltime started; /* start of current attempt */ CURLcode result; int ai_family; - uint8_t transport; + uint8_t transport_in; + uint8_t transport_out; int error; BIT(connected); /* cf has connected */ BIT(shutdown); /* cf has shutdown */ @@ -177,7 +186,8 @@ static CURLcode cf_ip_attempt_new(struct cf_ip_attempt **pa, struct Curl_easy *data, struct Curl_sockaddr_ex *addr, int ai_family, - uint8_t transport, + uint8_t transport_in, + uint8_t transport_out, cf_ip_connect_create *cf_create) { struct Curl_cfilter *wcf; @@ -191,12 +201,14 @@ static CURLcode cf_ip_attempt_new(struct cf_ip_attempt **pa, a->addr = *addr; a->ai_family = ai_family; - a->transport = transport; + a->transport_in = transport_in; + a->transport_out = transport_out; a->result = CURLE_OK; a->cf_create = cf_create; *pa = a; - result = a->cf_create(&a->cf, data, cf->conn, &a->addr, a->transport); + result = a->cf_create(&a->cf, data, cf->conn, &a->addr, + a->transport_in, a->transport_out); if(result) goto out; @@ -251,7 +263,8 @@ struct cf_ip_ballers { timediff_t attempt_delay_ms; int last_attempt_ai_family; uint32_t max_concurrent; - uint8_t transport; + uint8_t transport_in; + uint8_t transport_out; }; static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a, @@ -269,7 +282,8 @@ static CURLcode cf_ip_attempt_restart(struct cf_ip_attempt *a, a->inconclusive = FALSE; a->cf = NULL; - result = a->cf_create(&a->cf, data, cf->conn, &a->addr, a->transport); + result = a->cf_create(&a->cf, data, cf->conn, &a->addr, a->transport_in, + a->transport_out); if(!result) { bool dummy; /* the new filter might have sub-filters */ @@ -299,18 +313,20 @@ static void cf_ip_ballers_clear(struct Curl_cfilter *cf, static CURLcode cf_ip_ballers_init(struct cf_ip_ballers *bs, struct Curl_cfilter *cf, cf_ip_connect_create *cf_create, - uint8_t transport, + uint8_t transport_in, + uint8_t transport_out, timediff_t attempt_delay_ms, uint32_t max_concurrent) { memset(bs, 0, sizeof(*bs)); bs->cf_create = cf_create; - bs->transport = transport; + bs->transport_in = transport_in; + bs->transport_out = transport_out; bs->attempt_delay_ms = attempt_delay_ms; bs->max_concurrent = max_concurrent; bs->last_attempt_ai_family = AF_INET; /* so AF_INET6 is next */ - if(transport == TRNSPRT_UNIX) { + if(transport_in == TRNSPRT_UNIX) { #ifdef USE_UNIX_SOCKETS cf_ai_iter_init(&bs->addr_iter, cf, AF_UNIX); #else @@ -458,12 +474,13 @@ evaluate: if(bs->max_concurrent) cf_ip_ballers_prune(bs, cf, data, bs->max_concurrent - 1); - result = Curl_socket_addr_from_ai(&addr, ai, bs->transport); + result = Curl_socket_addr_from_ai(&addr, ai, bs->transport_out); if(result) goto out; result = cf_ip_attempt_new(&a, cf, data, &addr, ai_family, - bs->transport, bs->cf_create); + bs->transport_in, bs->transport_out, + 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); @@ -652,11 +669,13 @@ typedef enum { } cf_connect_state; struct cf_ip_happy_ctx { - uint8_t transport; + struct Curl_peer *peer; cf_ip_connect_create *cf_create; cf_connect_state state; struct cf_ip_ballers ballers; struct curltime started; + uint8_t transport_in; + uint8_t transport_out; BIT(dns_resolved); }; @@ -732,10 +751,11 @@ static CURLcode cf_ip_happy_init(struct Curl_cfilter *cf, return CURLE_OPERATION_TIMEDOUT; } - CURL_TRC_CF(data, cf, "init ip ballers for transport %u", ctx->transport); + CURL_TRC_CF(data, cf, "init ip ballers for transport %u", + ctx->transport_out); ctx->started = *Curl_pgrs_now(data); - return cf_ip_ballers_init(&ctx->ballers, cf, - ctx->cf_create, ctx->transport, + return cf_ip_ballers_init(&ctx->ballers, cf, ctx->cf_create, + ctx->transport_in, ctx->transport_out, data->set.happy_eyeballs_timeout, IP_HE_MAX_CONCURRENT_ATTEMPTS); } @@ -752,8 +772,10 @@ static void cf_ip_happy_ctx_clear(struct Curl_cfilter *cf, static void cf_ip_happy_ctx_destroy(struct cf_ip_happy_ctx *ctx) { - if(ctx) + if(ctx) { + Curl_peer_unlink(&ctx->peer); curlx_free(ctx); + } } static CURLcode cf_ip_happy_shutdown(struct Curl_cfilter *cf, @@ -973,9 +995,11 @@ struct Curl_cftype Curl_cft_ip_happy = { */ static CURLcode cf_ip_happy_create(struct Curl_cfilter **pcf, struct Curl_easy *data, + struct Curl_peer *peer, struct connectdata *conn, cf_ip_connect_create *cf_create, - uint8_t transport) + uint8_t transport_in, + uint8_t transport_out) { struct cf_ip_happy_ctx *ctx = NULL; CURLcode result; @@ -988,8 +1012,10 @@ static CURLcode cf_ip_happy_create(struct Curl_cfilter **pcf, result = CURLE_OUT_OF_MEMORY; goto out; } - ctx->transport = transport; + ctx->transport_in = transport_in; + ctx->transport_out = transport_out; ctx->cf_create = cf_create; + Curl_peer_link(&ctx->peer, peer); result = Curl_cf_create(pcf, &Curl_cft_ip_happy, ctx); @@ -1003,7 +1029,10 @@ out: CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, - uint8_t transport) + struct Curl_peer *peer, + uint8_t transport_in, + uint8_t transport_out, + bool tunnel_proxy) { cf_ip_connect_create *cf_create; struct Curl_cfilter *cf; @@ -1011,40 +1040,17 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at, /* Need to be first */ DEBUGASSERT(cf_at); - cf_create = get_cf_create(transport); + cf_create = get_cf_create(transport_out, tunnel_proxy); if(!cf_create) { - CURL_TRC_CF(data, cf_at, "unsupported transport type %u", transport); + CURL_TRC_CF(data, cf_at, "unsupported transport type %u%s", + transport_out, tunnel_proxy ? "to proxy" : ""); return CURLE_UNSUPPORTED_PROTOCOL; } - result = cf_ip_happy_create(&cf, data, cf_at->conn, cf_create, transport); + result = cf_ip_happy_create(&cf, data, peer, cf_at->conn, cf_create, + transport_in, transport_out); if(result) return result; Curl_conn_cf_insert_after(cf_at, cf); return CURLE_OK; } - -#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \ - defined(USE_PROXY_HTTP3) -CURLcode cf_ip_happy_quic_udp_insert_after(struct Curl_cfilter *cf_at, - struct Curl_easy *data) -{ - /* For H3 proxy: create happy eyeballs that races IPv4/IPv6 using raw - UDP sockets with TRNSPRT_QUIC transport. Using TRNSPRT_QUIC causes - cf_udp_connect() to call cf_udp_setup_quic() which connects the - socket to the peer address, making send() work without an explicit - destination. We use Curl_cf_udp_create (not Curl_cf_quic_create) - because H3-PROXY manages its own ngtcp2 QUIC stack on top. */ - struct Curl_cfilter *cf; - CURLcode result; - - DEBUGASSERT(cf_at); - result = cf_ip_happy_create(&cf, data, cf_at->conn, - Curl_cf_udp_create, TRNSPRT_QUIC); - if(result) - return result; - - Curl_conn_cf_insert_after(cf_at, cf); - return CURLE_OK; -} -#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */ diff --git a/lib/cf-ip-happy.h b/lib/cf-ip-happy.h index 970ec24881..90cecae889 100644 --- a/lib/cf-ip-happy.h +++ b/lib/cf-ip-happy.h @@ -29,6 +29,7 @@ struct connectdata; struct Curl_addrinfo; struct Curl_cfilter; struct Curl_easy; +struct Curl_peer; struct Curl_sockaddr_ex; /** @@ -46,11 +47,15 @@ typedef CURLcode cf_ip_connect_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport); + uint8_t transport_in, + uint8_t transport_out); CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, - uint8_t transport); + struct Curl_peer *peer, + uint8_t transport_in, + uint8_t transport_out, + bool tunnel_proxy); #if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \ defined(USE_PROXY_HTTP3) diff --git a/lib/cf-socket.c b/lib/cf-socket.c index eb782b65dc..729f8748bf 100644 --- a/lib/cf-socket.c +++ b/lib/cf-socket.c @@ -1768,7 +1768,8 @@ CURLcode Curl_cf_tcp_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport) + uint8_t transport_in, + uint8_t transport_out) { struct cf_socket_ctx *ctx = NULL; struct Curl_cfilter *cf = NULL; @@ -1776,7 +1777,8 @@ CURLcode Curl_cf_tcp_create(struct Curl_cfilter **pcf, (void)data; (void)conn; - DEBUGASSERT(transport == TRNSPRT_TCP); + (void)transport_in; + DEBUGASSERT(transport_out == TRNSPRT_TCP); if(!addr) { result = CURLE_BAD_FUNCTION_ARGUMENT; goto out; @@ -1788,7 +1790,7 @@ CURLcode Curl_cf_tcp_create(struct Curl_cfilter **pcf, goto out; } - result = cf_socket_ctx_init(ctx, addr, transport); + result = cf_socket_ctx_init(ctx, addr, transport_out); if(result) goto out; @@ -1934,7 +1936,8 @@ CURLcode Curl_cf_udp_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport) + uint8_t transport_in, + uint8_t transport_out) { struct cf_socket_ctx *ctx = NULL; struct Curl_cfilter *cf = NULL; @@ -1942,14 +1945,15 @@ CURLcode Curl_cf_udp_create(struct Curl_cfilter **pcf, (void)data; (void)conn; - DEBUGASSERT(transport == TRNSPRT_UDP || transport == TRNSPRT_QUIC); + (void)transport_in; + DEBUGASSERT(transport_out == TRNSPRT_UDP || transport_out == TRNSPRT_QUIC); ctx = curlx_calloc(1, sizeof(*ctx)); if(!ctx) { result = CURLE_OUT_OF_MEMORY; goto out; } - result = cf_socket_ctx_init(ctx, addr, transport); + result = cf_socket_ctx_init(ctx, addr, transport_out); if(result) goto out; @@ -1988,7 +1992,8 @@ CURLcode Curl_cf_unix_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport) + uint8_t transport_in, + uint8_t transport_out) { struct cf_socket_ctx *ctx = NULL; struct Curl_cfilter *cf = NULL; @@ -1996,14 +2001,15 @@ CURLcode Curl_cf_unix_create(struct Curl_cfilter **pcf, (void)data; (void)conn; - DEBUGASSERT(transport == TRNSPRT_UNIX); + (void)transport_in; + DEBUGASSERT(transport_out == TRNSPRT_UNIX); ctx = curlx_calloc(1, sizeof(*ctx)); if(!ctx) { result = CURLE_OUT_OF_MEMORY; goto out; } - result = cf_socket_ctx_init(ctx, addr, transport); + result = cf_socket_ctx_init(ctx, addr, transport_out); if(result) goto out; diff --git a/lib/cf-socket.h b/lib/cf-socket.h index 40c001cc14..9c1f3bf4b4 100644 --- a/lib/cf-socket.h +++ b/lib/cf-socket.h @@ -96,7 +96,8 @@ CURLcode Curl_cf_tcp_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport); + uint8_t transport_in, + uint8_t transport_out); /** * Creates a cfilter that opens a UDP socket to the given address @@ -109,7 +110,8 @@ CURLcode Curl_cf_udp_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport); + uint8_t transport_in, + uint8_t transport_out); /** * Creates a cfilter that opens a UNIX socket to the given address @@ -122,7 +124,8 @@ CURLcode Curl_cf_unix_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport); + uint8_t transport_in, + uint8_t transport_out); /** * Creates a cfilter that keeps a listening socket. diff --git a/lib/cfilters.c b/lib/cfilters.c index 3946c7231c..46c17c199d 100644 --- a/lib/cfilters.c +++ b/lib/cfilters.c @@ -690,7 +690,9 @@ bool Curl_conn_is_ip_connected(struct Curl_easy *data, int sockindex) static bool cf_is_ssl(struct Curl_cfilter *cf) { for(; cf; cf = cf->next) { - if(cf->cft->flags & CF_TYPE_SSL) + /* A tunneling proxy does not offer end2end encryption, even if + * it does SSL itself (e.g. QUIC H3 proxy) */ + if((cf->cft->flags & CF_TYPE_SSL) && !(cf->cft->flags & CF_TYPE_PROXY)) return TRUE; if(cf->cft->flags & CF_TYPE_IP_CONNECT) return FALSE; diff --git a/lib/connect.c b/lib/connect.c index c2038f4ee4..0ed7b22a5b 100644 --- a/lib/connect.c +++ b/lib/connect.c @@ -342,72 +342,14 @@ 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) { struct cf_setup_ctx *ctx = cf->ctx; CURLcode result = CURLE_OK; + struct Curl_peer *first_peer = + Curl_conn_get_first_peer(cf->conn, cf->sockindex); if(cf->connected) { *done = TRUE; @@ -425,38 +367,38 @@ connect_sub_chain: } if(ctx->state < CF_SETUP_CNNCT_EYEBALLS) { -#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); + /* What type of thing we do connect to first? + * - without a proxy, `ctx->transport` defines it + * - with non-tunneling proxy, `ctx->transport` also applies, but + * for QUIC we need the cf-h3-proxy, not the standard vquic one + * - with tunneling proxy, transport is defined by the proxytype + * chosen and `ctx->transport` is tunneled through it. + */ + uint8_t transport_out = ctx->transport; + bool tunnel_proxy = FALSE; +#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP) + CURL_TRC_CF(data, cf, "happy eyeballing, httpproxy=%d, type=%d, " + "transport=%d", + cf->conn->bits.httpproxy, cf->conn->http_proxy.proxytype, + ctx->transport); + if(cf->conn->bits.httpproxy && cf->conn->bits.tunnel_proxy) { + transport_out = + Curl_http_proxy_transport(cf->conn->http_proxy.proxytype); + tunnel_proxy = TRUE; + if((transport_out == TRNSPRT_QUIC) && (cf->conn->bits.socksproxy)) { + failf(data, "HTTP/3 proxy not possible via SOCKS"); + return CURLE_UNSUPPORTED_PROTOCOL; + } } - /* 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); +#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */ + result = cf_ip_happy_insert_after(cf, data, first_peer, + ctx->transport, transport_out, + tunnel_proxy); if(result) return result; - ctx->state = CF_SETUP_CNNCT_EYEBALLS; + ctx->state = (tunnel_proxy && (transport_out == TRNSPRT_QUIC)) ? + CF_SETUP_CNNCT_HTTP_PROXY : CF_SETUP_CNNCT_EYEBALLS; if(!cf->next || !cf->next->connected) goto connect_sub_chain; } @@ -491,9 +433,25 @@ connect_sub_chain: } if(ctx->state < CF_SETUP_CNNCT_HTTP_PROXY && cf->conn->bits.httpproxy) { - result = cf_setup_add_http_proxy(cf, data, ctx); - if(result) - return result; +#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, ctx->transport, cf->conn->http_proxy.proxytype); + if(result) + return result; + } +#endif /* !CURL_DISABLE_HTTP */ ctx->state = CF_SETUP_CNNCT_HTTP_PROXY; if(!cf->next || !cf->next->connected) goto connect_sub_chain; @@ -503,9 +461,8 @@ connect_sub_chain: if(ctx->state < CF_SETUP_CNNCT_HAPROXY) { #ifndef CURL_DISABLE_PROXY if(data->set.haproxyprotocol) { - if(Curl_conn_is_ssl(cf->conn, cf->sockindex)) { - failf(data, "haproxy protocol not supported with SSL " - "encryption in place (QUIC?)"); + if(ctx->transport == TRNSPRT_QUIC) { + failf(data, "haproxy protocol not support QUIC"); return CURLE_UNSUPPORTED_PROTOCOL; } result = Curl_cf_haproxy_insert_after(cf, data); diff --git a/lib/http_proxy.c b/lib/http_proxy.c index a52a1c3713..39cfb12446 100644 --- a/lib/http_proxy.c +++ b/lib/http_proxy.c @@ -753,8 +753,8 @@ struct Curl_cftype Curl_cft_http_proxy = { CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, - uint8_t proxytype, - bool udp_tunnel) + uint8_t transport, + uint8_t proxytype) { struct Curl_cfilter *cf; struct cf_proxy_ctx *ctx = NULL; @@ -771,7 +771,7 @@ CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, } Curl_peer_link(&ctx->dest, dest); ctx->proxytype = proxytype; - ctx->udp_tunnel = udp_tunnel; + ctx->udp_tunnel = (transport == TRNSPRT_QUIC); result = Curl_cf_create(&cf, &Curl_cft_http_proxy, ctx); if(result) @@ -784,4 +784,14 @@ out: return result; } +uint8_t Curl_http_proxy_transport(uint8_t proxytype) +{ + switch(proxytype) { + case CURLPROXY_HTTPS3: + return TRNSPRT_QUIC; + default: + return TRNSPRT_TCP; + } +} + #endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_PROXY */ diff --git a/lib/http_proxy.h b/lib/http_proxy.h index 0a5734e3d8..ef4becdacf 100644 --- a/lib/http_proxy.h +++ b/lib/http_proxy.h @@ -69,8 +69,8 @@ CURLcode Curl_http_proxy_inspect_tunnel_response( CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at, struct Curl_easy *data, struct Curl_peer *dest, - uint8_t proxytype, - bool udp_tunnel); + uint8_t transport, + uint8_t proxytype); extern struct Curl_cftype Curl_cft_http_proxy; @@ -83,4 +83,6 @@ extern struct Curl_cftype Curl_cft_http_proxy; #define IS_QUIC_PROXY(t) ((t) == CURLPROXY_HTTPS3) +uint8_t Curl_http_proxy_transport(uint8_t proxytype); + #endif /* HEADER_CURL_HTTP_PROXY_H */ diff --git a/lib/url.c b/lib/url.c index 926d29ed5f..b7ba30fe2b 100644 --- a/lib/url.c +++ b/lib/url.c @@ -1317,12 +1317,7 @@ static struct connectdata *allocate_conn(struct Curl_easy *data) #endif conn->ip_version = data->set.ipver; conn->bits.connect_only = (bool)data->set.connect_only; -#ifndef CURL_DISABLE_PROXY - if(conn->http_proxy.proxytype == CURLPROXY_HTTPS3) - conn->transport_wanted = TRNSPRT_QUIC; - else -#endif - conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */ + conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */ /* Store the local bind parameters that will be used for this connection */ if(data->set.str[STRING_DEVICE]) { diff --git a/lib/vquic/curl_ngtcp2.c b/lib/vquic/curl_ngtcp2.c index 8693ed16ee..20996e5a98 100644 --- a/lib/vquic/curl_ngtcp2.c +++ b/lib/vquic/curl_ngtcp2.c @@ -3120,7 +3120,8 @@ CURLcode Curl_cf_ngtcp2_create(struct Curl_cfilter **pcf, goto out; cf->conn = conn; - result = Curl_cf_udp_create(&cf->next, data, conn, addr, TRNSPRT_QUIC); + result = Curl_cf_udp_create(&cf->next, data, conn, addr, + TRNSPRT_QUIC, TRNSPRT_QUIC); if(result) goto out; cf->next->conn = cf->conn; diff --git a/lib/vquic/curl_quiche.c b/lib/vquic/curl_quiche.c index 43a16958a6..08b02fec78 100644 --- a/lib/vquic/curl_quiche.c +++ b/lib/vquic/curl_quiche.c @@ -1659,7 +1659,8 @@ CURLcode Curl_cf_quiche_create(struct Curl_cfilter **pcf, goto out; cf->conn = conn; - result = Curl_cf_udp_create(&cf->next, data, conn, addr, TRNSPRT_QUIC); + result = Curl_cf_udp_create(&cf->next, data, conn, addr, + TRNSPRT_QUIC, TRNSPRT_QUIC); if(result) goto out; cf->next->conn = cf->conn; diff --git a/lib/vquic/vquic.c b/lib/vquic/vquic.c index a35abfb2c9..dba907bbf0 100644 --- a/lib/vquic/vquic.c +++ b/lib/vquic/vquic.c @@ -769,10 +769,12 @@ CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport) + uint8_t transport_in, + uint8_t transport_out) { - (void)transport; - DEBUGASSERT(transport == TRNSPRT_QUIC); + (void)transport_in; + (void)transport_out; + DEBUGASSERT(transport_out == TRNSPRT_QUIC); #if defined(USE_NGTCP2) && defined(USE_NGHTTP3) return Curl_cf_ngtcp2_create(pcf, data, conn, addr); #elif defined(USE_QUICHE) diff --git a/lib/vquic/vquic.h b/lib/vquic/vquic.h index 59178acd94..e3d894f8c0 100644 --- a/lib/vquic/vquic.h +++ b/lib/vquic/vquic.h @@ -45,7 +45,8 @@ CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport); + uint8_t transport_in, + uint8_t transport_out); extern struct Curl_cftype Curl_cft_http3; diff --git a/tests/http/conftest.py b/tests/http/conftest.py index 5275b91bf9..225d63fe6d 100644 --- a/tests/http/conftest.py +++ b/tests/http/conftest.py @@ -176,7 +176,7 @@ def h2o_server(env) -> Generator[Union[H2oServer, bool], None, None]: h2o_logs = "\n".join(h2o.dump_logs()) pytest.skip(f"h2o server failed to start\n{h2o_logs}") yield h2o - h2o.stop() + h2o.kill() else: yield False @@ -190,6 +190,6 @@ def h2o_proxy(env) -> Generator[Union[H2oProxy, bool], None, None]: h2o_logs = "\n".join(h2o.dump_logs()) pytest.skip(f"h2o proxy failed to start\n{h2o_logs}") yield h2o - h2o.stop() + h2o.kill() else: yield False diff --git a/tests/http/test_60_h3_proxy.py b/tests/http/test_60_h3_proxy.py index ca4501f635..34d628445d 100644 --- a/tests/http/test_60_h3_proxy.py +++ b/tests/http/test_60_h3_proxy.py @@ -95,29 +95,29 @@ def _check_download_size(curl: CurlClient, expected_size: int): def _nghttpx_proxy_args( env: Env, nghttpx, + nghttpx_fwd, proxy_proto: str, tunnel: bool, - insecure: bool = False, ): - xargs = [ - "--proxy", - f"https://{env.proxy_domain}:{nghttpx._port}/", - "--resolve", - f"{env.proxy_domain}:{nghttpx._port}:127.0.0.1", - "--proxy-cacert", - env.ca.cert_file, - ] + port = env.pts_port(proxy_proto) + domain = env.proxy_domain + xxarg = None if proxy_proto == "h3": - xargs.append("--proxy-http3") + port = nghttpx.port + domain = env.domain1 + xxarg = "--proxy-http3" elif proxy_proto == "h2": - xargs.append("--proxy-http2") + xxarg = "--proxy-http2" + xargs = [ + "--proxy", f"https://{domain}:{port}/", + "--resolve", f"{domain}:{port}:127.0.0.1", + "--proxy-cacert", env.ca.cert_file + ] + if xxarg: + xargs.append(xxarg) if tunnel: xargs.append("--proxytunnel") - - xargs.extend(["--cacert", env.ca.cert_file, "--proxy-insecure"]) - if insecure: - xargs.append("--insecure") return xargs @@ -126,22 +126,13 @@ def _h2o_proxy_args( h2o_proxy, proxy_proto: str, tunnel: bool, - insecure: bool = False, ): - if proxy_proto == "h3": - pport = h2o_proxy.port - elif proxy_proto == "h2": - pport = h2o_proxy.h2_port - else: - pport = h2o_proxy.h1_port - + pport = env.pts_port(proxy_proto, use_h2o=True) xargs = [ - "--proxy", - f"https://{env.proxy_domain}:{pport}/", - "--resolve", - f"{env.proxy_domain}:{pport}:127.0.0.1", - "--proxy-cacert", - env.ca.cert_file, + "--proxy", f"https://{env.proxy_domain}:{pport}/", + "--resolve", f"{env.proxy_domain}:{pport}:127.0.0.1", + "--proxy-cacert", env.ca.cert_file, + "--cacert", env.ca.cert_file, ] if proxy_proto == "h2": xargs.append("--proxy-http2") @@ -151,9 +142,6 @@ def _h2o_proxy_args( if tunnel: xargs.append("--proxytunnel") - xargs.extend(["--cacert", env.ca.cert_file, "--proxy-insecure"]) - if insecure: - xargs.append("--insecure") return xargs @@ -195,7 +183,7 @@ class TestH3ProxySuccess: curl = CurlClient(env=env) url = f"https://localhost:{h2o_server.port}/data.json" proxy_args = _h2o_proxy_args( - env, h2o_proxy, proxy_proto, tunnel=True, insecure=True + env, h2o_proxy, proxy_proto, tunnel=True ) r = curl.http_download( @@ -235,14 +223,14 @@ class TestH3ProxyFailure: pytest.param( "h3", "h2", - "connect-udp response status 400", + "proxy closed connection", marks=MARK_NEEDS_NGHTTP2, id="fail_h3_over_h2_proxytunnel", ), pytest.param( "h3", "http/1.1", - "connect-udp tunnel failed, response 404", + "connect-udp tunnel failed", id="fail_h3_over_h1_proxytunnel", ), ], @@ -252,21 +240,24 @@ class TestH3ProxyFailure: env: Env, httpd, nghttpx, + nghttpx_fwd, alpn_proto, proxy_proto, exp_err, ): - _require_available(httpd=httpd, nghttpx=nghttpx) + _require_available(httpd=httpd, nghttpx=nghttpx, nghttpx_fwd=nghttpx_fwd) curl = CurlClient(env=env) - url = f"https://localhost:{httpd.ports['https']}/data.json" - proxy_args = _nghttpx_proxy_args(env, nghttpx, proxy_proto, tunnel=True) + url = f"https://localhost:{env.https_port}/data.json" + proxy_args = _nghttpx_proxy_args( + env, nghttpx, nghttpx_fwd, proxy_proto, tunnel=True + ) r = curl.http_download( urls=[url], alpn_proto=alpn_proto, with_stats=True, extra_args=proxy_args ) - assert r.exit_code != 0, f"Expected failure but curl succeeded: {r}" + assert r.exit_code != 0, f"Expected failure but curl succeeded: {r.dump_logs()}" assert exp_err in r.stderr.lower(), ( - f"Expected protocol/proxy error but got: {r.stderr}" + f"Expected protocol/proxy error but got: {r.dump_logs()}" ) @@ -284,14 +275,16 @@ class TestH3ProxyModeSelection: ], ) def test_60_03_h3_target_auto_connect_udp( - self, env: Env, httpd, nghttpx, proxy_proto + self, env: Env, httpd, nghttpx, nghttpx_fwd, proxy_proto ): - _require_available(httpd=httpd, nghttpx=nghttpx) + _require_available( + httpd=httpd, nghttpx=nghttpx, nghttpx_fwd=nghttpx_fwd + ) curl = CurlClient(env=env) url = f"https://localhost:{httpd.ports['https']}/data.json" proxy_args = _nghttpx_proxy_args( - env, nghttpx, proxy_proto, tunnel=False + env, nghttpx, nghttpx_fwd, proxy_proto, tunnel=False ) r = curl.http_download( urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args @@ -305,7 +298,7 @@ class TestH3ProxyModeSelection: "which nghttpx does not support" ) assert "connect-udp" in r.stderr.lower(), ( - f"expected CONNECT-UDP attempt in output, got: {r.stderr}" + f"expected CONNECT-UDP attempt in output, got: {r.dump_logs()}" ) @@ -324,6 +317,9 @@ class TestH3ProxyRuntimeGuards: @pytest.mark.skipif( condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support" ) + @pytest.mark.skipif( + condition=Env.curl_has_feature("proxy-HTTP3"), reason="curl has h3 proxy support" + ) def test_60_04_guard_proxy_http3_unsupported(self, env: Env, httpd): curl = CurlClient(env=env) url = f"https://localhost:{httpd.ports['https']}/data.json" @@ -332,7 +328,6 @@ class TestH3ProxyRuntimeGuards: "https://127.0.0.1:1/", "--proxy-http3", "--proxytunnel", - "--proxy-insecure", "--cacert", env.ca.cert_file, ] @@ -340,16 +335,9 @@ class TestH3ProxyRuntimeGuards: r = curl.http_download( urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args ) - if not env.curl_has_feature("proxy-HTTP3"): - r.check_exit_code(2) - assert UNSUPPORTED_OPT_MSG in r.stderr.lower(), ( - f"Expected unsupported option failure but got: {r.stderr}" - ) - return - - r.check_exit_code(1) - assert NGTCP2_ONLY_MSG in r.stderr.lower(), ( - f"Expected ngtcp2 guard failure but got: {r.stderr}" + r.check_exit_code(2) + assert UNSUPPORTED_OPT_MSG in r.stderr.lower(), ( + f"Expected unsupported option failure but got: {r.stderr}" ) @@ -398,26 +386,21 @@ class TestH3ProxyRobustness: proxy_port = h2o_proxy.port url = f"https://localhost:{h2o_server.port}/proxy-drop-20m" out_path = os.path.join(env.gen_dir, "proxy-drop.out") + if os.path.exists(out_path): + os.remove(out_path) args = [ env.curl, "--http1.1", - "--proxy", - f"https://{env.proxy_domain}:{proxy_port}/", - "--resolve", - f"{env.proxy_domain}:{proxy_port}:127.0.0.1", - "--proxy-cacert", - env.ca.cert_file, + "--proxy", f"https://{env.proxy_domain}:{proxy_port}/", + "--resolve", f"{env.proxy_domain}:{proxy_port}:127.0.0.1", "--proxy-http3", "--proxytunnel", - "--proxy-insecure", - "--cacert", - env.ca.cert_file, - "--limit-rate", - "100k", - "--max-time", - "20", - "-o", - out_path, + "--proxy-cacert", env.ca.cert_file, + "--cacert", env.ca.cert_file, + "--limit-rate", "10k", + "--max-time", "20", + "-o", out_path, + "-v", url, ] @@ -426,19 +409,14 @@ class TestH3ProxyRobustness: proc = subprocess.Popen( args=args, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True ) - time.sleep(1.0) - assert h2o_proxy.stop(), "failed to stop h2o proxy" + while not os.path.exists(out_path): + time.sleep(0.1) + assert h2o_proxy.kill(), "failed to stop h2o proxy" _, stderr = proc.communicate(timeout=30) assert proc.returncode != 0, ( "curl should fail when proxy is terminated mid-transfer" ) - serr = stderr.lower() - assert ( - "failed" in serr - or "transfer closed" in serr - or "recv failure" in serr - or "connection" in serr - ), f"Unexpected error output: {stderr}" + assert proc.returncode == 56, f'{stderr}' finally: if proc and (proc.poll() is None): proc.kill() @@ -463,7 +441,7 @@ class TestH3ProxyDataTransfer: _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) curl = CurlClient(env=env) url = f"https://localhost:{h2o_server.port}/download-10m" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) r = curl.http_download( urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args ) @@ -475,7 +453,7 @@ class TestH3ProxyDataTransfer: fdata = os.path.join(env.gen_dir, "upload-2m") curl = CurlClient(env=env) url = f"https://localhost:{httpd.ports['https']}/curltest/echo?id=[0-0]" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) r = curl.http_upload( urls=[url], data=f"@{fdata}", @@ -490,7 +468,7 @@ class TestH3ProxyDataTransfer: count = 5 curl = CurlClient(env=env) urln = f"https://localhost:{h2o_server.port}/download-1m?[0-{count - 1}]" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) proxy_args.extend(["--parallel", "--parallel-max", f"{count}"]) r = curl.http_download( urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args @@ -507,7 +485,7 @@ class TestH3ProxyConnectionManagement: _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) curl = CurlClient(env=env) url = f"https://localhost:{h2o_server.port}/data.json" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) proxy_args.extend(["--proxy-user", "testuser:testpass"]) r = curl.http_download( urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args @@ -519,7 +497,7 @@ class TestH3ProxyConnectionManagement: _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) curl = CurlClient(env=env) urln = f"https://localhost:{h2o_server.port}/data.json?[0-2]" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) r = curl.http_download( urls=[urln], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args ) @@ -528,30 +506,29 @@ class TestH3ProxyConnectionManagement: f"expected proxy connection reuse, got {r.total_connects} connects" ) + @pytest.mark.skipif(condition=not Env.curl_has_feature('SSLS-EXPORT'), + reason='curl lacks SSL session export support') def test_60_12_quic_session_resumption(self, env: Env, h2o_server, h2o_proxy): _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) - # First request establishes QUIC session - curl1 = CurlClient(env=env) + curl = CurlClient(env=env) url = f"https://localhost:{h2o_server.port}/data.json" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) - r1 = curl1.http_download( - urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + xargs = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) + session_file = os.path.join(env.gen_dir, 'test_60_12.sessions') + if os.path.exists(session_file): + os.remove(session_file) + xargs.extend(['--ssl-sessions', session_file]) + # First request establishes QUIC session + r1 = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=xargs ) r1.check_response(count=1, http_status=200) - # Second request from a fresh CurlClient; session may be reused - # by the TLS session cache if supported - curl2 = CurlClient(env=env) - r2 = curl2.http_download( - urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + xargs.extend(['--trace-config', 'ssls']) + r2 = curl.http_download( + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=xargs ) r2.check_response(count=1, http_status=200) - # Third request from a fresh CurlClient; session may be reused - # by the TLS session cache if supported - curl3 = CurlClient(env=env) - r3 = curl3.http_download( - urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args - ) - r3.check_response(count=1, http_status=200) + reuses = [line for line in r2.trace_lines if '[SSLS] took session for proxy.http.curl.se' in line] + assert len(reuses), f'{r2.dump_logs()}' class TestH3ProxyUdpTunnel: @@ -582,7 +559,7 @@ class TestH3ProxyUdpTunnel: _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) curl = CurlClient(env=env) url = f"https://localhost:{h2o_server.port}/{fname}" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) r = curl.http_download( urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args ) @@ -590,11 +567,17 @@ class TestH3ProxyUdpTunnel: _check_download_size(curl, fsize) @MARK_NEEDS_NGHTTPX - def test_60_14_udp_tunnel_capsule_absent(self, env: Env, httpd, nghttpx): - _require_available(httpd=httpd, nghttpx=nghttpx) + def test_60_14_udp_tunnel_capsule_absent( + self, env: Env, httpd, nghttpx, nghttpx_fwd + ): + _require_available( + httpd=httpd, nghttpx=nghttpx, nghttps_fwd=nghttpx_fwd + ) curl = CurlClient(env=env) url = f"https://localhost:{httpd.ports['https']}/data.json" - proxy_args = _nghttpx_proxy_args(env, nghttpx, "h3", tunnel=True) + proxy_args = _nghttpx_proxy_args( + env, nghttpx, nghttpx_fwd, "h3", tunnel=True + ) r = curl.http_download( urls=[url], alpn_proto="h3", with_stats=True, extra_args=proxy_args ) @@ -608,25 +591,21 @@ class TestH3ProxyEdgeCases: pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] - def test_60_15_connect_timeout(self, env: Env, h2o_server): - _require_available(h2o_server=h2o_server) + def test_60_15_connect_timeout(self, env: Env, h2o_proxy): + _require_available(h2o_proxy=h2o_proxy) curl = CurlClient(env=env, timeout=15) - url = f"https://localhost:{h2o_server.port}/data.json" - proxy_args = [ - "--proxy", - "https://192.0.2.1:1/", - "--proxy-http3", - "--proxytunnel", - "--proxy-insecure", - "--connect-timeout", - "3", - "--cacert", - env.ca.cert_file, + url = f"https://localhost:{h2o_proxy.port}/data.json" + # ipv6 0100::/64 is supposed to go into the void (rfc6666) + xargs = [ + '--proxy', 'https://xxx.invalid/', + '--resolve', 'xxx.invalid:443:0100::1,0100::2,0100::3', + '--proxy-http3', '--proxytunnel', + '--connect-timeout', '1', ] r = curl.http_download( - urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args + urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=xargs ) - assert r.exit_code != 0, "expected timeout connecting to unreachable proxy" + r.check_exit_code(28) # CURLE_OPERATION_TIMEDOUT assert r.duration.total_seconds() < 10, ( f"timeout not respected: took {r.duration.total_seconds():.1f}s" ) @@ -635,8 +614,8 @@ class TestH3ProxyEdgeCases: def test_60_16_h2_uses_connect_tcp_not_udp(self, env: Env, httpd, h2o_proxy): _require_available(httpd=httpd, h2o_proxy=h2o_proxy) curl = CurlClient(env=env) - url = f"https://localhost:{httpd.ports['https']}/data.json" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + url = f"https://localhost:{env.https_port}/data.json" + proxy_args = curl.get_proxy_args("h3", tunnel=True) # h2 inner traffic always uses CONNECT (TCP), never CONNECT-UDP, # even through an HTTP/3 proxy with --proxytunnel. h2o supports # CONNECT TCP tunneling, so this request succeeds. @@ -663,7 +642,7 @@ class TestH3ProxyHappyEyeballs: _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) curl = CurlClient(env=env, run_env={"CURL_DEBUG": "HAPPY-EYEBALLS,H3-PROXY"}) url = f"https://localhost:{h2o_server.port}/data.json" - proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True, insecure=True) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) r = curl.http_download( urls=[url], alpn_proto="http/1.1", with_stats=True, extra_args=proxy_args ) @@ -679,9 +658,7 @@ class TestH3ProxyHappyEyeballs: for alpn_proto in ["http/1.1", "h2", "h3"]: curl = CurlClient(env=env) url = f"https://localhost:{h2o_server.port}/data.json" - proxy_args = _h2o_proxy_args( - env, h2o_proxy, "h3", tunnel=True, insecure=True - ) + proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) proxy_args.append("--ipv4") r = curl.http_download( urls=[url], diff --git a/tests/http/testenv/curl.py b/tests/http/testenv/curl.py index 272b6045cb..5149a8578d 100644 --- a/tests/http/testenv/curl.py +++ b/tests/http/testenv/curl.py @@ -684,12 +684,13 @@ class CurlClient: def get_proxy_args(self, proto: str = 'http/1.1', proxys: bool = True, tunnel: bool = False, - use_ip: bool = False, use_ipv6: bool = False): + use_ip: bool = False, use_ipv6: bool = False, + use_h2o: bool = False): proxy_name = '[::1]' if use_ipv6 else \ self._server_addr if use_ip else self.env.proxy_domain if proxys: if tunnel: - pport = self.env.pts_port(proto) + pport = self.env.pts_port(proto, use_h2o=use_h2o) elif proto == 'h3': pport = self.env.h3proxys_port else: diff --git a/tests/http/testenv/env.py b/tests/http/testenv/env.py index 093092b4c5..4a18d65ffa 100644 --- a/tests/http/testenv/env.py +++ b/tests/http/testenv/env.py @@ -824,15 +824,16 @@ class Env: @property def h3proxys_port(self) -> int: - return self.CONFIG.ports["h3proxys"] + return self.CONFIG.ports["h2o_h3proxys"] - def pts_port(self, proto: str = "http/1.1") -> int: + def pts_port(self, proto: str = "http/1.1", use_h2o: bool = False) -> int: # proxy tunnel port + prefix = 'h2o_' if use_h2o else '' if proto == "h3": - return self.CONFIG.ports["h3proxys"] + return self.CONFIG.ports.get("h2o_h3proxys", 0) if proto == "h2": - return self.CONFIG.ports["h2proxys"] - return self.CONFIG.ports["proxys"] + return self.CONFIG.ports.get(f"{prefix}h2proxys", 0) + return self.CONFIG.ports[f"{prefix}proxys"] @property def caddy(self) -> str: diff --git a/tests/http/testenv/h2o.py b/tests/http/testenv/h2o.py index c67aaf1888..279cff8b90 100644 --- a/tests/http/testenv/h2o.py +++ b/tests/http/testenv/h2o.py @@ -160,6 +160,12 @@ class H2o: ) return True + def kill(self, wait_dead=True): + if self._process: + self._process.kill() + return True + return False + def restart(self): self.stop() return self.start() @@ -317,9 +323,9 @@ class H2oProxy(H2o): super().initial_start() def startup(ports: Dict[str, int]) -> bool: - self._port = ports["h3proxys"] - self._h2_port = ports["h2proxys"] - self._h1_port = ports["proxys"] + self._port = ports["h2o_h3proxys"] + self._h2_port = ports["h2o_h2proxys"] + self._h1_port = ports["h2o_proxys"] if self.start(): self.env.update_ports(ports) return True @@ -331,9 +337,9 @@ class H2oProxy(H2o): return alloc_ports_and_do( { - "h3proxys": socket.SOCK_DGRAM, - "h2proxys": socket.SOCK_STREAM, - "proxys": socket.SOCK_STREAM, + "h2o_h3proxys": socket.SOCK_DGRAM, + "h2o_h2proxys": socket.SOCK_STREAM, + "h2o_proxys": socket.SOCK_STREAM, }, startup, self.env.gen_root, diff --git a/tests/http/testenv/nghttpx.py b/tests/http/testenv/nghttpx.py index 0d95a34bce..c72a7f7f6d 100644 --- a/tests/http/testenv/nghttpx.py +++ b/tests/http/testenv/nghttpx.py @@ -47,7 +47,7 @@ class Nghttpx: self._name = name self._domain = domain self._port = 0 - self._https_port = 0 + self._port_is_quic = False self._cmd = env.nghttpx self._run_dir = os.path.join(env.gen_dir, name) self._pid_file = os.path.join(self._run_dir, 'nghttpx.pid') @@ -76,8 +76,12 @@ class Nghttpx: return self.reload() @property - def https_port(self): - return self._https_port + def port(self): + return self._port + + @property + def port_is_quic(self): + return self._port_is_quic def exists(self): return self._cmd and os.path.exists(self._cmd) @@ -150,18 +154,14 @@ class Nghttpx: curl = CurlClient(env=self.env, run_dir=self._tmp_dir) try_until = datetime.now() + timeout while datetime.now() < try_until: - if self._https_port > 0: - check_url = f'https://{self._domain}:{self._port}/' - r = curl.http_get(url=check_url, extra_args=[ - '--trace', 'curl.trace', '--trace-time', - '--connect-timeout', '1' - ]) - else: - check_url = f'https://{self._domain}:{self._port}/' - r = curl.http_get(url=check_url, extra_args=[ - '--trace', 'curl.trace', '--trace-time', - '--http3-only', '--connect-timeout', '1' - ]) + xargs = [ + '--trace', 'curl.trace', '--trace-time', + '--connect-timeout', '1' + ] + if self.port_is_quic: + xargs.extend(['--http3-only']) + check_url = f'https://{self._domain}:{self.port}/' + r = curl.http_get(url=check_url, extra_args=xargs) if r.exit_code != 0: return True log.debug(f'waiting for nghttpx to stop responding: {r}') @@ -173,18 +173,14 @@ class Nghttpx: curl = CurlClient(env=self.env, run_dir=self._tmp_dir) try_until = datetime.now() + timeout while datetime.now() < try_until: - if self._https_port > 0: - check_url = f'https://{self._domain}:{self._port}/' - r = curl.http_get(url=check_url, extra_args=[ - '--trace', 'curl.trace', '--trace-time', - '--connect-timeout', '1' - ]) - else: - check_url = f'https://{self._domain}:{self._port}/' - r = curl.http_get(url=check_url, extra_args=[ - '--http3-only', '--trace', 'curl.trace', '--trace-time', - '--connect-timeout', '1' - ]) + xargs = [ + '--trace', 'curl.trace', '--trace-time', + '--connect-timeout', '1' + ] + if self.port_is_quic: + xargs.extend(['--http3-only']) + check_url = f'https://{self._domain}:{self.port}/' + r = curl.http_get(url=check_url, extra_args=xargs) if r.exit_code == 0: return True time.sleep(.1) @@ -216,13 +212,18 @@ class NghttpxQuic(Nghttpx): def __init__(self, env: Env): super().__init__(env=env, name='nghttpx-quic', domain=env.domain1, cred_name=env.domain1) - self._https_port = env.https_port + self._https_port = 0 def initial_start(self): super().initial_start() def startup(ports: Dict[str, int]) -> bool: - self._port = ports['nghttpx_https'] + self._https_port = ports['nghttpx_https'] + if self.supports_h3(): + self._port = self.env.h3_port + self._port_is_quic = True + else: + self._port = self._https_port if self.start(): self.env.update_ports(ports) return True @@ -240,10 +241,10 @@ class NghttpxQuic(Nghttpx): creds = self.env.get_credentials(self._cred_name) assert creds # convince pytype this is not None self._loaded_cred_name = self._cred_name - args = [self._cmd, f'--frontend=*,{self._port};tls'] + args = [self._cmd, f'--frontend=*,{self._https_port};tls'] if self.supports_h3(): args.extend([ - f'--frontend=*,{self.env.h3_port};quic', + f'--frontend=*,{self._port};quic', '--frontend-quic-early-data', ]) args.extend([ diff --git a/tests/unit/unit2600.c b/tests/unit/unit2600.c index 39bec58560..47d91ce553 100644 --- a/tests/unit/unit2600.c +++ b/tests/unit/unit2600.c @@ -113,7 +113,8 @@ static int test_idx; struct cf_test_ctx { int idx; int ai_family; - uint8_t transport; + uint8_t transport_in; + uint8_t transport_out; char id[16]; struct curltime started; timediff_t fail_delay_ms; @@ -167,7 +168,8 @@ static CURLcode cf_test_create(struct Curl_cfilter **pcf, struct Curl_easy *data, struct connectdata *conn, struct Curl_sockaddr_ex *addr, - uint8_t transport) + uint8_t transport_in, + uint8_t transport_out) { static const struct Curl_cftype cft_test = { "TEST", @@ -201,7 +203,8 @@ static CURLcode cf_test_create(struct Curl_cfilter **pcf, } ctx->idx = test_idx++; ctx->ai_family = addr->family; - ctx->transport = transport; + ctx->transport_in = transport_in; + ctx->transport_out = transport_out; ctx->started = curlx_now(); current_tr->ongoing++; if(current_tr->ongoing > current_tr->max_concurrent)