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

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

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

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

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

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

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

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

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

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

View file

@ -150,8 +150,11 @@ LIB_CFILES = \
bufq.c \
bufref.c \
cf-dns.c \
capsule.c \
cf-capsule.c \
cf-h1-proxy.c \
cf-h2-proxy.c \
cf-h3-proxy.c \
cf-haproxy.c \
cf-https-connect.c \
cf-ip-happy.c \
@ -282,8 +285,11 @@ LIB_HFILES = \
bufq.h \
bufref.h \
cf-dns.h \
capsule.h \
cf-capsule.h \
cf-h1-proxy.h \
cf-h2-proxy.h \
cf-h3-proxy.h \
cf-haproxy.h \
cf-https-connect.h \
cf-ip-happy.h \

281
lib/capsule.c Normal file
View file

@ -0,0 +1,281 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h"
#include "curlx/dynbuf.h"
#include "cfilters.h"
#include "curl_trc.h"
#include "bufq.h"
#include "capsule.h"
/**
* Convert 64-bit value from network byte order to host byte order
*/
static uint64_t capsule_ntohll(uint64_t value)
{
#if defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_BIG_ENDIAN__)
return value;
#elif (defined(__GNUC__) || defined(__clang__)) && \
defined(__BYTE_ORDER__) && (__BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__)
return __builtin_bswap64(value);
#else
union {
uint64_t u64;
uint32_t u32[2];
} src, dst;
src.u64 = value;
dst.u32[0] = ntohl(src.u32[1]);
dst.u32[1] = ntohl(src.u32[0]);
return dst.u64;
#endif
}
/**
* Encode a variable-length integer into a plain buffer.
* @param buf Output buffer (must have at least 8 bytes)
* @param value Value to encode (must be <= 0x3FFFFFFFFFFFFFFF)
* @return Number of bytes written
*/
static size_t capsule_encode_varint_buf(uint8_t *buf, uint64_t value)
{
DEBUGASSERT(value <= 0x3FFFFFFFFFFFFFFF);
if(value <= 0x3F) {
buf[0] = (uint8_t)value;
return 1;
}
else if(value <= 0x3FFF) {
uint16_t encoded = (uint16_t)value & 0x3FFF;
encoded = ntohs(encoded | 0x4000);
memcpy(buf, &encoded, 2);
return 2;
}
else if(value <= 0x3FFFFFFF) {
uint32_t encoded = (uint32_t)value & 0x3FFFFFFF;
encoded = ntohl(encoded | 0x80000000);
memcpy(buf, &encoded, 4);
return 4;
}
else {
uint64_t encoded = (uint64_t)value & 0x3FFFFFFFFFFFFFFF;
encoded = capsule_ntohll(encoded | 0xC000000000000000);
memcpy(buf, &encoded, 8);
return 8;
}
}
static CURLcode capsule_peek_u8(struct bufq *recvbufq,
size_t offset,
uint8_t *pbyte)
{
const unsigned char *peek = NULL;
size_t peeklen = 0;
if(!Curl_bufq_peek_at(recvbufq, offset, &peek, &peeklen) || !peeklen)
return CURLE_AGAIN;
*pbyte = peek[0];
return CURLE_OK;
}
static CURLcode capsule_decode_varint_at(struct bufq *recvbufq,
size_t offset,
uint64_t *pvalue,
size_t *pconsumed)
{
uint8_t first_byte, byte;
uint64_t value;
size_t nbytes;
size_t i;
CURLcode result;
result = capsule_peek_u8(recvbufq, offset, &first_byte);
if(result)
return result;
nbytes = (size_t)1 << (first_byte >> 6); /* 1, 2, 4 or 8 bytes */
value = first_byte & 0x3F;
for(i = 1; i < nbytes; ++i) {
result = capsule_peek_u8(recvbufq, offset + i, &byte);
if(result)
return result;
value = (value << 8) | byte;
}
*pvalue = value;
*pconsumed = nbytes;
return CURLE_OK;
}
size_t Curl_capsule_encap_udp_hdr(uint8_t *hdr, size_t hdrlen,
size_t payload_len)
{
size_t off = 0;
DEBUGASSERT(hdrlen >= HTTP_CAPSULE_HEADER_MAX_SIZE);
if(hdrlen < HTTP_CAPSULE_HEADER_MAX_SIZE)
return 0;
hdr[off++] = 0; /* capsule type: HTTP Datagram */
off += capsule_encode_varint_buf(hdr + off, (uint64_t)payload_len + 1);
hdr[off++] = 0; /* context ID */
return off;
}
CURLcode Curl_capsule_encap_udp_datagram(struct dynbuf *dyn,
const void *buf, size_t blen)
{
CURLcode result;
uint8_t hdr[HTTP_CAPSULE_HEADER_MAX_SIZE];
size_t hdr_len;
curlx_dyn_init(dyn, HTTP_CAPSULE_HEADER_MAX_SIZE + blen);
hdr_len = Curl_capsule_encap_udp_hdr(hdr, sizeof(hdr), blen);
DEBUGASSERT(hdr_len);
if(!hdr_len)
return CURLE_FAILED_INIT;
result = curlx_dyn_addn(dyn, hdr, hdr_len);
if(result)
return result;
return curlx_dyn_addn(dyn, buf, blen);
}
size_t Curl_capsule_process_udp_raw(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct bufq *recvbufq,
unsigned char *buf, size_t len,
CURLcode *err)
{
const unsigned char *context_id, *capsule_type;
size_t read_size, varint_len;
uint64_t capsule_length;
size_t offset, payload_len;
size_t bytes_read = 0;
CURLcode result = CURLE_OK;
if(!len) {
*err = CURLE_BAD_FUNCTION_ARGUMENT;
return 0;
}
if(Curl_bufq_is_empty(recvbufq)) {
*err = CURLE_AGAIN;
return 0;
}
if(!Curl_bufq_peek(recvbufq, &capsule_type, &read_size) || !read_size) {
*err = CURLE_AGAIN;
return 0;
}
if(capsule_type[0]) {
infof(data, "Error! Invalid capsule type: %d", capsule_type[0]);
Curl_bufq_skip(recvbufq, 1);
*err = CURLE_RECV_ERROR;
return 0;
}
offset = 1;
result = capsule_decode_varint_at(recvbufq, offset, &capsule_length,
&varint_len);
if(result == CURLE_AGAIN) {
*err = CURLE_AGAIN;
return 0;
}
else if(result) {
*err = CURLE_RECV_ERROR;
return 0;
}
offset += varint_len;
if(!Curl_bufq_peek_at(recvbufq, offset, &context_id, &read_size) ||
!read_size) {
*err = CURLE_AGAIN;
return 0;
}
if(*context_id) {
infof(data, "Error! Invalid context ID: %02x", *context_id);
Curl_bufq_skip(recvbufq, offset + 1);
*err = CURLE_RECV_ERROR;
return 0;
}
offset += 1;
if(!capsule_length) {
infof(data, "Error! Invalid capsule length: 0");
Curl_bufq_skip(recvbufq, offset);
*err = CURLE_RECV_ERROR;
return 0;
}
if(capsule_length - 1 >= (uint64_t)SIZE_MAX) {
infof(data, "Error! Capsule length too large: %" CURL_FORMAT_CURL_OFF_T,
(curl_off_t)capsule_length);
*err = CURLE_RECV_ERROR;
return 0;
}
payload_len = (size_t)(capsule_length - 1);
if(Curl_bufq_len(recvbufq) < offset + payload_len) {
*err = CURLE_AGAIN;
return 0;
}
if(payload_len > len) {
infof(data, "UDP payload does not fit destination buffer: %zu > %zu",
payload_len, len);
Curl_bufq_skip(recvbufq, offset + payload_len);
*err = CURLE_RECV_ERROR;
return 0;
}
Curl_bufq_skip(recvbufq, offset);
if(!payload_len) {
*err = CURLE_OK;
return 0;
}
result = Curl_bufq_read(recvbufq, buf, payload_len, &bytes_read);
if(result || (bytes_read != payload_len)) {
infof(data, "Error! Read less than expected %zu %zu",
payload_len, bytes_read);
*err = CURLE_RECV_ERROR;
return 0;
}
if(cf && data) {
CURL_TRC_CF(data, cf, "Processed UDP capsule raw: size=%zu "
"length_left %zu", payload_len, Curl_bufq_len(recvbufq));
}
*err = CURLE_OK;
return bytes_read;
}
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

77
lib/capsule.h Normal file
View file

