This is an automated email from the ASF dual-hosted git repository.

vladimirsitnikov pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/jmeter.git


The following commit(s) were added to refs/heads/master by this push:
     new c6c6c07e5c fix(deps): update dependency dnsjava:dnsjava to v3, support 
ip:host for custom resolvers
c6c6c07e5c is described below

commit c6c6c07e5c8def261959c9abda8f64ca839ed392
Author: Mend Renovate <[email protected]>
AuthorDate: Mon Oct 27 17:27:23 2025 +0000

    fix(deps): update dependency dnsjava:dnsjava to v3, support ip:host for 
custom resolvers
---
 gradle/verification-keyring.keys                   |  44 ++++
 gradle/verification-metadata.xml                   |   6 +
 src/bom-thirdparty/build.gradle.kts                |   2 +-
 src/dist/src/dist/expected_release_jars.csv        |   2 +-
 src/licenses/build.gradle.kts                      |   2 +-
 .../protocol/http/control/DNSCacheManager.java     |  94 +++++++-
 .../protocol/http/control/DnsManagerTest.java      |  62 -----
 .../protocol/http/control/DNSCacheManagerTest.kt   | 112 ++++++++-
 .../jmeter/protocol/http/util/MockDnsServer.kt     | 250 +++++++++++++++++++++
 9 files changed, 501 insertions(+), 73 deletions(-)

diff --git a/gradle/verification-keyring.keys b/gradle/verification-keyring.keys
index 72f64f77dd..6aac783a60 100644
--- a/gradle/verification-keyring.keys
+++ b/gradle/verification-keyring.keys
@@ -4120,6 +4120,50 @@ 
x/uXMcOo9YWnMc38gnN+Em4HltkFgKPpM3lPzjN9DqJg5VlBkGUL9Xnm4os=
 =M/EH
 -----END PGP PUBLIC KEY BLOCK-----
 
+pub    0CA7139CBC7026F9
+uid    dnsjava <[email protected]>
+
+sub    6DA2B39D3A4085B5
+-----BEGIN PGP PUBLIC KEY BLOCK-----
+
+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
+PeLQQS4CUQM4f21Ij/FbUFuptPs=
+=1Vdr
+-----END PGP PUBLIC KEY BLOCK-----
+
 pub    0D3B328562A119A7
 uid    Aleksey Shipilev <[email protected]>
 
diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml
index 727aae1b23..06a1536bad 100644
--- a/gradle/verification-metadata.xml
+++ b/gradle/verification-metadata.xml
@@ -259,6 +259,7 @@
             <trusting group="com.github.tomakehurst"/>
             <trusting group="org.wiremock" name="wiremock" version="3.0.1"/>
          </trusted-key>
+         <trusted-key id="E06A36F67D8C1BD13F1F278D0CA7139CBC7026F9" 
group="dnsjava" name="dnsjava" version="3.6.3"/>
          <trusted-key id="E0D98C5FD55A8AF232290E58DEE12B9896F97E34" 
group="org.pcollections"/>
          <trusted-key id="E2ACB037933CDEAAB7BF77D49A2C7A98E457C53D" 
group="org.springframework"/>
          <trusted-key id="E3A9F95079E84CE201F7CF60BEDE11EAF1164480" 
group="org.hamcrest"/>
@@ -615,6 +616,11 @@
             <sha256 
value="958d4f0ee63caee175085f9396d298c2fd88278918bbf3337a32ec74ce488b61" 
origin="Generated by Gradle" reason="Artifact is not signed"/>
          </artifact>
       </component>
+      <component group="org.ow2" name="ow2" version="1.5">
+         <artifact name="ow2-1.5.pom">
+            <sha256 
value="0f8a1b116e760b8fe6389c51b84e4b07a70fc11082d4f936e453b583dd50b43b" 
origin="Generated by Gradle" reason="Artifact is not signed"/>
+         </artifact>
+      </component>
       <component group="org.ow2" name="ow2" version="1.5.1">
          <artifact name="ow2-1.5.1.pom">
             <sha256 
