From 59eac987f85d4ad0723ad9d016781aea13c4fbca Mon Sep 17 00:00:00 2001 From: Sergey Talov Date: Tue, 14 Nov 2023 13:18:29 +1100 Subject: [PATCH] TLS Support --- .../elasticache/ElastiCacheResolver.java | 31 ++++++- folsom/pom.xml | 5 ++ .../spotify/folsom/MemcacheClientBuilder.java | 11 ++- .../client/DefaultRawMemcacheClient.java | 30 ++++--- .../client/tls/DefaultSSLEngineFactory.java | 33 ++++++++ .../folsom/client/tls/SSLEngineFactory.java | 7 ++ .../folsom/reconnect/ReconnectingClient.java | 13 ++- .../com/spotify/folsom/IntegrationTest.java | 44 ++++++++-- .../com/spotify/folsom/MemcachedServer.java | 76 ++++++++++++++++-- .../client/DefaultRawMemcacheClientTest.java | 8 ++ folsom/src/test/resources/pki/README | 5 ++ folsom/src/test/resources/pki/test.key | 52 ++++++++++++ folsom/src/test/resources/pki/test.p12 | Bin 0 -> 4045 bytes folsom/src/test/resources/pki/test.pem | 29 +++++++ 14 files changed, 315 insertions(+), 29 deletions(-) create mode 100644 folsom/src/main/java/com/spotify/folsom/client/tls/DefaultSSLEngineFactory.java create mode 100644 folsom/src/main/java/com/spotify/folsom/client/tls/SSLEngineFactory.java create mode 100644 folsom/src/test/resources/pki/README create mode 100644 folsom/src/test/resources/pki/test.key create mode 100644 folsom/src/test/resources/pki/test.p12 create mode 100644 folsom/src/test/resources/pki/test.pem diff --git a/folsom-elasticache/src/main/java/com/spotify/folsom/elasticache/ElastiCacheResolver.java b/folsom-elasticache/src/main/java/com/spotify/folsom/elasticache/ElastiCacheResolver.java index ceff37da..8b66f632 100644 --- a/folsom-elasticache/src/main/java/com/spotify/folsom/elasticache/ElastiCacheResolver.java +++ b/folsom-elasticache/src/main/java/com/spotify/folsom/elasticache/ElastiCacheResolver.java @@ -27,6 +27,7 @@ import java.net.Socket; import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import javax.net.ssl.SSLSocketFactory; /** * Implement support for AWS ElastiCache node auto-discovery netty-transport ${netty.version} + + io.netty + netty-handler + ${netty.version} + io.netty netty-codec diff --git a/folsom/src/main/java/com/spotify/folsom/MemcacheClientBuilder.java b/folsom/src/main/java/com/spotify/folsom/MemcacheClientBuilder.java index 6f74ef4f..7d0f705c 100644 --- a/folsom/src/main/java/com/spotify/folsom/MemcacheClientBuilder.java +++ b/folsom/src/main/java/com/spotify/folsom/MemcacheClientBuilder.java @@ -37,6 +37,7 @@ import com.spotify.folsom.client.NoopTracer; import com.spotify.folsom.client.ascii.DefaultAsciiMemcacheClient; import com.spotify.folsom.client.binary.DefaultBinaryMemcacheClient; +import com.spotify.folsom.client.tls.SSLEngineFactory; import com.spotify.folsom.guava.HostAndPort; import com.spotify.folsom.ketama.AddressAndClient; import com.spotify.folsom.ketama.KetamaMemcacheClient; @@ -125,6 +126,8 @@ public class MemcacheClientBuilder { private final List passwords = new ArrayList<>(); private boolean skipAuth = false; + private SSLEngineFactory sslEngineFactory = null; + /** * Create a client builder for byte array values. * @@ -579,6 +582,11 @@ MemcacheClientBuilder withoutAuthenticationValidation() { return this; } + public MemcacheClientBuilder withSSLEngineFactory(final SSLEngineFactory sslEngineFactory) { + this.sslEngineFactory = sslEngineFactory; + return this; + } + /** * Create a client that uses the binary memcache protocol. * @@ -732,6 +740,7 @@ private RawMemcacheClient createReconnectingClient( metrics, maxSetLength, eventLoopGroup, - channelClass); + channelClass, + sslEngineFactory); } } diff --git a/folsom/src/main/java/com/spotify/folsom/client/DefaultRawMemcacheClient.java b/folsom/src/main/java/com/spotify/folsom/client/DefaultRawMemcacheClient.java index 9c82ad40..60f2b989 100644 --- a/folsom/src/main/java/com/spotify/folsom/client/DefaultRawMemcacheClient.java +++ b/folsom/src/main/java/com/spotify/folsom/client/DefaultRawMemcacheClient.java @@ -30,6 +30,7 @@ import com.spotify.folsom.RawMemcacheClient; import com.spotify.folsom.client.ascii.AsciiMemcacheDecoder; import com.spotify.folsom.client.binary.BinaryMemcacheDecoder; +import com.spotify.folsom.client.tls.SSLEngineFactory; import com.spotify.folsom.guava.HostAndPort; import com.spotify.folsom.ketama.AddressAndClient; import com.spotify.futures.CompletableFutures; @@ -43,6 +44,7 @@ import io.netty.channel.ChannelInboundHandler; import io.netty.channel.ChannelInitializer; import io.netty.channel.ChannelOption; +import io.netty.channel.ChannelPipeline; import io.netty.channel.ChannelPromise; import io.netty.channel.DefaultChannelPromise; import io.netty.channel.EventLoopGroup; @@ -52,6 +54,7 @@ import io.netty.channel.nio.NioEventLoopGroup; import io.netty.channel.socket.nio.NioSocketChannel; import io.netty.handler.codec.DecoderException; +import io.netty.handler.ssl.SslHandler; import io.netty.util.concurrent.DefaultThreadFactory; import java.io.IOException; import java.net.ConnectException; @@ -66,6 +69,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; import java.util.stream.Stream; +import javax.net.ssl.SSLEngine; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -112,7 +116,8 @@ public static CompletionStage connect( final Metrics metrics, final int maxSetLength, final EventLoopGroup eventLoopGroup, - final Class channelClass) { + final Class channelClass, + final SSLEngineFactory sslEngineFactory) { final ChannelInboundHandler decoder; if (binary) { @@ -124,14 +129,21 @@ public static CompletionStage connect( final ChannelHandler initializer = new ChannelInitializer() { @Override - protected void initChannel(final Channel ch) throws Exception { - ch.pipeline() - .addLast( - new TcpTuningHandler(), - decoder, - - // Downstream - new MemcacheEncoder()); + protected void initChannel(final Channel ch) { + final ChannelPipeline channelPipeline = ch.pipeline(); + channelPipeline.addLast(new TcpTuningHandler()); + + if (sslEngineFactory != null) { + final SSLEngine sslEngine = + sslEngineFactory.createSSLEngine(address.getHostText(), address.getPort()); + SslHandler sslHandler = new SslHandler(sslEngine); + // Disable SSL data aggregation + // it doesn't play well with memcached protocol and causes connection hangs + sslHandler.setWrapDataSize(0); + channelPipeline.addLast(sslHandler); + } + + channelPipeline.addLast(decoder, new MemcacheEncoder()); } }; diff --git a/folsom/src/main/java/com/spotify/folsom/client/tls/DefaultSSLEngineFactory.java b/folsom/src/main/java/com/spotify/folsom/client/tls/DefaultSSLEngineFactory.java new file mode 100644 index 00000000..61c7b827 --- /dev/null +++ b/folsom/src/main/java/com/spotify/folsom/client/tls/DefaultSSLEngineFactory.java @@ -0,0 +1,33 @@ +package com.spotify.folsom.client.tls; + +import java.security.NoSuchAlgorithmException; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLEngine; + +public class DefaultSSLEngineFactory implements SSLEngineFactory { + private final SSLContext sslContext; + private final boolean reuseSession; + + public DefaultSSLEngineFactory(final boolean reuseSession) throws NoSuchAlgorithmException { + this(SSLContext.getDefault(), reuseSession); + } + + public DefaultSSLEngineFactory(final SSLContext sslContext, final boolean reuseSession) { + this.sslContext = sslContext; + this.reuseSession = reuseSession; + } + + @Override + public SSLEngine createSSLEngine(final String hostname, final int port) { + final SSLEngine sslEngine; + + if (reuseSession) { + sslEngine = sslContext.createSSLEngine(hostname, port); + } else { + sslEngine = sslContext.createSSLEngine(); + } + + sslEngine.setUseClientMode(true); + return sslEngine; + } +} diff --git a/folsom/src/main/java/com/spotify/folsom/client/tls/SSLEngineFactory.java b/folsom/src/main/java/com/spotify/folsom/client/tls/SSLEngineFactory.java new file mode 100644 index 00000000..982c944e --- /dev/null +++ b/folsom/src/main/java/com/spotify/folsom/client/tls/SSLEngineFactory.java @@ -0,0 +1,7 @@ +package com.spotify.folsom.client.tls; + +import javax.net.ssl.SSLEngine; + +public interface SSLEngineFactory { + SSLEngine createSSLEngine(String hostname, int port); +} diff --git a/folsom/src/main/java/com/spotify/folsom/reconnect/ReconnectingClient.java b/folsom/src/main/java/com/spotify/folsom/reconnect/ReconnectingClient.java index 10d74749..96747f21 100644 --- a/folsom/src/main/java/com/spotify/folsom/reconnect/ReconnectingClient.java +++ b/folsom/src/main/java/com/spotify/folsom/reconnect/ReconnectingClient.java @@ -26,6 +26,7 @@ import com.spotify.folsom.client.DefaultRawMemcacheClient; import com.spotify.folsom.client.NotConnectedClient; import com.spotify.folsom.client.Request; +import com.spotify.folsom.client.tls.SSLEngineFactory; import com.spotify.folsom.guava.HostAndPort; import com.spotify.folsom.ketama.AddressAndClient; import io.netty.channel.Channel; @@ -77,7 +78,8 @@ public ReconnectingClient( final Metrics metrics, final int maxSetLength, final EventLoopGroup eventLoopGroup, - final Class channelClass) { + final Class channelClass, + final SSLEngineFactory sslEngineFactory) { this( backoffFunction, scheduledExecutorService, @@ -93,7 +95,8 @@ public ReconnectingClient( metrics, maxSetLength, eventLoopGroup, - channelClass), + channelClass, + sslEngineFactory), authenticator, address, reconnectionListener); @@ -113,7 +116,8 @@ public ReconnectingClient( final Metrics metrics, final int maxSetLength, final EventLoopGroup eventLoopGroup, - final Class channelClass) { + final Class channelClass, + final SSLEngineFactory sslEngineFactory) { this( backoffFunction, scheduledExecutorService, @@ -129,7 +133,8 @@ public ReconnectingClient( metrics, maxSetLength, eventLoopGroup, - channelClass), + channelClass, + sslEngineFactory), authenticator, address, new StandardReconnectionListener()); diff --git a/folsom/src/test/java/com/spotify/folsom/IntegrationTest.java b/folsom/src/test/java/com/spotify/folsom/IntegrationTest.java index 6771d678..64157f4a 100644 --- a/folsom/src/test/java/com/spotify/folsom/IntegrationTest.java +++ b/folsom/src/test/java/com/spotify/folsom/IntegrationTest.java @@ -29,6 +29,7 @@ import com.google.common.collect.Sets; import com.spotify.folsom.client.NoopMetrics; import com.spotify.folsom.client.Utils; +import com.spotify.folsom.client.tls.DefaultSSLEngineFactory; import com.spotify.futures.CompletableFutures; import java.time.Duration; import java.util.ArrayList; @@ -53,18 +54,24 @@ public class IntegrationTest { private static MemcachedServer server; + private static MemcachedServer tlsServer; - @Parameterized.Parameters(name = "{0}") + @Parameterized.Parameters(name = "{0},useTls={1}") public static Collection data() throws Exception { ArrayList res = new ArrayList<>(); - res.add(new Object[] {"ascii"}); - res.add(new Object[] {"binary"}); + res.add(new Object[] {"ascii", false}); + res.add(new Object[] {"ascii", true}); + res.add(new Object[] {"binary", false}); + res.add(new Object[] {"binary", true}); return res; } @Parameterized.Parameter(0) public String protocol; + @Parameterized.Parameter(1) + public Boolean useTls; + private MemcacheClient client; private AsciiMemcacheClient asciiClient; private BinaryMemcacheClient binaryClient; @@ -73,6 +80,19 @@ public static Collection data() throws Exception { @BeforeClass public static void setUpClass() throws Exception { server = MemcachedServer.SIMPLE_INSTANCE.get(); + + // Use self-signed test certs + String currentDirectory = System.getProperty("user.dir"); + System.setProperty( + "javax.net.ssl.keyStore", currentDirectory + "/src/test/resources/pki/test.p12"); + System.setProperty("javax.net.ssl.keyStoreType", "pkcs12"); + System.setProperty("javax.net.ssl.keyStorePassword", "changeit"); + System.setProperty( + "javax.net.ssl.trustStore", currentDirectory + "/src/test/resources/pki/test.p12"); + System.setProperty("javax.net.ssl.trustStoreType", "pkcs12"); + System.setProperty("javax.net.ssl.trustStorePassword", "changeit"); + + tlsServer = new MemcachedServer(true); } @Before @@ -89,13 +109,17 @@ public void setUp() throws Exception { } MemcacheClientBuilder builder = MemcacheClientBuilder.newStringClient() - .withAddress(server.getHost(), server.getPort()) + .withAddress(server().getHost(), server().getPort()) .withConnections(1) .withMaxOutstandingRequests(1000) .withMetrics(NoopMetrics.INSTANCE) .withRetry(false) .withRequestTimeoutMillis(100); + if (useTls) { + builder.withSSLEngineFactory(new DefaultSSLEngineFactory(true)); + } + if (ascii) { asciiClient = builder.connectAscii(); binaryClient = null; @@ -110,7 +134,7 @@ public void setUp() throws Exception { } private void cleanup() { - server.flush(); + server().flush(); } @After @@ -614,7 +638,7 @@ public void testDeleteWithCasWrongCas() throws Exception { public void testGetAllNodes() { final Map> nodes = client.getAllNodes(); assertEquals(1, nodes.size()); - final MemcacheClient client2 = nodes.get(server.getHost() + ":" + server.getPort()); + final MemcacheClient client2 = nodes.get(server().getHost() + ":" + server().getPort()); assertNotNull(client2); assertEquals(MemcacheStatus.OK, client.set(KEY1, VALUE1, 0).toCompletableFuture().join()); @@ -642,6 +666,14 @@ private String createValue(int size) { return sb.toString(); } + private MemcachedServer server() { + if (useTls) { + return tlsServer; + } else { + return server; + } + } + private void assumeAscii() { assumeTrue(isAscii()); } diff --git a/folsom/src/test/java/com/spotify/folsom/MemcachedServer.java b/folsom/src/test/java/com/spotify/folsom/MemcachedServer.java index 79d0303e..33ce4a7c 100644 --- a/folsom/src/test/java/com/spotify/folsom/MemcachedServer.java +++ b/folsom/src/test/java/com/spotify/folsom/MemcachedServer.java @@ -16,6 +16,8 @@ package com.spotify.folsom; import com.google.common.base.Suppliers; +import com.spotify.folsom.client.tls.DefaultSSLEngineFactory; +import java.security.NoSuchAlgorithmException; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; @@ -38,31 +40,63 @@ public enum AuthenticationMode { Suppliers.memoize(MemcachedServer::new)::get; public static int DEFAULT_PORT = 11211; + + private static final String MEMCACHED_VERSION = "1.6.21"; private final String username; private final String password; + private final boolean useTLS; public MemcachedServer() { this(null, null); } public MemcachedServer(String username, String password) { - this(username, password, DEFAULT_PORT, AuthenticationMode.NONE); + this(username, password, DEFAULT_PORT, AuthenticationMode.NONE, false); } public MemcachedServer(String username, String password, AuthenticationMode authenticationMode) { - this(username, password, DEFAULT_PORT, authenticationMode); + this(username, password, DEFAULT_PORT, authenticationMode, false); + } + + public MemcachedServer(boolean useTLS) { + this(null, null, DEFAULT_PORT, AuthenticationMode.NONE, useTLS); } public MemcachedServer( - String username, String password, int port, AuthenticationMode authenticationMode) { + String username, + String password, + int port, + AuthenticationMode authenticationMode, + boolean useTLS) { this.username = username; this.password = password; - container = new FixedHostPortGenericContainer("bitnami/memcached:1.6.21"); + this.useTLS = useTLS; + + if (useTLS) { + if (authenticationMode != AuthenticationMode.NONE) { + throw new RuntimeException( + "Authentication is not currently supported with the TLS-enabled container"); + } + + container = setupTLSContainer(); + } else { + container = setupContainer(username, password, authenticationMode); + } + if (port != DEFAULT_PORT) { container.withFixedExposedPort(DEFAULT_PORT, port); } else { container.withExposedPorts(DEFAULT_PORT); } + + start(authenticationMode); + } + + private FixedHostPortGenericContainer setupContainer( + String username, String password, AuthenticationMode authenticationMode) { + final FixedHostPortGenericContainer container = + new FixedHostPortGenericContainer("bitnami/memcached:" + MEMCACHED_VERSION); + switch (authenticationMode) { case NONE: break; @@ -78,7 +112,30 @@ public MemcachedServer( break; } - start(authenticationMode); + return container; + } + + private FixedHostPortGenericContainer setupTLSContainer() { + final FixedHostPortGenericContainer container = + new FixedHostPortGenericContainer("memcached:" + MEMCACHED_VERSION); + + container.withClasspathResourceMapping( + "/pki/test.pem", "/test-certs/test.pem", BindMode.READ_ONLY); + container.withClasspathResourceMapping( + "/pki/test.key", "/test-certs/test.key", BindMode.READ_ONLY); + + container.withCommand( + "memcached", + "--enable-ssl", + "-o", + "ssl_verify_mode=3", + "-o", + "ssl_chain_cert=/test-certs/test.pem", + "-o", + "ssl_key=/test-certs/test.key", + "-o", + "ssl_ca_cert=/test-certs/test.pem"); + return container; } public void stop() { @@ -102,6 +159,15 @@ public void start(AuthenticationMode authenticationMode) { if (username != null && password != null) { builder.withUsernamePassword(username, password); } + + if (useTLS) { + try { + builder.withSSLEngineFactory(new DefaultSSLEngineFactory(false)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + switch (authenticationMode) { case NONE: case SASL: diff --git a/folsom/src/test/java/com/spotify/folsom/client/DefaultRawMemcacheClientTest.java b/folsom/src/test/java/com/spotify/folsom/client/DefaultRawMemcacheClientTest.java index 8ff979a0..6f7e9a2e 100644 --- a/folsom/src/test/java/com/spotify/folsom/client/DefaultRawMemcacheClientTest.java +++ b/folsom/src/test/java/com/spotify/folsom/client/DefaultRawMemcacheClientTest.java @@ -80,6 +80,7 @@ public void testInvalidRequest() throws Exception { new NoopMetrics(), 1024 * 1024, null, + null, null) .toCompletableFuture() .get(); @@ -182,6 +183,7 @@ public void testRequestTimeout() throws Exception { new NoopMetrics(), 1024 * 1024, null, + null, null) .toCompletableFuture() .get(); @@ -221,6 +223,7 @@ public void testBinaryRequestRetry() throws Exception { new NoopMetrics(), maxSetLength, null, + null, null) .toCompletableFuture() .get(); @@ -265,6 +268,7 @@ public void testAsciiRequestRetry() throws Exception { new NoopMetrics(), maxSetLength, null, + null, null) .toCompletableFuture() .get(); @@ -301,6 +305,7 @@ public void testShutdown() throws Exception { new NoopMetrics(), 1024 * 1024, null, + null, null) .toCompletableFuture() .get(); @@ -328,6 +333,7 @@ public void testShutdownRequestExceptionInsteadOfOverloaded() throws Throwable { new NoopMetrics(), 1024 * 1024, null, + null, null) .toCompletableFuture() .get(); @@ -376,6 +382,7 @@ public void testShutdownRequestException() throws Throwable { new NoopMetrics(), 1024 * 1024, null, + null, null) .toCompletableFuture() .get(); @@ -416,6 +423,7 @@ public void testOutstandingRequestMetric() throws Exception { metrics, 1024 * 1024, null, + null, null) .toCompletableFuture() .get(); diff --git a/folsom/src/test/resources/pki/README b/folsom/src/test/resources/pki/README new file mode 100644 index 00000000..5b234ae2 --- /dev/null +++ b/folsom/src/test/resources/pki/README @@ -0,0 +1,5 @@ +These test certificates were generated as follows: + +openssl req -x509 -newkey rsa:4096 -keyout test.key -out test.pem -sha256 -days 3650 -nodes -subj '/CN=localhost' +openssl pkcs12 -export -inkey test.key -in test.pem -out test.p12 -password pass:changeit + diff --git a/folsom/src/test/resources/pki/test.key b/folsom/src/test/resources/pki/test.key new file mode 100644 index 00000000..822760c8 --- /dev/null +++ b/folsom/src/test/resources/pki/test.key @@ -0,0 +1,52 @@ +-----BEGIN PRIVATE KEY----- +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 +G6gcbkorel8YtBdPnaweR3yhIwhcyA== +-----END PRIVATE KEY----- diff --git a/folsom/src/test/resources/pki/test.p12 b/folsom/src/test/resources/pki/test.p12 new file mode 100644 index 0000000000000000000000000000000000000000..03d38dcfaabd9b6f8a06fb88c436cc9b25566e41 GIT binary patch literal 4045 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