Skip to content

Commit

Permalink
feat: Automatically configure connections using DNS. Part of #2043.
Browse files Browse the repository at this point in the history
wip: fix validation in InternalConnectorRegistry

wip: e2e postgres custom san test
  • Loading branch information
hessjcg committed Jan 14, 2025
1 parent 44f803f commit 89c1f22
Show file tree
Hide file tree
Showing 11 changed files with 389 additions and 57 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class CloudSqlInstanceName {
private final String regionId;
private final String instanceId;
private final String connectionName;
private final String domainName;

/**
* Checks if a string is a well-formed instance name.
Expand All @@ -55,7 +56,17 @@ public static boolean isValidInstanceName(String connectionName) {
* @param connectionName instance connection name in the format "PROJECT_ID:REGION_ID:INSTANCE_ID"
*/
CloudSqlInstanceName(String connectionName) {
this(connectionName, null);
}

/**
* Initializes a new CloudSqlInstanceName class containing the domain name.
*
* @param connectionName
*/
CloudSqlInstanceName(String connectionName, String domainName) {
this.connectionName = connectionName;
this.domainName = domainName;
Matcher matcher = CONNECTION_NAME.matcher(connectionName);
checkArgument(
matcher.matches(),
Expand Down Expand Up @@ -84,6 +95,10 @@ String getInstanceId() {
return instanceId;
}

String getDomainName() {
return domainName;
}

@Override
public String toString() {
return connectionName;
Expand Down
17 changes: 10 additions & 7 deletions core/src/main/java/com/google/cloud/sql/core/Connector.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ class Connector {
private final int serverProxyPort;
private final ConnectorConfig config;

private final InstanceConnectionNameResolver instanceNameResolver;

Connector(
ConnectorConfig config,
ConnectionInfoRepositoryFactory connectionInfoRepositoryFactory,
Expand All @@ -57,7 +59,8 @@ class Connector {
ListenableFuture<KeyPair> localKeyPair,
long minRefreshDelayMs,
long refreshTimeoutMs,
int serverProxyPort) {
int serverProxyPort,
InstanceConnectionNameResolver instanceNameResolver) {
this.config = config;

this.adminApi =
Expand All @@ -67,6 +70,7 @@ class Connector {
this.localKeyPair = localKeyPair;
this.minRefreshDelayMs = minRefreshDelayMs;
this.serverProxyPort = serverProxyPort;
this.instanceNameResolver = instanceNameResolver;
}

public ConnectorConfig getConfig() {
Expand Down Expand Up @@ -174,17 +178,16 @@ private ConnectionConfig resolveConnectionName(ConnectionConfig config) {
final String unresolvedName = config.getDomainName();
final Function<String, String> resolver =
config.getConnectorConfig().getInstanceNameResolver();
CloudSqlInstanceName name;
if (resolver != null) {
return config.withCloudSqlInstance(resolver.apply(unresolvedName));
name = instanceNameResolver.resolve(resolver.apply(unresolvedName));
} else {
throw new IllegalStateException(
"Can't resolve domain " + unresolvedName + ". ConnectorConfig.resolver is not set.");
name = instanceNameResolver.resolve(unresolvedName);
}
return config.withCloudSqlInstance(name.getConnectionName());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
String.format(
"Cloud SQL connection name is invalid: \"%s\"", config.getCloudSqlInstance()),
e);
String.format("Cloud SQL connection name is invalid: \"%s\"", config.getDomainName()), e);
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
/*
* Copyright 2025 Google LLC
*
* Licensed 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 com.google.cloud.sql.core;

import java.util.Collection;
import java.util.Objects;
import javax.naming.NameNotFoundException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
* An implementation of InstanceConnectionNameResolver that uses DNS TXT records to resolve an
* instance name from a domain name.
*/
class DnsInstanceConnectionNameResolver implements InstanceConnectionNameResolver {
private static final Logger logger =
LoggerFactory.getLogger(DnsInstanceConnectionNameResolver.class);

private final DnsResolver dnsResolver;

public DnsInstanceConnectionNameResolver(DnsResolver dnsResolver) {
this.dnsResolver = dnsResolver;
}

@Override
public CloudSqlInstanceName resolve(final String name) {
// Attempt to parse the instance name
try {
return new CloudSqlInstanceName(name);
} catch (IllegalArgumentException e) {
// Not a well-formed instance name.
}

// Next, attempt to resolve DNS name.
Collection<String> instanceNames;
try {
instanceNames = this.dnsResolver.resolveTxt(name);
} catch (NameNotFoundException ne) {
// No DNS record found. This is not a valid instance name.
throw new IllegalArgumentException(
String.format("Unable to resolve TXT record for \"%s\".", name));
}

// Use the first valid instance name from the list
// or throw an IllegalArgumentException if none of the values can be parsed.
return instanceNames.stream()
.map(
target -> {
try {
return new CloudSqlInstanceName(target, name);
} catch (IllegalArgumentException e) {
logger.info(
"Unable to parse instance name in TXT record for "
+ "domain name \"{}\" with target \"{}\"",
name,
target,
e);
return null;
}
})
.filter(Objects::nonNull)
.findFirst()
.orElseThrow(
() ->
new IllegalArgumentException(
String.format("Unable to parse values of TXT record for \"%s\".", name)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
import java.util.Collection;
import javax.naming.NameNotFoundException;

/** Wraps the Java DNS API. */
interface DnsResolver {
Collection<String> resolveTxt(String domainName) throws NameNotFoundException;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

package com.google.cloud.sql.core;

import com.google.common.base.Strings;
import java.net.Socket;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
Expand Down Expand Up @@ -103,12 +104,19 @@ private void checkCertificateChain(X509Certificate[] chain) throws CertificateEx
}

private void checkSan(X509Certificate[] chain) throws CertificateException {
List<String> sans = getSans(chain[0]);
String dns = instanceMetadata.getDnsName();
final String dns;
if (!Strings.isNullOrEmpty(instanceMetadata.getInstanceName().getDomainName())) {
dns = instanceMetadata.getInstanceName().getDomainName();
} else {
dns = instanceMetadata.getDnsName();
}

if (dns == null || dns.isEmpty()) {
throw new CertificateException(
"Instance metadata for " + instanceMetadata.getInstanceName() + " has an empty dnsName");
}

List<String> sans = getSans(chain[0]);
for (String san : sans) {
if (san.equalsIgnoreCase(dns)) {
return;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*
* Copyright 2025 Google LLC
*
* Licensed 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 com.google.cloud.sql.core;

/** Resolves the Cloud SQL Instance from the configuration name. */
interface InstanceConnectionNameResolver {

/**
* Resolves the CloudSqlInstanceName from a configuration string value.
*
* @param name the configuration string
* @return the CloudSqlInstanceName
* @throws IllegalArgumentException if the name cannot be resolved.
*/
CloudSqlInstanceName resolve(String name);
}
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,9 @@ public Socket connect(ConnectionConfig config) throws IOException, InterruptedEx

// Validate parameters
Preconditions.checkArgument(
config.getCloudSqlInstance() != null,
"cloudSqlInstance property not set. Please specify this property in the JDBC URL or the "
+ "connection Properties with value in form \"project:region:instance\"");
config.getCloudSqlInstance() != null || config.getDomainName() != null,
"cloudSqlInstance property and hostname not set. Please specify either cloudSqlInstance or the database hostname in the JDBC URL or the "
+ "connection Properties. cloudSqlInstance should contain a value in form \"project:region:instance\"");

return getConnector(config).connect(config, connectTimeoutMs);
}
Expand Down Expand Up @@ -332,7 +332,8 @@ private Connector createConnector(ConnectorConfig config) {
localKeyPair,
MIN_REFRESH_DELAY_MS,
connectTimeoutMs,
serverProxyPort);
serverProxyPort,
new DnsInstanceConnectionNameResolver(new JndiDnsResolver()));
}

/** Register the configuration for a named connector. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,15 @@ public LazyRefreshConnectionInfoCache(
ConnectionInfoRepository connectionInfoRepository,
CredentialFactory tokenSourceFactory,
KeyPair keyPair) {

CloudSqlInstanceName instanceName =
new CloudSqlInstanceName(config.getCloudSqlInstance(), config.getDomainName());

this.config = config;
this.instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance());
this.instanceName = instanceName;

AccessTokenSupplier accessTokenSupplier =
DefaultAccessTokenSupplier.newInstance(config.getAuthType(), tokenSourceFactory);
CloudSqlInstanceName instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance());

this.refreshStrategy =
new LazyRefreshStrategy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ public RefreshAheadConnectionInfoCache(
ListenableFuture<KeyPair> keyPair,
long minRefreshDelayMs) {
this.config = config;
this.instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance());
this.instanceName =
new CloudSqlInstanceName(config.getCloudSqlInstance(), config.getDomainName());

AccessTokenSupplier accessTokenSupplier =
DefaultAccessTokenSupplier.newInstance(config.getAuthType(), tokenSourceFactory);
CloudSqlInstanceName instanceName = new CloudSqlInstanceName(config.getCloudSqlInstance());
CloudSqlInstanceName instanceName =
new CloudSqlInstanceName(config.getCloudSqlInstance(), config.getDomainName());

this.refreshStrategy =
new RefreshAheadStrategy(
Expand Down
Loading

0 comments on commit 89c1f22

Please sign in to comment.