Skip to content

Commit 1ccbd3d

Browse files
committed
feat: add TLS support to TCP proxy listener
- Add `proxyTLSCertPath` and `proxyTLSKeyPath` options to configuration and options. - Update `Adapter.java` to use `SSLServerSocketFactory` when TLS is configured. - Add validation to require both cert and key files if either is set. - Add unit test for TLS validation in `AdapterTest.java`.
1 parent 52c6b98 commit 1ccbd3d

7 files changed

Lines changed: 175 additions & 16 deletions

File tree

google-cloud-spanner-cassandra/src/main/java/com/google/cloud/spanner/adapter/Adapter.java

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,9 @@ void start() {
9494
}
9595

9696
try {
97+
if (Strings.isNullOrEmpty(options.getProxyTLSCertPath()) != Strings.isNullOrEmpty(options.getProxyTLSKeyPath())) {
98+
throw new IllegalArgumentException("Both proxyTLSCertPath and proxyTLSKeyPath must be specified for TLS support");
99+
}
97100
Credentials credentials = options.getCredentials();
98101
if (options.usePlainText() || !Strings.isNullOrEmpty(options.getExperimentalHostEndpoint())) {
99102
credentials = null;
@@ -180,10 +183,19 @@ void start() {
180183
new AdapterClientWrapper(adapterClient, attachmentsCache, sessionManager);
181184

182185
// Start listening on the specified host and port.
183-
serverSocket =
184-
new ServerSocket(
185-
options.getTcpPort(), DEFAULT_CONNECTION_BACKLOG, options.getInetAddress());
186-
LOG.info("Local TCP server started on {}:{}", options.getInetAddress(), options.getTcpPort());
186+
if (options.useProxyTLS()) {
187+
try {
188+
serverSocket = createSSLServerSocket();
189+
LOG.info("Local TLS server started on {}:{}", options.getInetAddress(), options.getTcpPort());
190+
} catch (Exception e) {
191+
throw new RuntimeException("Failed to create TLS server socket", e);
192+
}
193+
} else {
194+
serverSocket =
195+
new ServerSocket(
196+
options.getTcpPort(), DEFAULT_CONNECTION_BACKLOG, options.getInetAddress());
197+
LOG.info("Local TCP server started on {}:{}", options.getInetAddress(), options.getTcpPort());
198+
}
187199

188200
if (executor == null) {
189201
executor = Executors.newCachedThreadPool();
@@ -195,6 +207,8 @@ void start() {
195207
started = true;
196208
LOG.info("Adapter started for database '{}'.", options.getDatabaseUri());
197209

210+
} catch (IllegalArgumentException e) {
211+
throw e;
198212
} catch (IOException | RuntimeException e) {
199213
throw new AdapterStartException(e);
200214
}
@@ -267,4 +281,38 @@ public AdapterStartException(Throwable cause) {
267281
super("Failed to start the adapter.", cause);
268282
}
269283
}
284+
285+
private ServerSocket createSSLServerSocket() throws Exception {
286+
javax.net.ssl.SSLContext sslContext = createSSLContext(options.getProxyTLSCertPath(), options.getProxyTLSKeyPath());
287+
return sslContext.getServerSocketFactory().createServerSocket(
288+
options.getTcpPort(), DEFAULT_CONNECTION_BACKLOG, options.getInetAddress());
289+
}
290+
291+
private javax.net.ssl.SSLContext createSSLContext(String certPath, String keyPath) throws Exception {
292+
java.security.cert.CertificateFactory cf = java.security.cert.CertificateFactory.getInstance("X.509");
293+
java.security.cert.X509Certificate cert;
294+
try (java.io.FileInputStream certIs = new java.io.FileInputStream(certPath)) {
295+
cert = (java.security.cert.X509Certificate) cf.generateCertificate(certIs);
296+
}
297+
298+
String keyStr = new String(java.nio.file.Files.readAllBytes(java.nio.file.Paths.get(keyPath)))
299+
.replace("-----BEGIN PRIVATE KEY-----", "")
300+
.replace("-----END PRIVATE KEY-----", "")
301+
.replaceAll("\\s", "");
302+
byte[] keyBytes = java.util.Base64.getDecoder().decode(keyStr);
303+
java.security.spec.PKCS8EncodedKeySpec spec = new java.security.spec.PKCS8EncodedKeySpec(keyBytes);
304+
java.security.KeyFactory kf = java.security.KeyFactory.getInstance("RSA");
305+
java.security.PrivateKey key = kf.generatePrivate(spec);
306+
307+
java.security.KeyStore ks = java.security.KeyStore.getInstance(java.security.KeyStore.getDefaultType());
308+
ks.load(null, null);
309+
ks.setKeyEntry("key", key, new char[0], new java.security.cert.Certificate[]{cert});
310+
311+
javax.net.ssl.KeyManagerFactory kmf = javax.net.ssl.KeyManagerFactory.getInstance(javax.net.ssl.KeyManagerFactory.getDefaultAlgorithm());
312+
kmf.init(ks, new char[0]);
313+
314+
javax.net.ssl.SSLContext sslContext = javax.net.ssl.SSLContext.getInstance("TLS");
315+
sslContext.init(kmf.getKeyManagers(), null, null);
316+
return sslContext;
317+
}
270318
}

google-cloud-spanner-cassandra/src/main/java/com/google/cloud/spanner/adapter/AdapterOptions.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ static class Builder {
4545
private String experimentalHostEndpoint = null;
4646
private String clientCertPath = null;
4747
private String clientKeyPath = null;
48+
private String proxyTLSCertPath = null;
49+
private String proxyTLSKeyPath = null;
4850

4951
/** The Cloud Spanner endpoint. */
5052
Builder spannerEndpoint(String spannerEndpoint) {
@@ -132,6 +134,13 @@ Builder useClientCert(String clientCertPath, String clientKeyPath) {
132134
return this;
133135
}
134136

137+
/** (Optional) Use TLS connection for the proxy listener. */
138+
Builder useProxyTLS(String proxyTLSCertPath, String proxyTLSKeyPath) {
139+
this.proxyTLSCertPath = proxyTLSCertPath;
140+
this.proxyTLSKeyPath = proxyTLSKeyPath;
141+
return this;
142+
}
143+
135144
private void validateHostConflict(
136145
String spannerEndpointToCheck, String experimentalHostEndpointToCheck) {
137146
if (!Strings.isNullOrEmpty(spannerEndpointToCheck)
@@ -161,6 +170,8 @@ AdapterOptions build() {
161170
private String experimentalHostEndpoint;
162171
private String clientCertPath;
163172
private String clientKeyPath;
173+
private String proxyTLSCertPath;
174+
private String proxyTLSKeyPath;
164175

165176
private AdapterOptions(Builder builder) {
166177
this.spannerEndpoint = builder.spannerEndpoint;
@@ -177,6 +188,8 @@ private AdapterOptions(Builder builder) {
177188
this.experimentalHostEndpoint = builder.experimentalHostEndpoint;
178189
this.clientCertPath = builder.clientCertPath;
179190
this.clientKeyPath = builder.clientKeyPath;
191+
this.proxyTLSCertPath = builder.proxyTLSCertPath;
192+
this.proxyTLSKeyPath = builder.proxyTLSKeyPath;
180193
}
181194

182195
static Builder newBuilder() {
@@ -242,4 +255,16 @@ String getClientCertPath() {
242255
String getClientKeyPath() {
243256
return clientKeyPath;
244257
}
258+
259+
boolean useProxyTLS() {
260+
return !Strings.isNullOrEmpty(proxyTLSCertPath) && !Strings.isNullOrEmpty(proxyTLSKeyPath);
261+
}
262+
263+
String getProxyTLSCertPath() {
264+
return proxyTLSCertPath;
265+
}
266+
267+
String getProxyTLSKeyPath() {
268+
return proxyTLSKeyPath;
269+
}
245270
}

google-cloud-spanner-cassandra/src/main/java/com/google/cloud/spanner/adapter/Launcher.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,8 @@ private AdapterOptions buildAdapterOptions(
230230
.metricsRecorder(metricsRecorder)
231231
.usePlainText(config.usePlainText())
232232
.setExperimentalHostEndpoint(config.getExperimentalHostEndpoint())
233-
.useClientCert(config.getClientCertPath(), config.getClientKeyPath());
233+
.useClientCert(config.getClientCertPath(), config.getClientKeyPath())
234+
.useProxyTLS(config.getProxyTLSCertPath(), config.getProxyTLSKeyPath());
234235
if (config.getMaxCommitDelayMillis() != null) {
235236
opBuilder.maxCommitDelay(Duration.ofMillis(config.getMaxCommitDelayMillis()));
236237
}

google-cloud-spanner-cassandra/src/main/java/com/google/cloud/spanner/adapter/LauncherConfig.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,8 @@ static LauncherConfig fromUserConfigs(UserConfigs userConfigs) throws UnknownHos
6363
final String experimentalHostEndpoint;
6464
final String clientCertPath;
6565
final String clientKeyPath;
66+
final String proxyTLSCertPath;
67+
final String proxyTLSKeyPath;
6668
HealthCheckConfig healthCheckConfig = null;
6769

6870
if (userConfigs.getGlobalClientConfigs() != null) {
@@ -84,13 +86,17 @@ static LauncherConfig fromUserConfigs(UserConfigs userConfigs) throws UnknownHos
8486
experimentalHostEndpoint = userConfigs.getGlobalClientConfigs().getExperimentalHostEndpoint();
8587
clientCertPath = userConfigs.getGlobalClientConfigs().getClientCertPath();
8688
clientKeyPath = userConfigs.getGlobalClientConfigs().getClientKeyPath();
89+
proxyTLSCertPath = userConfigs.getGlobalClientConfigs().getProxyTLSCertPath();
90+
proxyTLSKeyPath = userConfigs.getGlobalClientConfigs().getProxyTLSKeyPath();
8791
} else {
8892
globalSpannerEndpoint = ConfigConstants.DEFAULT_SPANNER_ENDPOINT;
8993
globalEnableBuiltInMetrics = false;
9094
usePlainText = false;
9195
experimentalHostEndpoint = null;
9296
clientCertPath = null;
9397
clientKeyPath = null;
98+
proxyTLSCertPath = null;
99+
proxyTLSKeyPath = null;
94100
}
95101

96102
List<ListenerConfig> listenerConfigs = new ArrayList<>();
@@ -104,7 +110,9 @@ static LauncherConfig fromUserConfigs(UserConfigs userConfigs) throws UnknownHos
104110
usePlainText,
105111
experimentalHostEndpoint,
106112
clientCertPath,
107-
clientKeyPath));
113+
clientKeyPath,
114+
proxyTLSCertPath,
115+
proxyTLSKeyPath));
108116
}
109117

110118
return new LauncherConfig(listenerConfigs, healthCheckConfig);
@@ -150,6 +158,8 @@ final class ListenerConfig {
150158
private final String experimentalHostEndpoint;
151159
private String clientCertPath;
152160
private String clientKeyPath;
161+
private final String proxyTLSCertPath;
162+
private final String proxyTLSKeyPath;
153163

154164
private ListenerConfig(Builder builder) {
155165
this.databaseUri = builder.databaseUri;
@@ -163,6 +173,8 @@ private ListenerConfig(Builder builder) {
163173
this.experimentalHostEndpoint = builder.experimentalHostEndpoint;
164174
this.clientCertPath = builder.clientCertPath;
165175
this.clientKeyPath = builder.clientKeyPath;
176+
this.proxyTLSCertPath = builder.proxyTLSCertPath;
177+
this.proxyTLSKeyPath = builder.proxyTLSKeyPath;
166178
}
167179

168180
public String getDatabaseUri() {
@@ -210,14 +222,24 @@ public String getClientKeyPath() {
210222
return clientKeyPath;
211223
}
212224

225+
public String getProxyTLSCertPath() {
226+
return proxyTLSCertPath;
227+
}
228+
229+
public String getProxyTLSKeyPath() {
230+
return proxyTLSKeyPath;
231+
}
232+
213233
static ListenerConfig fromListenerConfigs(
214234
ListenerConfigs listener,
215235
String globalSpannerEndpoint,
216236
boolean globalEnableBuiltInMetrics,
217237
boolean usePlainText,
218238
String experimentalHostEndpoint,
219239
String clientCertPath,
220-
String clientKeyPath)
240+
String clientKeyPath,
241+
String proxyTLSCertPath,
242+
String proxyTLSKeyPath)
221243
throws UnknownHostException {
222244
String host = listener.getHost() != null ? listener.getHost() : ConfigConstants.DEFAULT_HOST;
223245
int port = listener.getPort() != null ? listener.getPort() : ConfigConstants.DEFAULT_PORT;
@@ -238,6 +260,7 @@ static ListenerConfig fromListenerConfigs(
238260
.setExperimentalHostEndpoint(experimentalHostEndpoint)
239261
.usePlainText(usePlainText)
240262
.useClientCert(clientCertPath, clientKeyPath)
263+
.useProxyTLS(proxyTLSCertPath, proxyTLSKeyPath)
241264
.build();
242265
}
243266

@@ -269,6 +292,8 @@ static ListenerConfig fromProperties(Map<String, String> properties) throws Unkn
269292
properties.get(ConfigConstants.EXPERIMENTAL_HOST_ENDPOINT_PROP_KEY);
270293
String clientCertPath = properties.get(ConfigConstants.CLIENT_CERT_PATH_PROP_KEY);
271294
String clientKeyPath = properties.get(ConfigConstants.CLIENT_KEY_PATH_PROP_KEY);
295+
String proxyTLSCertPath = properties.get(ConfigConstants.PROXY_TLS_CERT_PATH_PROP_KEY);
296+
String proxyTLSKeyPath = properties.get(ConfigConstants.PROXY_TLS_KEY_PATH_PROP_KEY);
272297
String databaseUri = properties.get(ConfigConstants.DATABASE_URI_PROP_KEY);
273298
if (!Strings.isNullOrEmpty(experimentalHostEndpoint)) {
274299
if (!DatabaseName.isParsableFrom(databaseUri)) {
@@ -288,6 +313,7 @@ static ListenerConfig fromProperties(Map<String, String> properties) throws Unkn
288313
.usePlainText(usePlainText)
289314
.setExperimentalHostEndpoint(experimentalHostEndpoint)
290315
.useClientCert(clientCertPath, clientKeyPath)
316+
.useProxyTLS(proxyTLSCertPath, proxyTLSKeyPath)
291317
.build();
292318
}
293319

@@ -307,6 +333,8 @@ static class Builder {
307333
private String experimentalHostEndpoint;
308334
private String clientCertPath;
309335
private String clientKeyPath;
336+
private String proxyTLSCertPath;
337+
private String proxyTLSKeyPath;
310338

311339
private void validateHostConflict(
312340
String spannerEndpointToCheck, String experimentalHostEndpointToCheck) {
@@ -371,6 +399,12 @@ public Builder useClientCert(String clientCertPath, String clientKeyPath) {
371399
return this;
372400
}
373401

402+
public Builder useProxyTLS(String proxyTLSCertPath, String proxyTLSKeyPath) {
403+
this.proxyTLSCertPath = proxyTLSCertPath;
404+
this.proxyTLSKeyPath = proxyTLSKeyPath;
405+
return this;
406+
}
407+
374408
public ListenerConfig build() {
375409
return new ListenerConfig(this);
376410
}

google-cloud-spanner-cassandra/src/main/java/com/google/cloud/spanner/adapter/configs/ConfigConstants.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,6 @@ private ConfigConstants() {}
3939
public static final String EXPERIMENTAL_HOST_ENDPOINT_PROP_KEY = "experimentalHostEndpoint";
4040
public static final String CLIENT_CERT_PATH_PROP_KEY = "clientCertPath";
4141
public static final String CLIENT_KEY_PATH_PROP_KEY = "clientKeyPath";
42+
public static final String PROXY_TLS_CERT_PATH_PROP_KEY = "proxyTLSCertPath";
43+
public static final String PROXY_TLS_KEY_PATH_PROP_KEY = "proxyTLSKeyPath";
4244
}

google-cloud-spanner-cassandra/src/main/java/com/google/cloud/spanner/adapter/configs/GlobalClientConfigs.java

Lines changed: 31 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public class GlobalClientConfigs {
2828
private final String experimentalHostEndpoint;
2929
private final String clientCertPath;
3030
private final String clientKeyPath;
31+
private final String proxyTLSCertPath;
32+
private final String proxyTLSKeyPath;
3133

3234
public GlobalClientConfigs(
3335
String spannerEndpoint,
@@ -37,13 +39,28 @@ public GlobalClientConfigs(
3739
String experimentalHostEndpoint,
3840
String clientCertPath,
3941
String clientKeyPath) {
42+
this(spannerEndpoint, enableBuiltInMetrics, healthCheckEndpoint, usePlainText, experimentalHostEndpoint, clientCertPath, clientKeyPath, null, null);
43+
}
44+
45+
public GlobalClientConfigs(
46+
String spannerEndpoint,
47+
Boolean enableBuiltInMetrics,
48+
String healthCheckEndpoint,
49+
Boolean usePlainText,
50+
String experimentalHostEndpoint,
51+
String clientCertPath,
52+
String clientKeyPath,
53+
String proxyTLSCertPath,
54+
String proxyTLSKeyPath) {
4055
this.spannerEndpoint = spannerEndpoint;
4156
this.enableBuiltInMetrics = enableBuiltInMetrics;
4257
this.healthCheckEndpoint = healthCheckEndpoint;
4358
this.usePlainText = usePlainText;
4459
this.experimentalHostEndpoint = experimentalHostEndpoint;
4560
this.clientCertPath = clientCertPath;
4661
this.clientKeyPath = clientKeyPath;
62+
this.proxyTLSCertPath = proxyTLSCertPath;
63+
this.proxyTLSKeyPath = proxyTLSKeyPath;
4764
}
4865

4966
public GlobalClientConfigs(
@@ -84,22 +101,19 @@ public static GlobalClientConfigs fromMap(Map<String, Object> yamlMap) {
84101
String experimentalHostEndpoint = (String) yamlMap.get("experimentalHostEndpoint");
85102
String clientCertPath = (String) yamlMap.get("clientCertPath");
86103
String clientKeyPath = (String) yamlMap.get("clientKeyPath");
87-
if (Strings.isNullOrEmpty(clientCertPath) || Strings.isNullOrEmpty(clientKeyPath)) {
88-
return new GlobalClientConfigs(
89-
spannerEndpoint,
90-
enableBuiltInMetrics,
91-
healthCheckEndpoint,
92-
usePlainText,
93-
experimentalHostEndpoint);
94-
}
104+
String proxyTLSCertPath = (String) yamlMap.get("proxyTLSCertPath");
105+
String proxyTLSKeyPath = (String) yamlMap.get("proxyTLSKeyPath");
106+
95107
return new GlobalClientConfigs(
96108
spannerEndpoint,
97109
enableBuiltInMetrics,
98110
healthCheckEndpoint,
99111
usePlainText,
100112
experimentalHostEndpoint,
101113
clientCertPath,
102-
clientKeyPath);
114+
clientKeyPath,
115+
proxyTLSCertPath,
116+
proxyTLSKeyPath);
103117
}
104118

105119
public String getSpannerEndpoint() {
@@ -129,4 +143,12 @@ public String getClientCertPath() {
129143
public String getClientKeyPath() {
130144
return clientKeyPath;
131145
}
146+
147+
public String getProxyTLSCertPath() {
148+
return proxyTLSCertPath;
149+
}
150+
151+
public String getProxyTLSKeyPath() {
152+
return proxyTLSKeyPath;
153+
}
132154
}

google-cloud-spanner-cassandra/src/test/java/com/google/cloud/spanner/adapter/AdapterTest.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,4 +104,31 @@ public void stopWithoutStart() {
104104
// Adapter is in the not-started state.
105105
assertThrows(IllegalStateException.class, adapter::stop);
106106
}
107+
108+
@Test
109+
public void startWithInvalidTLSConfig() {
110+
// Only cert specified
111+
AdapterOptions options1 =
112+
new AdapterOptions.Builder()
113+
.spannerEndpoint(TEST_HOST)
114+
.tcpPort(TEST_PORT)
115+
.databaseUri(TEST_DATABASE_URI)
116+
.inetAddress(inetAddress)
117+
.useProxyTLS("cert.pem", null)
118+
.build();
119+
Adapter adapter1 = new Adapter(options1);
120+
assertThrows(IllegalArgumentException.class, adapter1::start);
121+
122+
// Only key specified
123+
AdapterOptions options2 =
124+
new AdapterOptions.Builder()
125+
.spannerEndpoint(TEST_HOST)
126+
.tcpPort(TEST_PORT)
127+
.databaseUri(TEST_DATABASE_URI)
128+
.inetAddress(inetAddress)
129+
.useProxyTLS(null, "key.pem")
130+
.build();
131+
Adapter adapter2 = new Adapter(options2);
132+
assertThrows(IllegalArgumentException.class, adapter2::start);
133+
}
107134
}

0 commit comments

Comments
 (0)