value="321ddbb7ee6fe4f53dea6b4cd6db74154d6bfa42391c1f763b361b9f485acf05" 
origin="Generated by Gradle"/>
diff --git a/src/bom-thirdparty/build.gradle.kts 
b/src/bom-thirdparty/build.gradle.kts
index 6e79280cc7..0ba7064d26 100644
--- a/src/bom-thirdparty/build.gradle.kts
+++ b/src/bom-thirdparty/build.gradle.kts
@@ -68,7 +68,7 @@ dependencies {
         api("commons-lang:commons-lang:2.6")
         api("commons-logging:commons-logging:1.3.5")
         api("commons-net:commons-net:3.12.0")
-        api("dnsjava:dnsjava:2.1.9")
+        api("dnsjava:dnsjava:3.6.3")
         api("io.burt:jmespath-core:0.6.0")
         api("io.burt:jmespath-jackson:0.6.0")
         api("javax.activation:javax.activation-api:1.2.0")
diff --git a/src/dist/src/dist/expected_release_jars.csv 
b/src/dist/src/dist/expected_release_jars.csv
index 137ad6251c..3321bc7501 100644
--- a/src/dist/src/dist/expected_release_jars.csv
+++ b/src/dist/src/dist/expected_release_jars.csv
@@ -49,7 +49,7 @@
 217795,darklaf-windows-2.7.3.jar
 362793,datamodel-jvm-4.7.3.jar
 98115,dec-0.1.2.jar
-320748,dnsjava-2.1.9.jar
+588267,dnsjava-3.6.3.jar
 16829,error_prone_annotations-2.24.0.jar
 1736381,freemarker-2.3.32.jar
 32359,geronimo-jms_1.1_spec-1.1.1.jar
diff --git a/src/licenses/build.gradle.kts b/src/licenses/build.gradle.kts
index e229246786..6a0ef80a10 100644
--- a/src/licenses/build.gradle.kts
+++ b/src/licenses/build.gradle.kts
@@ -104,7 +104,7 @@ val gatherBinaryLicenses by 
tasks.registering(GatherLicenseTask::class) {
     // That enables to have "version-independent" MIT license in 
licenses/slf4j-api, and
     // it would be copied provided the detected license for slf4j-api is MIT.
 
-    overrideLicense("dnsjava:dnsjava:2.1.9") {
+    overrideLicense("dnsjava:dnsjava:3.6.3") {
         expectedLicense = SpdxLicense.BSD_2_Clause
     }
 
diff --git 
a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/DNSCacheManager.java
 
b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/DNSCacheManager.java
index 8b67fa1f76..7af898b28d 100644
--- 
a/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/DNSCacheManager.java
+++ 
b/src/protocol/http/src/main/java/org/apache/jmeter/protocol/http/control/DNSCacheManager.java
@@ -19,7 +19,9 @@ package org.apache.jmeter.protocol.http.control;
 
 import java.io.Serializable;
 import java.net.InetAddress;
+import java.net.InetSocketAddress;
 import java.net.UnknownHostException;
+import java.time.Duration;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.LinkedHashMap;
@@ -46,6 +48,7 @@ import org.xbill.DNS.ExtendedResolver;
 import org.xbill.DNS.Lookup;
 import org.xbill.DNS.Record;
 import org.xbill.DNS.Resolver;
+import org.xbill.DNS.SimpleResolver;
 import org.xbill.DNS.TextParseException;
 import org.xbill.DNS.Type;
 
@@ -121,13 +124,22 @@ public class DNSCacheManager extends ConfigTestElement 
implements TestIterationL
     private Resolver createResolver() {
         CollectionProperty dnsServers = getServers();
         try {
-            String[] serverNames = new String[dnsServers.size()];
-            int index = 0;
+            List<Resolver> resolvers = new ArrayList<>();
             for (JMeterProperty jMeterProperty : dnsServers) {
-                serverNames[index] = jMeterProperty.getStringValue();
-                index++;
+                // it can be either ipv4 or ipv6
+                String hostPort = jMeterProperty.getStringValue();
+                InetSocketAddress address = parseHostPort(hostPort);
+                // dnsjava needs resolved address
+                InetSocketAddress resolvedDnsServer = new 
InetSocketAddress(address.getHostString(), address.getPort());
+                // Check if the address is unresolved (hostname couldn't be 
resolved or invalid IP format)
+                if (resolvedDnsServer.isUnresolved()) {
+                    throw new UnknownHostException("Cannot resolve DNS server 
address: " + hostPort);
+                }
+                SimpleResolver resolver = new 
SimpleResolver(resolvedDnsServer);
+                resolver.setTimeout(ExtendedResolver.DEFAULT_TIMEOUT); // it 
was previously in new ExtendedResolver(String[])
+                resolvers.add(resolver);
             }
-            ExtendedResolver result = new ExtendedResolver(serverNames);
+            ExtendedResolver result = new ExtendedResolver(resolvers);
             if (log.isDebugEnabled()) {
                 log.debug("Using DNS Resolvers: {}", 
Arrays.asList(result.getResolvers()));
             }
@@ -141,6 +153,73 @@ public class DNSCacheManager extends ConfigTestElement 
implements TestIterationL
         }
     }
 
+    /**
+     * Parses a hostPort string into an InetSocketAddress.
+     * Supports formats:
+     * - hostname (e.g., "one.one.one.one")
+     * - IPv4 (e.g., "1.1.1.1")
+     * - IPv6 (e.g., "::1", "2001:db8::1", "ff06:0:0:0:0:0:0:c3")
+     * - hostname:port (e.g., "one.one.one.one:53")
+     * - IPv4:port (e.g., "1.1.1.1:53")
+     * - [IPv6]:port (e.g., "[::1]:53", "[ff06:0:0:0:0:0:0:c3]:53")
+     *
+     * @param hostPort the host and optional port string
+     * @return InetSocketAddress with default port 53 if not specified
+     * @throws UnknownHostException if the format is invalid
+     */
+    @VisibleForTesting
+    static InetSocketAddress parseHostPort(String hostPort) throws 
UnknownHostException {
+        String host;
+        int port = 53; // Default DNS port
+
+        if (hostPort.startsWith("[")) {
+            // IPv6 with optional port: [::1] or [::1]:53
+            int closeBracket = hostPort.lastIndexOf(']');
+            if (closeBracket == -1) {
+                throw new UnknownHostException("Invalid IPv6 address format: " 
+ hostPort);
+            }
+            host = hostPort.substring(1, closeBracket);
+            if (closeBracket + 1 < hostPort.length()) {
+                if (hostPort.charAt(closeBracket + 1) == ':') {
+                    try {
+                        port = 
Integer.parseInt(hostPort.substring(closeBracket + 2));
+                    } catch (NumberFormatException e) {
+                        throw new UnknownHostException("Invalid port in: " + 
hostPort);
+                    }
+                } else {
+                    throw new UnknownHostException("Invalid format after IPv6 
address: " + hostPort);
+                }
+            }
+        } else {
+            // Could be:
+            //   * single colon: hostname:port, IPv4:port
+            //   * 0 or 2+ colons: hostname, IPv4, or bare IPv6
+            // Single colon means
+            int firstColon = hostPort.indexOf(':');
+            int secondColon = firstColon == -1 ? -1 : hostPort.indexOf(':', 
firstColon + 1);
+
+            if (firstColon == -1 || secondColon != -1) {
+                // Zero or 2+ colons indicate bare IPv4, bare hostname, or 
bare IPv6 address
+                // Examples: ::1, 2001:db8::1, ff06:0:0:0:0:0:0:c3
+                host = hostPort;
+            } else {
+                // Single colon: check if it's a port separator
+                int colonPos = hostPort.indexOf(':');
+                String possiblePort = hostPort.substring(colonPos + 1);
+                try {
+                    port = Integer.parseInt(possiblePort);
+                    // It's a valid port, so everything before is the host
+                    host = hostPort.substring(0, colonPos);
+                } catch (NumberFormatException e) {
+                    // Not a port, treat entire string as host
+                    host = hostPort;
+                }
+            }
+        }
+
+        return InetSocketAddress.createUnresolved(host, port);
+    }
+
     /**
      * Resolves address using system or custom DNS resolver
      */
@@ -270,7 +349,7 @@ public class DNSCacheManager extends ConfigTestElement 
implements TestIterationL
             Lookup lookup = new Lookup(host, Type.A);
             lookup.setCache(lookupCache);
             if (timeoutMs > 0) {
-                resolver.setTimeout(timeoutMs / 1000, timeoutMs % 1000);
+                resolver.setTimeout(Duration.ofMillis(timeoutMs));
             }
             lookup.setResolver(resolver);
             Record[] records = lookup.run();
@@ -281,6 +360,9 @@ public class DNSCacheManager extends ConfigTestElement 
implements TestIterationL
             for (int i = 0; i < records.length; i++) {
                 addresses[i] = ((ARecord) records[i]).getAddress();
             }
+        } catch (java.nio.channels.UnresolvedAddressException uae) {
+            // Thrown when DNS server address itself couldn't be resolved
+            throw new UnknownHostException("DNS server address is unresolved: 
" + uae.getMessage());
         } catch (TextParseException tpe) { // NOSONAR Exception handled
             log.debug("Failed to create Lookup object for host:{}, error 
message:{}", host, tpe.toString());
         }
diff --git 
a/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/DnsManagerTest.java
 
b/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/DnsManagerTest.java
deleted file mode 100644
index 1f52c9c9d7..0000000000
--- 
a/src/protocol/http/src/test/java/org/apache/jmeter/protocol/http/control/DnsManagerTest.java
+++ /dev/null
@@ -1,62 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to you 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 org.apache.jmeter.protocol.http.control;
-
-import org.apache.jmeter.protocol.http.sampler.HTTPSampler;
-import org.apache.jmeter.protocol.http.sampler.HTTPSamplerBase;
-import org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory;
-import org.apache.jmeter.protocol.http.sampler.ResultAsString;
-import org.apache.jmeter.samplers.SampleResult;
-import org.apache.jmeter.wiremock.WireMockExtension;
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.Assumptions;
-import org.junit.jupiter.api.extension.ExtendWith;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.MethodSource;
-
-import com.github.tomakehurst.wiremock.WireMockServer;
-
-@ExtendWith(WireMockExtension.class)
-public class DnsManagerTest {
-    @ParameterizedTest
-    
@MethodSource("org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory#getImplementations")
-    void badDnsInCustomResolverShouldFailHttpSampler(String 
httpImplementation, WireMockServer server) {
-        
Assumptions.assumeTrue(!HTTPSamplerFactory.IMPL_JAVA.equals(httpImplementation),
-                "Java implementation does not support custom DNS resolver 
yet");
-        DNSCacheManager dns = new DNSCacheManager();
-        dns.setCustomResolver(true);
-        dns.addServer("20.0.118.11");
-        // By default it uses 3 retries (see 
org.xbill.DNS.ExtendedResolver#setRetries)
-        dns.setTimeoutMs(2000);
-        HTTPSamplerBase http = 
HTTPSamplerFactory.newInstance(httpImplementation);
-        http.setDNSResolver(dns);
-        http.setMethod(HTTPSampler.GET);
-        http.setPort(server.port());
-        http.setDomain("localhost");
-        http.setPath("/index.html");
-
-        http.setRunningVersion(true);
-
-        SampleResult result = http.sample();
-        Assertions.assertEquals(
-                "Non HTTP response message: Failed to resolve host name: 
localhost",
-                result.getResponseMessage(), () ->
-                        "HTTP is using a custom DNS resolver, so it must fail 
resolving localhost \n" +
-                                ResultAsString.toString(result));
-    }
-}
diff --git 
a/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/control/DNSCacheManagerTest.kt
 
b/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/control/DNSCacheManagerTest.kt
index 1c23d84a4a..d56144eb3a 100644
--- 
a/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/control/DNSCacheManagerTest.kt
+++ 
b/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/control/DNSCacheManagerTest.kt
@@ -17,20 +17,37 @@
 
 package org.apache.jmeter.protocol.http.control
 
+import com.github.tomakehurst.wiremock.WireMockServer
+import com.github.tomakehurst.wiremock.client.WireMock.aResponse
+import com.github.tomakehurst.wiremock.client.WireMock.get
+import com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo
+import org.apache.jmeter.protocol.http.sampler.HTTPSampler
+import org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory
+import org.apache.jmeter.protocol.http.sampler.ResultAsString
+import org.apache.jmeter.protocol.http.util.MockDnsServer
+import org.apache.jmeter.wiremock.WireMockExtension
 import org.junit.jupiter.api.Assertions.assertEquals
 import org.junit.jupiter.api.Assertions.assertNotNull
 import org.junit.jupiter.api.Assertions.assertNull
 import org.junit.jupiter.api.Assertions.assertTrue
 import org.junit.jupiter.api.Assumptions.assumeTrue
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.assertAll
 import org.junit.jupiter.api.assertThrows
 import org.junit.jupiter.api.condition.DisabledIfSystemProperty
+import org.junit.jupiter.api.extension.ExtendWith
 import org.junit.jupiter.api.fail
+import org.junit.jupiter.params.ParameterizedTest
+import org.junit.jupiter.params.provider.CsvSource
+import org.junit.jupiter.params.provider.MethodSource
 import org.xbill.DNS.ExtendedResolver
 import org.xbill.DNS.ResolverConfig
+import java.net.Inet4Address
+import java.net.Inet6Address
 import java.net.InetAddress
 import java.net.UnknownHostException
 
+@ExtendWith(WireMockExtension::class)
 class DNSCacheManagerTest {
 
     companion object {
@@ -120,7 +137,7 @@ class DNSCacheManagerTest {
     fun `Valid DNS resolves and caches with custom resolve true`() {
         assumeLocalDnsResolverOK()
         for (dns in VALID_DNS_SERVERS) {
-            sut.addServer(dns)
+            sut.addServer(dns.hostString)
         }
         sut.isCustomResolver = true
         sut.timeoutMs = 5000
@@ -134,7 +151,7 @@ class DNSCacheManagerTest {
     fun `Cache should be used where entries exist`() {
         assumeLocalDnsResolverOK()
         for (dns in VALID_DNS_SERVERS) {
-            sut.addServer(dns)
+            sut.addServer(dns.hostString)
         }
         sut.isCustomResolver = true
         sut.timeoutMs = 5000
@@ -190,4 +207,95 @@ class DNSCacheManagerTest {
         }
         assertNull(sut.resolver, ".resolver")
     }
+
+    @ParameterizedTest
+    
@MethodSource("org.apache.jmeter.protocol.http.sampler.HTTPSamplerFactory#getImplementations")
+    fun `custom resolver should use mock DNS server to resolve host`(
+        httpImplementation: String,
+        server: WireMockServer
+    ) {
+        assumeTrue(
+            httpImplementation != HTTPSamplerFactory.IMPL_JAVA,
+            "Java implementation does not support custom DNS resolver yet"
+        )
+
+        // Set up WireMock to respond to requests
+        server.stubFor(
+            get(urlEqualTo("/index.html"))
+                .willReturn(
+                    aResponse()
+                        .withStatus(200)
+                        .withBody("OK")
+                )
+        )
+
+        val testDomainName = "non.existing.domain.for.tests"
+        // Resolve
+        val mockDnsServer = MockDnsServer(
+            answers = mapOf(
+                // This should map to WireMock listen address, and it is not 
clear
+                // how to tell if WireMock listens on IPv4 or IPv6
+                testDomainName to listOf("127.0.0.1")
+            )
+        )
+
+        mockDnsServer.start()
+        try {
+            // Use a custom DNS resolver with a mock DNS server
+            val dns = DNSCacheManager().apply {
+                isCustomResolver = true
+                addServer(
+                    when (mockDnsServer.localAddress) {
+                        is Inet4Address -> "127.0.0.1"
+                        is Inet6Address -> "[::1]"
+                        else -> TODO("Unexpected address type of 
mockDnsServer.localAddress: ${mockDnsServer.localAddress::class.simpleName}")
+                    } + ":" + mockDnsServer.boundPort
+                )
+            }
+
+            val http = 
HTTPSamplerFactory.newInstance(httpImplementation).apply {
+                dnsResolver = dns
+                method = HTTPSampler.GET
+                port = server.port()
+                domain = testDomainName
+                path = "/index.html"
+                isRunningVersion = true
+            }
+
+            val result = http.sample()
+
+            assertTrue(result.isSuccessful) {
+                "HTTP request should succeed using custom DNS resolver with 
mock DNS server. " +
+                    "Response: ${result.responseMessage}\n" +
+                    ResultAsString.toString(result)
+            }
+
+            assertEquals("200", result.responseCode) {
+                "Expected 200 response 
code\n${ResultAsString.toString(result)}"
+            }
+        } finally {
+            mockDnsServer.close()
+        }
+    }
+
+    @ParameterizedTest
+    @CsvSource(
+        "one.one.one.one, one.one.one.one, 53",
+        "1.1.1.1, 1.1.1.1, 53",
+        "::1, ::1, 53",
+        "one.one.one.one:8053, one.one.one.one, 8053",
+        "1.1.1.1:53, 1.1.1.1, 53",
+        "[::1]:53, ::1, 53",
+        "127.0.0.1:53, 127.0.0.1, 53",
+        "ff06:0:0:0:0:0:0:c3, ff06:0:0:0:0:0:0:c3, 53",
+        "2001:db8:85a3:0:0:8a2e:370:7334, 2001:db8:85a3:0:0:8a2e:370:7334, 53",
+        "[ff06:0:0:0:0:0:0:c3]:8053, ff06:0:0:0:0:0:0:c3, 8053"
+    )
+    fun parseHostPort(input: String, expectedHost: String, expectedPort: Int) {
+        val addr = DNSCacheManager.parseHostPort(input)
+        assertAll(
+            { assertEquals(expectedHost, addr.hostString) { "host from $input" 
} },
+            { assertEquals(expectedPort, addr.port) { "port from $input" } }
+        )
+    }
 }
diff --git 
a/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/util/MockDnsServer.kt
 
b/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/util/MockDnsServer.kt
new file mode 100644
index 0000000000..bc4cacb46d
--- /dev/null
+++ 
b/src/protocol/http/src/test/kotlin/org/apache/jmeter/protocol/http/util/MockDnsServer.kt
@@ -0,0 +1,250 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you 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 org.apache.jmeter.protocol.http.util
+
+import org.slf4j.LoggerFactory
+import org.xbill.DNS.AAAARecord
+import org.xbill.DNS.ARecord
+import org.xbill.DNS.Message
+import org.xbill.DNS.Name
+import org.xbill.DNS.Rcode
+import org.xbill.DNS.Record
+import org.xbill.DNS.Section
+import org.xbill.DNS.Type
+import java.io.Closeable
+import java.net.DatagramPacket
+import java.net.DatagramSocket
+import java.net.InetAddress
+import java.net.InetSocketAddress
+import java.time.Duration
+import java.util.concurrent.ConcurrentLinkedQueue
+import java.util.concurrent.ExecutorService
+import java.util.concurrent.Executors
+import java.util.concurrent.TimeUnit
+import java.util.concurrent.atomic.AtomicBoolean
+import java.util.concurrent.atomic.AtomicInteger
+
+/**
+ * A lightweight mock DNS server for testing purposes.
+ *
+ * Supports A and AAAA record queries with configurable responses. Useful for 
testing
+ * DNS resolution behavior without requiring external DNS infrastructure.
+ *
+ * Example usage:
+ * ```
+ * val server = MockDnsServer(
+ *     answers = mapOf("example.com" to listOf("192.0.2.1", "2001:db8::1"))
+ * )
+ * server.start()
+ * // ... perform tests using server.boundPort() ...
+ * server.close()
+ * ```
+ *
+ * @param port The port to bind to (0 = auto-assign)
+ * @param answers Static hostname-to-IP mappings (hostname without trailing 
dot)
+ * @param ttl Time-to-live for DNS records
+ * @param soTimeout Socket timeout for graceful shutdown checks
+ * @param answerProvider Custom record provider for dynamic responses 
(overrides [answers])
+ */
+class MockDnsServer(
+    private val port: Int = 0,
+    private val answers: Map<String, List<String>> = emptyMap(),
+    private val ttl: Duration = Duration.ofSeconds(60),
+    private val soTimeout: Duration = Duration.ofMillis(250),
+    private val answerProvider: ((Name, Int, Int) -> List<Record>)? = null
+) : Closeable {
+    companion object {
+        private val logger = LoggerFactory.getLogger(MockDnsServer::class.java)
+        private const val UDP_SIZE = 512
+    }
+
+    private val running = AtomicBoolean(false)
+    private val requests = AtomicInteger(0)
+    private val receivedQueries = ConcurrentLinkedQueue<QueryInfo>()
+
+    /**
+     * Returns the number of DNS queries received by this server.
+     */
+    val requestCount: Int get() = requests.get()
+
+    /**
+     * Returns a list of all DNS queries received by this server.
+     * Useful for test assertions.
+     */
+    fun getReceivedQueries(): List<QueryInfo> = receivedQueries.toList()
+
+    /**
+     * Clears the history of received queries.
+     */
+    fun clearQueryHistory() = receivedQueries.clear()
+
+    /**
+     * Information about a received DNS query.
+     */
+    data class QueryInfo(
+        val name: String,
+        val type: Int,
+        val typeName: String,
+        val dclass: Int
+    )
+
+    // single-thread executor for serving; daemon thread to not hang JVM
+    private val executor: ExecutorService = Executors.newSingleThreadExecutor 
{ r ->
+        Thread(r, "mock-dns-server-$port").apply { isDaemon = true }
+    }
+
+    @Volatile private var socket: DatagramSocket? = null
+
+    var boundPort: Int = -1
+        private set
+
+    val localAddress: InetAddress get() = socket!!.localAddress
+
+    /**
+     * Starts the DNS server on the configured port.
+     *
+     * @return The port the server is bound to
+     * @throws IllegalStateException if port is invalid
+     * @throws java.net.BindException if port is already in use
+     */
+    fun start(): Int {
+        if (!running.compareAndSet(false, true)) return boundPort // idempotent
+
+        require(port in 0..65535) { "Port must be in range 0-65535, got: 
$port" }
+
+        try {
+            val ds = DatagramSocket(port)
+            ds.soTimeout = soTimeout.toMillis().toInt()
+            socket = ds
+            boundPort = (ds.localSocketAddress as InetSocketAddress).port
+            logger.debug("MockDnsServer started on port $boundPort")
+            executor.execute { serve(ds) }
+            return boundPort
+        } catch (e: Exception) {
+            running.set(false)
+            throw e
+        }
+    }
+
+    private fun serve(ds: DatagramSocket) {
+        val buf = ByteArray(UDP_SIZE)
+        while (running.get()) {
+            try {
+                val packet = DatagramPacket(buf, buf.size)
+                ds.receive(packet) // times out periodically due to soTimeout
+                requests.incrementAndGet()
+                val request = Message(packet.data)
+                val response = buildResponse(request)
+                val wire = response.toWire()
+                val out = DatagramPacket(wire, wire.size, packet.address, 
packet.port)
+                ds.send(out)
+            } catch (e: java.net.SocketTimeoutException) {
+                // loop again, check running flag
+            } catch (e: Exception) {
+                if (running.get()) {
+                    logger.debug("MockDnsServer error while serving", e)
+                } else {
+                    break
+                }
+            }
+        }
+    }
+
+    private fun buildResponse(req: Message): Message {
+        val q = req.getQuestion()
+
+        // Record query for test inspection
+        receivedQueries.offer(
+            QueryInfo(
+                name = q.name.toString(true).trimEnd('.'),
+                type = q.type,
+                typeName = Type.string(q.type),
+                dclass = q.dClass
+            )
+        )
+
+        val resp = Message(req.header.id)
+        resp.addRecord(q, Section.QUESTION)
+        resp.header.rcode = Rcode.NOERROR
+        val records = provideRecords(q.name, q.type, q.dClass)
+        if (records.isEmpty()) {
+            resp.header.rcode = Rcode.NXDOMAIN
+        } else {
+            records.forEach { resp.addRecord(it, Section.ANSWER) }
+        }
+        return resp
+    }
+
+    private fun provideRecords(name: Name, type: Int, dclass: Int): 
List<Record> {
+        answerProvider?.let { return it(name, type, dclass) }
+        val key = name.toString(true).trimEnd('.') // canonical hostname key
+        val ips = answers[key] ?: return emptyList()
+        val ttl = this.ttl.toSeconds()
+        return ips.mapNotNull { ip ->
+            try {
+                val addr = InetAddress.getByName(ip)
+                val recordType = if (addr.address.size == 4) Type.A else 
Type.AAAA
+
+                // Return record only if query type matches or is ANY
+                when {
+                    type == Type.ANY -> when (recordType) {
+                        Type.A -> ARecord(name, dclass, ttl, addr)
+                        Type.AAAA -> AAAARecord(name, dclass, ttl, addr)
+                        else -> null
+                    }
+                    type == recordType -> when (recordType) {
+                        Type.A -> ARecord(name, dclass, ttl, addr)
+                        Type.AAAA -> AAAARecord(name, dclass, ttl, addr)
+                        else -> null
+                    }
+                    else -> null // query type doesn't match this address type
+                }
+            } catch (e: Exception) {
+                logger.debug("Failed to parse IP address: $ip", e)
+                null
+            }
+        }
+    }
+
+    override fun close() {
+        stop()
+    }
+
+    /**
+     * Stops the DNS server gracefully.
+     * This method is idempotent and can be called multiple times safely.
+     */
+    fun stop() {
+        if (!running.compareAndSet(true, false)) return // idempotent
+        logger.debug("Stopping MockDnsServer on port $boundPort")
+        try {
+            socket?.close()
+        } catch (e: Exception) {
+            logger.debug("Error closing socket", e)
+        }
+        socket = null
+        executor.shutdownNow()
+        try {
+            if (!executor.awaitTermination(1, TimeUnit.SECONDS)) {
+                logger.warn("MockDnsServer executor did not terminate in time")
+            }
+        } catch (e: InterruptedException) {
+            Thread.currentThread().interrupt()
+        }
+    }
+}

Reply via email to