Skip to content

Commit 3bb0597

Browse files
authored
feat: Add PSC DNS and Global Write Endpoint support to Java Connector (#2286)
The Private Service Connect (PSC) DNS Name Resolution & Fallback feature enables the connector to securely connect to Cloud SQL database instances using Private Service Connect (PSC) DNS and custom domain names (CNAMEs). The connector can now dial PSC instances using its DNS hostname (e.g., 0123456789ab.fedcba9876543.us-central1.sql-psc.goog) or a custom domain CNAME pointing to the instance PSC DNS hostname. The connectors will dynamically resolve this hostname to its real Cloud SQL instance connection name via the SQL Admin API. See also GoogleCloudPlatform/cloud-sql-go-connector#1106
1 parent 0345c57 commit 3bb0597

20 files changed

Lines changed: 925 additions & 120 deletions

.mvn/wrapper/maven-wrapper.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,4 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717
wrapperVersion=3.3.1
18-
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip
18+
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.15/apache-maven-3.9.15-bin.zip

core/src/main/java/com/google/cloud/sql/core/ConnectionInfo.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import com.google.cloud.sql.IpType;
2020
import java.time.Instant;
21+
import java.util.List;
2122
import java.util.Map;
2223
import java.util.stream.Collectors;
2324
import javax.net.ssl.SSLContext;
@@ -45,7 +46,7 @@ SSLContext getSslContext() {
4546
return sslContext;
4647
}
4748

48-
Map<IpType, String> getIpAddrs() {
49+
Map<IpType, List<String>> getIpAddrs() {
4950
return instanceMetadata.getIpAddrs();
5051
}
5152

@@ -55,15 +56,15 @@ SslData getSslData() {
5556

5657
ConnectionMetadata toConnectionMetadata(
5758
ConnectionConfig config, CloudSqlInstanceName instanceName) {
58-
String preferredIp = null;
59+
List<String> preferredIps = null;
5960

6061
for (IpType ipType : config.getIpTypes()) {
61-
preferredIp = getIpAddrs().get(ipType);
62-
if (preferredIp != null) {
62+
preferredIps = getIpAddrs().get(ipType);
63+
if (preferredIps != null && !preferredIps.isEmpty()) {
6364
break;
6465
}
6566
}
66-
if (preferredIp == null) {
67+
if (preferredIps == null || preferredIps.isEmpty()) {
6768
throw new IllegalArgumentException(
6869
String.format(
6970
"[%s] Cloud SQL instance does not have any IP addresses matching preferences (%s)",
@@ -72,7 +73,8 @@ ConnectionMetadata toConnectionMetadata(
7273
}
7374

7475
return new ConnectionMetadata(
75-
preferredIp,
76+
preferredIps,
77+
instanceMetadata.getIpAddrs(),
7678
sslData.getKeyManagerFactory(),
7779
sslData.getTrustManagerFactory(),
7880
sslData.getSslContext(),

core/src/main/java/com/google/cloud/sql/core/ConnectionInfoRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ ConnectionInfo getConnectionInfoSync(
3636
AccessTokenSupplier accessTokenSupplier,
3737
AuthType authType,
3838
KeyPair keyPair);
39+
40+
String resolveConnectionName(String region, String dnsName);
3941
}

core/src/main/java/com/google/cloud/sql/core/ConnectionMetadata.java

Lines changed: 66 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,12 @@
1616

1717
package com.google.cloud.sql.core;
1818

19+
import com.google.cloud.sql.IpType;
20+
import java.util.Collections;
21+
import java.util.HashMap;
1922
import java.util.List;
23+
import java.util.Map;
24+
import java.util.stream.Collectors;
2025
import javax.net.ssl.KeyManagerFactory;
2126
import javax.net.ssl.SSLContext;
2227
import javax.net.ssl.TrustManagerFactory;
@@ -26,29 +31,86 @@
2631
* instance.
2732
*/
2833
public class ConnectionMetadata {
29-
private final String preferredIpAddress;
34+
private final List<String> preferredIpAddresses;
35+
private final Map<IpType, List<String>> ipAddrs;
3036
private final KeyManagerFactory keyManagerFactory;
3137
private final TrustManagerFactory trustManagerFactory;
3238
private final SSLContext sslContext;
3339
private final List<String> mdxProtocolSupport;
3440

3541
/** Construct an immutable ConnectionMetadata. */
3642
public ConnectionMetadata(
37-
String preferredIpAddress,
43+
List<String> preferredIpAddresses,
44+
Map<IpType, List<String>> ipAddrs,
3845
KeyManagerFactory keyManagerFactory,
3946
TrustManagerFactory trustManagerFactory,
4047
SSLContext sslContext,
4148
List<String> mdxProtocolSupport) {
4249

43-
this.preferredIpAddress = preferredIpAddress;
50+
this.preferredIpAddresses = preferredIpAddresses;
51+
this.ipAddrs = ipAddrs;
4452
this.keyManagerFactory = keyManagerFactory;
4553
this.trustManagerFactory = trustManagerFactory;
4654
this.sslContext = sslContext;
4755
this.mdxProtocolSupport = mdxProtocolSupport;
4856
}
4957

58+
/** Construct an immutable ConnectionMetadata (Deprecated). */
59+
@Deprecated
60+
public ConnectionMetadata(
61+
String preferredIpAddress,
62+
Map<IpType, String> ipAddrs,
63+
KeyManagerFactory keyManagerFactory,
64+
TrustManagerFactory trustManagerFactory,
65+
SSLContext sslContext,
66+
List<String> mdxProtocolSupport) {
67+
this(
68+
Collections.singletonList(preferredIpAddress),
69+
compatMap(ipAddrs),
70+
keyManagerFactory,
71+
trustManagerFactory,
72+
sslContext,
73+
mdxProtocolSupport);
74+
}
75+
76+
private static Map<IpType, List<String>> compatMap(Map<IpType, String> map) {
77+
Map<IpType, List<String>> newMap = new HashMap<>();
78+
if (map != null) {
79+
map.forEach((k, v) -> newMap.put(k, Collections.singletonList(v)));
80+
}
81+
return newMap;
82+
}
83+
84+
@Deprecated
5085
public String getPreferredIpAddress() {
51-
return preferredIpAddress;
86+
return preferredIpAddresses == null || preferredIpAddresses.isEmpty()
87+
? null
88+
: preferredIpAddresses.get(0);
89+
}
90+
91+
public List<String> getPreferredIpAddresses() {
92+
return preferredIpAddresses;
93+
}
94+
95+
/**
96+
* Returns the map of IP addresses.
97+
*
98+
* @return the map of IP addresses.
99+
* @deprecated use {@link #getAllIpAddrs()} instead.
100+
*/
101+
@Deprecated
102+
public Map<IpType, String> getIpAddrs() {
103+
if (ipAddrs == null) {
104+
return null;
105+
}
106+
return ipAddrs.entrySet().stream()
107+
.collect(
108+
Collectors.toMap(
109+
Map.Entry::getKey, e -> e.getValue().isEmpty() ? null : e.getValue().get(0)));
110+
}
111+
112+
public Map<IpType, List<String>> getAllIpAddrs() {
113+
return ipAddrs;
52114
}
53115

54116
public KeyManagerFactory getKeyManagerFactory() {

core/src/main/java/com/google/cloud/sql/core/Connector.java

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import java.net.Socket;
3030
import java.net.UnknownHostException;
3131
import java.security.KeyPair;
32+
import java.util.ArrayList;
3233
import java.util.List;
3334
import java.util.Timer;
3435
import java.util.concurrent.ConcurrentHashMap;
@@ -68,18 +69,17 @@ class Connector {
6869
long minRefreshDelayMs,
6970
long refreshTimeoutMs,
7071
int serverProxyPort,
71-
InstanceConnectionNameResolver instanceNameResolver,
7272
DnsResolver dnsResolver,
7373
ProtocolHandler mdxProtocolHandler) {
7474
this.config = config;
7575
this.adminApi =
7676
connectionInfoRepositoryFactory.create(instanceCredentialFactory.create(), config);
77+
this.instanceNameResolver = new DnsInstanceConnectionNameResolver(dnsResolver, this.adminApi);
7778
this.instanceCredentialFactory = instanceCredentialFactory;
7879
this.executor = executor;
7980
this.localKeyPair = localKeyPair;
8081
this.minRefreshDelayMs = minRefreshDelayMs;
8182
this.serverProxyPort = serverProxyPort;
82-
this.instanceNameResolver = instanceNameResolver;
8383
this.dnsResolver = dnsResolver;
8484
this.instanceNameResolverTimer = new Timer("InstanceNameResolverTimer", true);
8585
this.mdxProtocolHandler = mdxProtocolHandler;
@@ -129,7 +129,8 @@ Socket connect(ConnectionConfig config, long timeoutMs) throws IOException {
129129
MonitoredCache instance = getConnection(config);
130130
try {
131131
ConnectionMetadata metadata = instance.getConnectionMetadata(timeoutMs);
132-
String instanceIp = metadata.getPreferredIpAddress();
132+
List<String> preferredIps = metadata.getPreferredIpAddresses();
133+
List<String> targets = new ArrayList<>();
133134

134135
// If a domain name was used to connect, resolve it to an IP address
135136
if (!Strings.isNullOrEmpty(instance.getConfig().getDomainName())) {
@@ -142,15 +143,18 @@ Socket connect(ConnectionConfig config, long timeoutMs) throws IOException {
142143
instance.getConfig().getCloudSqlInstance(),
143144
instance.getConfig().getDomainName(),
144145
addrs.get(0).getHostAddress()));
145-
instanceIp = addrs.get(0).getHostAddress();
146+
for (InetAddress addr : addrs) {
147+
targets.add(addr.getHostAddress());
148+
}
146149
} else {
147150
logger.debug(
148151
String.format(
149152
"[%s] custom DNS name %s resolved but returned no entries, using %s from"
150153
+ " instance metadata",
151154
instance.getConfig().getCloudSqlInstance(),
152155
instance.getConfig().getDomainName(),
153-
instanceIp));
156+
preferredIps.get(0)));
157+
targets.addAll(preferredIps);
154158
}
155159
} catch (UnknownHostException e) {
156160
logger.debug(
@@ -160,31 +164,50 @@ Socket connect(ConnectionConfig config, long timeoutMs) throws IOException {
160164
instance.getConfig().getCloudSqlInstance(),
161165
instance.getConfig().getDomainName(),
162166
e.getMessage(),
163-
instanceIp));
167+
preferredIps.get(0)));
168+
targets.addAll(preferredIps);
164169
}
170+
} else {
171+
targets.addAll(preferredIps);
165172
}
166173

167-
logger.debug(String.format("[%s] Connecting to instance.", instanceIp));
168-
169-
SSLSocket socket = (SSLSocket) metadata.getSslContext().getSocketFactory().createSocket();
170-
socket.setKeepAlive(true);
171-
socket.setTcpNoDelay(true);
172-
173-
socket.connect(new InetSocketAddress(instanceIp, serverProxyPort));
174+
IOException lastEx = null;
175+
SSLSocket socket = null;
176+
String successfulIp = null;
177+
for (String targetIp : targets) {
178+
logger.debug(String.format("[%s] Connecting to instance.", targetIp));
179+
try {
180+
socket = (SSLSocket) metadata.getSslContext().getSocketFactory().createSocket();
181+
socket.setKeepAlive(true);
182+
socket.setTcpNoDelay(true);
183+
socket.connect(new InetSocketAddress(targetIp, serverProxyPort));
184+
socket.startHandshake();
185+
successfulIp = targetIp;
186+
lastEx = null; // success
187+
break;
188+
} catch (IOException e) {
189+
logger.debug(String.format("[%s] Connection failed: %s", targetIp, e.getMessage()));
190+
lastEx = e;
191+
if (socket != null) {
192+
try {
193+
socket.close();
194+
} catch (IOException ce) {
195+
// ignore
196+
}
197+
}
198+
}
199+
}
174200

175-
try {
176-
socket.startHandshake();
177-
} catch (IOException e) {
178-
logger.debug("TLS handshake failed!");
179-
throw e;
201+
if (lastEx != null) {
202+
throw lastEx;
180203
}
181204

182205
if (metadata.isMdxClientProtocolTypeSupport()
183206
&& !Strings.isNullOrEmpty(config.getMdxClientProtocolType())) {
184207
socket = mdxProtocolHandler.connect(socket, config.getMdxClientProtocolType());
185208
}
186209

187-
logger.debug(String.format("[%s] Connected to instance successfully.", instanceIp));
210+
logger.debug(String.format("[%s] Connected to instance successfully.", successfulIp));
188211
instance.addSocket(socket);
189212

190213
return socket;

core/src/main/java/com/google/cloud/sql/core/DefaultConnectionInfoRepository.java

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
import java.util.ArrayList;
5050
import java.util.Arrays;
5151
import java.util.Base64;
52+
import java.util.Collections;
5253
import java.util.HashMap;
5354
import java.util.List;
5455
import java.util.Map;
@@ -271,14 +272,14 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
271272

272273
checkDatabaseCompatibility(instanceMetadata, authType, instanceName.getConnectionName());
273274

274-
Map<IpType, String> ipAddrs = new HashMap<>();
275+
Map<IpType, List<String>> ipAddrs = new HashMap<>();
275276
if (instanceMetadata.getIpAddresses() != null) {
276277
// Update the IP addresses and types need to connect with the instance.
277278
for (IpMapping addr : instanceMetadata.getIpAddresses()) {
278279
if ("PRIVATE".equals(addr.getType())) {
279-
ipAddrs.put(IpType.PRIVATE, addr.getIpAddress());
280+
ipAddrs.put(IpType.PRIVATE, Collections.singletonList(addr.getIpAddress()));
280281
} else if ("PRIMARY".equals(addr.getType())) {
281-
ipAddrs.put(IpType.PUBLIC, addr.getIpAddress());
282+
ipAddrs.put(IpType.PUBLIC, Collections.singletonList(addr.getIpAddress()));
282283
}
283284
// otherwise, we don't know how to handle this type, ignore it.
284285
}
@@ -291,27 +292,38 @@ private InstanceMetadata fetchMetadata(CloudSqlInstanceName instanceName, AuthTy
291292

292293
if (pscEnabled) {
293294
// Search the dns_names field for the PSC DNS Name.
294-
String pscDnsName = null;
295+
List<String> pscDnsNames = new ArrayList<>();
295296
if (instanceMetadata.getDnsNames() != null) {
296297
for (DnsNameMapping dnm : instanceMetadata.getDnsNames()) {
297298
if ("PRIVATE_SERVICE_CONNECT".equals(dnm.getConnectionType())
298299
&& "INSTANCE".equals(dnm.getDnsScope())) {
299-
pscDnsName = dnm.getName();
300-
break;
300+
pscDnsNames.add(dnm.getName());
301301
}
302302
}
303303
}
304304

305305
// If the psc dns name was not found, use the legacy dns_name field
306-
if (pscDnsName == null
306+
if (pscDnsNames.isEmpty()
307307
&& instanceMetadata.getDnsName() != null
308308
&& !instanceMetadata.getDnsName().isEmpty()) {
309-
pscDnsName = instanceMetadata.getDnsName();
309+
pscDnsNames.add(instanceMetadata.getDnsName());
310310
}
311311

312312
// If the psc dns name was found, add it to the ipaddrs map.
313-
if (pscDnsName != null) {
314-
ipAddrs.put(IpType.PSC, pscDnsName);
313+
if (!pscDnsNames.isEmpty()) {
314+
pscDnsNames.sort(
315+
(addr1, addr2) -> {
316+
boolean addr1IsPsc = addr1.endsWith(".sql-psc.goog");
317+
boolean addr2IsPsc = addr2.endsWith(".sql-psc.goog");
318+
if (addr1IsPsc && !addr2IsPsc) {
319+
return -1; // addr1 comes first
320+
}
321+
if (!addr1IsPsc && addr2IsPsc) {
322+
return 1; // addr2 comes first
323+
}
324+
return 0;
325+
});
326+
ipAddrs.put(IpType.PSC, pscDnsNames);
315327
}
316328
}
317329

@@ -544,4 +556,18 @@ private RuntimeException addExceptionContext(
544556
// Fallback to the generic description
545557
return new RuntimeException(message, ex);
546558
}
559+
560+
@Override
561+
public String resolveConnectionName(String region, String dnsName) {
562+
try {
563+
ConnectSettings settings =
564+
new ApiClientRetryingCallable<>(
565+
() -> apiClient.connect().resolve(region, dnsName).execute())
566+
.call();
567+
return settings.getConnectionName();
568+
} catch (Exception ex) {
569+
throw new RuntimeException(
570+
String.format("Failed to resolve PSC DNS name %s in region %s", dnsName, region), ex);
571+
}
572+
}
547573
}

0 commit comments

Comments
 (0)