This is an automated email from the ASF dual-hosted git repository.
zaynt4606 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/celeborn.git
The following commit(s) were added to refs/heads/main by this push:
new 9ae2834ee0 [CELEBORN-2386] Pass peer hostname to SSLEngine for SNI
support
9ae2834ee0 is described below
commit 9ae2834ee0cdb211687e2dbd0753b3c6417a1a26
Author: hzheng182 <[email protected]>
AuthorDate: Sat Oct 31 18:29:12 2026 +0800
[CELEBORN-2386] Pass peer hostname to SSLEngine for SNI support
### What changes were proposed in this pull request?
Thread peer hostname/port from
`TransportClientFactory.internalCreateClient()` through
`TransportContext.initializePipeline()` to `SSLFactory.createSSLEngine()`.
`SSLFactory.createEngine()` now calls `jdkSslContext.createSSLEngine(peerHost,
peerPort)` for client connections, which makes the JDK include the SNI
extension in the TLS ClientHello. Existing server-side and no-arg callers use
backward-compatible overloads.
### Why are the changes needed?
`SSLFactory.createEngine()` called `jdkSslContext.createSSLEngine()` with
no arguments. The JDK only includes the SNI extension in the TLS ClientHello
when a peer hostname is provided via `createSSLEngine(host, port)`. Without
SNI, TLS termination/routing that depends on server name (e.g. an SNI-based
ingress/gateway such as Istio TLS PASSTHROUGH or SIMPLE mode) cannot route or
differentiate connections by hostname. This fix is a prerequisite for
shared-port multi-tenant routing throu [...]
### Does this PR introduce _any_ user-facing change?
No. Client `SSLEngine` creation now includes SNI; server-side and existing
no-arg callers are unaffected.
### How was this patch tested?
- `SSLFactorySuiteJ`: client engines send SNI for a peer host name, and
omit it without a peer host or port, for IP literals, for server engines, and
through the existing no-arg overload.
- `SslConnectivitySuiteJ.testSniHostNameReachesServer`: connects through
`TransportClientFactory` to a host name (`sni.localhost`) with pooled and
unmanaged clients over TLS 1.2 and TLS 1.3, and asserts the server observes it
via `ExtendedSSLSession.getRequestedServerNames()`. This fails if the peer host
or port is dropped anywhere along `TransportClientFactory` ->
`TransportContext` -> `SSLFactory`. The JDK sends SNI only for host names
containing a dot, hence a `.localhost` name; th [...]
Closes #3764 from shellfish007/upstream-pr/ssl-sni-support.
Authored-by: hzheng182 <[email protected]>
Signed-off-by: zhengtao <[email protected]>
AI-Contributed/Feature: 0/49
AI-Contributed/UT: 0/191
---
.../celeborn/common/network/TransportContext.java | 23 ++++-
.../network/client/TransportClientFactory.java | 4 +-
.../celeborn/common/network/ssl/SSLFactory.java | 22 +++-
.../common/network/ssl/SSLFactorySuiteJ.java | 112 +++++++++++++++++++++
.../common/network/ssl/SslConnectivitySuiteJ.java | 79 +++++++++++++++
5 files changed, 236 insertions(+), 4 deletions(-)
diff --git
a/common/src/main/java/org/apache/celeborn/common/network/TransportContext.java
b/common/src/main/java/org/apache/celeborn/common/network/TransportContext.java
index 464e9d1e62..7145bdbaac 100644
---
a/common/src/main/java/org/apache/celeborn/common/network/TransportContext.java
+++
b/common/src/main/java/org/apache/celeborn/common/network/TransportContext.java
@@ -177,6 +177,15 @@ public class TransportContext implements Closeable {
return initializePipeline(channel, decoder, msgHandler, isClient);
}
+ public TransportChannelHandler initializePipeline(
+ SocketChannel channel,
+ ChannelInboundHandlerAdapter decoder,
+ boolean isClient,
+ String peerHost,
+ int peerPort) {
+ return initializePipeline(channel, decoder, msgHandler, isClient,
peerHost, peerPort);
+ }
+
public TransportChannelHandler initializePipeline(
SocketChannel channel, BaseMessageHandler resolvedMsgHandler, boolean
isClient) {
return initializePipeline(channel, new TransportFrameDecoder(),
resolvedMsgHandler, isClient);
@@ -187,6 +196,16 @@ public class TransportContext implements Closeable {
ChannelInboundHandlerAdapter decoder,
BaseMessageHandler resolvedMsgHandler,
boolean isClient) {
+ return initializePipeline(channel, decoder, resolvedMsgHandler, isClient,
null, -1);
+ }
+
+ public TransportChannelHandler initializePipeline(
+ SocketChannel channel,
+ ChannelInboundHandlerAdapter decoder,
+ BaseMessageHandler resolvedMsgHandler,
+ boolean isClient,
+ String peerHost,
+ int peerPort) {
try {
ChannelPipeline pipeline = channel.pipeline();
if (nettyLogger.getLoggingHandler() != null) {
@@ -199,7 +218,9 @@ public class TransportContext implements Closeable {
SslHandler sslHandler;
try {
- sslHandler = new SslHandler(sslFactory.createSSLEngine(isClient,
channel.alloc()));
+ sslHandler =
+ new SslHandler(
+ sslFactory.createSSLEngine(isClient, channel.alloc(),
peerHost, peerPort));
sslHandler.setHandshakeTimeoutMillis(conf.sslHandshakeTimeoutMs());
} catch (Exception e) {
throw new IllegalStateException("Error creating Netty SslHandler",
e);
diff --git
a/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
b/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
index b79383277a..f96ddceb96 100644
---
a/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
+++
b/common/src/main/java/org/apache/celeborn/common/network/client/TransportClientFactory.java
@@ -309,7 +309,9 @@ public class TransportClientFactory implements Closeable {
new ChannelInitializer<SocketChannel>() {
@Override
public void initChannel(SocketChannel ch) {
- TransportChannelHandler clientHandler =
context.initializePipeline(ch, decoder, true);
+ TransportChannelHandler clientHandler =
+ context.initializePipeline(
+ ch, decoder, true, address.getHostString(),
address.getPort());
clientRef.set(clientHandler.getClient());
}
});
diff --git
a/common/src/main/java/org/apache/celeborn/common/network/ssl/SSLFactory.java
b/common/src/main/java/org/apache/celeborn/common/network/ssl/SSLFactory.java
index a0eb106bf2..c3d872186f 100644
---
a/common/src/main/java/org/apache/celeborn/common/network/ssl/SSLFactory.java
+++
b/common/src/main/java/org/apache/celeborn/common/network/ssl/SSLFactory.java
@@ -284,7 +284,21 @@ public class SSLFactory {
* @return A valid {@link SSLEngine}.
*/
public SSLEngine createSSLEngine(boolean isClient, ByteBufAllocator
allocator) {
- SSLEngine engine = createEngine(isClient, allocator);
+ return createSSLEngine(isClient, allocator, null, -1);
+ }
+
+ /**
+ * Creates a new {@link SSLEngine} with peer hostname for SNI support.
+ *
+ * @param isClient Whether the engine is used in a client context
+ * @param allocator The {@link ByteBufAllocator to use}
+ * @param peerHost The peer hostname (used for SNI in client mode)
+ * @param peerPort The peer port
+ * @return A valid {@link SSLEngine}.
+ */
+ public SSLEngine createSSLEngine(
+ boolean isClient, ByteBufAllocator allocator, String peerHost, int
peerPort) {
+ SSLEngine engine = createEngine(isClient, allocator, peerHost, peerPort);
engine.setUseClientMode(isClient);
engine.setWantClientAuth(true);
engine.setEnabledProtocols(enabledProtocols(engine, requestedProtocol));
@@ -292,7 +306,11 @@ public class SSLFactory {
return engine;
}
- private SSLEngine createEngine(boolean isClient, ByteBufAllocator allocator)
{
+ private SSLEngine createEngine(
+ boolean isClient, ByteBufAllocator allocator, String peerHost, int
peerPort) {
+ if (isClient && peerHost != null && peerPort > 0) {
+ return jdkSslContext.createSSLEngine(peerHost, peerPort);
+ }
return jdkSslContext.createSSLEngine();
}
diff --git
a/common/src/test/java/org/apache/celeborn/common/network/ssl/SSLFactorySuiteJ.java
b/common/src/test/java/org/apache/celeborn/common/network/ssl/SSLFactorySuiteJ.java
new file mode 100644
index 0000000000..859fc3cb3a
--- /dev/null
+++
b/common/src/test/java/org/apache/celeborn/common/network/ssl/SSLFactorySuiteJ.java
@@ -0,0 +1,112 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package org.apache.celeborn.common.network.ssl;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import java.util.List;
+
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLEngine;
+
+import io.netty.buffer.ByteBufAllocator;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.apache.celeborn.common.CelebornConf;
+import org.apache.celeborn.common.network.TestHelper;
+import org.apache.celeborn.common.network.util.TransportConf;
+
+/** Tests that client engines carry the peer hostname so the ClientHello
includes SNI. */
+public class SSLFactorySuiteJ {
+
+ private static final String TEST_MODULE = "rpc";
+ // SNI carries DNS names only, so this must be multi-label and not an IP
literal.
+ private static final String PEER_HOST = "celeborn-master-1.example.com";
+ private static final int PEER_PORT = 9097;
+
+ private SSLFactory factory;
+
+ @Before
+ public void setUp() {
+ CelebornConf celebornConf = new CelebornConf();
+ TestHelper.updateCelebornConfWithMap(
+ celebornConf,
SslSampleConfigs.createDefaultConfigMapForModule(TEST_MODULE));
+ factory = SSLFactory.createSslFactory(new TransportConf(TEST_MODULE,
celebornConf));
+ assertNotNull(factory);
+ }
+
+ @After
+ public void tearDown() {
+ if (factory != null) {
+ factory.destroy();
+ factory = null;
+ }
+ }
+
+ private static void assertSniHost(SSLEngine engine, String expectedHost) {
+ List<SNIServerName> serverNames =
engine.getSSLParameters().getServerNames();
+ assertNotNull("expected an SNI server_name entry", serverNames);
+ assertEquals(1, serverNames.size());
+ assertEquals(new SNIHostName(expectedHost), serverNames.get(0));
+ }
+
+ private static void assertNoSni(SSLEngine engine) {
+ List<SNIServerName> serverNames =
engine.getSSLParameters().getServerNames();
+ assertTrue("expected no SNI server_name entry", serverNames == null ||
serverNames.isEmpty());
+ }
+
+ @Test
+ public void testClientEngineSendsSniForPeerHost() {
+ SSLEngine engine =
+ factory.createSSLEngine(true, ByteBufAllocator.DEFAULT, PEER_HOST,
PEER_PORT);
+ assertEquals(PEER_HOST, engine.getPeerHost());
+ assertEquals(PEER_PORT, engine.getPeerPort());
+ assertSniHost(engine, PEER_HOST);
+ }
+
+ @Test
+ public void testClientEngineWithoutPeerHostOmitsSni() {
+ assertNoSni(factory.createSSLEngine(true, ByteBufAllocator.DEFAULT, null,
PEER_PORT));
+ }
+
+ @Test
+ public void testClientEngineWithoutPeerPortOmitsSni() {
+ assertNoSni(factory.createSSLEngine(true, ByteBufAllocator.DEFAULT,
PEER_HOST, -1));
+ }
+
+ @Test
+ public void testClientEngineOmitsSniForIpLiteral() {
+ // RFC 6066 disallows IP literals in server_name, so the JDK drops the
extension.
+ assertNoSni(factory.createSSLEngine(true, ByteBufAllocator.DEFAULT,
"127.0.0.1", PEER_PORT));
+ }
+
+ @Test
+ public void testServerEngineOmitsSni() {
+ assertNoSni(factory.createSSLEngine(false, ByteBufAllocator.DEFAULT,
PEER_HOST, PEER_PORT));
+ }
+
+ @Test
+ public void testExistingOverloadOmitsSni() {
+ assertNoSni(factory.createSSLEngine(true, ByteBufAllocator.DEFAULT));
+ }
+}
diff --git
a/common/src/test/java/org/apache/celeborn/common/network/ssl/SslConnectivitySuiteJ.java
b/common/src/test/java/org/apache/celeborn/common/network/ssl/SslConnectivitySuiteJ.java
index 52fadb69c4..3e942e7574 100644
---
a/common/src/test/java/org/apache/celeborn/common/network/ssl/SslConnectivitySuiteJ.java
+++
b/common/src/test/java/org/apache/celeborn/common/network/ssl/SslConnectivitySuiteJ.java
@@ -19,16 +19,27 @@ package org.apache.celeborn.common.network.ssl;
import static org.apache.celeborn.common.util.JavaUtils.getLocalHost;
import static org.junit.Assert.*;
+import static org.junit.Assume.assumeTrue;
import java.io.Closeable;
import java.io.IOException;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
import java.nio.ByteBuffer;
+import java.util.List;
import java.util.Map;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
+import java.util.stream.Collectors;
+import javax.net.ssl.ExtendedSSLSession;
+import javax.net.ssl.SNIHostName;
+import javax.net.ssl.SNIServerName;
+import javax.net.ssl.SSLSession;
+
+import io.netty.handler.ssl.SslHandler;
import org.apache.commons.lang3.tuple.Pair;
import org.junit.Test;
@@ -105,6 +116,13 @@ public class SslConnectivitySuiteJ {
return clientFactory.createClient(getLocalHost(), server.getPort());
}
+ TransportClient createClient(String host, boolean unmanaged)
+ throws IOException, InterruptedException {
+ return unmanaged
+ ? clientFactory.createUnmanagedClient(host, server.getPort())
+ : clientFactory.createClient(host, server.getPort());
+ }
+
@Override
public void close() throws IOException {
JavaUtils.closeQuietly(server);
@@ -134,6 +152,28 @@ public class SslConnectivitySuiteJ {
}
}
+ // Replies with the SNI host names the server observed on the client's
connection.
+ private static class SniEchoMessageHandler extends BaseMessageHandler {
+
+ @Override
+ public void receive(
+ TransportClient client, RequestMessage requestMessage,
RpcResponseCallback callback) {
+ SSLSession session =
+
client.getChannel().pipeline().get(SslHandler.class).engine().getSession();
+ List<SNIServerName> serverNames = ((ExtendedSSLSession)
session).getRequestedServerNames();
+ String response =
+ serverNames.stream()
+ .map(name -> ((SNIHostName) name).getAsciiName())
+ .collect(Collectors.joining(","));
+ callback.onSuccess(JavaUtils.stringToBytes(response));
+ }
+
+ @Override
+ public boolean checkRegistered() {
+ return true;
+ }
+ }
+
// Pair<Success, Failure> ... should be an Either actually.
private Pair<String, String> sendRPC(TransportClient client, String message,
boolean canTimeout)
@@ -394,6 +434,45 @@ public class SslConnectivitySuiteJ {
}
}
+ // Connects through TransportClientFactory using a host name, and verifies
the server observes
+ // it as SNI - i.e. the peer host and port reach the client SSLEngine.
+ @Test
+ public void testSniHostNameReachesServer() throws Exception {
+ // The JDK sends SNI only for host names containing a dot, so "localhost"
is not enough.
+ // Names under ".localhost" resolve to loopback (RFC 6761) on most
resolvers.
+ final String sniHost = "sni.localhost";
+ assumeTrue(sniHost + " does not resolve", resolves(sniHost));
+
+ for (String protocol : new String[] {"TLSv1.2", "TLSv1.3"}) {
+ final Function<CelebornConf, CelebornConf> updateConf =
+ conf -> {
+ conf.set("celeborn.ssl." + TEST_MODULE + ".protocol", protocol);
+ return conf;
+ };
+ for (boolean unmanaged : new boolean[] {false, true}) {
+ try (TestTransportState state =
+ new TestTransportState(
+ new SniEchoMessageHandler(),
+ createTransportConf(TEST_MODULE, true, true, updateConf),
+ createTransportConf(TEST_MODULE, true, true, updateConf));
+ TransportClient client = state.createClient(sniHost, unmanaged)) {
+ Pair<String, String> response = sendRPC(client, "sni", false);
+ assertNull(response.getRight());
+ assertEquals(protocol + ", unmanaged=" + unmanaged, sniHost,
response.getLeft());
+ }
+ }
+ }
+ }
+
+ private static boolean resolves(String host) {
+ try {
+ InetAddress.getByName(host);
+ return true;
+ } catch (UnknownHostException e) {
+ return false;
+ }
+ }
+
@Test
public void testAutoSslConnectivity() throws Exception {