@ -0,0 +1,77 @@
#ifndef HEADER_CURL_CAPSULE_H
#define HEADER_CURL_CAPSULE_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include "curlx/dynbuf.h"
#include "bufq.h"
/* HTTP Capsule constants */
#define HTTP_CAPSULE_HEADER_MAX_SIZE 10
/* HTTP Capsule function prototypes */
/**
* Write the capsule header (type + varint length + context ID) into `hdr`.
* @param hdr Output buffer (must be >= HTTP_CAPSULE_HEADER_MAX_SIZE)
* @param hdrlen Size of `hdr` in bytes
* @param payload_len Length of the UDP payload that follows
* @return Number of header bytes written, or 0 on error
*/
size_t Curl_capsule_encap_udp_hdr(uint8_t *hdr, size_t hdrlen,
size_t payload_len);
/**
* Encapsulate UDP payload into HTTP Datagram capsule format
* @param dyn Dynamic buffer to write capsule to
* @param buf Payload buffer
* @param blen Payload buffer length
* @return CURLE_OK on success, error code on failure
*/
CURLcode Curl_capsule_encap_udp_datagram(struct dynbuf *dyn,
const void *buf, size_t blen);
/**
* Process one UDP capsule from buffer into raw datagram payload bytes.
* @param cf Connection filter
* @param data Easy handle
* @param recvbufq Buffer queue containing capsule data
* @param buf Output buffer for one datagram payload
* @param len Size of output buffer in bytes
* @param err Error code output
* @return Number of payload bytes written. Check `err` for status.
*/
size_t Curl_capsule_process_udp_raw(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct bufq *recvbufq,
unsigned char *buf, size_t len,
CURLcode *err);
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#endif /* HEADER_CURL_CAPSULE_H */

253
lib/cf-capsule.c Normal file
View file

@ -0,0 +1,253 @@
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
#include <curl/curl.h>
#include "urldata.h"
#include "cfilters.h"
#include "curl_trc.h"
#include "curlx/dynbuf.h"
#include "bufq.h"
#include "capsule.h"
#include "cf-capsule.h"
/* recv buffer: 4 chunks of 16KB = 64KB, enough for large datagrams */
#define CAPSULE_RECV_CHUNKS 4
#define CAPSULE_CHUNK_SIZE (16 * 1024)
struct cf_capsule_ctx {
struct bufq recvbuf;
struct cf_call_data call_data;
unsigned char *pending; /* unsent capsule bytes from partial write */
size_t pending_len; /* total length of pending buffer */
size_t pending_offset; /* bytes already sent from pending */
size_t pending_payload; /* original payload len for pending capsule */
};
static void capsule_cf_destroy(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_capsule_ctx *ctx = cf->ctx;
(void)data;
if(ctx) {
Curl_bufq_free(&ctx->recvbuf);
curlx_free(ctx->pending);
curlx_safefree(ctx);
}
}
static void capsule_cf_close(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
struct cf_capsule_ctx *ctx = cf->ctx;
CURL_TRC_CF(data, cf, "close");
cf->connected = FALSE;
if(ctx) {
Curl_bufq_reset(&ctx->recvbuf);
curlx_safefree(ctx->pending);
ctx->pending_len = 0;
ctx->pending_offset = 0;
ctx->pending_payload = 0;
}
if(cf->next)
cf->next->cft->do_close(cf->next, data);
}
static CURLcode capsule_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
if(cf->next) {
CURLcode result = cf->next->cft->do_connect(cf->next, data, done);
if(!result && *done)
cf->connected = TRUE;
return result;
}
*done = FALSE;
return CURLE_OK;
}
static CURLcode capsule_cf_send(struct Curl_cfilter *cf,
struct Curl_easy *data,
const uint8_t *buf, size_t len,
bool eos, size_t *pnwritten)
{
struct cf_capsule_ctx *ctx = cf->ctx;
struct dynbuf dyn;
size_t nwritten = 0;
size_t capsule_len;
size_t remaining;
CURLcode result;
(void)eos;
*pnwritten = 0;
if(ctx->pending) {
/* flush remaining bytes from a partially sent capsule */
remaining = ctx->pending_len - ctx->pending_offset;
result = Curl_conn_cf_send(cf->next, data,
ctx->pending + ctx->pending_offset,
remaining, FALSE, &nwritten);
if(result && result != CURLE_AGAIN) {
curlx_safefree(ctx->pending);
return result;
}
ctx->pending_offset += nwritten;
if(ctx->pending_offset < ctx->pending_len)
return CURLE_AGAIN;
/* pending capsule has been fully flusehd */
*pnwritten = ctx->pending_payload;
curlx_safefree(ctx->pending);
return CURLE_OK;
}
/* encapsulate new payload into a capsule */
result = Curl_capsule_encap_udp_datagram(&dyn, buf, len);
if(result) {
curlx_dyn_free(&dyn);
return result;
}
capsule_len = curlx_dyn_len(&dyn);
result = Curl_conn_cf_send(cf->next, data,
(const uint8_t *)curlx_dyn_ptr(&dyn),
capsule_len, FALSE, &nwritten);
if(result && result != CURLE_AGAIN) {
curlx_dyn_free(&dyn);
return result;
}
if(nwritten < capsule_len) {
/* partial or zero write - save unsent capsule bytes as pending */
remaining = capsule_len - nwritten;
ctx->pending = curlx_malloc(remaining);
if(!ctx->pending) {
curlx_dyn_free(&dyn);
return CURLE_OUT_OF_MEMORY;
}
memcpy(ctx->pending,
curlx_dyn_ptr(&dyn) + nwritten, remaining);
ctx->pending_len = remaining;
ctx->pending_offset = 0;
ctx->pending_payload = len;
curlx_dyn_free(&dyn);
return CURLE_AGAIN;
}
/* entire capsule sent */
curlx_dyn_free(&dyn);
*pnwritten = len;
return CURLE_OK;
}
static CURLcode capsule_cf_recv(struct Curl_cfilter *cf,
struct Curl_easy *data,
char *buf, size_t len,
size_t *pnread)
{
struct cf_capsule_ctx *ctx = cf->ctx;
CURLcode result;
size_t nread;
*pnread = 0;
/* fill our receive buffer from the filter below */
while(!Curl_bufq_is_full(&ctx->recvbuf)) {
result = Curl_cf_recv_bufq(cf->next, data, &ctx->recvbuf, 0, &nread);
if(result == CURLE_AGAIN)
break;
if(result)
return result;
if(!nread)
break;
}
/* try to extract a complete capsule datagram */
*pnread = Curl_capsule_process_udp_raw(cf, data, &ctx->recvbuf,
(unsigned char *)buf, len,
&result);
return result;
}
static bool capsule_cf_data_pending(struct Curl_cfilter *cf,
const struct Curl_easy *data)
{
struct cf_capsule_ctx *ctx = cf->ctx;
if(ctx && !Curl_bufq_is_empty(&ctx->recvbuf))
return TRUE;
return cf->next ? cf->next->cft->has_data_pending(cf->next, data) : FALSE;
}
struct Curl_cftype Curl_cft_capsule = {
"CAPSULE",
0,
0,
capsule_cf_destroy,
capsule_cf_connect,
capsule_cf_close,
Curl_cf_def_shutdown,
Curl_cf_def_adjust_pollset,
capsule_cf_data_pending,
capsule_cf_send,
capsule_cf_recv,
Curl_cf_def_cntrl,
Curl_cf_def_conn_is_alive,
Curl_cf_def_conn_keep_alive,
Curl_cf_def_query,
};
CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data)
{
struct Curl_cfilter *cf;
struct cf_capsule_ctx *ctx;
CURLcode result;
(void)data;
ctx = curlx_calloc(1, sizeof(*ctx));
if(!ctx)
return CURLE_OUT_OF_MEMORY;
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) {
Curl_bufq_free(&ctx->recvbuf);
curlx_free(ctx);
return result;
}
Curl_conn_cf_insert_after(cf_at, cf);
return CURLE_OK;
}
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */

40
lib/cf-capsule.h Normal file
View file

@ -0,0 +1,40 @@
#ifndef HEADER_CURL_CF_CAPSULE_H
#define HEADER_CURL_CF_CAPSULE_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_PROXY) && !defined(CURL_DISABLE_HTTP)
/* 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). */
CURLcode Curl_cf_capsule_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data);
extern struct Curl_cftype Curl_cft_capsule;
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#endif /* HEADER_CURL_CF_CAPSULE_H */

View file

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

View file

@ -32,7 +32,8 @@ struct Curl_peer;
CURLcode Curl_cf_h1_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
int httpversion);
int httpversion,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_h1_proxy;

View file

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

View file

