Skip to content

Commit 01991e3

Browse files
committed
Define a bounded capacity for the internal SNI to SslContext cache.
Motivation: The SNI to SslContext cache does not define a max size, this cache can be filled by TLS client when server SNI is enabled. Client can trigger to load multiple times the same SslContext for a given certificate with arbitrary SNI names when the server uses certificates with a wildcard CN. Changes: Introduce a max size based on LRU policy for the cache with a reasonnable default.
1 parent 93b9cde commit 01991e3

5 files changed

Lines changed: 141 additions & 25 deletions

File tree

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright (c) 2011-2019 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.core.impl.utils;
12+
13+
import java.util.LinkedHashMap;
14+
import java.util.Map;
15+
16+
/**
17+
* Simple LRU cache based, this class is not thread safe.
18+
*/
19+
public class LruCache<K, V> extends LinkedHashMap<K, V> {
20+
21+
private final int maxSize;
22+
23+
public LruCache(int maxSize) {
24+
super(8, 0.75f, true);
25+
if (maxSize < 1) {
26+
throw new IllegalArgumentException("Max size must be > 0");
27+
}
28+
this.maxSize = maxSize;
29+
}
30+
31+
@Override
32+
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
33+
return size() > maxSize;
34+
}
35+
}

vertx-core/src/main/java/io/vertx/core/internal/tls/SslContextManager.java

