From 75504a135a875c2a309ca0695fe8b0baab37ebee Mon Sep 17 00:00:00 2001 From: Andreas Wolf Date: Mon, 18 May 2026 10:35:07 +0200 Subject: [PATCH] Issue 5676: Introduced useTcpForFallbackDnsResolving Issue 5676: Added test Issue 5676: Reworked test Issue 5676: CR Remarks Issue 5676: CR Remarks Issue 5676: CR Remarks --- .../dns/AddressResolverOptionsConverter.java | 6 + .../core/dns/AddressResolverOptions.java | 26 ++++ .../dns/impl/DnsAddressResolverProvider.java | 3 +- .../io/vertx/it/networklogging/DnsTest.java | 2 +- .../io/vertx/test/fakedns/MockDnsServer.java | 138 +++++++++++++----- .../test/java/io/vertx/tests/dns/DNSTest.java | 2 +- .../io/vertx/tests/dns/NameResolverTest.java | 51 ++++++- .../io/vertx/tests/net/ProxyErrorTest.java | 2 +- .../tests/vertx/VertxStartFailureTest.java | 2 +- 9 files changed, 184 insertions(+), 48 deletions(-) diff --git a/vertx-core/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java b/vertx-core/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java index d397c670732..4d945062ba5 100644 --- a/vertx-core/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java +++ b/vertx-core/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java @@ -102,6 +102,11 @@ static void fromJson(Iterable> json, Address obj.setRoundRobinInetAddress((Boolean)member.getValue()); } break; + case "useTcpForFallbackDnsResolving": + if (member.getValue() instanceof Boolean) { + obj.setUseTcpForFallbackDnsResolving((Boolean)member.getValue()); + } + break; } } } @@ -141,5 +146,6 @@ static void toJson(AddressResolverOptions obj, java.util.Map jso json.put("ndots", obj.getNdots()); json.put("rotateServers", obj.isRotateServers()); json.put("roundRobinInetAddress", obj.isRoundRobinInetAddress()); + json.put("useTcpForFallbackDnsResolving", obj.isUseTcpForFallbackDnsResolving()); } } diff --git a/vertx-core/src/main/java/io/vertx/core/dns/AddressResolverOptions.java b/vertx-core/src/main/java/io/vertx/core/dns/AddressResolverOptions.java index fa5c2a2534d..da7747d14c7 100644 --- a/vertx-core/src/main/java/io/vertx/core/dns/AddressResolverOptions.java +++ b/vertx-core/src/main/java/io/vertx/core/dns/AddressResolverOptions.java @@ -116,6 +116,11 @@ public class AddressResolverOptions { */ public static final boolean DEFAULT_ROUND_ROBIN_INET_ADDRESS = false; + /** + * The default whether to use TCP for DNS resolving if resolving via UDP times out = false + */ + public static final boolean DEFAULT_USE_TCP_FOR_FALLBACK_DNS_RESOLVING = false; + private String hostsPath; private Buffer hostsValue; private TimeUnit hostsRefreshPeriodUnit; @@ -132,6 +137,7 @@ public class AddressResolverOptions { private int ndots; private boolean rotateServers; private boolean roundRobinInetAddress; + private boolean useTcpForFallbackDnsResolving; public AddressResolverOptions() { servers = DEFAULT_SERVERS; @@ -148,6 +154,7 @@ public AddressResolverOptions() { roundRobinInetAddress = DEFAULT_ROUND_ROBIN_INET_ADDRESS; hostsRefreshPeriodUnit = DEFAULT_HOSTS_REFRESH_PERIOD_UNIT; hostsRefreshPeriod = DEFAULT_HOSTS_REFRESH_PERIOD; + useTcpForFallbackDnsResolving = DEFAULT_USE_TCP_FOR_FALLBACK_DNS_RESOLVING; } public AddressResolverOptions(AddressResolverOptions other) { @@ -167,6 +174,7 @@ public AddressResolverOptions(AddressResolverOptions other) { this.ndots = other.ndots; this.rotateServers = other.rotateServers; this.roundRobinInetAddress = other.roundRobinInetAddress; + this.useTcpForFallbackDnsResolving = other.useTcpForFallbackDnsResolving; } public AddressResolverOptions(JsonObject json) { @@ -530,6 +538,24 @@ public AddressResolverOptions setRoundRobinInetAddress(boolean roundRobinInetAdd return this; } + /** + * @return the value {@code true} when using TCP for DNS resolving as a fallback is enabled when resolving via + * UDP times out. + */ + public boolean isUseTcpForFallbackDnsResolving() { + return useTcpForFallbackDnsResolving; + } + + /** + * Set to {@code true} to enable using TCP as a fallback for DNS resolving when using UDP times out. + * + * @return a reference to this, so the API can be used fluently + */ + public AddressResolverOptions setUseTcpForFallbackDnsResolving(boolean useTcpForFallbackDnsResolving) { + this.useTcpForFallbackDnsResolving = useTcpForFallbackDnsResolving; + return this; + } + public JsonObject toJson() { JsonObject json = new JsonObject(); AddressResolverOptionsConverter.toJson(this, json); diff --git a/vertx-core/src/main/java/io/vertx/core/dns/impl/DnsAddressResolverProvider.java b/vertx-core/src/main/java/io/vertx/core/dns/impl/DnsAddressResolverProvider.java index 3f4dfa78067..bc187fb83dd 100644 --- a/vertx-core/src/main/java/io/vertx/core/dns/impl/DnsAddressResolverProvider.java +++ b/vertx-core/src/main/java/io/vertx/core/dns/impl/DnsAddressResolverProvider.java @@ -107,7 +107,8 @@ private DnsAddressResolverProvider(VertxInternal vertx, AddressResolverOptions o DnsNameResolverBuilder builder = new DnsNameResolverBuilder(); builder.hostsFileEntriesResolver(this); builder.datagramChannelFactory(vertx.transport().datagramChannelFactory()); - builder.socketChannelFactory(() -> (SocketChannel) vertx.transport().channelFactory(false).newChannel()); + builder.socketChannelFactory(() -> (SocketChannel) vertx.transport().channelFactory(false).newChannel(), + options.isUseTcpForFallbackDnsResolving()); builder.nameServerProvider(nameServerAddressProvider); builder.queryServerAddressStream(new ThreadLocalNameServerAddressStream(nameServerAddressProvider, "")); builder.optResourceEnabled(options.isOptResourceEnabled()); diff --git a/vertx-core/src/test/java/io/vertx/it/networklogging/DnsTest.java b/vertx-core/src/test/java/io/vertx/it/networklogging/DnsTest.java index b0f3a5f960e..87677f5b76e 100644 --- a/vertx-core/src/test/java/io/vertx/it/networklogging/DnsTest.java +++ b/vertx-core/src/test/java/io/vertx/it/networklogging/DnsTest.java @@ -21,7 +21,7 @@ public class DnsTest extends VertxTestBase { @Test public void testDoNotLogActivity() throws Exception { - MockDnsServer mockDnsServer = new MockDnsServer(vertx); + MockDnsServer mockDnsServer = new MockDnsServer(); mockDnsServer.start(); try { DnsClient client = vertx.createDnsClient(new DnsClientOptions(new DnsClientOptions().setLogActivity(false)) diff --git a/vertx-core/src/test/java/io/vertx/test/fakedns/MockDnsServer.java b/vertx-core/src/test/java/io/vertx/test/fakedns/MockDnsServer.java index 9c69c14a690..797db8f677d 100644 --- a/vertx-core/src/test/java/io/vertx/test/fakedns/MockDnsServer.java +++ b/vertx-core/src/test/java/io/vertx/test/fakedns/MockDnsServer.java @@ -11,17 +11,18 @@ package io.vertx.test.fakedns; import io.netty.bootstrap.Bootstrap; +import io.netty.bootstrap.ServerBootstrap; import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; import io.netty.channel.*; import io.netty.channel.nio.NioIoHandler; import io.netty.channel.socket.DatagramChannel; +import io.netty.channel.socket.SocketChannel; import io.netty.channel.socket.nio.NioDatagramChannel; +import io.netty.channel.socket.nio.NioServerSocketChannel; import io.netty.handler.codec.dns.*; import io.netty.handler.codec.dns.DnsRecord; import io.netty.util.internal.PlatformDependent; -import io.vertx.core.Vertx; -import io.vertx.core.internal.VertxInternal; import java.net.InetAddress; import java.net.InetSocketAddress; @@ -42,21 +43,15 @@ public class MockDnsServer { private String ipAddress = IP_ADDRESS; private int port = PORT; - private EventLoopGroup eventLoop; - private ChannelFactory channelFactory; - private Channel channel; + private final EventLoopGroup eventLoop = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); + private final ChannelFactory channelFactory = NioDatagramChannel::new; + private Channel udpChannel; + private Channel tcpChannel; private RecordStore store; - private List failures = new CopyOnWriteArrayList<>(); + private final List failures = new CopyOnWriteArrayList<>(); private final Deque currentMessage = new ArrayDeque<>(); - - public MockDnsServer(Vertx vertx) { - this.eventLoop = ((VertxInternal) vertx).nettyEventLoopGroup().next(); - this.channelFactory = ((VertxInternal) vertx).transport().datagramChannelFactory(); - } - - public MockDnsServer() { - this.eventLoop = null; - } + private boolean enableTcp = false; + private boolean enableUdp = true; public MockDnsServer store(RecordStore store) { this.store = store; @@ -68,7 +63,12 @@ public RecordStore store() { } public InetSocketAddress localAddress() { - return (InetSocketAddress) channel.localAddress(); + if (udpChannel != null) { + return (InetSocketAddress) udpChannel.localAddress(); + } if (tcpChannel != null) { + return (InetSocketAddress) tcpChannel.localAddress(); + } + throw new IllegalStateException("At least one of the channels must be started"); } public MockDnsServer ipAddress(String ipAddress) { @@ -81,28 +81,41 @@ public MockDnsServer port(int p) { return this; } + public MockDnsServer enableTcp(boolean enableTcp) { + this.enableTcp = enableTcp; + return this; + } + + public MockDnsServer enableUdp(boolean enableUdp) { + this.enableUdp = enableUdp; + return this; + } + public void start() { - if (channel != null) { + if (udpChannel != null || tcpChannel != null) { throw new IllegalStateException(); } - EventLoopGroup eventLoop = this.eventLoop; - ChannelFactory channelFactory = this.channelFactory; - if (eventLoop == null) { - eventLoop = new MultiThreadIoEventLoopGroup(1, NioIoHandler.newFactory()); - channelFactory = NioDatagramChannel::new; + if (enableUdp) { + startUdp(); } + if (enableTcp) { + startTcp(); + } + } + + private void startUdp() { Bootstrap bootstrap = new Bootstrap() .group(eventLoop) .channelFactory(channelFactory) .handler(new ChannelInitializer() { @Override - protected void initChannel(DatagramChannel ch) throws Exception { + protected void initChannel(DatagramChannel ch) { ChannelPipeline p = ch.pipeline(); p.addLast(new DatagramDnsQueryDecoder()) .addLast(new DatagramDnsResponseEncoder()) .addLast(new SimpleChannelInboundHandler() { @Override - protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery msg) throws UnknownHostException { + protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery msg) { DnsRecord dnsRecord = msg.recordAt(DnsSection.QUESTION); synchronized (MockDnsServer.this) { currentMessage.add(msg); @@ -140,33 +153,78 @@ protected void channelRead0(ChannelHandlerContext ctx, DatagramDnsQuery msg) thr PlatformDependent.throwException(e); return; } finally { - if (!started && this.eventLoop == null) { + if (!started) { eventLoop.shutdownGracefully(0, 10, TimeUnit.SECONDS); } } if (fut.isSuccess()) { - channel = fut.channel(); + udpChannel = fut.channel(); } else { - if (this.eventLoop == null) { - eventLoop.shutdownGracefully(0, 10, TimeUnit.SECONDS); - } PlatformDependent.throwException(fut.cause()); } } + private void startTcp() { + ServerBootstrap tcpBootstrap = new ServerBootstrap() + .group(eventLoop) + .channel(NioServerSocketChannel.class) + .childHandler(new ChannelInitializer() { + @Override + protected void initChannel(SocketChannel ch) { + ChannelPipeline p = ch.pipeline(); + p.addLast(new TcpDnsQueryDecoder()) + .addLast(new TcpDnsResponseEncoder()) + .addLast(new SimpleChannelInboundHandler() { + @Override + protected void channelRead0(ChannelHandlerContext ctx, DnsQuery msg) { + DnsRecord dnsRecord = msg.recordAt(DnsSection.QUESTION); + synchronized (MockDnsServer.this) { + currentMessage.add(msg); + } + + DefaultDnsResponse response = new DefaultDnsResponse(msg.id(), DnsOpCode.QUERY); + response.addRecord(DnsSection.QUESTION, dnsRecord); + RecordStore s = store; + if (s != null) { + Collection records; + try { + records = s.getRecords((DnsQuestion) dnsRecord); + } catch (Throwable failure) { + failures.add(failure); + return; + } + if (records != null) { + for (DnsRecord record : records) { + response.addRecord(DnsSection.ANSWER, record); + } + } + } + ctx.writeAndFlush(response); + } + }); + } + }); + try { + ChannelFuture tcpFut = tcpBootstrap.bind(ipAddress, port).sync(); + tcpChannel = tcpFut.channel(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + PlatformDependent.throwException(e); + } + } + public List stop() throws Exception { - Channel channel = this.channel; - this.channel = null; + Channel tc = this.tcpChannel; + this.tcpChannel = null; + Channel channel = this.udpChannel; + this.udpChannel = null; + if (channel != null) { - try { - channel.close().sync(); - } finally { - if (this.eventLoop == null) { - channel - .eventLoop() - .shutdownGracefully(0, 10, TimeUnit.SECONDS); - } - } + channel.eventLoop().parent().shutdownGracefully(0, 10, TimeUnit.SECONDS); + channel.close().sync(); + } else if (tc != null) { + tc.eventLoop().parent().shutdownGracefully(0, 10, TimeUnit.SECONDS); + tc.close().sync(); } return failures; } diff --git a/vertx-core/src/test/java/io/vertx/tests/dns/DNSTest.java b/vertx-core/src/test/java/io/vertx/tests/dns/DNSTest.java index 821f190a334..d2637f9f3d5 100644 --- a/vertx-core/src/test/java/io/vertx/tests/dns/DNSTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/dns/DNSTest.java @@ -53,7 +53,7 @@ public DNSTest() { @Override public void setUp() throws Exception { super.setUp(); - mockDnsServer = new MockDnsServer(vertx); + mockDnsServer = new MockDnsServer(); mockDnsServer.start(); } diff --git a/vertx-core/src/test/java/io/vertx/tests/dns/NameResolverTest.java b/vertx-core/src/test/java/io/vertx/tests/dns/NameResolverTest.java index 54a8b82c157..3da6cf41e42 100644 --- a/vertx-core/src/test/java/io/vertx/tests/dns/NameResolverTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/dns/NameResolverTest.java @@ -80,7 +80,7 @@ private Future resolve(Vertx vertx, String host) { private AddressResolverOptions getAddressResolverOptions() { if (dnsServer == null) { - dnsServer = new MockDnsServer(vertx()).testResolveASameServer("127.0.0.1"); + dnsServer = new MockDnsServer().testResolveASameServer("127.0.0.1"); dnsServer.start(); dnsServerAddress = dnsServer.localAddress(); } @@ -196,6 +196,7 @@ public void testOptions() { assertEquals(AddressResolverOptions.DEFAULT_RD_FLAG, options.getRdFlag()); assertEquals(AddressResolverOptions.DEFAULT_NDOTS, options.getNdots()); assertEquals(AddressResolverOptions.DEFAULT_SEARCH_DOMAINS, options.getSearchDomains()); + assertEquals(AddressResolverOptions.DEFAULT_USE_TCP_FOR_FALLBACK_DNS_RESOLVING, options.isUseTcpForFallbackDnsResolving()); boolean optResourceEnabled = TestUtils.randomBoolean(); List servers = Arrays.asList("1.2.3.4", "5.6.7.8"); @@ -210,11 +211,13 @@ public void testOptions() { for (int i = 0; i < 2; i++) { searchDomains.add(TestUtils.randomAlphaString(15)); } + boolean useTcpForFallbackDnsResolving = true; assertSame(options, options.setOptResourceEnabled(optResourceEnabled)); assertSame(options, options.setServers(new ArrayList<>(servers))); assertSame(options, options.setCacheMinTimeToLive(0)); assertSame(options, options.setCacheMinTimeToLive(minTTL)); + assertSame(options, options.setUseTcpForFallbackDnsResolving(useTcpForFallbackDnsResolving)); try { options.setCacheMinTimeToLive(-1); fail("Should throw exception"); @@ -308,6 +311,7 @@ public void testOptions() { assertEquals(rdFlag, jsonCopy.getRdFlag()); assertEquals(ndots, jsonCopy.getNdots()); assertEquals(searchDomains, jsonCopy.getSearchDomains()); + assertEquals(useTcpForFallbackDnsResolving, jsonCopy.isUseTcpForFallbackDnsResolving()); } @Test @@ -323,6 +327,7 @@ public void testDefaultJsonOptions() { assertEquals(AddressResolverOptions.DEFAULT_RD_FLAG, options.getRdFlag()); assertEquals(AddressResolverOptions.DEFAULT_SEARCH_DOMAINS, options.getSearchDomains()); assertEquals(AddressResolverOptions.DEFAULT_NDOTS, options.getNdots()); + assertEquals(AddressResolverOptions.DEFAULT_USE_TCP_FOR_FALLBACK_DNS_RESOLVING, options.isUseTcpForFallbackDnsResolving()); } @Test @@ -900,7 +905,7 @@ private void testServerSelection(boolean rotateServers, boolean cache) throws Ex List dnsServers = new ArrayList<>(); try { for (int index = 1; index <= num; index++) { - MockDnsServer server = new MockDnsServer(vertx).store(MockDnsServer.A_store(Collections.singletonMap("vertx.io", "127.0.0." + index))); + MockDnsServer server = new MockDnsServer().store(MockDnsServer.A_store(Collections.singletonMap("vertx.io", "127.0.0." + index))); server.port(MockDnsServer.PORT + index); server.start(); dnsServers.add(server); @@ -976,7 +981,7 @@ private void testAddressSelection(AddressResolverOptions options, int expected) @Test public void testServerFailover() throws Exception { - MockDnsServer server = new MockDnsServer(vertx).store(MockDnsServer.A_store(Collections.singletonMap("vertx.io", "127.0.0.1"))).port(MockDnsServer.PORT + 2); + MockDnsServer server = new MockDnsServer().store(MockDnsServer.A_store(Collections.singletonMap("vertx.io", "127.0.0.1"))).port(MockDnsServer.PORT + 2); try { AddressResolverOptions options = new AddressResolverOptions(); options.setOptResourceEnabled(false); @@ -1002,4 +1007,44 @@ public void testServerFailover() throws Exception { server.stop(); } } + + @Test + public void testTcpFallbackDnsResolving() throws Exception { + MockDnsServer tcpDnsServer = new MockDnsServer() + .port(MockDnsServer.PORT - 10) + .testResolveA("127.0.0.1") + .enableTcp(true) + .enableUdp(false); + tcpDnsServer.start(); + try { + InetSocketAddress addr = tcpDnsServer.localAddress(); + String server = addr.getAddress().getHostAddress() + ":" + addr.getPort(); + + // With TCP fallback enabled, resolution should succeed despite UDP timeout + AddressResolverOptions tcpOptions = new AddressResolverOptions() + .addServer(server) + .setOptResourceEnabled(false) + .setQueryTimeout(1000) + .setUseTcpForFallbackDnsResolving(true); + NameResolver tcpResolver = new NameResolver(vertx, tcpOptions); + InetAddress resolved = tcpResolver.resolve("vertx.io").await(2, TimeUnit.SECONDS); + assertEquals("127.0.0.1", resolved.getHostAddress()); + + // With TCP fallback disabled, resolution should fail since UDP times out + AddressResolverOptions udpOnlyOptions = new AddressResolverOptions() + .addServer(server) + .setOptResourceEnabled(false) + .setQueryTimeout(1000) + .setUseTcpForFallbackDnsResolving(false); + NameResolver udpResolver = new NameResolver(vertx, udpOnlyOptions); + try { + udpResolver.resolve("vertx.io").await(2, TimeUnit.SECONDS); + fail("Should have failed without TCP fallback"); + } catch (Exception e) { + assertTrue(e instanceof UnknownHostException); + } + } finally { + tcpDnsServer.stop(); + } + } } diff --git a/vertx-core/src/test/java/io/vertx/tests/net/ProxyErrorTest.java b/vertx-core/src/test/java/io/vertx/tests/net/ProxyErrorTest.java index add0c4ce68f..7b40561be8d 100644 --- a/vertx-core/src/test/java/io/vertx/tests/net/ProxyErrorTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/net/ProxyErrorTest.java @@ -55,7 +55,7 @@ protected void tearDown() throws Exception { @Override protected VertxOptions getOptions() { if (dnsServer == null) { - dnsServer = new MockDnsServer(vertx()).testLookupNonExisting(); + dnsServer = new MockDnsServer().testLookupNonExisting(); dnsServer.start(); dnsServerAddress = dnsServer.localAddress(); } diff --git a/vertx-core/src/test/java/io/vertx/tests/vertx/VertxStartFailureTest.java b/vertx-core/src/test/java/io/vertx/tests/vertx/VertxStartFailureTest.java index b9a4a2fb6c6..4531ca1d354 100644 --- a/vertx-core/src/test/java/io/vertx/tests/vertx/VertxStartFailureTest.java +++ b/vertx-core/src/test/java/io/vertx/tests/vertx/VertxStartFailureTest.java @@ -40,7 +40,7 @@ public class VertxStartFailureTest extends AsyncTestBase { @Test public void testEventBusStartFailure() throws Exception { Vertx vertx = Vertx.vertx(); - MockDnsServer dnsServer = new MockDnsServer(vertx).testResolveASameServer("127.0.0.1"); + MockDnsServer dnsServer = new MockDnsServer().testResolveASameServer("127.0.0.1"); dnsServer.start(); try { InetSocketAddress dnsServerAddress = dnsServer.localAddress();