From 22aad44ef92ddd29ad2a3f9d39fc3df4f85a2ebc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 7 May 2026 17:36:07 -0300 Subject: [PATCH 01/15] utils: add HTTPS proxy URL parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Accept https:// in flb_utils_proxy_url_split alongside http://. Default port for https:// proxies is 443. Error messages and the default-port strdup calls are guarded behind FLB_HAVE_TLS so that non-TLS builds retain the original behaviour and error message ('only HTTP proxy is supported.'). Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- src/flb_utils.c | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/flb_utils.c b/src/flb_utils.c index 1fcd472e9ef..7a6077b145d 100644 --- a/src/flb_utils.c +++ b/src/flb_utils.c @@ -1869,9 +1869,17 @@ int flb_utils_proxy_url_split(const char *in_url, char **out_protocol, return -1; } - /* Only HTTP proxy is supported for now. */ - if (strcmp(protocol, "http") != 0) { + /* Only HTTP proxy is supported without TLS support. */ + if (strcmp(protocol, "http") != 0 +#ifdef FLB_HAVE_TLS + && strcmp(protocol, "https") != 0 +#endif + ) { +#ifdef FLB_HAVE_TLS + flb_error("only HTTP and HTTPS proxies are supported."); +#else flb_error("only HTTP proxy is supported."); +#endif goto error; } @@ -1949,7 +1957,11 @@ int flb_utils_proxy_url_split(const char *in_url, char **out_protocol, } } else if (*(end + 1) == '\0') { +#ifdef FLB_HAVE_TLS + port = flb_strdup(strcmp(protocol, "https") == 0 ? "443" : "80"); +#else port = flb_strdup("80"); +#endif if (!port) { flb_errno(); goto error; @@ -1988,7 +2000,11 @@ int flb_utils_proxy_url_split(const char *in_url, char **out_protocol, goto error; } +#ifdef FLB_HAVE_TLS + port = flb_strdup(strcmp(protocol, "https") == 0 ? "443" : "80"); +#else port = flb_strdup("80"); +#endif if (!port) { flb_errno(); goto error; From ad2da70d865b47d9c43e02118096defd220f083d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 7 May 2026 17:36:26 -0300 Subject: [PATCH 02/15] upstream: add HTTPS proxy TLS context support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the configured proxy URL uses https://, create a dedicated flb_tls context (proxy_tls_context) for the proxy TLS leg, stored in struct flb_upstream and freed in flb_upstream_destroy. The context uses the proxy hostname as its SNI vhost, and hostname verification is explicitly enabled via flb_tls_set_verify_hostname so the proxy certificate is fully validated. Also fixes two error-path bugs in flb_upstream_create: - flb_upstream_queue_init is now called immediately after flb_stream_setup so all subsequent error paths can safely call flb_upstream_destroy for centralised cleanup. - The unzip>tcp_host OOM check now calls flb_upstream_destroy instead of bare flb_free, preventing leaks of proxied_host, proxy_username, proxy_password, and proxy_tls_context. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- include/fluent-bit/flb_upstream.h | 7 +++++ src/flb_upstream.c | 45 ++++++++++++++++++++++++++++--- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/include/fluent-bit/flb_upstream.h b/include/fluent-bit/flb_upstream.h index 44d585c5bd9..63593b1372b 100644 --- a/include/fluent-bit/flb_upstream.h +++ b/include/fluent-bit/flb_upstream.h @@ -30,6 +30,10 @@ #include #include +#ifdef FLB_HAVE_TLS +#include +#endif + #include #include @@ -59,6 +63,9 @@ struct flb_upstream { int proxied_port; char *proxy_username; char *proxy_password; +#ifdef FLB_HAVE_TLS + struct flb_tls *proxy_tls_context; /* TLS context for the proxy (https proxy) */ +#endif /* * If an upstream context has been created in HA mode, this flag is diff --git a/src/flb_upstream.c b/src/flb_upstream.c index 62decd65ec1..93d290ebaf5 100644 --- a/src/flb_upstream.c +++ b/src/flb_upstream.c @@ -321,6 +321,10 @@ struct flb_upstream *flb_upstream_create(struct flb_config *config, config, NULL); + /* Initialize queues early so all error paths can safely call + * flb_upstream_destroy(u) for centralised cleanup. */ + flb_upstream_queue_init(&u->queue); + /* Set upstream to the http_proxy if it is specified. */ if (transport == FLB_TRANSPORT_TCP && flb_upstream_needs_proxy(host, config->http_proxy, config->no_proxy) == FLB_TRUE) { @@ -343,6 +347,35 @@ struct flb_upstream *flb_upstream_create(struct flb_config *config, u->proxy_password = flb_strdup(proxy_password); } +#ifdef FLB_HAVE_TLS + if (strcmp(proxy_protocol, "https") == 0) { + /* + * The proxy connection itself is TLS. Create a dedicated TLS + * context using the proxy hostname as the SNI (vhost). This + * context is separate from the destination TLS context so that + * each handshake uses the correct hostname. + */ + u->proxy_tls_context = flb_tls_create(FLB_TLS_CLIENT_MODE, + FLB_TRUE, 0, + proxy_host, + NULL, NULL, + NULL, NULL, NULL); + if (!u->proxy_tls_context) { + flb_error("[upstream] could not create TLS context for HTTPS proxy %s", + proxy_host); + flb_free(proxy_protocol); + flb_free(proxy_host); + flb_free(proxy_port); + flb_free(proxy_username); + flb_free(proxy_password); + flb_upstream_destroy(u); + return NULL; + } + + flb_tls_set_verify_hostname(u->proxy_tls_context, FLB_TRUE); + } +#endif + flb_free(proxy_protocol); flb_free(proxy_host); flb_free(proxy_port); @@ -355,15 +388,12 @@ struct flb_upstream *flb_upstream_create(struct flb_config *config, } if (!u->tcp_host) { - flb_free(u); + flb_upstream_destroy(u); return NULL; } flb_stream_enable_flags(&u->base, FLB_IO_ASYNC); - /* Initialize queues */ - flb_upstream_queue_init(&u->queue); - mk_list_add(&u->base._head, &config->upstreams); return u; @@ -695,6 +725,13 @@ int flb_upstream_destroy(struct flb_upstream *u) flb_free(u->proxy_username); flb_free(u->proxy_password); +#ifdef FLB_HAVE_TLS + if (u->proxy_tls_context) { + flb_tls_destroy(u->proxy_tls_context); + u->proxy_tls_context = NULL; + } +#endif + if (mk_list_is_set(&u->base._head) == 0) { mk_list_del(&u->base._head); } From d6b46f5f0925385efd287fd81c4c115c1ad64b4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 7 May 2026 17:36:46 -0300 Subject: [PATCH 03/15] io: add TLS handshake and I/O flag for HTTPS proxy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In flb_io_net_connect, when the upstream has a proxy_tls_context, perform a TLS handshake with the proxy before sending the HTTP CONNECT request. After a successful proxy TLS handshake, enable FLB_IO_TLS on the stream so that flb_io_net_write/read route all subsequent I/O (the CONNECT request and post-CONNECT data) through the proxy TLS session. Without this flag, plain-HTTP destinations would fall through to an unhandled path because their stream flags do not include FLB_IO_TLS. For HTTPS destinations the flag is already set, making this a no-op. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- src/flb_io.c | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/flb_io.c b/src/flb_io.c index 86afc8e8b79..c912a66297b 100644 --- a/src/flb_io.c +++ b/src/flb_io.c @@ -168,6 +168,38 @@ int flb_io_net_connect(struct flb_connection *connection, if (connection->stream->transport == FLB_TRANSPORT_TCP && connection->upstream->proxied_host) { +#ifdef FLB_HAVE_TLS + /* + * When the proxy URL uses https://, the connection to the proxy + * itself must be TLS-wrapped before the HTTP CONNECT tunnel is + * established. Use the dedicated proxy TLS context which carries + * the proxy hostname as the SNI (vhost). + */ + if (connection->upstream->proxy_tls_context != NULL) { + ret = flb_tls_session_create(connection->upstream->proxy_tls_context, + connection, + coro); + if (ret != 0) { + flb_debug("[http_client] proxy TLS handshake failed for %s:%i", + connection->upstream->tcp_host, + connection->upstream->tcp_port); + flb_socket_close(fd); + connection->fd = -1; + connection->event.fd = -1; + return -1; + } + /* + * Ensure all I/O (the CONNECT request and any subsequent + * data) is routed through the proxy TLS session. This is + * necessary when the ultimate destination is plain HTTP: + * the stream's FLB_IO_TLS flag is not set for such upstreams, + * but flb_io_net_write/read check that flag to decide whether + * to use connection->tls_session. For HTTPS destinations the + * flag is already set so this is a no-op. + */ + flb_stream_enable_flags(connection->stream, FLB_IO_TLS); + } +#endif ret = flb_http_client_proxy_connect(connection); if (ret == -1) { From 44bd0ed11dd9bf3a1b64bac9df60ab864c073ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 7 May 2026 17:37:07 -0300 Subject: [PATCH 04/15] tls: support TLS-in-TLS for HTTPS proxy tunnels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two related changes: SNI priority fix: when the TLS context carries an explicit vhost (e.g. the proxy hostname on a proxy TLS context), that vhost now takes priority over proxied_host in flb_tls_session_create. Previously proxied_host was used unconditionally for upstream connections, causing the proxy TLS handshake to advertise the destination hostname instead of the proxy hostname. TLS-in-TLS chaining: add session_set_outer to struct flb_tls_backend and implement tls_session_set_outer in the OpenSSL backend using BIO_f_ssl. When flb_tls_session_create detects an existing tls_session on the connection (the proxy TLS session), it chains the new inner session's I/O through the outer session via SSL_set_bio, so that the destination TLS handshake travels inside the already-established proxy TLS tunnel rather than going directly to the raw socket. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- include/fluent-bit/tls/flb_tls.h | 8 ++++++ src/tls/flb_tls.c | 49 +++++++++++++++++++++++++++++--- src/tls/openssl.c | 46 ++++++++++++++++++++++++++++++ 3 files changed, 99 insertions(+), 4 deletions(-) diff --git a/include/fluent-bit/tls/flb_tls.h b/include/fluent-bit/tls/flb_tls.h index 4d3135e4b98..3d4c482a602 100644 --- a/include/fluent-bit/tls/flb_tls.h +++ b/include/fluent-bit/tls/flb_tls.h @@ -106,6 +106,14 @@ struct flb_tls_backend { void (*session_invalidate) (void *); int (*session_destroy) (void *); const char *(*session_alpn_get) (void *); + /* + * Chain an inner TLS session's I/O through an outer TLS session. + * Used for TLS-in-TLS when connecting through an HTTPS proxy: after + * HTTP CONNECT is established over the proxy TLS, the destination TLS + * handshake data must be sent through (and encrypted by) the proxy TLS. + * Optional: may be NULL if the backend does not support it. + */ + int (*session_set_outer) (void *inner, void *outer); /* I/O */ int (*net_read) (struct flb_tls_session *, void *, size_t); diff --git a/src/tls/flb_tls.c b/src/tls/flb_tls.c index 2a0ebf0da2d..2aab79052d4 100644 --- a/src/tls/flb_tls.c +++ b/src/tls/flb_tls.c @@ -996,13 +996,20 @@ int flb_tls_session_create(struct flb_tls *tls, vhost = NULL; if (connection->type == FLB_UPSTREAM_CONNECTION) { - if (connection->upstream->proxied_host != NULL) { + if (tls->vhost != NULL) { + /* + * An explicit vhost in the TLS context takes priority. This + * covers the HTTPS proxy case where the proxy TLS context has + * its own vhost (= tcp_host) and must not fall through to + * proxied_host which belongs to the inner destination. + * Leave vhost as NULL so net_handshake() picks up tls->vhost. + */ + } + else if (connection->upstream->proxied_host != NULL) { vhost = flb_rtrim(connection->upstream->proxied_host, '.'); } else { - if (tls->vhost == NULL) { - vhost = flb_rtrim(connection->upstream->tcp_host, '.'); - } + vhost = flb_rtrim(connection->upstream->tcp_host, '.'); } } @@ -1022,6 +1029,40 @@ int flb_tls_session_create(struct flb_tls *tls, return -1; } + /* + * If an existing TLS session is already active on this connection + * (e.g. the proxy TLS session for an HTTPS proxy), chain the new + * session's I/O through it. The inner (destination) TLS handshake + * data must travel inside the outer (proxy) TLS tunnel rather than + * going directly to the raw socket. + */ + if (connection->tls_session != NULL && + tls->api->session_set_outer != NULL) { + result = tls->api->session_set_outer(session->ptr, + connection->tls_session->ptr); + if (result != 0) { + flb_error("[tls] failed to chain TLS session over proxy tunnel for %s", + flb_connection_get_remote_address(connection)); + + if (vhost != NULL) { + flb_free(vhost); + } + + tls->api->session_destroy(session->ptr); + flb_free(session); + return -1; + } + + /* + * The outer backend session ptr is now owned by the inner session + * (via outer_session). Release the outer flb_tls_session wrapper + * without going through flb_tls_session_destroy, which would free + * the backend ptr we just transferred. + */ + flb_free(connection->tls_session); + connection->tls_session = NULL; + } + session->tls = tls; session->connection = connection; diff --git a/src/tls/openssl.c b/src/tls/openssl.c index 72e8379ac5f..bf3597571b2 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -81,6 +81,7 @@ struct tls_session { char alpn[FLB_TLS_ALPN_MAX_LENGTH]; int continuation_flag; struct tls_context *parent; /* parent struct tls_context ref */ + struct tls_session *outer_session; /* outer TLS session for TLS-in-TLS (HTTPS proxy) */ }; static int host_is_ip_literal(const char *hostname, char *normalized, size_t normalized_size) @@ -1475,10 +1476,40 @@ static void *tls_session_create(struct flb_tls *tls, return session; } +/* + * Chain inner TLS session I/O through an outer TLS session. + * Used for TLS-in-TLS when connecting through an HTTPS proxy: the inner + * (destination) SSL object's BIO is replaced with a BIO_f_ssl wrapper + * around the outer (proxy) SSL object, so all inner TLS bytes flow + * through the already-established outer TLS tunnel. + */ +static int tls_session_set_outer(void *inner_ptr, void *outer_ptr) +{ + struct tls_session *inner = (struct tls_session *) inner_ptr; + struct tls_session *outer = (struct tls_session *) outer_ptr; + BIO *bio; + + bio = BIO_new(BIO_f_ssl()); + if (!bio) { + flb_error("[tls] could not create BIO for TLS-in-TLS tunnel"); + return -1; + } + + /* + * BIO_NOCLOSE: the outer SSL object must NOT be freed when this BIO + * is freed; we manage its lifecycle via inner->outer_session. + */ + BIO_set_ssl(bio, outer->ssl, BIO_NOCLOSE); + SSL_set_bio(inner->ssl, bio, bio); + inner->outer_session = outer; + return 0; +} + static int tls_session_destroy(void *session) { struct tls_session *ptr = session; struct tls_context *ctx; + struct tls_context *outer_ctx; if (!ptr) { return 0; @@ -1492,6 +1523,20 @@ static int tls_session_destroy(void *session) } SSL_free(ptr->ssl); + + /* + * If this session was chained over an outer TLS session (HTTPS proxy), + * BIO_NOCLOSE ensured SSL_free above did not free the outer SSL object. + * Free it explicitly now, under its own context mutex. + */ + if (ptr->outer_session != NULL) { + outer_ctx = ptr->outer_session->parent; + pthread_mutex_lock(&outer_ctx->mutex); + SSL_free(ptr->outer_session->ssl); + flb_free(ptr->outer_session); + pthread_mutex_unlock(&outer_ctx->mutex); + } + flb_free(ptr); pthread_mutex_unlock(&ctx->mutex); @@ -1932,6 +1977,7 @@ static struct flb_tls_backend tls_openssl = { .session_create = tls_session_create, .session_invalidate = tls_session_invalidate, .session_destroy = tls_session_destroy, + .session_set_outer = tls_session_set_outer, .net_read = tls_net_read, .net_write = tls_net_write, .net_handshake = tls_net_handshake, From d0bad063f80dfd1f8bf246f65b319760a81fe308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 7 May 2026 17:37:20 -0300 Subject: [PATCH 05/15] tests: internal: add HTTPS proxy support coverage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extend proxy_url_checks in utils.c with HTTPS proxy cases (explicit port, default port 443, credentials) and rejection cases for unsupported schemes (ftp://, socks5://). Add two tests to upstream_tls.c: one verifies that flb_upstream_create with an https:// proxy sets a non-NULL proxy_tls_context with verify_hostname enabled; another verifies that a plain http:// proxy leaves proxy_tls_context NULL. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- tests/internal/upstream_tls.c | 76 +++++++++++++++++++++++++++++++++++ tests/internal/utils.c | 14 ++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/tests/internal/upstream_tls.c b/tests/internal/upstream_tls.c index 00302651d95..f3a736693ef 100644 --- a/tests/internal/upstream_tls.c +++ b/tests/internal/upstream_tls.c @@ -7,6 +7,7 @@ #include #include #include +#include #include "flb_tests_internal.h" @@ -231,6 +232,79 @@ void test_tls_session_destroy_no_double_free(void) #endif } +/* + * Verify that flb_upstream_create creates a proxy_tls_context with + * verify_hostname enabled when an https:// proxy is configured. + */ +void test_upstream_create_https_proxy_sets_tls_context(void) +{ + struct flb_config *config; + struct flb_upstream *u; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + return; + } + + config->http_proxy = "https://proxy.example.com:8080"; + + u = flb_upstream_create(config, "dest.example.com", 443, + FLB_IO_TLS, NULL); + TEST_CHECK(u != NULL); + if (u == NULL) { + config->http_proxy = NULL; + flb_config_exit(config); + return; + } + + TEST_CHECK(u->proxy_tls_context != NULL); + TEST_MSG("proxy_tls_context should be non-NULL for https:// proxy"); + + if (u->proxy_tls_context != NULL) { + TEST_CHECK(u->proxy_tls_context->verify_hostname == FLB_TRUE); + TEST_MSG("proxy_tls_context should have verify_hostname enabled"); + } + + config->http_proxy = NULL; + flb_upstream_destroy(u); + flb_config_exit(config); +} + +/* + * Verify that flb_upstream_create does NOT create a proxy_tls_context + * when a plain http:// proxy is configured. + */ +void test_upstream_create_http_proxy_no_tls_context(void) +{ + struct flb_config *config; + struct flb_upstream *u; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + return; + } + + config->http_proxy = "http://proxy.example.com:3128"; + + u = flb_upstream_create(config, "dest.example.com", 80, + FLB_IO_TCP, NULL); + TEST_CHECK(u != NULL); + if (u == NULL) { + config->http_proxy = NULL; + flb_config_exit(config); + return; + } + + TEST_CHECK(u->proxy_tls_context == NULL); + TEST_MSG("proxy_tls_context should be NULL for plain http:// proxy"); + + config->http_proxy = NULL; + flb_upstream_destroy(u); + flb_config_exit(config); +} + void test_tls_reload_when_certificate_file_changes(void) { int ret; @@ -397,6 +471,8 @@ TEST_LIST = { #ifdef FLB_HAVE_TLS {"prepare_destroy_conn_marks_tls_session_stale", test_prepare_destroy_conn_marks_tls_session_stale}, {"tls_session_destroy_no_double_free", test_tls_session_destroy_no_double_free}, + {"upstream_create_https_proxy_sets_tls_context", test_upstream_create_https_proxy_sets_tls_context}, + {"upstream_create_http_proxy_no_tls_context", test_upstream_create_http_proxy_no_tls_context}, {"tls_reload_when_certificate_file_changes", test_tls_reload_when_certificate_file_changes}, #ifdef FLB_SYSTEM_LINUX {"tls_reload_when_certificate_file_is_replaced", test_tls_reload_when_certificate_file_is_replaced}, diff --git a/tests/internal/utils.c b/tests/internal/utils.c index 680b093fbe8..816022704da 100644 --- a/tests/internal/utils.c +++ b/tests/internal/utils.c @@ -658,8 +658,18 @@ struct proxy_url_check proxy_url_checks[] = { /* issue #5530. Password contains @ */ {0, "http://example_user:example_pass_w_@_char@proxy.com:8080", "http", "proxy.com", "8080", "example_user", "example_pass_w_@_char"}, - {-1, "https://proxy.com:8080", - NULL, NULL, NULL, NULL, NULL} + /* HTTPS proxy with explicit port */ + {0, "https://proxy.com:8080", + "https", "proxy.com", "8080", NULL, NULL}, + /* HTTPS proxy, default port 443 */ + {0, "https://proxy.com", + "https", "proxy.com", "443", NULL, NULL}, + /* HTTPS proxy with credentials */ + {0, "https://user:pass@proxy.com:443", + "https", "proxy.com", "443", "user", "pass"}, + /* Unsupported schemes must be rejected */ + {-1, "ftp://proxy.com:21", NULL, NULL, NULL, NULL, NULL}, + {-1, "socks5://proxy.com", NULL, NULL, NULL, NULL, NULL}, }; From 92d538d4663daaad17dcf010f4443e141e4e2037 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 28 May 2026 05:34:42 -0300 Subject: [PATCH 06/15] tls: document lock-order invariant in tls_session_destroy MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a short comment before the nested outer-context mutex lock in tls_session_destroy() to document that after session_set_outer() the outer backend session is owned only by the inner session, and no path should independently use it or acquire outer-context then inner-context locks for the same chained pair. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- src/tls/openssl.c | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/tls/openssl.c b/src/tls/openssl.c index bf3597571b2..dc8e5315545 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -1530,6 +1530,10 @@ static int tls_session_destroy(void *session) * Free it explicitly now, under its own context mutex. */ if (ptr->outer_session != NULL) { + /* After session_set_outer(), the outer backend session is owned only + * by the inner session; no path should independently use it or acquire + * outer-context then inner-context locks for the same chained pair. + */ outer_ctx = ptr->outer_session->parent; pthread_mutex_lock(&outer_ctx->mutex); SSL_free(ptr->outer_session->ssl); From 4f02d9640af887779f2ab21217e6e19bcaf4fc43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 4 Jun 2026 22:50:40 -0300 Subject: [PATCH 07/15] tests: internal: guard HTTPS proxy cases on FLB_HAVE_TLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- tests/internal/utils.c | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/internal/utils.c b/tests/internal/utils.c index 816022704da..888644c826b 100644 --- a/tests/internal/utils.c +++ b/tests/internal/utils.c @@ -658,6 +658,8 @@ struct proxy_url_check proxy_url_checks[] = { /* issue #5530. Password contains @ */ {0, "http://example_user:example_pass_w_@_char@proxy.com:8080", "http", "proxy.com", "8080", "example_user", "example_pass_w_@_char"}, + /* HTTPS proxy cases: accepted only when TLS support is compiled in */ +#ifdef FLB_HAVE_TLS /* HTTPS proxy with explicit port */ {0, "https://proxy.com:8080", "https", "proxy.com", "8080", NULL, NULL}, @@ -667,6 +669,12 @@ struct proxy_url_check proxy_url_checks[] = { /* HTTPS proxy with credentials */ {0, "https://user:pass@proxy.com:443", "https", "proxy.com", "443", "user", "pass"}, +#else + /* Without TLS support, HTTPS proxy URLs must be rejected */ + {-1, "https://proxy.com:8080", NULL, NULL, NULL, NULL, NULL}, + {-1, "https://proxy.com", NULL, NULL, NULL, NULL, NULL}, + {-1, "https://user:pass@proxy.com:443", NULL, NULL, NULL, NULL, NULL}, +#endif /* Unsupported schemes must be rejected */ {-1, "ftp://proxy.com:21", NULL, NULL, NULL, NULL, NULL}, {-1, "socks5://proxy.com", NULL, NULL, NULL, NULL, NULL}, From 90762e24603ca35da530cbec0917672938186124 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:06:18 -0300 Subject: [PATCH 08/15] upstream: add flb_upstream_proxy_tls_setup() for proxy TLS config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add flb_upstream_proxy_tls_setup() to rebuild an upstream's HTTPS proxy TLS context (u->proxy_tls_context) using caller-supplied verify/verify_hostname/ca_path/ca_file values instead of the hardcoded defaults set by flb_upstream_create() (verify and verify_hostname on, no CA). This lets callers trust HTTPS proxies signed by a private or corporate CA, independent of the destination TLS settings. The proxy hostname (u->tcp_host) is reused as the SNI vhost for the rebuilt context, matching what flb_upstream_create() already does. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- include/fluent-bit/flb_upstream.h | 6 ++++ src/flb_upstream.c | 46 +++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) diff --git a/include/fluent-bit/flb_upstream.h b/include/fluent-bit/flb_upstream.h index 63593b1372b..c809e683ffa 100644 --- a/include/fluent-bit/flb_upstream.h +++ b/include/fluent-bit/flb_upstream.h @@ -108,6 +108,12 @@ struct flb_upstream *flb_upstream_create_url(struct flb_config *config, int flb_upstream_destroy(struct flb_upstream *u); +#ifdef FLB_HAVE_TLS +int flb_upstream_proxy_tls_setup(struct flb_upstream *u, + int verify, int verify_hostname, + const char *ca_path, const char *ca_file); +#endif + int flb_upstream_set_property(struct flb_config *config, struct flb_net_setup *net, char *k, char *v); int flb_upstream_is_async(struct flb_upstream *u); diff --git a/src/flb_upstream.c b/src/flb_upstream.c index 93d290ebaf5..5248c6e3d61 100644 --- a/src/flb_upstream.c +++ b/src/flb_upstream.c @@ -399,6 +399,52 @@ struct flb_upstream *flb_upstream_create(struct flb_config *config, return u; } +#ifdef FLB_HAVE_TLS +/* + * Reconfigure the HTTPS proxy TLS context (if any) created by + * flb_upstream_create() using caller-provided verification settings. This is + * intentionally independent from the destination TLS context: proxy and + * destination are different TLS peers and must not share ca_file/ca_path/ + * verify settings. + * + * If the upstream has no proxy_tls_context (no HTTPS proxy in effect), this + * is a no-op. + */ +int flb_upstream_proxy_tls_setup(struct flb_upstream *u, + int verify, int verify_hostname, + const char *ca_path, const char *ca_file) +{ + struct flb_tls *tls; + + if (!u || !u->proxy_tls_context) { + return 0; + } + + /* + * u->tcp_host already holds the proxy hostname at this point (set by + * flb_upstream_create() when a proxy is in effect), so reuse it as the + * SNI vhost for the rebuilt context. + */ + tls = flb_tls_create(FLB_TLS_CLIENT_MODE, + verify, 0, + u->tcp_host, + ca_path, ca_file, + NULL, NULL, NULL); + if (!tls) { + flb_error("[upstream] could not reconfigure TLS context for HTTPS proxy %s", + u->tcp_host); + return -1; + } + + flb_tls_set_verify_hostname(tls, verify_hostname); + + flb_tls_destroy(u->proxy_tls_context); + u->proxy_tls_context = tls; + + return 0; +} +#endif + /* * Checks whehter a destinate URL should be proxied. */ From 48e2749c21d54a2089b366d4583ce78868b806c9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:07:16 -0300 Subject: [PATCH 09/15] output: add tls.proxy.* config options for HTTPS proxy TLS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tls.proxy.verify, tls.proxy.verify_hostname, tls.proxy.ca_path and tls.proxy.ca_file instance properties, following the existing tls.* naming convention but kept as separate fields so the proxy leg never inherits the destination's tls.* settings. Defaults are verify and verify_hostname on, matching flb_upstream_create()'s previous hardcoded behavior, so existing configurations are unaffected. flb_output_upstream_set() now calls flb_upstream_proxy_tls_setup() with these values whenever the upstream has an HTTPS proxy in effect. This is the single call site all output plugins funnel through, so no plugin files need changes to pick up the new options. Also destroy the proxy TLS context when net.proxy_env_ignore reverts a proxy, instead of leaving it allocated but inert. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- include/fluent-bit/flb_output.h | 10 +++++++ src/flb_output.c | 47 +++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/include/fluent-bit/flb_output.h b/include/fluent-bit/flb_output.h index 8e2b8fc3512..0c066954905 100644 --- a/include/fluent-bit/flb_output.h +++ b/include/fluent-bit/flb_output.h @@ -380,6 +380,16 @@ struct flb_output_instance { int tls_win_use_enterprise_certstore; /* Use enterprise CertStore */ char *tls_win_thumbprints; /* CertStore Thumbprints (Windows) */ # endif + + /* + * HTTPS proxy TLS settings: independent from the destination tls.* + * settings above, since the proxy leg and the destination leg are + * different TLS peers. + */ + int tls_proxy_verify; /* Verify proxy cert (default: true) */ + int tls_proxy_verify_hostname; /* Verify proxy hostname (default: true) */ + char *tls_proxy_ca_path; /* Path to CA certs for proxy verification */ + char *tls_proxy_ca_file; /* CA root cert for proxy verification */ #endif /* diff --git a/src/flb_output.c b/src/flb_output.c index 517608dfddf..3631c4486e3 100644 --- a/src/flb_output.c +++ b/src/flb_output.c @@ -200,6 +200,12 @@ static void flb_output_free_properties(struct flb_output_instance *ins) if (ins->tls_ciphers) { flb_sds_destroy(ins->tls_ciphers); } + if (ins->tls_proxy_ca_path) { + flb_sds_destroy(ins->tls_proxy_ca_path); + } + if (ins->tls_proxy_ca_file) { + flb_sds_destroy(ins->tls_proxy_ca_file); + } # if defined(FLB_SYSTEM_WINDOWS) if (ins->tls_win_certstore_name) { flb_sds_destroy(ins->tls_win_certstore_name); @@ -849,6 +855,10 @@ struct flb_output_instance *flb_output_new(struct flb_config *config, instance->tls_win_use_enterprise_certstore = FLB_FALSE; instance->tls_win_thumbprints = NULL; # endif + instance->tls_proxy_verify = FLB_TRUE; + instance->tls_proxy_verify_hostname = FLB_TRUE; + instance->tls_proxy_ca_path = NULL; + instance->tls_proxy_ca_file = NULL; #endif if (plugin->flags & FLB_OUTPUT_NET) { @@ -1114,6 +1124,20 @@ int flb_output_set_property(struct flb_output_instance *ins, else if (prop_key_check("tls.ciphers", k, len) == 0) { flb_utils_set_plugin_string_property("tls.ciphers", &ins->tls_ciphers, tmp); } + else if (prop_key_check("tls.proxy.verify", k, len) == 0 && tmp) { + ins->tls_proxy_verify = flb_utils_bool(tmp); + flb_sds_destroy(tmp); + } + else if (prop_key_check("tls.proxy.verify_hostname", k, len) == 0 && tmp) { + ins->tls_proxy_verify_hostname = flb_utils_bool(tmp); + flb_sds_destroy(tmp); + } + else if (prop_key_check("tls.proxy.ca_path", k, len) == 0) { + flb_utils_set_plugin_string_property("tls.proxy.ca_path", &ins->tls_proxy_ca_path, tmp); + } + else if (prop_key_check("tls.proxy.ca_file", k, len) == 0) { + flb_utils_set_plugin_string_property("tls.proxy.ca_file", &ins->tls_proxy_ca_file, tmp); + } # if defined(FLB_SYSTEM_WINDOWS) else if (prop_key_check("tls.windows.certstore_name", k, len) == 0 && tmp) { flb_utils_set_plugin_string_property("tls.windows.certstore_name", &ins->tls_win_certstore_name, tmp); @@ -1894,8 +1918,31 @@ int flb_output_upstream_set(struct flb_upstream *u, struct flb_output_instance * flb_free(u->proxy_password); u->proxy_password = NULL; } + +#ifdef FLB_HAVE_TLS + if (u->proxy_tls_context) { + flb_tls_destroy(u->proxy_tls_context); + u->proxy_tls_context = NULL; + } +#endif } +#ifdef FLB_HAVE_TLS + /* + * If flb_upstream_create() built a proxy TLS context (HTTPS proxy in + * effect), reconfigure it using this instance's tls.proxy.* settings + * instead of the hardcoded defaults. Independent from ins->tls* + * (destination TLS settings) by design. + */ + if (u->proxy_tls_context != NULL) { + flb_upstream_proxy_tls_setup(u, + ins->tls_proxy_verify, + ins->tls_proxy_verify_hostname, + ins->tls_proxy_ca_path, + ins->tls_proxy_ca_file); + } +#endif + return 0; } From 53202b239f90e628a9b9a468ad0a92d759a2a934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:07:59 -0300 Subject: [PATCH 10/15] tls: document tls.proxy.* config options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tls.proxy.ca_file, tls.proxy.ca_path, tls.proxy.verify and tls.proxy.verify_hostname to tls_configmap[], so they show up in generated docs and --help output the same way the destination tls.* options already do. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- src/tls/flb_tls.c | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/tls/flb_tls.c b/src/tls/flb_tls.c index 2aab79052d4..808134e9eaf 100644 --- a/src/tls/flb_tls.c +++ b/src/tls/flb_tls.c @@ -106,6 +106,32 @@ struct flb_config_map tls_configmap[] = { "Specify TLS ciphers up to TLSv1.2" }, + { + FLB_CONFIG_MAP_STR, "tls.proxy.ca_file", NULL, + 0, FLB_FALSE, 0, + "Absolute path to CA certificate file used to verify the HTTPS proxy " + "certificate. Independent from tls.ca_file (destination CA)" + }, + + { + FLB_CONFIG_MAP_STR, "tls.proxy.ca_path", NULL, + 0, FLB_FALSE, 0, + "Absolute path to scan for CA certificate files used to verify the " + "HTTPS proxy certificate" + }, + + { + FLB_CONFIG_MAP_BOOL, "tls.proxy.verify", "on", + 0, FLB_FALSE, 0, + "Force HTTPS proxy certificate validation" + }, + + { + FLB_CONFIG_MAP_BOOL, "tls.proxy.verify_hostname", "on", + 0, FLB_FALSE, 0, + "Enable or disable HTTPS proxy hostname verification" + }, + /* EOF */ {0} }; From 2da599ca7c091bd0789013451d56566592f1dbc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:09:01 -0300 Subject: [PATCH 11/15] io: keep proxy TLS state off the shared destination stream flags MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flb_io_net_connect() was calling flb_stream_enable_flags(stream, FLB_IO_TLS) whenever an HTTPS proxy's TLS session was in use, so that flb_io_net_write/read would route bytes through the proxy TLS session even for plain HTTP destinations. This mutated the shared upstream/stream object, not just the current connection, so it permanently marked the destination itself as TLS-enabled the first time any connection went through an HTTPS proxy. That pollution is visible in flb_http_client.c's Host header logic, which reads the stream's FLB_IO_TLS flag to decide whether to omit the port for HTTPS-on-443. A plain HTTP destination on port 443 behind an HTTPS proxy would incorrectly get "Host: example.com" instead of "Host: example.com:443". Add a new connection-scoped flag, FLB_IO_PROXY_TLS, and set it via flb_connection_enable_flags() instead of mutating the stream. Both flb_io_net_write() and flb_io_net_read() already read connection flags via flb_connection_get_flags(), so their TLS-session dispatch now also checks FLB_IO_PROXY_TLS, without ever touching the destination stream's own FLB_IO_TLS flag. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- include/fluent-bit/flb_io.h | 7 +++++++ src/flb_io.c | 23 ++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/include/fluent-bit/flb_io.h b/include/fluent-bit/flb_io.h index 45f14f5cfb4..e00f6d05cdd 100644 --- a/include/fluent-bit/flb_io.h +++ b/include/fluent-bit/flb_io.h @@ -40,6 +40,13 @@ /* Other features */ #define FLB_IO_IPV6 128 /* network I/O uses IPv6 */ +#define FLB_IO_PROXY_TLS 256 /* connection-scoped: an HTTPS proxy TLS + * session is active on this connection. + * Independent of FLB_IO_TLS, which + * describes the destination stream: must + * only ever be set on struct + * flb_connection.flags, never on the + * shared stream/upstream flags. */ struct flb_connection; diff --git a/src/flb_io.c b/src/flb_io.c index c912a66297b..c7a0e745d63 100644 --- a/src/flb_io.c +++ b/src/flb_io.c @@ -190,14 +190,19 @@ int flb_io_net_connect(struct flb_connection *connection, } /* * Ensure all I/O (the CONNECT request and any subsequent - * data) is routed through the proxy TLS session. This is - * necessary when the ultimate destination is plain HTTP: - * the stream's FLB_IO_TLS flag is not set for such upstreams, - * but flb_io_net_write/read check that flag to decide whether - * to use connection->tls_session. For HTTPS destinations the - * flag is already set so this is a no-op. + * data) is routed through the proxy TLS session. This must be + * a connection-scoped flag, not a stream-level one: the + * stream/upstream object is shared across every connection to + * this destination, and flb_io_net_write/read only need a + * per-connection signal to decide whether to use + * connection->tls_session. Setting the stream's FLB_IO_TLS + * flag here would permanently mark the destination itself as + * TLS-enabled, which corrupts destination-specific behavior + * for plain-HTTP destinations (e.g. Host header port handling + * in flb_http_client.c), even though only the proxy leg uses + * TLS. */ - flb_stream_enable_flags(connection->stream, FLB_IO_TLS); + flb_connection_enable_flags(connection, FLB_IO_PROXY_TLS); } #endif ret = flb_http_client_proxy_connect(connection); @@ -794,7 +799,7 @@ int flb_io_net_write(struct flb_connection *connection, const void *data, } } #ifdef FLB_HAVE_TLS - else if (flags & (FLB_IO_TLS | FLB_IO_DTLS)) { + else if (flags & (FLB_IO_TLS | FLB_IO_DTLS | FLB_IO_PROXY_TLS)) { if (flags & FLB_IO_ASYNC) { ret = flb_tls_net_write_async(coro, connection->tls_session, data, len, out_len); } @@ -846,7 +851,7 @@ ssize_t flb_io_net_read(struct flb_connection *connection, void *buf, size_t len } } #ifdef FLB_HAVE_TLS - else if (flags & (FLB_IO_TLS | FLB_IO_DTLS)) { + else if (flags & (FLB_IO_TLS | FLB_IO_DTLS | FLB_IO_PROXY_TLS)) { if (flags & FLB_IO_ASYNC) { ret = flb_tls_net_read_async(coro, connection->tls_session, buf, len); } From 7d96e782e59c0eeafd1aa93419408cbc2835ebef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Thu, 30 Jul 2026 16:10:06 -0300 Subject: [PATCH 12/15] tests: internal: add coverage for tls.proxy.* and proxy TLS isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add tests for the tls.proxy.* config wiring and the FLB_IO_PROXY_TLS fix. Two tests exercise flb_upstream_proxy_tls_setup() directly, one covering the no-proxy no-op case and the other checking that ca_file and verify_hostname are correctly applied to proxy_tls_context. Another test is a pure state check proving that the new FLB_IO_PROXY_TLS flag lands on the connection rather than leaking into the shared stream. A network-free regression test targets the exact bug reported. A plain HTTP destination on port 443 behind an HTTPS proxy must keep its port in the Host header. It inspects the queued Host header directly via flb_http_client(), so it needs no live connection. Finally, a small loopback proxy stub (pthread plus raw OpenSSL, guarded to non-Windows systems) backs three live checks covering a trusted-CA connect success, an untrusted-CA rejection, and a genuine nested TLS-in-TLS handshake for an HTTPS destination behind an HTTPS proxy, including an encrypted application-data round trip. The nested case exercises the automatic session_set_outer chaining in flb_tls_session_create end to end. The live checks add a dedicated self-signed cert/key pair (proxy_stub_certificate.pem / proxy_stub_private_key.pem) with a long validity window, since the existing certificate.pem fixture has already expired and a real handshake enforces that. They also close over a connection-ownership bug in the test helper itself: the proxy test connection is registered in the upstream's busy queue for flb_upstream_destroy() to reap, so freeing it a second time in the local cleanup helper was a double free once the upstream was destroyed. Confirmed with a normal run and under Valgrind (0 errors, all blocks freed) inside a Debian container, since this macOS host is missing the build toolchain. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- .../data/tls/proxy_stub_certificate.pem | 19 + .../data/tls/proxy_stub_private_key.pem | 28 + tests/internal/upstream_tls.c | 691 ++++++++++++++++++ 3 files changed, 738 insertions(+) create mode 100644 tests/internal/data/tls/proxy_stub_certificate.pem create mode 100644 tests/internal/data/tls/proxy_stub_private_key.pem diff --git a/tests/internal/data/tls/proxy_stub_certificate.pem b/tests/internal/data/tls/proxy_stub_certificate.pem new file mode 100644 index 00000000000..f0dfd38c9fd --- /dev/null +++ b/tests/internal/data/tls/proxy_stub_certificate.pem @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDFTCCAf2gAwIBAgIUVjWmq/vJ3OPe8KH07Wj3e798GQwwDQYJKoZIhvcNAQEL +BQAwGTEXMBUGA1UEAwwOZmxiLXByb3h5LXN0dWIwIBcNMjYwNzMwMTkzODQ3WhgP +MjEyNjA3MDYxOTM4NDdaMBkxFzAVBgNVBAMMDmZsYi1wcm94eS1zdHViMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAyy3TMVe1/SoO8epN+zwW1elgjWH3 +urb9rI0IEyCSS/mE1GZ/deZ9ks0fmOSN6nPWQGu5IE3dgQrFe53ejNpFFY31NwLO +owslrE9zBTKF8x0qOFt+gPYxMcLkq7RtUtxkH24jIXsRw4McOy9J9b2CBAq6vk6M +WlCj1fBExU9Lnsm9maUeIxNoogOvwyRk9p5vOGavCdUQEJe90LcZ3QD03+RBU1Wx +O1BkfmcY01tx/kIwPbU3Tppiit+k3to4NNQWHsNPV7Cj2JAwhcXSZEmSk9odc1sL +sA2rCqhpM8td+JtHsct/cRCIdgF3c8JTRjW68m088FoDRDAymtcbr/jTnQIDAQAB +o1MwUTAdBgNVHQ4EFgQUYzUTPh4/wX2DxPa+YliDay6bg8IwHwYDVR0jBBgwFoAU +YzUTPh4/wX2DxPa+YliDay6bg8IwDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B +AQsFAAOCAQEAoIq3ctlzV7JEMDrlubPJJdU9FsWXwihKpsAsGpx2jo3T8Fy8YoP7 +d1wIgXMs1QNPzMZyPsqnsC0gRRzdpc+s0r0KCo0gzog8RWi3V6mAuhIq4Rs+m7Cr +y2uYPSLoUh2kLlZPNyzRdFX0yvR4r2dD4kwqKa/kQISKM+OPbpIj5cLllVIzQjST +JQJYSPQFMjKvG86n/RGsEVTA0iplzdWS1jLHTQctvIFhn9OGJQYZtzxZ2mlcJOR/ +xzUm5OyGw/xs2BYoqo2PrHlI3zL0xDS0yGfWi9Lgfwi8Ro/XBBCdUFZrZS1XKUEy +RE1xwIDHbBlgtgj49Ysbm5arMXr0WNbG0w== +-----END CERTIFICATE----- diff --git a/tests/internal/data/tls/proxy_stub_private_key.pem b/tests/internal/data/tls/proxy_stub_private_key.pem new file mode 100644 index 00000000000..f087f9dba50 --- /dev/null +++ b/tests/internal/data/tls/proxy_stub_private_key.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDLLdMxV7X9Kg7x +6k37PBbV6WCNYfe6tv2sjQgTIJJL+YTUZn915n2SzR+Y5I3qc9ZAa7kgTd2BCsV7 +nd6M2kUVjfU3As6jCyWsT3MFMoXzHSo4W36A9jExwuSrtG1S3GQfbiMhexHDgxw7 +L0n1vYIECrq+ToxaUKPV8ETFT0ueyb2ZpR4jE2iiA6/DJGT2nm84Zq8J1RAQl73Q +txndAPTf5EFTVbE7UGR+ZxjTW3H+QjA9tTdOmmKK36Te2jg01BYew09XsKPYkDCF +xdJkSZKT2h1zWwuwDasKqGkzy134m0exy39xEIh2AXdzwlNGNbrybTzwWgNEMDKa +1xuv+NOdAgMBAAECggEAERdswaTYOVC1ayJDpxO59AqqxZntJfbIxiAjAsDlraBy +zQ9GP5vBCn1Y4Bqx2XwfFNI7A32pWXaXgDuaggzGbPbBHvyMd2izpZgVW+WRIQ9e +AtmnfZ+4KbB0XgFShPrnLUGtkN5ycxjvYgcrWJG1gzOH3ARtiBdqgM+yaUqvWM8H +MXpeoP3wwcWozm51aPCGQZQOp0piIVxNQh0kCQ/lO1/P0vSBkGzqd3OxxEQF3h74 +deu6GGhkuRFHEnqlFp4Dsww3YfAeG9hcOZbWenlA0K5bWHnHiQvGU6oTgFlRGg4C +TYjSWE1Pd9xmo6kRMSky1NjFsRlRcWGUp85He6WM3QKBgQD4p7DFIFED44fYei3R +VkQN1xc3IAP/WtOuA8qimA2RKusLRoglHkCjcbaR7l9HPW/JR+OfBAJnLKjMTNuy +dZh5tdotjLZfm6oM6xKPnf5LlMZDySW5kH1wwCnV6EDk6iEO4PYe8piI5hjMff1V +3GVAYclflxAYFpoE0jShfQx30wKBgQDRLj+RjXuXO6eyIdoec1wix6/D6ZUwq2/k +OXjJowhdYQ/osu88SzJCxhmN6Ho71bm3w21iyow6RFl7A38HtVZX3iorHonj1s/A ++itLQgLe4/9e8W9RhxnAv5YoMdLwJZjmOlRiWIAWucDlR8nfW92S8Q8bKSJsVGfh +eNCTF4ZQzwKBgQCGLsf/uK6/cHuQYG51Nx5Gcbn/b3F4zWTJ1RX2pCM+IXDxhsBV +d3veN7EUExSrdwXqErdMyncZgkoc5aTxKvB7TZ1NU0/fTDt5+SjtA4+E7eYIEnYf +K0/mFmz9F/ZdEdOBGfj3Pmb+CV18jMi3GtYw5hQrV3PB7AB1dZIdS0P9LQKBgHu6 +3HUO+BTbf3T5Wxh56fzUmt3KU6nnLhcT+6NhYPhDCnL9LE9At2kR3lm3Ml4kLGdL +HG+8GTf55+fAiKoZlAoDanP4610uWdLu5x7r2+DVgd859juGKzB84qfB2OCdjJRZ +2kIV0SYgF75HOVx57VUWa3YIXckwIAYZPV5UIum3AoGAWTzXerP4Pgwu2Ok0r/pp +jDkjoEFKsxs7q9K0ZFK/nMj0mlhe6fhx0XiyKrUlH9+wuHaK/fyj7PLOnt3I6n+4 +xq58MijGVjkAO5WU+p7IYF8Hj5esuC5FwnC2n3bY9ORfhSYa3vR20OkJlrCswiww +IbIhtxXcPXme9+9ey/MyLns= +-----END PRIVATE KEY----- diff --git a/tests/internal/upstream_tls.c b/tests/internal/upstream_tls.c index f3a736693ef..7fad52baf17 100644 --- a/tests/internal/upstream_tls.c +++ b/tests/internal/upstream_tls.c @@ -8,6 +8,9 @@ #include #include #include +#include +#include +#include #include "flb_tests_internal.h" @@ -19,6 +22,15 @@ #ifdef FLB_SYSTEM_WINDOWS #include +#else +#include +#include +#include +#include +#include +#include +#include +#define FLB_TEST_HAVE_PROXY_STUB #endif struct test_backend_ctx { @@ -465,6 +477,675 @@ void test_tls_reload_does_not_hide_concurrent_file_change(void) flb_free(dst_key); } +/* + * Verify that flb_upstream_proxy_tls_setup() is a safe no-op when the + * upstream has no HTTPS proxy in effect (proxy_tls_context == NULL). + */ +void test_upstream_proxy_tls_setup_noop_without_proxy(void) +{ + struct flb_config *config; + struct flb_upstream *u; + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + return; + } + + u = flb_upstream_create(config, "dest.example.com", 80, FLB_IO_TCP, NULL); + TEST_CHECK(u != NULL); + if (u == NULL) { + flb_config_exit(config); + return; + } + + TEST_CHECK(u->proxy_tls_context == NULL); + TEST_CHECK(flb_upstream_proxy_tls_setup(u, FLB_TRUE, FLB_TRUE, NULL, NULL) == 0); + TEST_CHECK(u->proxy_tls_context == NULL); + + flb_upstream_destroy(u); + flb_config_exit(config); +} + +/* + * Verify that flb_upstream_proxy_tls_setup() rebuilds the proxy TLS context + * using the caller-supplied ca_file/verify/verify_hostname instead of the + * hardcoded defaults set by flb_upstream_create(), and that these settings + * are independent from the destination TLS context. + */ +void test_upstream_proxy_tls_setup_configures_ca(void) +{ + struct flb_config *config; + struct flb_upstream *u; + char ca_file[4096]; + + snprintf(ca_file, sizeof(ca_file), "%sdata/tls/certificate.pem", + FLB_TESTS_DATA_PATH); + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + return; + } + + config->http_proxy = "https://proxy.example.com:8080"; + u = flb_upstream_create(config, "dest.example.com", 443, FLB_IO_TLS, NULL); + config->http_proxy = NULL; + + TEST_CHECK(u != NULL); + if (u == NULL) { + flb_config_exit(config); + return; + } + + TEST_CHECK(u->proxy_tls_context != NULL); + if (u->proxy_tls_context == NULL) { + flb_upstream_destroy(u); + flb_config_exit(config); + return; + } + + /* Hardcoded defaults from flb_upstream_create(): no CA, verify_hostname on. */ + TEST_CHECK(u->proxy_tls_context->ca_file == NULL); + TEST_CHECK(u->proxy_tls_context->verify_hostname == FLB_TRUE); + + TEST_CHECK(flb_upstream_proxy_tls_setup(u, FLB_TRUE, FLB_FALSE, + NULL, ca_file) == 0); + + TEST_CHECK(u->proxy_tls_context != NULL); + if (u->proxy_tls_context != NULL) { + TEST_CHECK(u->proxy_tls_context->ca_file != NULL); + TEST_MSG("proxy_tls_context->ca_file should reflect tls.proxy.ca_file"); + if (u->proxy_tls_context->ca_file != NULL) { + TEST_CHECK(strcmp(u->proxy_tls_context->ca_file, ca_file) == 0); + } + TEST_CHECK(u->proxy_tls_context->verify_hostname == FLB_FALSE); + TEST_MSG("proxy_tls_context->verify_hostname should reflect tls.proxy.verify_hostname"); + + /* The vhost is reused from the proxy hostname, independent of any + * destination tls.vhost setting. */ + TEST_CHECK(u->proxy_tls_context->vhost != NULL); + if (u->proxy_tls_context->vhost != NULL) { + TEST_CHECK(strcmp(u->proxy_tls_context->vhost, "proxy.example.com") == 0); + } + } + + flb_upstream_destroy(u); + flb_config_exit(config); +} + +/* + * Verify that the connection-scoped FLB_IO_PROXY_TLS flag (set by the fixed + * flb_io.c proxy-connect path) never leaks into the shared stream's flags. + * This directly protects flb_http_client.c's Host-header logic, which reads + * the stream's FLB_IO_TLS flag to decide whether to omit the port. + */ +void test_io_proxy_tls_flag_does_not_leak_to_stream(void) +{ + struct flb_connection conn = {0}; + struct flb_upstream upstream = {0}; + struct flb_config config = {0}; + flb_pipefd_t socket_pair[2]; + + TEST_CHECK(setup_conn(&conn, &upstream, &config, socket_pair) == 0); + + /* Simulate what flb_io_net_connect() now does for an HTTPS proxy leg. */ + flb_connection_enable_flags(&conn, FLB_IO_PROXY_TLS); + + TEST_CHECK(flb_stream_get_flag_status(&upstream.base, FLB_IO_TLS) == FLB_FALSE); + TEST_MSG("the shared stream must never observe FLB_IO_PROXY_TLS as FLB_IO_TLS"); + + TEST_CHECK((flb_connection_get_flags(&conn) & FLB_IO_PROXY_TLS) != 0); + TEST_CHECK((flb_connection_get_flags(&conn) & FLB_IO_TLS) == 0); + + flb_pipe_close(socket_pair[1]); + flb_pipe_close(conn.fd); +} + +/* + * Regression test for the exact scenario edsiper flagged: a plain HTTP + * destination on port 443 reached through an HTTPS proxy must keep ":443" + * in its Host header. Before the fix, flb_io_net_connect() enabled + * FLB_IO_TLS on the shared destination stream merely because the proxy leg + * was TLS, which made flb_http_client.c's is_https_default_port check treat + * the destination as HTTPS-on-default-port and drop the port. + * + * This test never performs any I/O: add_host_and_content_length() queues + * the computed Host header onto flb_http_client's headers list at + * flb_http_client() call time, before any network access is attempted. + */ +void test_http_client_host_header_not_polluted_by_proxy_tls(void) +{ + struct flb_connection conn = {0}; + struct flb_upstream upstream = {0}; + struct flb_config config = {0}; + struct flb_http_client *c; + struct flb_kv *kv; + struct mk_list *head; + int found_with_port = FLB_FALSE; + int found_without_port = FLB_FALSE; + + config.is_shutting_down = FLB_FALSE; + upstream.base.config = &config; + upstream.base.type = FLB_UPSTREAM; + upstream.base.transport = FLB_TRANSPORT_TCP; + /* Intentionally no FLB_IO_TLS here: this reproduces the fixed state + * where only the proxy leg is TLS and the destination stream flags + * are left untouched. */ + + conn.type = FLB_UPSTREAM_CONNECTION; + conn.fd = -1; + conn.stream = (struct flb_stream *) &upstream; + conn.net = &upstream.base.net; + + c = flb_http_client(&conn, FLB_HTTP_GET, "/", NULL, 0, + "dest.example.com", 443, NULL, 0); + TEST_CHECK(c != NULL); + if (c == NULL) { + return; + } + + mk_list_foreach(head, &c->headers) { + kv = mk_list_entry(head, struct flb_kv, _head); + if (flb_sds_casecmp(kv->key, "Host", 4) == 0) { + if (strcmp(kv->val, "dest.example.com:443") == 0) { + found_with_port = FLB_TRUE; + } + else if (strcmp(kv->val, "dest.example.com") == 0) { + found_without_port = FLB_TRUE; + } + } + } + + TEST_CHECK(found_with_port == FLB_TRUE); + TEST_MSG("Host header should retain :443 for a plain HTTP destination reached through an HTTPS proxy"); + TEST_CHECK(found_without_port == FLB_FALSE); + TEST_MSG("Host header must not drop :443 due to proxy-only TLS being mistaken for destination TLS"); + + flb_http_client_destroy(c); +} + +#ifdef FLB_TEST_HAVE_PROXY_STUB +/* + * Minimal loopback "HTTPS CONNECT proxy" used to exercise the real + * flb_io_net_connect() path end-to-end: TCP connect, proxy TLS handshake, + * HTTP CONNECT tunneling, and (optionally) a second, nested TLS handshake + * for the destination leg, mirroring what a real HTTPS destination behind + * an HTTPS proxy looks like on the wire. + */ +struct https_proxy_stub { + int listen_fd; + int port; + pthread_t thread; + SSL_CTX *ssl_ctx; + int nested_tls; /* also perform an inner TLS handshake after CONNECT */ + int outer_ok; /* outer handshake + CONNECT ack completed */ + int inner_ok; /* nested_tls only: inner handshake + echo round-trip ok */ +}; + +static void *https_proxy_stub_thread(void *arg) +{ + struct https_proxy_stub *stub = (struct https_proxy_stub *) arg; + int fd; + SSL *ssl; + char buf[1024]; + int n; + + fd = accept(stub->listen_fd, NULL, NULL); + if (fd < 0) { + return NULL; + } + + ssl = SSL_new(stub->ssl_ctx); + if (!ssl) { + close(fd); + return NULL; + } + SSL_set_fd(ssl, fd); + + if (SSL_accept(ssl) <= 0) { + SSL_free(ssl); + close(fd); + return NULL; + } + + /* Read (and discard) the CONNECT request. */ + n = SSL_read(ssl, buf, sizeof(buf) - 1); + if (n <= 0) { + SSL_free(ssl); + close(fd); + return NULL; + } + + /* Acknowledge the tunnel. */ + n = SSL_write(ssl, "HTTP/1.1 200 Connection Established\r\n\r\n", 40); + if (n <= 0) { + SSL_free(ssl); + close(fd); + return NULL; + } + + stub->outer_ok = FLB_TRUE; + + if (stub->nested_tls) { + BIO *bio; + SSL *inner_ssl; + + /* + * Layer a second, inner TLS server handshake on top of the + * already-established outer TLS stream, mirroring what + * tls_session_set_outer()/BIO_f_ssl() do on the client side in + * src/tls/openssl.c for the destination leg of TLS-in-TLS. + */ + bio = BIO_new(BIO_f_ssl()); + if (bio != NULL) { + BIO_set_ssl(bio, ssl, BIO_NOCLOSE); + + inner_ssl = SSL_new(stub->ssl_ctx); + if (inner_ssl != NULL) { + SSL_set_bio(inner_ssl, bio, bio); + + if (SSL_accept(inner_ssl) > 0) { + n = SSL_read(inner_ssl, buf, sizeof(buf) - 1); + if (n > 0) { + buf[n] = '\0'; + if (strcmp(buf, "ping") == 0 && + SSL_write(inner_ssl, "pong", 4) > 0) { + stub->inner_ok = FLB_TRUE; + } + } + SSL_shutdown(inner_ssl); + } + + /* Frees 'bio' too; BIO_NOCLOSE above keeps 'ssl' alive. */ + SSL_free(inner_ssl); + } + } + } + + SSL_shutdown(ssl); + SSL_free(ssl); + close(fd); + + return NULL; +} + +static int start_https_proxy_stub(struct https_proxy_stub *stub, + const char *crt_file, const char *key_file, + int nested_tls) +{ + struct sockaddr_in addr; + socklen_t addr_len; + int fd; + int one = 1; + + memset(stub, 0, sizeof(*stub)); + stub->listen_fd = -1; + stub->nested_tls = nested_tls; + + fd = socket(AF_INET, SOCK_STREAM, 0); + if (fd < 0) { + return -1; + } + setsockopt(fd, SOL_SOCKET, SO_REUSEADDR, &one, sizeof(one)); + + memset(&addr, 0, sizeof(addr)); + addr.sin_family = AF_INET; + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + addr.sin_port = 0; + + if (bind(fd, (struct sockaddr *) &addr, sizeof(addr)) != 0 || + listen(fd, 1) != 0) { + close(fd); + return -1; + } + + addr_len = sizeof(addr); + if (getsockname(fd, (struct sockaddr *) &addr, &addr_len) != 0) { + close(fd); + return -1; + } + + stub->listen_fd = fd; + stub->port = ntohs(addr.sin_port); + + stub->ssl_ctx = SSL_CTX_new(TLS_server_method()); + if (!stub->ssl_ctx) { + close(fd); + stub->listen_fd = -1; + return -1; + } + + if (SSL_CTX_use_certificate_file(stub->ssl_ctx, crt_file, SSL_FILETYPE_PEM) != 1 || + SSL_CTX_use_PrivateKey_file(stub->ssl_ctx, key_file, SSL_FILETYPE_PEM) != 1) { + SSL_CTX_free(stub->ssl_ctx); + stub->ssl_ctx = NULL; + close(fd); + stub->listen_fd = -1; + return -1; + } + + if (pthread_create(&stub->thread, NULL, https_proxy_stub_thread, stub) != 0) { + SSL_CTX_free(stub->ssl_ctx); + stub->ssl_ctx = NULL; + close(fd); + stub->listen_fd = -1; + return -1; + } + + return 0; +} + +static void stop_https_proxy_stub(struct https_proxy_stub *stub) +{ + if (stub->listen_fd >= 0) { + pthread_join(stub->thread, NULL); + close(stub->listen_fd); + stub->listen_fd = -1; + } + if (stub->ssl_ctx != NULL) { + SSL_CTX_free(stub->ssl_ctx); + stub->ssl_ctx = NULL; + } +} + +static struct flb_connection *build_proxy_test_connection(struct flb_upstream *u) +{ + struct flb_connection *conn; + + conn = flb_calloc(1, sizeof(struct flb_connection)); + if (conn == NULL) { + return NULL; + } + + conn->type = FLB_UPSTREAM_CONNECTION; + conn->fd = -1; + conn->event.fd = -1; + conn->dynamically_allocated = FLB_TRUE; + conn->stream = (struct flb_stream *) u; + conn->net = &u->base.net; + + mk_list_init(&conn->_head); + flb_upstream_queue_init(&u->queue); + mk_list_add(&conn->_head, &u->queue.busy_queue); + + return conn; +} + +static void destroy_proxy_test_connection(struct flb_connection *conn) +{ + if (conn == NULL) { + return; + } + + if (conn->fd > 0) { + flb_socket_close(conn->fd); + conn->fd = -1; + conn->event.fd = -1; + } + + /* + * Do not free 'conn' here: build_proxy_test_connection() registered it + * in u->queue.busy_queue with dynamically_allocated = TRUE, so + * flb_upstream_destroy(u) will walk the queue and destroy/free it + * (including any live tls_session) via destroy_conn(). Freeing it here + * too would double-free the connection once flb_upstream_destroy() is + * called. + */ +} + +/* + * A plain HTTP destination (no destination TLS context) reached through a + * trusted HTTPS proxy: the proxy CONNECT tunnel must succeed once + * tls.proxy.ca_file names a CA that trusts the proxy certificate, and the + * connection must come back with FLB_IO_PROXY_TLS set while the shared + * stream's FLB_IO_TLS flag stays untouched. Uses port 443 to also cover + * edsiper's literal "plain HTTP destination on port 443" scenario. + */ +void test_proxy_connect_trusted_ca_succeeds(void) +{ + struct https_proxy_stub stub; + struct flb_config *config; + struct flb_upstream *u; + struct flb_connection *conn; + char crt_file[4096]; + char key_file[4096]; + char proxy_url[64]; + int ret; + + snprintf(crt_file, sizeof(crt_file), "%sdata/tls/proxy_stub_certificate.pem", + FLB_TESTS_DATA_PATH); + snprintf(key_file, sizeof(key_file), "%sdata/tls/proxy_stub_private_key.pem", + FLB_TESTS_DATA_PATH); + + TEST_CHECK(start_https_proxy_stub(&stub, crt_file, key_file, FLB_FALSE) == 0); + if (stub.listen_fd < 0) { + return; + } + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + stop_https_proxy_stub(&stub); + return; + } + + snprintf(proxy_url, sizeof(proxy_url), "https://127.0.0.1:%d", stub.port); + config->http_proxy = proxy_url; + u = flb_upstream_create(config, "dest.example.com", 443, FLB_IO_TCP, NULL); + config->http_proxy = NULL; + + TEST_CHECK(u != NULL); + if (u == NULL) { + flb_config_exit(config); + stop_https_proxy_stub(&stub); + return; + } + + /* Trust the stub's self-signed certificate; hostname verification is + * left off since the cert's CN does not match 127.0.0.1. */ + TEST_CHECK(flb_upstream_proxy_tls_setup(u, FLB_TRUE, FLB_FALSE, + NULL, crt_file) == 0); + + conn = build_proxy_test_connection(u); + TEST_CHECK(conn != NULL); + if (conn != NULL) { + ret = flb_io_net_connect(conn, NULL); + TEST_CHECK(ret == 0); + TEST_MSG("expected proxy CONNECT with a trusted CA to succeed"); + + if (ret == 0) { + TEST_CHECK((flb_connection_get_flags(conn) & FLB_IO_PROXY_TLS) != 0); + TEST_CHECK(flb_stream_get_flag_status(&u->base, FLB_IO_TLS) == FLB_FALSE); + TEST_MSG("destination stream must not be marked TLS by the proxy-only handshake"); + } + + destroy_proxy_test_connection(conn); + } + + stop_https_proxy_stub(&stub); + TEST_CHECK(stub.outer_ok == FLB_TRUE); + + flb_upstream_destroy(u); + flb_config_exit(config); +} + +/* + * An HTTPS proxy whose certificate is not trusted (no tls.proxy.ca_file + * configured, so verification falls back to the system trust store, which + * does not know this self-signed test certificate) must cause the connect + * to fail before any CONNECT request is sent. + */ +void test_proxy_connect_untrusted_ca_rejected(void) +{ + struct https_proxy_stub stub; + struct flb_config *config; + struct flb_upstream *u; + struct flb_connection *conn; + char crt_file[4096]; + char key_file[4096]; + char proxy_url[64]; + int ret; + + snprintf(crt_file, sizeof(crt_file), "%sdata/tls/proxy_stub_certificate.pem", + FLB_TESTS_DATA_PATH); + snprintf(key_file, sizeof(key_file), "%sdata/tls/proxy_stub_private_key.pem", + FLB_TESTS_DATA_PATH); + + TEST_CHECK(start_https_proxy_stub(&stub, crt_file, key_file, FLB_FALSE) == 0); + if (stub.listen_fd < 0) { + return; + } + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + stop_https_proxy_stub(&stub); + return; + } + + snprintf(proxy_url, sizeof(proxy_url), "https://127.0.0.1:%d", stub.port); + config->http_proxy = proxy_url; + u = flb_upstream_create(config, "dest.example.com", 443, FLB_IO_TCP, NULL); + config->http_proxy = NULL; + + TEST_CHECK(u != NULL); + if (u == NULL) { + flb_config_exit(config); + stop_https_proxy_stub(&stub); + return; + } + + /* No CA configured: verification falls back to the system trust store, + * which must not trust this self-signed test certificate. */ + TEST_CHECK(flb_upstream_proxy_tls_setup(u, FLB_TRUE, FLB_FALSE, + NULL, NULL) == 0); + + conn = build_proxy_test_connection(u); + TEST_CHECK(conn != NULL); + if (conn != NULL) { + ret = flb_io_net_connect(conn, NULL); + TEST_CHECK(ret == -1); + TEST_MSG("expected proxy CONNECT with an untrusted CA to be rejected"); + + destroy_proxy_test_connection(conn); + } + + stop_https_proxy_stub(&stub); + TEST_CHECK(stub.outer_ok == FLB_FALSE); + + flb_upstream_destroy(u); + flb_config_exit(config); +} + +/* + * End-to-end nested TLS-in-TLS coverage for an HTTPS destination reached + * through an HTTPS proxy: after the outer proxy handshake and CONNECT ack, + * flb_tls_session_create() must automatically chain a second, inner + * destination TLS handshake through the outer session (session_set_outer), + * and application data must round-trip correctly through both layers. + */ +void test_proxy_connect_https_destination_nested_tls(void) +{ + struct https_proxy_stub stub; + struct flb_config *config; + struct flb_tls *dest_tls; + struct flb_upstream *u; + struct flb_connection *conn; + char crt_file[4096]; + char key_file[4096]; + char proxy_url[64]; + size_t out_len; + char buf[16]; + int ret; + + snprintf(crt_file, sizeof(crt_file), "%sdata/tls/proxy_stub_certificate.pem", + FLB_TESTS_DATA_PATH); + snprintf(key_file, sizeof(key_file), "%sdata/tls/proxy_stub_private_key.pem", + FLB_TESTS_DATA_PATH); + + TEST_CHECK(start_https_proxy_stub(&stub, crt_file, key_file, FLB_TRUE) == 0); + if (stub.listen_fd < 0) { + return; + } + + config = flb_config_init(); + TEST_CHECK(config != NULL); + if (config == NULL) { + stop_https_proxy_stub(&stub); + return; + } + + /* Destination TLS context: the same self-signed cert acts as its own + * CA. Hostname verification is left off since the cert's CN does not + * match 127.0.0.1. */ + dest_tls = flb_tls_create(FLB_TLS_CLIENT_MODE, FLB_TRUE, 0, NULL, + NULL, crt_file, NULL, NULL, NULL); + TEST_CHECK(dest_tls != NULL); + if (dest_tls == NULL) { + flb_config_exit(config); + stop_https_proxy_stub(&stub); + return; + } + + snprintf(proxy_url, sizeof(proxy_url), "https://127.0.0.1:%d", stub.port); + config->http_proxy = proxy_url; + u = flb_upstream_create(config, "dest.example.com", 443, FLB_IO_TLS, dest_tls); + config->http_proxy = NULL; + + TEST_CHECK(u != NULL); + if (u == NULL) { + flb_tls_destroy(dest_tls); + flb_config_exit(config); + stop_https_proxy_stub(&stub); + return; + } + + TEST_CHECK(flb_upstream_proxy_tls_setup(u, FLB_TRUE, FLB_FALSE, + NULL, crt_file) == 0); + + conn = build_proxy_test_connection(u); + TEST_CHECK(conn != NULL); + if (conn != NULL) { + ret = flb_io_net_connect(conn, NULL); + TEST_CHECK(ret == 0); + TEST_MSG("expected proxy CONNECT + nested destination TLS handshake to succeed"); + + if (ret == 0) { + TEST_CHECK((flb_connection_get_flags(conn) & FLB_IO_PROXY_TLS) != 0); + + ret = flb_io_net_write(conn, "ping", 4, &out_len); + TEST_CHECK(ret >= 0 && out_len == 4); + + if (ret >= 0 && out_len == 4) { + memset(buf, 0, sizeof(buf)); + ret = flb_io_net_read(conn, buf, 4); + TEST_CHECK(ret == 4); + TEST_CHECK(strncmp(buf, "pong", 4) == 0); + TEST_MSG("application data should round-trip through both TLS layers"); + } + } + + destroy_proxy_test_connection(conn); + } + + stop_https_proxy_stub(&stub); + TEST_CHECK(stub.outer_ok == FLB_TRUE); + TEST_CHECK(stub.inner_ok == FLB_TRUE); + TEST_MSG("nested destination TLS handshake and encrypted echo should both succeed"); + + /* + * flb_upstream_destroy() only owns/frees proxy_tls_context; the + * destination TLS context passed into flb_upstream_create() (dest_tls) + * is owned by the caller, matching production where it belongs to the + * output instance (ins->tls) and is freed independently. + */ + flb_upstream_destroy(u); + flb_tls_destroy(dest_tls); + flb_config_exit(config); +} +#endif /* FLB_TEST_HAVE_PROXY_STUB */ + #endif TEST_LIST = { @@ -479,6 +1160,16 @@ TEST_LIST = { #endif {"tls_reload_does_not_hide_concurrent_file_change", test_tls_reload_does_not_hide_concurrent_file_change}, + {"upstream_proxy_tls_setup_noop_without_proxy", test_upstream_proxy_tls_setup_noop_without_proxy}, + {"upstream_proxy_tls_setup_configures_ca", test_upstream_proxy_tls_setup_configures_ca}, + {"io_proxy_tls_flag_does_not_leak_to_stream", test_io_proxy_tls_flag_does_not_leak_to_stream}, + {"http_client_host_header_not_polluted_by_proxy_tls", + test_http_client_host_header_not_polluted_by_proxy_tls}, +#ifdef FLB_TEST_HAVE_PROXY_STUB + {"proxy_connect_trusted_ca_succeeds", test_proxy_connect_trusted_ca_succeeds}, + {"proxy_connect_untrusted_ca_rejected", test_proxy_connect_untrusted_ca_rejected}, + {"proxy_connect_https_destination_nested_tls", test_proxy_connect_https_destination_nested_tls}, +#endif #endif {0} }; From 43f80cf91c105fc5d65fd4e47792958e757e0dee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Fri, 31 Jul 2026 13:24:32 -0300 Subject: [PATCH 13/15] tests: internal: fix two CI-only failures in the new proxy stub tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit pr-compile-centos-7 failed to link: TLS_server_method() requires OpenSSL >= 1.1.0, which the CentOS 7 system OpenSSL predates. start_https_proxy_stub() now falls back to SSLv23_server_method() on older OpenSSL, matching the version-gated pattern already used in src/tls/openssl.c. run-windows-unit-tests failed on all three Windows targets: io_proxy_tls_flag_does_not_leak_to_stream() calls setup_conn(), which needs Winsock initialized first. The two other tests in this file that call setup_conn() already do this; this one was missing the same WSAStartup()/WSACleanup() guard, so the pipe creation silently failed on Windows and the test crashed with an access violation instead of just reporting the failed check. Both fixed and re-verified (all 14 tests pass) in a Debian container with a modern OpenSSL/toolchain, since this development host lacks the tools to reproduce either target directly. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- tests/internal/upstream_tls.c | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/internal/upstream_tls.c b/tests/internal/upstream_tls.c index 7fad52baf17..0f2a890d66e 100644 --- a/tests/internal/upstream_tls.c +++ b/tests/internal/upstream_tls.c @@ -587,6 +587,11 @@ void test_io_proxy_tls_flag_does_not_leak_to_stream(void) struct flb_config config = {0}; flb_pipefd_t socket_pair[2]; +#ifdef FLB_SYSTEM_WINDOWS + WSADATA wsa_data; + WSAStartup(0x0201, &wsa_data); +#endif + TEST_CHECK(setup_conn(&conn, &upstream, &config, socket_pair) == 0); /* Simulate what flb_io_net_connect() now does for an HTTPS proxy leg. */ @@ -600,6 +605,10 @@ void test_io_proxy_tls_flag_does_not_leak_to_stream(void) flb_pipe_close(socket_pair[1]); flb_pipe_close(conn.fd); + +#ifdef FLB_SYSTEM_WINDOWS + WSACleanup(); +#endif } /* @@ -809,7 +818,17 @@ static int start_https_proxy_stub(struct https_proxy_stub *stub, stub->listen_fd = fd; stub->port = ntohs(addr.sin_port); + /* + * TLS_server_method() requires OpenSSL >= 1.1.0; older builds (e.g. the + * system OpenSSL on CentOS 7) only have the versioned SSLv23_*_method() + * API, which src/tls/openssl.c already falls back to for the same + * reason. + */ +#if OPENSSL_VERSION_NUMBER < 0x10100000L + stub->ssl_ctx = SSL_CTX_new(SSLv23_server_method()); +#else stub->ssl_ctx = SSL_CTX_new(TLS_server_method()); +#endif if (!stub->ssl_ctx) { close(fd); stub->listen_fd = -1; From 01b01d8004d41d000d8044213117e83372560c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:52:09 -0300 Subject: [PATCH 14/15] output: fail output initialization on invalid tls.proxy.ca_file MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit flb_output_init_all() now validates tls.proxy.ca_file/ca_path eagerly (flb_output_proxy_tls_ca_check()), the same way the destination's tls.ca_file/ca_path already are. Before, an invalid path was silently ignored, since flb_output_upstream_set()'s return value isn't checked by any of its callers. Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- include/fluent-bit/flb_output.h | 3 +++ src/flb_output.c | 35 +++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/include/fluent-bit/flb_output.h b/include/fluent-bit/flb_output.h index 0c066954905..e28f9754882 100644 --- a/include/fluent-bit/flb_output.h +++ b/include/fluent-bit/flb_output.h @@ -1433,6 +1433,9 @@ int flb_output_oauth2_property_check(struct flb_output_instance *ins, struct flb_config *config); int flb_output_plugin_property_check(struct flb_output_instance *ins, struct flb_config *config); +#ifdef FLB_HAVE_TLS +int flb_output_proxy_tls_ca_check(struct flb_output_instance *ins); +#endif int flb_output_init_all(struct flb_config *config); int flb_output_check(struct flb_config *config); int flb_output_log_check(struct flb_output_instance *ins, int l); diff --git a/src/flb_output.c b/src/flb_output.c index 3631c4486e3..42a3f2b04d3 100644 --- a/src/flb_output.c +++ b/src/flb_output.c @@ -1407,6 +1407,36 @@ int flb_output_plugin_property_check(struct flb_output_instance *ins, return 0; } +#ifdef FLB_HAVE_TLS +/* Eagerly validate tls.proxy.ca_file/ca_path so a bad path fails init here, + * instead of being silently ignored later in flb_output_upstream_set(). */ +int flb_output_proxy_tls_ca_check(struct flb_output_instance *ins) +{ + struct flb_tls *tls_proxy_validate; + + if (ins->tls_proxy_ca_file == NULL && ins->tls_proxy_ca_path == NULL) { + return 0; + } + + tls_proxy_validate = flb_tls_create(FLB_TLS_CLIENT_MODE, + ins->tls_proxy_verify, + 0, + NULL, + ins->tls_proxy_ca_path, + ins->tls_proxy_ca_file, + NULL, NULL, NULL); + if (!tls_proxy_validate) { + flb_error("[output %s] error initializing TLS context for " + "tls.proxy.ca_file/tls.proxy.ca_path", + ins->name); + return -1; + } + + flb_tls_destroy(tls_proxy_validate); + return 0; +} +#endif + /* Trigger the output plugins setup callbacks to prepare them. */ int flb_output_init_all(struct flb_config *config) { @@ -1705,6 +1735,11 @@ int flb_output_init_all(struct flb_config *config) } # endif } + + if (flb_output_proxy_tls_ca_check(ins) == -1) { + flb_output_instance_destroy(ins); + return -1; + } #endif /* * Before to call the initialization callback, make sure that the received From 87915cf13386cd71bfefec4e45183eb2f051a9ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ant=C3=B4nio=20Franco?= <13881523+antoniomrfranco@users.noreply.github.com> Date: Tue, 4 Aug 2026 12:52:26 -0300 Subject: [PATCH 15/15] tests: internal: add coverage for flb_output_proxy_tls_ca_check MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the no-proxy no-op case, an invalid ca_file (must return -1), and a valid ca_file (must return 0). Signed-off-by: Antônio Franco <13881523+antoniomrfranco@users.noreply.github.com> --- tests/internal/upstream_tls.c | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/tests/internal/upstream_tls.c b/tests/internal/upstream_tls.c index 0f2a890d66e..b1f689af843 100644 --- a/tests/internal/upstream_tls.c +++ b/tests/internal/upstream_tls.c @@ -11,6 +11,7 @@ #include #include #include +#include #include "flb_tests_internal.h" @@ -574,6 +575,32 @@ void test_upstream_proxy_tls_setup_configures_ca(void) flb_config_exit(config); } +/* Invalid tls.proxy.ca_file/ca_path must fail output init. */ +void test_output_proxy_tls_ca_check(void) +{ + struct flb_output_instance ins = {0}; + char valid_ca_file[4096]; + + snprintf(valid_ca_file, sizeof(valid_ca_file), "%sdata/tls/proxy_stub_certificate.pem", + FLB_TESTS_DATA_PATH); + + snprintf(ins.name, sizeof(ins.name), "test"); + ins.tls_proxy_verify = FLB_TRUE; + + /* No proxy CA configured: nothing to validate. */ + ins.tls_proxy_ca_file = NULL; + ins.tls_proxy_ca_path = NULL; + TEST_CHECK(flb_output_proxy_tls_ca_check(&ins) == 0); + + /* Invalid ca_file: must fail so the caller aborts output init. */ + ins.tls_proxy_ca_file = "/this/path/does/not/exist.pem"; + TEST_CHECK(flb_output_proxy_tls_ca_check(&ins) == -1); + + /* Valid ca_file: must succeed. */ + ins.tls_proxy_ca_file = valid_ca_file; + TEST_CHECK(flb_output_proxy_tls_ca_check(&ins) == 0); +} + /* * Verify that the connection-scoped FLB_IO_PROXY_TLS flag (set by the fixed * flb_io.c proxy-connect path) never leaks into the shared stream's flags. @@ -1181,6 +1208,7 @@ TEST_LIST = { test_tls_reload_does_not_hide_concurrent_file_change}, {"upstream_proxy_tls_setup_noop_without_proxy", test_upstream_proxy_tls_setup_noop_without_proxy}, {"upstream_proxy_tls_setup_configures_ca", test_upstream_proxy_tls_setup_configures_ca}, + {"output_proxy_tls_ca_check", test_output_proxy_tls_ca_check}, {"io_proxy_tls_flag_does_not_leak_to_stream", test_io_proxy_tls_flag_does_not_leak_to_stream}, {"http_client_host_header_not_polluted_by_proxy_tls", test_http_client_host_header_not_polluted_by_proxy_tls},