Lines changed: 6 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import io.vertx.core.VertxException;
1818
import io.vertx.core.buffer.Buffer;
1919
import io.vertx.core.http.ClientAuth;
20+
import io.vertx.core.impl.utils.LruCache;
2021
import io.vertx.core.internal.ContextInternal;
2122
import io.vertx.core.net.*;
2223
import io.vertx.core.spi.tls.SslContextFactory;
@@ -36,6 +37,7 @@
3637
*/
3738
public class SslContextManager {
3839

40+
public static final int DEFAULT_SSL_CONTEXT_PROVIDER_CACHE_SIZE = 64;
3941
private static final Config NULL_CONFIG = new Config(null, null, null, null, null);
4042
static final EnumMap<ClientAuth, io.netty.handler.ssl.ClientAuth> CLIENT_AUTH_MAPPING = new EnumMap<>(ClientAuth.class);
4143

@@ -50,6 +52,10 @@ public class SslContextManager {
5052
private final Map<ConfigKey, Future<Config>> configMap;
5153
private final Map<ConfigKey, Future<SslContextProvider>> sslContextProviderMap;
5254

55+
public SslContextManager(SSLEngineOptions sslEngineOptions) {
56+
this(sslEngineOptions, DEFAULT_SSL_CONTEXT_PROVIDER_CACHE_SIZE);
57+
}
58+
5359
public SslContextManager(SSLEngineOptions sslEngineOptions, int cacheMaxSize) {
5460
this.configMap = new LruCache<>(cacheMaxSize);
5561
this.sslContextProviderMap = new LruCache<>(cacheMaxSize);
@@ -109,10 +115,6 @@ public synchronized int sniEntrySize() {
109115
return size;
110116
}
111117

112-
public SslContextManager(SSLEngineOptions sslEngineOptions) {
113-
this(sslEngineOptions, 256);
114-
}
115-
116118
public Future<SslContextProvider> resolveSslContextProvider(SSLOptions options, String endpointIdentificationAlgorithm, ClientAuth clientAuth, List<String> applicationProtocols, ContextInternal ctx) {
117119
return resolveSslContextProvider(options, endpointIdentificationAlgorithm, clientAuth, applicationProtocols, false, ctx);
118120
}
@@ -236,23 +238,6 @@ private Future<Config> buildConfig(SSLOptions sslOptions, boolean force, Context
236238
return promise.future();
237239
}
238240

239-
private static class LruCache<K, V> extends LinkedHashMap<K, V> {
240-
241-
private final int maxSize;
242-
243-
public LruCache(int maxSize) {
244-
if (maxSize < 1) {
245-
throw new UnsupportedOperationException();
246-
}
247-
this.maxSize = maxSize;
248-
}
249-
250-
@Override
251-
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
252-
return size() > maxSize;
253-
}
254-
}
255-
256241
private final static class ConfigKey {
257242
private final KeyCertOptions keyCertOptions;
258243
private final TrustOptions trustOptions;

vertx-core/src/main/java/io/vertx/core/internal/tls/SslContextProvider.java

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import io.netty.util.AsyncMapping;
1616
import io.vertx.core.VertxException;
1717
import io.vertx.core.http.ClientAuth;
18+
import io.vertx.core.impl.utils.LruCache;
1819
import io.vertx.core.internal.net.VertxSslContext;
1920
import io.vertx.core.spi.tls.SslContextFactory;
2021

@@ -23,7 +24,6 @@
2324
import java.security.cert.CertificateException;
2425
import java.security.cert.X509Certificate;
2526
import java.util.*;
26-
import java.util.concurrent.ConcurrentHashMap;
2727
import java.util.concurrent.Executor;
2828
import java.util.function.Function;
2929
import java.util.function.Supplier;
@@ -35,6 +35,8 @@
3535
*/
3636
public class SslContextProvider {
3737

38+
public static final int DEFAULT_SNI_CACHE_SIZE = 16;
39+
3840
private static int idx(boolean useAlpn) {
3941
return useAlpn ? 0 : 1;
4042
}
@@ -54,7 +56,7 @@ private static int idx(boolean useAlpn) {
5456

5557
private final SslContext[] sslContexts = new SslContext[2];
5658
private final Map<String, SslContext>[] sslContextMaps = new Map[]{
57-
new ConcurrentHashMap<>(), new ConcurrentHashMap<>()
59+
new LruCache<>(DEFAULT_SNI_CACHE_SIZE), new LruCache<>(DEFAULT_SNI_CACHE_SIZE)
5860
};
5961

6062
public SslContextProvider(boolean useWorkerPool,
@@ -88,7 +90,14 @@ public boolean useWorkerPool() {
8890
}
8991

9092
public int sniEntrySize() {
91-
return sslContextMaps[0].size() + sslContextMaps[1].size();
93+
int size;
94+
synchronized (sslContextMaps[0]) {
95+
size = sslContextMaps[0].size();
96+
}
97+
synchronized (sslContextMaps[1]) {
98+
size += sslContextMaps[1].size();
99+
}
100+
return size;
92101
}
93102

94103
public VertxSslContext createContext(boolean server,
@@ -123,7 +132,10 @@ public SslContext sslContext(String serverName, boolean useAlpn, boolean server)
123132
KeyManagerFactory kmf = resolveKeyManagerFactory(serverName);
124133
TrustManager[] trustManagers = resolveTrustManagers(serverName);
125134
if (kmf != null || trustManagers != null || !server) {
126-
return sslContextMaps[idx].computeIfAbsent(serverName, s -> createContext(server, kmf, trustManagers, s, useAlpn));
135+
Map<String, SslContext> ctxMap = sslContextMaps[idx];
136+
synchronized (ctxMap) {
137+
return ctxMap.computeIfAbsent(serverName, s -> createContext(server, kmf, trustManagers, s, useAlpn));
138+
}
127139
}
128140
}
129141
if (sslContexts[idx] == null) {

vertx-core/src/test/java/io/vertx/tests/net/NetTest.java

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
import io.vertx.core.internal.buffer.BufferInternal;
3535
import io.vertx.core.internal.net.NetClientInternal;
3636
import io.vertx.core.internal.net.NetSocketInternal;
37+
import io.vertx.core.internal.tls.SslContextProvider;
3738
import io.vertx.core.json.JsonArray;
3839
import io.vertx.core.json.JsonObject;
3940
import io.vertx.core.net.*;
@@ -1548,6 +1549,52 @@ public void testClientSniMultipleServerName() throws Exception {
15481549
assertEquals(receivedServerNames, serverNames);
15491550
}
15501551

1552+
@Test
1553+
public void testEnabledSniCacheSize() throws Exception {
1554+
testSniCacheSize(true);
1555+
}
1556+
1557+
@Test
1558+
public void testDisabledSniCacheSize() throws Exception {
1559+
testSniCacheSize(false);
1560+
}
1561+
1562+
private void testSniCacheSize(boolean sni) throws Exception {
1563+
List<String> receivedServerNames = Collections.synchronizedList(new ArrayList<>());
1564+
server = vertx.createNetServer(new NetServerOptions()
1565+
.setSni(sni)
1566+
.setSsl(true)
1567+
.setKeyCertOptions(Cert.SNI_JKS.get())
1568+
).connectHandler(so -> {
1569+
receivedServerNames.add(so.indicatedServerName());
1570+
});
1571+
startServer();
1572+
client = vertx.createNetClient(new NetClientOptions().setSsl(true).setHostnameVerificationAlgorithm("").setTrustAll(true));
1573+
int num = 100;
1574+
List<NetSocket> sockets = new ArrayList<>();
1575+
List<String> actualServerNames = new ArrayList<>();
1576+
for (int i = 0;i < num;i++) {
1577+
String serverName = i + ".host3.com";
1578+
NetSocket socket = client.connect(testAddress, serverName).await();
1579+
sockets.add(socket);
1580+
actualServerNames.add(serverName);
1581+
}
1582+
for (NetSocket socket : sockets) {
1583+
socket.close().await();
1584+
}
1585+
assertWaitUntil(() -> num == receivedServerNames.size());
1586+
int size = ((NetServerImpl) server).sniEntrySize();
1587+
if (sni) {
1588+
assertEquals(actualServerNames, receivedServerNames);
1589+
assertEquals(SslContextProvider.DEFAULT_SNI_CACHE_SIZE, size);
1590+
} else {
1591+
for (String receivedServerName : receivedServerNames) {
1592+
assertNull(receivedServerName);
1593+
}
1594+
assertEquals(0, size);
1595+
}
1596+
}
1597+
15511598
@Test
15521599
// SNI present an unknown server
15531600
public void testSniWithUnknownServer1() throws Exception {
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
/*
2+
* Copyright (c) 2011-2026 Contributors to the Eclipse Foundation
3+
*
4+
* This program and the accompanying materials are made available under the
5+
* terms of the Eclipse Public License 2.0 which is available at
6+
* http://www.eclipse.org/legal/epl-2.0, or the Apache License, Version 2.0
7+
* which is available at https://www.apache.org/licenses/LICENSE-2.0.
8+
*
9+
* SPDX-License-Identifier: EPL-2.0 OR Apache-2.0
10+
*/
11+
package io.vertx.tests.utils;
12+
13+
import io.vertx.core.impl.utils.LruCache;
14+
import org.junit.Test;
15+
16+
import static org.junit.Assert.*;
17+
18+
public class LruCacheTest {
19+
20+
@Test
21+
public void testExpiration() {
22+
LruCache<String, String> cache = new LruCache<>(3);
23+
cache.put("0", "0");
24+
cache.put("1", "1");
25+
cache.put("2", "2");
26+
assertTrue(cache.containsKey("0"));
27+
assertTrue(cache.containsKey("1"));
28+
assertTrue(cache.containsKey("2"));
29+
assertNotNull(cache.get("0"));
30+
assertNotNull(cache.get("2"));
31+
cache.put("3", "3");
32+
assertTrue(cache.containsKey("0"));
33+
assertFalse(cache.containsKey("1"));
34+
assertTrue(cache.containsKey("2"));
35+
assertTrue(cache.containsKey("3"));
36+
}
37+
}

0 commit comments

Comments
 (0)