From 6473f3af87f3c297ae1a48089bd81d74eae656f5 Mon Sep 17 00:00:00 2001 From: Adham Ahmed Hussein Mahrous Date: Wed, 15 Jul 2026 03:33:38 +0300 Subject: [PATCH 1/5] Add TLS compatibility mode configuration for HTTP senders Add a TLS compatibility mode option to HttpSenderConfig and expose it through OTLP HTTP exporter configuration. Implement support in the OkHttp sender by allowing compatible TLS negotiation for legacy TLS servers while keeping modern TLS as the default. Add tests covering modern TLS failure and compatible TLS success against a legacy TLSv1/TLSv1.1-only server. --- .../opentelemetry-sdk-common.txt | 16 +- .../otlp/internal/HttpExporterBuilder.java | 11 +- .../internal/ImmutableHttpSenderConfig.java | 10 +- .../okhttp/internal/OkHttpHttpSender.java | 13 +- .../internal/OkHttpHttpSenderProvider.java | 3 +- .../okhttp/internal/OkHttpHttpSenderTest.java | 4 +- .../OkHttpHttpSenderTlsCompatibilityTest.java | 280 ++++++++++++++++++ .../internal/OkHttpHttpSuppressionTest.java | 4 +- .../sdk/common/export/HttpSenderConfig.java | 11 + .../common/export/TlsCompatibilityMode.java | 31 ++ 10 files changed, 372 insertions(+), 11 deletions(-) create mode 100644 exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java create mode 100644 sdk/common/src/main/java/io/opentelemetry/sdk/common/export/TlsCompatibilityMode.java diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt index 8a99a2cd80e..a9cf0c5c55c 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -1,2 +1,14 @@ -Comparing source compatibility of opentelemetry-sdk-common-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.64.0.jar -No changes. \ No newline at end of file +Comparing source compatibility of opentelemetry-sdk-common-1.64.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.64.0.jar +*** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.common.export.HttpSenderConfig (not serializable) + === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 + +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode getTlsCompatibilityMode() ++++ NEW ENUM: PUBLIC(+) FINAL(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode (compatible) + +++ CLASS FILE FORMAT VERSION: 52.0 <- n.a. + +++ NEW INTERFACE: java.lang.constant.Constable + +++ NEW INTERFACE: java.lang.Comparable + +++ NEW INTERFACE: java.io.Serializable + +++ NEW SUPERCLASS: java.lang.Enum + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode MODERN + +++ NEW FIELD: PUBLIC(+) STATIC(+) FINAL(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode COMPATIBLE + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode valueOf(java.lang.String) + +++ NEW METHOD: PUBLIC(+) STATIC(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode[] values() diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java index 98a5a2e2b85..087386b2868 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/HttpExporterBuilder.java @@ -17,6 +17,7 @@ import io.opentelemetry.sdk.common.export.HttpSenderProvider; import io.opentelemetry.sdk.common.export.ProxyOptions; import io.opentelemetry.sdk.common.export.RetryPolicy; +import io.opentelemetry.sdk.common.export.TlsCompatibilityMode; import io.opentelemetry.sdk.common.internal.ComponentId; import io.opentelemetry.sdk.common.internal.StandardComponentId; import java.net.URI; @@ -69,6 +70,7 @@ public final class HttpExporterBuilder { private ComponentLoader componentLoader = ComponentLoader.forClassLoader(HttpExporterBuilder.class.getClassLoader()); @Nullable private ExecutorService executorService; + private TlsCompatibilityMode tlsCompatibilityMode = TlsCompatibilityMode.MODERN; public HttpExporterBuilder( StandardComponentId.ExporterType exporterType, String defaultEndpoint) { @@ -167,6 +169,11 @@ public HttpExporterBuilder setExecutorService(ExecutorService executorService) { return this; } + public HttpExporterBuilder setTlsCompatibilityMode(TlsCompatibilityMode tlsCompatibilityMode) { + this.tlsCompatibilityMode = tlsCompatibilityMode; + return this; + } + public HttpExporterBuilder exportAsJson() { this.exportAsJson = true; exporterType = mapToJsonTypeIfPossible(exporterType); @@ -204,6 +211,7 @@ public HttpExporterBuilder copy() { copy.internalTelemetryVersion = internalTelemetryVersion; copy.proxyOptions = proxyOptions; copy.componentLoader = componentLoader; + copy.tlsCompatibilityMode = tlsCompatibilityMode; return copy; } @@ -247,7 +255,8 @@ public HttpExporter build() { executorService, // 4mb to align with spec guidance - even though we don't do anything with the // response today, we will so better to have future-looking memory profile - 4 * 1024L * 1024L)); + 4 * 1024L * 1024L, + tlsCompatibilityMode)); LOGGER.log(Level.FINE, "Using HttpSender: " + httpSender.getClass().getName()); return new HttpExporter( diff --git a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java index a6d04ae6aa4..8b74b05a654 100644 --- a/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java +++ b/exporters/otlp/all/src/main/java/io/opentelemetry/exporter/otlp/internal/ImmutableHttpSenderConfig.java @@ -10,6 +10,7 @@ import io.opentelemetry.sdk.common.export.HttpSenderConfig; import io.opentelemetry.sdk.common.export.ProxyOptions; import io.opentelemetry.sdk.common.export.RetryPolicy; +import io.opentelemetry.sdk.common.export.TlsCompatibilityMode; import java.net.URI; import java.time.Duration; import java.util.List; @@ -36,7 +37,8 @@ static HttpSenderConfig create( @Nullable SSLContext sslContext, @Nullable X509TrustManager trustManager, @Nullable ExecutorService executorService, - long maxResponseBodySize) { + long maxResponseBodySize, + TlsCompatibilityMode tlsCompatibilityMode) { return new AutoValue_ImmutableHttpSenderConfig( endpoint, contentType, @@ -49,9 +51,13 @@ static HttpSenderConfig create( sslContext, trustManager, executorService, - maxResponseBodySize); + maxResponseBodySize, + tlsCompatibilityMode); } @Override public abstract long getMaxResponseBodySize(); + + @Override + public abstract TlsCompatibilityMode getTlsCompatibilityMode(); } diff --git a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java index b5c611d1bcb..c3eb1cc8bd4 100644 --- a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java +++ b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSender.java @@ -14,6 +14,7 @@ import io.opentelemetry.sdk.common.export.MessageWriter; import io.opentelemetry.sdk.common.export.ProxyOptions; import io.opentelemetry.sdk.common.export.RetryPolicy; +import io.opentelemetry.sdk.common.export.TlsCompatibilityMode; import java.io.IOException; import java.net.URI; import java.time.Duration; @@ -79,7 +80,8 @@ public OkHttpHttpSender( @Nullable SSLContext sslContext, @Nullable X509TrustManager trustManager, @Nullable ExecutorService executorService, - long maxResponseBodySize) { + long maxResponseBodySize, + TlsCompatibilityMode tlsCompatibilityMode) { int callTimeoutMillis = (int) Math.min(timeout.toMillis(), Integer.MAX_VALUE); int connectTimeoutMillis = (int) Math.min(connectTimeout.toMillis(), Integer.MAX_VALUE); @@ -109,8 +111,13 @@ public OkHttpHttpSender( boolean isPlainHttp = endpoint.getScheme().equals("http"); if (isPlainHttp) { builder.connectionSpecs(Collections.singletonList(ConnectionSpec.CLEARTEXT)); - } else if (sslContext != null && trustManager != null) { - builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager); + } else { + if (sslContext != null && trustManager != null) { + builder.sslSocketFactory(sslContext.getSocketFactory(), trustManager); + } + if (tlsCompatibilityMode == TlsCompatibilityMode.COMPATIBLE) { + builder.connectionSpecs(Collections.singletonList(ConnectionSpec.COMPATIBLE_TLS)); + } } this.client = builder.build(); diff --git a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java index 581cf9bb549..3b14382f2a3 100644 --- a/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java +++ b/exporters/sender/okhttp/src/main/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderProvider.java @@ -31,6 +31,7 @@ public HttpSender createSender(HttpSenderConfig httpSenderConfig) { httpSenderConfig.getSslContext(), httpSenderConfig.getTrustManager(), httpSenderConfig.getExecutorService(), - httpSenderConfig.getMaxResponseBodySize()); + httpSenderConfig.getMaxResponseBodySize(), + httpSenderConfig.getTlsCompatibilityMode()); } } diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java index 0edc216aee6..05dd078b5c4 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java @@ -12,6 +12,7 @@ import io.opentelemetry.sdk.common.CompletableResultCode; import io.opentelemetry.sdk.common.export.HttpResponse; import io.opentelemetry.sdk.common.export.MessageWriter; +import io.opentelemetry.sdk.common.export.TlsCompatibilityMode; import java.io.OutputStream; import java.net.ServerSocket; import java.net.URI; @@ -48,7 +49,8 @@ void send_rejectedExecution_callsOnError() { null, null, executor, - Long.MAX_VALUE); + Long.MAX_VALUE, + TlsCompatibilityMode.MODERN); AtomicReference responseRef = new AtomicReference<>(); AtomicReference errorRef = new AtomicReference<>(); diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java new file mode 100644 index 00000000000..2fff9b46f35 --- /dev/null +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java @@ -0,0 +1,280 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.exporter.sender.okhttp.internal; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assumptions.assumeTrue; + +import com.linecorp.armeria.testing.junit5.server.SelfSignedCertificateExtension; +import io.netty.bootstrap.ServerBootstrap; +import io.netty.channel.Channel; +import io.netty.channel.ChannelFutureListener; +import io.netty.channel.ChannelHandlerContext; +import io.netty.channel.ChannelInitializer; +import io.netty.channel.ChannelPipeline; +import io.netty.channel.EventLoopGroup; +import io.netty.channel.MultiThreadIoEventLoopGroup; +import io.netty.channel.SimpleChannelInboundHandler; +import io.netty.channel.nio.NioIoHandler; +import io.netty.channel.socket.SocketChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; +import io.netty.handler.codec.http.DefaultFullHttpResponse; +import io.netty.handler.codec.http.FullHttpRequest; +import io.netty.handler.codec.http.FullHttpResponse; +import io.netty.handler.codec.http.HttpObjectAggregator; +import io.netty.handler.codec.http.HttpResponseStatus; +import io.netty.handler.codec.http.HttpServerCodec; +import io.netty.handler.codec.http.HttpVersion; +import io.netty.handler.ssl.ApplicationProtocolConfig; +import io.netty.handler.ssl.SslContext; +import io.netty.handler.ssl.SslContextBuilder; +import io.opentelemetry.exporter.internal.TlsUtil; +import io.opentelemetry.sdk.common.export.HttpResponse; +import io.opentelemetry.sdk.common.export.MessageWriter; +import io.opentelemetry.sdk.common.export.TlsCompatibilityMode; +import java.io.OutputStream; +import java.net.InetSocketAddress; +import java.net.URI; +import java.security.NoSuchAlgorithmException; +import java.security.Security; +import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicReference; +import java.util.stream.Collectors; +import javax.annotation.Nullable; +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.X509TrustManager; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; +import org.junit.jupiter.api.parallel.Isolated; + +/** + * Tests TLS compatibility modes against a server that only supports legacy TLS protocols. + * + *

OkHttp's {@code ConnectionSpec.MODERN_TLS} and {@code ConnectionSpec.COMPATIBLE_TLS} differ in + * the supported TLS protocol versions. + * + *

The test server is restricted to TLSv1/TLSv1.1 because these are the protocol versions that + * distinguish MODERN_TLS from COMPATIBLE_TLS. + */ + +// This test temporarily changes the JVM-wide TLS disabled algorithms to enable legacy TLS protocol +// testing, so it must not run concurrently with other tests. +@Isolated +class OkHttpHttpSenderTlsCompatibilityTest { + + @RegisterExtension + static final SelfSignedCertificateExtension certificate = new SelfSignedCertificateExtension(); + + private static EventLoopGroup bossGroup; + private static EventLoopGroup workerGroup; + private static Channel serverChannel; + private static URI serverUri; + + @Nullable private static String previousDisabledAlgorithms; + + @BeforeAll + static void setup() throws Exception { + String disabledAlgorithms = Security.getProperty("jdk.tls.disabledAlgorithms"); + + previousDisabledAlgorithms = disabledAlgorithms; + + if (disabledAlgorithms + != null) { // remove (TLSv1, TLSv1.1) from the disabled algorithms so we can test legacy + // protocols + String updatedAlgorithms = + Arrays.stream(disabledAlgorithms.split(",")) + .map(String::trim) + .filter(algorithm -> !algorithm.equals("TLSv1") && !algorithm.equals("TLSv1.1")) + .collect(Collectors.joining(", ")); + + Security.setProperty("jdk.tls.disabledAlgorithms", updatedAlgorithms); + } + + bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + + workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); + + SslContext sslContext = + SslContextBuilder.forServer(certificate.privateKey(), certificate.certificate()) + .protocols("TLSv1", "TLSv1.1") + .applicationProtocolConfig(ApplicationProtocolConfig.DISABLED) + .build(); + + ServerBootstrap bootstrap = new ServerBootstrap(); + + bootstrap + .group(bossGroup, workerGroup) + .channel(NioServerSocketChannel.class) + .childHandler( + new ChannelInitializer() { + + @Override + protected void initChannel(SocketChannel ch) { + + ChannelPipeline pipeline = ch.pipeline(); + + pipeline.addLast(sslContext.newHandler(ch.alloc())); + + pipeline.addLast(new HttpServerCodec()); + + pipeline.addLast(new HttpObjectAggregator(1024 * 1024)); + + pipeline.addLast( + new SimpleChannelInboundHandler() { + + @Override + protected void channelRead0( + ChannelHandlerContext ctx, FullHttpRequest request) { + + FullHttpResponse response = + new DefaultFullHttpResponse( + HttpVersion.HTTP_1_1, HttpResponseStatus.OK); + + ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE); + } + }); + } + }); + + serverChannel = bootstrap.bind(0).sync().channel(); + + int port = ((InetSocketAddress) serverChannel.localAddress()).getPort(); + + serverUri = URI.create("https://localhost:" + port + "/"); + } + + @AfterAll + static void restoreDisabledAlgorithms() throws Exception { + if (serverChannel != null) { + serverChannel.close().sync(); + } + + if (bossGroup != null) { + bossGroup.shutdownGracefully().sync(); + } + + if (workerGroup != null) { + workerGroup.shutdownGracefully().sync(); + } + if (previousDisabledAlgorithms == null) { + Security.setProperty("jdk.tls.disabledAlgorithms", null); + } else { + Security.setProperty("jdk.tls.disabledAlgorithms", previousDisabledAlgorithms); + } + } + + @Test + void modernTls_cannotReachLegacyProtocolOnlyServer() throws Exception { + // If this JVM's TLS provider does not implement TLSv1/1.1, skip instead of + // failing for an unrelated reason. + assumeTrue( + supportsProtocol("TLSv1") || supportsProtocol("TLSv1.1"), + "TLSv1/TLSv1.1 are not supported by this JVM"); + + OkHttpHttpSender sender = buildSender(TlsCompatibilityMode.MODERN); + + Result result = send(sender); + + assertThat(result.response).isNull(); + assertThat(result.error) + .isNotNull(); // handshake failure: no protocol version in common with the server + } + + @Test + void compatibleTls_reachesLegacyProtocolOnlyServer() throws Exception { + assumeTrue( + supportsProtocol("TLSv1") || supportsProtocol("TLSv1.1"), + "TLSv1/TLSv1.1 are not supported by this JVM"); + + OkHttpHttpSender sender = buildSender(TlsCompatibilityMode.COMPATIBLE); + + Result result = send(sender); + + assertThat(result.error).isNull(); + assertThat(result.response).isNotNull(); + assertThat(result.response.getStatusCode()).isEqualTo(200); + } + + private static OkHttpHttpSender buildSender(TlsCompatibilityMode tlsCompatibilityMode) + throws Exception { + X509TrustManager trustManager = TlsUtil.trustManager(certificate.certificate().getEncoded()); + SSLContext sslContext = SSLContext.getInstance("TLS"); + // Use the same trust configuration in both tests so only the TLS compatibility + // mode differs. + sslContext.init(null, new TrustManager[] {trustManager}, null); + + return new OkHttpHttpSender( + serverUri, + "text/plain", + null, + Duration.ofSeconds(10), + Duration.ofSeconds(10), + Collections::emptyMap, + null, + null, + sslContext, + trustManager, + null, + Long.MAX_VALUE, + tlsCompatibilityMode); + } + + private static Result send(OkHttpHttpSender sender) throws InterruptedException { + AtomicReference responseRef = new AtomicReference<>(); + AtomicReference errorRef = new AtomicReference<>(); + CountDownLatch latch = new CountDownLatch(1); + + sender.send( + new NoOpRequestBodyWriter(), + response -> { + responseRef.set(response); + latch.countDown(); + }, + error -> { + errorRef.set(error); + latch.countDown(); + }); + + assertThat(latch.await(10, TimeUnit.SECONDS)).isTrue(); + return new Result(responseRef.get(), errorRef.get()); + } + + private static boolean supportsProtocol(String protocol) { + try { + SSLContext.getInstance(protocol); + return true; + } catch (NoSuchAlgorithmException e) { + return false; + } + } + + private static class Result { + @Nullable final HttpResponse response; + @Nullable final Throwable error; + + Result(@Nullable HttpResponse response, @Nullable Throwable error) { + this.response = response; + this.error = error; + } + } + + private static class NoOpRequestBodyWriter implements MessageWriter { + @Override + public void writeMessage(OutputStream output) {} + + @Override + public int getContentLength() { + return 0; + } + } +} diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java index c64247a9b36..22ce6d5e8fc 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSuppressionTest.java @@ -6,6 +6,7 @@ package io.opentelemetry.exporter.sender.okhttp.internal; import io.opentelemetry.sdk.common.export.MessageWriter; +import io.opentelemetry.sdk.common.export.TlsCompatibilityMode; import java.io.IOException; import java.io.OutputStream; import java.net.URI; @@ -47,6 +48,7 @@ OkHttpHttpSender createSender(String endpoint) { null, null, null, - Long.MAX_VALUE); + Long.MAX_VALUE, + TlsCompatibilityMode.MODERN); } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java index be40289dd9c..b6410c91fdd 100644 --- a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/HttpSenderConfig.java @@ -98,4 +98,15 @@ public interface HttpSenderConfig { default long getMaxResponseBodySize() { return 4 * 1024L * 1024L; } + + /** + * The TLS compatibility mode to use when connecting to an HTTPS endpoint. + * + *

Defaults to {@link TlsCompatibilityMode#MODERN}, which uses modern TLS versions only. {@link + * TlsCompatibilityMode#COMPATIBLE} enables support for legacy TLS versions for compatibility with + * older servers. + */ + default TlsCompatibilityMode getTlsCompatibilityMode() { + return TlsCompatibilityMode.MODERN; + } } diff --git a/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/TlsCompatibilityMode.java b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/TlsCompatibilityMode.java new file mode 100644 index 00000000000..663ada329e9 --- /dev/null +++ b/sdk/common/src/main/java/io/opentelemetry/sdk/common/export/TlsCompatibilityMode.java @@ -0,0 +1,31 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.sdk.common.export; + +/** + * Controls the range of TLS protocol versions a {@link HttpSender} is willing to negotiate when + * connecting to an HTTPS endpoint. + * + *

This is intentionally implementation-neutral: it says nothing about OkHttp's {@code + * ConnectionSpec} or any other sender-specific concept. Each {@link HttpSenderProvider} maps it to + * whatever mechanism its underlying HTTP client exposes. + */ +public enum TlsCompatibilityMode { + /** + * The default TLS configuration intended for modern servers and clients. In OkHttp terms, this + * corresponds to {@code ConnectionSpec.MODERN_TLS}. + */ + MODERN, + + /** + * A broader set of TLS protocol versions for compatibility with older endpoints. For OkHttp + * senders, this corresponds to {@code ConnectionSpec.COMPATIBLE_TLS}. + * + *

Prefer upgrading the client or server platform instead of using this mode when possible: it + * widens the negotiable protocol range down to TLSv1.0. + */ + COMPATIBLE +} From 65b48403adb79463a254353e4c475424f9689f3d Mon Sep 17 00:00:00 2001 From: Adham Ahmed Hussein Mahrous Date: Thu, 16 Jul 2026 02:02:29 +0300 Subject: [PATCH 2/5] Update OkHttpHttpSenderTest for TLS mode parameter --- .../exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java index 05dd078b5c4..06bc497c6d1 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTest.java @@ -213,7 +213,8 @@ private static OkHttpHttpSender newSender( null, null, executorService, - Long.MAX_VALUE); + Long.MAX_VALUE, + TlsCompatibilityMode.MODERN); } private static class NoOpRequestBodyWriter implements MessageWriter { From 29c655b3f63c9d469248e0460c40387a7e5de1e0 Mon Sep 17 00:00:00 2001 From: Adham Ahmed Hussein Mahrous Date: Thu, 16 Jul 2026 04:09:31 +0300 Subject: [PATCH 3/5] Use security properties file for TLS compatibility test Replace runtime TLS security property mutations with a `java.security.properties` override for `OkHttpHttpSenderTlsCompatibilityTest`. Applying the configuration at JVM bootstrap avoids test-order dependencies caused by JDK caching of TLS security properties and allows removal of `@Isolated` and runtime property manipulation logic. --- exporters/sender/okhttp/build.gradle.kts | 13 ++++++++ .../OkHttpHttpSenderTlsCompatibilityTest.java | 30 ------------------- .../resources/enable-legacy-tls-test.security | 8 +++++ 3 files changed, 21 insertions(+), 30 deletions(-) create mode 100644 exporters/sender/okhttp/src/test/resources/enable-legacy-tls-test.security diff --git a/exporters/sender/okhttp/build.gradle.kts b/exporters/sender/okhttp/build.gradle.kts index 85aa30f3125..8775368adaa 100644 --- a/exporters/sender/okhttp/build.gradle.kts +++ b/exporters/sender/okhttp/build.gradle.kts @@ -27,3 +27,16 @@ dependencies { testImplementation("com.linecorp.armeria:armeria-junit5") } + +tasks.withType().configureEach { + // OkHttpHttpSenderTlsCompatibilityTest needs TLSv1/TLSv1.1 available to test COMPATIBLE_TLS + // against a legacy-protocol-only server. jdk.tls.disabledAlgorithms is a security property + // (not a regular system property) that the JDK caches the first time any TLS code runs in + // the JVM, so mutating it at test runtime only works if nothing else in the shared test JVM + // has touched TLS yet -- not guaranteed. Overriding it via java.security.properties applies + // at JVM bootstrap, before that caching can happen, so it's always in effect. + systemProperty( + "java.security.properties", + file("src/test/resources/enable-legacy-tls-test.security").absolutePath + ) +} diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java index 2fff9b46f35..55030ecd8eb 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java @@ -39,14 +39,11 @@ import java.net.InetSocketAddress; import java.net.URI; import java.security.NoSuchAlgorithmException; -import java.security.Security; import java.time.Duration; -import java.util.Arrays; import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import java.util.stream.Collectors; import javax.annotation.Nullable; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManager; @@ -55,7 +52,6 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.RegisterExtension; -import org.junit.jupiter.api.parallel.Isolated; /** * Tests TLS compatibility modes against a server that only supports legacy TLS protocols. @@ -67,9 +63,6 @@ * distinguish MODERN_TLS from COMPATIBLE_TLS. */ -// This test temporarily changes the JVM-wide TLS disabled algorithms to enable legacy TLS protocol -// testing, so it must not run concurrently with other tests. -@Isolated class OkHttpHttpSenderTlsCompatibilityTest { @RegisterExtension @@ -80,26 +73,8 @@ class OkHttpHttpSenderTlsCompatibilityTest { private static Channel serverChannel; private static URI serverUri; - @Nullable private static String previousDisabledAlgorithms; - @BeforeAll static void setup() throws Exception { - String disabledAlgorithms = Security.getProperty("jdk.tls.disabledAlgorithms"); - - previousDisabledAlgorithms = disabledAlgorithms; - - if (disabledAlgorithms - != null) { // remove (TLSv1, TLSv1.1) from the disabled algorithms so we can test legacy - // protocols - String updatedAlgorithms = - Arrays.stream(disabledAlgorithms.split(",")) - .map(String::trim) - .filter(algorithm -> !algorithm.equals("TLSv1") && !algorithm.equals("TLSv1.1")) - .collect(Collectors.joining(", ")); - - Security.setProperty("jdk.tls.disabledAlgorithms", updatedAlgorithms); - } - bossGroup = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); workerGroup = new MultiThreadIoEventLoopGroup(NioIoHandler.newFactory()); @@ -166,11 +141,6 @@ static void restoreDisabledAlgorithms() throws Exception { if (workerGroup != null) { workerGroup.shutdownGracefully().sync(); } - if (previousDisabledAlgorithms == null) { - Security.setProperty("jdk.tls.disabledAlgorithms", null); - } else { - Security.setProperty("jdk.tls.disabledAlgorithms", previousDisabledAlgorithms); - } } @Test diff --git a/exporters/sender/okhttp/src/test/resources/enable-legacy-tls-test.security b/exporters/sender/okhttp/src/test/resources/enable-legacy-tls-test.security new file mode 100644 index 00000000000..d50868d1bfb --- /dev/null +++ b/exporters/sender/okhttp/src/test/resources/enable-legacy-tls-test.security @@ -0,0 +1,8 @@ +# Used only for this module's test JVM (see build.gradle.kts), via -Djava.security.properties. +# +# Mirrors the JDK's default jdk.tls.disabledAlgorithms, minus TLSv1 and TLSv1.1, so that +# OkHttpHttpSenderTlsCompatibilityTest can exercise COMPATIBLE_TLS against a legacy-protocol-only +# server. Setting this via java.security.properties applies at JVM bootstrap, before any class +# has a chance to initialize the JDK's TLS internals and cache the previous value +jdk.tls.disabledAlgorithms=SSLv3, RC4, DES, MD5withRSA, DH keySize < 1024, \ + EC keySize < 224, 3DES_EDE_CBC, anon, NULL, include jdk.disabled.namedCurves From 0126e0e16006f57ee80b572eff70435496c51e61 Mon Sep 17 00:00:00 2001 From: Adham Ahmed Hussein Mahrous Date: Thu, 16 Jul 2026 04:17:10 +0300 Subject: [PATCH 4/5] Fix formatting issues --- .../okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java index 55030ecd8eb..1022ec78539 100644 --- a/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java +++ b/exporters/sender/okhttp/src/test/java/io/opentelemetry/exporter/sender/okhttp/internal/OkHttpHttpSenderTlsCompatibilityTest.java @@ -62,7 +62,6 @@ *

The test server is restricted to TLSv1/TLSv1.1 because these are the protocol versions that * distinguish MODERN_TLS from COMPATIBLE_TLS. */ - class OkHttpHttpSenderTlsCompatibilityTest { @RegisterExtension From f9f297816b908cb69ad647bbda997683edfcf1e3 Mon Sep 17 00:00:00 2001 From: Adham Ahmed Hussein Mahrous Date: Thu, 16 Jul 2026 04:36:11 +0300 Subject: [PATCH 5/5] Update opentelemetry-sdk-common.txt --- docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt index a9cf0c5c55c..c115fb3cccd 100644 --- a/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt +++ b/docs/apidiffs/current_vs_latest/opentelemetry-sdk-common.txt @@ -1,4 +1,4 @@ -Comparing source compatibility of opentelemetry-sdk-common-1.64.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.64.0.jar +Comparing source compatibility of opentelemetry-sdk-common-1.65.0-SNAPSHOT.jar against opentelemetry-sdk-common-1.64.0.jar *** MODIFIED INTERFACE: PUBLIC ABSTRACT io.opentelemetry.sdk.common.export.HttpSenderConfig (not serializable) === CLASS FILE FORMAT VERSION: 52.0 <- 52.0 +++ NEW METHOD: PUBLIC(+) io.opentelemetry.sdk.common.export.TlsCompatibilityMode getTlsCompatibilityMode()