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
  • Loading branch information
hessjcg committed Jan 23, 2025
1 parent c389387 commit 3cd89b1
Show file tree
Hide file tree
Showing 9 changed files with 435 additions and 54 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,8 @@ jobs:
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME
POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
Expand All @@ -166,6 +168,8 @@ jobs:
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME }}"
POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME }}"
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
Expand Down Expand Up @@ -249,6 +253,8 @@ jobs:
POSTGRES_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CAS_PASS
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_CONNECTION_NAME
POSTGRES_CUSTOMER_CAS_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS
POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME
POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME
SQLSERVER_CONNECTION_NAME:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_CONNECTION_NAME
SQLSERVER_USER:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_USER
SQLSERVER_PASS:${{ vars.GOOGLE_CLOUD_PROJECT }}/SQLSERVER_PASS
Expand All @@ -274,6 +280,8 @@ jobs:
POSTGRES_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CAS_PASS }}"
POSTGRES_CUSTOMER_CAS_CONNECTION_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_CONNECTION_NAME }}"
POSTGRES_CUSTOMER_CAS_PASS: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS }}"
POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS_VALID_DOMAIN_NAME }}"
POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME: "${{ steps.secrets.outputs.POSTGRES_CUSTOMER_CAS_PASS_INVALID_DOMAIN_NAME }}"
SQLSERVER_CONNECTION_NAME: "${{ steps.secrets.outputs.SQLSERVER_CONNECTION_NAME }}"
SQLSERVER_USER: "${{ steps.secrets.outputs.SQLSERVER_USER }}"
SQLSERVER_PASS: "${{ steps.secrets.outputs.SQLSERVER_PASS }}"
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 @@ -181,17 +185,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,97 @@
/*
* 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) {
if (CloudSqlInstanceName.isValidInstanceName(name)) {
// name contains a well-formed instance name.
return new CloudSqlInstanceName(name);
}

if (CloudSqlInstanceName.isValidDomain(name)) {
// name contains a well-formed domain name.
return resolveDomainName(name);
}

// name is not well-formed, and therefore cannot be resolved.
throw new IllegalArgumentException(
String.format(
"Unable to resolve database instance for \"%s\". It should be a "
+ "well-formed instance name or domain name.",
name));
}

private CloudSqlInstanceName resolveDomainName(String 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 containing the instance name for "
+ "domain name \"%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,20 +104,31 @@ 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())) {
// If the connector is configured using a DNS name, validate the DNS name from the connector
// config.
dns = instanceMetadata.getInstanceName().getDomainName();
} else {
// If the connector is configured with an instance name, validate the DNS name from
// the instance metadata.
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;
}
}
throw new CertificateException(
"Server certificate does not contain expected name '"
+ instanceMetadata.getDnsName()
+ dns
+ "' for Cloud SQL instance "
+ instanceMetadata.getInstanceName());
}
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,11 @@ 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 or hostname was 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 +334,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
Loading

0 comments on commit 3cd89b1

Please sign in to comment.