From e11faa8d35b72b6ca9de2bb1a27b1532ec3497b5 Mon Sep 17 00:00:00 2001 From: Jin Hou Date: Sun, 17 Aug 2025 15:09:57 -0700 Subject: [PATCH 1/3] feat: Add DnsJavaResolver and related test --- .gitignore | 1 + core/pom.xml | 5 + .../cloud/sql/core/DnsJavaResolver.java | 109 ++++++++++++++++++ .../cloud/sql/core/DnsJavaResolverTest.java | 67 +++++++++++ pom.xml | 5 + 5 files changed, 187 insertions(+) create mode 100644 core/src/main/java/com/google/cloud/sql/core/DnsJavaResolver.java create mode 100644 core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java 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..8f15a4cdc --- /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) { + return Collections.emptyList(); + } + 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..2174ed19e --- /dev/null +++ b/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java @@ -0,0 +1,67 @@ +/* + * 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 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_notFound() throws NameNotFoundException { + DnsJavaResolver resolver = new DnsJavaResolver(); + Collection records = resolver.resolveTxt(INVALID_DOMAIN_NAME); + assertThat(records).isEmpty(); + } + + @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 From 800747085c4327f9c6770e0df07e42b8cc5fd8af Mon Sep 17 00:00:00 2001 From: Jin Hou Date: Tue, 26 Aug 2025 09:27:36 -0700 Subject: [PATCH 2/3] add additional unit test for multi-strings TXT record --- .../com/google/cloud/sql/core/DnsJavaResolverTest.java | 10 ++++++++++ 1 file changed, 10 insertions(+) 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 index 2174ed19e..51c3f3c5e 100644 --- a/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java +++ b/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java @@ -26,6 +26,8 @@ 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"; @@ -40,6 +42,14 @@ public void testResolveTxt_validDomainName() throws NameNotFoundException { 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() throws NameNotFoundException { DnsJavaResolver resolver = new DnsJavaResolver(); From c62853538034f2413d46e65b08ebe03ac254a95a Mon Sep 17 00:00:00 2001 From: Jin Hou Date: Tue, 26 Aug 2025 09:32:10 -0700 Subject: [PATCH 3/3] fix unit test to throw NameNotFoundException --- .../main/java/com/google/cloud/sql/core/DnsJavaResolver.java | 2 +- .../java/com/google/cloud/sql/core/DnsJavaResolverTest.java | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) 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 index 8f15a4cdc..5402e04bd 100644 --- a/core/src/main/java/com/google/cloud/sql/core/DnsJavaResolver.java +++ b/core/src/main/java/com/google/cloud/sql/core/DnsJavaResolver.java @@ -81,7 +81,7 @@ public Collection resolveTxt(String domainName) throws NameNotFoundExcep throw new NameNotFoundException("DNS record not found for " + domainName); } if (resultCode == Lookup.TYPE_NOT_FOUND) { - return Collections.emptyList(); + throw new NameNotFoundException("DNS record type TXT not found for " + domainName); } if (resultCode != Lookup.SUCCESSFUL) { throw new RuntimeException( 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 index 51c3f3c5e..d32bd2eb3 100644 --- a/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java +++ b/core/src/test/java/com/google/cloud/sql/core/DnsJavaResolverTest.java @@ -51,10 +51,9 @@ public void testResolveTxt_multiStringRecord() throws NameNotFoundException { } @Test - public void testResolveTxt_notFound() throws NameNotFoundException { + public void testResolveTxt_notFound() { DnsJavaResolver resolver = new DnsJavaResolver(); - Collection records = resolver.resolveTxt(INVALID_DOMAIN_NAME); - assertThat(records).isEmpty(); + assertThrows(NameNotFoundException.class, () -> resolver.resolveTxt(INVALID_DOMAIN_NAME)); } @Test