DBZ-8059 Updated FakeDNS to JDK 18 InetAddressResolver SPI

This commit is contained in:
Jakub Cechacek 2024-07-12 16:35:21 +02:00 committed by Ondrej Babec
parent b58c445ec7
commit a21ade7ed3
8 changed files with 181 additions and 211 deletions

View File

@ -10,7 +10,7 @@
import io.debezium.connector.mongodb.TestHelper;
import io.debezium.testing.testcontainers.MongoDbDeployment;
import io.debezium.testing.testcontainers.util.DockerUtils;
import io.debezium.testing.testcontainers.util.FakeDns;
import io.debezium.testing.testcontainers.util.dns.FakeDns;
/**
* DocumentDb representation of {@link MongoDbDeployment}
@ -27,7 +27,7 @@ public DocumentDb(String connectionString) {
@Override
public void start() {
// DocumentDB requires SSH tunneling and custom DNS resolution
var dns = FakeDns.getInstance().install();
var dns = FakeDns.getInstance().enable();
getHosts().forEach(DockerUtils::addFakeDnsEntry);
// Enable change streams

View File

@ -0,0 +1 @@
io.debezium.testing.testcontainers.util.dns.FakeDnsAddressResolverProvider

View File

@ -11,6 +11,8 @@
import org.slf4j.Logger;
import io.debezium.testing.testcontainers.util.dns.FakeDns;
public class DockerUtils {
public static final String CONTAINER_VM_LOG_SKIP = "container.vm.log.skip";
@ -50,7 +52,7 @@ public static void logContainerVMBanner(Logger logger, Collection<String> hosts,
var prop = System.getProperty(CONTAINER_VM_LOG_SKIP, "false");
var propertySkip = Boolean.parseBoolean(prop);
if (propertySkip || skip || !isContainerVM() || FakeDns.getInstance().isInstalled()) {
if (propertySkip || skip || !isContainerVM() || FakeDns.getInstance().isEnabled()) {
return;
}
@ -78,7 +80,7 @@ public static void logContainerVMBanner(Logger logger, Collection<String> hosts,
*/
public static void enableFakeDnsIfRequired() {
if (isContainerVM()) {
FakeDns.getInstance().install();
FakeDns.getInstance().enable();
}
}
@ -86,7 +88,7 @@ public static void enableFakeDnsIfRequired() {
* Disables {@link FakeDns} if enabled
*/
public static void disableFakeDns() {
FakeDns.getInstance().restore();
FakeDns.getInstance().disable();
}
/**
@ -105,7 +107,7 @@ public static void addFakeDnsEntry(String hostname) {
* @param address resolution address
*/
public static void addFakeDnsEntry(String hostname, InetAddress address) {
if (FakeDns.getInstance().isInstalled()) {
if (FakeDns.getInstance().isEnabled()) {
FakeDns.getInstance().addResolution(hostname, address);
}
}
@ -126,7 +128,7 @@ public static void removeFakeDnsEntry(String hostname) {
* @param address resolution address
*/
public static void removeFakeDnsEntry(String hostname, InetAddress address) {
if (FakeDns.getInstance().isInstalled()) {
if (FakeDns.getInstance().isEnabled()) {
FakeDns.getInstance().removeResolution(hostname, address);
}
}

View File

@ -1,204 +0,0 @@
/*
* Copyright Debezium Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.testing.testcontainers.util;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashMap;
import java.util.Map;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Fake DNS resolver which allows tests to work well with testcontainer under Docker Desktop
*
* Adaptation of https://github.com/CorfuDB/CorfuDB/blob/master/it/src/main/java/org/corfudb/universe/universe/docker/FakeDns.java
*/
public class FakeDns {
private static final Logger LOGGER = LoggerFactory.getLogger(FakeDns.class);
private static final FakeDns instance = new FakeDns();
private final Map<String, InetAddress> forwardResolutions = new HashMap<>();
private Object originalNameService = null;
/**
* whether the fake resolver has been installed
*/
private boolean installed = false;
private FakeDns() {
}
public static FakeDns getInstance() {
return instance;
}
public void addResolution(String hostname, InetAddress ip) {
synchronized (this) {
forwardResolutions.put(hostname, ip);
}
LOGGER.info("Added dns resolution: {} -> {}", hostname, ip);
}
public void removeResolution(String hostname, InetAddress ip) {
synchronized (this) {
forwardResolutions.remove(hostname, ip);
}
LOGGER.info("Removed dns resolution: {} -> {}", hostname, ip);
}
public synchronized boolean isInstalled() {
return installed;
}
/**
* Install the fake DNS resolver into the Java runtime.
*/
public FakeDns install() {
synchronized (this) {
if (installed) {
return this;
}
try {
originalNameService = installDns();
}
catch (Exception e) {
throw new IllegalStateException(e);
}
installed = true;
}
LOGGER.info("FakeDns is now ENABLED in JVM Runtime");
return this;
}
/**
* Restore default DNS resolver in Java runtime.
**/
public FakeDns restore() {
synchronized (this) {
if (!installed) {
return this;
}
try {
restoreDns(originalNameService);
}
catch (Exception e) {
throw new IllegalStateException(e);
}
installed = false;
}
LOGGER.info("FakeDns is now DISABLED in JVM Runtime");
return this;
}
private Object installDns()
throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, ClassNotFoundException, NoSuchFieldException {
// Override the NameService in Java 9 or later.
final Class<?> nameServiceInterface = Class.forName("java.net.InetAddress$NameService");
Field field = InetAddress.class.getDeclaredField("nameService");
// Get the default NameService to fall back to.
Method method = InetAddress.class.getDeclaredMethod("createNameService");
method.setAccessible(true);
Object fallbackNameService = method.invoke(null);
// Install a wrapping NameService proxy instance
Object installedNameService = Proxy.newProxyInstance(
nameServiceInterface.getClassLoader(),
new Class<?>[]{ nameServiceInterface },
new NameServiceListener(fallbackNameService));
// Set the NameService on the InetAddress field
field.setAccessible(true);
field.set(InetAddress.class, installedNameService);
return fallbackNameService;
}
private void restoreDns(Object nameService)
throws IllegalAccessException, NoSuchFieldException {
Field field = InetAddress.class.getDeclaredField("nameService");
field.setAccessible(true);
field.set(InetAddress.class, nameService);
}
public static <T extends Throwable> void propagateIfPossible(Throwable throwable, Class<T> declaredType) throws T {
if (declaredType.isInstance(throwable)) {
throw declaredType.cast(throwable);
}
}
/**
* NameServiceListener with a NameService implementation to fallback to.
*/
private class NameServiceListener implements InvocationHandler {
private final Object fallbackNameService;
/**
* Creates {@link NameServiceListener} with fallback NameService
* The parameter is untyped as {@code java.net.InetAddress$NameService} is private
*
* @param fallbackNameService fallback NameService
*/
NameServiceListener(Object fallbackNameService) {
this.fallbackNameService = fallbackNameService;
}
private InetAddress[] lookupAllHostAddr(String host) throws UnknownHostException {
InetAddress inetAddress;
synchronized (FakeDns.this) {
inetAddress = forwardResolutions.get(host);
}
if (inetAddress != null) {
return new InetAddress[]{ inetAddress };
}
return invokeFallBackMethod("lookupAllHostAddr", InetAddress[].class, host);
}
private String getHostByAddr(byte[] addr) throws UnknownHostException {
return invokeFallBackMethod("getHostsByAddr", String.class, addr);
}
private <T, P> T invokeFallBackMethod(String methodName, Class<T> returnType, P param) throws UnknownHostException {
try {
Method method = fallbackNameService
.getClass()
.getDeclaredMethod(methodName, param.getClass());
method.setAccessible(true);
var result = method.invoke(fallbackNameService, param);
return returnType.cast(result);
}
catch (ReflectiveOperationException | SecurityException e) {
propagateIfPossible(e.getCause(), UnknownHostException.class);
throw new AssertionError("unexpected reflection issue", e);
}
}
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
switch (method.getName()) {
case "lookupAllHostAddr":
return lookupAllHostAddr((String) args[0]);
case "getHostByAddr":
return getHostByAddr((byte[]) args[0]);
default:
throw new UnsupportedOperationException();
}
}
}
}

View File

@ -0,0 +1,94 @@
/*
* Copyright Debezium Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.testing.testcontainers.util.dns;
import java.net.InetAddress;
import java.util.Arrays;
import java.util.Map;
import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicBoolean;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Fake DNS resolver which allows tests to work well with testcontainer under Docker Desktop
*
* Adaptation of https://github.com/CorfuDB/CorfuDB/blob/master/it/src/main/java/org/corfudb/universe/universe/docker/FakeDns.java
*/
public class FakeDns {
public static final Logger LOGGER = LoggerFactory.getLogger(FakeDns.class);
private static final FakeDns instance = new FakeDns();
private final Map<String, InetAddress> registry;
private final AtomicBoolean enabled;
private FakeDns() {
this.enabled = new AtomicBoolean(false);
this.registry = new ConcurrentHashMap<>();
}
public static FakeDns getInstance() {
return instance;
}
public void addResolution(String hostname, InetAddress ip) {
registry.put(hostname, ip);
LOGGER.info("Added dns resolution: {} -> {}", hostname, ip);
}
public void removeResolution(String hostname, InetAddress ip) {
registry.remove(hostname, ip);
LOGGER.info("Removed dns resolution: {} -> {}", hostname, ip);
}
public Optional<InetAddress> resolve(String hostname) {
return isEnabled()
? resolveInternal(hostname)
: Optional.empty();
}
public Optional<String> resolve(byte[] address) {
return isEnabled()
? resolveInternal(address)
: Optional.empty();
}
private Optional<InetAddress> resolveInternal(String hostname) {
LOGGER.info("Performing Forward Lookup for HOST : {}", hostname);
return Optional.of(registry.get(hostname));
}
private Optional<String> resolveInternal(byte[] address) {
LOGGER.info("Performing Reverse Lookup for Address : {}", Arrays.toString(address));
return registry.keySet()
.stream()
.filter(host -> Arrays.equals(registry.get(host).getAddress(), address))
.findFirst();
}
public boolean isEnabled() {
return enabled.get();
}
/**
* Enable the fake DNS registry
*/
public FakeDns enable() {
this.enabled.set(true);
return this;
}
/**
* Disable the fake DNS registry
**/
public FakeDns disable() {
this.enabled.set(false);
return this;
}
}

View File

@ -0,0 +1,69 @@
/*
* Copyright Debezium Authors.
*
* Licensed under the Apache Software License version 2.0, available at http://www.apache.org/licenses/LICENSE-2.0
*/
package io.debezium.testing.testcontainers.util.dns;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.net.spi.InetAddressResolver;
import java.net.spi.InetAddressResolverProvider;
import java.util.stream.Stream;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Custom {@link InetAddressResolverProvider} that uses {@link FakeDnsAddressResolver} to resolve addresses.
*/
public class FakeDnsAddressResolverProvider extends InetAddressResolverProvider {
private static final Logger LOGGER = LoggerFactory.getLogger(FakeDnsAddressResolverProvider.class);
@Override
public InetAddressResolver get(Configuration configuration) {
LOGGER.info("Using Custom Address Resolver : {} ", this.name());
LOGGER.info("Registry initialised");
return new FakeDnsAddressResolver(FakeDns.getInstance(), configuration.builtinResolver());
}
@Override
public String name() {
return FakeDnsAddressResolver.class.getName();
}
/**
* When {@link FakeDns} is enabled the resolver will first perform lookups in the fake DNS registry.
* If the address is not found, the resolver will delegate to the fallback resolver.
* If {@link FakeDns} is disabled, the resolver will only delegate to the fallback resolver.
*/
public static final class FakeDnsAddressResolver implements InetAddressResolver {
private final InetAddressResolver fallbackResolver;
private final FakeDns fakeDns;
public FakeDnsAddressResolver(FakeDns fakeDns, InetAddressResolver fallbackResolver) {
this.fakeDns = fakeDns;
this.fallbackResolver = fallbackResolver;
}
@Override
public Stream<InetAddress> lookupByName(String host, LookupPolicy lookupPolicy) throws UnknownHostException {
var maybeAddress = fakeDns.resolve(host);
return maybeAddress.isPresent()
? maybeAddress.stream()
: fallbackResolver.lookupByName(host, lookupPolicy);
}
@Override
public String lookupByAddress(byte[] address) throws UnknownHostException {
var maybeHost = fakeDns.resolve(address);
return maybeHost.isPresent()
? maybeHost.get()
: fallbackResolver.lookupByAddress(address);
}
}
}

View File

@ -0,0 +1 @@
io.debezium.testing.testcontainers.util.dns.FakeDnsAddressResolverProvider

View File

@ -10,6 +10,13 @@
<artifactId>debezium-testing</artifactId>
<name>Debezium Testing</name>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>${debezium.java.source}</maven.compiler.source>
<maven.compiler.target>${debezium.java.specific.target}</maven.compiler.target>
<maven.compiler.release>${debezium.java.specific.target}</maven.compiler.release>
<maven.compiler.testRelease>${debezium.java.specific.target}</maven.compiler.testRelease>
</properties>
<modules>
<module>debezium-testing-testcontainers</module>
<module>debezium-testing-system</module>