@ -29,7 +29,8 @@
CURLcode Curl_cf_h2_proxy_insert_after(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest);
struct Curl_peer *dest,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_h2_proxy;

3478
lib/cf-h3-proxy.c Normal file

File diff suppressed because it is too large Load diff

42
lib/cf-h3-proxy.h Normal file
View file

@ -0,0 +1,42 @@
#ifndef HEADER_CURL_H3_PROXY_H
#define HEADER_CURL_H3_PROXY_H
/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
*
* This software is licensed as described in the file COPYING, which
* you should have received as part of this distribution. The terms
* are also available at https://curl.se/docs/copyright.html.
*
* You may opt to use, copy, modify, merge, publish, distribute and/or sell
* copies of the Software, and permit persons to whom the Software is
* furnished to do so, under the terms of the COPYING file.
*
* This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
* KIND, either express or implied.
*
* SPDX-License-Identifier: curl
*
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_PROXY) && \
defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \
defined(USE_NGTCP2) && defined(USE_OPENSSL)
CURLcode Curl_cf_h3_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_h3_proxy;
#endif
#endif /* HEADER_CURL_H3_PROXY_H */

View file

@ -1023,3 +1023,28 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
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 */

View file

@ -52,6 +52,15 @@ CURLcode cf_ip_happy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
uint8_t transport);
#if !defined(CURL_DISABLE_HTTP) && defined(USE_HTTP3) && \
defined(USE_PROXY_HTTP3)
/* For H3 proxy: create happy eyeballs that races IPv4/IPv6 using raw UDP
sockets with TRNSPRT_QUIC transport so the socket is connected to the
proxy peer. H3-PROXY manages its own ngtcp2 QUIC stack on top. */
CURLcode cf_ip_happy_quic_udp_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data);
#endif /* !CURL_DISABLE_HTTP && USE_HTTP3 && USE_PROXY_HTTP3 */
extern struct Curl_cftype Curl_cft_ip_happy;
#endif /* HEADER_CURL_IP_HAPPY_H */

View file

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

View file

@ -712,6 +712,9 @@ ${SIZEOF_TIME_T_CODE}
/* if libuv is in use */
#cmakedefine USE_LIBUV 1
/* if HTTP/3 proxy support is available */
#cmakedefine USE_PROXY_HTTP3 1
/* Define to 1 if you have the <uv.h> header file. */
#cmakedefine HAVE_UV_H 1

View file

