diff --git a/include/fluent-bit/flb_upstream.h b/include/fluent-bit/flb_upstream.h index 544f97fee90..1d66c490e1a 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 @@ -57,6 +61,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/include/fluent-bit/tls/flb_tls.h b/include/fluent-bit/tls/flb_tls.h index 018231218e2..ae9af1b4221 100644 --- a/include/fluent-bit/tls/flb_tls.h +++ b/include/fluent-bit/tls/flb_tls.h @@ -88,6 +88,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/flb_io.c b/src/flb_io.c index 38fbb3eed4b..8550e2e8339 100644 --- a/src/flb_io.c +++ b/src/flb_io.c @@ -136,6 +136,38 @@ int flb_io_net_connect(struct flb_connection *connection, } if (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) { diff --git a/src/flb_upstream.c b/src/flb_upstream.c index 89a0fae281f..3bad5c7e089 100644 --- a/src/flb_upstream.c +++ b/src/flb_upstream.c @@ -315,6 +315,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 (flb_upstream_needs_proxy(host, config->http_proxy, config->no_proxy) == FLB_TRUE) { flb_debug("[upstream] config->http_proxy: %s", config->http_proxy); @@ -336,6 +340,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); @@ -348,15 +381,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; @@ -688,6 +718,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); } 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; diff --git a/src/tls/flb_tls.c b/src/tls/flb_tls.c index 40961b6a12a..77810fd3269 100644 --- a/src/tls/flb_tls.c +++ b/src/tls/flb_tls.c @@ -617,13 +617,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, '.'); } } @@ -643,6 +650,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 cb8c2749561..8744d82d1f8 100644 --- a/src/tls/openssl.c +++ b/src/tls/openssl.c @@ -76,6 +76,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 tls_init(void) @@ -1279,10 +1280,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; @@ -1296,6 +1327,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); @@ -1688,6 +1733,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, diff --git a/tests/internal/upstream_tls.c b/tests/internal/upstream_tls.c index b847b056b79..0232333c9e6 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" @@ -161,12 +162,87 @@ 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); +} + #endif 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}, #endif {0} }; 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}, };