]> git.ipfire.org Git - thirdparty/curl.git/commitdiff
h3-proxy: fixes around H3 proxy
authorStefan Eissing <stefan@eissing.org>
Wed, 27 May 2026 14:50:18 +0000 (16:50 +0200)
committerViktor Szakats <commit@vsz.me>
Thu, 28 May 2026 12:41:27 +0000 (14:41 +0200)
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

25 files changed:
lib/cf-capsule.c
lib/cf-capsule.h
lib/cf-h2-proxy.c
lib/cf-h3-proxy.c
lib/cf-h3-proxy.h
lib/cf-ip-happy.c
lib/cf-ip-happy.h
lib/cf-socket.c
lib/cf-socket.h
lib/cfilters.c
lib/connect.c
lib/http_proxy.c
lib/http_proxy.h
lib/url.c
lib/vquic/curl_ngtcp2.c
lib/vquic/curl_quiche.c
lib/vquic/vquic.c
lib/vquic/vquic.h
tests/http/conftest.py
tests/http/test_60_h3_proxy.py
tests/http/testenv/curl.py
tests/http/testenv/env.py
tests/http/testenv/h2o.py
tests/http/testenv/nghttpx.py
tests/unit/unit2600.c

index 55a550954c4afa93cc6fd0b38b8f393215728788..333a8d7efe0dd944a896f9917149abae73d8a1a1 100644 (file)
@@ -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 */
index 437c9681b6ccecde8b8555e3135584985fa02798..e45983543a6834a477cc50ff48b0764d030776a1 100644 (file)
 
 #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). */
index 5eaa9571e646388565a922aae83485fd05ac3962..316ed6c75a8a2a0d623a41226f35691ea0af2509 100644 (file)
@@ -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:
index b3706034935181ff55941665f04f0d043e2c8f1f..5ca18ace838311103508a29b01de736f5870e443 100644 (file)
@@ -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,
index b2f16acc0eebc9f1d6adbe9a71b54680cc07ae6e..40f0fccf0698c6652e8dcf862363328a8e659747 100644 (file)
@@ -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
index 965415d4585051517118aee2a1343d69d1118a3b..cfada937c86b5c3098974c9ac7c2da046fe1ad13 100644 (file)
@@ -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);
-  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);
+  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;
 }
-#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
index 970ec248818b6d9488825e7894e90e932669c6d4..90cecae8894ab6aecc8d338ffac699e84a7bb719 100644 (file)
@@ -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)
index eb782b65dca6e9c0d14e873e542958f000582b21..729f8748bfc9afc44570742ef1605befa3f505ea 100644 (file)
@@ -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;
 
index 40c001cc14fd3ddb743e18fc962e2534edc26bc4..9c1f3bf4b4c04732a766e19b6b90d7a4b30011aa 100644 (file)
@@ -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.
index 3946c7231c3eac4246d856785f58c02eb281ce41..46c17c199d99055c24cc1341627866436b7c7e57 100644 (file)
@@ -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;
index c2038f4ee4dd14fc76cc17e4057433af198bd0fb..0ed7b22a5b48e41c1e699b6b449a6559ae672069 100644 (file)
@@ -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);
index a52a1c3713dcb5548fd7ac12e6b919648b29deac..39cfb12446818d324fd483625f6b3786d5085d7a 100644 (file)
@@ -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 */
index 0a5734e3d8a24db746eb6907930162344022d805..ef4becdacf98a6aa5f2acebd4d83ab85f1813ad4 100644 (file)
@@ -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 */
index 926d29ed5f77e0e9010aac0cdb9fad2e23d1af99..b7ba30fe2bb1e49a885f6b3e4d339753a3ce4784 100644 (file)
--- 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]) {
index 8693ed16ee749f9ca48696454a8edc486d3905a7..20996e5a9823b34b4e7f68de17039b95e6371664 100644 (file)
@@ -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;
index 43a16958a6ffb5fd0f48a3a068a7592afaecf1e6..08b02fec78f6bb549a1f6ae68e02eb7bef207eee 100644 (file)
@@ -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;
index a35abfb2c97ad3eb955aebdddbc3dc9862109700..dba907bbf090ebf7cda9e646d8bb8f20b2191d0a 100644 (file)
@@ -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)
index 59178acd940572bcdd2c015bd24275fec21ad2a5..e3d894f8c089aa282761dbf9bfaa9b8fc9006e42 100644 (file)
@@ -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;
 
index 5275b91bf969298b4475a071b236909b1d12c249..225d63fe6d1995486f14687bf9309202366c83c2 100644 (file)
@@ -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
index ca4501f6352d3d9f805c83488a6f34aaf0ff462b..34d628445d54012df647652b48f5ba7563b7e1cf 100644 (file)
@@ -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],
index 272b6045cbf80e523a8d2c77f626fd6adb655e22..5149a8578d7dd3e88137f500faea87bd38b00e73 100644 (file)
@@ -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:
index 093092b4c5dea229f196c53c71479aff7b54362e..4a18d65ffab68776f23c343578b402c36cb19dc6 100644 (file)
@@ -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:
index c67aaf18886cd1df445d73d55e675e2c3809df9a..279cff8b90c3b2cb0dbde2ac9ef81ec56973c40e 100644 (file)
@@ -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,
index 0d95a34bceb9954698edb875ca2e50539d5e0924..c72a7f7f6de302eef7c8f063ddc360b3e3be64b0 100644 (file)
@@ -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([
index 39bec585607f6798361a70581137db5e8a5559d7..47d91ce5530b85dcba363e9afb225f3d45747e43 100644 (file)
@@ -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)