@ -35,6 +35,7 @@
#include "http_proxy.h"
#include "cf-h1-proxy.h"
#include "cf-h2-proxy.h"
#include "cf-h3-proxy.h"
#include "cf-haproxy.h"
#include "cf-https-connect.h"
#include "cf-ip-happy.h"
@ -578,6 +579,9 @@ static struct trc_cft_def trc_cfts[] = {
{ &Curl_cft_h1_proxy, TRC_CT_PROXY },
#ifdef USE_NGHTTP2
{ &Curl_cft_h2_proxy, TRC_CT_PROXY },
#endif
#if defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3)
{ &Curl_cft_h3_proxy, TRC_CT_PROXY },
#endif
{ &Curl_cft_http_proxy, TRC_CT_PROXY },
#endif /* !CURL_DISABLE_HTTP */

View file

@ -1757,6 +1757,12 @@ CURLcode Curl_add_custom_headers(struct Curl_easy *data,
else
h[0] = data->set.headers;
break;
case HEADER_CONNECT_UDP:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
}
#else
(void)is_connect;
@ -2721,7 +2727,11 @@ static CURLcode http_check_new_conn(struct Curl_easy *data)
alpn = Curl_conn_get_alpn_negotiated(data, conn);
if(alpn && !strcmp("h3", alpn)) {
DEBUGASSERT(Curl_conn_http_version(data, conn) == 30);
#ifndef CURL_DISABLE_PROXY
if((Curl_conn_http_version(data, conn) == 30) || !conn->bits.proxy ||
conn->bits.tunnel_proxy)
#endif
DEBUGASSERT(Curl_conn_http_version(data, conn) == 30);
info_version = "HTTP/3";
}
else if(alpn && !strcmp("h2", alpn)) {
@ -4847,7 +4857,6 @@ struct name_const {
size_t namelen;
};
/* keep them sorted by length! */
static const struct name_const H2_NON_FIELD[] = {
{ STRCONST("Host") },
{ STRCONST("Upgrade") },
@ -4861,10 +4870,8 @@ static bool h2_permissible_field(struct dynhds_entry *e)
{
size_t i;
for(i = 0; i < CURL_ARRAYSIZE(H2_NON_FIELD); ++i) {
if(e->namelen < H2_NON_FIELD[i].namelen)
return TRUE;
if(e->namelen == H2_NON_FIELD[i].namelen &&
curl_strequal(H2_NON_FIELD[i].name, e->name))
curl_strnequal(H2_NON_FIELD[i].name, e->name, e->namelen))
return FALSE;
}
return TRUE;

View file

@ -83,8 +83,6 @@ char *Curl_checkProxyheaders(struct Curl_easy *data,
CURLcode Curl_add_timecondition(struct Curl_easy *data, struct dynbuf *req);
CURLcode Curl_add_custom_headers(struct Curl_easy *data, bool is_connect,
int httpversion, struct dynbuf *req);
CURLcode Curl_dynhds_add_custom(struct Curl_easy *data, bool is_connect,
struct dynhds *hds);
void Curl_http_to_fold(struct dynbuf *bf);

View file

@ -3021,3 +3021,6 @@ char *curl_pushheader_byname(struct curl_pushheaders *h, const char *name)
}
#endif /* !CURL_DISABLE_HTTP && USE_NGHTTP2 */
/* Do not leak this filter's call_data accessor in unity builds. */
#undef CF_CTX_CALL_DATA

View file

@ -33,13 +33,15 @@
#include "cfilters.h"
#include "cf-h1-proxy.h"
#include "cf-h2-proxy.h"
#include "cf-h3-proxy.h"
#include "cf-capsule.h"
#include "connect.h"
#include "vauth/vauth.h"
#include "curlx/strparse.h"
static CURLcode dynhds_add_custom(struct Curl_easy *data,
bool is_connect, int httpversion,
struct dynhds *hds)
bool is_udp, struct dynhds *hds)
{
struct connectdata *conn = data->conn;
struct curl_slist *h[2];
@ -49,10 +51,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data,
enum Curl_proxy_use proxy;
if(is_connect)
if(is_connect && !is_udp)
proxy = HEADER_CONNECT;
else if(is_connect && is_udp)
proxy = HEADER_CONNECT_UDP;
else
proxy = conn->bits.httpproxy && !conn->bits.tunnel_proxy ?
proxy = (conn->bits.httpproxy && !conn->bits.tunnel_proxy) ?
HEADER_PROXY : HEADER_SERVER;
switch(proxy) {
@ -72,6 +76,12 @@ static CURLcode dynhds_add_custom(struct Curl_easy *data,
else
h[0] = data->set.headers;
break;
case HEADER_CONNECT_UDP:
if(data->set.sep_headers)
h[0] = data->set.proxyheaders;
else
h[0] = data->set.headers;
break;
}
/* loop through one or two lists */
@ -166,15 +176,30 @@ struct cf_proxy_ctx {
struct Curl_peer *dest; /* tunnel destination */
uint8_t proxytype;
BIT(sub_filter_installed);
BIT(udp_tunnel);
};
static int proxy_http_ver_major(proxy_http_ver ver)
{
switch(ver) {
case PROXY_HTTP_V1:
return 11;
case PROXY_HTTP_V2:
return 20;
case PROXY_HTTP_V3:
return 30;
}
return 0;
}
CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
int httpversion)
proxy_http_ver ver)
{
char *authority = NULL;
int httpversion = proxy_http_ver_major(ver);
CURLcode result;
struct httpreq *req = NULL;
@ -201,7 +226,7 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
goto out;
/* If user is not overriding Host: header, we add for HTTP/1.x */
if(httpversion < 20 &&
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result)
@ -223,14 +248,15 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
goto out;
}
if(httpversion < 20 &&
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result)
goto out;
}
result = dynhds_add_custom(data, TRUE, httpversion, &req->headers);
result = dynhds_add_custom(data, TRUE, httpversion,
FALSE, &req->headers);
out:
if(result && req) {
@ -242,33 +268,327 @@ out:
return result;
}
CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
proxy_http_ver ver)
{
const char *proxy_scheme = "http";
const char *proxy_host = cf->conn->http_proxy.peer->hostname;
int httpversion = proxy_http_ver_major(ver);
char *authority = NULL;
char *path = NULL;
char *encoded_host = NULL;
struct httpreq *req = NULL;
bool proxy_ipv6_ip;
CURLcode result;
if(cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS ||
cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS2 ||
cf->conn->http_proxy.proxytype == CURLPROXY_HTTPS3)
proxy_scheme = "https";
proxy_ipv6_ip = cf->conn->http_proxy.peer->ipv6 != 0;
authority = curl_maprintf("%s%s%s:%d",
proxy_ipv6_ip ? "[" : "",
proxy_host,
proxy_ipv6_ip ? "]" : "",
cf->conn->http_proxy.peer->port);
if(!authority) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(dest->ipv6) {
/* RFC 9298: colons in IPv6 addresses MUST be percent-encoded
* in the URI template (e.g. "2001:db8::1" -> "2001%3Adb8%3A%3A1") */
const char *s = dest->hostname;
char *d;
size_t hlen = strlen(s);
encoded_host = curlx_malloc(hlen * 3 + 1);
if(!encoded_host) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
d = encoded_host;
while(*s) {
if(*s == ':') {
*d++ = '%';
*d++ = '3';
*d++ = 'A';
}
else
*d++ = *s;
s++;
}
*d = '\0';
path = curl_maprintf("/.well-known/masque/udp/%s/%u/",
encoded_host, (unsigned int)dest->port);
}
else {
path = curl_maprintf("/.well-known/masque/udp/%s/%u/",
dest->hostname, (unsigned int)dest->port);
}
if(!path) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
if(ver == PROXY_HTTP_V1) {
result = Curl_http_req_make(&req, "GET", sizeof("GET")-1,
proxy_scheme, strlen(proxy_scheme),
authority, strlen(authority),
path, strlen(path));
if(result)
goto out;
}
else if(ver == PROXY_HTTP_V2 || ver == PROXY_HTTP_V3) {
result = Curl_http_req_make(&req, "CONNECT", sizeof("CONNECT") - 1,
proxy_scheme, strlen(proxy_scheme),
authority, strlen(authority),
path, strlen(path));
if(result)
goto out;
}
else {
result = CURLE_FAILED_INIT;
goto out;
}
/* Setup the proxy-authorization header, if any */
result = Curl_http_output_auth(data, cf->conn, req->method, HTTPREQ_GET,
req->authority, NULL, TRUE);
if(result)
goto out;
/* If user is not overriding Host: header, we add for HTTP/1.x */
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Host"))) {
result = Curl_dynhds_cadd(&req->headers, "Host", authority);
if(result)
goto out;
}
if(data->req.hd_proxy_auth) {
result = Curl_dynhds_h1_cadd_line(&req->headers,
data->req.hd_proxy_auth);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("User-Agent")) &&
data->set.str[STRING_USERAGENT] && *data->set.str[STRING_USERAGENT]) {
result = Curl_dynhds_cadd(&req->headers, "User-Agent",
data->set.str[STRING_USERAGENT]);
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1 &&
!Curl_checkProxyheaders(data, cf->conn, STRCONST("Proxy-Connection"))) {
result = Curl_dynhds_cadd(&req->headers, "Proxy-Connection", "Keep-Alive");
if(result)
goto out;
}
if(ver == PROXY_HTTP_V1) {
result = Curl_dynhds_cadd(&req->headers, "Connection", "Upgrade");
if(result)
goto out;
result = Curl_dynhds_cadd(&req->headers, "Upgrade", "connect-udp");
if(result)
goto out;
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
else {
result = Curl_dynhds_cadd(&req->headers, ":Protocol", "connect-udp");
if(result)
goto out;
if(ver >= PROXY_HTTP_V2) {
result = Curl_dynhds_cadd(&req->headers, "Capsule-Protocol", "?1");
if(result)
goto out;
}
}
result = dynhds_add_custom(data, TRUE, httpversion,
TRUE, &req->headers);
out:
if(result && req) {
Curl_http_req_free(req);
req = NULL;
}
curlx_free(authority);
curlx_free(path);
curlx_free(encoded_host);
*preq = req;
return result;
}
CURLcode Curl_http_proxy_create_tunnel_request(
struct httpreq **preq, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_peer *dest,
proxy_http_ver ver, bool udp_tunnel)
{
CURLcode result;
if(udp_tunnel)
result = Curl_http_proxy_create_CONNECTUDP(preq, cf, data, dest, ver);
else
result = Curl_http_proxy_create_CONNECT(preq, cf, data, dest, ver);
if(result)
return result;
if(udp_tunnel)
infof(data, "Establishing %s proxy UDP tunnel to %s:%s",
(ver == PROXY_HTTP_V2) ? "HTTP/2" :
(ver == PROXY_HTTP_V3) ? "HTTP/3" : "HTTP",
data->state.up.hostname, data->state.up.port);
else
infof(data, "Establishing %s proxy tunnel to %s",
(ver == PROXY_HTTP_V2) ? "HTTP/2" :
(ver == PROXY_HTTP_V3) ? "HTTP/3" : "HTTP",
(*preq)->authority);
return CURLE_OK;
}
CURLcode Curl_http_proxy_inspect_tunnel_response(
struct Curl_cfilter *cf, struct Curl_easy *data,
struct http_resp *resp, bool udp_tunnel,
proxy_inspect_result *presult)
{
struct dynhds_entry *capsule_protocol = NULL;
struct dynhds_entry *auth_reply = NULL;
size_t i, header_count;
CURLcode result = CURLE_OK;
DEBUGASSERT(resp);
header_count = Curl_dynhds_count(&resp->headers);
if(udp_tunnel)
infof(data, "CONNECT-UDP Response Status %d", resp->status);
else
infof(data, "CONNECT Response Status %d", resp->status);
infof(data, "Response Headers (%zu total):", header_count);
for(i = 0; i < header_count; i++) {
struct dynhds_entry *entry = Curl_dynhds_getn(&resp->headers, i);
if(entry)
infof(data, " %s: %s", entry->name, entry->value);
}
if(resp->status == 401) {
auth_reply = Curl_dynhds_cget(&resp->headers, "WWW-Authenticate");
}
else if(resp->status == 407) {
auth_reply = Curl_dynhds_cget(&resp->headers, "Proxy-Authenticate");
}
if(auth_reply) {
CURL_TRC_CF(data, cf, "[0] CONNECT%s: fwd auth header '%s'",
udp_tunnel ? "-UDP" : "", auth_reply->value);
result = Curl_http_input_auth(data, resp->status == 407,
auth_reply->value);
if(result)
return result;
if(data->req.newurl) {
curlx_safefree(data->req.newurl);
*presult = PROXY_INSPECT_AUTH_RETRY;
return CURLE_OK;
}
}
if(udp_tunnel) {
if(resp->status / 100 == 2) {
capsule_protocol = Curl_dynhds_cget(&resp->headers,
"capsule-protocol");
if(capsule_protocol) {
if(strncmp(capsule_protocol->value, "?1", 2) == 0 &&
!capsule_protocol->value[2]) {
infof(data, "CONNECT-UDP tunnel established, response %d",
resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
failf(data, "Failed to establish CONNECT-UDP tunnel, response %d, "
"unsupported capsule-protocol value '%s'",
resp->status, capsule_protocol->value);
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
else {
/* NOTE proxies may not set capsule protocol in the headers */
infof(data, "CONNECT-UDP tunnel established, response %d "
"but no capsule-protocol header found", resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
}
else {
failf(data, "Failed to establish CONNECT-UDP tunnel, "
"response %d", resp->status);
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
}
if(resp->status / 100 == 2) {
infof(data, "CONNECT tunnel established, response %d", resp->status);
*presult = PROXY_INSPECT_OK;
return CURLE_OK;
}
*presult = PROXY_INSPECT_FAILED;
return CURLE_COULDNT_CONNECT;
}
static CURLcode http_proxy_cf_connect(struct Curl_cfilter *cf,
struct Curl_easy *data,
bool *done)
{
struct cf_proxy_ctx *ctx = cf->ctx;
CURLcode result;
const char *tunnel_type; /* Determine tunnel type once and reuse */
tunnel_type = ctx->udp_tunnel ? "CONNECT-UDP" : "CONNECT";
if(cf->connected) {
*done = TRUE;
return CURLE_OK;
}
CURL_TRC_CF(data, cf, "connect");
CURL_TRC_CF(data, cf, "%s", tunnel_type);
connect_sub:
result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
/* in case of h3_proxy, cf->next will be NULL initially */
if(cf->next) {
result = cf->next->cft->do_connect(cf->next, data, done);
if(result || !*done)
return result;
}
*done = FALSE;
if(!ctx->sub_filter_installed) {
const char *alpn = Curl_conn_cf_get_alpn_negotiated(cf->next, data);
const char *alpn = NULL;
/* in case of h3_proxy, cf->next will be NULL initially */
if(cf->next) {
alpn = Curl_conn_cf_get_alpn_negotiated(cf->next, data);
}
if(alpn)
infof(data, "CONNECT: '%s' negotiated", alpn);
infof(data, "%s: '%s' negotiated", tunnel_type, alpn);
else if(!alpn) {
/* No ALPN, proxytype rules. Fake ALPN */
infof(data, "CONNECT: no ALPN negotiated");
infof(data, "%s: no ALPN negotiated", tunnel_type);
switch(ctx->proxytype) {
case CURLPROXY_HTTP_1_0:
alpn = "http/1.0";
@ -276,6 +596,9 @@ connect_sub:
case CURLPROXY_HTTPS2:
alpn = "h2";
break;
case CURLPROXY_HTTPS3:
alpn = "h3";
break;
default:
alpn = "http/1.1";
break;
@ -284,7 +607,8 @@ connect_sub:
if(!strcmp(alpn, "http/1.0")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.0");
result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, 10);
result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, 10,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
@ -292,20 +616,32 @@ connect_sub:
int httpversion = (ctx->proxytype == CURLPROXY_HTTP_1_0) ? 10 : 11;
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/1.%d",
httpversion % 10);
result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, httpversion);
result = Curl_cf_h1_proxy_insert_after(cf, data, ctx->dest, httpversion,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#ifdef USE_NGHTTP2
else if(!strcmp(alpn, "h2")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/2");
result = Curl_cf_h2_proxy_insert_after(cf, data, ctx->dest);
result = Curl_cf_h2_proxy_insert_after(cf, data, ctx->dest,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#endif
#endif /* USE_NGHTTP2 */
#if defined(USE_PROXY_HTTP3) && defined(USE_NGHTTP3) && \
defined(USE_NGTCP2) && defined(USE_OPENSSL)
else if(!strcmp(alpn, "h3")) {
CURL_TRC_CF(data, cf, "installing subfilter for HTTP/3");
result = Curl_cf_h3_proxy_insert_after(cf, data, ctx->dest,
(bool)ctx->udp_tunnel);
if(result)
goto out;
}
#endif /* USE_PROXY_HTTP3 && USE_NGHTTP3 && USE_NGTCP2 && USE_OPENSSL */
else {
failf(data, "CONNECT: negotiated ALPN '%s' not supported", alpn);
failf(data, "%s: negotiated ALPN '%s' not supported", tunnel_type, alpn);
result = CURLE_COULDNT_CONNECT;
goto out;
}
@ -321,6 +657,19 @@ connect_sub:
* This means the protocol tunnel is established, we are done.
*/
DEBUGASSERT(ctx->sub_filter_installed);
if(ctx->udp_tunnel) {
#ifdef USE_PROXY_HTTP3
/* Insert capsule filter between us and the protocol sub-filter.
* This handles encap/decap of UDP datagrams in capsule format. */
result = Curl_cf_capsule_insert_after(cf, data);
if(result)
goto out;
CURL_TRC_CF(data, cf, "installed capsule filter for UDP tunnel");
#else
result = CURLE_NOT_BUILT_IN;
goto out;
#endif /* USE_PROXY_HTTP3 */
}
result = CURLE_OK;
}
@ -404,7 +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)
uint8_t proxytype,
bool udp_tunnel)
{
struct Curl_cfilter *cf;
struct cf_proxy_ctx *ctx = NULL;
@ -421,6 +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;
result = Curl_cf_create(&cf, &Curl_cft_http_proxy, ctx);
if(result)

View file

@ -32,14 +32,47 @@
enum Curl_proxy_use {
HEADER_SERVER, /* direct to server */
HEADER_PROXY, /* regular request to proxy */
HEADER_CONNECT /* sending CONNECT to a proxy */
HEADER_CONNECT, /* sending CONNECT to a proxy */
HEADER_CONNECT_UDP /* sending CONNECT-UDP to a proxy */
};
/* HTTP version for proxy tunnel request creation */
typedef enum {
PROXY_HTTP_V1 = 1,
PROXY_HTTP_V2 = 2,
PROXY_HTTP_V3 = 3
} proxy_http_ver;
/* Result from inspecting a proxy tunnel response */
typedef enum {
PROXY_INSPECT_OK, /* Tunnel established */
PROXY_INSPECT_FAILED, /* Tunnel failed */
PROXY_INSPECT_AUTH_RETRY /* Retry with auth */
} proxy_inspect_result;
CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
int httpversion);
proxy_http_ver ver);
CURLcode Curl_http_proxy_create_CONNECTUDP(struct httpreq **preq,
struct Curl_cfilter *cf,
struct Curl_easy *data,
struct Curl_peer *dest,
proxy_http_ver ver);
/* Create CONNECT or CONNECT-UDP request */
CURLcode Curl_http_proxy_create_tunnel_request(
struct httpreq **preq, struct Curl_cfilter *cf,
struct Curl_easy *data, struct Curl_peer *dest,
proxy_http_ver ver, bool udp_tunnel);
/* Inspect tunnel response for H2/H3 proxy (capsule-protocol, auth) */
struct http_resp;
CURLcode Curl_http_proxy_inspect_tunnel_response(
struct Curl_cfilter *cf, struct Curl_easy *data,
struct http_resp *resp, bool udp_tunnel,
proxy_inspect_result *presult);
/* Default proxy timeout in milliseconds */
#define PROXY_TIMEOUT (3600 * 1000)
@ -47,13 +80,17 @@ CURLcode Curl_http_proxy_create_CONNECT(struct httpreq **preq,
CURLcode Curl_cf_http_proxy_insert_after(struct Curl_cfilter *cf_at,
struct Curl_easy *data,
struct Curl_peer *dest,
uint8_t proxytype);
uint8_t proxytype,
bool udp_tunnel);
extern struct Curl_cftype Curl_cft_http_proxy;
#endif /* !CURL_DISABLE_PROXY && !CURL_DISABLE_HTTP */
#define IS_HTTPS_PROXY(t) (((t) == CURLPROXY_HTTPS) || \
((t) == CURLPROXY_HTTPS2))
((t) == CURLPROXY_HTTPS2) || \
((t) == CURLPROXY_HTTPS3))
#define IS_QUIC_PROXY(t) ((t) == CURLPROXY_HTTPS3)
#endif /* HEADER_CURL_HTTP_PROXY_H */

View file

@ -536,6 +536,37 @@ out:
#define UNIX_SOCKET_PREFIX "localhost"
#endif
CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data,
const char *scheme,
uint8_t *proxytype, const char *url)
{
if(!scheme)
return CURLE_OK;
if(curl_strequal("https", scheme)) {
if(*proxytype != CURLPROXY_HTTPS2 && *proxytype != CURLPROXY_HTTPS3)
*proxytype = CURLPROXY_HTTPS;
}
else if(curl_strequal("socks5h", scheme))
*proxytype = CURLPROXY_SOCKS5_HOSTNAME;
else if(curl_strequal("socks5", scheme))
*proxytype = CURLPROXY_SOCKS5;
else if(curl_strequal("socks4a", scheme))
*proxytype = CURLPROXY_SOCKS4A;
else if(curl_strequal("socks4", scheme) || curl_strequal("socks", scheme))
*proxytype = CURLPROXY_SOCKS4;
else if(curl_strequal("http", scheme)) {
if(*proxytype != CURLPROXY_HTTP_1_0)
*proxytype = CURLPROXY_HTTP;
}
else {
/* Any other xxx:// reject! */
failf(data, "Unsupported proxy scheme for \'%s\'", url);
return CURLE_COULDNT_CONNECT;
}
return CURLE_OK;
}
CURLcode Curl_peer_from_proxy_url(CURLU *uh,
struct Curl_easy *data,
const char *url,
@ -570,6 +601,7 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh,
break;
case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2:
case CURLPROXY_HTTPS3:
pp.scheme = &Curl_scheme_https;
break;
case CURLPROXY_SOCKS4:
@ -592,29 +624,9 @@ CURLcode Curl_peer_from_proxy_url(CURLU *uh,
}
else {
pp.scheme = Curl_get_scheme(scheme);
if(pp.scheme == &Curl_scheme_https) {
proxytype = (proxytype != CURLPROXY_HTTPS2) ?
CURLPROXY_HTTPS : CURLPROXY_HTTPS2;
}
else if(pp.scheme == &Curl_scheme_socks5h)
proxytype = CURLPROXY_SOCKS5_HOSTNAME;
else if(pp.scheme == &Curl_scheme_socks5)
proxytype = CURLPROXY_SOCKS5;
else if(pp.scheme == &Curl_scheme_socks4a)
proxytype = CURLPROXY_SOCKS4A;
else if((pp.scheme == &Curl_scheme_socks4) ||
(pp.scheme == &Curl_scheme_socks))
proxytype = CURLPROXY_SOCKS4;
else if(pp.scheme == &Curl_scheme_http) {
proxytype = (uint8_t)((proxytype != CURLPROXY_HTTP_1_0) ?
CURLPROXY_HTTP : CURLPROXY_HTTP_1_0);
}
else {
/* Any other xxx:// reject! */
failf(data, "Unsupported proxy scheme for \'%s\'", url);
result = CURLE_COULDNT_CONNECT;
result = Curl_scheme_to_proxytype(data, scheme, &proxytype, url);
if(result)
goto out;
}
}
DEBUGASSERT(pp.scheme);

View file

@ -94,6 +94,11 @@ CURLcode Curl_peer_from_connect_to(struct Curl_easy *data,
#ifndef CURL_DISABLE_PROXY
CURLcode Curl_scheme_to_proxytype(struct Curl_easy *data,
const char *scheme,
uint8_t *proxytype,
const char *url);
CURLcode Curl_peer_from_proxy_url(CURLU *uh,
struct Curl_easy *data,
const char *url,

View file

@ -1042,8 +1042,12 @@ static CURLcode setopt_long_proxy(struct Curl_easy *data, CURLoption option,
case CURLOPT_PROXYAUTH:
return httpauth(data, TRUE, (unsigned long)arg);
case CURLOPT_PROXYTYPE:
if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_SOCKS5_HOSTNAME))
if((arg < CURLPROXY_HTTP) || (arg > CURLPROXY_HTTPS3))
return CURLE_BAD_FUNCTION_ARGUMENT;
#ifndef USE_PROXY_HTTP3
if(arg == CURLPROXY_HTTPS3)
return CURLE_NOT_BUILT_IN;
#endif
s->proxytype = (unsigned char)arg;
break;
case CURLOPT_SOCKS5_AUTH:

View file

@ -99,6 +99,7 @@
#include "headers.h"
#include "curlx/strerr.h"
#include "curlx/strparse.h"
#include "peer.h"
/* Now for the protocols */
#include "ftp.h"
@ -1316,7 +1317,12 @@ 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;
conn->transport_wanted = TRNSPRT_TCP; /* most of them are TCP streams */
#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 */
/* Store the local bind parameters that will be used for this connection */
if(data->set.str[STRING_DEVICE]) {
@ -1793,6 +1799,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
{
char *proxyuser = NULL;
char *proxypasswd = NULL;
char *scheme = NULL;
CURLcode result = CURLE_OK;
/* Set the start proxy type for url scheme guessing */
uint8_t proxytype = for_pre_proxy ? CURLPROXY_SOCKS4 : data->set.proxytype;
@ -1807,7 +1814,21 @@ static CURLcode parse_proxy(struct Curl_easy *data,
these made up ones for proxies. Guess scheme for URLs without it. */
uc = curl_url_set(uhp, CURLUPART_URL, proxy,
CURLU_NON_SUPPORT_SCHEME | CURLU_GUESS_SCHEME);
if(uc) {
if(!uc) {
/* parsed okay as a URL - only update proxytype when scheme was explicit */
uc = curl_url_get(uhp, CURLUPART_SCHEME, &scheme, CURLU_NO_GUESS_SCHEME);
if(!uc) {
result = Curl_scheme_to_proxytype(data, scheme, &proxytype, proxy);
if(result)
goto error;
}
else if(uc != CURLUE_NO_SCHEME) {
result = CURLE_OUT_OF_MEMORY;
goto error;
}
/* else: no explicit scheme, keep the configured proxytype */
}
else {
failf(data, "Unsupported proxy syntax in \'%s\': %s", proxy,
curl_url_strerror(uc));
result = CURLE_COULDNT_RESOLVE_PROXY;
@ -1824,6 +1845,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
case CURLPROXY_HTTP_1_0:
case CURLPROXY_HTTPS:
case CURLPROXY_HTTPS2:
case CURLPROXY_HTTPS3:
if(for_pre_proxy) {
failf(data, "Unsupported pre-proxy type for \'%s\'", proxy);
result = CURLE_COULDNT_RESOLVE_PROXY;
@ -1878,6 +1900,7 @@ static CURLcode parse_proxy(struct Curl_easy *data,
proxyinfo->proxytype = proxytype;
error:
curlx_free(scheme);
curlx_free(proxyuser);
curlx_free(proxypasswd);
curl_url_cleanup(uhp);

View file

@ -491,6 +491,9 @@ static const struct feat features_table[] = {
#ifdef USE_NTLM
FEATURE("NTLM", NULL, CURL_VERSION_NTLM),
#endif
#ifdef USE_PROXY_HTTP3
FEATURE("PROXY-HTTP3", NULL, 0),
#endif
#ifdef USE_LIBPSL
FEATURE("PSL", NULL, CURL_VERSION_PSL),
#endif

View file

@ -72,6 +72,7 @@
#define QUIC_MAX_STREAMS (256 * 1024)
#define QUIC_HANDSHAKE_TIMEOUT (10 * NGTCP2_SECONDS)
#define QUIC_TUNNEL_INBUF_SIZE (64 * 1024)
/* We announce a small window size in transport param to the server,
* and grow that immediately to max when no rate limit is in place.
@ -95,6 +96,7 @@
#define H3_STREAM_SEND_BUFFER_MAX (10 * 1024 * 1024)
#define H3_STREAM_SEND_CHUNKS \
(H3_STREAM_SEND_BUFFER_MAX / H3_STREAM_CHUNK_SIZE)
#define QUIC_TUNNEL_INGRESS_PKT_LIMIT 1000
/*
* Store ngtcp2 version info in this buffer.
@ -139,6 +141,8 @@ struct cf_ngtcp2_ctx {
is accepted by peer */
CURLcode tls_vrfy_result; /* result of TLS peer verification */
int qlogfd;
unsigned char *tunnel_inbuf; /* ingress buffer for tunneled packets */
size_t tunnel_inbuf_len;
BIT(initialized);
BIT(tls_handshake_complete); /* TLS handshake is done */
BIT(use_earlydata); /* Using 0RTT data */
@ -156,6 +160,8 @@ static void cf_ngtcp2_ctx_init(struct cf_ngtcp2_ctx *ctx)
{
DEBUGASSERT(!ctx->initialized);
ctx->qlogfd = -1;
ctx->tunnel_inbuf = NULL;
ctx->tunnel_inbuf_len = 0;
ctx->version = NGTCP2_PROTO_VER_MAX;
Curl_bufcp_init(&ctx->stream_bufcp, H3_STREAM_CHUNK_SIZE,
H3_STREAM_POOL_SPARES);
@ -173,6 +179,8 @@ static void cf_ngtcp2_ctx_free(struct cf_ngtcp2_ctx *ctx)
curlx_dyn_free(&ctx->scratch);
Curl_uint32_hash_destroy(&ctx->streams);
Curl_ssl_peer_cleanup(&ctx->peer);
curlx_safefree(ctx->tunnel_inbuf);
ctx->tunnel_inbuf_len = 0;
}
curlx_free(ctx);
}
@ -493,7 +501,7 @@ static void quic_settings(struct cf_ngtcp2_ctx *ctx,
static CURLcode init_ngh3_conn(struct Curl_cfilter *cf,
struct Curl_easy *data);
static int cf_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data)
static int cb_ngtcp2_handshake_completed(ngtcp2_conn *tconn, void *user_data)
{
struct Curl_cfilter *cf = user_data;
struct cf_ngtcp2_ctx *ctx = cf ? cf->ctx : NULL;
@ -863,7 +871,7 @@ static ngtcp2_callbacks ng_callbacks = {
ngtcp2_crypto_client_initial_cb,
NULL, /* recv_client_initial */
ngtcp2_crypto_recv_crypto_data_cb,
cf_ngtcp2_handshake_completed,
cb_ngtcp2_handshake_completed,
NULL, /* recv_version_negotiation */
ngtcp2_crypto_encrypt_cb,
ngtcp2_crypto_decrypt_cb,
@ -982,6 +990,11 @@ static CURLcode cf_ngtcp2_adjust_pollset(struct Curl_cfilter *cf,
if(!ctx->qconn)
return CURLE_OK;
if(ctx->q.sockfd == CURL_SOCKET_BAD) {
/* Tunneled QUIC, no direct socket - delegate to next filter */
return cf->next->cft->adjust_pollset(cf->next, data, ps);
}
Curl_pollset_check(data, ps, ctx->q.sockfd, &want_recv, &want_send);
if(!want_send && !Curl_bufq_is_empty(&ctx->q.sendbuf))
want_send = TRUE;
@ -1904,8 +1917,72 @@ static CURLcode cf_progress_ingress(struct Curl_cfilter *cf,
rctx.pktx = pktx;
rctx.pkt_count = 0;
return vquic_recv_packets(cf, data, &ctx->q, 1000,
if(ctx->q.sockfd != CURL_SOCKET_BAD) {
/* Direct UDP socket (via happy eyeballs) */
return vquic_recv_packets(cf, data, &ctx->q, 1000,
cf_ngtcp2_recv_pkts, &rctx);
}
else {
/* Tunneled QUIC (CONNECT-UDP through proxy) */
unsigned char *buf;
size_t max_udp_payload = QUIC_TUNNEL_INBUF_SIZE;
size_t pkt_limit = QUIC_TUNNEL_INGRESS_PKT_LIMIT;
size_t nread;
struct sockaddr_storage remote_addr;
socklen_t remote_addrlen;
if(ctx->qconn) {
size_t max_path_payload;
max_path_payload =
ngtcp2_conn_get_path_max_tx_udp_payload_size(ctx->qconn);
if(max_path_payload > max_udp_payload)
max_udp_payload = max_path_payload;
}
if(ctx->tunnel_inbuf_len < max_udp_payload) {
unsigned char *newbuf =
(unsigned char *)curlx_realloc(ctx->tunnel_inbuf, max_udp_payload);
if(!newbuf)
return CURLE_OUT_OF_MEMORY;
ctx->tunnel_inbuf = newbuf;
ctx->tunnel_inbuf_len = max_udp_payload;
}
buf = ctx->tunnel_inbuf;
while(pkt_limit--) {
result = Curl_conn_cf_recv(cf->next, data, (char *)buf,
ctx->tunnel_inbuf_len, &nread);
if(result == CURLE_AGAIN) {
/* no more data available at the moment */
return CURLE_OK;
}
if(result) {
CURL_TRC_CF(data, cf, "ingress, recv from tunnel failed: %d",
result);
return result;
}
if(nread == 0) {
/* tunnel closed */
return CURLE_OK;
}
memcpy(&remote_addr, ctx->connected_path.remote.addr,
ctx->connected_path.remote.addrlen);
remote_addrlen = (socklen_t)ctx->connected_path.remote.addrlen;
result = cf_ngtcp2_recv_pkts(buf, nread, nread, &remote_addr,
remote_addrlen, 0, &rctx);
if(result)
return result;
if(!ctx->q.got_first_byte) {
ctx->q.got_first_byte = TRUE;
ctx->q.first_byte_at = ctx->q.last_op;
}
ctx->q.last_io = ctx->q.last_op;
}
return CURLE_OK;
}
}
/**
@ -2189,6 +2266,7 @@ static void cf_ngtcp2_ctx_close(struct cf_ngtcp2_ctx *ctx)
}
ctx->qlogfd = -1;
Curl_vquic_tls_cleanup(&ctx->tls);
Curl_ssl_peer_cleanup(&ctx->peer);
vquic_ctx_free(&ctx->q);
if(ctx->h3conn) {
nghttp3_conn_del(ctx->h3conn);
@ -2220,6 +2298,12 @@ static CURLcode cf_ngtcp2_shutdown(struct Curl_cfilter *cf,
return CURLE_OK;
}
if(!cf->next) {
Curl_bufq_reset(&ctx->q.sendbuf);
*done = TRUE;
return CURLE_OK;
}
CF_DATA_SAVE(save, cf, data);
*done = FALSE;
pktx_init(&pktx, cf, data);
@ -2648,30 +2732,81 @@ static CURLcode cf_connect_start(struct Curl_cfilter *cf,
if(result)
return result;
if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL))
return CURLE_QUIC_CONNECT_ERROR;
ctx->q.local_addrlen = sizeof(ctx->q.local_addr);
rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr,
&ctx->q.local_addrlen);
if(rv == -1)
return CURLE_QUIC_CONNECT_ERROR;
/* Query socket and remote address from sub-chain */
if(Curl_cf_socket_peek(cf->next, data, &ctx->q.sockfd, &sockaddr, NULL)) {
/* No direct socket - must be tunneled QUIC (CONNECT-UDP through proxy) */
ctx->q.sockfd = CURL_SOCKET_BAD;
}
ngtcp2_addr_init(&ctx->connected_path.local,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen);
ngtcp2_addr_init(&ctx->connected_path.remote,
&sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen);
if(ctx->q.sockfd != CURL_SOCKET_BAD) {
/* Direct UDP socket - get local address for ngtcp2 */
ctx->q.local_addrlen = sizeof(ctx->q.local_addr);
rv = getsockname(ctx->q.sockfd, (struct sockaddr *)&ctx->q.local_addr,
&ctx->q.local_addrlen);
if(rv == -1)
return CURLE_QUIC_CONNECT_ERROR;
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ngtcp2_addr_init(&ctx->connected_path.local,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen);
ngtcp2_addr_init(&ctx->connected_path.remote,
&sockaddr->curl_sa_addr, (socklen_t)sockaddr->addrlen);
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
}
else {
/* Tunneled QUIC (e.g. CONNECT-UDP): get remote address
from the connected filter below */
const struct Curl_sockaddr_ex *remote = NULL;
if(cf->next->cft->query(cf->next, data, CF_QUERY_REMOTE_ADDR, NULL,
CURL_UNCONST(&remote)))
return CURLE_QUIC_CONNECT_ERROR;
if(!remote)
return CURLE_QUIC_CONNECT_ERROR;
memset(&ctx->q.local_addr, 0, sizeof(ctx->q.local_addr));
switch(remote->family) {
case AF_INET:
((struct sockaddr_in *)&ctx->q.local_addr)->sin_family = AF_INET;
ctx->q.local_addrlen = sizeof(struct sockaddr_in);
break;
#ifdef USE_IPV6
case AF_INET6:
((struct sockaddr_in6 *)&ctx->q.local_addr)->sin6_family = AF_INET6;
ctx->q.local_addrlen = sizeof(struct sockaddr_in6);
break;
#endif
default:
return CURLE_QUIC_CONNECT_ERROR;
}
ngtcp2_addr_init(&ctx->connected_path.local,
(struct sockaddr *)&ctx->q.local_addr,
ctx->q.local_addrlen);
ngtcp2_addr_init(&ctx->connected_path.remote,
&remote->curl_sa_addr,
(socklen_t)remote->addrlen);
rc = ngtcp2_conn_client_new(&ctx->qconn, &ctx->dcid, &ctx->scid,
&ctx->connected_path,
NGTCP2_PROTO_VER_V1, &ng_callbacks,
&ctx->settings, &ctx->transport_params,
Curl_ngtcp2_mem(), cf);
if(rc)
return CURLE_QUIC_CONNECT_ERROR;
ctx->conn_ref.get_conn = get_conn;
ctx->conn_ref.user_data = cf;
}
result = Curl_vquic_tls_init(&ctx->tls, cf, data, &ctx->peer, &ALPN_SPEC_H3,
cf_ngtcp2_tls_ctx_setup, &ctx->tls,
@ -2720,8 +2855,8 @@ static CURLcode cf_ngtcp2_connect(struct Curl_cfilter *cf,
return CURLE_OK;
}
/* Connect the UDP filter first */
if(!cf->next->connected) {
/* Connect the sub-chain */
if(cf->next && !cf->next->connected) {
result = Curl_conn_cf_connect(cf->next, data, done);
if(result || !*done)
return result;
@ -2803,11 +2938,14 @@ out:
#ifdef CURLVERBOSE
if(result) {
struct ip_quadruple ip;
if(ctx->q.sockfd != CURL_SOCKET_BAD) {
/* Direct UDP socket - get IP info for error reporting */
struct ip_quadruple ip;
if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip))
infof(data, "QUIC connect to %s port %u failed: %s",
ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
if(!Curl_cf_socket_peek(cf->next, data, NULL, NULL, &ip))
infof(data, "QUIC connect to %s port %u failed: %s",
ip.remote_ip, ip.remote_port, curl_easy_strerror(result));
}
}
#endif
if(!result && ctx->qconn) {
@ -3003,4 +3141,33 @@ out:
return result;
}
CURLcode Curl_cf_ngtcp2_insert_after(struct Curl_cfilter *cf_at)
{
struct cf_ngtcp2_ctx *ctx = NULL;
struct Curl_cfilter *cf = NULL;
CURLcode result;
ctx = curlx_calloc(1, sizeof(*ctx));
if(!ctx) {
result = CURLE_OUT_OF_MEMORY;
goto out;
}
cf_ngtcp2_ctx_init(ctx);
result = Curl_cf_create(&cf, &Curl_cft_http3, ctx);
if(result)
goto out;
Curl_conn_cf_insert_after(cf_at, cf);
cf->conn = cf_at->conn;
out:
if(result) {
curlx_safefree(cf);
cf_ngtcp2_ctx_free(ctx);
}
return result;
}
#endif
/* Do not leak this filter's call_data accessor in unity builds. */
#undef CF_CTX_CALL_DATA

View file

@ -54,6 +54,8 @@ CURLcode Curl_cf_ngtcp2_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
struct Curl_sockaddr_ex *addr);
CURLcode Curl_cf_ngtcp2_insert_after(struct Curl_cfilter *cf_at);
#endif
#endif /* HEADER_CURL_VQUIC_CURL_NGTCP2_H */

View file

@ -156,6 +156,7 @@ static void cf_quiche_ctx_close(struct cf_quiche_ctx *ctx)
quiche_config_free(ctx->cfg);
ctx->cfg = NULL;
}
Curl_ssl_peer_cleanup(&ctx->peer);
}
static CURLcode cf_flush_egress(struct Curl_cfilter *cf,

View file

@ -72,6 +72,8 @@ CURLcode Curl_vquic_tls_init(struct curl_tls_ctx *ctx,
return CURLE_FAILED_INIT;
#endif
(void)session_reuse_cb;
if(peer->dest)
Curl_ssl_peer_cleanup(peer);
result = Curl_ssl_peer_init(peer, cf, tls_id, TRNSPRT_QUIC);
if(result)
return result;

View file

@ -261,6 +261,44 @@ out:
return result;
}
/* Split QUIC payload by datagram (gso) boundaries when sending over a
* non-UDP lower filter (for example CONNECT-UDP proxy tunnel). */
static CURLcode send_packet_no_gso_cf(struct Curl_cfilter *cf,
struct Curl_easy *data,
const uint8_t *pkt, size_t pktlen,
size_t gsolen, size_t *psent)
{
const uint8_t *p, *end = pkt + pktlen;
size_t sent, len;
CURLcode result = CURLE_OK;
VERBOSE(size_t calls = 0);
*psent = 0;
/* Send one datagram-sized chunk per call into the lower filter. */
for(p = pkt; p < end; p += len) {
len = CURLMIN(gsolen, (size_t)(end - p));
result = Curl_conn_cf_send(cf->next, data, p, len, FALSE, &sent);
/* Report forward progress even if we return CURLE_AGAIN later. */
*psent += sent;
VERBOSE(++calls);
/* Preserve lower-filter errors (including CURLE_AGAIN). */
if(result)
goto out;
if(sent < len) {
/* We need whole datagrams here. Partial accept means blocked. */
result = CURLE_AGAIN;
goto out;
}
}
out:
CURL_TRC_CF(data, cf, "vquic_cf_send(len=%zu, gso=%zu, calls=%zu)"
" -> %d, sent=%zu",
pktlen, gsolen, calls, result, *psent);
return result;
}
static CURLcode vquic_send_packets(struct Curl_cfilter *cf,
struct Curl_easy *data,
struct cf_quic_ctx *qctx,
@ -310,7 +348,22 @@ CURLcode vquic_flush(struct Curl_cfilter *cf, struct Curl_easy *data,
blen = qctx->split_len;
}
result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
if(qctx->sockfd != CURL_SOCKET_BAD) {
/* Direct UDP socket (via happy eyeballs) */
result = vquic_send_packets(cf, data, qctx, buf, blen, gsolen, &sent);
}
else {
/* Tunneled QUIC (CONNECT-UDP through proxy) */
if(gsolen && (blen > gsolen)) {
/* Send one datagram at a time to preserve packet boundaries. */
result = send_packet_no_gso_cf(cf, data, buf, blen, gsolen, &sent);
}
else {
/* No GSO aggregate to split, regular lower-filter send is enough. */
result = Curl_conn_cf_send(cf->next, data, buf, blen, FALSE, &sent);
}
}
if(result) {
if(result == CURLE_AGAIN) {
Curl_bufq_skip(&qctx->sendbuf, sent);
@ -699,6 +752,16 @@ CURLcode Curl_qlogdir(struct Curl_easy *data,
return CURLE_OK;
}
CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at)
{
#if defined(USE_NGTCP2) && defined(USE_NGHTTP3)
return Curl_cf_ngtcp2_insert_after(cf_at);
#else
(void)cf_at;
return CURLE_NOT_BUILT_IN;
#endif
}
CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,
@ -737,10 +800,6 @@ CURLcode Curl_conn_may_http3(struct Curl_easy *data,
failf(data, "HTTP/3 is not supported over a SOCKS proxy");
return CURLE_URL_MALFORMAT;
}
if(conn->bits.httpproxy && conn->bits.tunnel_proxy) {
failf(data, "HTTP/3 is not supported over an HTTP proxy");
return CURLE_URL_MALFORMAT;
}
#endif
return CURLE_OK;

View file

@ -39,6 +39,8 @@ CURLcode Curl_qlogdir(struct Curl_easy *data,
size_t scidlen,
int *qlogfdp);
CURLcode Curl_cf_quic_insert_after(struct Curl_cfilter *cf_at);
CURLcode Curl_cf_quic_create(struct Curl_cfilter **pcf,
struct Curl_easy *data,
struct connectdata *conn,

View file

@ -3713,8 +3713,11 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
return result;
}
if(data->set.fdebug && data->set.verbose) {
/* the SSL trace callback is only used for verbose logging */
if(data->set.fdebug && data->set.verbose &&
(peer->transport != TRNSPRT_QUIC)) {
/* the SSL trace callback is only used for verbose logging;
* QUIC connections use a different TLS record format that
* ossl_trace cannot handle */
SSL_CTX_set_msg_callback(octx->ssl_ctx, ossl_trace);
SSL_CTX_set_msg_callback_arg(octx->ssl_ctx, cf);
}
@ -4007,12 +4010,20 @@ static CURLcode ossl_connect_step1(struct Curl_cfilter *cf,
{
struct ssl_connect_data *connssl = cf->ctx;
struct ossl_ctx *octx = (struct ossl_ctx *)connssl->backend;
char tls_id[80];
BIO *bio;
CURLcode result;
DEBUGASSERT(ssl_connect_1 == connssl->connecting_state);
DEBUGASSERT(octx);
if(!connssl->peer.dest) {
Curl_ossl_version(tls_id, sizeof(tls_id));
result = Curl_ssl_peer_init(&connssl->peer, cf, tls_id, TRNSPRT_TCP);
if(result)
return result;
}
result = Curl_ossl_ctx_init(octx, cf, data, &connssl->peer,
connssl->alpn, NULL, NULL,
ossl_new_session_cb, cf,

View file

@ -1197,6 +1197,7 @@ void Curl_ssl_peer_cleanup(struct ssl_peer *peer)
Curl_peer_unlink(&peer->dest);
curlx_safefree(peer->sni);
curlx_safefree(peer->scache_key);
peer->transport = TRNSPRT_NONE;
peer->type = CURL_SSL_PEER_DNS;
}
@ -1206,6 +1207,8 @@ static void cf_close(struct Curl_cfilter *cf, struct Curl_easy *data)
if(connssl) {
connssl->ssl_impl->close(cf, data);
connssl->state = ssl_connection_none;
connssl->connecting_state = ssl_connect_1;
connssl->prefs_checked = FALSE;
Curl_ssl_peer_cleanup(&connssl->peer);
}
cf->connected = FALSE;

View file

@ -133,9 +133,6 @@ struct ssl_connect_data {
BIT(input_pending); /* data for SSL_read() may be available */
};
#undef CF_CTX_CALL_DATA
#define CF_CTX_CALL_DATA(cf) ((struct ssl_connect_data *)(cf)->ctx)->call_data
/* Definitions for SSL Implementations */
struct Curl_ssl {
@ -209,3 +206,9 @@ CURLcode Curl_on_session_reuse(struct Curl_cfilter *cf,
#endif /* USE_SSL */
#endif /* HEADER_CURL_VTLS_INT_H */
#ifdef USE_SSL
/* Restore the default SSL filter call_data accessor for unity builds. */
#undef CF_CTX_CALL_DATA
#define CF_CTX_CALL_DATA(cf) ((struct ssl_connect_data *)(cf)->ctx)->call_data
#endif