diff --git a/.gitignore b/.gitignore index 4927ba059..8b385d6b8 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ target/ **/.project **/.settings/ **/.classpath +.vscode/ # direnv .envrc diff --git a/core/pom.xml b/core/pom.xml index 37fd8f317..893b1b414 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -127,6 +127,11 @@ google-oauth-client + + dnsjava + dnsjava + + diff --git a/core/src/main/java/com/google/cloud/sql/core/DnsJavaResolver.java b/core/src/main/java/com/google/cloud/sql/core/DnsJavaResolver.java new file mode 100644 index 000000000..5402e04bd --- /dev/null +++ b/core/src/main/java/com/google/cloud/sql/core/DnsJavaResolver.java @@ -0,0 +1,109 @@ +/* + * 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.net.UnknownHostException; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.stream.Collectors; +import javax.naming.NameNotFoundException; +import org.xbill.DNS.Lookup; +import org.xbill.DNS.Record; +import org.xbill.DNS.SimpleResolver; +import org.xbill.DNS.TXTRecord; +import org.xbill.DNS.TextParseException; +import org.xbill.DNS.Type; + +/** DnsJavaResolver is a DnsResolver that uses the dnsjava library to perform DNS lookups. */ +public class DnsJavaResolver implements DnsResolver { + + private final SimpleResolver resolver; + + /** Creates a resolver using the system's default DNS settings. */ + public DnsJavaResolver() { + this.resolver = null; // dnsjava's Lookup uses default resolver if null. + } + + /** + * Creates a DNS resolver that uses a specific DNS server. + * + * @param dnsServer the DNS server hostname + * @param port the DNS server port (DNS servers usually use port 53) + */ + public DnsJavaResolver(String dnsServer, int port) { + try { + this.resolver = new SimpleResolver(dnsServer); + this.resolver.setPort(port); + } catch (UnknownHostException e) { + throw new IllegalArgumentException("Unknown DNS server host: " + dnsServer, e); + } + } + + /** + * Returns DNS TXT records for a domain name, sorted alphabetically. + * + * @param domainName the domain name to lookup + * @return the list of records + * @throws NameNotFoundException when the domain name does not resolve. + */ + @Override + public Collection resolveTxt(String domainName) throws NameNotFoundException { + try { + // 1. Create a Lookup object for the TXT record type. + Lookup lookup = new Lookup(domainName, Type.TXT); + + // 2. Set the custom resolver if one was provided in the constructor. + if (this.resolver != null) { + lookup.setResolver(this.resolver); + } + + // 3. Execute the DNS query. + lookup.run(); + + // 4. Check the result of the lookup. + int resultCode = lookup.getResult(); + if (resultCode == Lookup.HOST_NOT_FOUND) { + throw new NameNotFoundException("DNS record not found for " + domainName); + } + if (resultCode == Lookup.TYPE_NOT_FOUND) { + throw new NameNotFoundException("DNS record type TXT not found for " + domainName); + } + if (resultCode != Lookup.SUCCESSFUL) { + throw new RuntimeException( + "DNS lookup failed for " + domainName + ": " + lookup.getErrorString()); + } + + // 5. Process the records, sort them, and return. + Record[] records = lookup.getAnswers(); + if (records == null || records.length == 0) { + return Collections.emptyList(); + } + + // A single TXT record can contain multiple strings, so we use flatMap. + return Arrays.stream(records) + .map(r -> (TXTRecord) r) + .flatMap(txtRecord -> txtRecord.getStrings().stream()) + .sorted() // sort multiple records alphabetically + .collect(Collectors.toList()); + + } catch (TextParseException e) { + // This happens if the domainName is not a valid format. + throw new RuntimeException("Invalid domain name format: " + domainName, e); + } + } +} diff --git a/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java b/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java new file mode 100644 index 000000000..d32bd2eb3 --- /dev/null +++ b/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java @@ -0,0 +1,76 @@ +/* + * 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 static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertThrows; + +import java.util.Collection; +import javax.naming.NameNotFoundException; +import org.junit.Test; + +public class DnsJavaResolverTest { + + private static final String VALID_DOMAIN_NAME = "invalid-san-test.csqlconnectortest.com"; + private static final String MULTI_STRING_RECORD_DOMAIN_NAME = + "test-multi-string-txt-record.csqlconnectortest.com"; + private static final String VALID_DOMAIN_NAME_DATA = + "cloud-sql-connector-testing:us-central1:postgres-customer-cas-test"; + private static final String INVALID_DOMAIN_NAME = "not-a-real-domain.com"; + + @Test + public void testResolveTxt_validDomainName() throws NameNotFoundException { + DnsJavaResolver resolver = new DnsJavaResolver(); + Collection records = resolver.resolveTxt(VALID_DOMAIN_NAME); + assertThat(records).isNotEmpty(); + + // Check that the record contains the expected IP address. + assertThat(records).contains(VALID_DOMAIN_NAME_DATA); + } + + @Test + public void testResolveTxt_multiStringRecord() throws NameNotFoundException { + DnsJavaResolver resolver = new DnsJavaResolver(); + Collection records = resolver.resolveTxt(MULTI_STRING_RECORD_DOMAIN_NAME); + assertThat(records).isNotEmpty(); + assertThat(records).containsExactly("string1", "string2"); + } + + @Test + public void testResolveTxt_notFound() { + DnsJavaResolver resolver = new DnsJavaResolver(); + assertThrows(NameNotFoundException.class, () -> resolver.resolveTxt(INVALID_DOMAIN_NAME)); + } + + @Test + public void testDnsJavaResolver_invalidDnsServer() { + IllegalArgumentException ex = + assertThrows( + IllegalArgumentException.class, () -> new DnsJavaResolver("not-a-real-dns-server", 53)); + assertThat(ex).hasMessageThat().contains("Unknown DNS server host"); + } + + @Test + public void testDnsJavaResolver_validDnsServer() throws NameNotFoundException { + // Use a public DNS server to resolve a real domain. + // Note: "8.8.8.8" is Google's public DNS Server + DnsJavaResolver resolver = new DnsJavaResolver("8.8.8.8", 53); + Collection records = resolver.resolveTxt(VALID_DOMAIN_NAME); + assertThat(records).isNotEmpty(); + assertThat(records).contains(VALID_DOMAIN_NAME_DATA); + } +} diff --git a/pom.xml b/pom.xml index f4ead3ad3..5043054ad 100644 --- a/pom.xml +++ b/pom.xml @@ -197,6 +197,11 @@ jnr-unixsocket 0.38.23 + + dnsjava + dnsjava + 3.2.2 + org.graalvm.sdk nativeimage