From dd5c0786d748520006d65e91a9f00e54b44cc417 Mon Sep 17 00:00:00 2001 From: Julien Viet Date: Fri, 8 Sep 2023 09:39:30 +0200 Subject: [PATCH] The Vert.x DNS address resolver can load an hosts configuration file and cache the content forever. Sometimes the content of the file can change and the cached content becomes stale. Add a new AddressResolverOptions hostsRefreshPeriod property to let the resolver refresh the hosts file when its cached content is older than the last read + refresh period. --- .../dns/AddressResolverOptionsConverter.java | 6 + .../core/dns/AddressResolverOptions.java | 32 +++ .../io/vertx/core/impl/AddressResolver.java | 1 + .../java/io/vertx/core/impl/VertxImpl.java | 2 +- .../impl/resolver/DnsResolverProvider.java | 183 ++++++++++-------- .../core/spi/resolver/ResolverProvider.java | 3 +- .../core/dns/HostnameResolutionTest.java | 41 ++++ 7 files changed, 188 insertions(+), 80 deletions(-) diff --git a/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java b/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java index c4cee6a673d..c30585cdbac 100644 --- a/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java +++ b/src/main/generated/io/vertx/core/dns/AddressResolverOptionsConverter.java @@ -40,6 +40,11 @@ static void fromJson(Iterable> json, Address obj.setHostsPath((String)member.getValue()); } break; + case "hostsRefreshPeriod": + if (member.getValue() instanceof Number) { + obj.setHostsRefreshPeriod(((Number)member.getValue()).intValue()); + } + break; case "hostsValue": if (member.getValue() instanceof String) { obj.setHostsValue(io.vertx.core.buffer.Buffer.buffer(BASE64_DECODER.decode((String)member.getValue()))); @@ -115,6 +120,7 @@ static void toJson(AddressResolverOptions obj, java.util.Map jso if (obj.getHostsPath() != null) { json.put("hostsPath", obj.getHostsPath()); } + json.put("hostsRefreshPeriod", obj.getHostsRefreshPeriod()); if (obj.getHostsValue() != null) { json.put("hostsValue", BASE64_ENCODER.encodeToString(obj.getHostsValue().getBytes())); } diff --git a/src/main/java/io/vertx/core/dns/AddressResolverOptions.java b/src/main/java/io/vertx/core/dns/AddressResolverOptions.java index edce29b5b7e..5720629ccfe 100644 --- a/src/main/java/io/vertx/core/dns/AddressResolverOptions.java +++ b/src/main/java/io/vertx/core/dns/AddressResolverOptions.java @@ -58,6 +58,11 @@ public class AddressResolverOptions { */ public static final int DEFAULT_QUERY_TIMEOUT = 5000; + /** + * The default value for the hosts refresh value in millis = 0 (disabled) + */ + public static final int DEFAULT_HOSTS_REFRESH_PERIOD = 0; + /** * The default value for the max dns queries per query = 4 */ @@ -90,6 +95,7 @@ public class AddressResolverOptions { private String hostsPath; private Buffer hostsValue; + private int hostsRefreshPeriod; private List servers; private boolean optResourceEnabled; private int cacheMinTimeToLive; @@ -116,11 +122,13 @@ public AddressResolverOptions() { ndots = DEFAULT_NDOTS; rotateServers = DEFAULT_ROTATE_SERVERS; roundRobinInetAddress = DEFAULT_ROUND_ROBIN_INET_ADDRESS; + hostsRefreshPeriod = DEFAULT_HOSTS_REFRESH_PERIOD; } public AddressResolverOptions(AddressResolverOptions other) { this.hostsPath = other.hostsPath; this.hostsValue = other.hostsValue != null ? other.hostsValue.copy() : null; + this.hostsRefreshPeriod = other.hostsRefreshPeriod; this.servers = other.servers != null ? new ArrayList<>(other.servers) : null; this.optResourceEnabled = other.optResourceEnabled; this.cacheMinTimeToLive = other.cacheMinTimeToLive; @@ -182,6 +190,30 @@ public AddressResolverOptions setHostsValue(Buffer hostsValue) { return this; } + /** + * @return the hosts configuration refresh period in millis + */ + public int getHostsRefreshPeriod() { + return hostsRefreshPeriod; + } + + /** + * Set the hosts configuration refresh period in millis, {@code 0} disables it. + *

+ * The resolver caches the hosts configuration {@link #hostsPath file} after it has read it. When + * the content of this file can change, setting a positive refresh period will load the configuration + * file again when necessary. + * + * @param hostsRefreshPeriod the hosts configuration refresh period + */ + public AddressResolverOptions setHostsRefreshPeriod(int hostsRefreshPeriod) { + if (hostsRefreshPeriod < 0) { + throw new IllegalArgumentException("hostsRefreshPeriod must be >= 0"); + } + this.hostsRefreshPeriod = hostsRefreshPeriod; + return this; + } + /** * @return the list of dns server */ diff --git a/src/main/java/io/vertx/core/impl/AddressResolver.java b/src/main/java/io/vertx/core/impl/AddressResolver.java index 99889cafe96..3efc883801a 100644 --- a/src/main/java/io/vertx/core/impl/AddressResolver.java +++ b/src/main/java/io/vertx/core/impl/AddressResolver.java @@ -21,6 +21,7 @@ import io.vertx.core.impl.launcher.commands.ExecUtils; import io.vertx.core.impl.logging.Logger; import io.vertx.core.impl.logging.LoggerFactory; +import io.vertx.core.impl.resolver.DnsResolverProvider; import io.vertx.core.spi.resolver.ResolverProvider; import java.io.File; diff --git a/src/main/java/io/vertx/core/impl/VertxImpl.java b/src/main/java/io/vertx/core/impl/VertxImpl.java index b741a01bed7..0221ec5f31a 100644 --- a/src/main/java/io/vertx/core/impl/VertxImpl.java +++ b/src/main/java/io/vertx/core/impl/VertxImpl.java @@ -529,7 +529,7 @@ public DnsClient createDnsClient(DnsClientOptions options) { String host = options.getHost(); int port = options.getPort(); if (host == null || port < 0) { - DnsResolverProvider provider = new DnsResolverProvider(this, addressResolverOptions); + DnsResolverProvider provider = DnsResolverProvider.create(this, addressResolverOptions); InetSocketAddress address = provider.nameServerAddresses().get(0); // provide the host and port options = new DnsClientOptions(options) diff --git a/src/main/java/io/vertx/core/impl/resolver/DnsResolverProvider.java b/src/main/java/io/vertx/core/impl/resolver/DnsResolverProvider.java index 37f5f2f4eab..5f2e092f05e 100644 --- a/src/main/java/io/vertx/core/impl/resolver/DnsResolverProvider.java +++ b/src/main/java/io/vertx/core/impl/resolver/DnsResolverProvider.java @@ -11,21 +11,17 @@ package io.vertx.core.impl.resolver; -import io.netty.channel.ChannelFactory; import io.netty.channel.EventLoop; -import io.netty.channel.socket.DatagramChannel; import io.netty.channel.socket.SocketChannel; import io.netty.resolver.*; import io.netty.resolver.dns.*; import io.netty.util.NetUtil; -import io.netty.util.concurrent.EventExecutor; -import io.vertx.core.Context; -import io.vertx.core.Handler; -import io.vertx.core.Vertx; -import io.vertx.core.VertxException; +import io.vertx.core.*; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.impl.VertxInternal; import io.vertx.core.dns.AddressResolverOptions; import io.vertx.core.impl.AddressResolver; -import io.vertx.core.impl.VertxImpl; +import io.vertx.core.impl.ContextInternal; import io.vertx.core.spi.resolver.ResolverProvider; import java.io.File; @@ -34,27 +30,32 @@ import java.net.*; import java.util.*; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicLong; import static io.netty.util.internal.ObjectUtil.intValue; /** * @author Julien Viet */ -public class DnsResolverProvider implements ResolverProvider { +public class DnsResolverProvider implements ResolverProvider, HostsFileEntriesResolver { - private final Vertx vertx; + public static DnsResolverProvider create(VertxInternal vertx, AddressResolverOptions options) { + DnsResolverProvider provider = new DnsResolverProvider(vertx, options); + provider.refresh(); + return provider; + } + + private final VertxInternal vertx; private final List resolvers = Collections.synchronizedList(new ArrayList<>()); private AddressResolverGroup resolverGroup; private final List serverList = new ArrayList<>(); + private final String hostsPath; + private final Buffer hostsValue; + private final AtomicLong refreshTimestamp = new AtomicLong(); + private final long hostsRefreshPeriodNanos; + private volatile HostsFileEntries parsedHostsFile = new HostsFileEntries(Collections.emptyMap(), Collections.emptyMap()); - /** - * @return a list of DNS servers available to use - */ - public List nameServerAddresses() { - return serverList; - } - - public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) { + private DnsResolverProvider(VertxInternal vertx, AddressResolverOptions options) { List dnsServers = options.getServers(); if (dnsServers != null && dnsServers.size() > 0) { for (String dnsServer : dnsServers) { @@ -87,30 +88,8 @@ public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) { } } DnsServerAddresses nameServerAddresses = options.isRotateServers() ? DnsServerAddresses.rotational(serverList) : DnsServerAddresses.sequential(serverList); - DnsServerAddressStreamProvider nameServerAddressProvider = hostname -> { - return nameServerAddresses.stream(); - }; + DnsServerAddressStreamProvider nameServerAddressProvider = hostname -> nameServerAddresses.stream(); - HostsFileEntries entries; - if (options.getHostsPath() != null) { - File file = vertx.resolveFile(options.getHostsPath()).getAbsoluteFile(); - try { - if (!file.exists() || !file.isFile()) { - throw new IOException(); - } - entries = HostsFileParser.parse(file); - } catch (IOException e) { - throw new VertxException("Cannot read hosts file " + file.getAbsolutePath()); - } - } else if (options.getHostsValue() != null) { - try { - entries = HostsFileParser.parse(new StringReader(options.getHostsValue().toString())); - } catch (IOException e) { - throw new VertxException("Cannot read hosts config ", e); - } - } else { - entries = HostsFileParser.parseSilently(); - } int minTtl = intValue(options.getCacheMinTimeToLive(), 0); int maxTtl = intValue(options.getCacheMaxTimeToLive(), Integer.MAX_VALUE); @@ -119,38 +98,12 @@ public DnsResolverProvider(VertxImpl vertx, AddressResolverOptions options) { DnsCache authoritativeDnsServerCache = new DefaultDnsCache(minTtl, maxTtl, negativeTtl); this.vertx = vertx; - + this.hostsPath = options.getHostsPath(); + this.hostsValue = options.getHostsValue(); + this.hostsRefreshPeriodNanos = options.getHostsRefreshPeriod(); DnsNameResolverBuilder builder = new DnsNameResolverBuilder(); - builder.hostsFileEntriesResolver(new HostsFileEntriesResolver() { - @Override - public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { - if (inetHost.endsWith(".")) { - inetHost = inetHost.substring(0, inetHost.length() - 1); - } - InetAddress address = lookup(inetHost, resolvedAddressTypes); - if (address == null) { - address = lookup(inetHost.toLowerCase(Locale.ENGLISH), resolvedAddressTypes); - } - return address; - } - InetAddress lookup(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { - switch (resolvedAddressTypes) { - case IPV4_ONLY: - return entries.inet4Entries().get(inetHost); - case IPV6_ONLY: - return entries.inet6Entries().get(inetHost); - case IPV4_PREFERRED: - Inet4Address inet4Address = entries.inet4Entries().get(inetHost); - return inet4Address != null? inet4Address : entries.inet6Entries().get(inetHost); - case IPV6_PREFERRED: - Inet6Address inet6Address = entries.inet6Entries().get(inetHost); - return inet6Address != null? inet6Address : entries.inet4Entries().get(inetHost); - default: - throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes); - } - } - }); + builder.hostsFileEntriesResolver(this); builder.channelFactory(() -> vertx.transport().datagramChannel()); builder.socketChannelFactory(() -> (SocketChannel) vertx.transport().channelFactory(false).newChannel()); builder.nameServerProvider(nameServerAddressProvider); @@ -184,15 +137,44 @@ protected io.netty.resolver.AddressResolver newAddressResolve }; } - private static class ResolverRegistration { - private final io.netty.resolver.AddressResolver resolver; - private final EventLoop executor; - ResolverRegistration(io.netty.resolver.AddressResolver resolver, EventLoop executor) { - this.resolver = resolver; - this.executor = executor; + @Override + public InetAddress address(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { + if (inetHost.endsWith(".")) { + inetHost = inetHost.substring(0, inetHost.length() - 1); + } + if (hostsRefreshPeriodNanos > 0) { + ensureHostsFileFresh(hostsRefreshPeriodNanos); + } + InetAddress address = lookup(inetHost, resolvedAddressTypes); + if (address == null) { + address = lookup(inetHost.toLowerCase(Locale.ENGLISH), resolvedAddressTypes); + } + return address; + } + InetAddress lookup(String inetHost, ResolvedAddressTypes resolvedAddressTypes) { + switch (resolvedAddressTypes) { + case IPV4_ONLY: + return parsedHostsFile.inet4Entries().get(inetHost); + case IPV6_ONLY: + return parsedHostsFile.inet6Entries().get(inetHost); + case IPV4_PREFERRED: + Inet4Address inet4Address = parsedHostsFile.inet4Entries().get(inetHost); + return inet4Address != null? inet4Address : parsedHostsFile.inet6Entries().get(inetHost); + case IPV6_PREFERRED: + Inet6Address inet6Address = parsedHostsFile.inet6Entries().get(inetHost); + return inet6Address != null? inet6Address : parsedHostsFile.inet4Entries().get(inetHost); + default: + throw new IllegalArgumentException("Unknown ResolvedAddressTypes " + resolvedAddressTypes); } } + /** + * @return a list of DNS servers available to use + */ + public List nameServerAddresses() { + return serverList; + } + @Override public AddressResolverGroup resolver(AddressResolverOptions options) { return resolverGroup; @@ -200,7 +182,7 @@ public AddressResolverGroup resolver(AddressResolverOptions o @Override public void close(Handler doneHandler) { - Context context = vertx.getOrCreateContext(); + ContextInternal context = vertx.getOrCreateContext(); ResolverRegistration[] registrations = this.resolvers.toArray(new ResolverRegistration[0]); if (registrations.length == 0) { context.runOnContext(doneHandler); @@ -221,4 +203,49 @@ public void close(Handler doneHandler) { } } } + + public void refresh() { + ensureHostsFileFresh(0); + } + + private void ensureHostsFileFresh(long refreshPeriodNanos) { + long prev = refreshTimestamp.get(); + long now = System.nanoTime(); + if ((now - prev) >= refreshPeriodNanos && refreshTimestamp.compareAndSet(prev, now)) { + refreshHostsFile(); + } + } + + private void refreshHostsFile() { + HostsFileEntries entries; + if (hostsPath != null) { + File file = vertx.resolveFile(hostsPath).getAbsoluteFile(); + try { + if (!file.exists() || !file.isFile()) { + throw new IOException(); + } + entries = HostsFileParser.parse(file); + } catch (IOException e) { + throw new VertxException("Cannot read hosts file " + file.getAbsolutePath()); + } + } else if (hostsValue != null) { + try { + entries = HostsFileParser.parse(new StringReader(hostsValue.toString())); + } catch (IOException e) { + throw new VertxException("Cannot read hosts config ", e); + } + } else { + entries = HostsFileParser.parseSilently(); + } + parsedHostsFile = entries; + } + + private static class ResolverRegistration { + private final io.netty.resolver.AddressResolver resolver; + private final EventLoop executor; + ResolverRegistration(io.netty.resolver.AddressResolver resolver, EventLoop executor) { + this.resolver = resolver; + this.executor = executor; + } + } } diff --git a/src/main/java/io/vertx/core/spi/resolver/ResolverProvider.java b/src/main/java/io/vertx/core/spi/resolver/ResolverProvider.java index bf9e2910eab..7c570c4d298 100644 --- a/src/main/java/io/vertx/core/spi/resolver/ResolverProvider.java +++ b/src/main/java/io/vertx/core/spi/resolver/ResolverProvider.java @@ -17,6 +17,7 @@ import io.vertx.core.VertxException; import io.vertx.core.dns.AddressResolverOptions; import io.vertx.core.impl.VertxImpl; +import io.vertx.core.impl.VertxInternal; import io.vertx.core.impl.resolver.DnsResolverProvider; import io.vertx.core.impl.resolver.DefaultResolverProvider; import io.vertx.core.impl.logging.Logger; @@ -36,7 +37,7 @@ static ResolverProvider factory(Vertx vertx, AddressResolverOptions options) { // that use an unstable API and fallback on the default (blocking) provider try { if (!Boolean.getBoolean(DISABLE_DNS_RESOLVER_PROP_NAME)) { - return new DnsResolverProvider((VertxImpl) vertx, options); + return DnsResolverProvider.create((VertxInternal) vertx, options); } } catch (Throwable e) { if (e instanceof VertxException) { diff --git a/src/test/java/io/vertx/core/dns/HostnameResolutionTest.java b/src/test/java/io/vertx/core/dns/HostnameResolutionTest.java index a5ed8f9fec3..abb15b4d234 100644 --- a/src/test/java/io/vertx/core/dns/HostnameResolutionTest.java +++ b/src/test/java/io/vertx/core/dns/HostnameResolutionTest.java @@ -15,6 +15,7 @@ import io.netty.channel.Channel; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelInitializer; +import io.vertx.core.Future; import io.vertx.core.VertxException; import io.vertx.core.VertxOptions; import io.vertx.core.buffer.Buffer; @@ -22,6 +23,7 @@ import io.vertx.core.http.HttpMethod; import io.vertx.core.http.HttpServer; import io.vertx.core.impl.AddressResolver; +import io.vertx.core.impl.Utils; import io.vertx.core.impl.VertxImpl; import io.vertx.core.impl.VertxInternal; import io.vertx.core.json.JsonObject; @@ -32,12 +34,14 @@ import io.vertx.test.core.VertxTestBase; import io.vertx.test.fakedns.FakeDNSServer; import org.apache.directory.server.dns.messages.ResourceRecord; +import org.junit.Assume; import org.junit.Test; import java.io.File; import java.net.InetAddress; import java.net.InetSocketAddress; import java.net.UnknownHostException; +import java.nio.file.Files; import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CountDownLatch; @@ -415,6 +419,43 @@ public void testTrailingDotResolveFromHosts() { await(); } + @Test + public void testRefreshHosts1() throws Exception { + Assume.assumeFalse(Utils.isWindows()); + InetAddress addr = testRefreshHosts((int) TimeUnit.SECONDS.toNanos(1)); + assertEquals("192.168.0.16", addr.getHostAddress()); + assertEquals("server.net", addr.getHostName()); + } + + @Test + public void testRefreshHosts2() throws Exception { + InetAddress addr = testRefreshHosts(0); + assertEquals("192.168.0.15", addr.getHostAddress()); + assertEquals("server.net", addr.getHostName()); + } + + private InetAddress testRefreshHosts(int period) throws Exception { + File hosts = File.createTempFile("vertx", "hosts"); + hosts.deleteOnExit(); + Files.write(hosts.toPath(), "192.168.0.15 server.net".getBytes()); + AddressResolverOptions options = new AddressResolverOptions() + .setHostsPath(hosts.getAbsolutePath()) + .setHostsRefreshPeriod(period); + VertxInternal vertx = (VertxInternal) vertx(new VertxOptions().setAddressResolverOptions(options)); + InetAddress addr = Future.future(p -> vertx.resolveAddress("server.net", p)) + .toCompletionStage() + .toCompletableFuture() + .get(20, TimeUnit.SECONDS); + assertEquals("192.168.0.15", addr.getHostAddress()); + assertEquals("server.net", addr.getHostName()); + Files.write(hosts.toPath(), "192.168.0.16 server.net".getBytes()); + Thread.sleep(1000); + return Future.future(p -> vertx.resolveAddress("server.net", p)) + .toCompletionStage() + .toCompletableFuture() + .get(20, TimeUnit.SECONDS); + } + @Test public void testResolveMissingLocalhost() throws Exception {